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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Planning for `loop init --lanes` and `loop lanes add`: decides what to write, without touching the disk.
|
|
2
|
+
import { fillPlaceholders } from './template.js';
|
|
3
|
+
import { isValidLaneName, normalizePath, validateLanesConfig } from './lanes.js';
|
|
4
|
+
export const LANES_START = '<!-- compounding-loop:lanes:start -->';
|
|
5
|
+
export const LANES_END = '<!-- compounding-loop:lanes:end -->';
|
|
6
|
+
/** Shared files every new laned repo starts with; the user edits `.ai/lanes.json` to taste. */
|
|
7
|
+
export const DEFAULT_SHARED = ['package.json', 'package-lock.json', '.gitignore', '.ai/audit/**'];
|
|
8
|
+
// Used when the package has no templates/lanes/ files, so init never fails on a half-installed kit.
|
|
9
|
+
export const FALLBACK_LANE_TEMPLATES = {
|
|
10
|
+
task: `# Lane: {{lane}}
|
|
11
|
+
|
|
12
|
+
Run: 0 / 20
|
|
13
|
+
Status: not started
|
|
14
|
+
|
|
15
|
+
## Contract
|
|
16
|
+
**Goal:**
|
|
17
|
+
**Done when:**
|
|
18
|
+
**Constraints:** edit only the paths this lane owns in \`.ai/lanes.json\`: {{owns}}
|
|
19
|
+
**Out of scope:**
|
|
20
|
+
|
|
21
|
+
## Units, in order
|
|
22
|
+
1.
|
|
23
|
+
|
|
24
|
+
## Decisions
|
|
25
|
+
|
|
26
|
+
## Confirmed
|
|
27
|
+
|
|
28
|
+
## Guesses
|
|
29
|
+
|
|
30
|
+
## Tried
|
|
31
|
+
|
|
32
|
+
## Handoff
|
|
33
|
+
`,
|
|
34
|
+
outbox: `# Outbox: {{lane}}
|
|
35
|
+
|
|
36
|
+
Newest first. Format: \`YYYY-MM-DD HH:MM UTC · to <lane|all> · message\`.
|
|
37
|
+
`,
|
|
38
|
+
controlRoom: `# Control room notes
|
|
39
|
+
|
|
40
|
+
Written only by the control room routine. Every lane reads it at the start of a run. Newest first. Format: \`YYYY-MM-DD HH:MM UTC · to <lane|all> · message\`.
|
|
41
|
+
`,
|
|
42
|
+
claudeSection: `## Lanes
|
|
43
|
+
|
|
44
|
+
Lanes in this repo: {{lanes}}. \`.ai/lanes.json\` says who owns which paths; read it first. Edit only your lane's paths (and the shared ones). Every commit ends with a trailer line \`Lane: <your lane>\`; \`loop check-lanes\` fails a commit that crosses lanes.
|
|
45
|
+
|
|
46
|
+
Talk to other lanes by writing to your own \`.ai/lanes/<you>/outbox.md\` (newest first). At the start of each run read every other lane's outbox and \`.ai/control-room.md\`. Your task file is \`.ai/lanes/<you>/task.md\`.
|
|
47
|
+
`,
|
|
48
|
+
};
|
|
49
|
+
/** Staggered hourly cron minutes (7, 19, 31, 43, 55, ...) so lanes never start together. */
|
|
50
|
+
export function staggeredCron(index) {
|
|
51
|
+
return `${(7 + 12 * index) % 60} * * * *`;
|
|
52
|
+
}
|
|
53
|
+
/** Parses `a,b,c` into lane names; returns the first problem as `error`. */
|
|
54
|
+
export function parseLaneList(list) {
|
|
55
|
+
const names = list.split(',').map((n) => n.trim()).filter(Boolean);
|
|
56
|
+
if (names.length === 0)
|
|
57
|
+
return { names, error: 'Give at least one lane name, e.g. --lanes core,web.' };
|
|
58
|
+
for (const name of names) {
|
|
59
|
+
if (!isValidLaneName(name))
|
|
60
|
+
return { names, error: `"${name}" is not a valid lane name: use lowercase letters, digits and dashes, starting with a letter ("control" is reserved).` };
|
|
61
|
+
}
|
|
62
|
+
const dupe = names.find((n, i) => names.indexOf(n) !== i);
|
|
63
|
+
return dupe ? { names, error: `Lane "${dupe}" is listed twice.` } : { names };
|
|
64
|
+
}
|
|
65
|
+
/** Slash-separated and repo-relative: a Windows-style or absolute glob would be stored but could never match. */
|
|
66
|
+
function checkedGlob(lane, glob) {
|
|
67
|
+
const normalized = normalizePath(glob);
|
|
68
|
+
if (/^[a-z]:/i.test(normalized) || glob.trim().startsWith('/') || normalized.split('/').includes('..')) {
|
|
69
|
+
throw new Error(`Lane "${lane}": "${glob}" must be a path relative to the repo root, like src/api/**.`);
|
|
70
|
+
}
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
73
|
+
/** Lanes to add to `existing` (undefined = new config); throws with the reason when the result would be invalid. */
|
|
74
|
+
export function addLanes(existing, additions) {
|
|
75
|
+
const base = existing ?? { lanes: [], shared: [...DEFAULT_SHARED] };
|
|
76
|
+
additions = additions.map((lane) => ({ ...lane, owns: lane.owns.map((glob) => checkedGlob(lane.name, glob)) }));
|
|
77
|
+
for (const lane of additions) {
|
|
78
|
+
if (base.lanes.some((l) => l.name === lane.name))
|
|
79
|
+
throw new Error(`Lane "${lane.name}" already exists in .ai/lanes.json. Pick another name or edit the file.`);
|
|
80
|
+
}
|
|
81
|
+
const next = { lanes: [...base.lanes, ...additions], shared: base.shared };
|
|
82
|
+
const { errors } = validateLanesConfig(next);
|
|
83
|
+
if (errors.length > 0)
|
|
84
|
+
throw new Error(errors.join('\n'));
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
87
|
+
/** Same shape as `mergeCard`: re-running is a no-op, and a broken marker pair is reported, not guessed at. */
|
|
88
|
+
export function mergeLanesSection(existing, section, fileName = 'CLAUDE.md') {
|
|
89
|
+
const block = `${LANES_START}\n${section.trim()}\n${LANES_END}\n`;
|
|
90
|
+
if (existing === undefined || existing.trim() === '')
|
|
91
|
+
return block;
|
|
92
|
+
const start = existing.indexOf(LANES_START);
|
|
93
|
+
const end = existing.indexOf(LANES_END);
|
|
94
|
+
if ((start === -1) !== (end === -1) || (start !== -1 && end < start)) {
|
|
95
|
+
throw new Error(`${fileName} has a broken ${LANES_START} / ${LANES_END} pair: fix or remove the markers, then re-run.`);
|
|
96
|
+
}
|
|
97
|
+
if (start !== -1)
|
|
98
|
+
return existing.slice(0, start) + block + existing.slice(end + LANES_END.length).replace(/^\r?\n/, '');
|
|
99
|
+
return `${existing.replace(/\s*$/, '')}\n\n${block}`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Files for a laned repo. Config is rewritten only when it changed; task, outbox and control-room files are loop
|
|
103
|
+
* state, created if absent and never overwritten, even with `force`.
|
|
104
|
+
*/
|
|
105
|
+
export function planLanes({ config, newLanes, templates, existing, force }) {
|
|
106
|
+
const actions = [];
|
|
107
|
+
const json = `${JSON.stringify(config, null, 2)}\n`;
|
|
108
|
+
const current = existing.get('.ai/lanes.json');
|
|
109
|
+
if (current === undefined)
|
|
110
|
+
actions.push({ dest: '.ai/lanes.json', kind: 'create', content: json });
|
|
111
|
+
else if (current === json)
|
|
112
|
+
actions.push({ dest: '.ai/lanes.json', kind: 'skip', reason: 'already up to date' });
|
|
113
|
+
else if (force || newLanes.length > 0)
|
|
114
|
+
actions.push({ dest: '.ai/lanes.json', kind: 'update', content: json });
|
|
115
|
+
else
|
|
116
|
+
actions.push({ dest: '.ai/lanes.json', kind: 'skip', reason: 'exists with different lanes (use --force to replace, or `loop lanes add`)' });
|
|
117
|
+
const state = (dest, content) => {
|
|
118
|
+
if (existing.has(dest))
|
|
119
|
+
actions.push({ dest, kind: 'skip', reason: 'loop state, never overwritten' });
|
|
120
|
+
else
|
|
121
|
+
actions.push({ dest, kind: 'create', content });
|
|
122
|
+
};
|
|
123
|
+
for (const name of newLanes) {
|
|
124
|
+
const lane = config.lanes.find((l) => l.name === name);
|
|
125
|
+
const vars = { lane: name, owns: lane ? lane.owns.join(', ') : '', lanes: config.lanes.map((l) => l.name).join(', ') };
|
|
126
|
+
state(`.ai/lanes/${name}/task.md`, fillPlaceholders(templates.task, vars));
|
|
127
|
+
state(`.ai/lanes/${name}/outbox.md`, fillPlaceholders(templates.outbox, vars));
|
|
128
|
+
}
|
|
129
|
+
state('.ai/control-room.md', templates.controlRoom);
|
|
130
|
+
const section = fillPlaceholders(templates.claudeSection, { lanes: config.lanes.map((l) => l.name).join(', ') });
|
|
131
|
+
// AGENTS.md is merged only when the repo already has one; CLAUDE.md is always installed.
|
|
132
|
+
const instructionFiles = existing.has('AGENTS.md') ? ['CLAUDE.md', 'AGENTS.md'] : ['CLAUDE.md'];
|
|
133
|
+
for (const dest of instructionFiles) {
|
|
134
|
+
const current = existing.get(dest);
|
|
135
|
+
const merged = mergeLanesSection(current, section, dest);
|
|
136
|
+
if (merged === current)
|
|
137
|
+
actions.push({ dest, kind: 'skip', reason: 'lane section already installed' });
|
|
138
|
+
else
|
|
139
|
+
actions.push({ dest, kind: current === undefined ? 'create' : 'update', content: merged });
|
|
140
|
+
}
|
|
141
|
+
return actions;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=lanesInit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lanesInit.js","sourceRoot":"","sources":["../../src/core/lanesInit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
/** Reads the `Run: N / LIMIT` line of a task.md; undefined when the line is missing or malformed. */
|
|
7
|
+
export function parseRunCounter(taskText) {
|
|
8
|
+
const match = /^Run:\s*(\d+)\s*\/\s*(\d+)\b/m.exec(taskText);
|
|
9
|
+
if (!match)
|
|
10
|
+
return undefined;
|
|
11
|
+
return { run: Number(match[1]), limit: Number(match[2]) };
|
|
12
|
+
}
|
|
13
|
+
/** Parses the single UTC timestamp line of `.ai/session.lock`; undefined when it is not a valid date. */
|
|
14
|
+
export function parseLockTime(lockText) {
|
|
15
|
+
const line = lockText.trim().split(/\r?\n/)[0]?.trim();
|
|
16
|
+
if (!line)
|
|
17
|
+
return undefined;
|
|
18
|
+
// The lock is documented as UTC; a bare "2026-10-08 05:00" would otherwise parse as local time.
|
|
19
|
+
const hasZone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(line);
|
|
20
|
+
const at = new Date(hasZone ? line : `${line.replace(' ', 'T')}Z`);
|
|
21
|
+
return Number.isNaN(at.getTime()) ? undefined : at;
|
|
22
|
+
}
|
|
23
|
+
/** Counts file names like `done-v1.md`, `done-v2.md`. */
|
|
24
|
+
export function countDoneRounds(fileNames) {
|
|
25
|
+
return fileNames.filter((name) => /^done-v\d+\.md$/.test(name)).length;
|
|
26
|
+
}
|
|
27
|
+
async function exists(file) {
|
|
28
|
+
try {
|
|
29
|
+
await stat(file);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function readOptional(file) {
|
|
37
|
+
try {
|
|
38
|
+
return await readFile(file, 'utf8');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Commit time of HEAD, or undefined when the folder is not a git repo or has no commits. */
|
|
45
|
+
export async function readLastCommitAt(dir) {
|
|
46
|
+
try {
|
|
47
|
+
const { stdout } = await execFileAsync('git', ['log', '-1', '--format=%cI'], {
|
|
48
|
+
cwd: dir,
|
|
49
|
+
// Stop git walking up into a parent repo: a folder that is not itself a repo has no commits.
|
|
50
|
+
env: { ...process.env, GIT_CEILING_DIRECTORIES: path.dirname(path.resolve(dir)) },
|
|
51
|
+
});
|
|
52
|
+
const at = new Date(stdout.trim());
|
|
53
|
+
return Number.isNaN(at.getTime()) ? undefined : at;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Reads one loop's facts from its folder. Health failures are filled in by the health checks, not here. */
|
|
60
|
+
export async function readLoopFacts(dir) {
|
|
61
|
+
const aiDir = path.join(dir, '.ai');
|
|
62
|
+
const aiFiles = await readdir(aiDir).catch(() => []);
|
|
63
|
+
const lockText = await readOptional(path.join(aiDir, 'session.lock'));
|
|
64
|
+
const lockAt = lockText === undefined ? undefined : parseLockTime(lockText);
|
|
65
|
+
const lastCommitAt = await readLastCommitAt(dir);
|
|
66
|
+
return {
|
|
67
|
+
// The stop files sit at the repo root, per CLAUDE.md "Unattended mode".
|
|
68
|
+
hasBlocked: await exists(path.join(dir, 'BLOCKED.md')),
|
|
69
|
+
hasDone: await exists(path.join(dir, 'DONE.md')),
|
|
70
|
+
roundsDone: countDoneRounds(aiFiles),
|
|
71
|
+
...(lockAt && { lockAt }),
|
|
72
|
+
...(lastCommitAt && { lastCommitAt }),
|
|
73
|
+
healthFailures: [],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Reads `Run: N / LIMIT` from a loop's `.ai/task.md`. */
|
|
77
|
+
export async function readRunCounter(dir) {
|
|
78
|
+
const text = await readOptional(path.join(dir, '.ai', 'task.md'));
|
|
79
|
+
return text === undefined ? undefined : parseRunCounter(text);
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=repo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo.js","sourceRoot":"","sources":["../../src/core/repo.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { listItems } from './brief.js';
|
|
2
|
+
/** Items under the "next 10 improvements" heading of a DONE.md; empty when the section is missing. */
|
|
3
|
+
export function extractNextItems(doneText) {
|
|
4
|
+
const lines = doneText.split(/\r?\n/);
|
|
5
|
+
// Any heading about what comes next ("Next 10 improvements", "Ten best next pages", "Next steps").
|
|
6
|
+
const start = lines.findIndex((line) => /^#{1,6}\s+.*\b(next|ten best)\b/i.test(line));
|
|
7
|
+
if (start === -1)
|
|
8
|
+
return [];
|
|
9
|
+
const rest = lines.slice(start + 1);
|
|
10
|
+
const end = rest.findIndex((line) => /^#{1,6}\s/.test(line));
|
|
11
|
+
return listItems((end === -1 ? rest : rest.slice(0, end)).join('\n'));
|
|
12
|
+
}
|
|
13
|
+
/** Next archive name: one above the highest existing `done-vN.md` (so gaps never collide). */
|
|
14
|
+
export function nextDoneFileName(fileNames) {
|
|
15
|
+
const numbers = fileNames.flatMap((name) => {
|
|
16
|
+
const match = /^done-v(\d+)\.md$/.exec(name);
|
|
17
|
+
return match ? [Number(match[1])] : [];
|
|
18
|
+
});
|
|
19
|
+
return `done-v${Math.max(0, ...numbers) + 1}.md`;
|
|
20
|
+
}
|
|
21
|
+
/** Writes the next round's task.md from the items; `round` is the number of the round being started. */
|
|
22
|
+
export function buildNextRoundTask(items, round, runLimit = 30) {
|
|
23
|
+
const units = items.map((item, i) => `${i + 1}. ${item}`).join('\n');
|
|
24
|
+
return `# TASK: Round ${round}, improvements from the previous DONE.md
|
|
25
|
+
|
|
26
|
+
Run: 0 / ${runLimit}
|
|
27
|
+
Status: not started
|
|
28
|
+
|
|
29
|
+
## Contract
|
|
30
|
+
- Goal: ship the units below, most valuable first, without breaking what already passes.
|
|
31
|
+
- Done when: \`npm test\` passes and every unit has a test that proves it.
|
|
32
|
+
- Constraints: see CLAUDE.md hard rules.
|
|
33
|
+
- Out of scope: anything not listed under Units.
|
|
34
|
+
|
|
35
|
+
## Units, in order
|
|
36
|
+
${units}
|
|
37
|
+
|
|
38
|
+
## Decisions
|
|
39
|
+
|
|
40
|
+
## Confirmed
|
|
41
|
+
|
|
42
|
+
## Guesses
|
|
43
|
+
|
|
44
|
+
## Tried
|
|
45
|
+
|
|
46
|
+
## Handoff
|
|
47
|
+
(each run ends by writing: where it stopped, the exact next step, anything half-done)
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=rounds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rounds.js","sourceRoot":"","sources":["../../src/core/rounds.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,sGAAsG;AACtG,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACtC,mGAAmG;IACnG,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,kCAAkC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACvF,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAC5B,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC7D,OAAO,SAAS,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AACxE,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,gBAAgB,CAAC,SAA4B;IAC3D,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QACzC,MAAM,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACzC,CAAC,CAAC,CAAC;IACH,OAAO,SAAS,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;AACnD,CAAC;AAED,wGAAwG;AACxG,MAAM,UAAU,kBAAkB,CAAC,KAAwB,EAAE,KAAa,EAAE,QAAQ,GAAG,EAAE;IACvF,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrE,OAAO,iBAAiB,KAAK;;WAEpB,QAAQ;;;;;;;;;;EAUjB,KAAK;;;;;;;;;;;;CAYN,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { fillPlaceholders } from './template.js';
|
|
2
|
+
/** Used only when neither the repo nor the package ships a prompt file. A copy of runners/routine/prompt.md; a test keeps them equal. */
|
|
3
|
+
export const DEFAULT_PROMPT = `You are one scheduled run of the build loop for this repository ({{name}}). Nobody is watching; the owner reads the results later. Read CLAUDE.md and follow it exactly, especially "Unattended mode". IDEA.md is the brief. Your task file is \`.ai/task.md\`.
|
|
4
|
+
|
|
5
|
+
0. Run \`date -u\` and note it as this run's start time. Run \`git checkout main && git pull --ff-only\`. Every commit goes to \`main\` and is pushed with \`git push origin main\`. Never create or push a \`claude/\` branch: the next run starts from \`main\` and would never see it. If a push is rejected, run \`git pull --rebase\`, re-run the checks if incoming commits touched code, and push again.
|
|
6
|
+
1. Session lock: if \`.ai/session.lock\` holds a UTC time less than 90 minutes old, another session owns the repo, unless the lock is your own (the runner's run note says so when the loop runner created it). Otherwise reply "Nothing to do" and stop. If the file is older than that, it is stale: ignore it.
|
|
7
|
+
2. If \`DONE.md\` or \`BLOCKED.md\` exists, reply "Nothing to do" and stop.
|
|
8
|
+
3. Run the install command from CLAUDE.md (\`npm install\` unless it says otherwise). If it fails, write \`BLOCKED.md\` with the error and stop.
|
|
9
|
+
4. If \`.ai/task.md\` exists, run the resume protocol (.ai/method.md §9.2) and add 1 to its \`Run: N / 30\` counter. When the counter reaches 30, write \`BLOCKED.md\` saying "run budget reached", push, and stop. If there is no task file, write one from IDEA.md first.
|
|
10
|
+
5. Run the test command from CLAUDE.md (\`npm test\` unless it says otherwise) on the fresh pull. If it fails, fixing it is your first unit ("main stays green"). Otherwise take the next unit from the task file. Do one unit at a time.
|
|
11
|
+
6. After each unit run the test command from CLAUDE.md (\`npm test\` unless it says otherwise). Green: commit, have a reviewer subagent that did not write the code check the diff against IDEA.md ("find the three most likely ways this is wrong for a real user, and check each"), fix any blockers, run the tests again, pull, then push to \`main\`. Red: write the attempt and the reason under Tried in the task file, then discard the unit's uncommitted changes. The same failure twice means the stuck protocol (§9.3); its last step is \`BLOCKED.md\`.
|
|
12
|
+
7. A pushed unit does not end the run. Keep taking units until one of these is true: 40 minutes have passed since the start time (check \`date -u\` after each push), your context is about 60% full, or no unit is left.
|
|
13
|
+
8. Run the handoff (§9.1) in the task file so a fresh run with zero memory can continue at full quality. Commit it, push to \`main\`, and stop. If every done-when in the contract passes and the reviewer finds no blockers, run the retro (§12), write \`DONE.md\` (what was built, test results, anything unsure, the 10 best next improvements ranked by value) and push instead.
|
|
14
|
+
|
|
15
|
+
Never spend money, sign up for anything, create accounts, publish anything, add API keys, secrets, ads, analytics or tracking, copy other projects' content without a compatible licence, force-push, rewrite history, or touch anything outside this repository.`;
|
|
16
|
+
/** Fills `{{name}}`-style placeholders and normalises the ending. */
|
|
17
|
+
export function buildRunPrompt(promptText, vars) {
|
|
18
|
+
return `${fillPlaceholders(promptText, vars).trim()}\n`;
|
|
19
|
+
}
|
|
20
|
+
/** Tools an unattended round needs (git, npm test, edits, reviewer subagent, web search). Passed explicitly so `claude -p` can run them headless. */
|
|
21
|
+
export const ROUND_TOOLS = ['Bash', 'Edit', 'Write', 'Read', 'Glob', 'Grep', 'Agent', 'WebSearch'];
|
|
22
|
+
/** Argument array for `claude`; the prompt is a single argv entry, never part of a shell string. */
|
|
23
|
+
export function buildClaudeArgs(prompt, options) {
|
|
24
|
+
return [
|
|
25
|
+
'-p',
|
|
26
|
+
prompt,
|
|
27
|
+
...(options.model ? ['--model', options.model] : []),
|
|
28
|
+
...(options.skipPermissions ? ['--dangerously-skip-permissions'] : ['--permission-mode', 'acceptEdits', '--allowedTools', ROUND_TOOLS.join(',')]),
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=runPrompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runPrompt.js","sourceRoot":"","sources":["../../src/core/runPrompt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjD,yIAAyI;AACzI,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;kQAYoO,CAAC;AAEnQ,qEAAqE;AACrE,MAAM,UAAU,cAAc,CAAC,UAAkB,EAAE,IAAsC;IACvF,OAAO,GAAG,gBAAgB,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC;AAC1D,CAAC;AAQD,qJAAqJ;AACrJ,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,CAAU,CAAC;AAE5G,oGAAoG;AACpG,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,OAA0B;IACxE,OAAO;QACL,IAAI;QACJ,MAAM;QACN,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACpD,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,gCAAgC,CAAC,CAAC,CAAC,CAAC,CAAC,mBAAmB,EAAE,aAAa,EAAE,gBAAgB,EAAE,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;KAClJ,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const LOCK_MINUTES = 90;
|
|
2
|
+
export const RECENT_COMMIT_MINUTES = 60;
|
|
3
|
+
const minutesBetween = (later, earlier) => (later.getTime() - earlier.getTime()) / 60_000;
|
|
4
|
+
/** Applies the status rules from IDEA.md in order; a failing health check overrides everything. */
|
|
5
|
+
export function deriveStatus(facts, now = new Date()) {
|
|
6
|
+
const [firstFailure] = facts.healthFailures;
|
|
7
|
+
if (firstFailure !== undefined)
|
|
8
|
+
return { state: 'failing', reason: firstFailure };
|
|
9
|
+
if (facts.hasBlocked)
|
|
10
|
+
return { state: 'blocked', reason: 'BLOCKED.md is waiting for an answer' };
|
|
11
|
+
if (facts.hasDone)
|
|
12
|
+
return { state: 'done', reason: `Round ${facts.roundsDone + 1} finished` };
|
|
13
|
+
if (facts.lockAt && minutesBetween(now, facts.lockAt) < LOCK_MINUTES) {
|
|
14
|
+
return { state: 'building', reason: 'A session holds the lock' };
|
|
15
|
+
}
|
|
16
|
+
if (facts.lastCommitAt && minutesBetween(now, facts.lastCommitAt) < RECENT_COMMIT_MINUTES) {
|
|
17
|
+
return { state: 'building', reason: 'Committed in the last hour' };
|
|
18
|
+
}
|
|
19
|
+
return { state: 'waiting', reason: 'Waiting for the next scheduled run' };
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/core/status.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,CAAC;AAC/B,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,CAAC;AAExC,MAAM,cAAc,GAAG,CAAC,KAAW,EAAE,OAAa,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,GAAG,MAAM,CAAC;AAEtG,mGAAmG;AACnG,MAAM,UAAU,YAAY,CAAC,KAAgB,EAAE,GAAG,GAAS,IAAI,IAAI,EAAE;IACnE,MAAM,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC,cAAc,CAAC;IAC5C,IAAI,YAAY,KAAK,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IAClF,IAAI,KAAK,CAAC,UAAU;QAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,qCAAqC,EAAE,CAAC;IACjG,IAAI,KAAK,CAAC,OAAO;QAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,KAAK,CAAC,UAAU,GAAG,CAAC,WAAW,EAAE,CAAC;IAC9F,IAAI,KAAK,CAAC,MAAM,IAAI,cAAc,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,YAAY,EAAE,CAAC;QACrE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,0BAA0B,EAAE,CAAC;IACnE,CAAC;IACD,IAAI,KAAK,CAAC,YAAY,IAAI,cAAc,CAAC,GAAG,EAAE,KAAK,CAAC,YAAY,CAAC,GAAG,qBAAqB,EAAE,CAAC;QAC1F,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,4BAA4B,EAAE,CAAC;IACrE,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,oCAAoC,EAAE,CAAC;AAC5E,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { deriveStatus } from './status.js';
|
|
2
|
+
const ICON = { blocked: '!', failing: 'x', done: '+', building: '~', waiting: '-' };
|
|
3
|
+
// Needs-you states first: blocked, failing, done, then the quiet ones.
|
|
4
|
+
const ORDER = ['blocked', 'failing', 'done', 'building', 'waiting'];
|
|
5
|
+
/** Plain-text fleet table; status is always a word plus an icon, never colour alone. */
|
|
6
|
+
export function formatFleet(rows, now = new Date()) {
|
|
7
|
+
const lines = rows
|
|
8
|
+
.map((row) => ({ row, status: deriveStatus(row.facts, now) }))
|
|
9
|
+
.sort((a, b) => ORDER.indexOf(a.status.state) - ORDER.indexOf(b.status.state) || a.row.name.localeCompare(b.row.name))
|
|
10
|
+
.map(({ row, status }) => [
|
|
11
|
+
row.name,
|
|
12
|
+
`${ICON[status.state]} ${status.state}`,
|
|
13
|
+
`round ${row.facts.roundsDone + 1}`,
|
|
14
|
+
row.run ? `run ${row.run.run}/${row.run.limit}` : 'run -',
|
|
15
|
+
status.reason,
|
|
16
|
+
]);
|
|
17
|
+
const header = ['LOOP', 'STATUS', 'ROUND', 'RUN', 'WHY'];
|
|
18
|
+
const widths = header.map((h, i) => Math.max(h.length, ...lines.map((l) => (l[i] ?? '').length)));
|
|
19
|
+
const render = (cols) => cols.map((c, i) => c.padEnd(widths[i] ?? 0)).join(' ').trimEnd();
|
|
20
|
+
const needs = lines.filter((l) => /blocked|failing/.test(l[1] ?? '')).length;
|
|
21
|
+
const summary = needs === 0 ? 'Nothing needs you.' : `${needs} ${needs === 1 ? 'loop needs' : 'loops need'} you.`;
|
|
22
|
+
return [summary, '', render(header), ...lines.map(render)].join('\n');
|
|
23
|
+
}
|
|
24
|
+
const LANE_ICON = { blocked: '!', stalled: 'x', done: '+', building: '~', waiting: '-' };
|
|
25
|
+
/** One row per lane, for `loop status` on a laned loop. */
|
|
26
|
+
export function formatLanes(lanes, now = new Date()) {
|
|
27
|
+
const ago = (iso) => {
|
|
28
|
+
const minutes = Math.max(0, Math.round((now.getTime() - new Date(iso).getTime()) / 60_000));
|
|
29
|
+
return minutes < 60 ? `${minutes}m ago` : minutes < 2880 ? `${Math.round(minutes / 60)}h ago` : `${Math.round(minutes / 1440)}d ago`;
|
|
30
|
+
};
|
|
31
|
+
const rows = lanes.map((l) => [
|
|
32
|
+
l.name,
|
|
33
|
+
`${LANE_ICON[l.state]} ${l.state}`,
|
|
34
|
+
l.limit > 0 ? `run ${l.run}/${l.limit}` : 'run -',
|
|
35
|
+
l.lastCommit ? `${ago(l.lastCommit.at)}: ${l.lastCommit.subject.slice(0, 48)}` : 'no lane commits yet',
|
|
36
|
+
l.waitingOn?.length ? `waits on ${l.waitingOn.join(', ')}` : l.unanswered.length ? `${l.unanswered.length} unanswered` : '',
|
|
37
|
+
]);
|
|
38
|
+
const header = ['LANE', 'STATUS', 'RUN', 'LAST COMMIT', 'MESSAGES'];
|
|
39
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length)));
|
|
40
|
+
const render = (cols) => cols.map((c, i) => c.padEnd(widths[i] ?? 0)).join(' ').trimEnd();
|
|
41
|
+
return [render(header), ...rows.map(render)].join('\n');
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table.js","sourceRoot":"","sources":["../../src/core/table.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Pure helpers for `loop new`: names, placeholders and which files to fill.
|
|
2
|
+
/** Lowercase letters, digits, `-` and `_`; becomes a folder name and a GitHub repo name. */
|
|
3
|
+
const PROJECT_NAME = /^[a-z0-9][a-z0-9_-]{0,99}$/;
|
|
4
|
+
export function validateProjectName(name) {
|
|
5
|
+
return PROJECT_NAME.test(name)
|
|
6
|
+
? undefined
|
|
7
|
+
: `"${name}" is not a valid name: use lowercase letters, digits, "-" or "_" (max 100 characters, starting with a letter or digit).`;
|
|
8
|
+
}
|
|
9
|
+
const TEXT_EXTENSIONS = new Set(['.md', '.json', '.js', '.mjs', '.ts', '.html', '.css', '.txt', '.xml', '.toml', '.yml', '.yaml', '.gitignore']);
|
|
10
|
+
/** True for files whose `{{placeholders}}` should be filled; everything else is copied byte for byte. */
|
|
11
|
+
export function isTextFile(fileName) {
|
|
12
|
+
const dot = fileName.lastIndexOf('.');
|
|
13
|
+
return dot !== -1 && TEXT_EXTENSIONS.has(fileName.slice(dot).toLowerCase());
|
|
14
|
+
}
|
|
15
|
+
/** Replaces `{{key}}` with its value; unknown keys are left as they are so a typo stays visible. */
|
|
16
|
+
export function fillPlaceholders(text, vars) {
|
|
17
|
+
return text.replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (whole, key) => vars[key] ?? whole);
|
|
18
|
+
}
|
|
19
|
+
/** Names never copied out of a template. */
|
|
20
|
+
export const TEMPLATE_IGNORED = new Set(['node_modules', '.git', 'test-results', 'playwright-report']);
|
|
21
|
+
/** npm drops `.gitignore` from tarballs, so templates ship it as `gitignore`; this is the name it gets in a new repo. */
|
|
22
|
+
export function templateTargetName(fileName) {
|
|
23
|
+
return fileName === 'gitignore' ? '.gitignore' : fileName;
|
|
24
|
+
}
|
|
25
|
+
/** Hostname-safe form of a project name (`_` becomes `-`, no leading/trailing `-`, at most 63 characters) for `{{host}}`. */
|
|
26
|
+
export function hostLabel(name) {
|
|
27
|
+
return name.replace(/_/g, '-').slice(0, 63).replace(/^-+|-+$/g, '');
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template.js","sourceRoot":"","sources":["../../src/core/template.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAE5E,4FAA4F;AAC5F,MAAM,YAAY,GAAG,4BAA4B,CAAC;AAElD,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;QAC5B,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,IAAI,IAAI,yHAAyH,CAAC;AACxI,CAAC;AAED,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;AAEjJ,yGAAyG;AACzG,MAAM,UAAU,UAAU,CAAC,QAAgB;IACzC,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACtC,OAAO,GAAG,KAAK,CAAC,CAAC,IAAI,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;AAC9E,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,IAAsC;IACnF,OAAO,IAAI,CAAC,OAAO,CAAC,0BAA0B,EAAE,CAAC,KAAK,EAAE,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,CAAC;AAC9F,CAAC;AAED,4CAA4C;AAC5C,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,MAAM,EAAE,cAAc,EAAE,mBAAmB,CAAC,CAAC,CAAC;AAEvG,yHAAyH;AACzH,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,OAAO,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC5D,CAAC;AAED,6HAA6H;AAC7H,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AACtE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,uHAAuH"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
/** Fleet order from the contract: what needs the owner first. */
|
|
3
|
+
const ATTENTION_ORDER = ['failing', 'blocked', 'building', 'waiting', 'done'];
|
|
4
|
+
export function sortByAttention(loops) {
|
|
5
|
+
return [...loops].sort((a, b) => ATTENTION_ORDER.indexOf(a.state) - ATTENTION_ORDER.indexOf(b.state) || a.name.localeCompare(b.name));
|
|
6
|
+
}
|
|
7
|
+
/** The detail-only fields stay out of the fleet list. */
|
|
8
|
+
export function toSummary(detail) {
|
|
9
|
+
const { timeline: _timeline, contract: _contract, knowledge: _knowledge, decisions: _decisions, history: _history, ...summary } = detail;
|
|
10
|
+
return summary;
|
|
11
|
+
}
|
|
12
|
+
export function snapshotSource(snapshot) {
|
|
13
|
+
return {
|
|
14
|
+
loops: async () => sortByAttention(snapshot.loops.map(toSummary)),
|
|
15
|
+
loop: async (id) => snapshot.loops.find((l) => l.id === id),
|
|
16
|
+
inbox: async () => snapshot.inbox,
|
|
17
|
+
checks: async () => snapshot.checks,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async function readFirst(candidates) {
|
|
21
|
+
for (const file of candidates) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(await readFile(file, 'utf8'));
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code !== 'ENOENT')
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Appends the sixth loop (this repo building itself) after the five sites. Its checks are left out for now:
|
|
34
|
+
* web's health e2e asserts exactly the five sites' 10 checks (see outbox).
|
|
35
|
+
*/
|
|
36
|
+
export function withSelfLoop(snapshot, self) {
|
|
37
|
+
return { ...snapshot, loops: [...snapshot.loops, self.loop] };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Reads the demo snapshot: the five sites plus, when present, the sixth loop (this repo). In the repo they live
|
|
41
|
+
* in `demo/`; in the published package the build copies them next to the compiled server, because `demo/` does not ship.
|
|
42
|
+
*/
|
|
43
|
+
export async function loadDemoSnapshot(candidates = [new URL('./five-sites.json', import.meta.url), new URL('../../demo/five-sites.json', import.meta.url)], selfCandidates = [new URL('./this-repo.json', import.meta.url), new URL('../../demo/this-repo.json', import.meta.url)]) {
|
|
44
|
+
const snapshot = await readFirst(candidates);
|
|
45
|
+
if (!snapshot)
|
|
46
|
+
throw new Error('demo snapshot not found (run `node demo/build-demo.mjs`)');
|
|
47
|
+
const self = await readFirst(selfCandidates);
|
|
48
|
+
return self ? withSelfLoop(snapshot, self) : snapshot;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=data.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data.js","sourceRoot":"","sources":["../../src/server/data.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { findLoopDirs, readTimeline } from './loops.js';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
/** A tiny in-process pub/sub; each SSE connection subscribes for its lifetime. */
|
|
8
|
+
export class EventBus {
|
|
9
|
+
listeners = new Set();
|
|
10
|
+
subscribe(listener) {
|
|
11
|
+
this.listeners.add(listener);
|
|
12
|
+
return () => void this.listeners.delete(listener);
|
|
13
|
+
}
|
|
14
|
+
emit(event) {
|
|
15
|
+
for (const listener of this.listeners)
|
|
16
|
+
listener(event);
|
|
17
|
+
}
|
|
18
|
+
get size() {
|
|
19
|
+
return this.listeners.size;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const WATCHED_FILES = ['BLOCKED.md', 'DONE.md', path.join('.ai', 'session.lock'), path.join('.ai', 'task.md')];
|
|
23
|
+
/** A string that changes whenever a loop's HEAD or one of its status files changes. */
|
|
24
|
+
export async function fingerprint(dir) {
|
|
25
|
+
const [head] = await readTimeline(dir, 1);
|
|
26
|
+
// A `git fetch` only moves the upstream ref, so include it: the owner sees a push before pulling.
|
|
27
|
+
const upstream = await execFileAsync('git', ['rev-parse', '--short', '@{u}'], { cwd: dir }).then((r) => r.stdout.trim(), () => 'no-upstream');
|
|
28
|
+
const stamps = await Promise.all(WATCHED_FILES.map(async (file) => {
|
|
29
|
+
const info = await stat(path.join(dir, file)).catch(() => undefined);
|
|
30
|
+
return info ? `${file}:${info.mtimeMs}:${info.size}` : `${file}:-`;
|
|
31
|
+
}));
|
|
32
|
+
return [head?.sha ?? 'no-commits', upstream, ...stamps].join('|');
|
|
33
|
+
}
|
|
34
|
+
export const DEFAULT_WATCH = { intervalMs: 5_000, fetchIntervalMs: 5 * 60_000 };
|
|
35
|
+
/** Fetches quietly; a clone without a remote or network simply stays as it is. */
|
|
36
|
+
async function fetchClone(dir) {
|
|
37
|
+
await execFileAsync('git', ['fetch', '--quiet'], { cwd: dir, timeout: 60_000, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } }).catch(() => undefined);
|
|
38
|
+
}
|
|
39
|
+
const FETCH_CONCURRENCY = 4;
|
|
40
|
+
/** Fetches every clone, a few at a time so many clones do not start many git processes at once. */
|
|
41
|
+
async function fetchAll(projectsDir) {
|
|
42
|
+
const queue = await findLoopDirs(projectsDir);
|
|
43
|
+
const worker = async () => {
|
|
44
|
+
for (let dir = queue.shift(); dir !== undefined; dir = queue.shift())
|
|
45
|
+
await fetchClone(dir);
|
|
46
|
+
};
|
|
47
|
+
await Promise.all(Array.from({ length: FETCH_CONCURRENCY }, worker));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Polls every clone in `projectsDir` and emits events on change. Polling (not fs.watch) keeps it
|
|
51
|
+
* identical on Windows, macOS and Linux and also sees commits made by `git pull`. Returns a stop function
|
|
52
|
+
* that resolves once any poll already running has finished (on Windows a running `git` keeps the clone folder open).
|
|
53
|
+
*/
|
|
54
|
+
export function watchProjects(projectsDir, bus, options = DEFAULT_WATCH) {
|
|
55
|
+
const seen = new Map();
|
|
56
|
+
const blocked = new Set();
|
|
57
|
+
let first = true;
|
|
58
|
+
let running;
|
|
59
|
+
let stopped = false;
|
|
60
|
+
const tick = () => {
|
|
61
|
+
if (running || stopped)
|
|
62
|
+
return running ?? Promise.resolve();
|
|
63
|
+
running = poll().finally(() => {
|
|
64
|
+
running = undefined;
|
|
65
|
+
});
|
|
66
|
+
return running;
|
|
67
|
+
};
|
|
68
|
+
const poll = async () => {
|
|
69
|
+
const dirs = await findLoopDirs(projectsDir);
|
|
70
|
+
const nowBlocked = new Set();
|
|
71
|
+
for (const dir of dirs) {
|
|
72
|
+
const id = path.basename(dir);
|
|
73
|
+
// One unreadable clone must not stop the others from being checked.
|
|
74
|
+
const print = await fingerprint(dir).catch(() => seen.get(id) ?? 'unreadable');
|
|
75
|
+
if (!print.includes('BLOCKED.md:-'))
|
|
76
|
+
nowBlocked.add(id);
|
|
77
|
+
if (!first && seen.get(id) !== print)
|
|
78
|
+
bus.emit({ type: 'loop-updated', id });
|
|
79
|
+
seen.set(id, print);
|
|
80
|
+
}
|
|
81
|
+
for (const id of [...seen.keys()])
|
|
82
|
+
if (!dirs.some((d) => path.basename(d) === id))
|
|
83
|
+
seen.delete(id);
|
|
84
|
+
const inboxChanged = nowBlocked.size !== blocked.size || [...nowBlocked].some((id) => !blocked.has(id));
|
|
85
|
+
blocked.clear();
|
|
86
|
+
for (const id of nowBlocked)
|
|
87
|
+
blocked.add(id);
|
|
88
|
+
if (!first && inboxChanged)
|
|
89
|
+
bus.emit({ type: 'inbox-changed' });
|
|
90
|
+
first = false;
|
|
91
|
+
options.onPoll?.();
|
|
92
|
+
};
|
|
93
|
+
const timer = setInterval(() => void tick().catch(() => undefined), options.intervalMs);
|
|
94
|
+
const fetcher = options.fetchIntervalMs > 0
|
|
95
|
+
? setInterval(() => void fetchAll(projectsDir).catch(() => undefined), options.fetchIntervalMs)
|
|
96
|
+
: undefined;
|
|
97
|
+
void tick().catch(() => undefined);
|
|
98
|
+
return async () => {
|
|
99
|
+
stopped = true;
|
|
100
|
+
clearInterval(timer);
|
|
101
|
+
if (fetcher)
|
|
102
|
+
clearInterval(fetcher);
|
|
103
|
+
await running?.catch(() => undefined);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=events.js.map
|