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-408-20260815-143441.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
issue: 408
status: success
test_fix_retries: 0
duration: 1311
date: 2026-08-15
retries: 0
traces:
plan: .alpha-loop/sessions/session/20260815-141225/traces/prompts/plan-issue-408.md
implement: .alpha-loop/sessions/session/20260815-141225/logs/issue-408-implement.log
review: .alpha-loop/sessions/session/20260815-141225/traces/outputs/review-issue-408.log
diff: .alpha-loop/sessions/session/20260815-141225/diffs/issue-408.diff
---

## What Worked
- Stale context regeneration moved into the isolated session worktree and is committed to the session branch before publication.

## What Failed
- Review found that fresh runs with `auto_merge` disabled no longer synchronized agent assets; the regression was fixed before completion.

## Patterns
- Generated project files should be prepared, validated, and committed inside the session worktree rather than the base checkout.

## Anti-Patterns
- Do not treat valid generated diffs as an unconditional failure after regeneration succeeds.

## Suggested Skill Updates
- Update implementation-planning guidance to require an execution-mode matrix covering dry-run, auto-merge, non-auto-merge, and parallel-worker paths.
89 changes: 89 additions & 0 deletions .alpha-loop/learnings/session-session-20260815-141225.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"name": "session/20260815-141225",
"branch": "session/20260815-141225",
"completed": "2026-08-15T21:41:14.520Z",
"results": [
{
"issueNum": 408,
"title": "fix: first run after context goes stale always aborts — refreshProjectContext has no success path",
"status": "success",
"prUrl": "https://github.com/bradtaylorsf/alpha-loop/pull/428",
"testsPassing": true,
"verifyPassing": true,
"duration": 1311,
"filesChanged": 3
}
],
"stages": [
{
"stage": "plan",
"model": "",
"endpoint": "default",
"tokens_in": 78192,
"tokens_out": 60148,
"cost_usd": 0,
"wall_time_s": 247.448,
"tool_calls": 0,
"tool_errors": 0,
"stage_success": true,
"started_at": "2026-08-15T21:12:46.516Z",
"issue_num": 408
},
{
"stage": "implement",
"model": "",
"endpoint": "default",
"tokens_in": 697535,
"tokens_out": 536565,
"cost_usd": 0,
"wall_time_s": 700.915,
"tool_calls": 0,
"tool_errors": 0,
"stage_success": true,
"started_at": "2026-08-15T21:16:54.424Z",
"issue_num": 408
},
{
"stage": "review",
"model": "",
"endpoint": "default",
"tokens_in": 159723,
"tokens_out": 122864,
"cost_usd": 0,
"wall_time_s": 347.627,
"tool_calls": 0,
"tool_errors": 0,
"stage_success": true,
"started_at": "2026-08-15T21:28:39.753Z",
"issue_num": 408
},
{
"stage": "learn",
"model": "",
"endpoint": "default",
"tokens_in": 7072,
"tokens_out": 5440,
"cost_usd": 0,
"wall_time_s": 12.923,
"tool_calls": 0,
"tool_errors": 0,
"stage_success": true,
"started_at": "2026-08-15T21:34:41.690Z",
"issue_num": 408
},
{
"stage": "assumptions",
"model": "",
"endpoint": "default",
"tokens_in": 50865,
"tokens_out": 39127,
"cost_usd": 0,
"wall_time_s": 70.239,
"tool_calls": 0,
"tool_errors": 0,
"stage_success": true,
"started_at": "2026-08-15T21:34:41.715Z",
"issue_num": 408
}
]
}
16 changes: 16 additions & 0 deletions .alpha-loop/learnings/session-summary-session-20260815-141225.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Session Summary: session/20260815-141225

## Overview
- Issue 408 completed successfully in 22 minutes with no retries or test-fix cycles.

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

## Recurring Anti-Patterns
- No recurring cross-issue anti-patterns were observed.

## Recommendations
- Update implementation-planning guidance to require an execution-mode matrix covering dry-run, auto-merge, non-auto-merge, and parallel-worker paths.

## Metrics
| Metric | Value |
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ This lets you plan work in GitHub milestones and control exactly how much the lo

When `auto_merge` is enabled (default), Alpha Loop creates a session branch (e.g., `session/20260331-002240`) and merges each issue's PR into it. This keeps your main branch clean until you're ready to merge the whole session.

