@syntax-syllogism/aloop 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0/).
7
7
 
8
+ ## [0.6.0] - 2026-09-14
9
+
10
+ ### Added
11
+
12
+ - **Publish attestation.** The loop records the exact reviewer-approved commit
13
+ SHA and refuses to publish until a gate-backed review approval covers the tree
14
+ being shipped. Any mutation that lands after approval is re-gated, and the
15
+ approval is preserved across a resumed publish/PR phase.
16
+ - **Driver-owned publishing.** The driver, not the agent, performs the push and
17
+ pull-request/publish step. Publishing is bound to the configured remote,
18
+ retries are bounded, and PR descriptions are validated before use.
19
+ - **GitLab publish backend.** Publishing can target GitLab in addition to
20
+ GitHub, selected through the publish backend configuration.
21
+ - **Operational run commands.** `list`, `status`, `inspect`, `metrics`,
22
+ `cancel`, `clean`, and `doctor` inspect, cancel, clean up, and diagnose
23
+ persisted runs, each with `--json` output for scripting.
24
+ - **Run budgets.** Optional `budget` ceilings (`tokens`, `usd`, `wallClockMs`)
25
+ are enforced cumulatively across a run, including review rounds and repairs.
26
+ The runner stalls at the next phase or repair boundary when a limit is
27
+ reached and records the usage in the manifest; budget stalls are resumable.
28
+ - **Per-phase least privilege.** Phases declare a permission kind
29
+ (`read-only`, `write-worktree`, `publish`). Read-only phases run against
30
+ isolated source snapshots with artifact-only writes scoped to the run
31
+ directory.
32
+ - **Opt-in hermetic phase execution** to isolate a phase from ambient
33
+ environment and network state.
34
+ - **POSIX command shell configuration and Windows (Git Bash) support**, so runs
35
+ work across POSIX shells and on Windows.
36
+
37
+ ### Changed
38
+
39
+ - Renamed the `git` phase and its prompt to `pr-description`, reflecting its
40
+ narrowed responsibility now that publishing is driver-owned.
41
+ - Configuration now lints the phase order and rejects orderings that would
42
+ break the publish attestation boundary.
43
+
44
+ ### Fixed
45
+
46
+ - Make the config-import cache-buster collision-proof.
47
+ - Sync the work-item preset's verdict schema rules with the runner.
48
+ - Handle a failed Windows timeout teardown and isolate POSIX tests from
49
+ Windows CI.
50
+
8
51
  ## [0.5.0] - 2026-09-12
9
52
 
10
53
  ### Added
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `@syntax-syllogism/aloop` is an engine-agnostic agentic loop runner. It drives
4
4
  a task through file-based implementation, deterministic checks, review, repair,
5
- documentation, and git phases. The driver owns control flow; configured agent
5
+ documentation, and publishing phases. The driver owns control flow; configured agent
6
6
  CLIs provide judgment in fresh processes.
7
7
 
8
8
  ## Install
@@ -16,6 +16,16 @@ npx @syntax-syllogism/aloop --help
16
16
  The package is published to the public npm registry and needs no private
17
17
  registry configuration.
18
18
 
19
+ ## Platform support
20
+
21
+ Linux and macOS are supported. Windows is supported when aloop is launched
22
+ from Git Bash (Git for Windows), which provides the POSIX `sh` used for setup
23
+ and gate commands. Install Git for Windows, ensure `sh.exe` is on `PATH`, and
24
+ run aloop from a Git Bash terminal. WSL works as Linux.
25
+
26
+ Interactive confirmations use the Git Bash/Windows console when available. Use
27
+ `--yes` for unattended or piped runs when no console input is available.
28
+
19
29
  ## Quick start
20
30
 
21
31
  From a git repository with a `loop.config.mjs` (or with the defaults):
@@ -28,25 +38,32 @@ aloop --task-file plan.md
28
38
  Inspect the rendered plan first with `aloop --task "..." --name example
29
39
  --dry-run`. A terminal run asks for confirmation before each phase. Use `-y`
30
40
  only after the configured engines and workflow have been proven for the task
