@warren-dean/code-kit 0.6.0 → 0.7.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +37 -0
- package/README.md +19 -9
- package/bin/code-kit.mjs +353 -39
- package/hooks/guard-bash.mjs +30 -14
- package/hooks/lane-audit.mjs +44 -1
- package/hooks/lib/adapters/context-graph.mjs +75 -6
- package/hooks/lib/adapters/index.mjs +18 -0
- package/hooks/lib/config.mjs +43 -2
- package/hooks/lib/dependencies.mjs +26 -9
- package/hooks/lib/harness.mjs +2 -2
- package/hooks/lib/next.mjs +34 -3
- package/hooks/lib/plan.mjs +29 -0
- package/hooks/lib/queue.mjs +75 -0
- package/hooks/lib/reviews.mjs +18 -0
- package/hooks/lib/rules.mjs +8 -4
- package/hooks/lib/shell.mjs +1 -1
- package/hooks/lib/timeline.mjs +77 -0
- package/hooks/mod/art.mjs +200 -0
- package/hooks/mod/loop.mjs +68 -0
- package/hooks/mod/panes.mjs +943 -0
- package/hooks/mod/register.mjs +978 -15
- package/hooks/mod/register.test.ts +1449 -14
- package/hooks/mod/review.mjs +102 -0
- package/hooks/mod/view.mjs +430 -38
- package/package.json +1 -1
- package/skills/init/SKILL.md +5 -4
- package/skills/review/SKILL.md +8 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// When each story's work happened (docs/specs/11-panes.md, VIEW-2), from git and the project's own
|
|
2
|
+
// records: when its branch was made, its commits, its merge into the base, its send-backs and the
|
|
3
|
+
// approvals its commits logged. The mod draws these as a bar per lane on a shared time axis.
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { APPROVAL_LOG } from './rules.mjs';
|
|
6
|
+
import { sendBacks } from './reviews.mjs';
|
|
7
|
+
|
|
8
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* For each story with a branch, or done: { id, lane, branch, state, started, lastCommit, merged,
|
|
12
|
+
* sentBack: [ms], approvals: [{ at, names }] }, times in milliseconds or null.
|
|
13
|
+
*/
|
|
14
|
+
export function storyTimes(root, stories, base) {
|
|
15
|
+
const git = (...args) => {
|
|
16
|
+
try {
|
|
17
|
+
return execFileSync('git', ['-C', root, ...args], {
|
|
18
|
+
encoding: 'utf8',
|
|
19
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
20
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
21
|
+
}).trim();
|
|
22
|
+
} catch {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const lines = (text) => text.split('\n').filter(Boolean);
|
|
27
|
+
const merges = lines(git('log', '--merges', '--format=%ct%x09%s', base)).map((l) => {
|
|
28
|
+
const [t, ...subject] = l.split('\t');
|
|
29
|
+
return { at: Number(t) * 1000, subject: subject.join('\t') };
|
|
30
|
+
});
|
|
31
|
+
const backs = sendBacks(root);
|
|
32
|
+
const logged = (ref) => new Set(lines(git('show', `${ref}:${APPROVAL_LOG}`)));
|
|
33
|
+
const onBase = logged(base);
|
|
34
|
+
return stories
|
|
35
|
+
.filter((s) => s.branchExists || s.state === 'done')
|
|
36
|
+
.map((s) => {
|
|
37
|
+
const b = s.branch;
|
|
38
|
+
// The reflog's oldest entry is the branch's making, when it's still there.
|
|
39
|
+
const made = s.branchExists
|
|
40
|
+
? lines(git('reflog', 'show', '--format=%ct', `refs/heads/${b}`))
|
|
41
|
+
: [];
|
|
42
|
+
const commits = s.branchExists
|
|
43
|
+
? lines(git('log', '--format=%ct', `${base}..${b}`)).map((t) => Number(t) * 1000)
|
|
44
|
+
: [];
|
|
45
|
+
const named = new RegExp(`(^|[\\s'"])${escape(b)}($|[\\s'"])`);
|
|
46
|
+
const merged = merges.find((m) => named.test(m.subject))?.at ?? null;
|
|
47
|
+
const approvals = s.branchExists
|
|
48
|
+
? [...logged(b)]
|
|
49
|
+
.filter((l) => !onBase.has(l))
|
|
50
|
+
.flatMap((l) => {
|
|
51
|
+
try {
|
|
52
|
+
const e = JSON.parse(l);
|
|
53
|
+
return e.grantedAt || e.at
|
|
54
|
+
? [{ at: Date.parse(e.grantedAt ?? e.at), names: [e.approval].filter(Boolean) }]
|
|
55
|
+
: [];
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
: [];
|
|
61
|
+
return {
|
|
62
|
+
id: s.id,
|
|
63
|
+
lane: s.lane,
|
|
64
|
+
branch: b,
|
|
65
|
+
state: s.state,
|
|
66
|
+
started: made.length
|
|
67
|
+
? Number(made.at(-1)) * 1000
|
|
68
|
+
: commits.length
|
|
69
|
+
? Math.min(...commits)
|
|
70
|
+
: null,
|
|
71
|
+
lastCommit: commits.length ? Math.max(...commits) : null,
|
|
72
|
+
merged,
|
|
73
|
+
sentBack: backs.filter((x) => x.branch === b).map((x) => Date.parse(x.at)),
|
|
74
|
+
approvals,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Panes v2's pictures (docs/specs/11-panes.md, VIEW-2 and VIEW-3): a timeline bar per lane on a
|
|
2
|
+
// shared time axis, and a small pixel character per lane agent whose pose shows its state. The
|
|
3
|
+
// terminal draws characters as half-block cells (a Raster, repainted in place with $.ui.blit while
|
|
4
|
+
// the agent works); Desktop draws them, and the timelines, as SVG. Every state has a glyph too.
|
|
5
|
+
|
|
6
|
+
// --- the characters (VIEW-3) ---------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/** Each pose, 6 by 6 pixels: H the lane's tint, E eyes, X stalled eyes, L closed eyes, C a crown. */
|
|
9
|
+
const POSES = {
|
|
10
|
+
working: [
|
|
11
|
+
['..HH..', '.HEEH.', '..HH..', '.HHHH.', 'H.HH.H', '.H..H.'],
|
|
12
|
+
['..HH..', '.HEEH.', '..HH..', 'HHHHHH', '..HH..', '..HH..'],
|
|
13
|
+
],
|
|
14
|
+
waiting: [['..HH.H', '.HEEHH', '..HH..', '.HHH..', '..HH..', '.H..H.']],
|
|
15
|
+
quiet: [['......', '..HH..', '.HLLH.', '..HH..', '.HHHH.', '.H..H.']],
|
|
16
|
+
stalled: [['..HH..', '.HXXH.', '..HH..', '.HHHH.', '..HH..', '.H..H.']],
|
|
17
|
+
done: [['H.HH.H', 'HHEEHH', '..HH..', '..HH..', '..HH..', '.H..H.']],
|
|
18
|
+
idle: [['......', '..HH..', '.HLLH.', '..HH..', '.HHHH.', '.H..H.']],
|
|
19
|
+
};
|
|
20
|
+
/** What a pose reads as without its picture, and in words. */
|
|
21
|
+
export const POSE_GLYPH = {
|
|
22
|
+
working: '⚙',
|
|
23
|
+
waiting: '?',
|
|
24
|
+
quiet: '…',
|
|
25
|
+
stalled: '✗',
|
|
26
|
+
done: '✓',
|
|
27
|
+
idle: '·',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Each lane's tint, fixed by its place in the config so the person learns them; the lead's is gold. */
|
|
31
|
+
const TINTS = [0x4aa3ff, 0x5cc85c, 0xd070e0, 0x40c8c8, 0xff9a40, 0xff6070, 0xa0a0ff];
|
|
32
|
+
export function tintOf(lane, lanes) {
|
|
33
|
+
if (lane === 'lead') return 0xd8a840;
|
|
34
|
+
const i = lanes.filter((l) => l !== 'lead').indexOf(lane);
|
|
35
|
+
return TINTS[(i < 0 ? 0 : i) % TINTS.length];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const dim = (rgb) =>
|
|
39
|
+
(((rgb >> 16) & 0xff) >> 1) * 0x10000 + (((rgb >> 8) & 0xff) >> 1) * 0x100 + ((rgb & 0xff) >> 1);
|
|
40
|
+
|
|
41
|
+
/** The pose's frame as a grid of colours (null for none), the lead crowned. */
|
|
42
|
+
export function sprite(pose, frame, tint, { lead = false } = {}) {
|
|
43
|
+
const frames = POSES[pose] ?? POSES.idle;
|
|
44
|
+
const rows = frames[frame % frames.length].map((r) => [...r]);
|
|
45
|
+
if (lead) rows[0] = [...'C.CC.C'];
|
|
46
|
+
const body = pose === 'quiet' || pose === 'idle' ? dim(tint) : tint;
|
|
47
|
+
const colour = { H: body, E: 0x202020, X: 0xff3030, L: 0x707070, C: 0xffcc00 };
|
|
48
|
+
return rows.map((r) => r.map((c) => colour[c] ?? null));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const DEFAULT = 0x01000000;
|
|
52
|
+
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
53
|
+
function base64(bytes) {
|
|
54
|
+
let out = '';
|
|
55
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
56
|
+
const [a, b = 0, c = 0] = [bytes[i], bytes[i + 1], bytes[i + 2]];
|
|
57
|
+
const n = (a << 16) | (b << 8) | c;
|
|
58
|
+
out += B64[(n >> 18) & 63] + B64[(n >> 12) & 63];
|
|
59
|
+
out += i + 1 < bytes.length ? B64[(n >> 6) & 63] : '=';
|
|
60
|
+
out += i + 2 < bytes.length ? B64[n & 63] : '=';
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A sprite as a Raster's cells (VIEW-3): two pixels a cell, the upper as the half block's colour and
|
|
67
|
+
* the lower as its background. 6 columns by 3 rows.
|
|
68
|
+
*/
|
|
69
|
+
export function rasterCells(grid) {
|
|
70
|
+
const words = [];
|
|
71
|
+
for (let y = 0; y < grid.length; y += 2)
|
|
72
|
+
for (let x = 0; x < grid[y].length; x++) {
|
|
73
|
+
const top = grid[y][x];
|
|
74
|
+
const bottom = grid[y + 1]?.[x] ?? null;
|
|
75
|
+
if (top === null && bottom === null) words.push(0x20, DEFAULT, DEFAULT);
|
|
76
|
+
else if (top === null) words.push(0x2584, bottom, DEFAULT);
|
|
77
|
+
else words.push(0x2580, top, bottom ?? DEFAULT);
|
|
78
|
+
}
|
|
79
|
+
const bytes = [];
|
|
80
|
+
for (const w of words)
|
|
81
|
+
bytes.push(w & 0xff, (w >>> 8) & 0xff, (w >>> 16) & 0xff, (w >>> 24) & 0xff);
|
|
82
|
+
return base64(bytes);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const hex = (rgb) => `#${rgb.toString(16).padStart(6, '0')}`;
|
|
86
|
+
|
|
87
|
+
/** A sprite as SVG for Desktop, the working pose's frames alternating (SMIL), 4 px a pixel. */
|
|
88
|
+
export function spriteSvg(pose, tint, { lead = false } = {}) {
|
|
89
|
+
const frames = (POSES[pose] ?? POSES.idle).map((_, f) => sprite(pose, f, tint, { lead }));
|
|
90
|
+
const rects = (grid) =>
|
|
91
|
+
grid
|
|
92
|
+
.flatMap((row, y) =>
|
|
93
|
+
row.map((c, x) =>
|
|
94
|
+
c === null
|
|
95
|
+
? ''
|
|
96
|
+
: `<rect x="${x * 4}" y="${y * 4}" width="4" height="4" fill="${hex(c)}"/>`,
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
.join('');
|
|
100
|
+
const groups = frames.map((g, f) =>
|
|
101
|
+
frames.length > 1
|
|
102
|
+
? `<g visibility="${f ? 'hidden' : 'visible'}"><animate attributeName="visibility" values="${f ? 'hidden;visible' : 'visible;hidden'}" dur="0.8s" repeatCount="indefinite" calcMode="discrete"/>${rects(g)}</g>`
|
|
103
|
+
: `<g>${rects(g)}</g>`,
|
|
104
|
+
);
|
|
105
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" shape-rendering="crispEdges">${groups.join('')}</svg>`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A lane's pose (VIEW-3): stalled when flagged or quiet past the restart time, waiting while a call
|
|
110
|
+
* of its is held or its request is open, working while its agent is at work, quiet when it has
|
|
111
|
+
* gone quiet, done once its branch is finished, idle otherwise.
|
|
112
|
+
*/
|
|
113
|
+
export function poseOf(
|
|
114
|
+
lane,
|
|
115
|
+
{ live = null, waiting = false, flagged = false, shown = lane.state } = {},
|
|
116
|
+
) {
|
|
117
|
+
if (flagged || live?.level === 'stalled') return 'stalled';
|
|
118
|
+
if (waiting) return 'waiting';
|
|
119
|
+
if (live?.level === 'quiet') return 'quiet';
|
|
120
|
+
if (lane.active) return 'working';
|
|
121
|
+
if (['in review', 'queued'].includes(shown)) return 'done';
|
|
122
|
+
return 'idle';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- the timelines (VIEW-2) ----------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/** Each cell kind's glyph and colour: the glyph alone reads in a terminal without colour. */
|
|
128
|
+
export const BAR = {
|
|
129
|
+
building: { glyph: '█', color: 'blue' },
|
|
130
|
+
review: { glyph: '▒', color: 'yellow' },
|
|
131
|
+
sentBack: { glyph: '↩', color: 'magenta' },
|
|
132
|
+
approval: { glyph: '◆', color: 'cyan' },
|
|
133
|
+
merged: { glyph: '✓', color: 'green' },
|
|
134
|
+
none: { glyph: ' ', color: undefined },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** Where the shared axis starts: the earliest start among the lanes' stories, at most a day back. */
|
|
138
|
+
export function axisStart(times, now) {
|
|
139
|
+
const starts = times.map((t) => t.started).filter((t) => t !== null && t !== undefined);
|
|
140
|
+
return starts.length ? Math.max(Math.min(...starts), now - 24 * 3600000) : now - 3600000;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A story's bar as `width` cells from `start` to `now` (VIEW-2): building from its start, in review
|
|
145
|
+
* from its last commit once finished, ending at its merge or now; its send-backs, approvals and
|
|
146
|
+
* merge marked where they fell. [kind], a kind of BAR's.
|
|
147
|
+
*/
|
|
148
|
+
export function barCells(t, { start, now, width }) {
|
|
149
|
+
const span = Math.max(1, now - start) / width;
|
|
150
|
+
const finished =
|
|
151
|
+
['review', 'in review', 'queued', 'done'].includes(t.state) && t.lastCommit !== null;
|
|
152
|
+
const end = t.merged ?? now;
|
|
153
|
+
const cells = [];
|
|
154
|
+
for (let i = 0; i < width; i++) {
|
|
155
|
+
const mid = start + (i + 0.5) * span;
|
|
156
|
+
if (t.started === null || mid < t.started || mid > end + span / 2) cells.push('none');
|
|
157
|
+
else if (finished && mid >= t.lastCommit) cells.push('review');
|
|
158
|
+
else cells.push('building');
|
|
159
|
+
}
|
|
160
|
+
const mark = (at, kind) => {
|
|
161
|
+
if (at === null || at === undefined || at < start || at > now) return;
|
|
162
|
+
cells[Math.min(width - 1, Math.floor((at - start) / span))] = kind;
|
|
163
|
+
};
|
|
164
|
+
for (const at of t.sentBack ?? []) mark(at, 'sentBack');
|
|
165
|
+
for (const a of t.approvals ?? []) mark(a.at, 'approval');
|
|
166
|
+
mark(t.merged, 'merged');
|
|
167
|
+
return cells;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Runs of the same kind, to draw as one Text each: [{ kind, n }]. */
|
|
171
|
+
export function runs(cells) {
|
|
172
|
+
const out = [];
|
|
173
|
+
for (const kind of cells) {
|
|
174
|
+
if (out.at(-1)?.kind === kind) out.at(-1).n += 1;
|
|
175
|
+
else out.push({ kind, n: 1 });
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The bars as SVG for Desktop: a row per lane, the same kinds and colours. */
|
|
181
|
+
export function timelineSvg(rows, { width = 480, rowHeight = 10 } = {}) {
|
|
182
|
+
const FILL = {
|
|
183
|
+
building: '#4aa3ff',
|
|
184
|
+
review: '#e0c040',
|
|
185
|
+
sentBack: '#d070e0',
|
|
186
|
+
approval: '#40c8c8',
|
|
187
|
+
merged: '#5cc85c',
|
|
188
|
+
};
|
|
189
|
+
const cell = width / Math.max(1, rows[0]?.cells.length ?? 1);
|
|
190
|
+
const rects = rows
|
|
191
|
+
.flatMap((r, y) =>
|
|
192
|
+
r.cells.map((k, x) =>
|
|
193
|
+
k === 'none'
|
|
194
|
+
? ''
|
|
195
|
+
: `<rect x="${(x * cell).toFixed(1)}" y="${y * (rowHeight + 4)}" width="${cell.toFixed(1)}" height="${rowHeight}" fill="${FILL[k]}"/>`,
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
.join('');
|
|
199
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${rows.length * (rowHeight + 4)}" shape-rendering="crispEdges">${rects}</svg>`;
|
|
200
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The lead loop (spec 05), the pure parts: what to prompt the lead with for a step `code-kit next`
|
|
2
|
+
// reports, and what a quiet lane agent is due. The mod watches and prompts; the lead acts, with the
|
|
3
|
+
// existing skills, and the hooks judge every call as before.
|
|
4
|
+
|
|
5
|
+
/** The steps the loop hands the lead; the rest (spec-design, init, check) wait for the person. */
|
|
6
|
+
const LEAD_STEPS = new Set(['dispatch', 'review', 'lead', 'merge']);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The prompt for a step from `code-kit next --json` (LOOP-1), or null when it isn't the lead's to
|
|
10
|
+
* take. Every ready story is named: the dispatch skill decides how many lanes go (LOOP-2). A prompt
|
|
11
|
+
* names the skill rather than starting with it: Claude Code refuses a mod's prompt that begins with
|
|
12
|
+
* `/`, which would run a command as the person.
|
|
13
|
+
*/
|
|
14
|
+
export function leadPrompt(step, { cli = 'code-kit' } = {}) {
|
|
15
|
+
if (!step || !LEAD_STEPS.has(step.step)) return null;
|
|
16
|
+
const ids = (step.args ?? '').split(/\s+/).filter(Boolean);
|
|
17
|
+
// MQ-2: the head of the merge queue, where the lead merges within the delegated rules.
|
|
18
|
+
if (step.step === 'merge')
|
|
19
|
+
return ids.length
|
|
20
|
+
? `Merge ${ids[0]}, next in the merge queue: run node "${cli}" queue merge --delegated. It verifies the branch against the base as it is now, and sends it back to its lane if it conflicts or fails.`
|
|
21
|
+
: null;
|
|
22
|
+
if (step.step === 'dispatch')
|
|
23
|
+
return ids.length
|
|
24
|
+
? `Dispatch ${ids.join(', ')}: run /code-kit:dispatch ${ids.join(' ')}.`
|
|
25
|
+
: null;
|
|
26
|
+
// The review skill takes one story, or none for every story in review.
|
|
27
|
+
if (step.step === 'review')
|
|
28
|
+
return ids.length === 1
|
|
29
|
+
? `Review ${ids[0]}: run /code-kit:review ${ids[0]}.`
|
|
30
|
+
: `Review ${ids.join(', ')}: run /code-kit:review.`;
|
|
31
|
+
if (!ids.length) return null;
|
|
32
|
+
const branches = ids.map((id) => `lead/${id.toLowerCase()}`).join(', ');
|
|
33
|
+
return `Build ${ids.join(', ')} yourself: ${ids.length === 1 ? "it is the lead's own story" : "they are the lead's own stories"}. Work on ${branches}, then review ${ids.length === 1 ? 'it' : 'them'} like any branch.`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A step's identity: the loop never submits the same step twice while the project stands still. */
|
|
37
|
+
export const stepKey = (step) => `${step.step}:${step.args ?? ''}`;
|
|
38
|
+
|
|
39
|
+
/** The band's words for a step (LOOP-5): "Dispatch ST-7, ST-9". */
|
|
40
|
+
export function stepLabel(step) {
|
|
41
|
+
const ids = (step.args ?? '').split(/\s+/).filter(Boolean).join(', ');
|
|
42
|
+
const verb =
|
|
43
|
+
{ dispatch: 'Dispatch', review: 'Review', lead: 'Build', merge: 'Merge' }[step.step] ??
|
|
44
|
+
step.step;
|
|
45
|
+
return ids ? `${verb} ${ids}` : verb;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What a lane agent with no tool call for `quietMs` is due (LOOP-3): 'nudge' at stall.nudgeMinutes,
|
|
50
|
+
* 'restart' at stall.restartMinutes, 'flag' once its story has been restarted stall.maxRestarts
|
|
51
|
+
* times, or null. `agent` holds what was done already: { nudged, stopped }.
|
|
52
|
+
*/
|
|
53
|
+
export function stallDue(quietMs, agent, restarts, stall) {
|
|
54
|
+
const minutes = quietMs / 60000;
|
|
55
|
+
if (minutes >= stall.restartMinutes && !agent.stopped)
|
|
56
|
+
return restarts >= stall.maxRestarts ? 'flag' : 'restart';
|
|
57
|
+
if (minutes >= stall.nudgeMinutes && !agent.nudged) return 'nudge';
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The lead's prompt to stop a stalled agent and dispatch its story again (LOOP-3). */
|
|
62
|
+
export function restartPrompt({ type, id, story, minutes }) {
|
|
63
|
+
return `${type} (agent ${id}) on ${story} has made no tool call for ${minutes} minutes. Stop it with TaskStop, then dispatch ${story} again with the same brief on the same branch: /code-kit:dispatch ${story}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The message a quiet agent gets (LOOP-3). */
|
|
67
|
+
export const nudgeText = (minutes) =>
|
|
68
|
+
`No tool call for ${minutes} minutes: report where you are, or carry on.`;
|