compounding-loop 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +123 -3
- package/bin/loop.js +2 -0
- package/dist/cli/checkLanes.js +60 -0
- package/dist/cli/checkLanes.js.map +1 -0
- package/dist/cli/dashboard.js +52 -0
- package/dist/cli/dashboard.js.map +1 -0
- package/dist/cli/doctor.js +54 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/gh.js +29 -0
- package/dist/cli/gh.js.map +1 -0
- package/dist/cli/index.js +228 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.js +73 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/laneStatus.js +52 -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 +42 -0
- package/dist/cli/lock.js.map +1 -0
- package/dist/cli/loops.js +92 -0
- package/dist/cli/loops.js.map +1 -0
- package/dist/cli/new.js +54 -0
- package/dist/cli/new.js.map +1 -0
- package/dist/cli/run.js +169 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/core/blocked.js +42 -0
- package/dist/core/blocked.js.map +1 -0
- package/dist/core/brief.js +91 -0
- package/dist/core/brief.js.map +1 -0
- package/dist/core/doctor.js +49 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/gh.js +2 -0
- package/dist/core/gh.js.map +1 -0
- package/dist/core/init.js +59 -0
- package/dist/core/init.js.map +1 -0
- package/dist/core/laneStatus.js +82 -0
- package/dist/core/laneStatus.js.map +1 -0
- package/dist/core/lanes.js +139 -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/repo.js +81 -0
- package/dist/core/repo.js.map +1 -0
- package/dist/core/rounds.js +50 -0
- package/dist/core/rounds.js.map +1 -0
- package/dist/core/runPrompt.js +31 -0
- package/dist/core/runPrompt.js.map +1 -0
- package/dist/core/status.js +21 -0
- package/dist/core/status.js.map +1 -0
- package/dist/core/table.js +43 -0
- package/dist/core/table.js.map +1 -0
- package/dist/core/template.js +29 -0
- package/dist/core/template.js.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/server/data.js +50 -0
- package/dist/server/data.js.map +1 -0
- package/dist/server/events.js +106 -0
- package/dist/server/events.js.map +1 -0
- package/dist/server/five-sites.json +3071 -0
- package/dist/server/health.js +193 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/index.js +134 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/loops.js +145 -0
- package/dist/server/loops.js.map +1 -0
- package/dist/server/settings.js +25 -0
- package/dist/server/settings.js.map +1 -0
- package/dist/server/static-export.js +28 -0
- package/dist/server/static-export.js.map +1 -0
- package/dist/server/static.js +80 -0
- package/dist/server/static.js.map +1 -0
- package/dist/server/this-repo.json +371 -0
- package/dist/web/assets/index-B16QCSnY.css +2 -0
- package/dist/web/assets/index-CS82DKbF.js +49 -0
- package/dist/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/web/favicon.svg +1 -0
- package/dist/web/index.html +14 -0
- package/dist/web/logo.svg +1 -0
- package/method/COMPOUNDING_LOOP.md +603 -0
- package/method/example/K-0001.md +16 -0
- package/method/example/K-0002.md +16 -0
- package/method/example/K-0003.md +16 -0
- package/method/knowledge-index.md +5 -0
- package/method/operating-card.md +40 -0
- package/method/retro.md +10 -0
- package/method/reviewer-prompt.md +13 -0
- package/method/stuck-protocol.md +12 -0
- package/method/task-template.md +41 -0
- package/package.json +76 -4
- package/plugin/.claude-plugin/plugin.json +7 -0
- package/plugin/commands/handoff.md +10 -0
- package/plugin/commands/loop-init.md +13 -0
- package/plugin/commands/retro.md +11 -0
- package/plugin/commands/stuck.md +10 -0
- package/runners/github-actions/lanes.yml +103 -0
- package/runners/github-actions/loop.yml +51 -0
- package/runners/local/README.md +9 -0
- package/runners/local/crontab.txt +6 -0
- package/runners/local/launchd.plist +36 -0
- package/runners/local/task-scheduler.xml +32 -0
- package/runners/routine/README.md +56 -0
- package/runners/routine/control-room-prompt.md +16 -0
- package/runners/routine/lane-prompt.md +12 -0
- package/runners/routine/prompt.md +13 -0
- package/templates/chrome-extension/CLAUDE.md +18 -0
- package/templates/chrome-extension/IDEA.md +22 -0
- package/templates/chrome-extension/extension/background.js +5 -0
- 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 +25 -0
- package/templates/chrome-extension/extension/popup.css +3 -0
- package/templates/chrome-extension/extension/popup.html +15 -0
- package/templates/chrome-extension/extension/popup.js +16 -0
- package/templates/chrome-extension/gitignore +6 -0
- package/templates/chrome-extension/package.json +12 -0
- package/templates/chrome-extension/playwright.config.js +22 -0
- package/templates/chrome-extension/scripts/check.mjs +51 -0
- package/templates/chrome-extension/scripts/test.mjs +59 -0
- package/templates/chrome-extension/tests/popup.spec.js +37 -0
- package/templates/lanes/README.md +23 -0
- package/templates/lanes/ci-check-lanes.yml +38 -0
- package/templates/lanes/claude-section.md +17 -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/index.md +5 -0
- package/templates/static-site/.ai/log.md +3 -0
- package/templates/static-site/.ai/loop-prompt.md +10 -0
- package/templates/static-site/.ai/task.md +27 -0
- package/templates/static-site/.gitattributes +1 -0
- package/templates/static-site/CLAUDE.md +113 -0
- package/templates/static-site/IDEA.md +36 -0
- package/templates/static-site/gitignore +6 -0
- package/templates/static-site/netlify.toml +14 -0
- package/templates/static-site/package.json +14 -0
- package/templates/static-site/playwright.config.js +27 -0
- package/templates/static-site/scripts/check.mjs +188 -0
- package/templates/static-site/scripts/serve.mjs +44 -0
- package/templates/static-site/scripts/test.mjs +59 -0
- package/templates/static-site/site/index.html +23 -0
- package/templates/static-site/site/robots.txt +4 -0
- package/templates/static-site/site/sitemap.xml +4 -0
- package/templates/static-site/tests/pages.spec.js +21 -0
- package/templates/static-site/vercel.json +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Minntooos
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,4 +1,124 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Compounding Loop
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx compounding-loop init
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/compounding-loop) [](https://github.com/Minntooos/Compounding-Loop/actions/workflows/ci.yml) [](LICENSE)
|
|
10
|
+
|
|
11
|
+
**[Live demo](https://minntooos.github.io/Compounding-Loop/)**: the dashboard on the real five-site data, in your browser.
|
|
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).
|
|
18
|
+
|
|
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.
|
|
20
|
+
|
|
21
|
+
## Proof
|
|
22
|
+
|
|
23
|
+
Five calculator sites, each built by scheduled Claude Code runs working from a written brief. Numbers come from the repo data in [`demo/five-sites.json`](demo/five-sites.json).
|
|
24
|
+
|
|
25
|
+
| Site | Pages | Tests passing | Commits | Rounds | Elapsed* |
|
|
26
|
+
|---|---:|---:|---:|---:|---:|
|
|
27
|
+
| [powerstationruntime.netlify.app](https://powerstationruntime.netlify.app) | 55 | 278 | 112 | 5 / 5 | 22 h |
|
|
28
|
+
| [clearpoolmath.netlify.app](https://clearpoolmath.netlify.app) | 40 | 543 | 83 | 5 / 5 | 22 h |
|
|
29
|
+
| [yarnsums.netlify.app](https://yarnsums.netlify.app) | 40 | 419 | 109 | 5 / 5 | 19 h |
|
|
30
|
+
| [quiltcalculators.netlify.app](https://quiltcalculators.netlify.app) | 43 | 452 | 77 | 5 / 5 | 18 h |
|
|
31
|
+
| [mathtank.netlify.app](https://mathtank.netlify.app) | 37 | 433 | 83 | 5 / 5 | 18 h |
|
|
32
|
+
| **Total** | **215** | **2,125** | **464** | | |
|
|
33
|
+
|
|
34
|
+
\*Wall-clock time from the first to the last commit, rounded. It is not the time a human spent: that was writing five briefs.
|
|
35
|
+
|
|
36
|
+
## How it works
|
|
37
|
+
|
|
38
|
+
```mermaid
|
|
39
|
+
flowchart LR
|
|
40
|
+
B[Brief] --> T[task.md contract]
|
|
41
|
+
T --> R[Scheduled run]
|
|
42
|
+
R --> U[One unit of work]
|
|
43
|
+
U --> C{npm test}
|
|
44
|
+
C -- red --> X[Log under Tried, discard]
|
|
45
|
+
C -- green --> V[Reviewer subagent]
|
|
46
|
+
V --> P[Push]
|
|
47
|
+
P --> K[Retro: knowledge]
|
|
48
|
+
K --> R
|
|
49
|
+
R -. stuck .-> Q[BLOCKED.md: your inbox]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
1. **Brief and contract.** You write what "done" means as commands that can be checked.
|
|
53
|
+
2. **Scheduled runs.** A routine, a GitHub Action or `loop run` wakes up, reads its task file and does one unit at a time.
|
|
54
|
+
3. **Checks decide.** The agent never declares itself done; `npm test` does.
|
|
55
|
+
4. **Reviewer gate.** A fresh subagent attacks the diff before it is pushed.
|
|
56
|
+
5. **Compounding.** Every run writes what it learned into a small indexed knowledge base the next run reads first.
|
|
57
|
+
6. **Escalation.** When it is stuck it writes `BLOCKED.md`. You answer from the dashboard inbox or `loop answer`.
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
npx compounding-loop new static-site my-site # a new loop repo from a template
|
|
63
|
+
cd my-site
|
|
64
|
+
npx compounding-loop run # one round now
|
|
65
|
+
npx compounding-loop dashboard # the local dashboard
|
|
66
|
+
```
|
|
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).
|
|
69
|
+
|
|
70
|
+
Already have a repo? `npx compounding-loop init` adds the method to it without overwriting anything. Details: [Getting started](docs/getting-started.md).
|
|
71
|
+
|
|
72
|
+
## Just loop the agent?
|
|
73
|
+
|
|
74
|
+
An honest comparison with `while true; do claude -p "keep going"; done`:
|
|
75
|
+
|
|
76
|
+
| | Plain loop | Compounding Loop |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| Knows when it is done | When the model says so | When your checks pass |
|
|
79
|
+
| Reviews its own work | Same context, same blind spots | A fresh reviewer must find no blockers |
|
|
80
|
+
| Survives context limits | Starts cold every time | Writes a handoff the next run resumes from |
|
|
81
|
+
| Learns from mistakes | No | Notes and a retro, indexed |
|
|
82
|
+
| Overlapping runs | Collide on the same files | Session lock |
|
|
83
|
+
| Stuck for hours | Burns budget | Run budget, then `BLOCKED.md` |
|
|
84
|
+
| You can see what happened | `git log` | Fleet dashboard and inbox |
|
|
85
|
+
|
|
86
|
+
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.
|
|
87
|
+
|
|
88
|
+
## How it differs from other tools
|
|
89
|
+
|
|
90
|
+
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).
|
|
91
|
+
|
|
92
|
+
| Tool | What it is for | Where Compounding Loop differs |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| [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. |
|
|
95
|
+
| [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. |
|
|
96
|
+
| [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. |
|
|
97
|
+
| [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. |
|
|
98
|
+
| [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. |
|
|
99
|
+
|
|
100
|
+
If you work at the keyboard and want to watch agents in parallel, those tools are a good fit.
|
|
101
|
+
|
|
102
|
+
## Docs
|
|
103
|
+
|
|
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)
|
|
105
|
+
|
|
106
|
+
## FAQ
|
|
107
|
+
|
|
108
|
+
**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.
|
|
109
|
+
|
|
110
|
+
**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.
|
|
111
|
+
|
|
112
|
+
**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.
|
|
113
|
+
|
|
114
|
+
**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).
|
|
115
|
+
|
|
116
|
+
**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).
|
|
117
|
+
|
|
118
|
+
**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
|
+
|
|
120
|
+
**Does it need a GitHub login?** Only to create a private repo with `loop new`; `--dry-run` and every test work offline.
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT. See [LICENSE](LICENSE).
|
package/bin/loop.js
ADDED
|
@@ -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,52 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export const DEFAULT_PORT = 4321;
|
|
5
|
+
/** Loads the server lazily so every other command works before (or without) the dashboard being built. */
|
|
6
|
+
async function loadStartServer() {
|
|
7
|
+
const specifier = '../server/index.js';
|
|
8
|
+
try {
|
|
9
|
+
const mod = (await import(specifier));
|
|
10
|
+
if (typeof mod.startServer === 'function')
|
|
11
|
+
return mod.startServer;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
// Only "the server file itself is missing" is expected; a missing dependency inside it must surface as is.
|
|
15
|
+
const serverMissing = error instanceof Error && /Cannot find module|ERR_MODULE_NOT_FOUND/.test(error.message) && /server[\\/]index/.test(error.message);
|
|
16
|
+
if (!serverMissing)
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
throw new Error('The dashboard server is not available in this build (src/server/index.ts must export startServer).');
|
|
20
|
+
}
|
|
21
|
+
/** Command and arguments that open a URL in the default browser; always an argument array, never a shell string. */
|
|
22
|
+
export function browserCommand(url, platform = process.platform) {
|
|
23
|
+
if (platform === 'darwin')
|
|
24
|
+
return ['open', [url]];
|
|
25
|
+
if (platform === 'win32')
|
|
26
|
+
return ['rundll32', ['url.dll,FileProtocolHandler', url]];
|
|
27
|
+
return ['xdg-open', [url]];
|
|
28
|
+
}
|
|
29
|
+
function openBrowser(url) {
|
|
30
|
+
const [command, args] = browserCommand(url);
|
|
31
|
+
// Best effort: a missing opener must not stop the server, the URL is printed anyway.
|
|
32
|
+
execFile(command, args, () => undefined).on('error', () => undefined);
|
|
33
|
+
}
|
|
34
|
+
/** Starts the local dashboard server and (unless told not to) opens the browser. */
|
|
35
|
+
export async function runDashboard(options, deps = {}) {
|
|
36
|
+
if (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535) {
|
|
37
|
+
throw new Error(`"${options.port}" is not a valid port (0-65535).`);
|
|
38
|
+
}
|
|
39
|
+
const start = deps.start ?? (await loadStartServer());
|
|
40
|
+
const server = await start({ port: options.port, demo: options.demo, projectsDir: path.resolve(options.projectsDir) }).catch((error) => {
|
|
41
|
+
if (error.code === 'EADDRINUSE')
|
|
42
|
+
throw new Error(`Port ${options.port} is already in use. Pick another with --port.`);
|
|
43
|
+
throw error;
|
|
44
|
+
});
|
|
45
|
+
(deps.log ?? console.log)(`Dashboard${options.demo ? ' (demo)' : ''}: ${server.url}`);
|
|
46
|
+
if (options.open)
|
|
47
|
+
(deps.open ?? openBrowser)(server.url);
|
|
48
|
+
return server;
|
|
49
|
+
}
|
|
50
|
+
/** True once the server lane has shipped `startServer`; lets tests run the real thing only when it exists. */
|
|
51
|
+
export const serverShipped = (root) => existsSync(path.join(root, 'src', 'server', 'index.ts'));
|
|
52
|
+
//# sourceMappingURL=dashboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../../src/cli/dashboard.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/gh.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
/** Runs a command and rethrows with its stderr, not the raw command line execFile puts in the message. */
|
|
5
|
+
async function run(command, args, cwd) {
|
|
6
|
+
try {
|
|
7
|
+
return (await execFileAsync(command, args, { cwd })).stdout;
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
const stderr = error.stderr;
|
|
11
|
+
const detail = typeof stderr === 'string' && stderr.trim() ? stderr.trim() : error instanceof Error ? error.message : String(error);
|
|
12
|
+
throw new Error(`\`${command} ${args[0] ?? ''}\` failed: ${detail}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Uses the user's own `gh` login; no token is read, stored or logged here. */
|
|
16
|
+
export const realGh = {
|
|
17
|
+
async createRepoFromFolder(folder, name, options) {
|
|
18
|
+
await run('git', ['init', '-q'], folder);
|
|
19
|
+
await run('git', ['checkout', '-q', '-B', 'main'], folder);
|
|
20
|
+
await run('git', ['add', '-A'], folder);
|
|
21
|
+
// Fresh machines often have no git identity; fall back only when none is configured.
|
|
22
|
+
const hasIdentity = await run('git', ['config', 'user.email'], folder).then((out) => out.trim() !== '', () => false);
|
|
23
|
+
const identity = hasIdentity ? [] : ['-c', 'user.name=Compounding Loop', '-c', 'user.email=loop@users.noreply.github.com'];
|
|
24
|
+
await run('git', [...identity, 'commit', '-qm', 'Start from the Compounding Loop template'], folder);
|
|
25
|
+
await run('gh', ['repo', 'create', name, options.private ? '--private' : '--public', '--source', '.', '--push'], folder);
|
|
26
|
+
return (await run('gh', ['repo', 'view', '--json', 'url', '--jq', '.url'], folder)).trim();
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=gh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gh.js","sourceRoot":"","sources":["../../src/cli/gh.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C,0GAA0G;AAC1G,KAAK,UAAU,GAAG,CAAC,OAAe,EAAE,IAAc,EAAE,GAAW;IAC7D,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,aAAa,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;IAC9D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,GAAI,KAA8B,CAAC,MAAM,CAAC;QACtD,MAAM,MAAM,GAAG,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACpI,MAAM,IAAI,KAAK,CAAC,KAAK,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,cAAc,MAAM,EAAE,CAAC,CAAC;IACvE,CAAC;AACH,CAAC;AAED,+EAA+E;AAC/E,MAAM,CAAC,MAAM,MAAM,GAAO;IACxB,KAAK,CAAC,oBAAoB,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO;QAC9C,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;QACzC,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;QAC3D,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;QACxC,qFAAqF;QACrF,MAAM,WAAW,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;QACrH,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,4BAA4B,EAAE,IAAI,EAAE,0CAA0C,CAAC,CAAC;QAC3H,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,0CAA0C,CAAC,EAAE,MAAM,CAAC,CAAC;QACrG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;QACzH,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7F,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { formatLaneCheck, runCheckLanes } from './checkLanes.js';
|
|
5
|
+
import { runDoctor } from './doctor.js';
|
|
6
|
+
import { runAddLanes } from './lanesInit.js';
|
|
7
|
+
import { parseLaneList } from '../core/lanesInit.js';
|
|
8
|
+
import { DEFAULT_PORT, runDashboard } from './dashboard.js';
|
|
9
|
+
import { realGh } from './gh.js';
|
|
10
|
+
import { runInit } from './init.js';
|
|
11
|
+
import { runAnswer, runNextRound, runStatus } from './loops.js';
|
|
12
|
+
import { runNew } from './new.js';
|
|
13
|
+
import { runRound } from './run.js';
|
|
14
|
+
const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
15
|
+
// Commands are added by the core lane: init, new, run, status, next-round, answer, dashboard.
|
|
16
|
+
export const program = new Command('loop')
|
|
17
|
+
.description('Unattended, verified Claude Code build loops. Write the brief, check back tomorrow.')
|
|
18
|
+
.version(pkg.version);
|
|
19
|
+
program
|
|
20
|
+
.command('init')
|
|
21
|
+
.description('Add the Compounding Loop method to a repo (idempotent; never overwrites without --force)')
|
|
22
|
+
.argument('[dir]', 'target repo folder', '.')
|
|
23
|
+
.option('--force', 'skip the brief check and overwrite existing kit files')
|
|
24
|
+
.option('--dry-run', 'show what would be written without writing')
|
|
25
|
+
.option('--lanes <names>', 'also set up parallel lanes, e.g. --lanes core,web,docs (writes .ai/lanes.json and per-lane files)')
|
|
26
|
+
.action(async (dir, options) => {
|
|
27
|
+
const laneList = options.lanes === undefined ? undefined : parseLaneList(options.lanes);
|
|
28
|
+
if (laneList?.error) {
|
|
29
|
+
console.error(laneList.error);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const result = await runInit(path.resolve(dir), { force: Boolean(options.force), dryRun: Boolean(options.dryRun), ...(laneList && { skipTask: true }) });
|
|
34
|
+
if (result.refused) {
|
|
35
|
+
console.error(result.refused);
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
for (const action of result.actions)
|
|
40
|
+
console.log(`${action.kind.padEnd(9)} ${action.dest}${action.reason ? ` (${action.reason})` : ''}`);
|
|
41
|
+
if (result.missing.includes('method/operating-card.md'))
|
|
42
|
+
console.error('warning: the Operating Card was NOT installed, so CLAUDE.md is unchanged.');
|
|
43
|
+
for (const rel of result.missing)
|
|
44
|
+
console.error(`warning: ${rel} is not in this package yet; used a built-in fallback or skipped it`);
|
|
45
|
+
if (laneList) {
|
|
46
|
+
try {
|
|
47
|
+
const lanes = await runAddLanes(path.resolve(dir), laneList.names.map((name) => ({ name, owns: [`${name}/**`] })), { force: Boolean(options.force), dryRun: Boolean(options.dryRun), existingOk: true });
|
|
48
|
+
for (const action of lanes.actions)
|
|
49
|
+
console.log(`${action.kind.padEnd(9)} ${action.dest}${action.reason ? ` (${action.reason})` : ''}`);
|
|
50
|
+
for (const rel of lanes.missing)
|
|
51
|
+
console.error(`warning: ${rel} is not in this package yet; used a built-in fallback`);
|
|
52
|
+
console.log('Each lane owns `<name>/**` for now: edit "owns" in .ai/lanes.json to match your folders, then run `loop check-lanes`.');
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
fail(error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
const lanesCommand = program.command('lanes').description('Manage parallel lanes (see .ai/lanes.json)');
|
|
60
|
+
lanesCommand
|
|
61
|
+
.command('add')
|
|
62
|
+
.description('Add a lane that owns the given path globs, with its task file and outbox')
|
|
63
|
+
.argument('<name>', 'lane name: lowercase letters, digits, dashes')
|
|
64
|
+
.requiredOption('--owns <globs...>', 'path globs this lane owns, e.g. --owns "src/api/**" "tests/api/**"')
|
|
65
|
+
.option('--cron <cron>', 'cron expression for the lane (default: next staggered hourly minute)')
|
|
66
|
+
.option('--dir <dir>', 'repo folder', '.')
|
|
67
|
+
.option('--dry-run', 'show what would be written without writing')
|
|
68
|
+
.action(async (name, options) => {
|
|
69
|
+
try {
|
|
70
|
+
const parsed = parseLaneList(name);
|
|
71
|
+
if (parsed.error || parsed.names.length !== 1)
|
|
72
|
+
throw new Error(parsed.error ?? 'Add one lane at a time.');
|
|
73
|
+
const lanes = await runAddLanes(path.resolve(options.dir), [{ name, owns: options.owns, ...(options.cron && { cron: options.cron }) }], { force: false, dryRun: Boolean(options.dryRun) });
|
|
74
|
+
for (const action of lanes.actions)
|
|
75
|
+
console.log(`${action.kind.padEnd(9)} ${action.dest}${action.reason ? ` (${action.reason})` : ''}`);
|
|
76
|
+
for (const rel of lanes.missing)
|
|
77
|
+
console.error(`warning: ${rel} is not in this package yet; used a built-in fallback`);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
fail(error);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
program
|
|
84
|
+
.command('new')
|
|
85
|
+
.description('Create a new loop repo from a template (private GitHub repo unless --dry-run)')
|
|
86
|
+
.argument('<template>', 'template name, e.g. static-site')
|
|
87
|
+
.argument('[name]', 'folder and repo name (defaults to the template name)')
|
|
88
|
+
.option('--dry-run', 'write the folder locally and never touch GitHub')
|
|
89
|
+
.option('--public', 'create the GitHub repo as public (default: private)')
|
|
90
|
+
.action(async (template, name, options) => {
|
|
91
|
+
try {
|
|
92
|
+
const result = await runNew(template, name ?? template, {
|
|
93
|
+
parentDir: process.cwd(), dryRun: Boolean(options.dryRun), publicRepo: Boolean(options.public), gh: realGh,
|
|
94
|
+
});
|
|
95
|
+
console.log(`Created ${result.dir}${result.repoUrl ? `\nGitHub: ${result.repoUrl}` : ''}`);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
const fail = (error) => {
|
|
103
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
};
|
|
106
|
+
program
|
|
107
|
+
.command('status')
|
|
108
|
+
.description('Fleet table for one or more loop folders')
|
|
109
|
+
.argument('[dirs...]', 'loop folders', ['.'])
|
|
110
|
+
.action(async (dirs) => {
|
|
111
|
+
try {
|
|
112
|
+
console.log(await runStatus(dirs.map((d) => path.resolve(d))));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
fail(error);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
program
|
|
119
|
+
.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')
|
|
121
|
+
.argument('[dir]', 'loop folder', '.')
|
|
122
|
+
.option('--dry-run', 'report what would happen without writing')
|
|
123
|
+
.action(async (dir, options) => {
|
|
124
|
+
try {
|
|
125
|
+
const result = await runNextRound(path.resolve(dir), { dryRun: Boolean(options.dryRun) });
|
|
126
|
+
console.log(`${options.dryRun ? 'Would archive' : 'Archived'} DONE.md as .ai/${result.archivedAs}; next round has ${result.units} units.`);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
fail(error);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
program
|
|
133
|
+
.command('answer')
|
|
134
|
+
.description('Answer a BLOCKED.md: record the answer, remove the file, commit')
|
|
135
|
+
.argument('[answer]', 'your answer; omit with --accept to take the agent\'s best guess')
|
|
136
|
+
.option('--accept', 'accept the best guess in BLOCKED.md')
|
|
137
|
+
.option('--dir <dir>', 'loop folder', '.')
|
|
138
|
+
.option('--push', 'push the commit so the loop restarts without another step')
|
|
139
|
+
.option('--dry-run', 'show the question and answer without writing')
|
|
140
|
+
.action(async (answer, options) => {
|
|
141
|
+
try {
|
|
142
|
+
if (!options.accept && !answer)
|
|
143
|
+
throw new Error('Pass an answer, or --accept to take the best guess.');
|
|
144
|
+
const result = await runAnswer(path.resolve(options.dir), options.accept ? 'accept' : (answer ?? ''), { dryRun: Boolean(options.dryRun), push: Boolean(options.push) });
|
|
145
|
+
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.';
|
|
146
|
+
console.log(`Question: ${result.question}\nAnswer: ${result.answer}${ending}`);
|
|
147
|
+
if (result.pushed === false)
|
|
148
|
+
process.exitCode = 1;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
fail(error);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
program
|
|
155
|
+
.command('run')
|
|
156
|
+
.description('Run one build round now with Claude Code, using the same prompt the scheduler uses')
|
|
157
|
+
.argument('[dir]', 'loop folder', '.')
|
|
158
|
+
.option('--dry-run', 'print the prompt and stop; never starts claude')
|
|
159
|
+
.option('--skip-permissions', 'pass --dangerously-skip-permissions to claude (you accept the risk)')
|
|
160
|
+
.option('--model <model>', 'model for this round')
|
|
161
|
+
.option('--lane <name>', 'run as this lane from .ai/lanes.json (own task file, outbox and session lock)')
|
|
162
|
+
.action(async (dir, options) => {
|
|
163
|
+
try {
|
|
164
|
+
const result = await runRound(path.resolve(dir), {
|
|
165
|
+
dryRun: Boolean(options.dryRun), skipPermissions: Boolean(options.skipPermissions), ...(options.model && { model: options.model }),
|
|
166
|
+
...(options.lane && { lane: options.lane }),
|
|
167
|
+
});
|
|
168
|
+
if (result.skipped)
|
|
169
|
+
console.log(result.skipped);
|
|
170
|
+
else if (options.dryRun)
|
|
171
|
+
console.log(`Prompt from ${result.source}:\n\n${result.prompt}`);
|
|
172
|
+
if (result.exitCode)
|
|
173
|
+
process.exitCode = result.exitCode;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
fail(error);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
program
|
|
180
|
+
.command('dashboard')
|
|
181
|
+
.description('Start the local dashboard and open it in your browser')
|
|
182
|
+
.option('--demo', 'show the built-in snapshot of five real loops')
|
|
183
|
+
.option('--port <port>', 'port to listen on', String(DEFAULT_PORT))
|
|
184
|
+
.option('--no-open', 'do not open the browser')
|
|
185
|
+
.option('--projects <dir>', 'folder that holds your loops', '.')
|
|
186
|
+
.action(async (options) => {
|
|
187
|
+
try {
|
|
188
|
+
const server = await runDashboard({ demo: Boolean(options.demo), port: Number(options.port), open: options.open, projectsDir: options.projects });
|
|
189
|
+
for (const signal of ['SIGINT', 'SIGTERM'])
|
|
190
|
+
process.once(signal, () => void Promise.resolve(server.close()).finally(() => process.exit(0)));
|
|
191
|
+
// The server keeps the process alive; Ctrl+C stops it.
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
fail(error);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
program
|
|
198
|
+
.command('check-lanes')
|
|
199
|
+
.description('Fail if a commit with a "Lane: <name>" trailer touched files outside its lane (see .ai/lanes.json)')
|
|
200
|
+
.argument('[dir]', 'repo folder', '.')
|
|
201
|
+
.option('--range <range>', 'commits to check, e.g. origin/main..HEAD (default: the last 50)')
|
|
202
|
+
.action(async (dir, options) => {
|
|
203
|
+
try {
|
|
204
|
+
const result = await runCheckLanes(path.resolve(dir), options.range);
|
|
205
|
+
console.log(formatLaneCheck(result));
|
|
206
|
+
if (result.problems.length > 0)
|
|
207
|
+
process.exitCode = 1;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
fail(error);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
program
|
|
214
|
+
.command('doctor')
|
|
215
|
+
.description('Check that node, git, gh, claude and Playwright Chromium are installed, and say how to fix what is not')
|
|
216
|
+
.action(async () => {
|
|
217
|
+
try {
|
|
218
|
+
const { report, passed } = await runDoctor();
|
|
219
|
+
console.log(report);
|
|
220
|
+
if (!passed)
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
fail(error);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
await program.parseAsync();
|
|
228
|
+
//# sourceMappingURL=index.js.map
|