compounding-loop 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -3
- package/dist/cli/claim.js +84 -0
- package/dist/cli/claim.js.map +1 -0
- package/dist/cli/draft.js +69 -0
- package/dist/cli/draft.js.map +1 -0
- package/dist/cli/index.js +284 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/laneRound.js +96 -0
- package/dist/cli/laneRound.js.map +1 -0
- package/dist/cli/laneStatus.js +3 -1
- package/dist/cli/laneStatus.js.map +1 -1
- package/dist/cli/lanesInit.js +8 -2
- package/dist/cli/lanesInit.js.map +1 -1
- package/dist/cli/lock.js +22 -2
- package/dist/cli/lock.js.map +1 -1
- package/dist/cli/loops.js +7 -0
- package/dist/cli/loops.js.map +1 -1
- package/dist/cli/next.js +18 -0
- package/dist/cli/next.js.map +1 -0
- package/dist/cli/pace.js +41 -0
- package/dist/cli/pace.js.map +1 -0
- package/dist/cli/pause.js +81 -0
- package/dist/cli/pause.js.map +1 -0
- package/dist/cli/run.js +18 -6
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/runs.js +118 -0
- package/dist/cli/runs.js.map +1 -0
- package/dist/cli/schedule.js +177 -0
- package/dist/cli/schedule.js.map +1 -0
- package/dist/cli/shipCheck.js +59 -0
- package/dist/cli/shipCheck.js.map +1 -0
- package/dist/core/brief.js +35 -12
- package/dist/core/brief.js.map +1 -1
- package/dist/core/draftBrief.js +68 -0
- package/dist/core/draftBrief.js.map +1 -0
- package/dist/core/laneRound.js +63 -0
- package/dist/core/laneRound.js.map +1 -0
- package/dist/core/lanes.js +15 -2
- package/dist/core/lanes.js.map +1 -1
- package/dist/core/lanesInit.js +19 -0
- package/dist/core/lanesInit.js.map +1 -1
- 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 +17 -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/outcomes.js +51 -0
- package/dist/core/parse/outcomes.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/policy.js +56 -0
- package/dist/core/parse/policy.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/schedule.js +101 -0
- package/dist/core/parse/schedule.js.map +1 -0
- package/dist/core/parse/steer.js +68 -0
- package/dist/core/parse/steer.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 +18 -8
- package/dist/core/repo.js.map +1 -1
- package/dist/core/runPrompt.js +8 -2
- 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 +3 -1
- package/dist/core/table.js.map +1 -1
- package/dist/server/actionBody.js +54 -0
- package/dist/server/actionBody.js.map +1 -0
- package/dist/server/checkLabels.js +32 -0
- package/dist/server/checkLabels.js.map +1 -0
- package/dist/server/claude.js +18 -0
- package/dist/server/claude.js.map +1 -0
- package/dist/server/controls.js +99 -0
- package/dist/server/controls.js.map +1 -0
- package/dist/server/data.js +9 -3
- package/dist/server/data.js.map +1 -1
- package/dist/server/draftInputs.js +33 -0
- package/dist/server/draftInputs.js.map +1 -0
- package/dist/server/health.js +9 -30
- package/dist/server/health.js.map +1 -1
- package/dist/server/index.js +77 -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/laneSchedule.js +15 -0
- package/dist/server/laneSchedule.js.map +1 -0
- package/dist/server/live/build.js +178 -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 +11 -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 +61 -0
- package/dist/server/live/urls.js.map +1 -0
- package/dist/server/loops.js +263 -41
- package/dist/server/loops.js.map +1 -1
- package/dist/server/paceNote.js +13 -0
- package/dist/server/paceNote.js.map +1 -0
- package/dist/server/roundState.js +51 -0
- package/dist/server/roundState.js.map +1 -0
- package/dist/server/runs.js +144 -0
- package/dist/server/runs.js.map +1 -0
- package/dist/server/shipped.js +35 -0
- package/dist/server/shipped.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 +2818 -106
- package/dist/web/assets/index-BozIcdeb.js +88 -0
- package/dist/web/assets/index-Te_olhNA.css +2 -0
- package/dist/web/index.html +2 -2
- package/method/COMPOUNDING_LOOP.md +26 -0
- package/method/retro.md +7 -0
- package/package.json +1 -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 +55 -2
- package/runners/local/README.md +3 -1
- package/runners/local/compounding-loop.service +20 -0
- package/runners/local/compounding-loop.timer +11 -0
- package/runners/routine/README.md +5 -1
- package/runners/routine/control-room-prompt.md +4 -14
- package/runners/routine/lane-prompt.md +4 -10
- package/runners/routine/prompt.md +2 -2
- package/templates/lanes/README.md +5 -1
- package/templates/lanes/claude-section.md +5 -1
- package/templates/lanes/control-run.md +37 -0
- package/templates/lanes/lane-run.md +33 -0
- package/templates/lanes/schedule.json +4 -0
- package/templates/lanes/steer.md +3 -0
- package/templates/lanes/task.md +1 -1
- package/templates/static-site/.ai/loop-prompt.md +1 -1
- package/templates/static-site/CLAUDE.md +2 -0
- package/dist/web/assets/index-B16QCSnY.css +0 -2
- package/dist/web/assets/index-CS82DKbF.js +0 -49
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ npx compounding-loop init
|
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/compounding-loop) [](https://github.com/Minntooos/Compounding-Loop/actions/workflows/ci.yml) [](LICENSE)
|
|
10
10
|
|
|
11
|
-
**[
|
|
11
|
+
**[Watch this repo build itself](https://minntooos.github.io/Compounding-Loop/)**: the live demo reads this repo's commits from GitHub in your browser, next to the real five-site data. Steering and scheduling: [Glance, steer, sleep](#glance-steer-sleep). Working with a teammate? [Five steps](docs/team.md).
|
|
12
12
|
|
|
13
13
|

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