If project context is stale when a session starts, Alpha Loop regenerates context and managed agent instructions inside the isolated session worktree. Valid generated changes become the session branch's first commit before that branch is pushed or its draft PR is opened, so they follow the normal review path and never dirty or push the base-branch checkout. Invalid or missing scan output stops the session before publication.

### Learnings

Each completed issue produces a learning file in `.alpha-loop/learnings/` that is committed with that issue's implementation PR. It includes:
Expand Down Expand Up @@ -865,7 +867,7 @@ alpha-loop run --epics 205,166,214

The queue is validated before any work starts. Each listed issue must exist, be labeled `epic`, not be duplicated, and be open unless it is already closed as completed. Alpha Loop processes sequential queues in the given order, creates/finalizes one session branch and PR per epic, and stops on the first failure. By default, queues use `stacked` ancestry. For independent work, add `--queue-branch-mode independent --parallel 2`: Alpha Loop derives topological waves from queued `Depends on #N` references, runs up to two ready epics as child processes, and waits for each wave before starting dependents. A failed epic does not cancel siblings already running; unrelated later epics continue, failed dependents are recorded as skipped, and the queue exits nonzero.

Parallel worker output is stored in `.alpha-loop/sessions/queue-<timestamp>/epic-<N>.log`. The atomic `queue.json` manifest records the concurrency limit, waves, dependencies, per-epic status and log path, and dependency-failure skips. Use `alpha-loop history queue-<timestamp>` to inspect progress. `--parallel` requires independent mode; stacked branches are inherently sequential. `--dry-run` prints the validated wave schedule without mutating GitHub or git state.
Parallel worker output is stored in `.alpha-loop/sessions/queue-<timestamp>/epic-<N>.log`. The atomic `queue.json` manifest records the concurrency limit, waves, dependencies, per-epic status and log path, and dependency-failure skips. Each worker independently refreshes stale context only in its own session worktree and carries any generated changes on its own session branch; the coordinator does not write generated files to the shared checkout. Use `alpha-loop history queue-<timestamp>` to inspect progress. `--parallel` requires independent mode; stacked branches are inherently sequential. `--dry-run` prints the validated wave schedule without mutating GitHub or git state.

Sub-issues are processed in checklist order (not issue-number order). Each sub-issue PR gets `Part of #165` appended, and the epic body's checkboxes auto-flip from `- [ ]` to `- [x]` as PRs merge. When every sub-issue has shipped, the loop runs a verification pass against each sub-issue's acceptance criteria — on `pass` the epic is auto-closed, on `partial` or `fail` it stays open with a `needs-human-input` label and a structured comment explaining the gaps.

Expand Down
2 changes: 1 addition & 1 deletion docs/epics.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ Queue execution is fail-stop by default. Alpha Loop stops at the first epic that

With `--parallel`, Alpha Loop parses queued `Depends on #N`, `after #N`, `requires #N`, and `blocked by #N` references into deterministic topological waves while preserving requested order within each wave. It starts at most `n` child `alpha-loop run --epic <N>` workers at once and waits for all started siblings before advancing. If one worker fails, its running siblings finish; epics that do not depend on the failure still run in later waves, while direct and transitive dependents are skipped with failure details. Any worker failure or dependency skip makes the final queue exit nonzero.

Before workers start, the coordinator syncs generated agent assets and refreshes project context once in the shared checkout. Each child then performs git mutations only in its session worktree and streams combined stdout/stderr to `.alpha-loop/sessions/queue-<timestamp>/epic-<N>.log`; queue context and structured result artifacts live beside the log. The existing per-epic session lock remains the last-resort guard against two workers targeting the same epic.
The coordinator does not sync or refresh generated files in the shared checkout. Each child checks context freshness and runs any required context scan plus managed-agent-asset sync only in its isolated session worktree. Valid generated changes are committed to that worker's session branch before it is pushed or its draft PR is opened; invalid or missing scan output fails the worker before publication. Worker output streams to `.alpha-loop/sessions/queue-<timestamp>/epic-<N>.log`, and queue context plus structured result artifacts live beside the log. The existing per-epic session lock remains the last-resort guard against two workers targeting the same epic.

If the process crashes inside a child issue before its branch has a PR, run `alpha-loop resume` to recover stranded `agent/issue-*` branches as WIP PRs. Recovered PRs are not marked complete; run the project tests and final verification before merging, then inspect the queue manifest and continue with the remaining epic IDs.

