compounding-loop 0.0.0-stage → 0.1.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 +93 -3
- package/bin/loop.js +2 -0
- package/dist/cli/dashboard.js +52 -0
- package/dist/cli/dashboard.js.map +1 -0
- package/dist/cli/gh.js +29 -0
- package/dist/cli/gh.js.map +1 -0
- package/dist/cli/index.js +144 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.js +72 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/loops.js +72 -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 +81 -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/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/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 +24 -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 +36 -0
- package/dist/server/data.js.map +1 -0
- package/dist/server/events.js +105 -0
- package/dist/server/events.js.map +1 -0
- package/dist/server/five-sites.json +3071 -0
- package/dist/server/health.js +133 -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 +142 -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/web/assets/index-BDdtnG33.js +49 -0
- package/dist/web/assets/index-D3-N8DN4.css +2 -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 +569 -0
- package/method/example/K-0001.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 +71 -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/loop.yml +51 -0
- package/runners/local/README.md +8 -0
- package/runners/local/crontab.txt +6 -0
- package/runners/local/task-scheduler.xml +32 -0
- package/runners/routine/README.md +19 -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/manifest.json +9 -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/static-site/.ai/index.md +5 -0
- package/templates/static-site/.ai/log.md +3 -0
- package/templates/static-site/.ai/loop-prompt.md +10 -0
- package/templates/static-site/.ai/task.md +27 -0
- package/templates/static-site/.gitattributes +1 -0
- package/templates/static-site/CLAUDE.md +113 -0
- package/templates/static-site/IDEA.md +36 -0
- package/templates/static-site/gitignore +6 -0
- package/templates/static-site/netlify.toml +14 -0
- package/templates/static-site/package.json +14 -0
- package/templates/static-site/playwright.config.js +27 -0
- package/templates/static-site/scripts/check.mjs +188 -0
- package/templates/static-site/scripts/serve.mjs +44 -0
- package/templates/static-site/scripts/test.mjs +59 -0
- package/templates/static-site/site/index.html +23 -0
- package/templates/static-site/site/robots.txt +4 -0
- package/templates/static-site/site/sitemap.xml +4 -0
- package/templates/static-site/tests/pages.spec.js +21 -0
- package/templates/static-site/vercel.json +19 -0
package/dist/cli/run.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { appendFile, mkdir, readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseLockTime } from '../core/repo.js';
|
|
5
|
+
import { LOCK_MINUTES } from '../core/status.js';
|
|
6
|
+
import { buildClaudeArgs, buildRunPrompt, DEFAULT_PROMPT } from '../core/runPrompt.js';
|
|
7
|
+
import { packageRoot } from './init.js';
|
|
8
|
+
const exists = (file) => stat(file).then(() => true, () => false);
|
|
9
|
+
/** 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) {
|
|
11
|
+
const candidates = [
|
|
12
|
+
path.join(dir, '.ai', 'loop-prompt.md'),
|
|
13
|
+
path.join(dir, 'runners', 'routine', 'prompt.md'),
|
|
14
|
+
path.join(root, 'runners', 'routine', 'prompt.md'),
|
|
15
|
+
];
|
|
16
|
+
for (const file of candidates) {
|
|
17
|
+
const text = await readFile(file, 'utf8').catch(() => undefined);
|
|
18
|
+
if (text !== undefined)
|
|
19
|
+
return { text, source: file };
|
|
20
|
+
}
|
|
21
|
+
return { text: DEFAULT_PROMPT, source: 'built-in default' };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Finds an executable on PATH. Node cannot start `.cmd` shims without a shell (and we never build shell strings),
|
|
25
|
+
* so on Windows only `.exe` files count; a lone `claude.cmd` gets an explanatory error.
|
|
26
|
+
*/
|
|
27
|
+
export async function resolveExecutable(name, platform = process.platform, pathEnv = process.env.PATH ?? '', has = exists) {
|
|
28
|
+
if (platform !== 'win32')
|
|
29
|
+
return name;
|
|
30
|
+
const dirs = pathEnv.split(path.win32.delimiter).filter(Boolean);
|
|
31
|
+
for (const dir of dirs)
|
|
32
|
+
if (await has(path.win32.join(dir, `${name}.exe`)))
|
|
33
|
+
return path.win32.join(dir, `${name}.exe`);
|
|
34
|
+
for (const dir of dirs) {
|
|
35
|
+
if (await has(path.win32.join(dir, `${name}.cmd`))) {
|
|
36
|
+
throw new Error(`Found ${name}.cmd but Node cannot start it safely. Install the native ${name}.exe build of Claude Code, or run the round from a terminal with \`${name} -p\`.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`Could not find ${name}.exe on PATH. Is Claude Code installed?`);
|
|
40
|
+
}
|
|
41
|
+
const realSpawn = async (command, args, cwd) => {
|
|
42
|
+
const executable = await resolveExecutable(command);
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const child = spawn(executable, args, { cwd, stdio: 'inherit' });
|
|
45
|
+
child.on('error', (error) => reject(new Error(`Could not start \`${command}\`: ${error.message}. Is Claude Code installed and on your PATH?`)));
|
|
46
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
/** Minutes left on a fresh `.ai/session.lock`, or undefined when no session holds the repo. */
|
|
50
|
+
export function lockMinutesLeft(lockText, now) {
|
|
51
|
+
const at = lockText === undefined ? undefined : parseLockTime(lockText);
|
|
52
|
+
if (!at)
|
|
53
|
+
return undefined;
|
|
54
|
+
const left = LOCK_MINUTES - (now.getTime() - at.getTime()) / 60_000;
|
|
55
|
+
return left > 0 ? Math.ceil(left) : undefined;
|
|
56
|
+
}
|
|
57
|
+
/** One `{"startedAt","endedAt"}` line per round in `.ai/runs.jsonl`; the dashboard's health checks read it. */
|
|
58
|
+
export async function recordRun(dir, startedAt, endedAt) {
|
|
59
|
+
await mkdir(path.join(dir, '.ai'), { recursive: true });
|
|
60
|
+
await appendFile(path.join(dir, '.ai', 'runs.jsonl'), `${JSON.stringify({ startedAt: startedAt.toISOString(), endedAt: endedAt.toISOString() })}\n`);
|
|
61
|
+
}
|
|
62
|
+
/** `loop run`: one build round now, using the same prompt the scheduler uses. */
|
|
63
|
+
export async function runRound(dir, options, spawner = realSpawn) {
|
|
64
|
+
const { text, source } = await loadPromptText(dir);
|
|
65
|
+
const prompt = buildRunPrompt(text, { name: path.basename(path.resolve(dir)) });
|
|
66
|
+
for (const stop of ['DONE.md', 'BLOCKED.md']) {
|
|
67
|
+
if (await exists(path.join(dir, stop)))
|
|
68
|
+
return { skipped: `${stop} exists: nothing to do`, prompt, source };
|
|
69
|
+
}
|
|
70
|
+
const lockLeft = lockMinutesLeft(await readFile(path.join(dir, '.ai', 'session.lock'), 'utf8').catch(() => undefined), new Date());
|
|
71
|
+
if (lockLeft !== undefined)
|
|
72
|
+
return { skipped: `another session holds .ai/session.lock (about ${lockLeft} min left): nothing to do`, prompt, source };
|
|
73
|
+
if (options.dryRun)
|
|
74
|
+
return { prompt, source };
|
|
75
|
+
console.error(`Running one round in ${dir} (prompt: ${source})`);
|
|
76
|
+
const startedAt = new Date();
|
|
77
|
+
const exitCode = await spawner('claude', buildClaudeArgs(prompt, { skipPermissions: options.skipPermissions, ...(options.model && { model: options.model }) }), dir);
|
|
78
|
+
await recordRun(dir, startedAt, new Date());
|
|
79
|
+
return { prompt, source, exitCode };
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
function sectionText(text, label) {
|
|
2
|
+
const lines = text.split(/\r?\n/);
|
|
3
|
+
const start = lines.findIndex((l) => label.test(l.replace(/^#+\s*|\*\*/g, '').trim()));
|
|
4
|
+
if (start === -1)
|
|
5
|
+
return '';
|
|
6
|
+
const first = (lines[start] ?? '').replace(/^#+\s*/, '').replace(/\*\*/g, '');
|
|
7
|
+
const inline = first.replace(/^[^:]*:/, '').trim();
|
|
8
|
+
const body = inline && first.includes(':') ? [inline] : [];
|
|
9
|
+
for (const line of lines.slice(start + 1)) {
|
|
10
|
+
if (/^#+\s/.test(line) || /^\s*\*\*[^*]+\*\*:?/.test(line))
|
|
11
|
+
break;
|
|
12
|
+
body.push(line);
|
|
13
|
+
}
|
|
14
|
+
return body.join('\n').trim();
|
|
15
|
+
}
|
|
16
|
+
/** Pulls the question and best guess out of a BLOCKED.md, tolerating headings or bold labels. */
|
|
17
|
+
export function parseBlocked(text) {
|
|
18
|
+
const question = sectionText(text, /^(the )?(specific )?question/i);
|
|
19
|
+
const bestGuess = sectionText(text, /^(my |the )?best guess/i);
|
|
20
|
+
return { question: question || text.trim(), bestGuess, raw: text };
|
|
21
|
+
}
|
|
22
|
+
/** The block appended to task.md so the next run sees the owner's answer as a decision. */
|
|
23
|
+
export function formatAnswer(note, answer, date) {
|
|
24
|
+
const question = note.question.split(/\r?\n/)[0] ?? '';
|
|
25
|
+
return `### Owner answer, ${date}\nQuestion: ${question}\nAnswer: ${answer.trim()}\n`;
|
|
26
|
+
}
|
|
27
|
+
/** Inserts an answer block under "## Decisions" (or at the end when the file has none). */
|
|
28
|
+
export function addAnswerToTask(taskText, block) {
|
|
29
|
+
const match = /^## Decisions[^\n]*\n/m.exec(taskText);
|
|
30
|
+
if (!match)
|
|
31
|
+
return `${taskText.replace(/\s*$/, '')}\n\n## Decisions\n${block}`;
|
|
32
|
+
const at = match.index + match[0].length;
|
|
33
|
+
return `${taskText.slice(0, at)}${block}\n${taskText.slice(at)}`;
|
|
34
|
+
}
|
|
35
|
+
export const BUDGET_EXTENSION = 30;
|
|
36
|
+
/** True when BLOCKED.md was written because the run budget ran out (it has no real question). */
|
|
37
|
+
export const isBudgetBlock = (text) => /run budget/i.test(text);
|
|
38
|
+
/** Raises the limit of `Run: N / LIMIT` to N + BUDGET_EXTENSION; leaves other text alone. */
|
|
39
|
+
export function extendRunBudget(taskText, extra = BUDGET_EXTENSION) {
|
|
40
|
+
return taskText.replace(/^Run:\s*(\d+)\s*\/\s*\d+/m, (_whole, run) => `Run: ${run} / ${Number(run) + extra}`);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=blocked.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blocked.js","sourceRoot":"","sources":["../../src/core/blocked.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Brief linter. Pure and free of Node imports: the web wizard runs the same code in the browser.
|
|
2
|
+
export const MIN_BRIEF_SCORE = 60;
|
|
3
|
+
const VAGUE_WORDS = /\b(good|nice|great|modern|fast|clean|user-friendly|intuitive|robust|scalable|beautiful)\b/i;
|
|
4
|
+
const MIN_WORDS = 120;
|
|
5
|
+
/** Bold labels (`**Goal:** text`) count as the deepest heading level. */
|
|
6
|
+
const LABEL_LEVEL = 7;
|
|
7
|
+
function splitSections(text) {
|
|
8
|
+
const flat = [];
|
|
9
|
+
for (const line of text.split(/\r?\n/)) {
|
|
10
|
+
const heading = /^(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line);
|
|
11
|
+
const label = /^\s*\*\*(.+?)\*\*:?\s*(.*)$/.exec(line) ?? /^\s*\*\*(.+?):\*\*\s*(.*)$/.exec(line);
|
|
12
|
+
if (heading)
|
|
13
|
+
flat.push({ heading: heading[2] ?? '', level: (heading[1] ?? '#').length, own: '' });
|
|
14
|
+
else if (label && !/^\s*[-*+]\s/.test(line))
|
|
15
|
+
flat.push({ heading: (label[1] ?? '').replace(/:$/, ''), level: LABEL_LEVEL, own: `${label[2] ?? ''}\n` });
|
|
16
|
+
else {
|
|
17
|
+
const last = flat[flat.length - 1];
|
|
18
|
+
if (last)
|
|
19
|
+
last.own += `${line}\n`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return flat.map((section, i) => {
|
|
23
|
+
let body = section.own;
|
|
24
|
+
for (const later of flat.slice(i + 1)) {
|
|
25
|
+
if (later.level <= section.level)
|
|
26
|
+
break;
|
|
27
|
+
body += later.own;
|
|
28
|
+
}
|
|
29
|
+
return { heading: section.heading, body };
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const findSection = (sections, pattern) => sections.find((s) => pattern.test(s.heading));
|
|
33
|
+
/** Bullet and numbered-list items of a section body, without their markers. */
|
|
34
|
+
export function listItems(body) {
|
|
35
|
+
return body
|
|
36
|
+
.split(/\r?\n/)
|
|
37
|
+
.map((line) => /^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.+)$/.exec(line)?.[1]?.trim())
|
|
38
|
+
.filter((item) => Boolean(item));
|
|
39
|
+
}
|
|
40
|
+
/** A must-have is vague when it is very short, or leans on a fuzzy word with no number, path or command to check. */
|
|
41
|
+
export function isVagueItem(item) {
|
|
42
|
+
const hasEvidence = /`[^`]+`|\d/.test(item);
|
|
43
|
+
if (item.split(/\s+/).length < 4 && !hasEvidence)
|
|
44
|
+
return true;
|
|
45
|
+
return VAGUE_WORDS.test(item) && !hasEvidence;
|
|
46
|
+
}
|
|
47
|
+
/** Scores a brief (the project's IDEA.md) and names what is missing. */
|
|
48
|
+
export function lintBrief(text) {
|
|
49
|
+
const sections = splitSections(text);
|
|
50
|
+
const gaps = [];
|
|
51
|
+
const miss = (gap) => gaps.push(gap);
|
|
52
|
+
const oneLine = findSection(sections, /^\W*(one[- ]line|summary|overview|goal|what (this|it) is|pitch)\b/i);
|
|
53
|
+
if (!oneLine || oneLine.body.trim().length < 20) {
|
|
54
|
+
miss({ id: 'one-line', cost: 15, message: 'No one-line description of what you are building.' });
|
|
55
|
+
}
|
|
56
|
+
const audience = findSection(sections, /^\W*(who\b|audience|users?\b|customers?\b|target)/i);
|
|
57
|
+
if (!audience || audience.body.trim().length < 20) {
|
|
58
|
+
miss({ id: 'audience', cost: 15, message: 'No audience: say who this is for.' });
|
|
59
|
+
}
|
|
60
|
+
const mustHeading = findSection(sections, /^\W*(must[- ]?haves?|requirements?|features?|deliverables?|scope\b)/i);
|
|
61
|
+
const items = mustHeading ? listItems(mustHeading.body) : [];
|
|
62
|
+
if (items.length < 3) {
|
|
63
|
+
miss({ id: 'must-haves', cost: 20, message: 'Fewer than three must-haves listed as bullets.' });
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const vague = items.filter(isVagueItem);
|
|
67
|
+
if (vague.length > items.length / 2) {
|
|
68
|
+
miss({
|
|
69
|
+
id: 'vague-must-haves',
|
|
70
|
+
cost: 10,
|
|
71
|
+
message: `Vague must-haves (${vague.length} of ${items.length}): add numbers, paths or commands that can be checked.`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const doneWhen = findSection(sections, /^\W*(done[- ]when|definition of done|acceptance|success criteria)/i);
|
|
76
|
+
const checkable = doneWhen ? /`[^`]+`|\b(npm|pnpm|yarn|pytest|make|cargo)\b|\btests?\s+(pass|green)/i.test(doneWhen.body) : false;
|
|
77
|
+
if (!checkable) {
|
|
78
|
+
miss({ id: 'done-when', cost: 25, message: 'No checkable done-when: list commands or tests that must pass.' });
|
|
79
|
+
}
|
|
80
|
+
const outOfScope = findSection(sections, /^\W*(out of scope|not in scope|non-goals?|not included|won'?t)/i);
|
|
81
|
+
if (!outOfScope || outOfScope.body.trim().length < 10) {
|
|
82
|
+
miss({ id: 'out-of-scope', cost: 15, message: 'No out-of-scope section: say what the loop must not build.' });
|
|
83
|
+
}
|
|
84
|
+
if (text.split(/\s+/).filter(Boolean).length < MIN_WORDS) {
|
|
85
|
+
miss({ id: 'length', cost: 10, message: `Under ${MIN_WORDS} words: too thin for an unattended build.` });
|
|
86
|
+
}
|
|
87
|
+
const score = Math.max(0, 100 - gaps.reduce((sum, gap) => sum + gap.cost, 0));
|
|
88
|
+
return { score, gaps };
|
|
89
|
+
}
|
|
90
|
+
export const briefPasses = (lint) => lint.score >= MIN_BRIEF_SCORE;
|
|
91
|
+
//# sourceMappingURL=brief.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brief.js","sourceRoot":"","sources":["../../src/core/brief.ts"],"names":[],"mappings":"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"}
|
package/dist/core/gh.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gh.js","sourceRoot":"","sources":["../../src/core/gh.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Planning for `loop init`: decides what to write, without touching the disk.
|
|
2
|
+
import { briefPasses, lintBrief } from './brief.js';
|
|
3
|
+
export const CARD_START = '<!-- compounding-loop:start -->';
|
|
4
|
+
export const CARD_END = '<!-- compounding-loop:end -->';
|
|
5
|
+
const AGENTS_POINTER = '# AGENTS.md\n\nThis repository follows the rules in [CLAUDE.md](CLAUDE.md). Read it first; it applies to any coding agent.\n';
|
|
6
|
+
/** Wraps the card in markers and merges it into an existing CLAUDE.md. Re-running is a no-op. */
|
|
7
|
+
export function mergeCard(existing, card) {
|
|
8
|
+
const block = `${CARD_START}\n${card.trim()}\n${CARD_END}\n`;
|
|
9
|
+
if (existing === undefined || existing.trim() === '')
|
|
10
|
+
return block;
|
|
11
|
+
const start = existing.indexOf(CARD_START);
|
|
12
|
+
const end = existing.indexOf(CARD_END);
|
|
13
|
+
if ((start === -1) !== (end === -1) || (start !== -1 && end < start)) {
|
|
14
|
+
throw new Error(`CLAUDE.md has a broken ${CARD_START} / ${CARD_END} pair: fix or remove the markers, then re-run.`);
|
|
15
|
+
}
|
|
16
|
+
if (start !== -1 && end > start) {
|
|
17
|
+
return existing.slice(0, start) + block + existing.slice(end + CARD_END.length).replace(/^\r?\n/, '');
|
|
18
|
+
}
|
|
19
|
+
return `${existing.replace(/\s*$/, '')}\n\n${block}`;
|
|
20
|
+
}
|
|
21
|
+
/** Returns why a brief blocks `init`, or undefined when it is good enough. */
|
|
22
|
+
export function briefBlocker(brief) {
|
|
23
|
+
if (brief === undefined)
|
|
24
|
+
return 'IDEA.md is missing: write the brief first (or pass --force).';
|
|
25
|
+
const lint = lintBrief(brief);
|
|
26
|
+
if (briefPasses(lint))
|
|
27
|
+
return undefined;
|
|
28
|
+
const gaps = lint.gaps.map((gap) => ` - ${gap.message}`).join('\n');
|
|
29
|
+
return `IDEA.md scores ${lint.score}/100 (needs 60):\n${gaps}`;
|
|
30
|
+
}
|
|
31
|
+
export function planInit({ kit, card, existing, force }) {
|
|
32
|
+
const actions = [];
|
|
33
|
+
for (const file of kit) {
|
|
34
|
+
const current = existing.get(file.dest);
|
|
35
|
+
if (current === undefined)
|
|
36
|
+
actions.push({ dest: file.dest, kind: 'create', content: file.content });
|
|
37
|
+
else if (current === file.content)
|
|
38
|
+
actions.push({ dest: file.dest, kind: 'skip', reason: 'already up to date' });
|
|
39
|
+
else if (force && !file.keep)
|
|
40
|
+
actions.push({ dest: file.dest, kind: 'overwrite', content: file.content });
|
|
41
|
+
else
|
|
42
|
+
actions.push({ dest: file.dest, kind: 'skip', reason: file.keep ? 'loop state, never overwritten' : 'exists (use --force to overwrite)' });
|
|
43
|
+
}
|
|
44
|
+
if (card !== undefined) {
|
|
45
|
+
const current = existing.get('CLAUDE.md');
|
|
46
|
+
const merged = mergeCard(current, card);
|
|
47
|
+
if (merged === current)
|
|
48
|
+
actions.push({ dest: 'CLAUDE.md', kind: 'skip', reason: 'Operating Card already installed' });
|
|
49
|
+
else
|
|
50
|
+
actions.push({ dest: 'CLAUDE.md', kind: current === undefined ? 'create' : 'update', content: merged });
|
|
51
|
+
}
|
|
52
|
+
const agents = existing.get('AGENTS.md');
|
|
53
|
+
if (agents === undefined)
|
|
54
|
+
actions.push({ dest: 'AGENTS.md', kind: 'create', content: AGENTS_POINTER });
|
|
55
|
+
else
|
|
56
|
+
actions.push({ dest: 'AGENTS.md', kind: 'skip', reason: 'exists' });
|
|
57
|
+
return actions;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/core/init.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. 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,24 @@
|
|
|
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
|
+
//# 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"}
|