compounding-loop 0.1.0 → 0.4.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/README.md +40 -6
- package/dist/cli/checkLanes.js +60 -0
- package/dist/cli/checkLanes.js.map +1 -0
- package/dist/cli/claim.js +84 -0
- package/dist/cli/claim.js.map +1 -0
- package/dist/cli/doctor.js +54 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/index.js +253 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +2 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/laneRound.js +96 -0
- package/dist/cli/laneRound.js.map +1 -0
- package/dist/cli/laneStatus.js +54 -0
- package/dist/cli/laneStatus.js.map +1 -0
- package/dist/cli/lanesInit.js +56 -0
- package/dist/cli/lanesInit.js.map +1 -0
- package/dist/cli/lock.js +62 -0
- package/dist/cli/lock.js.map +1 -0
- package/dist/cli/loops.js +30 -3
- package/dist/cli/loops.js.map +1 -1
- package/dist/cli/new.js +5 -5
- package/dist/cli/new.js.map +1 -1
- package/dist/cli/next.js +18 -0
- package/dist/cli/next.js.map +1 -0
- package/dist/cli/pause.js +81 -0
- package/dist/cli/pause.js.map +1 -0
- package/dist/cli/run.js +119 -19
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/runs.js +83 -0
- package/dist/cli/runs.js.map +1 -0
- package/dist/cli/shipCheck.js +59 -0
- package/dist/cli/shipCheck.js.map +1 -0
- package/dist/core/doctor.js +49 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/laneRound.js +63 -0
- package/dist/core/laneRound.js.map +1 -0
- package/dist/core/laneStatus.js +82 -0
- package/dist/core/laneStatus.js.map +1 -0
- package/dist/core/lanes.js +149 -0
- package/dist/core/lanes.js.map +1 -0
- package/dist/core/lanesInit.js +143 -0
- package/dist/core/lanesInit.js.map +1 -0
- package/dist/core/next.js +34 -0
- package/dist/core/next.js.map +1 -0
- package/dist/core/parse/changelog.js +41 -0
- package/dist/core/parse/changelog.js.map +1 -0
- package/dist/core/parse/claim.js +24 -0
- package/dist/core/parse/claim.js.map +1 -0
- package/dist/core/parse/controlRoom.js +38 -0
- package/dist/core/parse/controlRoom.js.map +1 -0
- package/dist/core/parse/index.js +12 -0
- package/dist/core/parse/index.js.map +1 -0
- package/dist/core/parse/markers.js +20 -0
- package/dist/core/parse/markers.js.map +1 -0
- package/dist/core/parse/paused.js +26 -0
- package/dist/core/parse/paused.js.map +1 -0
- package/dist/core/parse/runs.js +132 -0
- package/dist/core/parse/runs.js.map +1 -0
- package/dist/core/parse/task.js +96 -0
- package/dist/core/parse/task.js.map +1 -0
- package/dist/core/repo.js +2 -7
- package/dist/core/repo.js.map +1 -1
- package/dist/core/runPrompt.js +9 -3
- package/dist/core/runPrompt.js.map +1 -1
- package/dist/core/shipCheck.js +45 -0
- package/dist/core/shipCheck.js.map +1 -0
- package/dist/core/table.js +22 -1
- package/dist/core/table.js.map +1 -1
- package/dist/server/checkLabels.js +27 -0
- package/dist/server/checkLabels.js.map +1 -0
- package/dist/server/controls.js +72 -0
- package/dist/server/controls.js.map +1 -0
- package/dist/server/data.js +28 -8
- package/dist/server/data.js.map +1 -1
- package/dist/server/events.js +1 -0
- package/dist/server/events.js.map +1 -1
- package/dist/server/health.js +47 -8
- package/dist/server/health.js.map +1 -1
- package/dist/server/index.js +29 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/laneChecks.js +25 -0
- package/dist/server/laneChecks.js.map +1 -0
- package/dist/server/live/build.js +163 -0
- package/dist/server/live/build.js.map +1 -0
- package/dist/server/live/github.js +54 -0
- package/dist/server/live/github.js.map +1 -0
- package/dist/server/live/index.js +9 -0
- package/dist/server/live/index.js.map +1 -0
- package/dist/server/live/merge.js +16 -0
- package/dist/server/live/merge.js.map +1 -0
- package/dist/server/live/urls.js +59 -0
- package/dist/server/live/urls.js.map +1 -0
- package/dist/server/loops.js +146 -39
- package/dist/server/loops.js.map +1 -1
- package/dist/server/roundState.js +27 -0
- package/dist/server/roundState.js.map +1 -0
- package/dist/server/runs.js +81 -0
- package/dist/server/runs.js.map +1 -0
- package/dist/server/static-export.js +22 -1
- package/dist/server/static-export.js.map +1 -1
- package/dist/server/taskBullets.js +33 -0
- package/dist/server/taskBullets.js.map +1 -0
- package/dist/server/this-repo.json +1574 -0
- package/dist/web/assets/index-BOPytDNU.js +53 -0
- package/dist/web/assets/index-BSHEI7fa.css +2 -0
- package/dist/web/index.html +3 -3
- package/method/COMPOUNDING_LOOP.md +55 -0
- package/method/example/K-0002.md +16 -0
- package/method/example/K-0003.md +16 -0
- package/method/retro.md +7 -0
- package/package.json +6 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/commands/loop-lanes.md +11 -0
- package/plugin/commands/loop-next.md +11 -0
- package/plugin/commands/loop-pause.md +10 -0
- package/runners/github-actions/lanes.yml +156 -0
- package/runners/local/README.md +4 -1
- package/runners/local/compounding-loop.service +20 -0
- package/runners/local/compounding-loop.timer +11 -0
- package/runners/local/launchd.plist +36 -0
- package/runners/routine/README.md +38 -1
- package/runners/routine/control-room-prompt.md +16 -0
- package/runners/routine/lane-prompt.md +12 -0
- package/runners/routine/prompt.md +3 -3
- package/templates/chrome-extension/extension/icons/icon128.png +0 -0
- package/templates/chrome-extension/extension/icons/icon16.png +0 -0
- package/templates/chrome-extension/extension/icons/icon48.png +0 -0
- package/templates/chrome-extension/extension/manifest.json +19 -3
- package/templates/chrome-extension/gitignore +1 -1
- package/templates/lanes/README.md +23 -0
- package/templates/lanes/ci-check-lanes.yml +38 -0
- package/templates/lanes/claude-section.md +20 -0
- package/templates/lanes/control-room.md +3 -0
- package/templates/lanes/outbox.md +5 -0
- package/templates/lanes/task.md +36 -0
- package/templates/static-site/.ai/loop-prompt.md +2 -2
- package/templates/static-site/CLAUDE.md +2 -0
- package/templates/static-site/gitignore +1 -1
- package/dist/web/assets/index-BDdtnG33.js +0 -49
- package/dist/web/assets/index-D3-N8DN4.css +0 -2
package/README.md
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
# Compounding Loop
|
|
2
2
|
|
|
3
|
-
**Write the brief, check back tomorrow.**
|
|
3
|
+
**Write the brief, check back tomorrow.** Several unattended Claude Code lanes, one repo, each lane owning its own paths so they do not step on each other, every round verified by your own tests. Local dashboard included.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npx compounding-loop init
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
](https://www.npmjs.com/package/compounding-loop) [](https://github.com/Minntooos/Compounding-Loop/actions/workflows/ci.yml) [](LICENSE)
|
|
10
|
+
|
|
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. Working with a teammate? [Five steps](docs/team.md).
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
**Lanes:** each lane is a scheduled run that owns a set of paths (`.ai/lanes.json`). `loop check-lanes` fails any commit that touches files outside its lane, so parallel runs do not collide. This repo is the proof: five lanes (core, kit, server, web, docs) built and documented it by pushing to one `main`. See [Lanes](docs/lanes.md).
|
|
10
18
|
|
|
11
19
|
Most agent tools show a demo. This one ships with proof: the method in this repo built five live sites, 215 pages in all, with nobody watching, and made **verification** the product. A round only counts when the repo's own tests pass and a reviewer that did not write the code has checked the diff.
|
|
12
20
|
|
|
@@ -57,6 +65,10 @@ npx compounding-loop run # one round now
|
|
|
57
65
|
npx compounding-loop dashboard # the local dashboard
|
|
58
66
|
```
|
|
59
67
|
|
|
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). Sharing a loop with a teammate: [Work on a loop with a teammate](docs/team.md).
|
|
69
|
+
|
|
70
|
+
**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).
|
|
71
|
+
|
|
60
72
|
Already have a repo? `npx compounding-loop init` adds the method to it without overwriting anything. Details: [Getting started](docs/getting-started.md).
|
|
61
73
|
|
|
62
74
|
## Just loop the agent?
|
|
@@ -75,17 +87,39 @@ An honest comparison with `while true; do claude -p "keep going"; done`:
|
|
|
75
87
|
|
|
76
88
|
If your task is one afternoon of work and you are at the keyboard, a plain loop is fine. This pays off for work that runs for days.
|
|
77
89
|
|
|
90
|
+
## How it differs from other tools
|
|
91
|
+
|
|
92
|
+
These tools solve a neighbouring problem: running several agents side by side while you watch. Compounding Loop is for work that runs unattended and has to prove itself. Descriptions come from each project's own page (checked 2026-10-08).
|
|
93
|
+
|
|
94
|
+
| Tool | What it is for | Where Compounding Loop differs |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| [Claude Squad](https://github.com/smtg-ai/claude-squad) | A terminal app that manages several agents, each in its own tmux session and git worktree; an experimental `-y` flag auto-accepts prompts. | Squad isolates work by branch. We isolate by path ownership on one `main`, and a round only counts when your tests and a reviewer pass. |
|
|
97
|
+
| [Parallel Code](https://github.com/johannesjo/parallel-code) | Runs several agents at once, each in its own git worktree; you merge the branch back yourself. | Interactive and merge-driven. We are scheduled, and the agent never marks itself done. |
|
|
98
|
+
| [Agent Orchestrator](https://github.com/ComposioHQ/agent-orchestrator) | A local desktop workspace where an orchestrator delegates work to agents on separate branches and worktrees, shown on a Kanban board. | A desktop planner. We are a plain-file method (task file, notes, handoffs) you can read in git. |
|
|
99
|
+
| [Vibe Kanban](https://github.com/BloopAI/vibe-kanban) | Plan issues on a kanban board, then run coding agents in workspaces with a branch and a terminal. Its page says it is sunsetting. | We have no board; the contract is a file of checkable commands. |
|
|
100
|
+
| [Claude Code agent teams](https://code.claude.com/docs/en/agent-teams) | Experimental, off by default: a lead session coordinates teammates with a shared task list and messages. Needs an interactive session (not `claude -p`). | Teams are live and interactive. Lanes are scheduled, unattended and leave a git trail. They can be combined. |
|
|
101
|
+
|
|
102
|
+
If you work at the keyboard and want to watch agents in parallel, those tools are a good fit.
|
|
103
|
+
|
|
78
104
|
## Docs
|
|
79
105
|
|
|
80
|
-
[Getting started](docs/getting-started.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)
|
|
106
|
+
[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) · [Release](docs/release.md) · [Templates](docs/templates.md) · [FAQ](docs/faq.md) · [Troubleshooting](docs/troubleshooting.md) · [Contributing](CONTRIBUTING.md)
|
|
81
107
|
|
|
82
108
|
## FAQ
|
|
83
109
|
|
|
84
|
-
**
|
|
110
|
+
**Isn't this cron + `claude -p`?** Partly, and the table above shows what is added: your checks decide when a round is done, a fresh reviewer attacks the diff, each run writes a handoff and notes the next run reads, a run budget stops runaway loops, and lanes keep parallel runs out of each other's files.
|
|
111
|
+
|
|
112
|
+
**What does it cost (plan usage vs API)?** The tool is free and MIT licensed. Runs use whatever your Claude Code login uses: plan usage on a subscription, or API billing if you authenticate with a key. The `Run: N / LIMIT` budget caps how many runs happen. More lanes means more runs, so cost scales with lanes times runs.
|
|
113
|
+
|
|
114
|
+
**Why not agent teams?** Agent teams are experimental, off by default and need an interactive session ([docs](https://code.claude.com/docs/en/agent-teams)). Lanes are for scheduled runs nobody is watching. Use teams when you are at the keyboard; use lanes when you are away.
|
|
115
|
+
|
|
116
|
+
**What happens when it goes wrong?** A red round is discarded and logged under Tried. The same failure twice triggers the stuck protocol, then a `BLOCKED.md` appears in your dashboard inbox. When the run budget is reached the lane stops. Everything is a git commit, so it is reversible. See [Troubleshooting](docs/troubleshooting.md).
|
|
117
|
+
|
|
118
|
+
**Is my code sent anywhere?** Only what Claude Code and git already send. Compounding Loop has no telemetry and no call home, the dashboard binds to loopback, and tokens come from your environment or `gh` and are never stored. See [SECURITY.md](SECURITY.md).
|
|
85
119
|
|
|
86
|
-
**
|
|
120
|
+
**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.
|
|
87
121
|
|
|
88
|
-
**
|
|
122
|
+
**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.
|
|
89
123
|
|
|
90
124
|
**Does it need a GitHub login?** Only to create a private repo with `loop new`; `--dry-run` and every test work offline.
|
|
91
125
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { checkCommitFiles, parseLaneTrailer, parseLanesConfig } from '../core/lanes.js';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
// core.quotepath=false keeps non-ASCII names readable in git's output.
|
|
8
|
+
const git = async (cwd, ...args) => (await execFileAsync('git', ['-c', 'core.quotepath=false', ...args], { cwd, maxBuffer: 64 * 1024 * 1024 })).stdout;
|
|
9
|
+
const RECORD = '\u001e';
|
|
10
|
+
/** Checks every commit in `range` (default: the last 50 commits) against `.ai/lanes.json`. */
|
|
11
|
+
export async function runCheckLanes(dir, range) {
|
|
12
|
+
const text = await readFile(path.join(dir, '.ai', 'lanes.json'), 'utf8').catch(() => undefined);
|
|
13
|
+
if (text === undefined)
|
|
14
|
+
throw new Error('No .ai/lanes.json here. Create one with `loop init --lanes a,b,c`.');
|
|
15
|
+
const { config, errors } = parseLanesConfig(text);
|
|
16
|
+
if (!config)
|
|
17
|
+
throw new Error(`.ai/lanes.json is invalid:\n- ${errors.join('\n- ')}`);
|
|
18
|
+
if (!range && !(await git(dir, 'rev-parse', '--verify', '--quiet', 'HEAD').then(() => true, () => false))) {
|
|
19
|
+
return { checked: 0, untagged: [], skipped: [], problems: [] };
|
|
20
|
+
}
|
|
21
|
+
const log = await git(dir, 'log', '--no-merges', `--format=${RECORD}%H%n%B`, ...(range ? [range] : ['-n', '50'])).catch((error) => {
|
|
22
|
+
throw new Error(`git could not read ${range ?? 'the history'}: ${error instanceof Error ? error.message.split('\n')[0] : String(error)}. Check the range (for example origin/main..HEAD).`);
|
|
23
|
+
});
|
|
24
|
+
const result = { checked: 0, untagged: [], skipped: [], problems: [] };
|
|
25
|
+
const shallow = (await git(dir, 'rev-parse', '--is-shallow-repository')).trim() === 'true';
|
|
26
|
+
for (const record of log.split(RECORD).filter((r) => r.trim() !== '')) {
|
|
27
|
+
const [hash = '', ...body] = record.split('\n');
|
|
28
|
+
const short = hash.slice(0, 7);
|
|
29
|
+
result.checked++;
|
|
30
|
+
const lane = parseLaneTrailer(body.join('\n'));
|
|
31
|
+
if (!lane) {
|
|
32
|
+
result.untagged.push(short);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// Without its parent, a shallow clone's oldest commit would look like it added every file.
|
|
36
|
+
if (shallow && (await git(dir, 'rev-list', '--parents', '-n', '1', hash)).trim().split(' ').length === 1) {
|
|
37
|
+
result.skipped.push(short);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const files = (await git(dir, 'diff-tree', '-z', '--no-commit-id', '--name-only', '-r', '--root', hash)).split('\0').filter(Boolean);
|
|
41
|
+
for (const violation of checkCommitFiles(config, lane, files))
|
|
42
|
+
result.problems.push(`${short} (Lane: ${lane}): ${violation.message}`);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
export function formatLaneCheck(result) {
|
|
47
|
+
const lines = [`Checked ${result.checked} commit(s).`];
|
|
48
|
+
if (result.checked === 0)
|
|
49
|
+
lines.push('Nothing to check: no commits in the range (or no commits yet). If you passed --range, is it right (for example origin/main..HEAD)?');
|
|
50
|
+
if (result.skipped.length > 0)
|
|
51
|
+
lines.push(`${result.skipped.length} skipped because this is a shallow clone (${result.skipped.join(', ')}). Use fetch-depth: 0 to check them.`);
|
|
52
|
+
if (result.untagged.length > 0)
|
|
53
|
+
lines.push(`${result.untagged.length} without a Lane: trailer (not an error): ${result.untagged.join(', ')}`);
|
|
54
|
+
if (result.problems.length === 0)
|
|
55
|
+
lines.push('All lane-tagged commits stayed in their lane.');
|
|
56
|
+
else
|
|
57
|
+
lines.push(...result.problems.map((p) => `FAIL ${p}`));
|
|
58
|
+
return lines.join('\n');
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=checkLanes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkLanes.js","sourceRoot":"","sources":["../../src/cli/checkLanes.ts"],"names":[],"mappings":"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"}
|
|
@@ -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,54 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readdir } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { doctorPassed, evaluateDoctor, formatDoctor } from '../core/doctor.js';
|
|
7
|
+
import { resolveInvocation } from './run.js';
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
/** Runs `command args` and reports success plus its first output; never throws. */
|
|
10
|
+
export async function probe(command, args) {
|
|
11
|
+
try {
|
|
12
|
+
const invocation = await resolveInvocation(command, args);
|
|
13
|
+
const { stdout, stderr } = await execFileAsync(invocation.command, invocation.args, { timeout: 15_000 });
|
|
14
|
+
return { ok: true, output: stdout.trim() || stderr.trim() };
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
return { ok: false, output: error instanceof Error ? error.message : String(error) };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Folders where Playwright keeps downloaded browsers, most specific first. */
|
|
21
|
+
export function playwrightCacheDirs(env, platform, home) {
|
|
22
|
+
const custom = env.PLAYWRIGHT_BROWSERS_PATH;
|
|
23
|
+
if (custom && custom !== '0')
|
|
24
|
+
return [custom];
|
|
25
|
+
if (platform === 'win32')
|
|
26
|
+
return [path.win32.join(env.LOCALAPPDATA ?? path.win32.join(home, 'AppData', 'Local'), 'ms-playwright')];
|
|
27
|
+
if (platform === 'darwin')
|
|
28
|
+
return [path.posix.join(home, 'Library', 'Caches', 'ms-playwright')];
|
|
29
|
+
return [path.posix.join(home, '.cache', 'ms-playwright'), '/opt/pw-browsers'];
|
|
30
|
+
}
|
|
31
|
+
export async function findChromium(dirs, list = (dir) => readdir(dir)) {
|
|
32
|
+
for (const dir of dirs) {
|
|
33
|
+
const found = (await list(dir).catch(() => [])).find((name) => /^chromium(?:_headless_shell)?-\d+$/.test(name));
|
|
34
|
+
if (found)
|
|
35
|
+
return path.join(dir, found);
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
export async function gatherDoctorInput() {
|
|
40
|
+
const gh = await probe('gh', ['--version']);
|
|
41
|
+
const [git, claude, ghAuth, chromium] = await Promise.all([
|
|
42
|
+
probe('git', ['--version']),
|
|
43
|
+
probe('claude', ['--version']),
|
|
44
|
+
gh.ok ? probe('gh', ['auth', 'status']) : Promise.resolve({ ok: false, output: 'gh missing' }),
|
|
45
|
+
findChromium(playwrightCacheDirs(process.env, process.platform, os.homedir())),
|
|
46
|
+
]);
|
|
47
|
+
return { nodeVersion: process.version, git, gh, ghAuth, claude, chromium };
|
|
48
|
+
}
|
|
49
|
+
/** `loop doctor`: prints the report and returns whether every required tool is present. */
|
|
50
|
+
export async function runDoctor(gather = gatherDoctorInput) {
|
|
51
|
+
const checks = evaluateDoctor(await gather());
|
|
52
|
+
return { report: formatDoctor(checks), passed: doctorPassed(checks) };
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
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';
|
|
7
|
+
import { formatLaneCheck, runCheckLanes } from './checkLanes.js';
|
|
8
|
+
import { runDoctor } from './doctor.js';
|
|
9
|
+
import { runAddLanes } from './lanesInit.js';
|
|
10
|
+
import { parseLaneList } from '../core/lanesInit.js';
|
|
4
11
|
import { DEFAULT_PORT, runDashboard } from './dashboard.js';
|
|
5
12
|
import { realGh } from './gh.js';
|
|
6
13
|
import { runInit } from './init.js';
|
|
7
14
|
import { runAnswer, runNextRound, runStatus } from './loops.js';
|
|
15
|
+
import { formatLaneRound, runLaneRound } from './laneRound.js';
|
|
8
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';
|
|
9
20
|
import { runRound } from './run.js';
|
|
10
21
|
const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
11
22
|
// Commands are added by the core lane: init, new, run, status, next-round, answer, dashboard.
|
|
@@ -18,8 +29,15 @@ program
|
|
|
18
29
|
.argument('[dir]', 'target repo folder', '.')
|
|
19
30
|
.option('--force', 'skip the brief check and overwrite existing kit files')
|
|
20
31
|
.option('--dry-run', 'show what would be written without writing')
|
|
32
|
+
.option('--lanes <names>', 'also set up parallel lanes, e.g. --lanes core,web,docs (writes .ai/lanes.json and per-lane files)')
|
|
21
33
|
.action(async (dir, options) => {
|
|
22
|
-
const
|
|
34
|
+
const laneList = options.lanes === undefined ? undefined : parseLaneList(options.lanes);
|
|
35
|
+
if (laneList?.error) {
|
|
36
|
+
console.error(laneList.error);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const result = await runInit(path.resolve(dir), { force: Boolean(options.force), dryRun: Boolean(options.dryRun), ...(laneList && { skipTask: true }) });
|
|
23
41
|
if (result.refused) {
|
|
24
42
|
console.error(result.refused);
|
|
25
43
|
process.exitCode = 1;
|
|
@@ -31,6 +49,43 @@ program
|
|
|
31
49
|
console.error('warning: the Operating Card was NOT installed, so CLAUDE.md is unchanged.');
|
|
32
50
|
for (const rel of result.missing)
|
|
33
51
|
console.error(`warning: ${rel} is not in this package yet; used a built-in fallback or skipped it`);
|
|
52
|
+
if (laneList) {
|
|
53
|
+
try {
|
|
54
|
+
const lanes = await runAddLanes(path.resolve(dir), laneList.names.map((name) => ({ name, owns: [`${name}/**`] })), { force: Boolean(options.force), dryRun: Boolean(options.dryRun), existingOk: true });
|
|
55
|
+
for (const action of lanes.actions)
|
|
56
|
+
console.log(`${action.kind.padEnd(9)} ${action.dest}${action.reason ? ` (${action.reason})` : ''}`);
|
|
57
|
+
for (const rel of lanes.missing)
|
|
58
|
+
console.error(`warning: ${rel} is not in this package yet; used a built-in fallback`);
|
|
59
|
+
console.log('Each lane owns `<name>/**` for now: edit "owns" in .ai/lanes.json to match your folders, then run `loop check-lanes`.');
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
fail(error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const lanesCommand = program.command('lanes').description('Manage parallel lanes (see .ai/lanes.json)');
|
|
67
|
+
lanesCommand
|
|
68
|
+
.command('add')
|
|
69
|
+
.description('Add a lane that owns the given path globs, with its task file and outbox')
|
|
70
|
+
.argument('<name>', 'lane name: lowercase letters, digits, dashes')
|
|
71
|
+
.requiredOption('--owns <globs...>', 'path globs this lane owns, e.g. --owns "src/api/**" "tests/api/**"')
|
|
72
|
+
.option('--cron <cron>', 'cron expression for the lane (default: next staggered hourly minute)')
|
|
73
|
+
.option('--dir <dir>', 'repo folder', '.')
|
|
74
|
+
.option('--dry-run', 'show what would be written without writing')
|
|
75
|
+
.action(async (name, options) => {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = parseLaneList(name);
|
|
78
|
+
if (parsed.error || parsed.names.length !== 1)
|
|
79
|
+
throw new Error(parsed.error ?? 'Add one lane at a time.');
|
|
80
|
+
const lanes = await runAddLanes(path.resolve(options.dir), [{ name, owns: options.owns, ...(options.cron && { cron: options.cron }) }], { force: false, dryRun: Boolean(options.dryRun) });
|
|
81
|
+
for (const action of lanes.actions)
|
|
82
|
+
console.log(`${action.kind.padEnd(9)} ${action.dest}${action.reason ? ` (${action.reason})` : ''}`);
|
|
83
|
+
for (const rel of lanes.missing)
|
|
84
|
+
console.error(`warning: ${rel} is not in this package yet; used a built-in fallback`);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
fail(error);
|
|
88
|
+
}
|
|
34
89
|
});
|
|
35
90
|
program
|
|
36
91
|
.command('new')
|
|
@@ -69,11 +124,19 @@ program
|
|
|
69
124
|
});
|
|
70
125
|
program
|
|
71
126
|
.command('next-round')
|
|
72
|
-
.description('After DONE.md: archive it as .ai/done-vN.md and write the next round\'s task.md from its "next 10" list')
|
|
127
|
+
.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')
|
|
73
128
|
.argument('[dir]', 'loop folder', '.')
|
|
74
129
|
.option('--dry-run', 'report what would happen without writing')
|
|
130
|
+
.option('--brief <file>', 'laned repos: the next round\'s brief (scored first)')
|
|
131
|
+
.option('--no-commit', 'with --brief: write the changes but do not commit')
|
|
132
|
+
.option('--json', 'with --brief: print the plan as JSON (shape: RoundPlan in src/core/laneRound.ts)')
|
|
75
133
|
.action(async (dir, options) => {
|
|
76
134
|
try {
|
|
135
|
+
if (options.brief) {
|
|
136
|
+
const result = await runLaneRound(path.resolve(dir), { briefFile: options.brief, dryRun: Boolean(options.dryRun), commit: options.commit });
|
|
137
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatLaneRound(result, Boolean(options.dryRun)));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
77
140
|
const result = await runNextRound(path.resolve(dir), { dryRun: Boolean(options.dryRun) });
|
|
78
141
|
console.log(`${options.dryRun ? 'Would archive' : 'Archived'} DONE.md as .ai/${result.archivedAs}; next round has ${result.units} units.`);
|
|
79
142
|
}
|
|
@@ -87,13 +150,17 @@ program
|
|
|
87
150
|
.argument('[answer]', 'your answer; omit with --accept to take the agent\'s best guess')
|
|
88
151
|
.option('--accept', 'accept the best guess in BLOCKED.md')
|
|
89
152
|
.option('--dir <dir>', 'loop folder', '.')
|
|
153
|
+
.option('--push', 'push the commit so the loop restarts without another step')
|
|
90
154
|
.option('--dry-run', 'show the question and answer without writing')
|
|
91
155
|
.action(async (answer, options) => {
|
|
92
156
|
try {
|
|
93
157
|
if (!options.accept && !answer)
|
|
94
158
|
throw new Error('Pass an answer, or --accept to take the best guess.');
|
|
95
|
-
const result = await runAnswer(path.resolve(options.dir), options.accept ? 'accept' : (answer ?? ''), { dryRun: Boolean(options.dryRun) });
|
|
96
|
-
|
|
159
|
+
const result = await runAnswer(path.resolve(options.dir), options.accept ? 'accept' : (answer ?? ''), { dryRun: Boolean(options.dryRun), push: Boolean(options.push) });
|
|
160
|
+
const ending = options.dryRun ? '\n(dry run: nothing written)' : result.pushed ? '\nCommitted and pushed. The loop restarts on its next run.' : result.pushed === false ? `\nCommitted, but the push failed: ${result.pushError}\nFix that (for a rejected push: \`git pull --rebase && git push\`) to restart the loop.` : '\nCommitted. Push (or use --push next time) to restart the loop.';
|
|
161
|
+
console.log(`Question: ${result.question}\nAnswer: ${result.answer}${ending}`);
|
|
162
|
+
if (result.pushed === false)
|
|
163
|
+
process.exitCode = 1;
|
|
97
164
|
}
|
|
98
165
|
catch (error) {
|
|
99
166
|
fail(error);
|
|
@@ -106,10 +173,14 @@ program
|
|
|
106
173
|
.option('--dry-run', 'print the prompt and stop; never starts claude')
|
|
107
174
|
.option('--skip-permissions', 'pass --dangerously-skip-permissions to claude (you accept the risk)')
|
|
108
175
|
.option('--model <model>', 'model for this round')
|
|
176
|
+
.option('--lane <name>', 'run as this lane from .ai/lanes.json (own task file, outbox and session lock)')
|
|
177
|
+
.option('--as <name>', 'your name for lane claims: a claim by this name does not stop the run')
|
|
109
178
|
.action(async (dir, options) => {
|
|
110
179
|
try {
|
|
111
180
|
const result = await runRound(path.resolve(dir), {
|
|
112
181
|
dryRun: Boolean(options.dryRun), skipPermissions: Boolean(options.skipPermissions), ...(options.model && { model: options.model }),
|
|
182
|
+
...(options.lane && { lane: options.lane }),
|
|
183
|
+
...(options.as && { as: options.as }),
|
|
113
184
|
});
|
|
114
185
|
if (result.skipped)
|
|
115
186
|
console.log(result.skipped);
|
|
@@ -140,5 +211,183 @@ program
|
|
|
140
211
|
fail(error);
|
|
141
212
|
}
|
|
142
213
|
});
|
|
214
|
+
program
|
|
215
|
+
.command('check-lanes')
|
|
216
|
+
.description('Fail if a commit with a "Lane: <name>" trailer touched files outside its lane (see .ai/lanes.json)')
|
|
217
|
+
.argument('[dir]', 'repo folder', '.')
|
|
218
|
+
.option('--range <range>', 'commits to check, e.g. origin/main..HEAD (default: the last 50)')
|
|
219
|
+
.action(async (dir, options) => {
|
|
220
|
+
try {
|
|
221
|
+
const result = await runCheckLanes(path.resolve(dir), options.range);
|
|
222
|
+
console.log(formatLaneCheck(result));
|
|
223
|
+
if (result.problems.length > 0)
|
|
224
|
+
process.exitCode = 1;
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
fail(error);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
program
|
|
231
|
+
.command('doctor')
|
|
232
|
+
.description('Check that node, git, gh, claude and Playwright Chromium are installed, and say how to fix what is not')
|
|
233
|
+
.action(async () => {
|
|
234
|
+
try {
|
|
235
|
+
const { report, passed } = await runDoctor();
|
|
236
|
+
console.log(report);
|
|
237
|
+
if (!passed)
|
|
238
|
+
process.exitCode = 1;
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
fail(error);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
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.` : '');
|
|
245
|
+
program
|
|
246
|
+
.command('claim')
|
|
247
|
+
.description('Tell teammates you are working a lane: writes .ai/lanes/<lane>/claim, commits and pushes it')
|
|
248
|
+
.argument('<lane>', 'lane name from .ai/lanes.json')
|
|
249
|
+
.option('--as <name>', 'your name (default: git config user.name)')
|
|
250
|
+
.option('--minutes <n>', 'how long the claim lasts', '90')
|
|
251
|
+
.option('--force', 'take over a lane someone else has claimed')
|
|
252
|
+
.option('--no-push', 'commit but do not push')
|
|
253
|
+
.option('--dir <dir>', 'repo folder', '.')
|
|
254
|
+
.action(async (lane, options) => {
|
|
255
|
+
try {
|
|
256
|
+
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 });
|
|
257
|
+
console.log(`Claimed ${r.lane} as ${r.name} until ${r.expiresAt?.toISOString()}.${pushNote(r)}`);
|
|
258
|
+
if (r.pushed === false)
|
|
259
|
+
process.exitCode = 1;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
fail(error);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
program
|
|
266
|
+
.command('release')
|
|
267
|
+
.description('Remove your claim on a lane (commits and pushes)')
|
|
268
|
+
.argument('<lane>', 'lane name from .ai/lanes.json')
|
|
269
|
+
.option('--no-push', 'commit but do not push')
|
|
270
|
+
.option('--dir <dir>', 'repo folder', '.')
|
|
271
|
+
.action(async (lane, options) => {
|
|
272
|
+
try {
|
|
273
|
+
const r = await runRelease(path.resolve(options.dir), lane, { push: options.push });
|
|
274
|
+
console.log(r.committed ? `Released ${lane}.${pushNote(r)}` : `Lane ${lane} had no committed claim; nothing to release.`);
|
|
275
|
+
if (r.pushed === false)
|
|
276
|
+
process.exitCode = 1;
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
fail(error);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
program
|
|
283
|
+
.command('next')
|
|
284
|
+
.description('List open units across all lanes with their claims, and recommend one nobody has claimed')
|
|
285
|
+
.argument('[dir]', 'repo folder', '.')
|
|
286
|
+
.option('--json', 'print the report as JSON (shape: NextReport in src/core/types.ts)')
|
|
287
|
+
.action(async (dir, options) => {
|
|
288
|
+
try {
|
|
289
|
+
const report = await runNext(path.resolve(dir));
|
|
290
|
+
console.log(options.json ? JSON.stringify(report, null, 2) : formatNext(report));
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
fail(error);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
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.` : '');
|
|
297
|
+
program
|
|
298
|
+
.command('pause')
|
|
299
|
+
.description('Stop every lane run and the control room: writes .ai/PAUSED, commits (Lane: control) and pushes')
|
|
300
|
+
.option('--reason <text>', 'why (shown on the dashboard)')
|
|
301
|
+
.option('--as <name>', 'your name (default: git config user.name)')
|
|
302
|
+
.option('--no-push', 'commit but do not push')
|
|
303
|
+
.option('--dir <dir>', 'repo folder', '.')
|
|
304
|
+
.action(async (options) => {
|
|
305
|
+
try {
|
|
306
|
+
const r = await runPause(path.resolve(options.dir), { ...(options.reason && { reason: options.reason }), ...(options.as && { as: options.as }), push: options.push });
|
|
307
|
+
console.log(r.changed ? `Paused. Scheduled runs now stop at their first step; \`loop resume\` undoes it.${pauseNote(r)}` : `Already paused by ${r.by}.`);
|
|
308
|
+
if (r.pushed === false)
|
|
309
|
+
process.exitCode = 1;
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
fail(error);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
program
|
|
316
|
+
.command('resume')
|
|
317
|
+
.description('Undo `loop pause`: removes .ai/PAUSED, commits (Lane: control) and pushes')
|
|
318
|
+
.option('--no-push', 'commit but do not push')
|
|
319
|
+
.option('--dir <dir>', 'repo folder', '.')
|
|
320
|
+
.action(async (options) => {
|
|
321
|
+
try {
|
|
322
|
+
const r = await runResume(path.resolve(options.dir), { push: options.push });
|
|
323
|
+
console.log(r.changed ? `Resumed.${pauseNote(r)}` : 'Not paused; nothing to do.');
|
|
324
|
+
if (r.pushed === false)
|
|
325
|
+
process.exitCode = 1;
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
fail(error);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
program
|
|
332
|
+
.command('ship-check')
|
|
333
|
+
.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')
|
|
334
|
+
.argument('[dir]', 'repo folder', '.')
|
|
335
|
+
.option('--json', 'print the checklist as JSON (shape: ShipCheck in src/core/shipCheck.ts)')
|
|
336
|
+
.action(async (dir, options) => {
|
|
337
|
+
try {
|
|
338
|
+
const result = await runShipCheck(path.resolve(dir));
|
|
339
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatShipCheck(result));
|
|
340
|
+
if (!result.ready)
|
|
341
|
+
process.exitCode = 1;
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
fail(error);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
const runsCommand = program
|
|
348
|
+
.command('runs')
|
|
349
|
+
.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)')
|
|
350
|
+
.option('--json', 'print the report as JSON (shape: RunsReport in src/cli/runs.ts)')
|
|
351
|
+
.option('--hours <n>', 'how far back to look', '24')
|
|
352
|
+
.option('--dir <dir>', 'repo folder', '.')
|
|
353
|
+
.action(async (options) => {
|
|
354
|
+
try {
|
|
355
|
+
const report = await readRuns(path.resolve(options.dir), { hours: Number(options.hours) });
|
|
356
|
+
console.log(options.json ? JSON.stringify(report, null, 2) : formatRuns(report));
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
fail(error);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
runsCommand
|
|
363
|
+
.command('log')
|
|
364
|
+
.description('Append one line to a lane\'s run log; every lane run ends with this and commits the file with its last commit')
|
|
365
|
+
.requiredOption('--lane <name>', 'lane name from .ai/lanes.json')
|
|
366
|
+
.option('--units <list...>', 'units finished this run')
|
|
367
|
+
.option('--passed <n>', 'tests passed on the last npm test')
|
|
368
|
+
.option('--failed <n>', 'tests failed on the last npm test')
|
|
369
|
+
.option('--pushed', 'this run pushed to main')
|
|
370
|
+
.option('--no-pushed', 'this run pushed nothing')
|
|
371
|
+
.option('--session <url>', 'session link or id')
|
|
372
|
+
.option('--note <text>', 'one line of context')
|
|
373
|
+
.option('--dir <dir>', 'repo folder', '.')
|
|
374
|
+
.action(async (options) => {
|
|
375
|
+
try {
|
|
376
|
+
// The parent `runs` command declares --dir too and Commander gives it the value wherever it appears.
|
|
377
|
+
const dir = options.dir !== '.' ? options.dir : (runsCommand.opts().dir);
|
|
378
|
+
const entry = await logRun(path.resolve(dir), options.lane, {
|
|
379
|
+
...(options.units && { units: options.units }),
|
|
380
|
+
...(options.passed !== undefined && { passed: Number(options.passed) }),
|
|
381
|
+
...(options.failed !== undefined && { failed: Number(options.failed) }),
|
|
382
|
+
pushed: options.pushed ?? false,
|
|
383
|
+
...(options.session && { session: options.session }),
|
|
384
|
+
...(options.note && { note: options.note }),
|
|
385
|
+
});
|
|
386
|
+
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.`);
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
fail(error);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
143
392
|
await program.parseAsync();
|
|
144
393
|
//# sourceMappingURL=index.js.map
|