31
- shapes you intend to automate.
41
+ shapes you intend to automate. Required phases cannot be skipped interactively:
42
+ declining one asks you to run it or quit. `docs` is optional by default; see
43
+ [`docs/loop.md`](docs/loop.md#phases) for custom-phase rules and the full phase
44
+ reference.
32
45
 
33
46
  ## How the loop works
34
47
 
35
48
  The default pipeline is:
36
49
 
37
50
  1. `implement` — make the requested change in an isolated git worktree.
38
- 2. `gate` — run deterministic commands such as `npm test`.
39
- 3. `review` — write a machine-readable verdict JSON file.
40
- 4. `address` — repair blocking findings, then repeat the gate and review up to
51
+ 2. `docs` — update documentation when the implementation requires it.
52
+ 3. `gate` — run deterministic commands such as `npm test`.
53
+ 4. `review` — write a machine-readable verdict JSON file.
54
+ 5. `address` — repair blocking findings, then repeat the gate and review up to
41
55
  the configured round limit.
42
- 5. `docs` — update documentation when the implementation requires it.
43
- 6. `git` — verify the tree and prepare the branch for an open pull request.
56
+ 6. `pr-description` — author the title and body in the run directory.
57
+ 7. `publish` — push and verify the attested branch, then create or update a
58
+ draft pull request.
44
59
 
45
60
  Phases communicate through files rather than shared agent sessions. A review
46
61
  verdict has the shape `{ verdict, summary, blocking, nits }`; malformed verdicts
47
62
  fail closed. The runner never merges a branch and never treats an agent's prose
48
- as a deterministic test result. See [`docs/loop.md`](docs/loop.md) for the
49
- complete guide and verdict contract.
63
+ as a deterministic test result. Each real run also records the repository SHAs,
64
+ rendered prompt and configuration hashes, gate receipts, verdict binding, and
65
+ phase artifacts in its run manifest. See [`docs/loop.md`](docs/loop.md) for the
66
+ complete guide, manifest schema, and verdict contract.
50
67
 
51
68
  ## Configuration
52
69
 
@@ -59,8 +76,10 @@ export default {
59
76
  default: { name: 'claude', effort: 'high' },
60
77
  review: { name: 'codex', effort: 'high' },
61
78
  },
62
- phases: ['implement', 'gate', 'review', 'address', 'docs', 'git'],
79
+ phases: ['implement', 'docs', 'gate', 'review', 'address', 'pr-description', 'publish'],
63
80
  gate: ['npm test'],
81
+ shell: 'sh',
82
+ publish: { backend: 'github', draft: true },
64
83
  setup: ['npm ci'],
65
84
  maxRounds: 3,
66
85
  };
@@ -72,14 +91,29 @@ an engine-specific `model` and `effort`. `--config` selects a configuration
72
91
  file outside the current repository; command-line phase and round options take
73
92
  precedence.
74
93
 
94
+ GitLab publishing is also bundled: set `publish: { backend: 'gitlab', draft:
95
+ true }` to use the authenticated `glab` CLI. For a REST or MCP integration,
96
+ import `gitlabBackend` and inject the small GitLab-native transport port:
97
+
98
+ ```js
99
+ import { gitlabBackend } from '@syntax-syllogism/aloop';
100
+
101
+ const myMcpTransport = { checkAuth, getMergeRequest, createMergeRequest, updateMergeRequest };
102
+ export default { publish: { backend: gitlabBackend({ transport: myMcpTransport }), draft: true } };
103
+ ```
104
+
75
105
  ### Bring your own engine
76
106
 
77
107
  Register an adapter under `adapters` when another CLI should run a phase. Its
