compounding-loop 0.2.0 → 0.5.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.
Files changed (149) hide show
  1. package/README.md +19 -3
  2. package/dist/cli/claim.js +84 -0
  3. package/dist/cli/claim.js.map +1 -0
  4. package/dist/cli/draft.js +69 -0
  5. package/dist/cli/draft.js.map +1 -0
  6. package/dist/cli/index.js +284 -1
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/laneRound.js +96 -0
  9. package/dist/cli/laneRound.js.map +1 -0
  10. package/dist/cli/laneStatus.js +3 -1
  11. package/dist/cli/laneStatus.js.map +1 -1
  12. package/dist/cli/lanesInit.js +8 -2
  13. package/dist/cli/lanesInit.js.map +1 -1
  14. package/dist/cli/lock.js +22 -2
  15. package/dist/cli/lock.js.map +1 -1
  16. package/dist/cli/loops.js +7 -0
  17. package/dist/cli/loops.js.map +1 -1
  18. package/dist/cli/next.js +18 -0
  19. package/dist/cli/next.js.map +1 -0
  20. package/dist/cli/pace.js +41 -0
  21. package/dist/cli/pace.js.map +1 -0
  22. package/dist/cli/pause.js +81 -0
  23. package/dist/cli/pause.js.map +1 -0
  24. package/dist/cli/run.js +18 -6
  25. package/dist/cli/run.js.map +1 -1
  26. package/dist/cli/runs.js +118 -0
  27. package/dist/cli/runs.js.map +1 -0
  28. package/dist/cli/schedule.js +177 -0
  29. package/dist/cli/schedule.js.map +1 -0
  30. package/dist/cli/shipCheck.js +59 -0
  31. package/dist/cli/shipCheck.js.map +1 -0
  32. package/dist/core/brief.js +35 -12
  33. package/dist/core/brief.js.map +1 -1
  34. package/dist/core/draftBrief.js +68 -0
  35. package/dist/core/draftBrief.js.map +1 -0
  36. package/dist/core/laneRound.js +63 -0
  37. package/dist/core/laneRound.js.map +1 -0
  38. package/dist/core/lanes.js +15 -2
  39. package/dist/core/lanes.js.map +1 -1
  40. package/dist/core/lanesInit.js +19 -0
  41. package/dist/core/lanesInit.js.map +1 -1
  42. package/dist/core/next.js +34 -0
  43. package/dist/core/next.js.map +1 -0
  44. package/dist/core/parse/changelog.js +41 -0
  45. package/dist/core/parse/changelog.js.map +1 -0
  46. package/dist/core/parse/claim.js +24 -0
  47. package/dist/core/parse/claim.js.map +1 -0
  48. package/dist/core/parse/controlRoom.js +38 -0
  49. package/dist/core/parse/controlRoom.js.map +1 -0
  50. package/dist/core/parse/index.js +17 -0
  51. package/dist/core/parse/index.js.map +1 -0
  52. package/dist/core/parse/markers.js +20 -0
  53. package/dist/core/parse/markers.js.map +1 -0
  54. package/dist/core/parse/outcomes.js +51 -0
  55. package/dist/core/parse/outcomes.js.map +1 -0
  56. package/dist/core/parse/paused.js +26 -0
  57. package/dist/core/parse/paused.js.map +1 -0
  58. package/dist/core/parse/policy.js +56 -0
  59. package/dist/core/parse/policy.js.map +1 -0
  60. package/dist/core/parse/runs.js +132 -0
  61. package/dist/core/parse/runs.js.map +1 -0
  62. package/dist/core/parse/schedule.js +101 -0
  63. package/dist/core/parse/schedule.js.map +1 -0
  64. package/dist/core/parse/steer.js +68 -0
  65. package/dist/core/parse/steer.js.map +1 -0
  66. package/dist/core/parse/task.js +96 -0
  67. package/dist/core/parse/task.js.map +1 -0
  68. package/dist/core/repo.js +18 -8
  69. package/dist/core/repo.js.map +1 -1
  70. package/dist/core/runPrompt.js +8 -2
  71. package/dist/core/runPrompt.js.map +1 -1
  72. package/dist/core/shipCheck.js +45 -0
  73. package/dist/core/shipCheck.js.map +1 -0
  74. package/dist/core/table.js +3 -1
  75. package/dist/core/table.js.map +1 -1
  76. package/dist/server/actionBody.js +54 -0
  77. package/dist/server/actionBody.js.map +1 -0
  78. package/dist/server/checkLabels.js +32 -0
  79. package/dist/server/checkLabels.js.map +1 -0
  80. package/dist/server/claude.js +18 -0
  81. package/dist/server/claude.js.map +1 -0
  82. package/dist/server/controls.js +99 -0
  83. package/dist/server/controls.js.map +1 -0
  84. package/dist/server/data.js +9 -3
  85. package/dist/server/data.js.map +1 -1
  86. package/dist/server/draftInputs.js +33 -0
  87. package/dist/server/draftInputs.js.map +1 -0
  88. package/dist/server/health.js +9 -30
  89. package/dist/server/health.js.map +1 -1
  90. package/dist/server/index.js +77 -0
  91. package/dist/server/index.js.map +1 -1
  92. package/dist/server/laneChecks.js +25 -0
  93. package/dist/server/laneChecks.js.map +1 -0
  94. package/dist/server/laneSchedule.js +15 -0
  95. package/dist/server/laneSchedule.js.map +1 -0
  96. package/dist/server/live/build.js +178 -0
  97. package/dist/server/live/build.js.map +1 -0
  98. package/dist/server/live/github.js +54 -0
  99. package/dist/server/live/github.js.map +1 -0
  100. package/dist/server/live/index.js +11 -0
  101. package/dist/server/live/index.js.map +1 -0
  102. package/dist/server/live/merge.js +16 -0
  103. package/dist/server/live/merge.js.map +1 -0
  104. package/dist/server/live/urls.js +61 -0
  105. package/dist/server/live/urls.js.map +1 -0
  106. package/dist/server/loops.js +263 -41
  107. package/dist/server/loops.js.map +1 -1
  108. package/dist/server/paceNote.js +13 -0
  109. package/dist/server/paceNote.js.map +1 -0
  110. package/dist/server/roundState.js +51 -0
  111. package/dist/server/roundState.js.map +1 -0
  112. package/dist/server/runs.js +144 -0
  113. package/dist/server/runs.js.map +1 -0
  114. package/dist/server/shipped.js +35 -0
  115. package/dist/server/shipped.js.map +1 -0
  116. package/dist/server/static-export.js +22 -1
  117. package/dist/server/static-export.js.map +1 -1
  118. package/dist/server/taskBullets.js +33 -0
  119. package/dist/server/taskBullets.js.map +1 -0
  120. package/dist/server/this-repo.json +2818 -106
  121. package/dist/web/assets/index-BozIcdeb.js +88 -0
  122. package/dist/web/assets/index-Te_olhNA.css +2 -0
  123. package/dist/web/index.html +2 -2
  124. package/method/COMPOUNDING_LOOP.md +26 -0
  125. package/method/retro.md +7 -0
  126. package/package.json +1 -1
  127. package/plugin/.claude-plugin/plugin.json +1 -1
  128. package/plugin/commands/loop-lanes.md +11 -0
  129. package/plugin/commands/loop-next.md +11 -0
  130. package/plugin/commands/loop-pause.md +10 -0
  131. package/runners/github-actions/lanes.yml +55 -2
  132. package/runners/local/README.md +3 -1
  133. package/runners/local/compounding-loop.service +20 -0
  134. package/runners/local/compounding-loop.timer +11 -0
  135. package/runners/routine/README.md +5 -1
  136. package/runners/routine/control-room-prompt.md +4 -14
  137. package/runners/routine/lane-prompt.md +4 -10
  138. package/runners/routine/prompt.md +2 -2
  139. package/templates/lanes/README.md +5 -1
  140. package/templates/lanes/claude-section.md +5 -1
  141. package/templates/lanes/control-run.md +37 -0
  142. package/templates/lanes/lane-run.md +33 -0
  143. package/templates/lanes/schedule.json +4 -0
  144. package/templates/lanes/steer.md +3 -0
  145. package/templates/lanes/task.md +1 -1
  146. package/templates/static-site/.ai/loop-prompt.md +1 -1
  147. package/templates/static-site/CLAUDE.md +2 -0
  148. package/dist/web/assets/index-B16QCSnY.css +0 -2
  149. package/dist/web/assets/index-CS82DKbF.js +0 -49
