@warren-dean/code-kit 0.12.2 → 0.14.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.
@@ -663,6 +663,8 @@ export function askWords(request, files = [request?.what]) {
663
663
  }
664
664
 
665
665
  export function approvalLabel(name) {
666
+ // REG-2: the person's approval to weaken a test.
667
+ if (name === 'tests') return 'a weakened test';
666
668
  return name.startsWith('dep-') ? name.slice(4).replaceAll('+', '/') : name;
667
669
  }
668
670
 
@@ -786,6 +788,7 @@ export function bandLines({
786
788
  lines.push({
787
789
  kind: 'proposal',
788
790
  text: `code-kit can keep the build moving between your prompts. Next: ${loop.proposal.label}.`,
791
+ proposal: loop.proposal,
789
792
  actions: [
790
793
  { id: 'keepGoing', label: 'Keep going' },
791
794
  { id: 'askEach', label: 'Ask each time' },
@@ -799,8 +802,12 @@ export function bandLines({
799
802
  text: loop.proposal.repeated
800
803
  ? `${loop.proposal.label}: still next after ${loop.proposal.repeated} prompts. Ask the lead again?`
801
804
  : `${loop.proposal.label}?`,
802
- actions: [{ id: 'go', label: 'Go' }],
805
+ proposal: loop.proposal,
806
+ actions: [{ id: 'go', label: loop.proposal.repeated ? 'Ask again' : 'Go' }],
803
807
  });
808
+ // LOOP-11, LOOP-12: what the loop read in the lead's reply holds it until the person says.
809
+ const waitingLine = waitingOf(loop?.waiting);
810
+ if (waitingLine) lines.push(waitingLine);
804
811
  // A held call's request is asked on its own line above, not again here.
805
812
  // CARD-2: counted as decisions, not files: one approval for a lane covers every file it was asked for.
806
813
  const asks = approvalAsks((requests?.open ?? []).filter((r) => !held.some((h) => sameAsk(h, r))));
@@ -913,7 +920,14 @@ export function bandLines({
913
920
  // With the mission rows drawn, the loop's state and its Pause or Resume are on the `now` row.
914
921
  else if (mission.length) {
915
922
  // nothing more
916
- } else if (loop?.state === 'paused')
923
+ } else if (loop?.state === 'elsewhere')
924
+ // LOOP-10: another session in this project runs the loop; this one never prompts its lead.
925
+ lines.push({
926
+ kind: 'loop',
927
+ text: 'the loop runs in another session',
928
+ actions: [{ id: 'leadHere', label: 'Lead from here' }],
929
+ });
930
+ else if (loop?.state === 'paused')
917
931
  lines.push({ kind: 'loop', text: 'loop paused', actions: [{ id: 'resume', label: 'Resume' }] });
918
932
  else if (loop?.state === 'running' && loop.started)
919
933
  lines.push({
@@ -928,6 +942,48 @@ export function bandLines({
928
942
  return lines;
929
943
  }
930
944
 
945
+ /**
946
+ * LOOP-11, LOOP-12: the band's line for what the loop waits on, or null: the lead waiting on the
947
+ * person (its question, when the reply asked one), a turn the person stopped, a step the lead didn't
948
+ * take, or design stories left to the person and the lead.
949
+ */
950
+ export function waitingOf(waiting) {
951
+ if (!waiting) return null;
952
+ if (waiting.kind === 'person')
953
+ return {
954
+ kind: 'waiting',
955
+ text: waiting.question
956
+ ? `The lead waits on you: ${waiting.question}`
957
+ : `The lead waits on you${waiting.reason ? `: ${waiting.reason}` : ''}`,
958
+ waiting,
959
+ actions: waiting.question
960
+ ? [{ id: 'openDecision', label: 'Answer…' }]
961
+ : [{ id: 'carryOn', label: 'Carry on' }],
962
+ };
963
+ if (waiting.kind === 'stopped')
964
+ return {
965
+ kind: 'waiting',
966
+ text: "You stopped the lead's turn: the loop waits for you",
967
+ waiting,
968
+ actions: [{ id: 'carryOn', label: 'Carry on' }],
969
+ };
970
+ if (waiting.kind === 'blocked')
971
+ return {
972
+ kind: 'waiting',
973
+ text: `${waiting.label}: the lead didn't take it${waiting.reason ? `: ${waiting.reason}` : ''}`,
974
+ waiting,
975
+ actions: [{ id: 'askAgain', label: 'Ask again' }],
976
+ };
977
+ if (waiting.kind === 'design')
978
+ return {
979
+ kind: 'designing',
980
+ text: `${waiting.label}: with you and the lead, so the loop doesn't prompt it again`,
981
+ waiting,
982
+ actions: [{ id: 'askAgain', label: 'Ask the lead' }],
983
+ };
984
+ return null;
985
+ }
986
+
931
987
  /** The band, drawn with the elements `$.ui.resolve(e)` returned; `onAction(id, line)` acts. */
932
988
  export function band(lines, { Box, Text, Button }, onAction) {
933
989
  const COLOR = {
@@ -936,6 +992,8 @@ export function band(lines, { Box, Text, Button }, onAction) {
936
992
  queue: 'blue',
937
993
  stalled: 'red',
938
994
  proposal: 'cyan',
995
+ waiting: 'cyan',
996
+ designing: 'blue',
939
997
  loop: 'green',
940
998
  done: 'green',
941
999
  approvals: 'yellow',
@@ -1073,29 +1131,12 @@ export function approvePane(
1073
1131
  ],
1074
1132
  }),
1075
1133
  // ACT-5: an aside about this approval, answered here and never in the lead's conversation.
1076
- ...aside.thread.flatMap((turn, i) => [
1077
- Text({ key: `aside-q-${i}`, color: 'cyan', children: [`You: ${turn.question}`] }),
1078
- turn.pending
1079
- ? Text({ key: `aside-a-${i}`, dimColor: true, children: ['Asking…'] })
1080
- : turn.error
1081
- ? Text({ key: `aside-a-${i}`, color: 'red', children: [turn.error] })
1082
- : Markdown
1083
- ? Markdown({ key: `aside-a-${i}`, text: turn.answer })
1084
- : Text({ key: `aside-a-${i}`, children: [turn.answer] }),
1085
- ]),
1086
- ...(onAsk
1087
- ? [
1088
- Input({
1089
- key: 'approve-ask',
1090
- label: 'Ask about it',
1091
- value: aside.draft,
1092
- placeholder: 'What is it for, and what does approving it let happen?',
1093
- submitLabel: 'Ask',
1094
- onInput: onAskInput,
1095
- onSubmit: onAsk,
1096
- }),
1097
- ]
1098
- : []),
1134
+ ...asideNodes(
1135
+ aside,
1136
+ { Text, Input, Markdown },
1137
+ { onAskInput, onAsk },
1138
+ 'What is it for, and what does approving it let happen?',
1139
+ ),
1099
1140
  Input({
1100
1141
  key: 'approve-reason',
1101
1142
  label: 'Reason',
@@ -1118,6 +1159,73 @@ export function approvePane(
1118
1159
  });
1119
1160
  }
1120
1161
 
1162
+ /**
1163
+ * ACT-5: an aside in a pane: each question asked and its answer (or that it's being asked, or why there
1164
+ * is none), then the box to ask the next.
1165
+ */
1166
+ function asideNodes(aside, { Text, Input, Markdown }, { onAskInput, onAsk }, placeholder) {
1167
+ return [
1168
+ ...aside.thread.flatMap((turn, i) => [
1169
+ Text({ key: `aside-q-${i}`, color: 'cyan', children: [`You: ${turn.question}`] }),
1170
+ turn.pending
1171
+ ? Text({ key: `aside-a-${i}`, dimColor: true, children: ['Asking…'] })
1172
+ : turn.error
1173
+ ? Text({ key: `aside-a-${i}`, color: 'red', children: [turn.error] })
1174
+ : Markdown
1175
+ ? Markdown({ key: `aside-a-${i}`, text: turn.answer })
1176
+ : Text({ key: `aside-a-${i}`, children: [turn.answer] }),
1177
+ ]),
1178
+ ...(onAsk
1179
+ ? [
1180
+ Input({
1181
+ key: 'ask-about',
1182
+ label: 'Ask about it',
1183
+ value: aside.draft,
1184
+ placeholder,
1185
+ submitLabel: 'Ask',
1186
+ onInput: onAskInput,
1187
+ onSubmit: onAsk,
1188
+ }),
1189
+ ]
1190
+ : []),
1191
+ ];
1192
+ }
1193
+
1194
+ /**
1195
+ * ASK-5: the side call's prompt for a question about one of the person's decisions (a question from the
1196
+ * lead, or one open in their brief): what code-kit knows of it, the aside so far, and their question.
1197
+ */
1198
+ export function questionAsidePrompt({ q, thread = [], question }) {
1199
+ return [
1200
+ "The person is deciding how to answer a question in code-kit's Decisions pane, and asks you about it first. Answer in a few short sentences, in plain words: what the question means, why it matters for the build, what the choices are and what each leads to, and what you'd recommend if you know enough to. Use what you know from this conversation, the project's docs you've read, and the facts below. If you don't know, say so; don't guess. This answer goes to that pane, not into the conversation: don't act on it.",
1201
+ '',
1202
+ q.kind === 'brief'
1203
+ ? `An open question in ${q.path}:`
1204
+ : q.kind === 'lead'
1205
+ ? 'What the lead asked in its last reply, and waits on:'
1206
+ : `A question the lead asked (${q.id}):`,
1207
+ `- ${q.question}`,
1208
+ ...(q.owner ? [`- who decides: ${q.owner}`] : []),
1209
+ ...(q.kind === 'brief' && q.blocks ? [`- what it holds up: ${q.blocks}`] : []),
1210
+ ...(q.story ? [`- story: ${q.story}`] : []),
1211
+ ...(q.why ? [`- why the lead can't settle it: ${q.why}`] : []),
1212
+ ...(q.options?.length
1213
+ ? [
1214
+ `- the lead's options: ${q.options.map((o) => `${o.label}${o.effect ? ` (${o.effect})` : ''}`).join('; ')}`,
1215
+ ]
1216
+ : []),
1217
+ ...(thread.length
1218
+ ? [
1219
+ '',
1220
+ 'Asked so far:',
1221
+ ...thread.flatMap((t) => [`Q: ${t.question}`, `A: ${t.answer ?? t.error ?? ''}`]),
1222
+ ]
1223
+ : []),
1224
+ '',
1225
+ `Question: ${question}`,
1226
+ ].join('\n');
1227
+ }
1228
+
1121
1229
  /**
1122
1230
  * ACT-5: the side call's prompt for a question about an approval: what code-kit knows of the request (the
1123
1231
  * names, the lane, what was refused and why, its story, the limits it missed), the aside so far, and the
@@ -1161,10 +1269,11 @@ export const ANSWER_ID = 'code-kit-answer';
1161
1269
  /** ASK-2, ASK-4: the person's answer to the lead's question, or to one in their brief, typed. */
1162
1270
  export function answerPane(
1163
1271
  answering,
1164
- { Box, Text, Input, Button },
1165
- { onInput, onSubmit, onCancel },
1272
+ { Box, Text, Input, Button, Markdown },
1273
+ { onInput, onSubmit, onCancel, onAskInput, onAsk },
1166
1274
  ) {
1167
1275
  const { kind, question, text, error } = answering;
1276
+ const aside = answering.aside ?? { thread: [], draft: '' };
1168
1277
  return Box({
1169
1278
  flexDirection: 'column',
1170
1279
  rowGap: 1,
@@ -1175,9 +1284,33 @@ export function answerPane(
1175
1284
  children: [
1176
1285
  kind === 'brief'
1177
1286
  ? `An open question in ${answering.path}. The lead records your answer in the brief's decisions and builds on it.`
1178
- : `${answering.id}, from the lead${answering.story ? `, for ${answering.story}` : ''}. Your answer is recorded and sent to the lead.`,
1287
+ : kind === 'lead'
1288
+ ? 'From the lead, which waits on it. Your answer is sent to the lead.'
1289
+ : `${answering.id}, from the lead${answering.story ? `, for ${answering.story}` : ''}. Your answer is recorded and sent to the lead.`,
1179
1290
  ],
1180
1291
  }),
1292
+ ...(kind === 'brief' && (answering.owner || answering.blocks)
1293
+ ? [
1294
+ Text({
1295
+ dimColor: true,
1296
+ children: [
1297
+ [
1298
+ answering.owner && `Who decides: ${answering.owner}`,
1299
+ answering.blocks && `Holds up: ${answering.blocks}`,
1300
+ ]
1301
+ .filter(Boolean)
1302
+ .join(' · '),
1303
+ ],
1304
+ }),
1305
+ ]
1306
+ : []),
1307
+ // ASK-5: not sure what it means? Ask, here, before answering.
1308
+ ...asideNodes(
1309
+ aside,
1310
+ { Text, Input, Markdown },
1311
+ { onAskInput, onAsk },
1312
+ 'What does it mean, and what would you suggest?',
1313
+ ),
1181
1314
  Input({
1182
1315
  key: 'answer-text',
1183
1316
  label: 'Answer',
@@ -82,7 +82,7 @@ const layers = changed
82
82
  if (layers.length)
83
83
  fail('Imports break the layer rules (.claude/code-kit.json › layers):', layers.join('\n'));
84
84
 
85
- const failure = runChecks(config, changed, root, actor);
85
+ const { failure } = runChecks(config, changed, root, actor);
86
86
  if (failure) fail(failure.title, failure.output);
87
87
 
88
88
  // A lane hands back committed work on its own branch; the lead reviews and merges it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warren-dean/code-kit",
3
- "version": "0.12.2",
3
+ "version": "0.14.0",
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",
@@ -61,6 +61,7 @@ Requirements (read each one in full, with its acceptance criteria):
61
61
  - …
62
62
 
63
63
  What to build: <the story's description, and anything the lead decided that the spec doesn't say>
64
+ Fixes: <for a fix story, what was broken and where it was seen; write the test that fails because of it first>
64
65
  Contracts to use: <paths of the schemas, events or tables this story relies on>
65
66
  Out of scope: <what neighbouring stories or other lanes will do>
66
67
 
@@ -29,6 +29,8 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" verify --base <base>
29
29
  - new layer violations, and a baseline that grew;
30
30
  - screens without an approved design;
31
31
  - committed secrets;
32
+ - tests weakened (deleted, skipped or focused, cut down, or a snapshot rewritten) without the person's `tests` approval in the log;
33
+ - for a fix (its story has `**Fixes:**`, or the branch is `<lane>/fix-…`), that its tests fail without the fix, as the bug, and pass with it;
32
34
  - the project's checks for the changed files.
33
35
 
34
36
  Any problem it reports means the branch goes back. Don't fix a lane's code yourself; the lead doesn't write lane paths.
@@ -73,7 +75,7 @@ Report the table, `verify`'s result, the security review's findings (or that non
73
75
  node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" queue add <branch>
74
76
  ```
75
77
 
76
- Branches merge from the queue one at a time, in the order their reviews passed, each verified against the base as it is then. Where `approvals.delegate.merge` is on and the harness's autonomy is `autonomous`, you merge the head: `code-kit queue merge --delegated` (code-kit's loop asks you when it's its turn). Otherwise the person merges it, with Merge in the code-kit band or `code-kit queue merge --person`. A head that no longer merges cleanly, or fails verify against the moved base, is sent back to its lane on its own; dispatch the fix as for any send-back. When the branch is merged, remove its worktree.
78
+ Branches merge from the queue one at a time, in the order their reviews passed, each verified against the base as it is then. Where `approvals.delegate.merge` is on and the harness's autonomy is `autonomous`, you merge the head: `code-kit queue merge --delegated` (code-kit's loop asks you when it's its turn). Otherwise the person merges it, with Merge in the code-kit band or `code-kit queue merge --person`. The queue records the commit your review passed and merges exactly that commit. A branch that moves on after your review leaves the queue: review it again, then queue it again. A head that no longer merges cleanly, or fails verify against the moved base, is sent back to its lane on its own; dispatch the fix as for any send-back. When the branch is merged, remove its worktree.
77
79
 
78
80
  - **Send back.** List each problem, where it is, and what "fixed" looks like. Record the send-back, so `status` and `next` read the branch as waiting for its fix rather than finished:
79
81
 
@@ -111,7 +111,7 @@ These are what `/code-kit:init` turns into enforced rules, so be concrete.
111
111
  - a **QA lane** (`qa`, agent `test-engineer`) whenever the product has screens or journeys that cross lanes: it owns the end-to-end suites (for example `e2e/**` or `apps/*/e2e/**`), in the runner that suits each surface (Playwright for web, Maestro or Detox for mobile), and its command runs them. Nobody else owns these tests, so without it nothing proves a journey works across the lanes;
112
112
  - 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;
113
113
  - milestones, each a usable increment;
114
- - 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. `code-kit status` and the dispatch skill read them. A story that needs two lanes is two stories, with a dependency between them;
114
+ - 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;
115
115
  - 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;
116
116
  - **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;
117
117
  - **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.
@@ -26,5 +26,7 @@ Always:
26
26
  - A new dependency needs a person's approval for your lane. If the install is refused, put the package, why it's needed and the approval command from the refusal in a change request for the lead, and stop.
27
27
  - Build every feature end to end. No mocks, stubs, placeholder screens or "coming soon".
28
28
  - Prove each acceptance criterion with a test whose title names the requirement ID, e.g. `BOOK-4 …`.
29
+ - Fixing a bug: first write a test that fails because of the bug, through the code that's already there, then fix it. `verify` runs your test without the fix, and it must fail on an assertion, not because it calls something the fix adds.
30
+ - Never weaken a test to get to green: no deleting, skipping or focusing tests, cutting cases or assertions, or rewriting snapshots. The commit is refused without the person's approval. Fix the code; if the test itself is wrong, say why in a change request for the lead.
29
31
  - Commit your work on your branch before finishing; the stop hook runs the checks and requires it.
30
32
  - Finish with: what you did, the spec section, the commands you ran with their output, open questions.
@@ -1,6 +1,6 @@
1
1
  ## Enforced, not advised
2
2
 
3
- The code-kit plugin enforces the rules in `.claude/code-kit.json` on every tool call:
3
+ The code-kit plugin enforces the rules in `.claude/code-kit.json` on every file edit, shell command and finish:
4
4
 
5
5
  - who writes where (the lead delegates lane work to the lane agents in `.claude/agents/`);
6
6
  - the layer rules ({{layer names, in order}});
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.
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.
4
4
 
5
5
  ## Lanes
6
6