78
- `command({ prompt, cwd, addDirs, agent })` function returns `{ command, args }`.
79
- It may also provide `efforts` validation and `createRenderer()` for streaming
80
- output. Because adapters run commands with write access, non-interactive or
81
- auto-approve flags can allow an agent to change files without asking. Review
82
- the adapter and its permissions before enabling unattended runs.
108
+ `command({ prompt, cwd, addDirs, readOnlyDirs, permissions, artifactOnly, agent })` function returns `{ command, args }`. For a read-only phase,
109
+ `cwd` is always the run directory and `addDirs` contains only artifact roots;
110
+ `readOnlyDirs` is an optional source-inspection input for adapters that can
111
+ honor it. The runner supplies a permission level and restricts `addDirs`
112
+ accordingly; the adapter must translate `permissions` into its own read-only
113
+ or write-capable invocation flags. It may also provide `efforts`
114
+ validation and `createRenderer()` for streaming output. Custom adapter flags
115
+ are vendor-specific and are not verified by aloop. See the [phase permission
116
+ contract](docs/loop.md#phase-permissions) before enabling unattended runs.
83
117
 
84
118
  ## Prompt overrides and the work-item preset
85
119
 
package/bin/loop.mjs CHANGED
@@ -1,12 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { parseArgs } from 'node:util';
4
+ import { readFile } from 'node:fs/promises';
5
+ import { join } from 'node:path';
4
6
  import { isMainEntrypoint } from '../src/entrypoint.mjs';
7
+ import {
8
+ cancelRun,
9
+ cleanRuns,
10
+ doctor,
11
+ getAggregateMetrics,
12
+ getRun,
13
+ inspectRun,
14
+ listRuns,
15
+ resolveRunsDir,
16
+ } from '../src/operations.mjs';
17
+ import { computeRunMetrics } from '../src/metrics.mjs';
5
18
  import { runLoop } from '../src/pipeline.mjs';
6
19
 
20
+ const commands = new Set(['run', 'list', 'status', 'inspect', 'cancel', 'clean', 'doctor', 'metrics']);
21
+
7
22
  export function parseLoopArgs(argv) {
8
- const { values } = parseArgs({
9
- args: argv,
23
+ const command = commands.has(argv[0]) ? argv[0] : 'run';
24
+ const commandArgv = command === 'run' && argv[0] !== 'run' ? argv : argv.slice(1);
25
+ const { values, positionals } = parseArgs({
26
+ args: commandArgv,
10
27
  options: {
11
28
  task: { type: 'string', short: 't' },
12
29
  'task-file': { type: 'string', short: 'f' },
@@ -23,17 +40,33 @@ export function parseLoopArgs(argv) {
23
40
  yes: { type: 'boolean', short: 'y' },
24
41
  'no-worktree': { type: 'boolean' },
25
42
  'dry-run': { type: 'boolean' },
43
+ json: { type: 'boolean' },
44
+ metrics: { type: 'boolean' },
45
+ 'older-than': { type: 'string' },
26
46
  help: { type: 'boolean', short: 'h' },
27
47
  },
28
- allowPositionals: false,
48
+ allowPositionals: true,
29
49
  strict: true,
30
50
  });
31
- if (values.help) return { help: true };
51
+ if (values.help) return { command, help: true };
32
52
  const maxRounds = values['max-rounds'] ? Number(values['max-rounds']) : undefined;
33
53
  if (maxRounds !== undefined && (!Number.isInteger(maxRounds) || maxRounds < 1)) {
34
54
  throw new Error('--max-rounds must be a positive integer');
35
55
  }
56
+ const olderThanDays = values['older-than'] === undefined ? undefined : Number(values['older-than']);
57
+ if (olderThanDays !== undefined && (!Number.isFinite(olderThanDays) || olderThanDays < 0)) {
58
+ throw new Error('--older-than must be a non-negative number of days');
59
+ }
60
+ const [runName] = positionals;
61
+ if (positionals.length > 1 || (['status', 'inspect', 'cancel'].includes(command) && !runName)) {
62
+ throw new Error(`${command} requires exactly one run name`);
63
+ }
64
+ if (command !== 'run' && !['status', 'inspect', 'cancel'].includes(command) && positionals.length) {
65
+ throw new Error(`${command} does not accept positional arguments`);
66
+ }
36
67
  return {
68
+ command,
69
+ runName,
37
70
  task: values.task,
38
71
  taskFile: values['task-file'],
39
72
  name: values.name,
@@ -49,13 +82,34 @@ export function parseLoopArgs(argv) {
49
82
  yes: values.yes,
50
83
  noWorktree: values['no-worktree'],
51
84
  dryRun: values['dry-run'],
85
+ json: values.json,
86
+ metrics: values.metrics,
87
+ olderThanDays,
52
88
  };
53
89
  }
54
90
 
55
91
  export function usage() {
56
92
  return [
57
- 'Usage: aloop [options]',
93
+ 'Usage: aloop [command] [options]',
94
+ '',
95
+ 'Commands:',
96
+ ' list List persisted runs',
97
+ ' status <name> Show current phase, verdict, and metrics',
98
+ ' inspect <name> Show manifest, phase evidence, and metrics',
99
+ ' metrics Show aggregate metrics across runs',
100
+ ' cancel <name> Stop a run and release its lock',
101
+ ' clean Preview/remove completed runs older than 30 days',
102
+ ' doctor Check configuration and local tooling',
103
+ ' run Start or resume a run (the default)',
104
+ '',
105
+ 'Operational options:',
106
+ ' --json Emit machine-readable JSON',
107
+ ' --metrics Include per-run metrics in list output',
108
+ ' --older-than <n> Clean completed runs older than n days',
109
+ ' -y, --yes Confirm destructive clean operations',
110
+ ' --dry-run Preview clean operations without changing files',
58
111
  '',
112
+ 'Run options:',
59
113
  ' -t, --task <text> Inline task description',
60
114
  ' -f, --task-file <path> Path to a task/plan/spec file',
61
115
  ' -n, --name <slug> Explicit run identity/slug',
@@ -68,19 +122,155 @@ export function usage() {
68
122
  ' --max-rounds <n> Cap on review/repair rounds',
69
123
  ' --from <phase> Start at this phase',
70
124
  ' --resume Skip phases already recorded complete',
71
- ' -y, --yes Run unattended (no per-phase confirmation; required without a terminal)',
72
125
  ' --no-worktree Work in the current checkout instead of a worktree',
126
+ ' -y, --yes Run unattended (no per-phase confirmation; required without a terminal)',
73
127
  ' --dry-run Print the plan and rendered prompts, run nothing',
74
128
  ].join('\n');
75
129
  }
76
130
 
131
+ function formatMetric(value) {
132
+ return value === null || value === undefined ? '?' : String(value);
133
+ }
134
+
135
+ async function metricsForRun(run) {
136
+ try {
137
+ const manifest = JSON.parse(await readFile(join(run.runDir, 'manifest.json'), 'utf8'));
138
+ return computeRunMetrics(manifest);
139
+ } catch {
140
+ return computeRunMetrics({ phases: [] });
141
+ }
142
+ }
143
+
144
+ function printList(runs, output = console, includeMetrics = false) {
145
+ if (!runs.length) return output.log('No runs found.');
146
+ const header = ['NAME', 'STATUS', 'PHASE', 'BRANCH', 'UPDATED', 'PR'];
147
+ if (includeMetrics) header.push('DURATION', 'TOKENS', 'COST');
148
+ output.log(header.join('\t'));
149
+ for (const run of runs) {
150
+ const row = [run.name, run.status, run.currentPhase ?? '-', run.branch ?? '-', run.updatedAt ?? run.startedAt ?? '-', run.prUrl ?? '-'];
151
+ if (includeMetrics && run.metrics) {
152
+ row.push(`${run.metrics.total.durationMs}ms`, formatMetric(run.metrics.total.tokens), formatMetric(run.metrics.total.cost));
153
+ }
154
+ output.log(row.join('\t'));
155
+ }
156
+ }
157
+
158
+ function printMetricsLines(metrics, output = console) {
159
+ output.log(`duration : ${metrics.total.durationMs}ms`);
160
+ output.log(`tokens : ${formatMetric(metrics.total.tokens)}`);
161
+ output.log(`cost : ${formatMetric(metrics.total.cost)}`);
162
+ output.log(`catch : ${formatMetric(metrics.reviewer.catchRate)}`);
163
+ }
164
+
165
+ function printStatus(run, output = console, metrics = null) {
166
+ output.log(`run : ${run.name}`);
167
+ output.log(`status : ${run.status}`);
168
+ output.log(`phase : ${run.currentPhase ?? '-'}`);
169
+ output.log(`verdict : ${run.verdict ?? '-'}`);
170
+ output.log(`gate : ${run.gateStatus ?? '-'}`);
171
+ output.log(`branch : ${run.branch ?? '-'}`);
172
+ output.log(`updated : ${run.updatedAt ?? run.startedAt ?? '-'}`);
173
+ output.log(`PR : ${run.prUrl ?? '-'}`);
174
+ if (metrics) printMetricsLines(metrics, output);
175
+ }
176
+
177
+ function printAggregate(metrics, output = console) {
178
+ output.log(`runs : ${metrics.runs}`);
179
+ output.log(`duration : ${metrics.total.durationMs}ms`);
180
+ output.log(`tokens : ${formatMetric(metrics.total.tokens)}`);
181
+ output.log(`cost : ${formatMetric(metrics.total.cost)}`);
182
+ output.log(`convergence : ${formatMetric(metrics.convergence.convergenceRate)}`);
183
+ output.log(`catch : ${formatMetric(metrics.reviewer.catchRate)}`);
184
+ }
185
+
186
+ function printInspect(run, output = console, metrics = null) {
187
+ printStatus(run, output, metrics);
188
+ output.log(`run dir : ${run.runDir}`);
189
+ for (const phase of run.phases) {
190
+ output.log(`\n[${phase.phase}] ${phase.status}`);
191
+ for (const field of ['inputSha', 'outputSha', 'promptHash', 'configHash', 'engine', 'verdict', 'gateReceipts', 'artifacts']) {
192
+ if (phase[field] !== undefined && phase[field] !== null) output.log(`${field} : ${JSON.stringify(phase[field])}`);
193
+ }
194
+ }
195
+ }
196
+
197
+ export async function runOperationalCommand(args, { cwd = process.cwd(), output = console } = {}) {
198
+ const options = { cwd, configPath: args.config };
199
+ if (args.command === 'list') {
200
+ const result = await listRuns(options);
201
+ if (args.metrics) {
202
+ for (const run of result) run.metrics = await metricsForRun(run);
203
+ }
204
+ if (args.json) output.log(JSON.stringify(result, null, 2));
205
+ else printList(result, output, args.metrics);
206
+ return result;
207
+ }
208
+ if (args.command === 'status') {
209
+ const run = await getRun(args.runName, options);
210
+ const metrics = computeRunMetrics(run.manifest);
211
+ const result = { ...run.summary, metrics };
212
+ if (args.json) output.log(JSON.stringify(result, null, 2));
213
+ else printStatus(run.summary, output, metrics);
214
+ return result;
215
+ }
216
+ if (args.command === 'inspect') {
217
+ const result = await inspectRun(args.runName, options);
218
+ const metrics = computeRunMetrics(result.manifest ?? { phases: result.phases });
219
+ if (args.json) output.log(JSON.stringify({ ...result, metrics }, null, 2));
220
+ else printInspect(result, output, metrics);
221
+ return result;
222
+ }
223
+ if (args.command === 'metrics') {
224
+ const { runsDir } = await resolveRunsDir(cwd, args.config);
225
+ const result = await getAggregateMetrics(runsDir);
226
+ if (args.json) output.log(JSON.stringify(result, null, 2));
227
+ else printAggregate(result, output);
228
+ return result;
229
+ }
230
+ if (args.command === 'cancel') {
231
+ const result = await cancelRun(args.runName, options);
232
+ if (args.json) output.log(JSON.stringify(result, null, 2));
233
+ else printStatus(result, output);
234
+ return result;
235
+ }
236
+ if (args.command === 'clean') {
237
+ const result = await cleanRuns({
238
+ ...options,
239
+ olderThanDays: args.olderThanDays ?? 30,
240
+ dryRun: args.dryRun,
241
+ yes: args.yes,
242
+ });
243
+ if (args.json) output.log(JSON.stringify(result, null, 2));
244
+ else {
245
+ output.log(`${result.dryRun ? 'Would remove' : 'Removed'} ${result.dryRun ? result.candidates.length : result.removed.length} run(s).`);
246
+ for (const run of result.candidates) output.log(` ${run.name} (${run.updatedAt ?? run.startedAt ?? 'unknown date'})`);
247
+ if (!args.yes && !args.dryRun && result.candidates.length) output.log('Preview only: rerun with --yes to remove these runs.');
248
+ for (const error of result.errors) output.error?.(` ${error.name}: ${error.error}`);
249
+ }
250
+ return result;
251
+ }
252
+ if (args.command === 'doctor') {
253
+ const result = await doctor(options);
254
+ if (args.json) output.log(JSON.stringify(result, null, 2));
255
+ else {
256
+ for (const check of result.checks) output.log(`${check.ok ? '✓' : '✗'} ${check.name}: ${check.detail}`);
257
+ output.log(result.ok ? 'Doctor: OK' : 'Doctor: problems found');
258
+ }
259
+ return result;
260
+ }
261
+ throw new Error(`Unknown command: ${args.command}`);
262
+ }
263
+
77
264
  if (isMainEntrypoint(import.meta.url)) {
78
265
  try {
79
266
  const args = parseLoopArgs(process.argv.slice(2));
80
267
  if (args.help) {
81
268
  console.log(usage());
82
- } else {
269
+ } else if (args.command === 'run') {
83
270
  await runLoop({ args });
271
+ } else {
272
+ const result = await runOperationalCommand(args);
273
+ if (args.command === 'doctor' && !result.ok) process.exitCode = 1;
84
274
  }
85
275
  } catch (error) {
86
276
  console.error(error.message);
package/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "@syntax-syllogism/aloop",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Syntax & Syllogism agentic loop runner.",
5
5
  "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.mjs",
8
+ "./backends/github": "./src/publish.mjs",
9
+ "./backends/gitlab": "./src/backends/gitlab.mjs",
10
+ "./src/*": "./src/*"
11
+ },
6
12
  "engines": {
7
13
  "node": ">=22"
8
14
  },
@@ -6,7 +6,7 @@ export default {
6
6
  default: { name: 'claude', effort: 'high' },
7
7
  review: { name: 'codex', effort: 'high' },
8
8
  },
9
- phases: ['implement', 'gate', 'review', 'address', 'docs', 'git'],
9
+ phases: ['implement', 'docs', 'gate', 'review', 'address', 'pr-description', 'publish'],
10
10
  gate: ['npm test'],
11
11
  maxRounds: 3,
12
12
  timeoutMs: 30 * 60 * 1000,
@@ -1,6 +1,7 @@
1
- Do a documentation-as-built pass with the committed work in the current
2
- branch `{{BRANCH}}`, in the worktree at `{{REPO}}`. The work implements
3
- `{{TASK_NAME}}` and has passed code review.
1
+ Do a documentation-as-built pass with the committed implementation in the
2
+ current branch `{{BRANCH}}`, in the worktree at `{{REPO}}`. This phase runs
3
+ before the gate and review phases, so its commit is part of the artifact they
4
+ inspect. The work implements `{{TASK_NAME}}`.
4
5
 
5
6
  This is a docs/agent-guidance reconciliation pass only. Do not make code changes
6
7
  unless they are strictly necessary to correct documentation generation or broken
@@ -28,4 +29,5 @@ commit. Keep the worktree clean for the next phase. If the documentation is
28
29
  already accurate, make no changes and leave the worktree clean.
29
30
 
30
31
  If you find a bug while reading, note it in your final summary (and under `## Changelog` in the task file if present)
31
- rather than fixing it; a code change at this point would go out unreviewed.
32
+ rather than fixing it. Later `address` rounds may revise documentation as part
33
+ of their reviewed fixes.
@@ -0,0 +1,27 @@
1
+ The code-producing phases have already committed their work in `{{REPO}}`, on
2
+ branch `{{BRANCH}}`. Verify the worktree is clean and do not create any code
3
+ commits in this phase.
4
+
5
+ - Inspect the full worktree and recent branch history before writing the
6
+ description.
7
+ - Do _NOT_ include notes about co-author / author.
8
+ - Report the existing code commit hashes and messages, validation performed,
9
+ and whether the worktree is clean.
10
+ - Write `{{RUN_DIR}}/pr.md` with `Title: <one-line title>` on the first line,
11
+ a blank line, and a body of fewer than 250 words.
12
+
13
+ The PR body should cover what changed and why, a summary of the task, the
14
+ review outcome (how many rounds, plus any recorded disagreements from the task file's
15
+ `## Code Review` section if present), and anything deliberately left out of scope.
16
+
17
+ ## Boundaries
18
+
19
+ - If the worktree is **not** clean — `git status --porcelain` reports anything —
20
+ do **not** edit files, tests, or configuration to make it clean, and do not
21
+ create commits. A dirty tree at this phase is a pipeline error: stop
22
+ immediately and report the offending paths. Reconciling it is out of scope
23
+ here, and the earlier phase that left the work uncommitted must be resumed
24
+ instead.
25
+ - Do not push, create, edit, merge, or enable auto-merge on a pull request. The
26
+ driver performs and verifies those operations after this phase.
27
+ - Do not modify the worktree or create commits.
@@ -10,7 +10,9 @@ section, creating the section at the end of the file if it is absent. Start with
10
10
  AND Time as there may be multiple rounds of code reviews. Provide a summary of your
11
11
  review and note any gaps in completion of the task.
12
12
 
13
- The implementation, repair, and documentation phases commit their work. The
13
+ The implementation and documentation phases have already committed their work,
14
+ so documentation is included in the reviewed diff. The repair phase may revise
15
+ documentation while committing fixes or may record a written rebuttal. The
14
16
  worktree should be clean; confirm that with `git status`. Review the complete
15
17
  cumulative branch diff with `git diff {{BASE_BRANCH}}...HEAD`, including all
16
18
  new files. Review all of it, not a sample.
@@ -68,6 +70,9 @@ In addition to your notes in the task file (if present), write this JSON to
68
70
  Rules the loop enforces — violating them stalls the run:
69
71
 
70
72
  - `blocking` holds every 🔴 and 🟡 finding. `nits` holds 🟢 and 💬.
73
+ - `blocking` and `nits` must be arrays when supplied. Each finding must be an
74
+ object with a non-empty `issue` or `summary` string; `file`, when included,
75
+ must be a string and `line`, when included, must be a number.
71
76
  - `CHANGES_REQUESTED` requires at least one entry in `blocking`.
72
77
  - `APPROVED` requires `blocking` to be empty.
73
78
  - Approve only if you would merge this as-is. If the gate status above is
@@ -56,4 +56,5 @@ Judge them by process exit code on unfiltered output.
56
56
  At the end of the phase, commit the fixes and their tests in a conventional
57
57
  commit whose message names the review findings addressed. Keep the worktree
58
58
  clean for the next phase. If there is nothing to commit, leave the worktree
59
- clean.
59
+ clean. When blocking findings exist, that is valid only if you recorded a
60
+ rebuttal in the response file; otherwise the phase stalls.
package/prompts/docs.md CHANGED
@@ -1,7 +1,8 @@
1
1
  {{TASK_CONTEXT}}
2
2
 
3
- Do a documentation-as-built pass for the committed work on branch `{{BRANCH}}`,
4
- in the worktree at `{{REPO}}`.
3
+ Do a documentation-as-built pass for the committed implementation on branch
4
+ `{{BRANCH}}`, in the worktree at `{{REPO}}`. This phase runs before the gate and
5
+ review phases, so its commit is part of the artifact they inspect.
5
6
 
6
7
  This is a documentation reconciliation pass only. Do not make code changes
7
8
  unless they are strictly necessary to correct documentation generation or a
@@ -16,7 +17,8 @@ documentation changes, such as checking links and paths by inspection and
16
17
  reviewing the documentation diff.
17
18
 
18
19
  If you find a bug while reading, note it in your final summary rather than
19
- fixing it; a code change at this point would go out unreviewed.
20
+ fixing it. Code changes are outside this documentation-only pass. Later
21
+ `address` rounds may revise documentation as part of their reviewed fixes.
20
22
 
21
23
  At the end of the phase, commit documentation changes in a `docs:` conventional
22
24
  commit. Keep the worktree clean for the next phase. If the documentation is
@@ -5,12 +5,17 @@ branch `{{BRANCH}}`.
5
5
 
6
6
  Verify the worktree is clean and do not create any code commits in this phase.
7
7
 
8
- - Inspect the full worktree and recent branch history before pushing.
8
+ - Inspect the full worktree and recent branch history before writing the
9
+ description.
9
10
  - Do _NOT_ include notes about co-author / author.
10
11
  - Report the existing code commit hashes and messages, validation performed,
11
12
  and whether the worktree is clean.
12
- - Push to `{{REMOTE}}` and create a PR to `{{BASE_BRANCH}}` on the same repo. PR
13
- body should be < 250 words.
13
+ - Write `{{RUN_DIR}}/pr.md` with this exact format. The first line is a
14
+ one-line title, followed by a blank line and a body of fewer than 250 words:
15
+
16
+ Title: <pull request title>
17
+
18
+ <pull request body>
14
19
 
15
20
  The PR body should cover what changed and why, a summary of the task, the review
16
21
  outcome (how many rounds, plus any recorded disagreements), and anything
@@ -22,8 +27,6 @@ deliberately left out of scope.
22
27
  do **not** edit files, tests, or configuration to make it clean, and do not
23
28
  create commits. A dirty tree at this phase is a pipeline error: stop
24
29
  immediately and report the offending paths. Reconciling it is out of scope.
25
- - **Do not merge the pull request**, and do not enable auto-merge. A human
26
- decides; an open PR is the correct end state for this run.
27
- - Never force-push, and never rewrite commits that already exist on `{{REMOTE}}`.
28
- - If the push is rejected because the branch moved, stop and report it rather
29
- than forcing anything.
30
+ - Do not push, create, edit, merge, or enable auto-merge on a pull request. The
31
+ driver performs and verifies those operations after this phase.
32
+ - Do not modify the worktree or create commits.
package/prompts/review.md CHANGED
@@ -6,10 +6,12 @@ in the worktree at `{{REPO}}` for bugs, quality, and alignment with the task.
6
6
  Write your prose review to `{{RUN_DIR}}/review-round-{{ROUND}}.md`. Include a
7
7
  summary of the review and any gaps in completion of the task.
8
8
 
9
- The implementation, repair, and documentation phases commit their work. The
10
- worktree should be clean; confirm that with `git status`. Review the complete
11
- cumulative branch diff with `git diff {{BASE_BRANCH}}...HEAD`, including all
12
- new files. Review all of it, not a sample.
9
+ The implementation and documentation phases have already committed their work,
10
+ so documentation is included in the reviewed diff. The repair phase may revise
11
+ documentation while committing fixes or may record a written rebuttal in the
12
+ run directory. The worktree should be clean; confirm that with `git status`.
13
+ Review the complete cumulative branch diff with `git diff {{BASE_BRANCH}}...HEAD`,
14
+ including all new files. Review all of it, not a sample.
13
15
 
14
16
  This is review round {{ROUND}} of at most {{MAX_ROUNDS}}.
15
17
 
@@ -65,6 +67,9 @@ prose or your stdout.
65
67
  Rules the loop enforces — violating them stalls the run:
66
68
 
67
69
  - `blocking` holds every 🔴 and 🟡 finding. `nits` holds 🟢 and 💬.
70
+ - `blocking` and `nits` must be arrays when supplied. Each finding must be an
71
+ object with a non-empty `issue` or `summary` string; `file`, when included,
72
+ must be a string and `line`, when included, must be a number.
68
73
  - `CHANGES_REQUESTED` requires at least one entry in `blocking`.
69
74
  - `APPROVED` requires `blocking` to be empty.
70
75
  - Approve only if you would merge this as-is. If the gate status above is