Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .alpha-loop/learnings/issue-410-20260815-140639.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
issue: 410
status: success
test_fix_retries: 0
duration: 884
date: 2026-08-15
retries: 0
traces:
plan: .alpha-loop/sessions/session/20260815-135132/traces/prompts/plan-issue-410.md
implement: .alpha-loop/sessions/session/20260815-135132/logs/issue-410-implement.log
review: .alpha-loop/sessions/session/20260815-135132/traces/outputs/review-issue-410.log
diff: .alpha-loop/sessions/session/20260815-135132/diffs/issue-410.diff
---

## What Worked
- Centralized Conventional Commit title and type derivation in `src/lib/conventional-commits.ts`.

## What Failed
- Nothing.

## Patterns
- Preserve valid title prefixes, then infer from labels, with `chore` as the conservative fallback.

## Anti-Patterns
- Hardcoding `feat:` independently at commit and PR creation call sites.

## Suggested Skill Updates
- None.
89 changes: 89 additions & 0 deletions .alpha-loop/learnings/session-session-20260815-135132.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"name": "session/20260815-135132",
"branch": "session/20260815-135132",
"completed": "2026-08-15T21:12:00.428Z",
"results": [
{
"issueNum": 410,
"title": "fix: loop hardcodes feat: on every PR and commit — double prefix, and bug fixes bump the minor version",
"status": "success",
"prUrl": "https://github.com/bradtaylorsf/alpha-loop/pull/422",
"testsPassing": true,
"verifyPassing": true,
"duration": 884,
"filesChanged": 4
}
],
"stages": [
{
"stage": "plan",
"model": "",
"endpoint": "default",
"tokens_in": 63857,
"tokens_out": 49121,
"cost_usd": 0,
"wall_time_s": 155.34,
"tool_calls": 0,
"tool_errors": 0,
"stage_success": true,
"started_at": "2026-08-15T20:51:50.076Z",
"issue_num": 410
},
{
"stage": "implement",
"model": "",
"endpoint": "default",
"tokens_in": 511645,
"tokens_out": 393573,
"cost_usd": 0,
"wall_time_s": 513.035,
"tool_calls": 0,
"tool_errors": 0,
"stage_success": true,
"started_at": "2026-08-15T20:54:25.900Z",
"issue_num": 410
},
{
"stage": "review",
"model": "",
"endpoint": "default",
"tokens_in": 115096,
"tokens_out": 88535,
"cost_usd": 0,
"wall_time_s": 201.238,
"tool_calls": 0,
"tool_errors": 0,
"stage_success": true,
"started_at": "2026-08-15T21:03:03.390Z",
"issue_num": 410
},
{
"stage": "learn",
"model": "",
"endpoint": "default",
"tokens_in": 6001,
"tokens_out": 4616,
"cost_usd": 0,
"wall_time_s": 11.116,
"tool_calls": 0,
"tool_errors": 0,
"stage_success": true,
"started_at": "2026-08-15T21:06:39.985Z",
"issue_num": 410
},
{
"stage": "assumptions",
"model": "",
"endpoint": "default",
"tokens_in": 20514,
"tokens_out": 15780,
"cost_usd": 0,
"wall_time_s": 58.788,
"tool_calls": 0,
"tool_errors": 0,
"stage_success": true,
"started_at": "2026-08-15T21:06:40.008Z",
"issue_num": 410
}
]
}
16 changes: 16 additions & 0 deletions .alpha-loop/learnings/session-summary-session-20260815-135132.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Session Summary: session/20260815-135132

## Overview
- Issue #410 completed successfully in 15 minutes with no retries or test fixes. The implementation centralized Conventional Commit title and type derivation, removing duplicated behavior from commit and PR creation paths.

## Recurring Patterns
- No cross-issue recurring patterns could be established from a single issue.

## Recurring Anti-Patterns
- No recurring failures occurred.

## Recommendations
- Route all commit and PR title generation through `src/lib/conventional-commits.ts`.

## Metrics
| Metric | Value |
27 changes: 18 additions & 9 deletions src/commands/resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
type FeedbackClassification,
} from '../lib/session-state.js';
import { emitLifecycleEvent } from '../lib/events.js';
import { conventionalTitle } from '../lib/conventional-commits.js';