Expand Down
153 changes: 97 additions & 56 deletions src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import {
import { hasVision } from '../lib/vision.js';
import { contextNeedsRefresh } from '../lib/context.js';
import { runPreflight } from '../lib/preflight.js';
import { syncAgentAssets, resolveHarnesses } from './sync.js';
import { HARNESS_REGISTRY, syncAgentAssets, resolveHarnesses } from './sync.js';
import { saveCapturedCase, detectFailureStep } from '../lib/eval.js';
import { readGateResult, formatGateFindings } from '../lib/pipeline.js';
import { spawnAgent } from '../lib/agent.js';
Expand Down Expand Up @@ -900,6 +900,28 @@ async function runIssueSession(
: undefined;
const queueContext = target.type === 'epic' ? target.queue : undefined;
const activeMilestone = target.type === 'flat' ? target.activeMilestone : '';
const refreshContextInSession = contextNeedsRefresh(process.cwd());

if (config.dryRun) {
if (config.autoMerge) {
log.dry('Would sync agent assets in the session worktree before run');
if (refreshContextInSession) {
log.dry('Would refresh project context and instructions in the session worktree');
}
} else if (refreshContextInSession) {
log.dry('Would stop because project context is stale and auto_merge is disabled');
} else {
log.dry('Would sync agent assets before run');
}
} else if (refreshContextInSession && !config.autoMerge) {
throw new CommandExitError({
code: 'project-context-refresh-required',
message: 'Project context is stale, but auto_merge is disabled so Alpha Loop cannot carry the ' +
'refresh on a reviewable session branch. Enable auto_merge or run "alpha-loop scan" on an issue branch.',
});
} else if (!config.autoMerge) {
syncProjectAgentAssets(config);
}

if (target.type === 'issue') {
log.info(`Processing targeted issue #${target.issue.number}: ${target.issue.title}`);
Expand Down Expand Up @@ -944,7 +966,13 @@ async function runIssueSession(
try {
if (config.autoMerge) {
try {
await ensureSessionWorktree(session, config);
await ensureSessionWorktree(session, config, {
prepare: (worktreePath) => prepareSessionProjectFiles(
config,
worktreePath,
refreshContextInSession,
),
});
} catch (err) {
const message = `Failed to set up session worktree: ${err instanceof Error ? err.message : err}`;
log.error(message);
Expand Down Expand Up @@ -1002,63 +1030,97 @@ async function cleanupSessionWorktree(
}

function syncProjectAgentAssets(config: Config): void {
if (config.dryRun) {
log.dry('Would sync agent assets before run');
return;
}

const syncResult = syncAgentAssets(resolveHarnesses(config.harnesses, config.agent));
if (syncResult.synced) {
log.success('Agent assets synced before run');
}
}

async function refreshProjectContext(config: Config): Promise<void> {
if (!contextNeedsRefresh()) {
log.info('Project context is fresh');
return;
}

if (config.dryRun) {
log.dry('Would refresh project context and instructions');
return;
function generatedSessionPaths(config: Config): string[] {
const paths = new Set<string>([
'.alpha-loop/context.md',
'.alpha-loop/templates/instructions.md',
]);
for (const harness of resolveHarnesses(config.harnesses, config.agent)) {
const harnessConfig = HARNESS_REGISTRY[harness];
if (!harnessConfig) continue;
paths.add(harnessConfig.skillsDir);
if (harnessConfig.agentsDir) paths.add(harnessConfig.agentsDir);
if (harnessConfig.instructionsFile) paths.add(harnessConfig.instructionsFile);
}
return Array.from(paths);
}

log.info('Project context is stale or missing. Generating...');
const { scanCommand } = await import('./scan.js');
scanCommand();

const statusResult = exec('git status --porcelain .alpha-loop/ AGENTS.md CLAUDE.md');
if (!statusResult.stdout.trim()) {
if (contextNeedsRefresh()) {
async function prepareSessionProjectFiles(
config: Config,
worktreePath: string,
refreshContext: boolean,
): Promise<{ commitMessage: string; paths: string[] } | undefined> {
if (refreshContext) {
log.info('Project context is stale or missing. Generating in the session worktree...');
const { scanProject } = await import('./scan.js');
const scanResult = scanProject(worktreePath, config);
const backupPath = join(worktreePath, '.alpha-loop', 'templates', 'instructions.md.bak');
if (existsSync(backupPath)) unlinkSync(backupPath);
if (!scanResult.contextWritten || !scanResult.instructionsWritten) {
const missingOutputs = [
!scanResult.contextWritten ? 'project context' : '',
!scanResult.instructionsWritten ? 'managed instructions' : '',
].filter(Boolean).join(' or ');
throw new CommandExitError({
code: 'project-context-refresh-required',
message: 'Project context refresh did not produce a fresh context file. ' +
'Run "alpha-loop scan", review the output, and commit it through a pull request before retrying.',
message: `Project context refresh did not produce fresh ${missingOutputs}. ` +
'Review the scan output and retry; no session branch or pull request was published.',
});
}
return;
} else {
log.info('Project context is fresh');
}

const syncResult = syncAgentAssets(
resolveHarnesses(config.harnesses, config.agent),
{ projectDir: worktreePath },
);
if (syncResult.synced) {
log.success('Agent assets synced in the session worktree before run');
}

const generatedPaths = generatedSessionPaths(config);
const pathspecs = generatedPaths.map((generatedPath) => shellQuote(generatedPath)).join(' ');
const statusResult = exec(`git status --porcelain -- ${pathspecs}`, { cwd: worktreePath });
if (statusResult.exitCode !== 0) {
throw new CommandExitError({
code: 'project-context-refresh-required',
message: `Could not inspect generated session files: ${statusResult.stderr || statusResult.stdout}`,
});
}
if (!statusResult.stdout.trim()) return undefined;

const validation = validateGeneratedMarkdownForCommit(process.cwd(), statusResult.stdout);
const validation = validateGeneratedMarkdownForCommit(worktreePath, statusResult.stdout);
if (!validation.valid) {
log.warn('Generated context/instructions failed validation:');
for (const error of validation.errors) {
log.warn(` ${error}`);
}
throw new CommandExitError({
code: 'project-context-refresh-required',
message: 'Project context refresh failed validation. Fix the generated files on an issue branch ' +
'and commit them through a pull request before retrying.',
message: 'Generated project context or instructions failed validation. ' +
'No session branch or pull request was published; review the scan output and retry.',
});
}

throw new CommandExitError({
code: 'project-context-refresh-required',
message: 'Project context was refreshed and left uncommitted. Review the generated files, ' +
'commit them on an issue branch, and merge them through a pull request before retrying. ' +
`Alpha Loop will not push directly to ${config.baseBranch}.`,
});
const addResult = exec(`git add -A -- ${pathspecs}`, { cwd: worktreePath });
if (addResult.exitCode !== 0) {
throw new CommandExitError({
code: 'project-context-refresh-required',
message: `Could not stage validated generated session files: ${addResult.stderr || addResult.stdout}`,
});
}

return {
commitMessage: 'chore: refresh project context and agent assets',
paths: generatedPaths,
};
}

async function executeSessionRun(
Expand Down Expand Up @@ -1204,14 +1266,6 @@ async function executeSessionRun(
process.on('SIGINT', handleSigint);
process.on('SIGTERM', handleSigterm);

// Parallel queue workers share a coordinator checkout. The coordinator performs
// generated-file preparation once before spawning them so workers only mutate
// their session worktrees.
const isParallelQueueWorker = options.queueContext !== undefined;
if (!isParallelQueueWorker) {
syncProjectAgentAssets(config);
}

// Pre-flight test validation
log.step('Running pre-flight test validation...');
if (!config.skipPreflight && !config.skipTests) {
Expand Down Expand Up @@ -1281,12 +1335,6 @@ async function executeSessionRun(
}
}

// Refresh project metadata before queue discovery. If generated content changes,
// stop so it can be reviewed and merged through a normal branch/PR first.
if (!isParallelQueueWorker) {
await refreshProjectContext(config);
}

// --- Fetch issue queue ---
// When an epic is selected, the queue is its sub-issues in checklist order.
// Otherwise, fetch via the usual project/label/milestone path and exclude
Expand Down Expand Up @@ -3020,13 +3068,6 @@ async function runEpicQueue(config: Config, options: RunOptions): Promise<void>
return;
}

if (parallelLimit !== undefined) {
// Shared generated assets and context live in the coordinator checkout.
// Prepare them once before children begin worktree-only execution.
syncProjectAgentAssets(config);
await refreshProjectContext(config);
}

const scheduledWaves = parallelLimit !== undefined
? buildEpicQueueWaves(validation.entries)
: validation.entries.map((entry) => [entry]);
Expand Down
Loading