@warren-dean/code-kit 0.20.0 → 0.22.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 +24 -0
- package/README.md +75 -50
- package/bin/code-kit.mjs +364 -25
- package/hooks/guard-agent.mjs +36 -0
- package/hooks/guard-bash.mjs +67 -13
- package/hooks/guard-paths.mjs +14 -2
- package/hooks/hooks.json +20 -0
- package/hooks/lane-audit.mjs +38 -1
- package/hooks/lib/adapters/hub/client.mjs +15 -0
- package/hooks/lib/checks.mjs +5 -1
- package/hooks/lib/config.mjs +149 -6
- package/hooks/lib/decisionstore.mjs +89 -0
- package/hooks/lib/harness.mjs +7 -0
- package/hooks/lib/hook.mjs +7 -0
- package/hooks/lib/instructions.mjs +46 -13
- package/hooks/lib/laneclaims.mjs +89 -0
- package/hooks/lib/layers.mjs +11 -0
- package/hooks/lib/loop-state.mjs +20 -2
- package/hooks/lib/next.mjs +34 -1
- package/hooks/lib/plan.mjs +3 -1
- package/hooks/lib/questions.mjs +23 -5
- package/hooks/lib/regression.mjs +2 -1
- package/hooks/lib/rules.mjs +20 -13
- package/hooks/lib/schedule.mjs +86 -0
- package/hooks/lib/sessions.mjs +223 -0
- package/hooks/lib/snapshot.mjs +80 -2
- package/hooks/lib/standards.mjs +450 -0
- package/hooks/lib/team.mjs +9 -2
- package/hooks/lib/verify.mjs +23 -1
- package/hooks/mod/decisions.mjs +1 -28
- package/hooks/mod/loop.mjs +8 -0
- package/hooks/mod/mission.mjs +21 -19
- package/hooks/mod/register.mjs +117 -40
- package/hooks/mod/register.test.ts +248 -73
- package/hooks/mod/review.mjs +1 -0
- package/hooks/mod/view.mjs +98 -47
- package/hooks/post-edit-check.mjs +8 -1
- package/hooks/session.mjs +24 -0
- package/hooks/stop-check.mjs +23 -3
- package/package.json +1 -1
- package/skills/decide/SKILL.md +1 -1
- package/skills/dispatch/SKILL.md +2 -2
- package/skills/init/SKILL.md +17 -3
- package/skills/lead/SKILL.md +28 -0
- package/skills/next/SKILL.md +2 -2
- package/skills/review/SKILL.md +2 -1
- package/skills/spec-design/SKILL.md +10 -7
- package/templates/agent.md +1 -0
- package/templates/claude-md.md +5 -1
- package/templates/plan.md +2 -1
package/bin/code-kit.mjs
CHANGED
|
@@ -29,9 +29,18 @@
|
|
|
29
29
|
// report [--json] | report send|discard <id> the person sends a draft to code-kit's GitHub, or not
|
|
30
30
|
// stops [--session id] [--json] finish checks that refused an agent's last stop, still failing
|
|
31
31
|
// sent-back <branch> [--reason …] records that a review sent the branch back at its current commit
|
|
32
|
+
// standards [--json] [--for <path>...]
|
|
33
|
+
// the project's standards, with an organisation's, and their problems
|
|
32
34
|
// settings [--json] | settings set <key> <value> --reason … [--via pane]
|
|
33
35
|
// the harness settings; set is the person's own change
|
|
34
36
|
// record approvals a person gave in chat (with approvals.lead on, the lead runs it)
|
|
37
|
+
// decisions [--json] [--all] what waits on the person in this session, and a count of each other's
|
|
38
|
+
// who which session this is, whether it leads, and its task (spec 23)
|
|
39
|
+
// sessions [--json] | sessions lead the sessions working the project; make this one the lead
|
|
40
|
+
// task "<what>" --touches globs | task done | task take <session>
|
|
41
|
+
// a working session's ad hoc task and the paths it leases
|
|
42
|
+
// lanes [--json] | lanes release <lane>
|
|
43
|
+
// the lanes running a story now, one each across sessions (spec 23)
|
|
35
44
|
// --config <file> reads a draft (.claude/code-kit.draft.json) instead of .claude/code-kit.json.
|
|
36
45
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
37
46
|
import {
|
|
@@ -50,10 +59,12 @@ import { BASELINE_FILE, loadBaseline } from '../hooks/lib/baseline.mjs';
|
|
|
50
59
|
import { matchesAny } from '../hooks/lib/glob.mjs';
|
|
51
60
|
import {
|
|
52
61
|
CONFIG_FILE,
|
|
62
|
+
approvalSettings,
|
|
53
63
|
committedConfig,
|
|
54
64
|
effectiveConfig,
|
|
55
65
|
mergeDelegation,
|
|
56
66
|
validate,
|
|
67
|
+
withApprovalSetting,
|
|
57
68
|
} from '../hooks/lib/config.mjs';
|
|
58
69
|
import { diffConfigs, ownershipMoves } from '../hooks/lib/diff.mjs';
|
|
59
70
|
import { CODE, importsOf, layerOf, layerProblems, targetOf } from '../hooks/lib/layers.mjs';
|
|
@@ -131,6 +142,27 @@ import { dependencyRuleProblems, npmFacts, packageOf } from '../hooks/lib/regist
|
|
|
131
142
|
import { heldTo, laneOfBranch, verifyBranch } from '../hooks/lib/verify.mjs';
|
|
132
143
|
import { skippedLine } from '../hooks/lib/checks.mjs';
|
|
133
144
|
import { TESTS_APPROVAL } from '../hooks/lib/regression.mjs';
|
|
145
|
+
import { laneClaim, laneClaims, releaseLane, shortSession } from '../hooks/lib/laneclaims.mjs';
|
|
146
|
+
import { slicesOf } from '../hooks/lib/schedule.mjs';
|
|
147
|
+
import {
|
|
148
|
+
leadSession,
|
|
149
|
+
makeLead,
|
|
150
|
+
memberOf,
|
|
151
|
+
readSession,
|
|
152
|
+
seeSession,
|
|
153
|
+
sessions,
|
|
154
|
+
setTask,
|
|
155
|
+
takeTask,
|
|
156
|
+
} from '../hooks/lib/sessions.mjs';
|
|
157
|
+
import { globsOverlap } from '../hooks/lib/teamfacts.mjs';
|
|
158
|
+
import { decisionsFor, openDecisions } from '../hooks/lib/decisionstore.mjs';
|
|
159
|
+
import {
|
|
160
|
+
appliesTo,
|
|
161
|
+
projectStandards,
|
|
162
|
+
readInherited,
|
|
163
|
+
standardLine,
|
|
164
|
+
writeInherited,
|
|
165
|
+
} from '../hooks/lib/standards.mjs';
|
|
134
166
|
|
|
135
167
|
const out = (s) => process.stdout.write(`${s}\n`);
|
|
136
168
|
// Piped into `head` and the like: stop quietly when the reader closes.
|
|
@@ -157,6 +189,8 @@ const laneName = option('--lane');
|
|
|
157
189
|
const intoName = option('--into');
|
|
158
190
|
const via = option('--via');
|
|
159
191
|
const sessionId = option('--session');
|
|
192
|
+
// SESS-1: the session calling, as Claude Code tells every command an agent runs, unless --session says.
|
|
193
|
+
const callerSession = sessionId ?? process.env.CLAUDE_CODE_SESSION_ID ?? null;
|
|
160
194
|
const minutesGiven = option('--minutes');
|
|
161
195
|
const answerGiven = option('--answer');
|
|
162
196
|
const leaseGiven = option('--lease');
|
|
@@ -187,6 +221,7 @@ const FLAGS = [
|
|
|
187
221
|
'--blocks',
|
|
188
222
|
'--take',
|
|
189
223
|
'--dry-run',
|
|
224
|
+
'--for',
|
|
190
225
|
];
|
|
191
226
|
const flag = (name) => args.includes(name);
|
|
192
227
|
const writeBaseline = flag('--write');
|
|
@@ -269,6 +304,23 @@ function check() {
|
|
|
269
304
|
}
|
|
270
305
|
}
|
|
271
306
|
|
|
307
|
+
/** SESS-1: which session the caller is, whether it leads the project, and its task. */
|
|
308
|
+
function whoAmI() {
|
|
309
|
+
load();
|
|
310
|
+
const root = resolve('.');
|
|
311
|
+
const me = callerSession ? readSession(root, callerSession) : null;
|
|
312
|
+
if (!me)
|
|
313
|
+
return out(
|
|
314
|
+
callerSession
|
|
315
|
+
? `Session ${shortSession(callerSession)}, which code-kit hasn't recorded yet.`
|
|
316
|
+
: 'Not run from a Claude Code session, so no member of the team.',
|
|
317
|
+
);
|
|
318
|
+
const lead = leadSession(root);
|
|
319
|
+
out(
|
|
320
|
+
`${memberOf(me)}: ${me.role === 'lead' ? 'the lead session' : `a working session${lead ? ` (${memberOf(lead)} leads)` : ' (no session leads now)'}`}${me.task ? `, working on "${me.task.title}"` : ''}.`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
272
324
|
function who(paths) {
|
|
273
325
|
const { config } = load();
|
|
274
326
|
for (const p of paths) {
|
|
@@ -427,6 +479,7 @@ const VERIFY_GROUPS = {
|
|
|
427
479
|
design: 'Designs before screens',
|
|
428
480
|
tests: 'Weakened tests',
|
|
429
481
|
regression: 'Fixes and their tests',
|
|
482
|
+
standards: 'Standards',
|
|
430
483
|
checks: 'Checks',
|
|
431
484
|
};
|
|
432
485
|
|
|
@@ -1010,7 +1063,7 @@ function requests() {
|
|
|
1010
1063
|
const open = openRequests(resolve('.')).map((r, i) => ({
|
|
1011
1064
|
...r,
|
|
1012
1065
|
n: i + 1,
|
|
1013
|
-
by: decidedBy(r.names, config),
|
|
1066
|
+
by: decidedBy(r.names, config, r.lane ? { kind: 'lane', lane: r.lane } : { kind: 'lead' }),
|
|
1014
1067
|
}));
|
|
1015
1068
|
const granted = approvalsInForce(resolve('.'));
|
|
1016
1069
|
// CARD-6: every approval given, newest first.
|
|
@@ -1034,6 +1087,159 @@ function requests() {
|
|
|
1034
1087
|
);
|
|
1035
1088
|
}
|
|
1036
1089
|
|
|
1090
|
+
/**
|
|
1091
|
+
* OWN-1: the decisions waiting on the person in this session (the lead's questions, the approvals its
|
|
1092
|
+
* refusals asked for), and a count of those in each other session; `--all` lists every session's.
|
|
1093
|
+
*/
|
|
1094
|
+
function decisionsCommand() {
|
|
1095
|
+
const { config } = load();
|
|
1096
|
+
const root = resolve('.');
|
|
1097
|
+
if (rest.length) die('code-kit decisions [--json] [--all]');
|
|
1098
|
+
const stories =
|
|
1099
|
+
config.docs?.specs && config.docs?.plan
|
|
1100
|
+
? buildStatus('.', config, base(config, false), tracked()).stories
|
|
1101
|
+
: [];
|
|
1102
|
+
if (flag('--all')) {
|
|
1103
|
+
const all = openDecisions(root, { stories });
|
|
1104
|
+
if (flag('--json')) return out(JSON.stringify(all, null, 2));
|
|
1105
|
+
if (!all.length) return out('Nothing waits on the person.');
|
|
1106
|
+
for (const d of all)
|
|
1107
|
+
out(
|
|
1108
|
+
`${pad(d.member ?? 'any session', 30)} ${pad(d.kind, 8)} ${d.what}${d.count > 1 ? ` (asked ${d.count} times)` : ''}`,
|
|
1109
|
+
);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const r = decisionsFor(root, callerSession, { stories });
|
|
1113
|
+
if (flag('--json')) return out(JSON.stringify(r, null, 2));
|
|
1114
|
+
if (!r.mine.length) out('Nothing waits on the person in this session.');
|
|
1115
|
+
for (const d of r.mine)
|
|
1116
|
+
out(`${pad(d.kind, 8)} ${d.what}${d.count > 1 ? ` (asked ${d.count} times)` : ''}`);
|
|
1117
|
+
for (const e of r.elsewhere)
|
|
1118
|
+
out(`${e.count} decision${e.count === 1 ? '' : 's'} in session ${e.member}, answered there.`);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* SESS-1, SESS-2: the sessions working the project, which one leads, and each one's task; `beat` (the mod,
|
|
1123
|
+
* while a session is open) says the session is still there; `lead` makes the caller's session the lead.
|
|
1124
|
+
*/
|
|
1125
|
+
function sessionsCommand() {
|
|
1126
|
+
load();
|
|
1127
|
+
const root = resolve('.');
|
|
1128
|
+
const [sub] = rest;
|
|
1129
|
+
if (sub === 'beat' || sub === 'lead') {
|
|
1130
|
+
if (!callerSession) die(`code-kit sessions ${sub} needs the session: --session <id>.`);
|
|
1131
|
+
if (sub === 'beat') return seeSession(root, callerSession) && out('Seen.');
|
|
1132
|
+
const was = leadSession(root);
|
|
1133
|
+
makeLead(root, callerSession);
|
|
1134
|
+
return out(
|
|
1135
|
+
`This session (${shortSession(callerSession)}) leads the project now${was && was.id !== callerSession ? `; ${shortSession(was.id)} is a working session` : ''}.`,
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
if (sub !== undefined) die('code-kit sessions [--json] | sessions lead | sessions beat');
|
|
1139
|
+
const all = sessions(root).filter((s) => s.live || s.task);
|
|
1140
|
+
if (flag('--json'))
|
|
1141
|
+
return out(
|
|
1142
|
+
JSON.stringify(
|
|
1143
|
+
all.map((s) => ({ ...s, member: memberOf(s) })),
|
|
1144
|
+
null,
|
|
1145
|
+
2,
|
|
1146
|
+
),
|
|
1147
|
+
);
|
|
1148
|
+
if (!all.length) return out('No session is working this project.');
|
|
1149
|
+
for (const s of all)
|
|
1150
|
+
out(
|
|
1151
|
+
`${pad(memberOf(s), 30)} ${pad(s.live ? s.role : 'ended', 8)} ${s.task ? `${s.task.title}${s.task.paths?.length ? ` (${s.task.paths.join(', ')})` : ''}${s.live ? '' : ', stale'}` : '-'}${s.id === callerSession ? ' ← this session' : ''}`,
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* SESS-4: an ad hoc task, for a session working without a story: recorded with the paths it leases, so
|
|
1157
|
+
* the other sessions' agents leave them alone; `done` ends it; `take <session>` moves a stale one here.
|
|
1158
|
+
*/
|
|
1159
|
+
function taskCommand() {
|
|
1160
|
+
load();
|
|
1161
|
+
const root = resolve('.');
|
|
1162
|
+
const [sub] = rest;
|
|
1163
|
+
if (!callerSession)
|
|
1164
|
+
die(
|
|
1165
|
+
'code-kit task needs the session: run it from Claude Code (CLAUDE_CODE_SESSION_ID), or pass --session <id>.',
|
|
1166
|
+
);
|
|
1167
|
+
if (sub === 'done') {
|
|
1168
|
+
const had = readSession(root, callerSession)?.task;
|
|
1169
|
+
if (!had) return out('This session has no task.');
|
|
1170
|
+
setTask(root, callerSession, null);
|
|
1171
|
+
return out(`Done: "${had.title}". Its paths are free for other sessions.`);
|
|
1172
|
+
}
|
|
1173
|
+
if (sub === 'take') {
|
|
1174
|
+
if (rest.length !== 2) die('code-kit task take <session>');
|
|
1175
|
+
const r = takeTask(root, rest[1], callerSession);
|
|
1176
|
+
if (!r.ok) die(r.why);
|
|
1177
|
+
return out(
|
|
1178
|
+
`This session took over "${r.task.title}" from ${memberOf(r.from)}${r.task.paths?.length ? `, leasing ${r.task.paths.join(', ')}` : ''}.`,
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
if (sub === undefined) {
|
|
1182
|
+
const s = readSession(root, callerSession);
|
|
1183
|
+
if (flag('--json')) return out(JSON.stringify(s?.task ?? null));
|
|
1184
|
+
return out(
|
|
1185
|
+
s?.task
|
|
1186
|
+
? `"${s.task.title}"${s.task.paths?.length ? `, leasing ${s.task.paths.join(', ')}` : ''}.`
|
|
1187
|
+
: 'This session has no task.',
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
if (rest.length !== 1)
|
|
1191
|
+
die(
|
|
1192
|
+
'code-kit task "<what you\'re doing>" --touches "<globs>" | task done | task take <session>',
|
|
1193
|
+
);
|
|
1194
|
+
const paths = (touchesGiven ?? '')
|
|
1195
|
+
.split(',')
|
|
1196
|
+
.map((g) => g.trim())
|
|
1197
|
+
.filter(Boolean);
|
|
1198
|
+
const clash = sessions(root).find(
|
|
1199
|
+
(s) =>
|
|
1200
|
+
s.live &&
|
|
1201
|
+
s.id !== callerSession &&
|
|
1202
|
+
s.task?.paths?.some((p) => paths.some((q) => globsOverlap(p, q))),
|
|
1203
|
+
);
|
|
1204
|
+
if (clash)
|
|
1205
|
+
die(
|
|
1206
|
+
`Another session (${memberOf(clash)}) is working on "${clash.task.title}", leasing ${clash.task.paths.join(', ')}. Pick paths it doesn't lease, or wait until it's done.`,
|
|
1207
|
+
);
|
|
1208
|
+
const branch = currentBranch(root) ?? null;
|
|
1209
|
+
setTask(root, callerSession, { title: sub.trim(), paths, branch });
|
|
1210
|
+
out(
|
|
1211
|
+
`This session is working on "${sub.trim()}"${paths.length ? `, leasing ${paths.join(', ')}` : ''}. \`code-kit task done\` when it's finished.`,
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* SCHED-1: the lanes claimed now, one story each across every session, or one lane released: for an
|
|
1217
|
+
* agent the lead saw stop without finishing, so its lane can take the next story.
|
|
1218
|
+
*/
|
|
1219
|
+
function lanesCommand() {
|
|
1220
|
+
const { config } = load();
|
|
1221
|
+
const root = resolve('.');
|
|
1222
|
+
if (rest[0] === 'release') {
|
|
1223
|
+
const lane = rest[1];
|
|
1224
|
+
if (!lane || rest.length !== 2) die('code-kit lanes release <lane>');
|
|
1225
|
+
if (!config.lanes[lane])
|
|
1226
|
+
die(`There is no lane "${lane}". Lanes: ${Object.keys(config.lanes).join(', ')}.`);
|
|
1227
|
+
const held = laneClaim(root, lane);
|
|
1228
|
+
if (!releaseLane(root, lane)) return out(`The ${lane} lane isn't claimed.`);
|
|
1229
|
+
return out(
|
|
1230
|
+
`Released the ${lane} lane${held.story ? ` (it was running ${held.story})` : ''}. Its next story can start.`,
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
if (rest.length) die('code-kit lanes [--json] | lanes release <lane>');
|
|
1234
|
+
const claims = laneClaims(root);
|
|
1235
|
+
if (flag('--json')) return out(JSON.stringify(claims, null, 2));
|
|
1236
|
+
if (!claims.length) return out('No lane is running a story.');
|
|
1237
|
+
for (const c of claims)
|
|
1238
|
+
out(
|
|
1239
|
+
`${c.lane.padEnd(12)} ${(c.story ?? 'a story').padEnd(8)} session ${shortSession(c.session) ?? '?'}, since ${c.since.slice(0, 16).replace('T', ' ')} UTC${c.stale ? ' (stale: no agent seen to finish in 12 hours)' : ''}`,
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1037
1243
|
/**
|
|
1038
1244
|
* The harness settings (`harness` in the config): listed, or one changed as the person's own act. The
|
|
1039
1245
|
* change is written to the config and given a kit approval with the person's reason, so the commit that
|
|
@@ -1045,33 +1251,50 @@ function settings() {
|
|
|
1045
1251
|
const [, key, value] = rest;
|
|
1046
1252
|
if (!key || value === undefined)
|
|
1047
1253
|
die('code-kit settings set <key> <value> --reason "<why>" [--via pane]');
|
|
1048
|
-
|
|
1049
|
-
|
|
1254
|
+
const approvalKey = key.startsWith('approvals.');
|
|
1255
|
+
if (!(key in SETTINGS) && !approvalKey)
|
|
1256
|
+
die(
|
|
1257
|
+
`${key} isn't a setting. They are: ${Object.keys(SETTINGS).join(', ')}, and approvals.<name> (see code-kit settings).`,
|
|
1258
|
+
);
|
|
1050
1259
|
if (!reason?.trim()) die('Give a reason with --reason "…": the approval log keeps it.');
|
|
1051
|
-
|
|
1260
|
+
// WHO-3: who decides an approval is the person's setting too, written out in full.
|
|
1261
|
+
const changed = approvalKey
|
|
1262
|
+
? withApprovalSetting(raw, config, key, value)
|
|
1263
|
+
: withSetting(raw, key, parseSetting(key, value));
|
|
1264
|
+
if (!changed)
|
|
1265
|
+
die(
|
|
1266
|
+
`${key} isn't an approval setting, or ${JSON.stringify(value)} isn't one of its values. See code-kit settings.`,
|
|
1267
|
+
);
|
|
1052
1268
|
const problems = validate(changed);
|
|
1053
1269
|
if (problems.length) die(`Nothing changed: ${problems.join('; ')}`);
|
|
1054
1270
|
writeFileSync(resolve(file), `${JSON.stringify(changed, null, 2)}\n`);
|
|
1055
1271
|
const mark =
|
|
1056
1272
|
via === 'pane' ? '(changed in the code-kit pane)' : '(changed with code-kit settings)';
|
|
1057
1273
|
grantApprovals(resolve('.'), ['kit'], null, reason, { kit: mark });
|
|
1274
|
+
const now = approvalKey
|
|
1275
|
+
? approvalSettings(effectiveConfig(changed, resolve('.'))).find((r) => r.key === key)?.value
|
|
1276
|
+
: settingOf(changed.harness, key);
|
|
1058
1277
|
return out(
|
|
1059
|
-
`Set harness.${key} to ${JSON.stringify(
|
|
1278
|
+
`Set ${approvalKey ? key : `harness.${key}`} to ${JSON.stringify(now)}. Commit ${file} to keep it; the approval log will carry your reason.`,
|
|
1060
1279
|
);
|
|
1061
1280
|
}
|
|
1062
1281
|
if (rest.length) die('code-kit settings [--json] | settings set <key> <value> --reason "<why>"');
|
|
1063
|
-
const rows =
|
|
1064
|
-
key,
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1282
|
+
const rows = [
|
|
1283
|
+
...Object.entries(SETTINGS).map(([key, s]) => ({
|
|
1284
|
+
key,
|
|
1285
|
+
value: settingOf(config.harness, key),
|
|
1286
|
+
default: s.default,
|
|
1287
|
+
about: s.about,
|
|
1288
|
+
section: 'Harness',
|
|
1289
|
+
// Chosen in the config, or the default standing in.
|
|
1290
|
+
set: settingOf(raw.harness, key) !== undefined,
|
|
1291
|
+
})),
|
|
1292
|
+
...approvalSettings(config),
|
|
1293
|
+
];
|
|
1071
1294
|
if (flag('--json')) return out(JSON.stringify(rows, null, 2));
|
|
1072
1295
|
for (const r of rows)
|
|
1073
1296
|
out(
|
|
1074
|
-
`${r.key.padEnd(
|
|
1297
|
+
`${r.key.padEnd(44)} ${JSON.stringify(r.value).padEnd(14)} ${r.value === r.default ? '' : `(default ${JSON.stringify(r.default)}) `}${r.about}`,
|
|
1075
1298
|
);
|
|
1076
1299
|
}
|
|
1077
1300
|
|
|
@@ -1253,7 +1476,14 @@ function status() {
|
|
|
1253
1476
|
if (flag('--json')) {
|
|
1254
1477
|
out(
|
|
1255
1478
|
JSON.stringify(
|
|
1256
|
-
{
|
|
1479
|
+
{
|
|
1480
|
+
base: from,
|
|
1481
|
+
stories,
|
|
1482
|
+
slices: slicesOf(stories),
|
|
1483
|
+
requirements: byRequirement,
|
|
1484
|
+
problems,
|
|
1485
|
+
drift,
|
|
1486
|
+
},
|
|
1257
1487
|
null,
|
|
1258
1488
|
2,
|
|
1259
1489
|
),
|
|
@@ -1271,9 +1501,18 @@ function status() {
|
|
|
1271
1501
|
? s.branch
|
|
1272
1502
|
: '';
|
|
1273
1503
|
out(
|
|
1274
|
-
` ${pad(s.id, 7)} ${pad(s.state, 12)} ${pad(s.lane || '-', 10)} ${pad(s.requirements.join(', '), 24)} ${detail}`.trimEnd(),
|
|
1504
|
+
` ${pad(s.id, 7)} ${pad(s.state, 12)} ${pad(s.lane || '-', 10)} ${pad(s.requirements.join(', '), 24)} ${detail}${s.slice ? `${detail ? ' · ' : ''}slice ${s.slice}` : ''}`.trimEnd(),
|
|
1275
1505
|
);
|
|
1276
1506
|
}
|
|
1507
|
+
// SCHED-2: the slices, in plan order, and which are open.
|
|
1508
|
+
const slices = slicesOf(stories);
|
|
1509
|
+
if (slices.length) {
|
|
1510
|
+
out('\nSlices:');
|
|
1511
|
+
for (const sl of slices)
|
|
1512
|
+
out(
|
|
1513
|
+
` ${pad(sl.name, 24)} ${pad(sl.done ? 'done' : sl.open ? 'open' : 'not started', 12)} ${sl.stories.join(', ')}`,
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1277
1516
|
out('\nRequirements:');
|
|
1278
1517
|
for (const r of byRequirement) {
|
|
1279
1518
|
const tests = r.tests.length ? `tested (${r.tests.join(', ')})` : 'no test names it';
|
|
@@ -1393,7 +1632,7 @@ async function approve(given) {
|
|
|
1393
1632
|
const rules = config.approvals.delegate;
|
|
1394
1633
|
if (!rules)
|
|
1395
1634
|
die(
|
|
1396
|
-
'This project
|
|
1635
|
+
'This project keeps every approval for the person ("approvals.delegate" is false). Ask the person.',
|
|
1397
1636
|
);
|
|
1398
1637
|
// HELM-3: the lead approves a package for a story it has read, not by default.
|
|
1399
1638
|
const packages = names.filter((n) => n.startsWith(DEPENDENCY_APPROVAL));
|
|
@@ -1410,8 +1649,16 @@ async function approve(given) {
|
|
|
1410
1649
|
: null;
|
|
1411
1650
|
const outside = [];
|
|
1412
1651
|
for (const name of names) {
|
|
1652
|
+
// WHO-2: a weakened test on a lane's branch, after the lead's review; on its own, the person's.
|
|
1413
1653
|
if (name === TESTS_APPROVAL) {
|
|
1414
|
-
outside.push(`${name}:
|
|
1654
|
+
if (!rules.tests) outside.push(`${name}: weakened tests aren't delegated`);
|
|
1655
|
+
else if (!laneName)
|
|
1656
|
+
outside.push(
|
|
1657
|
+
`${name}: on your own branch a weakened test is the person's; you approve one only for a lane, with --lane`,
|
|
1658
|
+
);
|
|
1659
|
+
else if (!storyGiven)
|
|
1660
|
+
outside.push(`${name}: name the story whose branch weakens the test, with --story ST-n`);
|
|
1661
|
+
else marks[name] = delegatedMark(`tests, reviewed by the lead, for ${story.id}`);
|
|
1415
1662
|
continue;
|
|
1416
1663
|
}
|
|
1417
1664
|
if (name.startsWith(DEPENDENCY_APPROVAL)) {
|
|
@@ -1725,6 +1972,12 @@ async function pulseCommand(raw, team, provider) {
|
|
|
1725
1972
|
`${JSON.stringify({ at: Date.now(), entries: input.usage })}\n`,
|
|
1726
1973
|
);
|
|
1727
1974
|
}
|
|
1975
|
+
// STD-6: the organisation's standards, for the hooks and instructions, read every ten minutes.
|
|
1976
|
+
const inherited = readInherited(root);
|
|
1977
|
+
if (!inherited.at || Date.now() - Date.parse(inherited.at) >= REPORT_MINUTES * 60_000) {
|
|
1978
|
+
const r = provider.standards();
|
|
1979
|
+
if (r && r.ok !== false && Array.isArray(r.standards)) writeInherited(root, r);
|
|
1980
|
+
}
|
|
1728
1981
|
// Building the snapshot reads git: at most once a minute.
|
|
1729
1982
|
const built = join(mainCheckout(root), '.claude/state/report-built.json');
|
|
1730
1983
|
let at = 0;
|
|
@@ -1926,6 +2179,7 @@ function askCommand() {
|
|
|
1926
2179
|
if (flag('--blocks') && !storyGiven) die('--blocks holds a story: name it with --story ST-n.');
|
|
1927
2180
|
const r = askQuestion(root, {
|
|
1928
2181
|
by: 'lead',
|
|
2182
|
+
session: callerSession,
|
|
1929
2183
|
question,
|
|
1930
2184
|
why: whyGiven ?? null,
|
|
1931
2185
|
story: storyGiven ?? null,
|
|
@@ -2017,7 +2271,9 @@ function loopCommand() {
|
|
|
2017
2271
|
const r = claimLead(root, sessionId, { take: flag('--take') });
|
|
2018
2272
|
if (!r.ok)
|
|
2019
2273
|
die(
|
|
2020
|
-
|
|
2274
|
+
r.lead
|
|
2275
|
+
? `Another session (${shortSession(r.lead.session)}, seen ${r.lead.at}) leads this project and runs its loop. Leave it there, or lead from here with --take.`
|
|
2276
|
+
: 'No session leads this project now, and this is a working session: the loop runs only in the lead session. Lead from here with --take.',
|
|
2021
2277
|
);
|
|
2022
2278
|
return out('This session runs the lead loop.');
|
|
2023
2279
|
}
|
|
@@ -2026,9 +2282,11 @@ function loopCommand() {
|
|
|
2026
2282
|
'code-kit loop [--json] | loop pause | loop resume | loop lead --session <id> [--take] | loop release --session <id>',
|
|
2027
2283
|
);
|
|
2028
2284
|
const state = readLoop(root);
|
|
2285
|
+
// SESS-2: with --session, whether that session leads the project or works in it.
|
|
2286
|
+
const role = sessionId ? (readSession(root, sessionId)?.role ?? null) : null;
|
|
2029
2287
|
out(
|
|
2030
2288
|
flag('--json')
|
|
2031
|
-
? JSON.stringify(state)
|
|
2289
|
+
? JSON.stringify({ ...state, ...(sessionId ? { role } : {}) })
|
|
2032
2290
|
: `${state.paused ? `The lead loop is paused (since ${state.at}).` : 'The lead loop is running.'}${state.lead ? ` Session ${state.lead.session} runs it.` : ''}`,
|
|
2033
2291
|
);
|
|
2034
2292
|
}
|
|
@@ -2041,8 +2299,11 @@ function instructionsCommand() {
|
|
|
2041
2299
|
const { config } = load();
|
|
2042
2300
|
const root = resolve('.');
|
|
2043
2301
|
if (rest[0] === 'show') {
|
|
2044
|
-
if (!['lead', 'lane'].includes(rest[1])) die('code-kit instructions show lead|lane');
|
|
2045
|
-
|
|
2302
|
+
if (!['lead', 'lane'].includes(rest[1])) die('code-kit instructions show lead|lane [<lane>]');
|
|
2303
|
+
if (rest[2] && !config.lanes[rest[2]])
|
|
2304
|
+
die(`No lane is called ${rest[2]}: ${Object.keys(config.lanes).join(', ') || 'none'}.`);
|
|
2305
|
+
// A named lane's rules carry the standards for its layers (spec 22, STD-3).
|
|
2306
|
+
return out(rest[1] === 'lead' ? leadSection(root, config) : agentRules(root, config, rest[2]));
|
|
2046
2307
|
}
|
|
2047
2308
|
const changed = flag('--write') ? writeInstructions(root, config) : [];
|
|
2048
2309
|
const files = instructionsState(root, config);
|
|
@@ -2068,7 +2329,7 @@ function instructionsCommand() {
|
|
|
2068
2329
|
for (const f of files) out(` ${f.file}: ${said[f.state]}`);
|
|
2069
2330
|
if (files.some((f) => f.state === 'legacy'))
|
|
2070
2331
|
out(
|
|
2071
|
-
'\nTo merge one: replace its old code-kit part (in CLAUDE.md from `## Enforced, not advised` to the line about changing the rules; in an agent file from `Always:` to the end of its list) with what `code-kit instructions show lead` or `show lane
|
|
2332
|
+
'\nTo merge one: replace its old code-kit part (in CLAUDE.md from `## Enforced, not advised` to the line about changing the rules; in an agent file from `Always:` to the end of its list) with what `code-kit instructions show lead` or `show lane <lane>` prints. Keep every line the project wrote itself (its own rules, its layer order, its docs) outside the markers, under a heading of its own, so a later refresh never touches them.',
|
|
2072
2333
|
);
|
|
2073
2334
|
}
|
|
2074
2335
|
|
|
@@ -2147,8 +2408,78 @@ function worktreesCommand() {
|
|
|
2147
2408
|
);
|
|
2148
2409
|
}
|
|
2149
2410
|
|
|
2411
|
+
/**
|
|
2412
|
+
* Spec 22: `standards` lists the standards that apply (the project's file, merged with what an
|
|
2413
|
+
* organisation's team provider supplies) and what's wrong with them (STD-1, STD-6); `--for <path>...`
|
|
2414
|
+
* keeps the ones for those files' layers, as a review reads them (STD-4). Exits 1 on a problem.
|
|
2415
|
+
*/
|
|
2416
|
+
function standardsCommand() {
|
|
2417
|
+
const { raw, config } = load();
|
|
2418
|
+
const root = resolve('.');
|
|
2419
|
+
const team = teamOf(raw);
|
|
2420
|
+
let fetched = null;
|
|
2421
|
+
if (team && team.provider !== 'github') {
|
|
2422
|
+
const r = teamProvider(root, team).standards();
|
|
2423
|
+
if (r && r.ok !== false && Array.isArray(r.standards)) writeInherited(root, r);
|
|
2424
|
+
else fetched = r?.why ?? "The team provider didn't answer.";
|
|
2425
|
+
}
|
|
2426
|
+
const found = projectStandards(root, config);
|
|
2427
|
+
const { waivers } = readInherited(root);
|
|
2428
|
+
const files = flag('--for') ? rest : [];
|
|
2429
|
+
const layersOf = (f) =>
|
|
2430
|
+
config.layers.length ? [layerOf(f, config.layers)?.name].filter(Boolean) : null;
|
|
2431
|
+
const standards = files.length
|
|
2432
|
+
? found.standards.filter((s) => files.some((f) => appliesTo(s, layersOf(f))))
|
|
2433
|
+
: found.standards;
|
|
2434
|
+
if (flag('--json')) {
|
|
2435
|
+
out(
|
|
2436
|
+
JSON.stringify(
|
|
2437
|
+
{
|
|
2438
|
+
file: found.file,
|
|
2439
|
+
exists: found.exists,
|
|
2440
|
+
version: found.version,
|
|
2441
|
+
...(files.length ? { for: files } : {}),
|
|
2442
|
+
standards,
|
|
2443
|
+
waivers,
|
|
2444
|
+
problems: found.problems,
|
|
2445
|
+
...(fetched ? { provider: fetched } : {}),
|
|
2446
|
+
},
|
|
2447
|
+
null,
|
|
2448
|
+
2,
|
|
2449
|
+
),
|
|
2450
|
+
);
|
|
2451
|
+
if (found.problems.length) process.exit(1);
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
if (!found.exists && !found.standards.length)
|
|
2455
|
+
return out(
|
|
2456
|
+
`This project has no standards yet: ${found.file} doesn't exist. /code-kit:init suggests them from the specs or the code, and the person approves each.`,
|
|
2457
|
+
);
|
|
2458
|
+
if (fetched)
|
|
2459
|
+
out(`The organisation's standards weren't refreshed (${fetched}); using the last ones read.`);
|
|
2460
|
+
const inherited = found.standards.filter((s) => s.origin !== 'own').length;
|
|
2461
|
+
out(
|
|
2462
|
+
`${found.standards.length} standard${found.standards.length === 1 ? '' : 's'} apply${files.length ? `; ${standards.length} to ${files.join(', ')}` : ''}: ${found.standards.length - inherited} from ${found.file}${inherited ? `, ${inherited} from the organisation${found.version ? ` (version ${found.version})` : ''}` : ''}.`,
|
|
2463
|
+
);
|
|
2464
|
+
for (const s of standards) {
|
|
2465
|
+
const rule = s.applied === 'enforced' && s.enforcedBy ? ` · enforced by ${s.enforcedBy}` : '';
|
|
2466
|
+
const where = s.layers?.length ? ` · ${s.layers.join(', ')}` : '';
|
|
2467
|
+
const origin = s.origin === 'own' ? '' : ` · ${s.origin}`;
|
|
2468
|
+
out(` ${standardLine({ ...s, origin: 'own', waiver: null })}${where}${rule}${origin}`);
|
|
2469
|
+
if (s.waiver)
|
|
2470
|
+
out(
|
|
2471
|
+
` waived${s.waiver.paths?.length ? ` for ${s.waiver.paths.join(', ')}` : ''}${s.waiver.expires ? ` until ${s.waiver.expires}` : ''}${s.waiver.approvedBy ? `, approved by ${s.waiver.approvedBy}` : ''}${s.waiver.reason ? `: ${s.waiver.reason}` : ''}`,
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
if (!found.problems.length) return;
|
|
2475
|
+
out(`\nProblems:`);
|
|
2476
|
+
for (const p of found.problems) out(` ${p}`);
|
|
2477
|
+
process.exit(1);
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2150
2480
|
const commands = {
|
|
2151
2481
|
check,
|
|
2482
|
+
standards: standardsCommand,
|
|
2152
2483
|
worktrees: worktreesCommand,
|
|
2153
2484
|
instructions: instructionsCommand,
|
|
2154
2485
|
loop: loopCommand,
|
|
@@ -2157,6 +2488,10 @@ const commands = {
|
|
|
2157
2488
|
requests,
|
|
2158
2489
|
stops,
|
|
2159
2490
|
settings,
|
|
2491
|
+
lanes: lanesCommand,
|
|
2492
|
+
sessions: sessionsCommand,
|
|
2493
|
+
decisions: decisionsCommand,
|
|
2494
|
+
task: taskCommand,
|
|
2160
2495
|
hold: () => (rest.length === 1 ? hold(rest[0]) : usage()),
|
|
2161
2496
|
queue,
|
|
2162
2497
|
team: teamCommand,
|
|
@@ -2179,7 +2514,7 @@ const commands = {
|
|
|
2179
2514
|
diff,
|
|
2180
2515
|
baseline,
|
|
2181
2516
|
graph,
|
|
2182
|
-
who: () => (rest.length ? who(rest) :
|
|
2517
|
+
who: () => (rest.length ? who(rest) : whoAmI()),
|
|
2183
2518
|
};
|
|
2184
2519
|
function usage() {
|
|
2185
2520
|
die(
|
|
@@ -2187,7 +2522,8 @@ function usage() {
|
|
|
2187
2522
|
' | verify [--base ref] [--branch name] [--no-checks] | status [--base ref] [--json]\n' +
|
|
2188
2523
|
' | security [--base ref] [--branch name] [--json]\n' +
|
|
2189
2524
|
' | next [--base ref] [--json] | adapters | trace <path>... [--json]\n' +
|
|
2190
|
-
' | instructions [--write] [--json] | instructions show lead|lane\n' +
|
|
2525
|
+
' | instructions [--write] [--json] | instructions show lead|lane [<lane>]\n' +
|
|
2526
|
+
' | standards [--json] [--for <path>...]\n' +
|
|
2191
2527
|
' | worktrees [--json] | worktrees release <branch> | worktrees prune\n' +
|
|
2192
2528
|
' | approve <name>... --reason "…" [--lane name] [--config file]\n' +
|
|
2193
2529
|
' | merge <branch> --delegated|--person [--into branch] | requests [--json] | requests drop <n>|all\n' +
|
|
@@ -2197,6 +2533,9 @@ function usage() {
|
|
|
2197
2533
|
' | report [--json] | report send|discard <id>\n' +
|
|
2198
2534
|
' | stops [--session id] [--json] | sent-back <branch> [--reason "…"]\n' +
|
|
2199
2535
|
' | settings [--json] | settings set <key> <value> --reason "…" [--via pane]\n' +
|
|
2536
|
+
' | lanes [--json] | lanes release <lane> | sessions [--json] | sessions lead\n' +
|
|
2537
|
+
' | who | task "<what>" --touches globs | task done | task take <session>\n' +
|
|
2538
|
+
' | decisions [--json] [--all]\n' +
|
|
2200
2539
|
' | hold <id> [--minutes N] | hold <id> --answer approved|refused\n' +
|
|
2201
2540
|
' | queue [--json] | queue add|drop <branch> | queue rules | queue merge --delegated|--person\n' +
|
|
2202
2541
|
' | loop [--json] | loop pause|resume | loop lead|release --session id [--take]\n' +
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PreToolUse on Agent (docs/specs/23-the-team.md, SCHED-1): a lane runs one story at a time, across every
|
|
3
|
+
// session in the project. Starting a lane's agent claims the lane; a second agent for a claimed lane is
|
|
4
|
+
// refused, naming the story and the session that has it. Any other agent starts as before.
|
|
5
|
+
import { block, start } from './lib/hook.mjs';
|
|
6
|
+
import { CLI } from './lib/rules.mjs';
|
|
7
|
+
import { claimLane, shortSession } from './lib/laneclaims.mjs';
|
|
8
|
+
import { memberOf, readSession } from './lib/sessions.mjs';
|
|
9
|
+
|
|
10
|
+
const { input, project, config, error } = start();
|
|
11
|
+
// A broken config is reported by the other hooks; it doesn't stop agents starting.
|
|
12
|
+
if (error) process.exit(0);
|
|
13
|
+
const call = input.tool_input ?? {};
|
|
14
|
+
const lane = Object.entries(config.lanes).find(([, l]) => l.agent === call.subagent_type)?.[0];
|
|
15
|
+
if (!lane) process.exit(0);
|
|
16
|
+
const story =
|
|
17
|
+
String(call.description ?? '')
|
|
18
|
+
.match(/\bST-\d+\b/i)?.[0]
|
|
19
|
+
?.toUpperCase() ?? null;
|
|
20
|
+
const session = input.session_id ?? null;
|
|
21
|
+
const claimed = claimLane(project, { lane, story, session });
|
|
22
|
+
if (!claimed.ok) {
|
|
23
|
+
const c = claimed.claim;
|
|
24
|
+
// SESS-1: the session by its member and, where it has one, its task.
|
|
25
|
+
const holder = readSession(project, c.session);
|
|
26
|
+
const where =
|
|
27
|
+
c.session && c.session === session
|
|
28
|
+
? 'in this session'
|
|
29
|
+
: c.session
|
|
30
|
+
? `in another session (${holder ? memberOf(holder) : shortSession(c.session)}${holder?.task ? `, working on "${holder.task.title}"` : ''})`
|
|
31
|
+
: 'here';
|
|
32
|
+
block(
|
|
33
|
+
`Blocked: the ${lane} lane is already running ${c.story ?? 'a story'} ${where}, since ${c.since.slice(11, 16)} UTC. A lane runs one story at a time across the project: two agents in one lane build on the same files in their worktrees and conflict at merge.\n` +
|
|
34
|
+
`Start ${story ?? 'this story'} when ${c.story ?? 'that story'} is finished, or dispatch a ready story in another lane meanwhile. If that agent has stopped and the lane is free, release it first:\n node "${CLI}" lanes release ${lane}`,
|
|
35
|
+
);
|
|
36
|
+
}
|