export type ResumeOptions = {
issue?: string;
Expand Down Expand Up @@ -309,18 +310,24 @@ function prExists(repo: string, branch: string): boolean {
}

/**
* Fetch the issue title from GitHub.
* Fetch the issue title and labels from GitHub for release-safe PR naming.
*/
function getIssueTitle(repo: string, issueNum: number): string {
function getIssueMetadata(repo: string, issueNum: number): { title: string; labels: string[] } {
const result = ghExec(
`gh issue view ${issueNum} --repo ${shellQuote(repo)} --json title`,
`gh issue view ${issueNum} --repo ${shellQuote(repo)} --json title,labels`,
);
if (result.exitCode !== 0) return `Issue #${issueNum}`;
if (result.exitCode !== 0) return { title: `Issue #${issueNum}`, labels: [] };
try {
const data = JSON.parse(result.stdout) as { title: string };
return data.title;
const data = JSON.parse(result.stdout) as {
title: string;
labels?: Array<string | { name?: string }>;
};
const labels = (data.labels ?? [])
.map((label) => typeof label === 'string' ? label : label.name)
.filter((label): label is string => typeof label === 'string');
return { title: data.title, labels };
} catch {
return `Issue #${issueNum}`;
return { title: `Issue #${issueNum}`, labels: [] };
}
}

Expand Down Expand Up @@ -372,7 +379,8 @@ async function resumeBranch(
const repo = config.repo;
const baseBranch = config.baseBranch;

const title = getIssueTitle(repo, issueNum);
const issue = getIssueMetadata(repo, issueNum);
const { title } = issue;
log.step(`Resuming issue #${issueNum}: ${title}`);

const branchWorktree = checkoutBranchForResume(branch);
Expand Down Expand Up @@ -460,7 +468,7 @@ This PR was recovered by \`alpha-loop resume\`. Resume creates the PR and runs b
repo,
base: baseBranch,
head: branch,
title: `feat: ${title} (closes #${issueNum})`,
title: conventionalTitle(issue, `(closes #${issueNum})`),
body: prBody,
cwd: branchWorktree,
});
Expand Down Expand Up @@ -1051,6 +1059,7 @@ export async function resumePausedIssueFromManifest(
config,
session,
{
issueLabels: issue.labels,
resumeContext: context.contextText,
resumeStage: context.resumeStage,
existingPrUrl: context.existingPrUrl,
Expand Down
5 changes: 3 additions & 2 deletions src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1569,7 +1569,7 @@ async function executeSessionRun(
issuesToProcess = [];
}
}
const quickProcessed: Array<{ number: number; title: string }> = [];
const quickProcessed: Array<{ number: number; title: string; labels?: string[] }> = [];

for (const issue of issuesToProcess) {
// Check duration limit before each issue
Expand Down Expand Up @@ -1602,14 +1602,15 @@ async function executeSessionRun(
session,
{
signal: runAbortController.signal,
issueLabels: issue.labels,
...(epicPromptContext ? { epicContext: epicPromptContext } : {}),
...(quickWorktree ? { quickWorktree } : {}),
},
);
if (runAbortController.signal.aborted) break;
session.results.push(result);
if (quickWorktree && result.status === 'success' && !isRecoveredRunResult(result)) {
quickProcessed.push({ number: issue.number, title: issue.title });
quickProcessed.push({ number: issue.number, title: issue.title, labels: issue.labels });
}

// Flip the epic checklist box when this sub-issue succeeded.
Expand Down
74 changes: 74 additions & 0 deletions src/lib/conventional-commits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
export const CONVENTIONAL_COMMIT_TYPES = [
'fix',
'feat',
'chore',
'docs',
'refactor',
'test',
'perf',
'build',
'ci',
] as const;

export type ConventionalCommitType = typeof CONVENTIONAL_COMMIT_TYPES[number];

export type ConventionalIssue = {
title: string;
labels?: readonly string[];
};

const PREFIX_PATTERN = CONVENTIONAL_COMMIT_TYPES.join('|');
const CONVENTIONAL_PREFIX = new RegExp(`^(${PREFIX_PATTERN}):\\s*`);
const DOUBLE_PREFIX = new RegExp(`^(${PREFIX_PATTERN}):\\s*(${PREFIX_PATTERN}):`);

const LABEL_TYPES: ReadonlyArray<readonly [string, ConventionalCommitType]> = [
['bug', 'fix'],
['enhancement', 'feat'],
['feature', 'feat'],
['documentation', 'docs'],
];

/** Reject titles that would make release classification depend on the outer prefix. */
export function assertNoDoubleConventionalPrefix(title: string): void {
if (DOUBLE_PREFIX.test(title.trim())) {
throw new Error(`Generated title contains a double Conventional Commit prefix: ${title}`);
}
}

/** Resolve an issue's Conventional Commit type without assuming every change is a feature. */
export function conventionalType(issue: ConventionalIssue): ConventionalCommitType {
const titleType = issue.title.trim().match(CONVENTIONAL_PREFIX)?.[1] as ConventionalCommitType | undefined;
if (titleType) return titleType;

const labels = new Set((issue.labels ?? []).map((label) => label.trim().toLowerCase()));
return LABEL_TYPES.find(([label]) => labels.has(label))?.[1] ?? 'chore';
}

/** Preserve an existing supported prefix or add the type derived from issue labels. */
export function conventionalTitle(issue: ConventionalIssue, suffix?: string): string {
const issueTitle = issue.title.trim();
const title = CONVENTIONAL_PREFIX.test(issueTitle)
? issueTitle
: `${conventionalType(issue)}: ${issueTitle}`;
const withSuffix = suffix?.trim() ? `${title} ${suffix.trim()}` : title;
assertNoDoubleConventionalPrefix(withSuffix);
return withSuffix;
}

/** Aggregate titles use release-safe precedence: feat, then fix, then chore. */
export function conventionalBatchType(issues: readonly ConventionalIssue[]): 'feat' | 'fix' | 'chore' {
const types = issues.map(conventionalType);
if (types.includes('feat')) return 'feat';
if (types.includes('fix')) return 'fix';
return 'chore';
}

/** Build a guarded aggregate title for batch and quick-session artifacts. */
export function conventionalBatchTitle(
issues: readonly ConventionalIssue[],
description: string,
): string {
const title = `${conventionalBatchType(issues)}: ${description.trim()}`;
assertNoDoubleConventionalPrefix(title);
return title;
}
Loading