@warren-dean/code-kit 0.21.0 → 0.22.1
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 +20 -0
- package/README.md +46 -23
- package/bin/code-kit.mjs +271 -21
- package/hooks/guard-agent.mjs +36 -0
- package/hooks/guard-bash.mjs +23 -11
- package/hooks/guard-paths.mjs +11 -0
- package/hooks/hooks.json +20 -0
- package/hooks/lane-audit.mjs +38 -1
- package/hooks/lib/adapters/hub/client.mjs +177 -25
- package/hooks/lib/config.mjs +148 -5
- 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 +1 -1
- package/hooks/lib/laneclaims.mjs +89 -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 +18 -12
- package/hooks/lib/schedule.mjs +86 -0
- package/hooks/lib/sessions.mjs +223 -0
- package/hooks/lib/snapshot.mjs +9 -3
- 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 +118 -41
- package/hooks/mod/register.test.ts +244 -73
- package/hooks/mod/view.mjs +98 -47
- package/hooks/post-edit-check.mjs +3 -0
- package/hooks/session.mjs +24 -0
- package/hooks/stop-check.mjs +12 -0
- package/package.json +1 -1
- package/skills/decide/SKILL.md +1 -1
- package/skills/dispatch/SKILL.md +2 -2
- package/skills/init/SKILL.md +1 -1
- package/skills/lead/SKILL.md +28 -0
- package/skills/next/SKILL.md +1 -1
- package/skills/spec-design/SKILL.md +2 -1
- package/skills/team-init/SKILL.md +1 -1
- package/templates/claude-md.md +3 -1
- package/templates/plan.md +2 -1
package/hooks/mod/view.mjs
CHANGED
|
@@ -256,7 +256,7 @@ export function lanesPane(
|
|
|
256
256
|
: l.branch
|
|
257
257
|
? ` · ${l.branch}`
|
|
258
258
|
: ''
|
|
259
|
-
}`
|
|
259
|
+
}${l.story.slice ? ` · slice ${l.story.slice}` : ''}`
|
|
260
260
|
: l.name === LEAD
|
|
261
261
|
? 'the main session'
|
|
262
262
|
: (l.agent ?? '');
|
|
@@ -728,13 +728,25 @@ export function bandLines({
|
|
|
728
728
|
team = null,
|
|
729
729
|
questions = [],
|
|
730
730
|
brief = [],
|
|
731
|
-
denied = [],
|
|
732
731
|
reports = [],
|
|
732
|
+
working = false,
|
|
733
|
+
task = null,
|
|
734
|
+
elsewhere = [],
|
|
733
735
|
}) {
|
|
734
736
|
const lines = [];
|
|
735
|
-
// MISSION-1: the goal, now, next and the agents at work, above what needs the person.
|
|
736
|
-
|
|
737
|
+
// MISSION-1: the goal, now, next and the agents at work, above what needs the person. SESS-3: a working
|
|
738
|
+
// session's band holds only its own: no mission, queue, reviews or team.
|
|
739
|
+
const mission = working ? [] : missionLines({ state, loop, leadBusy, live, nextStep, combined });
|
|
737
740
|
lines.push(...mission);
|
|
741
|
+
// SESS-4: a working session's task, or that it records one before changing code.
|
|
742
|
+
if (working)
|
|
743
|
+
lines.push({
|
|
744
|
+
kind: 'task',
|
|
745
|
+
text: task
|
|
746
|
+
? `your task: ${task.title}${task.paths?.length ? ` · leasing ${task.paths.join(', ')}` : ''}`
|
|
747
|
+
: 'a working session: record your task before changing code (code-kit task "…" --touches …)',
|
|
748
|
+
actions: [],
|
|
749
|
+
});
|
|
738
750
|
if (notice)
|
|
739
751
|
lines.push({ kind: 'notice', text: notice, actions: [{ id: 'dismiss', label: 'Dismiss' }] });
|
|
740
752
|
if (!state || state.kind !== 'ok') return lines;
|
|
@@ -758,31 +770,15 @@ export function bandLines({
|
|
|
758
770
|
],
|
|
759
771
|
});
|
|
760
772
|
}
|
|
761
|
-
//
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
lines.push({
|
|
765
|
-
kind: 'denied',
|
|
766
|
-
text: `${denied.length > 1 ? `${denied.length} calls refused by Claude Code: ` : 'Claude Code refused '}${d.who}'s ${d.what}`,
|
|
767
|
-
denial: d,
|
|
768
|
-
denials: denied,
|
|
769
|
-
actions: [
|
|
770
|
-
...(d.command ? [{ id: 'runDenied', label: 'Put it in the prompt' }] : []),
|
|
771
|
-
{ id: 'deniedDone', label: 'Done it' },
|
|
772
|
-
{ id: 'deniedNo', label: 'Not now' },
|
|
773
|
-
],
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
// LOOP-3: a lane stalled past its restarts waits for the person.
|
|
773
|
+
// OWN-2: a call Claude Code's own permission check refused is the Lanes pane's activity, not a
|
|
774
|
+
// decision: the agent finds another way, or the lead asks the person (`code-kit ask`).
|
|
775
|
+
// LOOP-3, WHO-4: a lane stalled past its restarts is the lead's to sort out; the person sees a note.
|
|
777
776
|
for (const f of loop?.flagged ?? [])
|
|
778
777
|
lines.push({
|
|
779
778
|
kind: 'stalled',
|
|
780
|
-
text: `${f.lane} stalled ${f.count} times on ${f.story}`,
|
|
779
|
+
text: `${f.lane} stalled ${f.count} times on ${f.story}: the lead decides how it goes on`,
|
|
781
780
|
stall: f,
|
|
782
|
-
actions: [
|
|
783
|
-
{ id: 'lanes', label: 'Lanes' },
|
|
784
|
-
{ id: 'resumeStall', label: 'Resume' },
|
|
785
|
-
],
|
|
781
|
+
actions: [{ id: 'lanes', label: 'Lanes' }],
|
|
786
782
|
});
|
|
787
783
|
// LOOP-5: under autonomy propose, each step waits for Go.
|
|
788
784
|
if (loop?.proposal?.first)
|
|
@@ -797,15 +793,20 @@ export function bandLines({
|
|
|
797
793
|
{ id: 'loopOff', label: 'Off' },
|
|
798
794
|
],
|
|
799
795
|
});
|
|
796
|
+
else if (loop?.proposal?.repeated)
|
|
797
|
+
// LOOP-8, WHO-4: a step the lead was asked for three times and didn't take is a note, not a decision.
|
|
798
|
+
lines.push({
|
|
799
|
+
kind: 'stuck',
|
|
800
|
+
text: `${loop.proposal.label}: still next after ${loop.proposal.repeated} prompts to the lead`,
|
|
801
|
+
proposal: loop.proposal,
|
|
802
|
+
actions: [{ id: 'go', label: 'Ask again' }],
|
|
803
|
+
});
|
|
800
804
|
else if (loop?.proposal)
|
|
801
805
|
lines.push({
|
|
802
806
|
kind: 'proposal',
|
|
803
|
-
|
|
804
|
-
text: loop.proposal.repeated
|
|
805
|
-
? `${loop.proposal.label}: still next after ${loop.proposal.repeated} prompts. Ask the lead again?`
|
|
806
|
-
: `${loop.proposal.label}?`,
|
|
807
|
+
text: `${loop.proposal.label}?`,
|
|
807
808
|
proposal: loop.proposal,
|
|
808
|
-
actions: [{ id: 'go', label:
|
|
809
|
+
actions: [{ id: 'go', label: 'Go' }],
|
|
809
810
|
});
|
|
810
811
|
// LOOP-11, LOOP-12: what the loop read in the lead's reply holds it until the person says.
|
|
811
812
|
const waitingLine = waitingOf(loop?.waiting);
|
|
@@ -833,7 +834,7 @@ export function bandLines({
|
|
|
833
834
|
actions: [{ id: 'openDecision', label: 'Answer…' }],
|
|
834
835
|
});
|
|
835
836
|
// ASK-4: the brief's open questions, the person's to answer.
|
|
836
|
-
if (brief.length)
|
|
837
|
+
if (brief.length && !working)
|
|
837
838
|
lines.push({
|
|
838
839
|
kind: 'brief',
|
|
839
840
|
text: `${plural(brief.length, 'open question', 'open questions')} in your brief: ${brief[0].question}`,
|
|
@@ -848,7 +849,7 @@ export function bandLines({
|
|
|
848
849
|
const inReview = loopOff
|
|
849
850
|
? state.lanes.filter((l) => l.state === 'in review').map((l) => l.story)
|
|
850
851
|
: [];
|
|
851
|
-
if (inReview.length) {
|
|
852
|
+
if (inReview.length && !working) {
|
|
852
853
|
const [story] = inReview;
|
|
853
854
|
const ids = inReview.map((s) => s.id).join(', ');
|
|
854
855
|
lines.push(
|
|
@@ -891,7 +892,7 @@ export function bandLines({
|
|
|
891
892
|
const autonomous = (check?.harness?.autonomy ?? 'autonomous') === 'autonomous';
|
|
892
893
|
// SETUP-2: with no rules on the base yet, the lead puts them there first and then merges.
|
|
893
894
|
const leadMerges = autonomous && (check?.delegatesMerge || check?.firstMerge);
|
|
894
|
-
if (next && !leadMerges)
|
|
895
|
+
if (next && !leadMerges && !working)
|
|
895
896
|
lines.push({
|
|
896
897
|
kind: 'queue',
|
|
897
898
|
text: `${next.branch} is next to merge`,
|
|
@@ -899,9 +900,9 @@ export function bandLines({
|
|
|
899
900
|
actions: [{ id: 'mergeQueue', label: 'Merge' }],
|
|
900
901
|
});
|
|
901
902
|
// TEAM-7, TEAM-9: what of the team needs this person.
|
|
902
|
-
lines.push(...teamLines(team, check?.lanes ?? {}));
|
|
903
|
+
if (!working) lines.push(...teamLines(team, check?.lanes ?? {}));
|
|
903
904
|
// REPORT-3: a bug report for code-kit an agent drafted, for the person to read and send, or not.
|
|
904
|
-
if (reports.length)
|
|
905
|
+
if (reports.length && !working)
|
|
905
906
|
lines.push({
|
|
906
907
|
kind: 'report',
|
|
907
908
|
text: `${reports.length > 1 ? `${reports.length} bug reports for code-kit to send: ` : 'A bug report for code-kit to send: '}${reports[0].title}`,
|
|
@@ -922,11 +923,14 @@ export function bandLines({
|
|
|
922
923
|
// With the mission rows drawn, the loop's state and its Pause or Resume are on the `now` row.
|
|
923
924
|
else if (mission.length) {
|
|
924
925
|
// nothing more
|
|
925
|
-
} else if (loop?.state === 'elsewhere')
|
|
926
|
-
//
|
|
926
|
+
} else if (loop?.state === 'elsewhere' || loop?.state === 'unled')
|
|
927
|
+
// SESS-2: another session leads the project, or none does; this one never prompts its lead.
|
|
927
928
|
lines.push({
|
|
928
929
|
kind: 'loop',
|
|
929
|
-
text:
|
|
930
|
+
text:
|
|
931
|
+
loop.state === 'elsewhere'
|
|
932
|
+
? 'the lead is in another session'
|
|
933
|
+
: 'no session leads the project: the loop waits for one',
|
|
930
934
|
actions: [{ id: 'leadHere', label: 'Lead from here' }],
|
|
931
935
|
});
|
|
932
936
|
else if (loop?.state === 'paused')
|
|
@@ -937,6 +941,13 @@ export function bandLines({
|
|
|
937
941
|
text: `loop on${loop.last ? ` · last: ${loop.last}` : ''}`,
|
|
938
942
|
actions: [{ id: 'pause', label: 'Pause' }],
|
|
939
943
|
});
|
|
944
|
+
// OWN-1: another session's decisions are answered there: here, only how many.
|
|
945
|
+
for (const e of elsewhere)
|
|
946
|
+
lines.push({
|
|
947
|
+
kind: 'elsewhere',
|
|
948
|
+
text: `${plural(e.count, 'decision', 'decisions')} in session ${e.member}, answered there`,
|
|
949
|
+
actions: [],
|
|
950
|
+
});
|
|
940
951
|
// BAND-4: a digit for every action, in the order they're drawn.
|
|
941
952
|
let digit = 0;
|
|
942
953
|
for (const line of lines)
|
|
@@ -969,9 +980,10 @@ export function waitingOf(waiting) {
|
|
|
969
980
|
waiting,
|
|
970
981
|
actions: [{ id: 'carryOn', label: 'Carry on' }],
|
|
971
982
|
};
|
|
983
|
+
// WHO-4: a step the lead said it couldn't take is a note: the loop asks again once the project moves.
|
|
972
984
|
if (waiting.kind === 'blocked')
|
|
973
985
|
return {
|
|
974
|
-
kind: '
|
|
986
|
+
kind: 'stuck',
|
|
975
987
|
text: `${waiting.label}: the lead didn't take it${waiting.reason ? `: ${waiting.reason}` : ''}`,
|
|
976
988
|
waiting,
|
|
977
989
|
actions: [{ id: 'askAgain', label: 'Ask again' }],
|
|
@@ -1006,8 +1018,8 @@ export function band(lines, { Box, Text, Button }, onAction) {
|
|
|
1006
1018
|
facts: 'yellow',
|
|
1007
1019
|
question: 'cyan',
|
|
1008
1020
|
brief: 'cyan',
|
|
1009
|
-
denied: 'red',
|
|
1010
1021
|
report: 'magenta',
|
|
1022
|
+
stuck: 'yellow',
|
|
1011
1023
|
};
|
|
1012
1024
|
const LIVE = { quiet: 'yellow', stalled: 'red' };
|
|
1013
1025
|
const LABEL = 8;
|
|
@@ -1516,7 +1528,17 @@ const CHOICES = {
|
|
|
1516
1528
|
'compact.milestone': ['ask', 'auto', 'off'],
|
|
1517
1529
|
};
|
|
1518
1530
|
const shown = (v) =>
|
|
1519
|
-
v === null || v === ''
|
|
1531
|
+
v === null || v === ''
|
|
1532
|
+
? "the session's"
|
|
1533
|
+
: v === true
|
|
1534
|
+
? 'on'
|
|
1535
|
+
: v === false
|
|
1536
|
+
? 'off'
|
|
1537
|
+
: v === 'lead'
|
|
1538
|
+
? 'the lead'
|
|
1539
|
+
: v === 'person'
|
|
1540
|
+
? 'you'
|
|
1541
|
+
: String(v);
|
|
1520
1542
|
|
|
1521
1543
|
/**
|
|
1522
1544
|
* The Settings view: each setting from `code-kit settings --json` with a control, and, once one is
|
|
@@ -1524,7 +1546,8 @@ const shown = (v) =>
|
|
|
1524
1546
|
*/
|
|
1525
1547
|
export function settingsView(rows, pending, { Box, Text, Select, Input, Button }, handlers) {
|
|
1526
1548
|
const control = (r) => {
|
|
1527
|
-
const listed =
|
|
1549
|
+
const listed =
|
|
1550
|
+
r.choices ?? CHOICES[r.key] ?? (typeof r.default === 'boolean' ? [true, false] : null);
|
|
1528
1551
|
// A value the list doesn't name (a model given by its id) is still shown, as the one chosen.
|
|
1529
1552
|
const choices =
|
|
1530
1553
|
listed && !listed.map(String).includes(String(r.value ?? '')) ? [...listed, r.value] : listed;
|
|
@@ -1544,7 +1567,7 @@ export function settingsView(rows, pending, { Box, Text, Select, Input, Button }
|
|
|
1544
1567
|
onSubmit: (v) => handlers.onChoose(r.key, v.trim()),
|
|
1545
1568
|
});
|
|
1546
1569
|
};
|
|
1547
|
-
const
|
|
1570
|
+
const rowView = (r) =>
|
|
1548
1571
|
Box({
|
|
1549
1572
|
key: `setting-${r.key}`,
|
|
1550
1573
|
flexDirection: 'column',
|
|
@@ -1553,7 +1576,15 @@ export function settingsView(rows, pending, { Box, Text, Select, Input, Button }
|
|
|
1553
1576
|
flexDirection: 'row',
|
|
1554
1577
|
columnGap: 2,
|
|
1555
1578
|
children: [
|
|
1556
|
-
Box({
|
|
1579
|
+
Box({
|
|
1580
|
+
width: 34,
|
|
1581
|
+
children: [
|
|
1582
|
+
Text({
|
|
1583
|
+
bold: true,
|
|
1584
|
+
children: [r.section === 'Approvals' ? r.key.replace(/^approvals\./, '') : r.key],
|
|
1585
|
+
}),
|
|
1586
|
+
],
|
|
1587
|
+
}),
|
|
1557
1588
|
control(r),
|
|
1558
1589
|
...(r.value !== r.default
|
|
1559
1590
|
? [Text({ dimColor: true, children: [`default ${shown(r.default)}`] })]
|
|
@@ -1570,8 +1601,26 @@ export function settingsView(rows, pending, { Box, Text, Select, Input, Button }
|
|
|
1570
1601
|
}),
|
|
1571
1602
|
Text({ dimColor: true, children: [` ${r.about}`] }),
|
|
1572
1603
|
],
|
|
1573
|
-
})
|
|
1574
|
-
|
|
1604
|
+
});
|
|
1605
|
+
// WHO-3: the harness, then who decides each approval.
|
|
1606
|
+
const section = (name) => rows.filter((r) => (r.section ?? 'Harness') === name);
|
|
1607
|
+
const approvals = section('Approvals');
|
|
1608
|
+
const rowsView = [
|
|
1609
|
+
...section('Harness').map(rowView),
|
|
1610
|
+
...(approvals.length
|
|
1611
|
+
? [
|
|
1612
|
+
Text({ key: 'approvals-heading', bold: true, color: 'cyan', children: ['APPROVALS'] }),
|
|
1613
|
+
Text({
|
|
1614
|
+
key: 'approvals-about',
|
|
1615
|
+
dimColor: true,
|
|
1616
|
+
children: [
|
|
1617
|
+
"Who decides what a lane can't do on its own. The lead decides what's technical; you keep product, design, CI and the kit.",
|
|
1618
|
+
],
|
|
1619
|
+
}),
|
|
1620
|
+
...approvals.map(rowView),
|
|
1621
|
+
]
|
|
1622
|
+
: []),
|
|
1623
|
+
];
|
|
1575
1624
|
const confirm = pending
|
|
1576
1625
|
? [
|
|
1577
1626
|
Box({
|
|
@@ -1627,7 +1676,9 @@ export function settingsView(rows, pending, { Box, Text, Select, Input, Button }
|
|
|
1627
1676
|
Box({ flexDirection: 'column', children: rowsView }),
|
|
1628
1677
|
Text({
|
|
1629
1678
|
dimColor: true,
|
|
1630
|
-
children: [
|
|
1679
|
+
children: [
|
|
1680
|
+
'Lanes, paths, layers and which paths are protected change through /code-kit:init.',
|
|
1681
|
+
],
|
|
1631
1682
|
}),
|
|
1632
1683
|
],
|
|
1633
1684
|
});
|
|
@@ -10,6 +10,7 @@ import { block, start } from './lib/hook.mjs';
|
|
|
10
10
|
import { brokenLayerRule, layerProblems } from './lib/layers.mjs';
|
|
11
11
|
import { noteFor } from './lib/standards.mjs';
|
|
12
12
|
import { projectWorktrees, relativeTo, worktreeRoot } from './lib/rules.mjs';
|
|
13
|
+
import { noteWrite } from './lib/sessions.mjs';
|
|
13
14
|
|
|
14
15
|
const { input, project, config, error } = start();
|
|
15
16
|
const target = input.tool_input?.file_path;
|
|
@@ -20,6 +21,8 @@ const root = found && projectWorktrees(project).includes(found) ? found : null;
|
|
|
20
21
|
if (!root) process.exit(0); // another repository, or Claude's memory: not this project's code
|
|
21
22
|
const rel = relativeTo(root, target);
|
|
22
23
|
if (!existsSync(join(root, rel))) process.exit(0);
|
|
24
|
+
// SESS-5: in the checkout every session shares, which session wrote the file, for a blocked git to name.
|
|
25
|
+
if (root === projectWorktrees(project)[0]) noteWrite(project, input.session_id, rel);
|
|
23
26
|
|
|
24
27
|
// Violations recorded in the baseline (brownfield code as it was adopted) don't block; new ones do.
|
|
25
28
|
const layers = layerProblems(rel, readFileSync(join(root, rel), 'utf8'), config);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SessionStart and SessionEnd (docs/specs/23-the-team.md): a session starting is recorded as a member
|
|
3
|
+
// of the team, the lead session when no other live session leads (SESS-2); as it ends, it leads nothing
|
|
4
|
+
// more, and the lanes its agents had claimed are free again (SCHED-1). Its task stays, stale (SESS-4).
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { loadConfig } from './lib/config.mjs';
|
|
7
|
+
import { releaseSession } from './lib/laneclaims.mjs';
|
|
8
|
+
import { endSession, startSession } from './lib/sessions.mjs';
|
|
9
|
+
import { projectDir } from './lib/rules.mjs';
|
|
10
|
+
|
|
11
|
+
let input = {};
|
|
12
|
+
try {
|
|
13
|
+
input = JSON.parse(readFileSync(0, 'utf8') || '{}');
|
|
14
|
+
} catch {
|
|
15
|
+
// no input: nothing to do
|
|
16
|
+
}
|
|
17
|
+
const project = projectDir();
|
|
18
|
+
const { config } = loadConfig(project);
|
|
19
|
+
const session = input.session_id ?? null;
|
|
20
|
+
if (config && session && process.argv[2] === 'start') startSession(project, session);
|
|
21
|
+
if (config && session && process.argv[2] === 'end') {
|
|
22
|
+
endSession(project, session);
|
|
23
|
+
releaseSession(project, session);
|
|
24
|
+
}
|
package/hooks/stop-check.mjs
CHANGED
|
@@ -11,10 +11,22 @@ import { brokenLayerRule, layerProblems } from './lib/layers.mjs';
|
|
|
11
11
|
import { noteFor } from './lib/standards.mjs';
|
|
12
12
|
import { actorFor, actorRoot, currentBranch, reportPath } from './lib/rules.mjs';
|
|
13
13
|
import { storyOfBranch } from './lib/teamfacts.mjs';
|
|
14
|
+
import { releaseLane } from './lib/laneclaims.mjs';
|
|
14
15
|
import { auditProblems, changedFiles } from './lane-audit.mjs';
|
|
15
16
|
|
|
16
17
|
const { input, project, config, error } = start();
|
|
17
18
|
|
|
19
|
+
// SCHED-1: a lane agent whose finish goes through frees its lane for the next story. A refused finish
|
|
20
|
+
// (exit 2) keeps it: the agent carries on.
|
|
21
|
+
const finishingLane =
|
|
22
|
+
input.agent_id && config
|
|
23
|
+
? Object.entries(config.lanes).find(([, l]) => l.agent === input.agent_type)?.[0]
|
|
24
|
+
: null;
|
|
25
|
+
if (finishingLane)
|
|
26
|
+
process.on('exit', (code) => {
|
|
27
|
+
if (code === 0) releaseLane(project, finishingLane, { session: input.session_id ?? null });
|
|
28
|
+
});
|
|
29
|
+
|
|
18
30
|
// Stop hooks share one `stop_hook_active` flag, so another plugin's block would make this check skip
|
|
19
31
|
// itself. Instead this hook remembers its own last block per session and agent. The same problem
|
|
20
32
|
// reported MAX_REPEATS times in a row means no progress: say so and let the finish through rather than
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warren-dean/code-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "Keeps AI coding agents inside your project's rules: lanes, layer rules, protected paths and proof before finish, enforced by Claude Code hooks and in CI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Warren Langeveldt",
|
package/skills/decide/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: The lead decides what a lane asked for and couldn't do itself (a ne
|
|
|
5
5
|
|
|
6
6
|
# Decide a lane's request
|
|
7
7
|
|
|
8
|
-
The person sets the guardrails: the specs, the
|
|
8
|
+
The person is the product owner and sets the guardrails: the specs, the screens' visual design, CI and the pipelines, the kit (the rules in `.claude/code-kit.json`), and product questions the spec doesn't answer, plus anything with production, cost or strategy implications. Everything technical is yours by default: contracts, migrations, packages, merges into the base, and a weakened test on a lane's branch once you've reviewed it (`code-kit approve tests --lane <lane> --delegated --story ST-n --reason "…"`); on your own branch a weakened test is the person's. Inside them, you run the team. A lane that needs something it can't do itself asks you, and you decide on the merits. Passing a request to the person because you could is not deciding: they'd approve it by default, and the check would be gone.
|
|
9
9
|
|
|
10
10
|
The CLI is `node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs"`.
|
|
11
11
|
|
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dispatch
|
|
3
|
-
description: Hand stories from the plan to their lane agents,
|
|
3
|
+
description: Hand stories from the plan to their lane agents, one per lane, every free lane at once. Picks the ready stories, checks each is buildable, briefs each lane agent with its story, requirements and acceptance criteria, and starts it on its own branch and worktree. Use when the lead is ready to start building, asks what to work on next, or runs /code-kit:dispatch [story IDs, a milestone, or "next"].
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Dispatch stories to lanes
|
|
@@ -34,7 +34,7 @@ Then check:
|
|
|
34
34
|
- **No open question blocks it.** If a spec's open questions touch a cited requirement, don't dispatch the story; list the question for the person.
|
|
35
35
|
- **The contracts exist.** If the story needs an API schema, event or database contract that isn't written yet, that's the lead's work first. Write it (the lead owns contracts), commit it on the lead's branch, then dispatch. A lane never invents a contract.
|
|
36
36
|
- **The lane is right.** Run `who` on the paths the story will touch. If they belong to two lanes, split the story or sequence it; don't hand one lane another's files.
|
|
37
|
-
- **
|
|
37
|
+
- **One story per lane.** A lane runs one story at a time across the whole project, every session included: two agents in one lane build on the same files in their worktrees and conflict at merge. Different lanes run in parallel; that's what the plan's dependencies are for. `code-kit next` offers at most one story for each free lane, the open slice's first (`**Slice:**`), and starting a second agent for a lane that's running one is refused, naming the story and the session that has it. `code-kit lanes` lists what each lane is running. If a lane's agent stopped without finishing (it was stopped, or its session ended), `code-kit lanes release <lane>` frees the lane for its next story.
|
|
38
38
|
|
|
39
39
|
Commit everything the lanes need first. Each lane branches from the lead's branch as committed, so uncommitted work isn't there.
|
|
40
40
|
|
package/skills/init/SKILL.md
CHANGED
|
@@ -74,7 +74,7 @@ Write `.claude/code-kit.draft.json`. Follow `${CLAUDE_PLUGIN_ROOT}/README.md`
|
|
|
74
74
|
- **`postEdit`:** the project's existing per-file linters and formatters, with `{file}`.
|
|
75
75
|
- **`checks`:** the typecheck, test, architecture and database commands, each scoped by `files`. Only include commands that pass on the current code, or ones the person chose to keep.
|
|
76
76
|
- **`harness.autonomy`:** ask the person how hands-off they want the build. With the code-kit mod (Claude Code 2.1.287 or later), once the plan has stories, a lead loop can keep the work moving between their prompts: it prompts the lead to dispatch ready stories, review finished ones and merge them where merges are delegated. Offer the three choices in plain words: `autonomous` (it keeps going on its own; they can pause it from the band), `propose` (each step waits in the band for their Go), or `off`. Put their answer in the draft as `"harness": { "autonomy": "…" }`. If they'd rather decide later, leave it out: the loop then asks them in the band before its first step. With `autonomous`, the lead owns delivery, so also draft `"merge": true` in `approvals.delegate`: the lead merges a reviewed branch once every check passes.
|
|
77
|
-
- **`approvals.delegate`, who decides what (HELM-1):** the person
|
|
77
|
+
- **`approvals.delegate`, who decides what (HELM-1, WHO-1):** the person is the product owner; the lead decides everything technical, so lanes ask the lead, never the person. With no `approvals.delegate`, that's the default: the lead approves new packages (only when the story's requirements need them and they fit the spec, the principles and the architecture; npm packages must also pass the registry limits on licence, popularity and age), every protected change except the specs (`spec`), the screens' designs (`design`), CI (`ci`) and the kit, merges into the base, and weakened tests on a lane's branch after its review. Show the person the split in two lines, what they keep and what the lead decides, and ask if they want to keep anything else. Only if they do, draft `approvals.delegate` with exactly what the lead keeps (`{ "protected": [...], "merge": true, "tests": true, "dependencies": {} }`, leaving out what they keep), or `false` to keep everything. They can change it later in the Settings tab's Approvals section.
|
|
78
78
|
- **`harness.agents.lanes.model`:** ask which model the lane agents run on. Each lane agent builds one story inside its own paths, with its requirements and acceptance criteria in its brief, and the hooks, `verify` and the lead's review catch what it gets wrong, so Sonnet usually does the work well for much less than the session's model; recommend `sonnet`, and say the lead, the reviewer and the security reviewer keep the session's model. The mod sets it as each lane agent starts; a lane's agent file with its own `model:` keeps that. Put their answer in `harness` as `"agents": { "lanes": { "model": "sonnet" } }`, or leave it out for the session's model.
|
|
79
79
|
- **`harness.report.on`:** ask whether agents may draft bug reports for code-kit itself when code-kit gets in their way (a refusal that shouldn't have happened, a crash). Each report holds only code-kit's own facts (its version, the rule, what it said, the command), never the project's code, paths or data, and waits in their Decisions tab: they read it and send it to code-kit's public GitHub under their own account, or discard it. Off unless they say yes; put `"report": { "on": true }` in `harness` if they do.
|
|
80
80
|
- **`docs.standards`:** `docs/standards.md` unless the project keeps its standards elsewhere (see Standards, below).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lead
|
|
3
|
+
description: Make this Claude Code session the project's lead session, where code-kit's loop runs and the plan's stories are dispatched from, when several sessions work one project. Says which session leads now and what moving the lead does, then gives the person the command that moves it. Use when the person says this session should lead, asks which session leads, or runs /code-kit:lead.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lead from this session
|
|
7
|
+
|
|
8
|
+
Several Claude Code sessions can work one project at once. One is the lead session: code-kit's loop runs there, and the plan's stories are dispatched from it. The others are working sessions: they stay quiet (no loop prompts, no other session's decisions), and each records its ad hoc task, with the paths it leases, before it changes code.
|
|
9
|
+
|
|
10
|
+
The CLI is `node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs"`, run from the project root.
|
|
11
|
+
|
|
12
|
+
1. See who leads now:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" who
|
|
16
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" sessions
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
2. If this session already leads, say so and stop.
|
|
20
|
+
3. Otherwise, tell the person what moving the lead does: this session starts running the loop and dispatching the plan's stories, and the session that led becomes a working session, which stops prompting its lead. Choosing the lead is theirs, so give them the command to run themselves (the hooks refuse it from an agent), or point them to **Lead from here** in the code-kit band:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
! node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" sessions lead
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
4. Once they have, run `who` again and confirm this session leads.
|
|
27
|
+
|
|
28
|
+
A working session doesn't need this to work: it records its task (`code-kit task "<what>" --touches "<globs>"`), works in a worktree of its own, and `code-kit task done` when it's finished.
|
package/skills/next/SKILL.md
CHANGED
|
@@ -46,7 +46,7 @@ Run the named skill with the Skill tool: `code-kit:<step>`, with `args` (and the
|
|
|
46
46
|
- **`update`:** run `code-kit:init`. `why` names what the setup is missing (the design gate, design stories, a QA lane, a security reviewer); init adds those, shows the person the difference, and sets `"setup": 3`. Until then no new story starts; work already started goes on to review and merge.
|
|
47
47
|
- **`instructions`:** there's no skill to run. With `args` `write`, run `code-kit instructions --write`. With `merge`, some files were written before code-kit marked its part: run `code-kit instructions` to list them, replace each old part with what `code-kit instructions show lead` (CLAUDE.md) or `show lane <lane>` (that lane's agent file) prints, keep every line the project wrote itself outside the markers, then run `--write` for the rest. Commit it on a branch and merge it like your own stories.
|
|
48
48
|
- **`lead`:** there's no skill to run. Build each story yourself, on the branch `lead/st-<n>`: spec-check it first if it cites requirements, do the work, commit, then run `code-kit:review` on the branch like any other. A spike's result goes in the docs it names.
|
|
49
|
-
- **`wait`:** say which stories are being built and on which branches, then stop. When a lane agent finishes, its notification arrives in this session; run this skill again then. If a story has been in progress with no agent running (the session ended, or the agent stopped without committing), dispatch it again.
|
|
49
|
+
- **`wait`:** say which stories are being built and on which branches, and which ready stories wait for their lane (a lane runs one story at a time), then stop. When a lane agent finishes, its notification arrives in this session; run this skill again then. If a story has been in progress with no agent running (the session ended, or the agent stopped without committing), dispatch it again.
|
|
50
50
|
- **`done`:** give the `status` summary in a few lines, then offer to run spec-design for the next milestone or feature.
|
|
51
51
|
|
|
52
52
|
## 3. Keep going until the person is needed
|
|
@@ -114,7 +114,8 @@ These are what `/code-kit:init` turns into enforced rules, so be concrete.
|
|
|
114
114
|
- no lane for security: it owns no paths. init adds a read-only security reviewer that checks every story touching the threat models' sensitive parts, so security requirements are delivered by the lanes whose code they constrain;
|
|
115
115
|
- milestones, each a usable increment;
|
|
116
116
|
- stories, each small enough for one lane to finish on one branch. Each story is a `### ST-<n> Title` heading with `**Lane:**`, `**Requirements:**` (the IDs it delivers, or `none` for a spike or a foundation that delivers none), `**Depends on:**` and `**Status:** todo` lines, and for a story that fixes a bug, `**Fixes:**` with what was broken. `code-kit status` and the dispatch skill read them. A story that needs two lanes is two stories, with a dependency between them;
|
|
117
|
-
- sequencing: contracts and shared foundations first, as the lead's own stories (`**Lane:** lead`, built by the lead rather than dispatched), then work that can run in parallel;
|
|
117
|
+
- sequencing: contracts and shared foundations first, as the lead's own stories (`**Lane:** lead`, built by the lead rather than dispatched), then work that can run in parallel. A lane runs one story at a time, so parallel work is stories in different lanes;
|
|
118
|
+
- **slices**: group the stories that together make one feature someone could see working (a vertical slice across lanes, such as "Book a class") with a `**Slice:** <name>` line on each, in plan order, contract story first. A free lane builds the open slice's stories before the next slice's (`harness.slices.open`, 1 by default), so the feature in progress finishes and merges before the next one spreads across the lanes;
|
|
118
119
|
- **a test story per journey**: for each main user journey, a story in the `qa` lane citing the journey's requirements and depending on the screen and service stories it goes through, so the journey is proven end to end as soon as its parts are built;
|
|
119
120
|
- **design before code**: when there are screens, a design story for each area's screens (`**Lane:** design`, citing the requirements its screens serve), early in the plan, and every story that builds those screens depends on it. The lead designs them with the person (`/code-kit:design`), and no screen is built until its design is approved.
|
|
120
121
|
|
|
@@ -48,7 +48,7 @@ Each is optional; ask, and do only what they want:
|
|
|
48
48
|
|
|
49
49
|
- **CODEOWNERS** (TEAM-11): `team codeowners --write` writes `.github/CODEOWNERS` from the config, so GitHub asks the tech lead and lane owners to review. Then they require code owners' review in a ruleset on the protected branch.
|
|
50
50
|
- **A Project board** (TEAM-12): with a GitHub Project's owner and number, add `"board": { "owner": "<user or organisation>", "number": <n> }` to `team` in the config; the mod then mirrors the plan onto it.
|
|
51
|
-
- **A hosted team provider, only when the config already names one:** hosted providers are in preview, so never offer or suggest one. When `team.provider` is `{ "<name>": "<https address>" }` (set by hand), connect this machine: run `team provider status`; if it isn't signed in, run `team provider signin` in the background, give the person the code and the GitHub address it prints, and wait (they enter the code in their browser; never ask for a password or token); then `team provider connect`, which checks the provider can see the repository
|
|
51
|
+
- **A hosted team provider, only when the config already names one:** hosted providers are in preview, so never offer or suggest one. When `team.provider` is `{ "<name>": "<https address>" }` (set by hand), connect this machine: run `team provider status`; if it isn't signed in, run `team provider signin` in the background, give the person the code and the GitHub address it prints, and wait (they enter the code in their browser; never ask for a password or token); then `team provider connect`, which checks the provider can see the repository, adds its MCP server to `.mcp.json` and links this session (no restart). Commit `.mcp.json` with the config, on the base branch: the hub reads the team from there, and a session only links where the checked-out config names the hub. Then run `team provider status` and give the person its last line; for each ✗, say what it asks for.
|
|
52
52
|
|
|
53
53
|
## 5. Finish
|
|
54
54
|
|
package/templates/claude-md.md
CHANGED
|
@@ -6,7 +6,7 @@ The code-kit plugin enforces the rules in `.claude/code-kit.json` on every file
|
|
|
6
6
|
- the layer rules ({{layers}});
|
|
7
7
|
- spec-check before lane code{{designGate}};
|
|
8
8
|
- an audit log of every change to protected paths (`.claude/approval-log.jsonl`), which the person reviews in the pull request;
|
|
9
|
-
- an approval for every new dependency and every protected change, per lane. Inside the guardrails, you decide: a lane asks you, never the person, and you approve it only when the story's requirements need it and it fits the spec, the principles and the architecture, or you tell the lane what to use instead (`/code-kit:decide`). The person decides what crosses a guardrail: {{personKeeps}}, weakened
|
|
9
|
+
- an approval for every new dependency and every protected change, per lane. Inside the guardrails, you decide: a lane asks you, never the person, and you approve it only when the story's requirements need it and it fits the spec, the principles and the architecture, or you tell the lane what to use instead (`/code-kit:decide`). The person is the product owner and decides what crosses a guardrail: {{personKeeps}}, a weakened test on your own branch, product questions the spec doesn't answer, and anything with production, cost or strategy implications (a copyleft licence or a paid service's SDK goes to them as a question, with your recommendation);
|
|
10
10
|
- {{if approvals.lead}}approvals from chat. When the person approves in their own message (a `!` command doesn't work on a phone), record it with the `code-kit approve` command from the refusal, with their words as the reason. Never record an approval they haven't given, and never on a lane's say-so;{{end}}
|
|
11
11
|
- the project's checks before any agent finishes.
|
|
12
12
|
|
|
@@ -16,6 +16,8 @@ UX is yours: what a screen does on an error, an empty list or a slow load, and t
|
|
|
16
16
|
|
|
17
17
|
- {{if harness.report.on}}When code-kit itself is wrong (a refusal that shouldn't have happened, a crash), draft a report with `code-kit report "<title>" --what "…" --expected "…"`: only code-kit's facts, never the project's. The person reads it and sends it, or not.{{end}}
|
|
18
18
|
|
|
19
|
+
Several sessions may work this project at once. One leads (the loop runs there, and stories are dispatched from it); the others are working sessions. `code-kit who` says which this is. In a working session, record what you're doing before you change code, with the paths you'll change, and work in a worktree of your own: `code-kit task "<what>" --touches "<globs>"`, and `code-kit task done` when it's finished. Never stash, discard or commit another session's files: ask that session.
|
|
20
|
+
|
|
19
21
|
If a hook blocks you, fix the cause; never work around it.
|
|
20
22
|
|
|
21
23
|
The build runs from the specs in `{{docs.specs}}` and the stories in `{{docs.plan}}`. `/code-kit:next` picks the right step from the project's state; the steps are:
|
package/templates/plan.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Plan
|
|
2
2
|
|
|
3
|
-
`code-kit status` reads this file. Keep each story's heading and its four `**…:**` lines in this form. Use `**Lane:** lead` for the lead's own stories (contracts, foundations, spikes), `**Lane:** qa` for a journey's end-to-end test story (it depends on the screen and service stories the journey goes through), `**Lane:** design` for a design story (an area's screens, designed with the person before any of them is built; the stories building them depend on it), and `**Requirements:** none` for a story that delivers no requirement. A story that fixes a bug adds a fifth line, `**Fixes:** <what was broken, and where it was seen>`: `code-kit verify` then requires a test that fails without the fix and passes with it.
|
|
3
|
+
`code-kit status` reads this file. Keep each story's heading and its four `**…:**` lines in this form. Use `**Lane:** lead` for the lead's own stories (contracts, foundations, spikes), `**Lane:** qa` for a journey's end-to-end test story (it depends on the screen and service stories the journey goes through), `**Lane:** design` for a design story (an area's screens, designed with the person before any of them is built; the stories building them depend on it), and `**Requirements:** none` for a story that delivers no requirement. A story that fixes a bug adds a fifth line, `**Fixes:** <what was broken, and where it was seen>`: `code-kit verify` then requires a test that fails without the fix and passes with it. A story that belongs to a slice (a feature that can be shown working, built across lanes) adds `**Slice:** <name>`: a lane takes the open slice's stories first.
|
|
4
4
|
|
|
5
5
|
## Lanes
|
|
6
6
|
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
**Lane:** {{lane}}
|
|
18
18
|
**Requirements:** {{AREA-1, AREA-2}}
|
|
19
19
|
**Depends on:** {{ST-n, or none}}
|
|
20
|
+
**Slice:** {{the feature it's part of, or leave the line out}}
|
|
20
21
|
**Status:** todo
|
|
21
22
|
|
|
22
23
|
{{What gets built, in two or three sentences. The acceptance criteria are the cited requirements'.}}
|