@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 +43 -0
- package/README.md +49 -15
- package/bin/loop.mjs +197 -7
- package/package.json +7 -1
- package/presets/work-item/loop.config.mjs +1 -1
- package/presets/work-item/prompts/docs.md +6 -4
- package/presets/work-item/prompts/pr-description.md +27 -0
- package/presets/work-item/prompts/review.md +6 -1
- package/prompts/address.md +2 -1
- package/prompts/docs.md +5 -3
- package/prompts/{git.md → pr-description.md} +11 -8
- package/prompts/review.md +9 -4
- package/src/adapters.mjs +84 -9
- package/src/backends/gitlab.mjs +231 -0
- package/src/command.mjs +156 -6
- package/src/config.mjs +278 -53
- package/src/git.mjs +26 -0
- package/src/hermetic.mjs +160 -0
- package/src/index.mjs +22 -1
- package/src/manifest.mjs +67 -0
- package/src/metrics.mjs +251 -0
- package/src/operations.mjs +651 -0
- package/src/pipeline.mjs +505 -427
- package/src/policy.mjs +63 -0
- package/src/publish.mjs +183 -0
- package/src/reporter.mjs +102 -0
- package/src/runner.mjs +509 -0
- package/src/state.mjs +338 -16
- package/src/verdict.mjs +36 -2
- package/src/worktree.mjs +33 -0
- package/presets/work-item/prompts/git.md +0 -44
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
|
|
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. `
|
|
39
|
-
3. `
|
|
40
|
-
4. `
|
|
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
|
-
|
|
43
|
-
|
|
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.
|
|
49
|
-
|
|
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', '
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
the adapter
|
|
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
|
|
9
|
-
|
|
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:
|
|
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.
|
|
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', '
|
|
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
|
|
2
|
-
branch `{{BRANCH}}`, in the worktree at `{{REPO}}`.
|
|
3
|
-
|
|
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
|
|
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
|
|
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
|
package/prompts/address.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
13
|
-
body
|
|
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
|
-
-
|
|
26
|
-
|
|
27
|
-
-
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|