compounding-loop 0.1.0 → 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/README.md +36 -6
- package/dist/cli/checkLanes.js +60 -0
- package/dist/cli/checkLanes.js.map +1 -0
- package/dist/cli/doctor.js +54 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/index.js +87 -3
- 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/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 +23 -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/run.js +104 -16
- package/dist/cli/run.js.map +1 -1
- package/dist/core/doctor.js +49 -0
- package/dist/core/doctor.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/runPrompt.js +1 -1
- package/dist/core/table.js +20 -1
- package/dist/core/table.js.map +1 -1
- package/dist/server/data.js +20 -6
- 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 +65 -5
- package/dist/server/health.js.map +1 -1
- package/dist/server/loops.js +6 -3
- package/dist/server/loops.js.map +1 -1
- 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/index.html +3 -3
- package/method/COMPOUNDING_LOOP.md +34 -0
- package/method/example/K-0002.md +16 -0
- package/method/example/K-0003.md +16 -0
- package/package.json +6 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/runners/github-actions/lanes.yml +103 -0
- package/runners/local/README.md +2 -1
- 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 +1 -1
- 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 +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/loop-prompt.md +1 -1
- 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/dist/cli/loops.js
CHANGED
|
@@ -5,7 +5,8 @@ import { promisify } from 'node:util';
|
|
|
5
5
|
import { addAnswerToTask, BUDGET_EXTENSION, extendRunBudget, formatAnswer, isBudgetBlock, parseBlocked } from '../core/blocked.js';
|
|
6
6
|
import { readLoopFacts, readRunCounter } from '../core/repo.js';
|
|
7
7
|
import { buildNextRoundTask, extractNextItems, nextDoneFileName } from '../core/rounds.js';
|
|
8
|
-
import { formatFleet } from '../core/table.js';
|
|
8
|
+
import { formatFleet, formatLanes } from '../core/table.js';
|
|
9
|
+
import { readLaneStatuses } from './laneStatus.js';
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
10
11
|
const git = (cwd, ...args) => execFileAsync('git', args, { cwd });
|
|
11
12
|
/** `loop status`: one row per folder. */
|
|
@@ -15,7 +16,13 @@ export async function runStatus(dirs, now = new Date()) {
|
|
|
15
16
|
const run = await readRunCounter(dir);
|
|
16
17
|
rows.push({ name: path.basename(path.resolve(dir)), facts: await readLoopFacts(dir), ...(run && { run }) });
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
+
const blocks = [formatFleet(rows, now)];
|
|
20
|
+
for (const dir of dirs) {
|
|
21
|
+
const lanes = await readLaneStatuses(dir, now);
|
|
22
|
+
if (lanes)
|
|
23
|
+
blocks.push(`Lanes in ${path.basename(path.resolve(dir))}:\n${formatLanes(lanes, now)}`);
|
|
24
|
+
}
|
|
25
|
+
return blocks.join('\n\n');
|
|
19
26
|
}
|
|
20
27
|
/** `loop next-round`: archive DONE.md as `.ai/done-vN.md` and write the next task.md from its "next 10" list. */
|
|
21
28
|
export async function runNextRound(dir, options) {
|
|
@@ -67,6 +74,19 @@ export async function runAnswer(dir, answer, options) {
|
|
|
67
74
|
await git(dir, 'add', '-A', '--', ...paths);
|
|
68
75
|
await git(dir, '-c', 'user.name=Compounding Loop', '-c', 'user.email=loop@users.noreply.github.com', 'commit', '-qm', 'Answer BLOCKED.md and restart the loop', '--', ...paths);
|
|
69
76
|
}
|
|
70
|
-
|
|
77
|
+
const result = { question: note.question, bestGuess: note.bestGuess, answer: finalAnswer };
|
|
78
|
+
if (options.push && !options.dryRun) {
|
|
79
|
+
try {
|
|
80
|
+
await git(dir, 'push');
|
|
81
|
+
result.pushed = true;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
result.pushed = false;
|
|
85
|
+
const output = `${error.stderr ?? ''}\n${error instanceof Error ? error.message : String(error)}`;
|
|
86
|
+
const lines = output.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
87
|
+
result.pushError = lines.find((l) => /rejected|fatal|error:/i.test(l)) ?? lines[0] ?? 'git push failed';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
71
91
|
}
|
|
72
92
|
//# sourceMappingURL=loops.js.map
|
package/dist/cli/loops.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loops.js","sourceRoot":"","sources":["../../src/cli/loops.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACnI,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC3F,OAAO,EAAE,WAAW,EAAiB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"loops.js","sourceRoot":"","sources":["../../src/cli/loops.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/new.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { cp, mkdir, readdir, readFile,
|
|
1
|
+
import { cp, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fillPlaceholders, hostLabel, isTextFile, TEMPLATE_IGNORED, templateTargetName, validateProjectName } from '../core/template.js';
|
|
4
4
|
import { packageRoot, runInit } from './init.js';
|
|
5
|
+
/** Project templates for `loop new`; `lanes` holds the files `loop init --lanes` writes, not a template. */
|
|
5
6
|
export async function listTemplates(root = packageRoot) {
|
|
6
7
|
const entries = await readdir(path.join(root, 'templates'), { withFileTypes: true }).catch(() => []);
|
|
7
|
-
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
8
|
+
return entries.filter((e) => e.isDirectory() && e.name !== 'lanes').map((e) => e.name).sort();
|
|
8
9
|
}
|
|
9
10
|
/** Copies `source` to `dest`, filling `{{placeholders}}` in text files. */
|
|
10
11
|
async function copyTemplate(source, dest, vars) {
|
|
@@ -46,9 +47,8 @@ export async function runNew(template, name, options) {
|
|
|
46
47
|
return { dir, repoUrl };
|
|
47
48
|
}
|
|
48
49
|
catch (error) {
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
throw new Error(`Could not create the GitHub repo (is \`gh auth login\` done, and is the name free?). The folder was removed.\n${error instanceof Error ? error.message : String(error)}`);
|
|
50
|
+
// Keep the folder: it holds the user's work from here on, and deleting it would also lose any edits made meanwhile.
|
|
51
|
+
throw new Error(`Could not create the GitHub repo (is \`gh auth login\` done, and is the name free?). The folder was kept at ${dir}.\nTo finish later: cd ${name} && gh repo create ${name} --private --source . --push\n${error instanceof Error ? error.message : String(error)}`);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
//# sourceMappingURL=new.js.map
|
package/dist/cli/new.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"new.js","sourceRoot":"","sources":["../../src/cli/new.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"new.js","sourceRoot":"","sources":["../../src/cli/new.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/run.js
CHANGED
|
@@ -4,21 +4,42 @@ import path from 'node:path';
|
|
|
4
4
|
import { parseLockTime } from '../core/repo.js';
|
|
5
5
|
import { LOCK_MINUTES } from '../core/status.js';
|
|
6
6
|
import { buildClaudeArgs, buildRunPrompt, DEFAULT_PROMPT } from '../core/runPrompt.js';
|
|
7
|
+
import { parseLanesConfig } from '../core/lanes.js';
|
|
7
8
|
import { packageRoot } from './init.js';
|
|
9
|
+
import { withSessionLock } from './lock.js';
|
|
8
10
|
const exists = (file) => stat(file).then(() => true, () => false);
|
|
9
11
|
/** First readable prompt: the repo's own, then the routine prompt in the repo, then the package's, then the built-in. */
|
|
10
|
-
export async function loadPromptText(dir, root = packageRoot) {
|
|
12
|
+
export async function loadPromptText(dir, root = packageRoot, lane) {
|
|
11
13
|
const candidates = [
|
|
14
|
+
...(lane ? [path.join(dir, '.ai', 'lanes', lane, 'prompt.md'), path.join(dir, 'runners', 'routine', 'lane-prompt.md'), path.join(root, 'runners', 'routine', 'lane-prompt.md')] : []),
|
|
12
15
|
path.join(dir, '.ai', 'loop-prompt.md'),
|
|
13
16
|
path.join(dir, 'runners', 'routine', 'prompt.md'),
|
|
14
17
|
path.join(root, 'runners', 'routine', 'prompt.md'),
|
|
15
18
|
];
|
|
16
|
-
for (const file of candidates) {
|
|
19
|
+
for (const [index, file] of candidates.entries()) {
|
|
17
20
|
const text = await readFile(file, 'utf8').catch(() => undefined);
|
|
18
|
-
if (text
|
|
19
|
-
|
|
21
|
+
if (text === undefined)
|
|
22
|
+
continue;
|
|
23
|
+
// The first three candidates are written for a lane; any other prompt is generic and needs the intro.
|
|
24
|
+
const forLane = lane !== undefined && index < 3;
|
|
25
|
+
return { text: lane && !forLane ? `${laneIntro(lane)}\n\n${text}` : text, source: file };
|
|
20
26
|
}
|
|
21
|
-
return { text: DEFAULT_PROMPT, source: 'built-in default' };
|
|
27
|
+
return { text: lane ? `${laneIntro(lane)}\n\n${DEFAULT_PROMPT}` : DEFAULT_PROMPT, source: 'built-in default' };
|
|
28
|
+
}
|
|
29
|
+
/** Used when no lane prompt file exists: points the generic round prompt at this lane's files. */
|
|
30
|
+
export function laneIntro(lane) {
|
|
31
|
+
return `You are the **${lane}** lane of a multi-lane loop. Your lane's task file is \`.ai/lanes/${lane}/task.md\` (wherever this prompt says \`.ai/task.md\`, use it), your outbox is \`.ai/lanes/${lane}/outbox.md\`, your lock is \`.ai/lanes/${lane}/session.lock\`, and your stop files are \`.ai/lanes/${lane}/DONE.md\` and \`BLOCKED.md\`. Edit only the paths \`${lane}\` owns in \`.ai/lanes.json\` (plus shared ones), and end every commit message with the trailer line \`Lane: ${lane}\`.`;
|
|
32
|
+
}
|
|
33
|
+
/** Fails early, with the valid names, when `--lane` is not in `.ai/lanes.json`. */
|
|
34
|
+
export async function assertKnownLane(dir, lane) {
|
|
35
|
+
const text = await readFile(path.join(dir, '.ai', 'lanes.json'), 'utf8').catch(() => undefined);
|
|
36
|
+
if (text === undefined)
|
|
37
|
+
throw new Error('No .ai/lanes.json here, so --lane has nothing to refer to. Run `loop init --lanes a,b,c` first, or drop --lane.');
|
|
38
|
+
const { config, errors } = parseLanesConfig(text);
|
|
39
|
+
if (!config)
|
|
40
|
+
throw new Error(`.ai/lanes.json is invalid:\n- ${errors.join('\n- ')}`);
|
|
41
|
+
if (!config.lanes.some((l) => l.name === lane))
|
|
42
|
+
throw new Error(`Unknown lane "${lane}". Lanes in .ai/lanes.json: ${config.lanes.map((l) => l.name).join(', ')}.`);
|
|
22
43
|
}
|
|
23
44
|
/**
|
|
24
45
|
* Finds an executable on PATH. Node cannot start `.cmd` shims without a shell (and we never build shell strings),
|
|
@@ -27,6 +48,11 @@ export async function loadPromptText(dir, root = packageRoot) {
|
|
|
27
48
|
export async function resolveExecutable(name, platform = process.platform, pathEnv = process.env.PATH ?? '', has = exists) {
|
|
28
49
|
if (platform !== 'win32')
|
|
29
50
|
return name;
|
|
51
|
+
if (/\.(?:cmd|bat)$/i.test(name))
|
|
52
|
+
throw new Error(`Node cannot start ${name} safely. Point at a native .exe instead.`);
|
|
53
|
+
// A full path (e.g. process.execPath) needs no PATH search, and appending `.exe` would break it.
|
|
54
|
+
if (path.win32.isAbsolute(name))
|
|
55
|
+
return name;
|
|
30
56
|
const dirs = pathEnv.split(path.win32.delimiter).filter(Boolean);
|
|
31
57
|
for (const dir of dirs)
|
|
32
58
|
if (await has(path.win32.join(dir, `${name}.exe`)))
|
|
@@ -38,8 +64,59 @@ export async function resolveExecutable(name, platform = process.platform, pathE
|
|
|
38
64
|
}
|
|
39
65
|
throw new Error(`Could not find ${name}.exe on PATH. Is Claude Code installed?`);
|
|
40
66
|
}
|
|
41
|
-
|
|
42
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Where an npm-installed `<name>.cmd` shim really points: the `bin` entry of the package installed next to it.
|
|
69
|
+
* Returns undefined when the layout is not the usual global one.
|
|
70
|
+
*/
|
|
71
|
+
export async function shimEntry(shim, packageName, binName, read = (file) => readFile(file, 'utf8').catch(() => undefined)) {
|
|
72
|
+
const packageDir = path.win32.join(path.win32.dirname(shim), 'node_modules', ...packageName.split('/'));
|
|
73
|
+
const text = await read(path.win32.join(packageDir, 'package.json'));
|
|
74
|
+
if (text === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
let bin;
|
|
77
|
+
try {
|
|
78
|
+
bin = JSON.parse(text).bin;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const rel = typeof bin === 'string' ? bin : bin && typeof bin === 'object' ? (bin[binName] ?? Object.values(bin).find((v) => typeof v === 'string')) : undefined;
|
|
84
|
+
return typeof rel === 'string' ? path.win32.join(packageDir, rel) : undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Fallback for other layouts (pnpm and similar; Volta shims call its own launcher and are not covered): npm-style `.cmd` shims name their script as
|
|
88
|
+
* `"%dp0%\\path\\cli.js"` (or `%~dp0`), relative to the shim's own folder.
|
|
89
|
+
*/
|
|
90
|
+
export async function shimScript(shim, read = (file) => readFile(file, 'utf8').catch(() => undefined)) {
|
|
91
|
+
const text = await read(shim);
|
|
92
|
+
const match = text === undefined ? null : /%(?:~dp0|dp0%)\\?([^"%\r\n]+\.(?:c|m)?js)"/i.exec(text);
|
|
93
|
+
return match?.[1] ? path.win32.join(path.win32.dirname(shim), match[1]) : undefined;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Like `resolveExecutable`, but also copes with the usual Windows install (`npm i -g @anthropic-ai/claude-code`),
|
|
97
|
+
* which only has `claude.cmd`: we run the package's own JS entry with this Node, no shell involved.
|
|
98
|
+
*/
|
|
99
|
+
export async function resolveInvocation(name, args, platform = process.platform, pathEnv = process.env.PATH ?? '', has = exists, read, nodePath = process.execPath) {
|
|
100
|
+
try {
|
|
101
|
+
return { command: await resolveExecutable(name, platform, pathEnv, has), args };
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (platform !== 'win32' || path.win32.isAbsolute(name))
|
|
105
|
+
throw error;
|
|
106
|
+
for (const dir of pathEnv.split(path.win32.delimiter).filter(Boolean)) {
|
|
107
|
+
const shim = path.win32.join(dir, `${name}.cmd`);
|
|
108
|
+
if (!(await has(shim)))
|
|
109
|
+
continue;
|
|
110
|
+
const entry = (await shimEntry(shim, name === 'claude' ? '@anthropic-ai/claude-code' : name, name, read)) ?? (await shimScript(shim, read));
|
|
111
|
+
if (!entry)
|
|
112
|
+
throw error;
|
|
113
|
+
return /\.exe$/i.test(entry) ? { command: entry, args } : { command: nodePath, args: [entry, ...args] };
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const realSpawn = async (command, rawArgs, cwd) => {
|
|
119
|
+
const { command: executable, args } = await resolveInvocation(command, rawArgs);
|
|
43
120
|
return new Promise((resolve, reject) => {
|
|
44
121
|
const child = spawn(executable, args, { cwd, stdio: 'inherit' });
|
|
45
122
|
child.on('error', (error) => reject(new Error(`Could not start \`${command}\`: ${error.message}. Is Claude Code installed and on your PATH?`)));
|
|
@@ -61,21 +138,32 @@ export async function recordRun(dir, startedAt, endedAt) {
|
|
|
61
138
|
}
|
|
62
139
|
/** `loop run`: one build round now, using the same prompt the scheduler uses. */
|
|
63
140
|
export async function runRound(dir, options, spawner = realSpawn) {
|
|
64
|
-
const {
|
|
65
|
-
|
|
66
|
-
|
|
141
|
+
const { lane } = options;
|
|
142
|
+
if (lane)
|
|
143
|
+
await assertKnownLane(dir, lane);
|
|
144
|
+
const laneDir = lane ? path.join('.ai', 'lanes', lane) : '.ai';
|
|
145
|
+
const { text, source } = await loadPromptText(dir, packageRoot, lane);
|
|
146
|
+
const lockFile = path.join(dir, laneDir, 'session.lock');
|
|
147
|
+
const lockNote = `Note from \`loop run\`: it created \`${path.posix.join(...laneDir.split(path.sep), 'session.lock')}\` for this very session. That lock is yours, not another session's: ignore it in the lock step and do not delete it.`;
|
|
148
|
+
const prompt = `${buildRunPrompt(text, { name: path.basename(path.resolve(dir)), lane: lane ?? '' })}\n${lockNote}\n`;
|
|
149
|
+
const stopFiles = [...(lane ? [path.join(laneDir, 'DONE.md'), path.join(laneDir, 'BLOCKED.md')] : []), 'DONE.md', 'BLOCKED.md'];
|
|
150
|
+
for (const stop of stopFiles) {
|
|
67
151
|
if (await exists(path.join(dir, stop)))
|
|
68
152
|
return { skipped: `${stop} exists: nothing to do`, prompt, source };
|
|
69
153
|
}
|
|
70
|
-
const lockLeft = lockMinutesLeft(await readFile(
|
|
154
|
+
const lockLeft = lockMinutesLeft(await readFile(lockFile, 'utf8').catch(() => undefined), new Date());
|
|
71
155
|
if (lockLeft !== undefined)
|
|
72
|
-
return { skipped: `another session holds .
|
|
156
|
+
return { skipped: `another session holds ${path.join(laneDir, 'session.lock')} (about ${lockLeft} min left): nothing to do`, prompt, source };
|
|
73
157
|
if (options.dryRun)
|
|
74
158
|
return { prompt, source };
|
|
75
|
-
console.error(`Running one round in ${dir} (prompt: ${source})`);
|
|
159
|
+
console.error(`Running one round in ${dir}${lane ? ` as lane ${lane}` : ''} (prompt: ${source})`);
|
|
76
160
|
const startedAt = new Date();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
161
|
+
try {
|
|
162
|
+
const exitCode = await withSessionLock(lockFile, () => spawner('claude', buildClaudeArgs(prompt, { skipPermissions: options.skipPermissions, ...(options.model && { model: options.model }) }), dir));
|
|
163
|
+
return { prompt, source, exitCode };
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
await recordRun(dir, startedAt, new Date()); // a failed round still counts for the dashboard's fast-failure check
|
|
167
|
+
}
|
|
80
168
|
}
|
|
81
169
|
//# sourceMappingURL=run.js.map
|
package/dist/cli/run.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACvF,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Pure part of `loop doctor`: turn probe results into checks with a fix for each failure.
|
|
2
|
+
export const MIN_NODE_MAJOR = 20;
|
|
3
|
+
export function parseNodeMajor(version) {
|
|
4
|
+
const major = Number(/^v?(\d+)/.exec(version.trim())?.[1]);
|
|
5
|
+
return Number.isFinite(major) ? major : 0;
|
|
6
|
+
}
|
|
7
|
+
const firstLine = (text) => text.trim().split(/\r?\n/)[0] ?? '';
|
|
8
|
+
export function evaluateDoctor(input) {
|
|
9
|
+
const nodeOk = parseNodeMajor(input.nodeVersion) >= MIN_NODE_MAJOR;
|
|
10
|
+
return [
|
|
11
|
+
{
|
|
12
|
+
id: 'node', label: `Node.js ${MIN_NODE_MAJOR} or newer`, required: true, ok: nodeOk, detail: input.nodeVersion,
|
|
13
|
+
fix: nodeOk ? '' : `Install Node.js ${MIN_NODE_MAJOR}+ from https://nodejs.org (or use nvm/fnm), then open a new terminal.`,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'git', label: 'git', required: true, ok: input.git.ok, detail: input.git.ok ? firstLine(input.git.output) : 'not found',
|
|
17
|
+
fix: input.git.ok ? '' : 'Install git from https://git-scm.com/downloads and make sure `git --version` works.',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'claude', label: 'Claude Code (claude)', required: true, ok: input.claude.ok, detail: input.claude.ok ? firstLine(input.claude.output) : firstLine(input.claude.output) || 'not found',
|
|
21
|
+
fix: input.claude.ok ? '' : input.claude.output.includes('.cmd') ? input.claude.output : 'Install Claude Code (https://docs.claude.com/en/docs/claude-code), then run `claude` once to sign in.',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'gh', label: 'GitHub CLI (gh), needed by `loop new` and for pushing', required: false, ok: input.gh.ok, detail: input.gh.ok ? firstLine(input.gh.output) : 'not found',
|
|
25
|
+
fix: input.gh.ok ? '' : 'Install gh from https://cli.github.com. Without it, use `loop new --dry-run` and create the repo yourself.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'gh-auth', label: 'gh is signed in', required: false, ok: input.ghAuth.ok, detail: input.ghAuth.ok ? 'signed in' : input.gh.ok ? 'not signed in' : 'skipped (gh missing)',
|
|
29
|
+
fix: input.ghAuth.ok || !input.gh.ok ? '' : 'Run `gh auth login` and choose GitHub.com.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'chromium', label: 'Playwright Chromium, needed by generated sites\' browser tests', required: false, ok: input.chromium !== undefined, detail: input.chromium ?? 'not found',
|
|
33
|
+
fix: input.chromium !== undefined ? '' : 'Run `npx playwright install chromium` (add `npx playwright install-deps chromium` on Linux if Chrome fails to start).',
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
export function doctorPassed(checks) {
|
|
38
|
+
return checks.every((c) => c.ok || !c.required);
|
|
39
|
+
}
|
|
40
|
+
export function formatDoctor(checks) {
|
|
41
|
+
const lines = checks.map((c) => {
|
|
42
|
+
const mark = c.ok ? 'ok ' : c.required ? 'MISSING' : 'missing';
|
|
43
|
+
return `${mark} ${c.label}: ${c.detail}${c.fix ? `\n fix: ${c.fix}` : ''}`;
|
|
44
|
+
});
|
|
45
|
+
const bad = checks.filter((c) => !c.ok && c.required).length;
|
|
46
|
+
lines.push('', bad > 0 ? `${bad} required tool(s) missing. Fix them and run \`loop doctor\` again.` : 'Everything required is installed.');
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/core/doctor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Pure derivation of one lane's state from the files and commits around it.
|
|
2
|
+
import { LOCK_MINUTES } from './status.js';
|
|
3
|
+
/** A lane with neither a commit nor a lock for this long is stalled (same rule as the dashboard's health check). */
|
|
4
|
+
export const STALL_MINUTES = 120;
|
|
5
|
+
/** Rough minutes between runs of a 5-field cron expression; undefined when it cannot be read. */
|
|
6
|
+
export function cronPeriodMinutes(cron) {
|
|
7
|
+
const fields = cron.trim().split(/\s+/);
|
|
8
|
+
if (fields.length !== 5)
|
|
9
|
+
return undefined;
|
|
10
|
+
const [minute, hour, , , weekday] = fields;
|
|
11
|
+
const step = (field) => /^\*\/(\d+)$/.exec(field)?.[1];
|
|
12
|
+
const count = (field) => field.split(',').length;
|
|
13
|
+
const minuteStep = step(minute);
|
|
14
|
+
if (minuteStep)
|
|
15
|
+
return Number(minuteStep) || undefined;
|
|
16
|
+
if (minute === '*')
|
|
17
|
+
return 1;
|
|
18
|
+
const hourStep = step(hour);
|
|
19
|
+
if (hourStep)
|
|
20
|
+
return Number(hourStep) * 60 || undefined;
|
|
21
|
+
if (hour === '*')
|
|
22
|
+
return 60;
|
|
23
|
+
const perDay = 1440 / count(hour);
|
|
24
|
+
return weekday === '*' ? perDay : 10_080 / count(weekday) / count(hour);
|
|
25
|
+
}
|
|
26
|
+
/** A lane is stalled after missing two scheduled runs; `STALL_MINUTES` when it has no readable cron. */
|
|
27
|
+
export function stallMinutesFor(cron) {
|
|
28
|
+
const period = cron === undefined ? undefined : cronPeriodMinutes(cron);
|
|
29
|
+
return period === undefined ? STALL_MINUTES : period * 2;
|
|
30
|
+
}
|
|
31
|
+
const minutesSince = (now, then) => (now.getTime() - then.getTime()) / 60_000;
|
|
32
|
+
export function deriveLaneState(facts, now = new Date()) {
|
|
33
|
+
if (facts.hasBlocked)
|
|
34
|
+
return 'blocked';
|
|
35
|
+
if (facts.hasDone)
|
|
36
|
+
return 'done';
|
|
37
|
+
if (facts.lockAt && minutesSince(now, facts.lockAt) < LOCK_MINUTES)
|
|
38
|
+
return 'building';
|
|
39
|
+
const latest = [facts.lockAt, facts.lastCommitAt].filter((d) => d !== undefined).map((d) => d.getTime());
|
|
40
|
+
if (latest.length > 0 && minutesSince(now, new Date(Math.max(...latest))) > (facts.stallMinutes ?? STALL_MINUTES))
|
|
41
|
+
return 'stalled';
|
|
42
|
+
return 'waiting';
|
|
43
|
+
}
|
|
44
|
+
const OUTBOX_LINE = /^(\d{4}-\d\d-\d\d) (\d\d:\d\d) UTC · to ([^·]+?) · (.+)$/;
|
|
45
|
+
/** Parses `YYYY-MM-DD HH:MM UTC · to <lane|a, b|all> · message` lines; other lines are ignored. */
|
|
46
|
+
export function parseOutbox(from, text) {
|
|
47
|
+
const messages = [];
|
|
48
|
+
for (const line of text.split(/\r?\n/)) {
|
|
49
|
+
const m = OUTBOX_LINE.exec(line.trim());
|
|
50
|
+
if (!m)
|
|
51
|
+
continue;
|
|
52
|
+
const [, date, time, to, body] = m;
|
|
53
|
+
messages.push({ from, to: to.split(/\s*,\s*|\s+and\s+/).map((t) => t.trim().toLowerCase()).filter(Boolean), at: `${date}T${time}:00.000Z`, text: body.trim() });
|
|
54
|
+
}
|
|
55
|
+
return messages;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A message to a named lane counts as unanswered until that lane writes any later outbox entry.
|
|
59
|
+
* A later commit by that lane (`lastCommitAt`, ISO) also counts as a reply, since a lane often answers by shipping.
|
|
60
|
+
* This is a heuristic, so it only ever informs; it never fails anything.
|
|
61
|
+
*/
|
|
62
|
+
export function summarizeOutboxes(outboxes, lastCommitAt = new Map()) {
|
|
63
|
+
const lastWrite = new Map();
|
|
64
|
+
for (const [lane, messages] of outboxes)
|
|
65
|
+
lastWrite.set(lane, [...messages.map((m) => m.at), lastCommitAt.get(lane) ?? ''].sort().at(-1) ?? '');
|
|
66
|
+
const unanswered = new Map();
|
|
67
|
+
const waitingOn = new Map();
|
|
68
|
+
for (const [from, messages] of outboxes) {
|
|
69
|
+
for (const message of messages) {
|
|
70
|
+
for (const to of new Set(message.to)) {
|
|
71
|
+
if (to === 'all' || to === from || !outboxes.has(to))
|
|
72
|
+
continue;
|
|
73
|
+
if ((lastWrite.get(to) ?? '') >= message.at)
|
|
74
|
+
continue;
|
|
75
|
+
unanswered.set(to, [...(unanswered.get(to) ?? []), { from, to, at: message.at, text: message.text }]);
|
|
76
|
+
waitingOn.set(from, [...new Set([...(waitingOn.get(from) ?? []), to])]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { unanswered, waitingOn };
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=laneStatus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"laneStatus.js","sourceRoot":"","sources":["../../src/core/laneStatus.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Lane config: who owns which paths. Pure (no Node imports) so the dashboard can reuse it.
|
|
2
|
+
/** The supervising routine's commit trailer; it may touch anything. */
|
|
3
|
+
export const CONTROL_LANE = 'control';
|
|
4
|
+
const LANE_NAME = /^[a-z][a-z0-9-]{0,30}$/;
|
|
5
|
+
export function isValidLaneName(name) {
|
|
6
|
+
return LANE_NAME.test(name) && name !== CONTROL_LANE;
|
|
7
|
+
}
|
|
8
|
+
/** Backslashes to slashes, no leading `./` or `/`, so Windows-style paths match POSIX globs. */
|
|
9
|
+
export function normalizePath(file) {
|
|
10
|
+
return file.replace(/\\/g, '/').replace(/^(?:\.?\/)+/, '');
|
|
11
|
+
}
|
|
12
|
+
const REGEX_SPECIAL = /[.+^${}()|[\]\\]/g;
|
|
13
|
+
/** Compiles `**` (any depth), `*` (one segment), `?` (one character) and literals to an anchored regex. */
|
|
14
|
+
function globToRegExp(glob) {
|
|
15
|
+
const g = normalizePath(glob);
|
|
16
|
+
let out = '';
|
|
17
|
+
for (let i = 0; i < g.length; i++) {
|
|
18
|
+
const c = g[i];
|
|
19
|
+
if (c === '*' && g[i + 1] === '*') {
|
|
20
|
+
const slashAfter = g[i + 2] === '/';
|
|
21
|
+
out += slashAfter ? '(?:.*/)?' : '.*';
|
|
22
|
+
i += slashAfter ? 2 : 1;
|
|
23
|
+
}
|
|
24
|
+
else if (c === '*')
|
|
25
|
+
out += '[^/]*';
|
|
26
|
+
else if (c === '?')
|
|
27
|
+
out += '[^/]';
|
|
28
|
+
else
|
|
29
|
+
out += c.replace(REGEX_SPECIAL, '\\$&');
|
|
30
|
+
}
|
|
31
|
+
return new RegExp(`^${out}$`);
|
|
32
|
+
}
|
|
33
|
+
export function matchGlob(glob, file) {
|
|
34
|
+
return globToRegExp(glob).test(normalizePath(file));
|
|
35
|
+
}
|
|
36
|
+
function matchesAny(globs, file) {
|
|
37
|
+
return globs.some((glob) => matchGlob(glob, file));
|
|
38
|
+
}
|
|
39
|
+
/** A concrete path the glob matches, used to detect two globs claiming the same files. */
|
|
40
|
+
function sampleFor(glob) {
|
|
41
|
+
return normalizePath(glob).replace(/\*\*\/?/g, 'x/').replace(/\/$/, '').replace(/[*?]/g, 'x');
|
|
42
|
+
}
|
|
43
|
+
function globsOverlap(a, b) {
|
|
44
|
+
return matchGlob(a, sampleFor(b)) || matchGlob(b, sampleFor(a));
|
|
45
|
+
}
|
|
46
|
+
function stringArray(value) {
|
|
47
|
+
return Array.isArray(value) && value.every((v) => typeof v === 'string' && v.trim() !== '') ? value : undefined;
|
|
48
|
+
}
|
|
49
|
+
/** Validates an already-parsed JSON value. Returns every problem found, not just the first. */
|
|
50
|
+
export function validateLanesConfig(raw) {
|
|
51
|
+
const errors = [];
|
|
52
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
53
|
+
return { errors: ['.ai/lanes.json must be an object with "lanes" and "shared".'] };
|
|
54
|
+
const obj = raw;
|
|
55
|
+
if (!Array.isArray(obj.lanes) || obj.lanes.length === 0)
|
|
56
|
+
return { errors: ['.ai/lanes.json needs at least one lane in "lanes".'] };
|
|
57
|
+
const lanes = [];
|
|
58
|
+
obj.lanes.forEach((item, index) => {
|
|
59
|
+
const at = `lanes[${index}]`;
|
|
60
|
+
if (typeof item !== 'object' || item === null)
|
|
61
|
+
return void errors.push(`${at} must be an object.`);
|
|
62
|
+
const entry = item;
|
|
63
|
+
const name = typeof entry.name === 'string' ? entry.name : '';
|
|
64
|
+
if (!isValidLaneName(name))
|
|
65
|
+
errors.push(`${at}: name "${name}" is invalid. Use lowercase letters, digits and dashes, starting with a letter ("${CONTROL_LANE}" is reserved).`);
|
|
66
|
+
else if (lanes.some((l) => l.name === name))
|
|
67
|
+
errors.push(`${at}: lane "${name}" is listed twice.`);
|
|
68
|
+
const owns = stringArray(entry.owns);
|
|
69
|
+
if (!owns || owns.length === 0)
|
|
70
|
+
errors.push(`${at} (${name || '?'}): "owns" must be a non-empty list of globs.`);
|
|
71
|
+
if (entry.cron !== undefined && typeof entry.cron !== 'string')
|
|
72
|
+
errors.push(`${at} (${name || '?'}): "cron" must be a string.`);
|
|
73
|
+
if (name && owns)
|
|
74
|
+
lanes.push({ name, owns, ...(typeof entry.cron === 'string' ? { cron: entry.cron } : {}) });
|
|
75
|
+
});
|
|
76
|
+
const shared = obj.shared === undefined ? [] : stringArray(obj.shared);
|
|
77
|
+
if (!shared)
|
|
78
|
+
errors.push('"shared" must be a list of globs.');
|
|
79
|
+
for (let i = 0; i < lanes.length; i++) {
|
|
80
|
+
for (let j = i + 1; j < lanes.length; j++) {
|
|
81
|
+
for (const a of lanes[i].owns) {
|
|
82
|
+
for (const b of lanes[j].owns) {
|
|
83
|
+
if (globsOverlap(a, b))
|
|
84
|
+
errors.push(`Lanes "${lanes[i].name}" and "${lanes[j].name}" both own files matching "${a}" / "${b}". Make the globs disjoint or move the path to "shared".`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return errors.length > 0 || !shared ? { errors } : { config: { lanes, shared }, errors };
|
|
90
|
+
}
|
|
91
|
+
export function parseLanesConfig(text) {
|
|
92
|
+
try {
|
|
93
|
+
return validateLanesConfig(JSON.parse(text));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return { errors: ['.ai/lanes.json is not valid JSON.'] };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** The lane that owns the file, "shared" for a shared path, or undefined when nobody does. */
|
|
100
|
+
export function laneForPath(config, file) {
|
|
101
|
+
const lane = config.lanes.find((l) => matchesAny(l.owns, file) || matchGlob(`.ai/lanes/${l.name}/**`, file));
|
|
102
|
+
if (lane)
|
|
103
|
+
return lane.name;
|
|
104
|
+
return matchesAny(config.shared, file) ? 'shared' : undefined;
|
|
105
|
+
}
|
|
106
|
+
/** Reads the last `Lane: <name>` line of a commit message (it may be followed by other trailers); undefined when there is none. */
|
|
107
|
+
export function parseLaneTrailer(message) {
|
|
108
|
+
const lines = message.trimEnd().split(/\r?\n/);
|
|
109
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
110
|
+
const line = lines[i].trim();
|
|
111
|
+
const match = /^lane:\s*(\S+)$/i.exec(line);
|
|
112
|
+
if (match)
|
|
113
|
+
return match[1].toLowerCase();
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
/** Files a commit with the given `Lane:` trailer may not touch, each with a fix-it message. */
|
|
118
|
+
export function checkCommitFiles(config, lane, files) {
|
|
119
|
+
if (lane === CONTROL_LANE)
|
|
120
|
+
return [];
|
|
121
|
+
const self = config.lanes.find((l) => l.name === lane);
|
|
122
|
+
if (!self) {
|
|
123
|
+
const known = config.lanes.map((l) => l.name).join(', ');
|
|
124
|
+
return [{ file: '(trailer)', message: `unknown lane "${lane}". Use one of: ${known}, or fix the trailer.` }];
|
|
125
|
+
}
|
|
126
|
+
const violations = [];
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const owner = laneForPath(config, file);
|
|
129
|
+
// A path listed in "shared" is open to every lane even when an "owns" glob also covers it (e.g. src/core/types.ts).
|
|
130
|
+
if (owner === self.name || owner === 'shared' || matchesAny(config.shared, file))
|
|
131
|
+
continue;
|
|
132
|
+
const fix = owner
|
|
133
|
+
? `it belongs to lane "${owner}". Ask that lane through your outbox (.ai/lanes/${self.name}/outbox.md) instead of editing it.`
|
|
134
|
+
: `no lane owns it. Add it to a lane's "owns" or to "shared" in .ai/lanes.json.`;
|
|
135
|
+
violations.push({ file: normalizePath(file), message: `lane "${self.name}" touched ${normalizePath(file)}, but ${fix}` });
|
|
136
|
+
}
|
|
137
|
+
return violations;
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=lanes.js.map
|