@warren-dean/code-kit 0.11.0 → 0.11.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 +9 -0
- package/README.md +3 -3
- package/bin/code-kit.mjs +9 -1
- package/hooks/lib/rules.mjs +214 -4
- package/hooks/mod/decisions.mjs +2 -1
- package/hooks/mod/loop.mjs +49 -0
- package/hooks/mod/register.mjs +18 -2
- package/hooks/mod/register.test.ts +28 -0
- package/hooks/mod/view.mjs +45 -9
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +1 -1
- package/skills/review/SKILL.md +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"name": "code-kit",
|
|
10
10
|
"source": "./",
|
|
11
11
|
"description": "Keeps AI coding agents inside your project's rules, from idea to merged code: spec design, lanes, layer rules, protected paths with an approval log, spec-check and design gates, proof-before-finish checks, dispatch, review, build status and CI verification, configured per project from its docs",
|
|
12
|
-
"version": "0.11.
|
|
12
|
+
"version": "0.11.1",
|
|
13
13
|
"category": "development",
|
|
14
14
|
"tags": ["guardrails", "architecture", "lanes", "layers", "spec-driven", "ci", "multi-agent"],
|
|
15
15
|
"license": "MIT"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-kit",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Keeps AI coding agents inside your project's rules, from idea to merged code: spec design, lanes, layer rules, protected paths with an approval log, spec-check and design gates, proof-before-finish checks, dispatch, review, build status and CI verification, configured per project from its docs",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Warren Langeveldt"
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
What changed in each release of code-kit, newest first. Versions follow the plugin manifest and the npm package `@warren-dean/code-kit`. Each release is also on [GitHub Releases](https://github.com/warrendeanlangeveldt/code-kit/releases).
|
|
4
4
|
|
|
5
|
+
## 0.11.1 (2026-10-12)
|
|
6
|
+
|
|
7
|
+
The loop and the lead stop talking past each other, and approvals ask only what still needs you.
|
|
8
|
+
|
|
9
|
+
- **The loop never asks for a story an agent is still on.** `code-kit next` reads only branches, so the loop kept asking the lead to review stories their lane agents were still building, or already being reviewed, and to dispatch stories started a moment ago. It now leaves out every story a running agent's task names ("ST-10 capture pipeline", "Review domain/st-78"), and when nothing's left it waits, saying for what, until an agent finishes.
|
|
10
|
+
- **Approvals are counted as decisions.** "contract for the platform lane: 5 files" is one approval, not five; approving it covers them all.
|
|
11
|
+
- **Only requests that still wait are shown.** A request closes once an approval is given after it, even if that approval has since lapsed or was used up, and once its file has been committed by someone who could. A read-only agent, such as the reviewer running a code generator, is never approved to write: it's told to put the file back, and you aren't asked.
|
|
12
|
+
- **The history of approvals.** `/approvals` and `code-kit requests` list every approval given, newest first: when, what for, which lane, who gave it (you, or the lead within your delegated rules), the reason, and the files and branches it covered, merged or not.
|
|
13
|
+
|
|
5
14
|
## 0.11.0 (2026-10-12)
|
|
6
15
|
|
|
7
16
|
Teams get their own pane and their own setup, and a hosted team provider can stand down to GitHub.
|
package/README.md
CHANGED
|
@@ -279,7 +279,7 @@ With Claude Code 2.1.287 or later, code-kit's mod shows the project in the sessi
|
|
|
279
279
|
|
|
280
280
|
- **The band** sits above the prompt. While the build is moving (the lead mid-turn, an agent at work, the loop started) it opens with the mission: **goal**, the milestone being built and how many of its stories are done; **now**, what the lead is doing and the loop's state, with **Pause** or **Resume**; **next**, the step the loop takes next; and **agents**, a line per agent at work with its current tool and how long since. With Context Graph installed, a line says when a lane edited files without understanding them, and only then. Beneath, labelled **you**, what only you can decide, each with the story it's for ("for ST-4 Booking form (BOOK-1)"), and **d** ("Why, and what each choice does") opens it as a card in the Decisions tab; usage and the plan's pause are labelled **note**. In a team (`team` in the config), it also names what of the team needs you, and only that: your stale claim (Keep or Release), a claim of yours changing the same files as someone else's, an intent touching what you own or lease (every intent, with Agree and Drop, for the tech lead), and a pull request in a lane you own. While the loop runs, a lane's finished work is the lead's to review and merge, so it never waits here; with the loop off, Review and Merge are offered to you. With nothing happening and nothing waiting, the band is gone. It shows:
|
|
281
281
|
- an agent's call held for you: installing a new package, writing or committing a protected file, or a lane changing the kit. Rather than stopping the agent, the call waits while the band asks ("web wants to install dayjs · 1:53"). **Approve** opens the confirmation and, once you confirm, the same call runs; **Refuse**, or no answer within `harness.hold.minutes` (2 unless set), refuses it as before. Under `autonomy: autonomous`, a call within `approvals.delegate` is approved as delegated without asking. Nothing is held under `claude -p` or the SDK, where nobody can answer;
|
|
282
|
-
- the lead loop, under `harness.autonomy`. `/code-kit:init` asks you how hands-off you want it. Until you've chosen, the loop's first step waits in the band ("code-kit can keep the build moving between your prompts") with **Keep going**, **Ask each time** and **Off**, and your answer is recorded as your setting. Once the lead has finished a turn, the loop keeps the build moving: with the lead idle and nothing typed in the prompt, it asks `code-kit next` and prompts the lead to dispatch the ready stories, review a finished one, or build its own, never the same step twice while nothing has changed. A step the lead was prompted with twice in a row and that is still next goes to you instead, in the band: "Merge lead/st-1: still next after 2 prompts. Ask the lead again?" with **Go**. **Pause** and **Resume** stop and start it. Under `propose`, each step waits in the band for **Go**; under `off`, the loop does nothing. A lane agent with no tool call for `harness.stall.nudgeMinutes` (5) is messaged, unless its work is done (it handed back, or its branch is in review or the merge queue); at `stall.restartMinutes` (10) the lead is asked to stop it and dispatch its story again, at most `stall.maxRestarts` (2) times, and then the band flags the lane. When every story is done, the band says so and the loop stops. With `harness.agents.reviewer.on`, a finished branch is first reviewed in the background by code-kit's reviewer agent, which the lead starts: it runs `verify` and the review checklist, read-only, and grades each finding blocker, concern or nit. The lead's review prompt then carries the findings, and a blocker means the branch is sent back. The pane shows each review, running with its clock or what it found. If the reviewer hasn't reported within 15 minutes, or the plan's 5-hour use is past `harness.background.pauseAtPercent`, the lead reviews without it. A branch the lead's review passes joins the merge queue (`code-kit queue`). Branches merge one at a time, in the order they passed, each verified against the base as it is then: the lead merges where `approvals.delegate.merge` is on under `autonomous`, otherwise the band shows the head with **Merge** for you. Who may merge is read from the config committed on the base, so a branch can't grant itself merges. There's no first merge for you: setting up commits the rules you approved straight onto the base, and in a project set up before that, the lead puts them there itself (`code-kit queue rules`) before its first merge. A merged lane's worktree is removed when it holds nothing uncommitted. A head that conflicts or fails verify goes back to its lane. The pane lists the queue, and lanes whose branch waits in it read `queued`;
|
|
282
|
+
- the lead loop, under `harness.autonomy`. `/code-kit:init` asks you how hands-off you want it. Until you've chosen, the loop's first step waits in the band ("code-kit can keep the build moving between your prompts") with **Keep going**, **Ask each time** and **Off**, and your answer is recorded as your setting. Once the lead has finished a turn, the loop keeps the build moving: with the lead idle and nothing typed in the prompt, it asks `code-kit next` and prompts the lead to dispatch the ready stories, review a finished one, or build its own, never the same step twice while nothing has changed, and never a story an agent is still on: one its lane agent is building, or a reviewer is reviewing, waits until that agent finishes ("waiting: ST-10 is still with its agent"). A step the lead was prompted with twice in a row and that is still next goes to you instead, in the band: "Merge lead/st-1: still next after 2 prompts. Ask the lead again?" with **Go**. **Pause** and **Resume** stop and start it. Under `propose`, each step waits in the band for **Go**; under `off`, the loop does nothing. A lane agent with no tool call for `harness.stall.nudgeMinutes` (5) is messaged, unless its work is done (it handed back, or its branch is in review or the merge queue); at `stall.restartMinutes` (10) the lead is asked to stop it and dispatch its story again, at most `stall.maxRestarts` (2) times, and then the band flags the lane. When every story is done, the band says so and the loop stops. With `harness.agents.reviewer.on`, a finished branch is first reviewed in the background by code-kit's reviewer agent, which the lead starts: it runs `verify` and the review checklist, read-only, and grades each finding blocker, concern or nit. The lead's review prompt then carries the findings, and a blocker means the branch is sent back. The pane shows each review, running with its clock or what it found. If the reviewer hasn't reported within 15 minutes, or the plan's 5-hour use is past `harness.background.pauseAtPercent`, the lead reviews without it. A branch the lead's review passes joins the merge queue (`code-kit queue`). Branches merge one at a time, in the order they passed, each verified against the base as it is then: the lead merges where `approvals.delegate.merge` is on under `autonomous`, otherwise the band shows the head with **Merge** for you. Who may merge is read from the config committed on the base, so a branch can't grant itself merges. There's no first merge for you: setting up commits the rules you approved straight onto the base, and in a project set up before that, the lead puts them there itself (`code-kit queue rules`) before its first merge. A merged lane's worktree is removed when it holds nothing uncommitted. A head that conflicts or fails verify goes back to its lane. The pane lists the queue, and lanes whose branch waits in it read `queued`;
|
|
283
283
|
- approvals agents have asked for: **Approve…** opens a confirmation with a reason prefilled from the request, which you keep or rewrite;
|
|
284
284
|
- stories finished and waiting for review: **Review** asks the lead to run `/code-kit:review` on the branch, and **Merge** runs `verify`, checks included, then merges if it passes;
|
|
285
285
|
- a finish check failing in this session: **Lanes** opens the pane;
|
|
@@ -291,7 +291,7 @@ With Claude Code 2.1.287 or later, code-kit's mod shows the project in the sessi
|
|
|
291
291
|
- **`/lanes`** opens the Lanes pane. Its first line counts the stories by state (building, in review, queued, sent back, blocked, ready, merged), with the loop's state and the plan's 5-hour use; press a count to show only those lanes. Beneath it, whatever needs you (held calls, approvals, reviews, merges, stalled lanes, outliers) is pinned with its key. Tabs on **1** to **7**: **Lanes** opens on the loop as a card: its mode, the steps it took, the one running and the next. Then each lane on fixed columns: its name, state and story, its agent's current or last tool call with the time since beneath (amber once quiet, red when stalled), and its tokens; the lead reads orchestrating while its turn runs. A selected lane shows its stories as cards (state, requirements, what each waits on or its branch), then its agent and the paths it owns. **Decisions**, each decision that needs you as a card: what you asked for, the story and its requirements, what was happening, what's already settled, why the lead couldn't settle it, and each option with what it does; **Journal**, the build from your request to now in plain sentences, by day: the request and the decisions in the brief, the specification, each story handed out, sent back and merged, each approval and who gave it, each milestone finished, and the loop's steps this session, yours in yellow. Each lane has its own small pixel character, tinted in the lane's colour, whose pose shows its state: working (animated while its agent works), waiting on you, quiet, stalled, done or resting; the lead's wears a crown. Under each lane, its story's timeline on a shared time axis: building, then in review, with send-backs (↩), approvals (◆) and the merge (✓) marked where they fell; **Code map**, the whole project's folders by layer when no story is at work, or a story's files in a column per layer with the imports between them: a file fills with a lane's colour while that lane reads or edits it, and says whether its why (its Context Graph card) is written, out of date or not written yet; **j**/**k** select a file to see its card and what it imports and what imports it, and **o** opens it in Context Graph (box-drawing text in the terminal, an SVG on Desktop); **Trail**, every requirement in the specs as a cell by spec, its glyph and colour saying whether it's done and tested, done but untested, in progress, todo, without a story or removed, with a legend counting each. **h**/**l** and **j**/**k** move between cells and specs (or pick one from the list), showing the requirement's stories, their branches, the code they changed and the tests that name it; beneath the cells, the spec's trail drawn as a flow, each requirement to its stories, code and tests, with a missing link dashed in red; **Queue**, the merge queue; and **Usage**. **j** and **k** move between lanes, and the selected lane's acts are on **a** (approve), **r** (review), **s** (send back, with your reason), **m** (merge), **n** (nudge its agent) and **x** (stop it); **p** pauses or resumes the loop, and **f** filters the lanes by name, agent or story. **Enter** (or **o**) opens the selected lane's story: its requirements beside its spec-check report, the reviewer's findings by severity (each a way into the diff), `verify`'s problems, its usage and loop steps, and its diff against the base, file by file, with the acts its state allows. **b** or Escape goes back to the lanes. Inline (when the terminal can't dock it beside the transcript), it shows the counts, what needs you and a line per lane. It stays current within 2 seconds of a commit, a branch change, a plan edit or an agent starting or stopping. `/lanes` again, or Escape, closes it.
|
|
292
292
|
- **Refusals** a code-kit hook gives an agent are drawn as cards: what was refused, the rule, what to do, and Approve… where your approval would allow it. The full text is a press away.
|
|
293
293
|
- **`/harness`** opens the harness settings: how the lead loop acts, how long a call waiting on your approval is held, when a stalled lane is nudged and restarted, the reviewer agent and its model, and the use at which background agents pause. Changing one asks your reason, writes `.claude/code-kit.json` and logs a `kit` approval. Lanes, paths and rules still change through `/code-kit:init`.
|
|
294
|
-
- **`/approvals`** lists the
|
|
294
|
+
- **`/approvals`** lists the approvals waiting (one per approval and lane, with every file it covers), the approvals in force with minutes left, and every approval given, newest first: who gave it (you, or the lead within your delegated rules), its reason, and the files and branches it covered, merged or not. A request stops waiting once an approval is given for it, or its file is committed by someone who could; a read-only agent such as the reviewer never asks. **`/verify-branch`** runs `code-kit verify` on this branch. Neither calls the model.
|
|
295
295
|
|
|
296
296
|
Approving from the band records `(approved in the code-kit pane)` with your reason, and a merge from it says it was merged by the person from the pane. A settings change is logged as made in the pane. These happen only on your press: the hooks refuse `approve --via pane`, `merge --person` and `settings set` from every agent. Outside a project with `.claude/code-kit.json`, the mod draws nothing. Mods don't draw in the VS Code extension's chat panel, the Agent SDK or `claude -p`.
|
|
297
297
|
|
|
@@ -358,7 +358,7 @@ code-kit status [--base ref] [--json] # stories and requirements against the sp
|
|
|
358
358
|
code-kit next [--base ref] [--json] # the step to take now, from the project's state
|
|
359
359
|
code-kit trace <path>... [--json] # what a file is for: requirements, lane, layer rules
|
|
360
360
|
code-kit merge <branch> --delegated|--person [--into branch] # merge a branch that passes verify: the lead where approvals.delegate.merge is on, or the person
|
|
361
|
-
code-kit requests [--json] # approval requests
|
|
361
|
+
code-kit requests [--json] # approval requests still waiting, the approvals in force, and every approval given
|
|
362
362
|
code-kit stops [--session id] [--json] # finish checks that refused an agent's last stop and still fail
|
|
363
363
|
code-kit sent-back <branch> [--reason "…"] # record a review's send-back: status shows the story as sent back until the branch moves on
|
|
364
364
|
code-kit settings [--json] # the harness settings in force, with their defaults
|
package/bin/code-kit.mjs
CHANGED
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
approvalLifetime,
|
|
81
81
|
currentBranch,
|
|
82
82
|
PANE,
|
|
83
|
+
approvalHistory,
|
|
83
84
|
approvalsInForce,
|
|
84
85
|
delegatedMark,
|
|
85
86
|
grantApprovals,
|
|
@@ -912,7 +913,9 @@ function requests() {
|
|
|
912
913
|
load();
|
|
913
914
|
const open = openRequests(resolve('.'));
|
|
914
915
|
const granted = approvalsInForce(resolve('.'));
|
|
915
|
-
|
|
916
|
+
// CARD-6: every approval given, newest first.
|
|
917
|
+
const history = approvalHistory(resolve('.')).slice(0, 100);
|
|
918
|
+
if (flag('--json')) return out(JSON.stringify({ open, inForce: granted, history }, null, 2));
|
|
916
919
|
out(open.length ? `${open.length} approval request(s) open:` : 'No approval requests open.');
|
|
917
920
|
for (const r of open)
|
|
918
921
|
out(
|
|
@@ -923,6 +926,11 @@ function requests() {
|
|
|
923
926
|
out(
|
|
924
927
|
` ${a.name} for ${a.lane ? `the ${a.lane} lane` : 'any agent'}, ${a.minutesLeft} min left: ${a.reason}`,
|
|
925
928
|
);
|
|
929
|
+
out(history.length ? `\nApprovals given (newest first):` : '\nNo approvals given yet.');
|
|
930
|
+
for (const h of history.slice(0, 30))
|
|
931
|
+
out(
|
|
932
|
+
` ${h.at.slice(0, 16).replace('T', ' ')} ${h.name} for ${h.lane ? `the ${h.lane} lane` : 'any agent'}, by ${h.by}${h.files.length ? ` · ${h.files.length} file${h.files.length === 1 ? '' : 's'}${h.branches.length ? ` on ${h.branches.join(', ')}` : ''}` : ''}: ${h.reason}`,
|
|
933
|
+
);
|
|
926
934
|
}
|
|
927
935
|
|
|
928
936
|
/**
|
package/hooks/lib/rules.mjs
CHANGED
|
@@ -62,6 +62,8 @@ export const RELAYED = '(given in chat, recorded by the lead)';
|
|
|
62
62
|
export const PANE = '(approved in the code-kit pane)';
|
|
63
63
|
/** Refusals a person's approval would allow, for the mod's band and `code-kit requests`. */
|
|
64
64
|
export const REQUESTS_FILE = '.claude/state/requests.jsonl';
|
|
65
|
+
/** Every approval given through code-kit, and every one used up, kept for the history (CARD-6). */
|
|
66
|
+
export const GIVEN_FILE = '.claude/state/approvals-given.jsonl';
|
|
65
67
|
|
|
66
68
|
/** The command the lead runs to record approvals a person gave in chat. */
|
|
67
69
|
/** Marks an approval the lead granted on its own, within `approvals.delegate`; `rule` names the rule. */
|
|
@@ -107,11 +109,52 @@ export function approvalHowTo(names, actor, root, allows = 'these edits', config
|
|
|
107
109
|
export function grantApprovals(root, names, lane, reason, marks = {}) {
|
|
108
110
|
const dir = lane ? join(approvalsDir(root), lane) : approvalsDir(root);
|
|
109
111
|
mkdirSync(dir, { recursive: true });
|
|
110
|
-
|
|
112
|
+
const at = new Date().toISOString();
|
|
113
|
+
const files = names.map((name) => {
|
|
111
114
|
const file = join(dir, name);
|
|
112
115
|
writeFileSync(file, `${reason.trim()} ${marks[name] ?? RELAYED}\n`);
|
|
113
116
|
return file;
|
|
114
117
|
});
|
|
118
|
+
noteGiven(
|
|
119
|
+
root,
|
|
120
|
+
names.map((name) => ({
|
|
121
|
+
at,
|
|
122
|
+
name,
|
|
123
|
+
...(lane ? { lane } : {}),
|
|
124
|
+
reason: `${reason.trim()} ${marks[name] ?? RELAYED}`,
|
|
125
|
+
})),
|
|
126
|
+
);
|
|
127
|
+
return files;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const givenFile = (root) => join(projectWorktrees(root)[0] ?? root, GIVEN_FILE);
|
|
131
|
+
|
|
132
|
+
function noteGiven(root, entries) {
|
|
133
|
+
try {
|
|
134
|
+
const file = givenFile(root);
|
|
135
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
136
|
+
appendFileSync(file, entries.map((e) => `${JSON.stringify(e)}\n`).join(''));
|
|
137
|
+
} catch {
|
|
138
|
+
// the approval stands; only its history misses it
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The approvals given through code-kit (and those used up), oldest first. */
|
|
143
|
+
export function approvalsGiven(root) {
|
|
144
|
+
try {
|
|
145
|
+
return readFileSync(givenFile(root), 'utf8')
|
|
146
|
+
.split('\n')
|
|
147
|
+
.filter(Boolean)
|
|
148
|
+
.flatMap((l) => {
|
|
149
|
+
try {
|
|
150
|
+
return [JSON.parse(l)];
|
|
151
|
+
} catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
} catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
115
158
|
}
|
|
116
159
|
|
|
117
160
|
/** `path` with symlinks resolved, even if it doesn't exist yet (macOS /var → /private/var). */
|
|
@@ -249,8 +292,21 @@ export function useApproval(root, name, actor) {
|
|
|
249
292
|
for (const file of [
|
|
250
293
|
...(actor?.kind === 'lane' ? [join(dir, actor.lane, name)] : []),
|
|
251
294
|
join(dir, name),
|
|
252
|
-
])
|
|
253
|
-
|
|
295
|
+
]) {
|
|
296
|
+
const a = inForce(file);
|
|
297
|
+
if (!a) continue;
|
|
298
|
+
// Kept in the history, and as the answer to the request it was given for, once its file is gone.
|
|
299
|
+
noteGiven(root, [
|
|
300
|
+
{
|
|
301
|
+
at: a.grantedAt,
|
|
302
|
+
name,
|
|
303
|
+
...(file !== join(dir, name) ? { lane: actor.lane } : {}),
|
|
304
|
+
reason: a.reason,
|
|
305
|
+
usedAt: new Date().toISOString(),
|
|
306
|
+
},
|
|
307
|
+
]);
|
|
308
|
+
return rmSync(file, { force: true });
|
|
309
|
+
}
|
|
254
310
|
}
|
|
255
311
|
|
|
256
312
|
const requestsFile = (root) => join(projectWorktrees(root)[0] ?? root, REQUESTS_FILE);
|
|
@@ -283,6 +339,9 @@ export function readRequests(root) {
|
|
|
283
339
|
* never changes the refusal, so any failure here is ignored.
|
|
284
340
|
*/
|
|
285
341
|
export function recordRequest(root, actor, names, what, why, now = Date.now()) {
|
|
342
|
+
// A read-only agent (a reviewer) is never approved to write: what it changed goes back, not to the
|
|
343
|
+
// person as a request.
|
|
344
|
+
if (actor?.kind === 'readonly') return;
|
|
286
345
|
try {
|
|
287
346
|
const request = {
|
|
288
347
|
at: new Date(now).toISOString(),
|
|
@@ -307,16 +366,161 @@ export function recordRequest(root, actor, names, what, why, now = Date.now()) {
|
|
|
307
366
|
/** Requests from the last 60 minutes that an approval in force doesn't yet answer, newest first. */
|
|
308
367
|
export function openRequests(root, now = Date.now()) {
|
|
309
368
|
const open = [];
|
|
369
|
+
const given = approvalsGiven(root);
|
|
310
370
|
for (const r of readRequests(root).reverse()) {
|
|
311
371
|
if (now - Date.parse(r.at) >= APPROVAL_MINUTES * 60_000) continue;
|
|
312
372
|
if (open.some((o) => sameRequest(o, r))) continue;
|
|
373
|
+
// From before read-only agents stopped asking: only the lead and the lanes are ever approved.
|
|
374
|
+
if (!r.lane && r.actor !== 'lead session') continue;
|
|
313
375
|
const actor = r.lane ? { kind: 'lane', lane: r.lane } : { kind: 'lead' };
|
|
314
376
|
if (r.names.every((n) => approval(root, n, actor))) continue;
|
|
377
|
+
// Answered already: approved after it was asked (since expired, or used up by the commit it
|
|
378
|
+
// allowed), or the file it named was committed since by someone who could.
|
|
379
|
+
if (r.names.every((n) => grantedSince(root, given, n, r))) continue;
|
|
380
|
+
if (committedSince(root, r)) continue;
|
|
315
381
|
open.push(r);
|
|
316
382
|
}
|
|
317
383
|
return open;
|
|
318
384
|
}
|
|
319
385
|
|
|
386
|
+
/** Whether `name` was approved for the request's lane (or for everyone) at or after it was asked. */
|
|
387
|
+
function grantedSince(root, given, name, r) {
|
|
388
|
+
const asked = Date.parse(r.at);
|
|
389
|
+
if (
|
|
390
|
+
given.some(
|
|
391
|
+
(g) => g.name === name && (!g.lane || g.lane === r.lane) && Date.parse(g.at) >= asked,
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
return true;
|
|
395
|
+
const dir = approvalsDir(root);
|
|
396
|
+
return [...(r.lane ? [join(dir, r.lane, name)] : []), join(dir, name)].some((file) => {
|
|
397
|
+
try {
|
|
398
|
+
return statSync(file).isFile() && statSync(file).mtimeMs >= asked;
|
|
399
|
+
} catch {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Whether the file a request named has been committed, on any branch, since it was asked. */
|
|
406
|
+
function committedSince(root, r) {
|
|
407
|
+
const path = String(r.what);
|
|
408
|
+
if (/\s/.test(path) || (!path.includes('/') && !path.includes('.'))) return false;
|
|
409
|
+
try {
|
|
410
|
+
const out = execFileSync(
|
|
411
|
+
'git',
|
|
412
|
+
[
|
|
413
|
+
'-C',
|
|
414
|
+
root,
|
|
415
|
+
'log',
|
|
416
|
+
'--all',
|
|
417
|
+
'-1',
|
|
418
|
+
'--format=%ct',
|
|
419
|
+
`--since=@${Math.floor(Date.parse(r.at) / 1000)}`,
|
|
420
|
+
'--',
|
|
421
|
+
path,
|
|
422
|
+
],
|
|
423
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
|
|
424
|
+
);
|
|
425
|
+
return Boolean(out.trim());
|
|
426
|
+
} catch {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* CARD-6: every approval given, newest first: { at, name, lane, reason, by, files, branches, used,
|
|
433
|
+
* expired }. From the approvals given through code-kit, the approvals on disk (a person's own `!` echo
|
|
434
|
+
* included), and the audit log in every worktree, so an approval shows before its branch merges.
|
|
435
|
+
*/
|
|
436
|
+
export function approvalHistory(root, now = Date.now()) {
|
|
437
|
+
const grants = [];
|
|
438
|
+
const near = (a, b) => Math.abs(Date.parse(a) - Date.parse(b)) < 2000;
|
|
439
|
+
const add = (g) => {
|
|
440
|
+
const found = grants.find(
|
|
441
|
+
(x) => x.name === g.name && (x.lane ?? null) === (g.lane ?? null) && near(x.at, g.at),
|
|
442
|
+
);
|
|
443
|
+
if (found) {
|
|
444
|
+
if (g.usedAt) found.used = true;
|
|
445
|
+
return found;
|
|
446
|
+
}
|
|
447
|
+
const grant = {
|
|
448
|
+
at: g.at,
|
|
449
|
+
name: g.name,
|
|
450
|
+
lane: g.lane ?? null,
|
|
451
|
+
reason: g.reason ?? '',
|
|
452
|
+
files: [],
|
|
453
|
+
branches: [],
|
|
454
|
+
used: Boolean(g.usedAt),
|
|
455
|
+
};
|
|
456
|
+
grants.push(grant);
|
|
457
|
+
return grant;
|
|
458
|
+
};
|
|
459
|
+
for (const g of approvalsGiven(root)) add(g);
|
|
460
|
+
const dir = approvalsDir(root);
|
|
461
|
+
const look = (folder, lane) => {
|
|
462
|
+
let names = [];
|
|
463
|
+
try {
|
|
464
|
+
names = readdirSync(folder);
|
|
465
|
+
} catch {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
for (const name of names) {
|
|
469
|
+
const file = join(folder, name);
|
|
470
|
+
const st = statSync(file);
|
|
471
|
+
if (st.isDirectory()) {
|
|
472
|
+
if (!lane) look(file, name);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
add({
|
|
476
|
+
at: new Date(st.mtimeMs).toISOString(),
|
|
477
|
+
name,
|
|
478
|
+
lane,
|
|
479
|
+
reason: readFileSync(file, 'utf8').trim(),
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
look(dir, null);
|
|
484
|
+
const seen = new Set();
|
|
485
|
+
for (const tree of projectWorktrees(root).length ? projectWorktrees(root) : [root]) {
|
|
486
|
+
let lines = [];
|
|
487
|
+
try {
|
|
488
|
+
lines = readFileSync(join(tree, APPROVAL_LOG), 'utf8').split('\n').filter(Boolean);
|
|
489
|
+
} catch {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
for (const line of lines) {
|
|
493
|
+
if (seen.has(line)) continue;
|
|
494
|
+
seen.add(line);
|
|
495
|
+
let e;
|
|
496
|
+
try {
|
|
497
|
+
e = JSON.parse(line);
|
|
498
|
+
} catch {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (!e.approval || e.approval === 'lead') continue;
|
|
502
|
+
const grant =
|
|
503
|
+
grants.find((x) => x.name === e.approval && near(x.at, e.grantedAt ?? e.at)) ??
|
|
504
|
+
add({ at: e.grantedAt ?? e.at, name: e.approval, reason: e.reason });
|
|
505
|
+
if (e.file && !grant.files.includes(e.file)) grant.files.push(e.file);
|
|
506
|
+
if (e.branch && !grant.branches.includes(e.branch)) grant.branches.push(e.branch);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return grants
|
|
510
|
+
.map((g) => ({
|
|
511
|
+
...g,
|
|
512
|
+
by: /within the delegated rules/.test(g.reason)
|
|
513
|
+
? 'the lead, within your delegated rules'
|
|
514
|
+
: /given in chat, recorded by the lead/.test(g.reason)
|
|
515
|
+
? 'you, in chat'
|
|
516
|
+
: /in the [\w -]+ pane/.test(g.reason)
|
|
517
|
+
? 'you, in the pane'
|
|
518
|
+
: 'you',
|
|
519
|
+
expired: g.used || now - Date.parse(g.at) >= approvalMinutes(g.name) * 60_000,
|
|
520
|
+
}))
|
|
521
|
+
.sort((a, b) => Date.parse(b.at) - Date.parse(a.at));
|
|
522
|
+
}
|
|
523
|
+
|
|
320
524
|
/** Approvals in force now: [{ name, lane?, reason, grantedAt, minutesLeft }]. */
|
|
321
525
|
export function approvalsInForce(root, now = Date.now()) {
|
|
322
526
|
const dir = approvalsDir(root);
|
|
@@ -434,6 +638,8 @@ export function writeProblem(actor, rel, root, config) {
|
|
|
434
638
|
// A package a person approved may change the manifests and lockfiles its install rewrites.
|
|
435
639
|
if (dependencyChange(actor, rel, root, config)) return null;
|
|
436
640
|
const guarded = protectedEntry(rel, config);
|
|
641
|
+
if (actor.kind === 'readonly' && (guarded || !matchesAny(rel, config.anyActor)))
|
|
642
|
+
return readonlyProblem(actor, rel);
|
|
437
643
|
// The lead writes protected files without an approval: the person reviews them in the pull request,
|
|
438
644
|
// and every committed change is in the audit log. Anyone else needs a person's approval as well.
|
|
439
645
|
if (guarded && actor.kind !== 'lead' && !approval(root, guarded.approval, actor)) {
|
|
@@ -441,12 +647,16 @@ export function writeProblem(actor, rel, root, config) {
|
|
|
441
647
|
return `${rel} is ${guarded.why} and needs a person's approval. Ask the lead (a person) to run\n${approvalHowTo(guarded.approval, actor, root, undefined, config)}`;
|
|
442
648
|
}
|
|
443
649
|
if (matchesAny(rel, config.anyActor)) return null;
|
|
444
|
-
if (actor.kind === 'readonly') return
|
|
650
|
+
if (actor.kind === 'readonly') return readonlyProblem(actor, rel);
|
|
445
651
|
if (!owns(actor, rel, config)) return ownershipProblem(actor, rel, config);
|
|
446
652
|
const specs = actor.kind === 'lane' && config.docs?.specs;
|
|
447
653
|
return (specs && specCheckProblem(rel, root)) || screenProblem(rel, root, config);
|
|
448
654
|
}
|
|
449
655
|
|
|
656
|
+
/** A read-only agent's write, refused, with how to undo one a command it ran made (a build, a generator). */
|
|
657
|
+
const readonlyProblem = (actor, rel) =>
|
|
658
|
+
`${actor.label} is read-only; it may not write ${rel}. If a command you ran changed it (a build, a formatter or a code generator), put it back with git restore -- ${rel} (or delete it if it's new), and run such commands only in a throwaway worktree of the branch.`;
|
|
659
|
+
|
|
450
660
|
function outsideAllowed(actor, rel, root, config) {
|
|
451
661
|
if (actor.kind !== 'lead') return false;
|
|
452
662
|
const target = resolve(root, rel);
|
package/hooks/mod/decisions.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// the person, each with its lineage. The band's "you" lines say what needs them; a decision card says why:
|
|
3
3
|
// what they asked for, the story and requirements it belongs to, what the agent was doing, what was already
|
|
4
4
|
// settled, why the lead couldn't settle this itself, and each option with what happens next.
|
|
5
|
+
import { askWords } from './view.mjs';
|
|
5
6
|
|
|
6
7
|
/** The kinds of band line that are the person's to decide; the rest are notes. */
|
|
7
8
|
export const DECISION_KINDS = new Set([
|
|
@@ -151,7 +152,7 @@ export function decisionCards(lines, context = {}) {
|
|
|
151
152
|
})),
|
|
152
153
|
doing:
|
|
153
154
|
line.held?.command ??
|
|
154
|
-
line.request
|
|
155
|
+
(line.request ? askWords(line.request, line.files ?? [line.request.what]) : null) ??
|
|
155
156
|
(line.proposal?.label ? `Next step: ${line.proposal.label}` : null) ??
|
|
156
157
|
(line.intent
|
|
157
158
|
? `Intent #${line.intent.id}: ${line.intent.about}${line.intent.touches.length ? ` (touches ${line.intent.touches.join(', ')})` : ''}`
|
package/hooks/mod/loop.mjs
CHANGED
|
@@ -14,6 +14,55 @@ const LEAD_STEPS = new Set([
|
|
|
14
14
|
'update',
|
|
15
15
|
]);
|
|
16
16
|
|
|
17
|
+
/** The steps that name stories, which an agent already at work on one shouldn't be asked to start again. */
|
|
18
|
+
const STORY_STEPS = new Set(['dispatch', 'review', 'lead', 'design']);
|
|
19
|
+
const AT_WORK = new Set(['pending', 'running', 'waiting']);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* LOOP-9: the stories agents are at work on now, by the task each was started with: a lane agent is
|
|
23
|
+
* started as "ST-12 booking form", a reviewer as "Review web/st-12". `code-kit next` reads only the
|
|
24
|
+
* branches, so it can't tell a branch still being built, or already under review, from one waiting.
|
|
25
|
+
*/
|
|
26
|
+
export function storiesInFlight(agents) {
|
|
27
|
+
const ids = new Set();
|
|
28
|
+
for (const a of agents ?? []) {
|
|
29
|
+
if (!AT_WORK.has(a.status)) continue;
|
|
30
|
+
const text = `${a.description ?? ''}`;
|
|
31
|
+
for (const m of text.matchAll(/\bST-(\d+)\b/gi)) ids.add(`ST-${m[1]}`);
|
|
32
|
+
for (const m of text.matchAll(/\b[\w-]+\/st-(\d+)\b/gi)) ids.add(`ST-${m[1]}`);
|
|
33
|
+
}
|
|
34
|
+
return ids;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* LOOP-9: the step from `code-kit next`, without the stories already in flight. A step left with none
|
|
39
|
+
* gives way to the next one `next` listed; with nothing left, the loop waits, saying for what, until an
|
|
40
|
+
* agent finishes (which moves the project on).
|
|
41
|
+
*/
|
|
42
|
+
export function withoutInFlight(step, inFlight) {
|
|
43
|
+
if (!step || !inFlight?.size) return step;
|
|
44
|
+
const chain = [step, ...(step.then ?? [])].map(({ then: _then, ...s }) => s);
|
|
45
|
+
const kept = [];
|
|
46
|
+
const busy = [];
|
|
47
|
+
for (const s of chain) {
|
|
48
|
+
if (!STORY_STEPS.has(s.step)) {
|
|
49
|
+
kept.push(s);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const ids = (s.args ?? '').split(/\s+/).filter(Boolean);
|
|
53
|
+
const free = ids.filter((id) => !inFlight.has(id.toUpperCase()));
|
|
54
|
+
busy.push(...ids.filter((id) => inFlight.has(id.toUpperCase())));
|
|
55
|
+
if (free.length) kept.push({ ...s, args: free.join(' ') });
|
|
56
|
+
}
|
|
57
|
+
if (kept.length) return { ...kept[0], ...(kept.length > 1 ? { then: kept.slice(1) } : {}) };
|
|
58
|
+
const names = [...new Set(busy)];
|
|
59
|
+
return {
|
|
60
|
+
step: 'wait',
|
|
61
|
+
why: `${names.join(', ')} ${names.length === 1 ? 'is' : 'are'} still with ${names.length === 1 ? 'its agent' : 'their agents'}; the loop goes on when one finishes`,
|
|
62
|
+
inFlight: names,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
17
66
|
/**
|
|
18
67
|
* The prompt for a step from `code-kit next --json` (LOOP-1), or null when it isn't the lead's to
|
|
19
68
|
* take. Every ready story is named: the dispatch skill decides how many lanes go (LOOP-2). A prompt
|
package/hooks/mod/register.mjs
CHANGED
|
@@ -45,7 +45,16 @@ import {
|
|
|
45
45
|
sendBackPane,
|
|
46
46
|
toolLine,
|
|
47
47
|
} from './panes.mjs';
|
|
48
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
leadPrompt,
|
|
50
|
+
nudgeText,
|
|
51
|
+
restartPrompt,
|
|
52
|
+
stallDue,
|
|
53
|
+
stepKey,
|
|
54
|
+
stepLabel,
|
|
55
|
+
storiesInFlight,
|
|
56
|
+
withoutInFlight,
|
|
57
|
+
} from './loop.mjs';
|
|
49
58
|
import { goalOf, nextText } from './mission.mjs';
|
|
50
59
|
import { contextOf, decisionCards } from './decisions.mjs';
|
|
51
60
|
import { DEFAULT_TEAM_UI, TEAM_ID, teamFrame } from './teamview.mjs';
|
|
@@ -790,7 +799,14 @@ export function register(on) {
|
|
|
790
799
|
return act.submit(prompt, 'restart', story);
|
|
791
800
|
}
|
|
792
801
|
const ran = await $.process.run(['node', cli, 'next', '--json'], { cwd });
|
|
793
|
-
|
|
802
|
+
// LOOP-9: never a story an agent is already at work on, building or reviewing it.
|
|
803
|
+
const step =
|
|
804
|
+
ran.exitCode === 0
|
|
805
|
+
? withoutInFlight(
|
|
806
|
+
parseJson(ran.stdout),
|
|
807
|
+
storiesInFlight(await $.agent.list().catch(() => [])),
|
|
808
|
+
)
|
|
809
|
+
: null;
|
|
794
810
|
// MISSION-1: the band shows what the loop takes next.
|
|
795
811
|
loop.nextStep = step;
|
|
796
812
|
if (!step) {
|
|
@@ -1527,6 +1527,34 @@ test('LOOP-1 and LOOP-2 with the lead idle, the loop dispatches every ready stor
|
|
|
1527
1527
|
await ui.unmount();
|
|
1528
1528
|
});
|
|
1529
1529
|
|
|
1530
|
+
test('LOOP-9 the loop never asks for a story an agent is still on: it waits, and goes on once the agent finishes', async ($, on) => {
|
|
1531
|
+
const w = project();
|
|
1532
|
+
w.next = { step: 'dispatch', args: 'ST-9', then: [] };
|
|
1533
|
+
const clock = await start($, on, w);
|
|
1534
|
+
await leadTurn($);
|
|
1535
|
+
await settle(clock);
|
|
1536
|
+
expect(w.prompts).toEqual(['Dispatch ST-9: run /code-kit:dispatch ST-9.']);
|
|
1537
|
+
// Dispatched, and ST-4 still being built: next (reading only branches) offers both again.
|
|
1538
|
+
w.agents = [
|
|
1539
|
+
{ id: 'a1', type: 'web-engineer', status: 'running', description: 'ST-4 booking form' },
|
|
1540
|
+
{ id: 'a2', type: 'core-engineer', status: 'running', description: 'ST-9 pricing' },
|
|
1541
|
+
];
|
|
1542
|
+
w.next = { step: 'review', args: 'ST-4', then: [{ step: 'dispatch', args: 'ST-9' }] };
|
|
1543
|
+
await leadTurn($, 't-2');
|
|
1544
|
+
await settle(clock);
|
|
1545
|
+
expect(w.prompts).toHaveLength(1);
|
|
1546
|
+
const band = await bandUi($);
|
|
1547
|
+
expect(
|
|
1548
|
+
await band.find({ type: 'Text', text: /waiting: ST-4, ST-9 are still with their agents/ }),
|
|
1549
|
+
).toBeDefined();
|
|
1550
|
+
await band.unmount();
|
|
1551
|
+
// The web agent finishes: ST-4 is reviewed; ST-9 is still being built, so it isn't dispatched again.
|
|
1552
|
+
w.agents = [{ ...w.agents[0], status: 'completed' }, w.agents[1]];
|
|
1553
|
+
await clock.advance(2000);
|
|
1554
|
+
await settle(clock);
|
|
1555
|
+
expect(w.prompts.slice(1)).toEqual(['Review ST-4: run /code-kit:review ST-4.']);
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1530
1558
|
test('LOOP-1 nothing is submitted mid-turn, or while the person has a draft, or twice for the same step', async ($, on) => {
|
|
1531
1559
|
const w = project();
|
|
1532
1560
|
const clock = await start($, on, w);
|
package/hooks/mod/view.mjs
CHANGED
|
@@ -630,6 +630,31 @@ export const APPROVE_ID = 'code-kit-approve';
|
|
|
630
630
|
export const RESULT_ID = 'code-kit-result';
|
|
631
631
|
|
|
632
632
|
/** An approval name as a person reads it: a dependency approval by its package. */
|
|
633
|
+
/**
|
|
634
|
+
* CARD-2: open requests as the decisions they are: one per approval and lane, with every file (or
|
|
635
|
+
* command) it was asked for, newest first. Approving it covers them all.
|
|
636
|
+
*/
|
|
637
|
+
export function approvalAsks(open) {
|
|
638
|
+
const asks = new Map();
|
|
639
|
+
for (const r of open ?? []) {
|
|
640
|
+
const key = `${[...r.names].sort().join(',')}|${r.lane ?? r.actor}`;
|
|
641
|
+
const ask = asks.get(key) ?? { request: r, files: [] };
|
|
642
|
+
if (!ask.files.includes(r.what)) ask.files.push(r.what);
|
|
643
|
+
asks.set(key, ask);
|
|
644
|
+
}
|
|
645
|
+
return [...asks.values()];
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** What an approval decision covers, in a line: "contract for the platform lane: a.ts, b.ts (2 files)". */
|
|
649
|
+
export function askWords(request, files = [request?.what]) {
|
|
650
|
+
const who = request.lane ? `the ${request.lane} lane` : request.actor;
|
|
651
|
+
const list =
|
|
652
|
+
files.length > 4
|
|
653
|
+
? `${files.slice(0, 4).join(', ')} and ${files.length - 4} more`
|
|
654
|
+
: files.join(', ');
|
|
655
|
+
return `${request.names.map(approvalLabel).join(', ')} for ${who}: ${list}${files.length > 1 ? ` (${files.length} files)` : ''}`;
|
|
656
|
+
}
|
|
657
|
+
|
|
633
658
|
export function approvalLabel(name) {
|
|
634
659
|
return name.startsWith('dep-') ? name.slice(4).replaceAll('+', '/') : name;
|
|
635
660
|
}
|
|
@@ -745,12 +770,14 @@ export function bandLines({
|
|
|
745
770
|
actions: [{ id: 'go', label: 'Go' }],
|
|
746
771
|
});
|
|
747
772
|
// A held call's request is asked on its own line above, not again here.
|
|
748
|
-
|
|
749
|
-
|
|
773
|
+
// CARD-2: counted as decisions, not files: one approval for a lane covers every file it was asked for.
|
|
774
|
+
const asks = approvalAsks((requests?.open ?? []).filter((r) => !held.some((h) => sameAsk(h, r))));
|
|
775
|
+
if (asks.length)
|
|
750
776
|
lines.push({
|
|
751
777
|
kind: 'approvals',
|
|
752
|
-
text: `${plural(
|
|
753
|
-
request:
|
|
778
|
+
text: `${plural(asks.length, 'approval', 'approvals')} waiting`,
|
|
779
|
+
request: asks[0].request,
|
|
780
|
+
files: asks[0].files,
|
|
754
781
|
actions: [{ id: 'approve', label: 'Approve…' }],
|
|
755
782
|
});
|
|
756
783
|
// DECIDE-1: reviewing a lane's work is the lead's. Only with the loop off, where the person runs the
|
|
@@ -1129,12 +1156,13 @@ export function refusalView(card, expanded, { Box, Text, Button }, { onApprove,
|
|
|
1129
1156
|
export function approvalsText(requests) {
|
|
1130
1157
|
const open = requests?.open ?? [];
|
|
1131
1158
|
const inForce = requests?.inForce ?? [];
|
|
1159
|
+
const history = requests?.history ?? [];
|
|
1160
|
+
const asks = approvalAsks(open);
|
|
1132
1161
|
const lines = [
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
),
|
|
1162
|
+
asks.length
|
|
1163
|
+
? `${plural(asks.length, 'approval', 'approvals')} waiting:`
|
|
1164
|
+
: 'No requests waiting.',
|
|
1165
|
+
...asks.map((a) => ` ${askWords(a.request, a.files)}`),
|
|
1138
1166
|
inForce.length
|
|
1139
1167
|
? `${plural(inForce.length, 'approval', 'approvals')} in force:`
|
|
1140
1168
|
: 'No approvals in force.',
|
|
@@ -1142,6 +1170,14 @@ export function approvalsText(requests) {
|
|
|
1142
1170
|
(a) =>
|
|
1143
1171
|
` ${approvalLabel(a.name)} for ${a.lane ? `the ${a.lane} lane` : 'any agent'}, ${a.minutesLeft} min left: ${a.reason}`,
|
|
1144
1172
|
),
|
|
1173
|
+
// CARD-6: what was given, newest first, whether or not its branch has merged.
|
|
1174
|
+
history.length ? 'Approvals given, newest first:' : 'No approvals given yet.',
|
|
1175
|
+
...history
|
|
1176
|
+
.slice(0, 20)
|
|
1177
|
+
.map(
|
|
1178
|
+
(h) =>
|
|
1179
|
+
` ${new Date(h.at).toLocaleString('en-AU', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })} ${approvalLabel(h.name)} for ${h.lane ? `the ${h.lane} lane` : 'any agent'}, by ${h.by}${h.files.length ? ` (${h.files.length} file${h.files.length === 1 ? '' : 's'})` : ''}: ${h.reason}`,
|
|
1180
|
+
),
|
|
1145
1181
|
];
|
|
1146
1182
|
return lines.join('\n');
|
|
1147
1183
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warren-dean/code-kit",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.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/dispatch/SKILL.md
CHANGED
|
@@ -48,7 +48,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" team claim ST-<n> --lease "<glob>,
|
|
|
48
48
|
|
|
49
49
|
The lease is the paths the story will create or change: its lane's folders for this feature, and any contract it changes. A claim someone else holds is refused with who holds it; pick another story or ask them. Stories in a lane another person owns are theirs to dispatch. Making the story's branch also claims it, and each commit on it adds what it changed to the lease; merging it ends the claim.
|
|
50
50
|
|
|
51
|
-
Start each story with the Agent tool: `subagent_type` is the lane's agent, `isolation: "worktree"`, and run them in the background. Start every story in the batch in the same message so they run in parallel.
|
|
51
|
+
Start each story with the Agent tool: `subagent_type` is the lane's agent, `isolation: "worktree"`, and run them in the background, with a `description` that starts with the story's id ("ST-12 booking form"): the loop reads it to know the story is in flight, and won't ask for it again while the agent works. Start every story in the batch in the same message so they run in parallel.
|
|
52
52
|
|
|
53
53
|
The brief is the agent's whole context. Write it so the agent needs nothing else:
|
|
54
54
|
|
package/skills/review/SKILL.md
CHANGED
|
@@ -57,7 +57,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" security --base <base> --branch <b
|
|
|
57
57
|
|
|
58
58
|
It does when the diff touches sign-in, permissions, input from outside, personal data, encryption, payments, secrets, or queries and commands built from strings, adds dependencies, or the story cites a security requirement; it says which, and where. When it does:
|
|
59
59
|
|
|
60
|
-
- Run the `security-reviewer` agent (Agent tool, `subagent_type` `security-reviewer`) with the branch, the base, the story, its requirements, the spec files it touches (their `## Threat model` and `## Security requirements`) and the reasons `security` gave. It reads only, and ends with the same findings block as code-kit's reviewer: each finding graded blocker, concern or nit, with where, the attack and the fix.
|
|
60
|
+
- Run the `security-reviewer` agent (Agent tool, `subagent_type` `security-reviewer`, its `description` naming the branch, such as "Security review web/st-12", so the loop knows the story is under review) with the branch, the base, the story, its requirements, the spec files it touches (their `## Threat model` and `## Security requirements`) and the reasons `security` gave. It reads only, and ends with the same findings block as code-kit's reviewer: each finding graded blocker, concern or nit, with where, the attack and the fix.
|
|
61
61
|
- **A blocker fails the review**, like a failing test: the branch goes back to its lane with the finding (where, the attack, the fix). Concerns are yours to weigh: send back the ones the threat model says matter, and put the rest in the report. Nits go in the report.
|
|
62
62
|
- No `security-reviewer` agent in the project: `security` says so. Review the diff yourself against the same list in `${CLAUDE_PLUGIN_ROOT}/templates/security-reviewer.md`, and tell the person that re-running `/code-kit:init` adds the agent.
|
|
63
63
|
- A background reviewer can't start agents: it lists the security review as due, as a concern with the reasons, and the lead runs it.
|