package/README.md CHANGED
@@ -8,7 +8,7 @@ npx compounding-loop init
8
8
 
9
9
  [![npm](https://img.shields.io/npm/v/compounding-loop)](https://www.npmjs.com/package/compounding-loop) [![CI](https://github.com/Minntooos/Compounding-Loop/actions/workflows/ci.yml/badge.svg)](https://github.com/Minntooos/Compounding-Loop/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
10
10
 
11
- **[Live demo](https://minntooos.github.io/Compounding-Loop/)**: the dashboard on the real five-site data, in your browser.
11
+ **[Watch this repo build itself](https://minntooos.github.io/Compounding-Loop/)**: the live demo reads this repo's commits from GitHub in your browser, next to the real five-site data. Steering and scheduling: [Glance, steer, sleep](#glance-steer-sleep). Working with a teammate? [Five steps](docs/team.md).
12
12
 
13
13
  ![The Compounding Loop fleet screen: six loops, one with lanes](docs/assets/screens/fleet-light-laptop.png)
14
14
 
@@ -56,6 +56,18 @@ flowchart LR
56
56
  5. **Compounding.** Every run writes what it learned into a small indexed knowledge base the next run reads first.
57
57
  6. **Escalation.** When it is stuck it writes `BLOCKED.md`. You answer from the dashboard inbox or `loop answer`.
58
58
 
59
+ ## Glance, steer, sleep
60
+
61
+ Leave a laned loop running, look at the dashboard, and steer it without opening a chat. The top strip says in five seconds whether anything needs you, whether lanes are running or stalled, whether runs hit the usage limit, whether CI is green and whether it is ready to ship.
62
+
63
+ ```sh
64
+ npx compounding-loop steer docs "Update the README screenshots first" # saved as a file in the repo, read at the lane's next run
65
+ npx compounding-loop schedule snooze web 6h # or pause, resume, wake
66
+ npx compounding-loop pace set sprint # how much checking each run does
67
+ ```
68
+
69
+ Lanes with nothing to do back off (3 h, then 6 h, then asleep until there is steering, a message or a new round), so a loop can stay on around the clock and a skipped run costs seconds. Routine prompts stay short and point at `.ai/lane-run.md`, so changing how every lane runs is one commit. Details: [Steer, schedule and pace a loop](docs/steering.md).
70
+
59
71
  ## Quick start
60
72
 
61
73
  ```sh
@@ -65,7 +77,9 @@ npx compounding-loop run # one round now
65
77
  npx compounding-loop dashboard # the local dashboard
66
78
  ```
67
79
 
68
- Several lanes in one repo (needs an `IDEA.md` brief first): `npx compounding-loop init --lanes core,web,docs`, then `npx compounding-loop run --lane core`. Check ownership with `npx compounding-loop check-lanes`. Details: [Lanes](docs/lanes.md).
80
+ Several lanes in one repo (needs an `IDEA.md` brief first): `npx compounding-loop init --lanes core,web,docs`, then `npx compounding-loop run --lane core`. Check ownership with `npx compounding-loop check-lanes`. Details: [Lanes](docs/lanes.md). Sharing a loop with a teammate: [Work on a loop with a teammate](docs/team.md).
81
+
82
+ **Run the loop without tokens.** Pause every lane (`npx compounding-loop pause`, undo with `resume`), start the next round from a brief in a laned repo (`next-round --brief`), see which scheduled runs happened (`runs`) and check whether you can ship (`ship-check`). The dashboard has buttons for all of it (Pause all, Start next round, Release), each with a confirm step, plus the run history and a Ready to ship checklist: see [Dashboard](docs/dashboard.md#controls). Releases go through a GitHub Actions workflow with npm trusted publishing and a protected `release` environment: see [Release](docs/release.md).
69
83
 
70
84
  Already have a repo? `npx compounding-loop init` adds the method to it without overwriting anything. Details: [Getting started](docs/getting-started.md).
71
85
 
@@ -101,7 +115,7 @@ If you work at the keyboard and want to watch agents in parallel, those tools ar
101
115
 
102
116
  ## Docs
103
117
 
104
- [Getting started](docs/getting-started.md) · [Lanes](docs/lanes.md) · [Concepts](docs/concepts.md) · [Runners](docs/runners.md) · [Dashboard](docs/dashboard.md) · [Templates](docs/templates.md) · [FAQ](docs/faq.md) · [Troubleshooting](docs/troubleshooting.md) · [Contributing](CONTRIBUTING.md)
118
+ [Getting started](docs/getting-started.md) · [Lanes](docs/lanes.md) · [Team](docs/team.md) · [Concepts](docs/concepts.md) · [Runners](docs/runners.md) · [Dashboard](docs/dashboard.md) · [Steering](docs/steering.md) · [Release](docs/release.md) · [Templates](docs/templates.md) · [FAQ](docs/faq.md) · [Troubleshooting](docs/troubleshooting.md) · [Contributing](CONTRIBUTING.md)
105
119
 
106
120
  ## FAQ
107
121
 
@@ -117,6 +131,8 @@ If you work at the keyboard and want to watch agents in parallel, those tools ar
117
131
 
118
132
  **What won't it do?** It will not publish, spend money, create accounts or change repo settings on its own, and it stops and asks (`BLOCKED.md`) rather than guess.
119
133
 
134
+ **Why does the live demo sometimes say "Snapshot"?** The demo page reads this repo from GitHub's public API without a login, which allows 60 requests an hour per IP address (a page load uses about three). When that runs out, or you are offline, it shows the bundled snapshot and the header chip says so; it switches back to live on the next refresh once GitHub allows requests again. Nothing is sent anywhere else.
135
+
120
136
  **Does it need a GitHub login?** Only to create a private repo with `loop new`; `--dry-run` and every test work offline.
121
137
 
122
138
  ## License
@@ -0,0 +1,84 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { parseLanesConfig } from '../core/lanes.js';
6
+ import { activeClaim, formatClaim } from '../core/parse/claim.js';
7
+ const execFileAsync = promisify(execFile);
8
+ const git = (cwd, ...args) => execFileAsync('git', args, { cwd });
9
+ async function requireLane(dir, lane) {
10
+ const text = await readFile(path.join(dir, '.ai', 'lanes.json'), 'utf8').catch(() => undefined);
11
+ if (text === undefined)
12
+ throw new Error('No .ai/lanes.json here. Create one with `loop init --lanes a,b,c`.');
13
+ const { config, errors } = parseLanesConfig(text);
14
+ if (!config)
15
+ throw new Error(`.ai/lanes.json is invalid:\n- ${errors.join('\n- ')}`);
16
+ if (!config.lanes.some((l) => l.name === lane))
17
+ throw new Error(`No lane "${lane}". Lanes here: ${config.lanes.map((l) => l.name).join(', ')}.`);
18
+ }
19
+ const claimPath = (lane) => `.ai/lanes/${lane}/claim`;
20
+ const readClaim = (dir, lane) => readFile(path.join(dir, ...claimPath(lane).split('/')), 'utf8').catch(() => undefined);
21
+ async function pushIfWanted(dir, result, push) {
22
+ if (push === false)
23
+ return;
24
+ try {
25
+ try {
26
+ await git(dir, 'push');
27
+ }
28
+ catch {
29
+ // Five lanes push to one branch, so a rejection is routine: rebase once and retry.
30
+ await git(dir, 'pull', '--rebase', '-q');
31
+ await git(dir, 'push');
32
+ }
33
+ result.pushed = true;
34
+ }
35
+ catch (error) {
36
+ result.pushed = false;
37
+ const lines = (error instanceof Error ? error.message : String(error)).split('\n').map((l) => l.trim()).filter(Boolean);
38
+ result.pushError = lines.find((l) => /rejected|fatal|error:/i.test(l)) ?? lines[0] ?? 'git push failed';
39
+ }
40
+ }
41
+ async function commitTrailer(dir, lane, subject, file) {
42
+ await git(dir, 'commit', '-qm', `${subject}\n\nLane: ${lane}`, '--', file);
43
+ }
44
+ /** Writes the claim file, commits it with a `Lane:` trailer and (by default) pushes. */
45
+ export async function runClaim(dir, lane, options = {}) {
46
+ await requireLane(dir, lane);
47
+ const now = options.now ?? new Date();
48
+ const minutes = options.minutes ?? 90;
49
+ if (!Number.isFinite(minutes) || minutes <= 0)
50
+ throw new Error('--minutes must be a positive number.');
51
+ const name = (options.as ?? (await git(dir, 'config', 'user.name').then((r) => r.stdout.trim(), () => ''))).trim();
52
+ if (!name || name.includes('·') || /[\r\n]/.test(name))
53
+ throw new Error('Say who you are with --as <name> (git user.name is not set, or contains "·").');
54
+ const held = activeClaim(await readClaim(dir, lane), now);
55
+ if (held && held.name.toLowerCase() !== name.toLowerCase() && !options.force) {
56
+ throw new Error(`Lane "${lane}" is claimed by ${held.name} until ${held.expiresAt.toISOString()}. Pick another lane (\`loop next\`), wait, or pass --force.`);
57
+ }
58
+ const expiresAt = new Date(now.getTime() + minutes * 60_000);
59
+ const file = path.join(dir, ...claimPath(lane).split('/'));
60
+ await mkdir(path.dirname(file), { recursive: true });
61
+ await writeFile(file, formatClaim(name, expiresAt));
62
+ await git(dir, 'add', '--', claimPath(lane));
63
+ await commitTrailer(dir, lane, `${lane}: claimed by ${name} until ${expiresAt.toISOString()}`, claimPath(lane));
64
+ const result = { lane, name, expiresAt, committed: true };
65
+ await pushIfWanted(dir, result, options.push);
66
+ return result;
67
+ }
68
+ /** Removes the claim file and commits that. A lane without a claim is not an error. */
69
+ export async function runRelease(dir, lane, options = {}) {
70
+ await requireLane(dir, lane);
71
+ const text = await readClaim(dir, lane);
72
+ if (text === undefined)
73
+ return { lane, name: '', committed: false };
74
+ const tracked = (await git(dir, 'ls-files', '--', claimPath(lane))).stdout.trim() !== '';
75
+ await rm(path.join(dir, ...claimPath(lane).split('/')), { force: true });
76
+ if (!tracked)
77
+ return { lane, name: '', committed: false };
78
+ await git(dir, 'add', '--', claimPath(lane));
79
+ await commitTrailer(dir, lane, `${lane}: claim released`, claimPath(lane));
80
+ const result = { lane, name: '', committed: true };
81
+ await pushIfWanted(dir, result, options.push);
82
+ return result;
83
+ }
84
+ //# sourceMappingURL=claim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claim.js","sourceRoot":"","sources":["../../src/cli/claim.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { lintBrief } from '../core/brief.js';
5
+ import { buildDraftPrompt, draftClaudeArgs, extractOpenFindings, extractOutOfScope, nextRoundNumber } from '../core/draftBrief.js';
6
+ import { parseLanesConfig } from '../core/lanes.js';
7
+ import { parseSteer, steerPath, unacknowledged } from '../core/parse/steer.js';
8
+ import { extractNextItems } from '../core/rounds.js';
9
+ import { resolveInvocation } from './run.js';
10
+ export const DRAFT_FILE = '.ai/next-round.draft.md';
11
+ const read = (dir, rel) => readFile(path.join(dir, ...rel.split('/')), 'utf8').catch(() => undefined);
12
+ export const realClaude = async (rawArgs, cwd) => {
13
+ const { command, args } = await resolveInvocation('claude', rawArgs);
14
+ return new Promise((resolve, reject) => {
15
+ execFile(command, args, { cwd, maxBuffer: 16 * 1024 * 1024, timeout: 15 * 60_000 }, (error, stdout) => {
16
+ if (error)
17
+ reject(new Error(`Could not run \`claude -p\`: ${error.message}. Is Claude Code installed, on your PATH and signed in? \`loop next-round --draft --print-prompt\` prints the prompt to paste yourself.`));
18
+ else
19
+ resolve(stdout);
20
+ });
21
+ });
22
+ };
23
+ /** Collects the ingredients: DONE.md next-10 lists, open audit findings, unacknowledged steer, the old out-of-scope. */
24
+ export async function gatherDraftPrompt(dir) {
25
+ const idea = (await read(dir, 'IDEA.md')) ?? '';
26
+ const lanes = parseLanesConfig((await read(dir, '.ai/lanes.json')) ?? '').config?.lanes.map((l) => l.name) ?? [];
27
+ const next = [];
28
+ for (const source of ['DONE.md', ...lanes.map((l) => `.ai/lanes/${l}/DONE.md`)]) {
29
+ const items = extractNextItems((await read(dir, source)) ?? '');
30
+ if (items.length)
31
+ next.push({ source, items });
32
+ }
33
+ const findings = [];
34
+ for (const file of (await readdir(path.join(dir, '.ai', 'audit')).catch(() => [])).filter((f) => f.endsWith('.md')).sort()) {
35
+ findings.push(...extractOpenFindings(`.ai/audit/${file}`, (await read(dir, `.ai/audit/${file}`)) ?? ''));
36
+ }
37
+ const steer = [];
38
+ for (const lane of lanes) {
39
+ const entries = unacknowledged(parseSteer((await read(dir, steerPath(lane))) ?? ''));
40
+ if (entries.length)
41
+ steer.push({ lane, entries });
42
+ }
43
+ const name = path.basename(dir);
44
+ return buildDraftPrompt({ name, round: nextRoundNumber(idea), next, findings, steer, outOfScope: extractOutOfScope(idea) });
45
+ }
46
+ /** The draft goes inside the repo, as a new .md file (only the default draft file may be overwritten). */
47
+ async function assertDraftTarget(dir, outFile, isDefault) {
48
+ const rel = path.relative(path.resolve(dir), outFile);
49
+ if (rel.startsWith('..') || path.isAbsolute(rel) || !/\.md$/i.test(outFile))
50
+ throw new Error(`--out must be a .md file inside ${dir}, not ${outFile}.`);
51
+ if (!isDefault && (await stat(outFile).then(() => true, () => false)))
52
+ throw new Error(`${rel} already exists; pick a new file name for the draft (nothing is overwritten).`);
53
+ }
54
+ /** `loop next-round --draft`: builds the prompt, runs `claude -p` through the usual runner, writes the draft and scores it. */
55
+ export async function runDraft(dir, options = {}) {
56
+ const prompt = await gatherDraftPrompt(dir);
57
+ if (options.printPrompt)
58
+ return { prompt };
59
+ const draft = (await (options.claude ?? realClaude)(draftClaudeArgs(prompt, options.model), dir)).trim();
60
+ if (!draft)
61
+ throw new Error('Claude returned an empty draft. Run again, or use --print-prompt and paste the prompt into Claude Code yourself.');
62
+ const rel = options.out ?? DRAFT_FILE;
63
+ const outFile = path.resolve(dir, rel);
64
+ await assertDraftTarget(dir, outFile, rel === DRAFT_FILE);
65
+ await mkdir(path.dirname(outFile), { recursive: true });
66
+ await writeFile(outFile, `${draft}\n`);
67
+ return { prompt, draft, outFile, lint: lintBrief(draft) };
68
+ }
69
+ //# sourceMappingURL=draft.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft.js","sourceRoot":"","sources":["../../src/cli/draft.ts"],"names":[],"mappings":"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"}
package/dist/cli/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { Command } from 'commander';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { runClaim, runRelease } from './claim.js';
5
+ import { runNext } from './next.js';
6
+ import { formatNext } from '../core/next.js';
4
7
  import { formatLaneCheck, runCheckLanes } from './checkLanes.js';
5
8
  import { runDoctor } from './doctor.js';
6
9
  import { runAddLanes } from './lanesInit.js';
@@ -9,8 +12,15 @@ import { DEFAULT_PORT, runDashboard } from './dashboard.js';
9
12
  import { realGh } from './gh.js';
10
13
  import { runInit } from './init.js';
11
14
  import { runAnswer, runNextRound, runStatus } from './loops.js';
15
+ import { formatLaneRound, runLaneRound } from './laneRound.js';
12
16
  import { runNew } from './new.js';
17
+ import { formatShipCheck, runShipCheck } from './shipCheck.js';
18
+ import { formatRuns, logRun, readRuns } from './runs.js';
19
+ import { runPause, runResume } from './pause.js';
13
20
  import { runRound } from './run.js';
21
+ import { runDraft } from './draft.js';
22
+ import { formatPaceReport, runPaceSet, runPaceShow } from './pace.js';
23
+ import { formatScheduleReport, runScheduleAction, runScheduleShow, runScheduleUpdate, runSteer } from './schedule.js';
14
24
  const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
15
25
  // Commands are added by the core lane: init, new, run, status, next-round, answer, dashboard.
16
26
  export const program = new Command('loop')
@@ -117,11 +127,31 @@ program
117
127
  });
118
128
  program
119
129
  .command('next-round')
120
- .description('After DONE.md: archive it as .ai/done-vN.md and write the next round\'s task.md from its "next 10" list')
130
+ .description('After DONE.md: archive it as .ai/done-vN.md and write the next round\'s task.md from its "next 10" list. With --brief in a laned repo: put the brief first in IDEA.md, archive every DONE.md, reset each lane\'s task file')
121
131
  .argument('[dir]', 'loop folder', '.')
122
132
  .option('--dry-run', 'report what would happen without writing')
133
+ .option('--brief <file>', 'laned repos: the next round\'s brief (scored first)')
134
+ .option('--no-commit', 'with --brief: write the changes but do not commit')
135
+ .option('--json', 'with --brief: print the plan as JSON (shape: RoundPlan in src/core/laneRound.ts)')
136
+ .option('--draft', 'ask Claude (claude -p) to draft the next brief from the DONE.md lists, open audit findings and steering; writes it and prints its score')
137
+ .option('--out <file>', 'with --draft: where the draft goes (default .ai/next-round.draft.md)')
138
+ .option('--print-prompt', 'with --draft: print the prompt only, to paste into Claude Code yourself')
139
+ .option('--model <model>', 'with --draft: the Claude model to use')
123
140
  .action(async (dir, options) => {
124
141
  try {
142
+ if (options.draft || options.printPrompt) {
143
+ const r = await runDraft(path.resolve(dir), { ...(options.printPrompt && { printPrompt: true }), ...(options.out && { out: options.out }), ...(options.model && { model: options.model }) });
144
+ if (!r.draft)
145
+ console.log(r.prompt);
146
+ else
147
+ console.log(`Draft written to ${path.relative(process.cwd(), r.outFile) || r.outFile}. Brief score ${r.lint?.score}/100${r.lint?.gaps.length ? `; missing: ${r.lint.gaps.map((g) => g.message).join(' ')}` : ''}\nReview it, then: loop next-round --brief ${options.out ?? '.ai/next-round.draft.md'}`);
148
+ return;
149
+ }
150
+ if (options.brief) {
151
+ const result = await runLaneRound(path.resolve(dir), { briefFile: options.brief, dryRun: Boolean(options.dryRun), commit: options.commit });
152
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatLaneRound(result, Boolean(options.dryRun)));
153
+ return;
154
+ }
125
155
  const result = await runNextRound(path.resolve(dir), { dryRun: Boolean(options.dryRun) });
126
156
  console.log(`${options.dryRun ? 'Would archive' : 'Archived'} DONE.md as .ai/${result.archivedAs}; next round has ${result.units} units.`);
127
157
  }
@@ -159,11 +189,13 @@ program
159
189
  .option('--skip-permissions', 'pass --dangerously-skip-permissions to claude (you accept the risk)')
160
190
  .option('--model <model>', 'model for this round')
161
191
  .option('--lane <name>', 'run as this lane from .ai/lanes.json (own task file, outbox and session lock)')
192
+ .option('--as <name>', 'your name for lane claims: a claim by this name does not stop the run')
162
193
  .action(async (dir, options) => {
163
194
  try {
164
195
  const result = await runRound(path.resolve(dir), {
165
196
  dryRun: Boolean(options.dryRun), skipPermissions: Boolean(options.skipPermissions), ...(options.model && { model: options.model }),
166
197
  ...(options.lane && { lane: options.lane }),
198
+ ...(options.as && { as: options.as }),
167
199
  });
168
200
  if (result.skipped)
169
201
  console.log(result.skipped);
@@ -224,5 +256,256 @@ program
224
256
  fail(error);
225
257
  }
226
258
  });
259
+ const pushNote = (r) => (r.pushed === false ? `\nCommitted, but the push failed: ${r.pushError}\nFix that (for a rejected push: \`git pull --rebase && git push\`) so teammates see it.` : '');
260
+ program
261
+ .command('claim')
262
+ .description('Tell teammates you are working a lane: writes .ai/lanes/<lane>/claim, commits and pushes it')
263
+ .argument('<lane>', 'lane name from .ai/lanes.json')
264
+ .option('--as <name>', 'your name (default: git config user.name)')
265
+ .option('--minutes <n>', 'how long the claim lasts', '90')
266
+ .option('--force', 'take over a lane someone else has claimed')
267
+ .option('--no-push', 'commit but do not push')
268
+ .option('--dir <dir>', 'repo folder', '.')
269
+ .action(async (lane, options) => {
270
+ try {
271
+ const r = await runClaim(path.resolve(options.dir), lane, { ...(options.as && { as: options.as }), minutes: Number(options.minutes), force: Boolean(options.force), push: options.push });
272
+ console.log(`Claimed ${r.lane} as ${r.name} until ${r.expiresAt?.toISOString()}.${pushNote(r)}`);
273
+ if (r.pushed === false)
274
+ process.exitCode = 1;
275
+ }
276
+ catch (error) {
277
+ fail(error);
278
+ }
279
+ });
280
+ program
281
+ .command('release')
282
+ .description('Remove your claim on a lane (commits and pushes)')
283
+ .argument('<lane>', 'lane name from .ai/lanes.json')
284
+ .option('--no-push', 'commit but do not push')
285
+ .option('--dir <dir>', 'repo folder', '.')
286
+ .action(async (lane, options) => {
287
+ try {
288
+ const r = await runRelease(path.resolve(options.dir), lane, { push: options.push });
289
+ console.log(r.committed ? `Released ${lane}.${pushNote(r)}` : `Lane ${lane} had no committed claim; nothing to release.`);
290
+ if (r.pushed === false)
291
+ process.exitCode = 1;
292
+ }
293
+ catch (error) {
294
+ fail(error);
295
+ }
296
+ });
297
+ program
298
+ .command('next')
299
+ .description('List open units across all lanes with their claims, and recommend one nobody has claimed')
300
+ .argument('[dir]', 'repo folder', '.')
301
+ .option('--json', 'print the report as JSON (shape: NextReport in src/core/types.ts)')
302
+ .action(async (dir, options) => {
303
+ try {
304
+ const report = await runNext(path.resolve(dir));
305
+ console.log(options.json ? JSON.stringify(report, null, 2) : formatNext(report));
306
+ }
307
+ catch (error) {
308
+ fail(error);
309
+ }
310
+ });
311
+ const pauseNote = (r) => (r.pushed === false ? `\nCommitted, but the push failed: ${r.pushError}\nFix that (for a rejected push: \`git pull --rebase && git push\`) so the lanes see it.` : '');
312
+ program
313
+ .command('pause')
314
+ .description('Stop every lane run and the control room: writes .ai/PAUSED, commits (Lane: control) and pushes')
315
+ .option('--reason <text>', 'why (shown on the dashboard)')
316
+ .option('--as <name>', 'your name (default: git config user.name)')
317
+ .option('--no-push', 'commit but do not push')
318
+ .option('--dir <dir>', 'repo folder', '.')
319
+ .action(async (options) => {
320
+ try {
321
+ const r = await runPause(path.resolve(options.dir), { ...(options.reason && { reason: options.reason }), ...(options.as && { as: options.as }), push: options.push });
322
+ console.log(r.changed ? `Paused. Scheduled runs now stop at their first step; \`loop resume\` undoes it.${pauseNote(r)}` : `Already paused by ${r.by}.`);
323
+ if (r.pushed === false)
324
+ process.exitCode = 1;
325
+ }
326
+ catch (error) {
327
+ fail(error);
328
+ }
329
+ });
330
+ program
331
+ .command('resume')
332
+ .description('Undo `loop pause`: removes .ai/PAUSED, commits (Lane: control) and pushes')
333
+ .option('--no-push', 'commit but do not push')
334
+ .option('--dir <dir>', 'repo folder', '.')
335
+ .action(async (options) => {
336
+ try {
337
+ const r = await runResume(path.resolve(options.dir), { push: options.push });
338
+ console.log(r.changed ? `Resumed.${pauseNote(r)}` : 'Not paused; nothing to do.');
339
+ if (r.pushed === false)
340
+ process.exitCode = 1;
341
+ }
342
+ catch (error) {
343
+ fail(error);
344
+ }
345
+ });
346
+ const scheduleNote = (r) => (r.pushed === false ? `\nCommitted, but the push failed: ${r.pushError}` : '');
347
+ const scheduleCmd = program.command('schedule').description('Which lanes are due (.ai/schedule.json): show, pause, resume, snooze, wake, update');
348
+ const scheduleOpts = (c) => c.option('--no-commit', 'write the file but do not commit').option('--no-push', 'commit but do not push').option('--dir <dir>', 'repo folder', '.');
349
+ scheduleCmd
350
+ .command('show')
351
+ .description('Each lane: due, paused, sleeping or snoozed, with idle runs')
352
+ .option('--json', 'print the report as JSON (shape: ScheduleReport in src/cli/schedule.ts)')
353
+ .option('--dir <dir>', 'repo folder', '.')
354
+ .action(async (options) => {
355
+ try {
356
+ const report = await runScheduleShow(path.resolve(options.dir));
357
+ console.log(options.json ? JSON.stringify(report, null, 2) : formatScheduleReport(report));
358
+ }
359
+ catch (error) {
360
+ fail(error);
361
+ }
362
+ });
363
+ const simple = (name, args, description, build, withReason) => {
364
+ const c = scheduleOpts(scheduleCmd.command(`${name} ${args}`).description(description));
365
+ if (withReason)
366
+ c.option('--reason <text>', 'why');
367
+ c.action(async (...argv) => {
368
+ const opts = argv[argv.length - 2];
369
+ const positional = argv.slice(0, -2);
370
+ try {
371
+ const r = await runScheduleAction(path.resolve(opts.dir), build(positional, opts), { commit: opts.commit, push: opts.push });
372
+ console.log(r.changed.length ? `${name}: ${r.changed.join(', ')}${scheduleNote(r)}` : `${name}: nothing to change.`);
373
+ if (r.pushed === false)
374
+ process.exitCode = 1;
375
+ }
376
+ catch (error) {
377
+ fail(error);
378
+ }
379
+ });
380
+ };
381
+ simple('pause', '<lane>', 'Hold a lane back until it is resumed', ([lane], o) => ({ action: 'pause', lane: lane, ...(o.reason && { reason: o.reason }) }), true);
382
+ simple('resume', '<lane>', 'Undo `schedule pause`', ([lane]) => ({ action: 'resume', lane: lane }), false);
383
+ simple('snooze', '<lane> <until>', 'Hold a lane back until a time (ISO, a date, or 3h / 2d)', ([lane, until], o) => ({ action: 'snooze', lane: lane, until: until, ...(o.reason && { reason: o.reason }) }), true);
384
+ simple('wake', '<lane|all>', 'Make a sleeping or snoozed lane due now', ([lane]) => ({ action: 'wake', lane: lane }), false);
385
+ scheduleOpts(scheduleCmd.command('update').description('Idle backoff from the run logs, and wake lanes that have steering, a message or a new round'))
386
+ .option('--new-round', 'a new round started: wake every lane')
387
+ .option('--lane <name>', 'change only this lane\'s entry and commit as that lane (the end-of-run backoff)')
388
+ .action(async (options) => {
389
+ try {
390
+ const r = await runScheduleUpdate(path.resolve(options.dir), { commit: options.commit, push: options.push, ...(options.newRound && { newRound: true }), ...(options.lane && { lane: options.lane }) });
391
+ console.log(r.changed.length ? `update: ${r.changed.join(', ')}${scheduleNote(r)}` : 'update: nothing to change.');
392
+ if (r.pushed === false)
393
+ process.exitCode = 1;
394
+ }
395
+ catch (error) {
396
+ fail(error);
397
+ }
398
+ });
399
+ const paceCmd = program.command('pace').description('How much checking a run does (.ai/policy.json): show, set');
400
+ paceCmd
401
+ .command('show')
402
+ .description('The global pace and any lane overrides, with what each skips')
403
+ .option('--json', 'print the report as JSON (shape: PaceReport in src/cli/pace.ts)')
404
+ .option('--dir <dir>', 'repo folder', '.')
405
+ .action(async (options) => {
406
+ try {
407
+ const report = await runPaceShow(path.resolve(options.dir));
408
+ console.log(options.json ? JSON.stringify(report, null, 2) : formatPaceReport(report));
409
+ }
410
+ catch (error) {
411
+ fail(error);
412
+ }
413
+ });
414
+ scheduleOpts(paceCmd.command('set').description('Set the pace: careful, balanced, fast or sprint (commits with Lane: control)'))
415
+ .argument('<preset>', 'careful | balanced | fast | sprint')
416
+ .option('--lane <name>', 'override only this lane')
417
+ .action(async (preset, options) => {
418
+ try {
419
+ const r = await runPaceSet(path.resolve(options.dir), preset, { commit: options.commit, push: options.push, ...(options.lane && { lane: options.lane }) });
420
+ console.log(r.changed.length ? `pace: ${preset}${options.lane ? ` for ${options.lane}` : ''}${scheduleNote(r)}` : 'pace: nothing to change.');
421
+ if (r.pushed === false)
422
+ process.exitCode = 1;
423
+ }
424
+ catch (error) {
425
+ fail(error);
426
+ }
427
+ });
428
+ program
429
+ .command('steer')
430
+ .description('Leave a note for a lane in .ai/lanes/<lane>/steer.md, wake the lane and commit (Lane: control)')
431
+ .argument('<lane>', 'lane name')
432
+ .argument('<text>', 'what the lane should do')
433
+ .option('--standing', 'applies to every run until removed (default: the next run only)')
434
+ .option('--author <name>', 'your name (default: git config user.name)')
435
+ .option('--no-commit', 'write the file but do not commit')
436
+ .option('--no-push', 'commit but do not push')
437
+ .option('--dir <dir>', 'repo folder', '.')
438
+ .action(async (lane, text, options) => {
439
+ try {
440
+ const r = await runSteer(path.resolve(options.dir), lane, text, { commit: options.commit, push: options.push, ...(options.standing && { standing: true }), ...(options.author && { author: options.author }) });
441
+ console.log(`Steer saved for ${lane}${options.standing ? ' (standing)' : ''}.${scheduleNote(r)}`);
442
+ if (r.pushed === false)
443
+ process.exitCode = 1;
444
+ }
445
+ catch (error) {
446
+ fail(error);
447
+ }
448
+ });
449
+ program
450
+ .command('ship-check')
451
+ .description('Is this repo ready to release? Checks DONE.md, open findings, CHANGELOG, the tag, a clean tree and the latest test result; exit 0 only if ready')
452
+ .argument('[dir]', 'repo folder', '.')
453
+ .option('--json', 'print the checklist as JSON (shape: ShipCheck in src/core/shipCheck.ts)')
454
+ .action(async (dir, options) => {
455
+ try {
456
+ const result = await runShipCheck(path.resolve(dir));
457
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatShipCheck(result));
458
+ if (!result.ready)
459
+ process.exitCode = 1;
460
+ }
461
+ catch (error) {
462
+ fail(error);
463
+ }
464
+ });
465
+ const runsCommand = program
466
+ .command('runs')
467
+ .description('List each lane\'s logged runs and the scheduled runs that left no trace (from .ai/lanes/<lane>/runs.jsonl and each lane\'s cron)')
468
+ .option('--json', 'print the report as JSON (shape: RunsReport in src/cli/runs.ts)')
469
+ .option('--hours <n>', 'how far back to look', '24')
470
+ .option('--dir <dir>', 'repo folder', '.')
471
+ .action(async (options) => {
472
+ try {
473
+ const report = await readRuns(path.resolve(options.dir), { hours: Number(options.hours) });
474
+ console.log(options.json ? JSON.stringify(report, null, 2) : formatRuns(report));
475
+ }
476
+ catch (error) {
477
+ fail(error);
478
+ }
479
+ });
480
+ runsCommand
481
+ .command('log')
482
+ .description('Append one line to a lane\'s run log; every lane run ends with this and commits the file with its last commit')
483
+ .requiredOption('--lane <name>', 'lane name from .ai/lanes.json')
484
+ .option('--units <list...>', 'units finished this run')
485
+ .option('--passed <n>', 'tests passed on the last npm test')
486
+ .option('--failed <n>', 'tests failed on the last npm test')
487
+ .option('--pushed', 'this run pushed to main')
488
+ .option('--no-pushed', 'this run pushed nothing')
489
+ .option('--session <url>', 'session link or id')
490
+ .option('--note <text>', 'one line of context')
491
+ .option('--dir <dir>', 'repo folder', '.')
492
+ .action(async (options) => {
493
+ try {
494
+ // The parent `runs` command declares --dir too and Commander gives it the value wherever it appears.
495
+ const dir = options.dir !== '.' ? options.dir : (runsCommand.opts().dir);
496
+ const entry = await logRun(path.resolve(dir), options.lane, {
497
+ ...(options.units && { units: options.units }),
498
+ ...(options.passed !== undefined && { passed: Number(options.passed) }),
499
+ ...(options.failed !== undefined && { failed: Number(options.failed) }),
500
+ pushed: options.pushed ?? false,
501
+ ...(options.session && { session: options.session }),
502
+ ...(options.note && { note: options.note }),
503
+ });
504
+ console.log(`Logged ${entry.lane} run at ${entry.at} (${entry.pushed ? 'pushed' : 'pushed nothing'}). Commit .ai/lanes/${entry.lane}/runs.jsonl with your last commit.`);
505
+ }
506
+ catch (error) {
507
+ fail(error);
508
+ }
509
+ });
227
510
  await program.parseAsync();
228
511
  //# sourceMappingURL=index.js.map