@warren-dean/code-kit 0.11.0 → 0.12.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.
@@ -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.0",
12
+ "version": "0.12.0",
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.0",
3
+ "version": "0.12.0",
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,26 @@
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.12.0 (2026-10-13)
6
+
7
+ What only you can decide reaches you, and waits until you answer.
8
+
9
+ - **Questions from the lead.** What the lead can't settle itself (a product choice, an account only you can create, wording only you can sign off) is asked with `code-kit ask`, and waits in the Decisions tab with the lead's options and what each does. Your answer, a press or your own words, is sent to the lead. A question can hold its story, which reads "waiting on you": nothing dispatches or reviews it, and the stories that depend on it wait, until you answer.
10
+ - **Your brief's open questions** are decisions too, each with **Answer…** (the lead records it in the brief) and **Later**.
11
+ - **What Claude Code itself refused.** A call Claude Code's own permission check refused, which code-kit can't approve past, waits in the band with **Put it in the prompt** (the command after `!`, for you to run), **Done it** and **Not now**.
12
+ - **Approval requests wait until answered.** They no longer vanish after 60 minutes; one nobody needs is dropped with **Not needed**, or by the lead with `code-kit requests drop`.
13
+ - **A card for each decision.** Two approvals waiting are two cards in the Decisions tab, each answered on its own.
14
+ - **Bug reports for code-kit.** With `harness.report.on` (off unless you turn it on; init asks), an agent that code-kit got in the way of drafts a report with only code-kit's own facts, your project's folders and email addresses taken out. You read it and send it to code-kit's GitHub with your own `gh` (a comment, if it's filed already), or get the link to file it, or discard it.
15
+
16
+ ## 0.11.1 (2026-10-12)
17
+
18
+ The loop and the lead stop talking past each other, and approvals ask only what still needs you.
19
+
20
+ - **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.
21
+ - **Approvals are counted as decisions.** "contract for the platform lane: 5 files" is one approval, not five; approving it covers them all.
22
+ - **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.
23
+ - **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.
24
+
5
25
  ## 0.11.0 (2026-10-12)
6
26
 
7
27
  Teams get their own pane and their own setup, and a hosted team provider can stand down to GitHub.
package/README.md CHANGED
@@ -279,19 +279,23 @@ 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`;
283
- - approvals agents have asked for: **Approve…** opens a confirmation with a reason prefilled from the request, which you keep or rewrite;
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
+ - approvals agents have asked for: **Approve…** opens a confirmation with a reason prefilled from the request, which you keep or rewrite. A request waits until it's answered, its file is committed by someone who could, or it's dropped (at most a week); on its card, **Not needed** drops it;
284
+ - questions from the lead ("The lead asks: which weather provider?"): what it can't settle and only you can, asked with `code-kit ask`. **Answer…** opens it as a card with each of the lead's options and what it does, and **Something else…** for your own words; your answer is recorded and sent to the lead. A question can hold its story, which then reads `waiting on you`: nothing dispatches or reviews it, and the stories that depend on it wait, until you answer;
285
+ - your brief's open questions: **Answer…** sends your answer to the lead, which records it in the brief's decisions; **Later** puts one off until the next session;
286
+ - a call Claude Code's own permission check refused (its auto mode, a deny rule), which code-kit can't approve past: "Claude Code refused the lead's `pnpm db:generate`". **Put it in the prompt** puts a command in your prompt after `!` for you to run, **Done it** tells the lead you ran or allowed it, and **Not now** tells it to carry on with other work;
287
+ - with `harness.report.on`, a bug report for code-kit an agent drafted when code-kit got in its way: only code-kit's own facts (its version, the rule, what it said, the command), with the project's folders and email addresses taken out. **Read it** shows the exact text; **Send** files it on code-kit's GitHub with your own `gh` (a comment, if it's filed already; without `gh`, you get the link to file it), and **Discard** drops it;
284
288
  - 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
289
  - a finish check failing in this session: **Lanes** opens the pane;
286
290
  - a story using more than three times the median of the stories finished this session (once three have finished): **Lanes** opens the pane;
287
291
  - background agents paused, once the plan’s 5-hour use passes `harness.background.pauseAtPercent` (80% unless set).
288
292
 
289
- Each button has a digit: type it in an empty prompt to press it.
293
+ Each button has a digit: type it in an empty prompt to press it. The band keeps one line per kind ("2 approvals waiting"); the Decisions tab has a card for each decision.
290
294
 
291
295
  - **`/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
296
  - **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
- - **`/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 requests waiting and the approvals in force, with minutes left. **`/verify-branch`** runs `code-kit verify` on this branch. Neither calls the model.
297
+ - **`/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, the use at which background agents pause, and whether agents draft bug reports for code-kit. 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`.
298
+ - **`/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
299
 
296
300
  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
301
 
@@ -358,7 +362,12 @@ code-kit status [--base ref] [--json] # stories and requirements against the sp
358
362
  code-kit next [--base ref] [--json] # the step to take now, from the project's state
359
363
  code-kit trace <path>... [--json] # what a file is for: requirements, lane, layer rules
360
364
  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 the hooks refused, still open, and the approvals in force
365
+ code-kit requests [--json] # approval requests still waiting, the approvals in force, and every approval given
366
+ code-kit requests drop <n>[,<n>…]|all [--reason "…"] # stop waiting on requests nobody needs answered (the lead, or Not needed in the pane)
367
+ code-kit ask "<question>" [--story ST-n] [--blocks] [--why "…"] [--option "Label: what it does"]... # the lead asks you what it can't settle; it waits in your Decisions tab
368
+ code-kit ask [--json] | ask answer <id> "<answer>" | ask withdraw <id> --reason "…" # the questions; your answer (given in chat, recorded by the lead); one settled another way
369
+ code-kit report "<title>" --what "…" [--expected …] [--command …] [--refusal …] [--rule …] # with harness.report.on, an agent drafts a bug report for code-kit
370
+ code-kit report [--json] | report send|discard <id> # the drafts; sending files it on code-kit's GitHub with your gh, which only you do
362
371
  code-kit stops [--session id] [--json] # finish checks that refused an agent's last stop and still fail
363
372
  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
373
  code-kit settings [--json] # the harness settings in force, with their defaults
@@ -419,7 +428,7 @@ code-kit approve <name>... --reason "…" [--lane name] [--delegated | --via pan
419
428
  | `setup` | no | The version of init's setup the project has (3 now). Without it, or lower, `next` names what the project is missing (the design gate, design stories before screens, a QA lane, a security reviewer) and starts no new story until re-running init adds it |
420
429
  | `team` | no | Preview. For several people in one repository: `{ techLead, staleDays?, intents?, board?, provider? }`. `techLead` is the GitHub handle of the person who owns the plan, specs and contracts; `staleDays` is how many working days without a push make a claim stale (default 3); `intents` is the discussion category intents go in (default `Intents`; create it once in the repository's Settings › Discussions); `board` is `{ owner, number }`, a GitHub Project the plan is mirrored onto; `provider` is `"github"` (default) or `{ command, fallback? }` for another team provider, such as the code-kit teams hub; with `fallback: "github"`, claims go through GitHub whenever that provider can't be reached. See docs/specs/16-teams.md. |
421
430
  | `branches.protected` | no | Default `["main", "master"]` |
422
- | `harness` | no | What the mod's harness does in the session: `autonomy` (`autonomous`, `propose` or `off`; default `autonomous`), `hold.minutes` (how long a call waiting on your approval is held, 0 to 10; default 2), `stall.nudgeMinutes` and `stall.restartMinutes` (default 5 and 10), `stall.maxRestarts` (default 2), `agents.reviewer.on` (default `false`) and `agents.reviewer.model` (default the session's), and `background.pauseAtPercent` (default 80). Change them with `/harness` or `code-kit settings set`. |
431
+ | `harness` | no | What the mod's harness does in the session: `autonomy` (`autonomous`, `propose` or `off`; default `autonomous`), `hold.minutes` (how long a call waiting on your approval is held, 0 to 10; default 2), `stall.nudgeMinutes` and `stall.restartMinutes` (default 5 and 10), `stall.maxRestarts` (default 2), `agents.reviewer.on` (default `false`) and `agents.reviewer.model` (default the session's), `background.pauseAtPercent` (default 80) and `report.on` (agents draft bug reports for code-kit, which you send or discard; default `false`). Change them with `/harness` or `code-kit settings set`. |
423
432
 
424
433
  Add `.claude/approvals/`, `.claude/state/` and `.claude/code-kit.draft.json` to `.gitignore`, and `.claude/approval-log.jsonl merge=union` to `.gitattributes`, so merging branches that both logged changes keeps every entry. Commit `.claude/code-kit.baseline.json` if the project has one.
425
434
 
@@ -454,7 +463,7 @@ code-kit approve dep-dayjs --lane web --reason "Yes, add dayjs for ST-12"
454
463
 
455
464
  ### Requests
456
465
 
457
- Every refusal that a person's approval would allow is recorded as a request in `.claude/state/requests.jsonl` in the main checkout: who was refused, the approvals that would allow it, the command or file, and why. A request stays open until approvals for all its names are in force, or for 60 minutes. `code-kit requests` lists the open ones and the approvals in force, with minutes left, and the code-kit mod shows them above the prompt.
466
+ Every refusal that a person's approval would allow is recorded as a request in `.claude/state/requests.jsonl` in the main checkout: who was refused, the approvals that would allow it, the command or file, and why. A request stays open until approvals for all its names answer it, its file is committed by someone who could, or it's dropped (`code-kit requests drop`), and at most a week. `code-kit requests` lists the open ones, numbered, and the approvals in force, with minutes left, and the code-kit mod shows them above the prompt.
458
467
 
459
468
  `--via pane` and `merge --person` are the person's own acts, which the code-kit mod uses for a press in its pane. The hooks refuse both from every agent.
460
469
 
package/bin/code-kit.mjs CHANGED
@@ -20,6 +20,13 @@
20
20
  // merge <branch> --delegated|--person [--into branch] merge a branch that passes verify: the lead
21
21
  // (approvals.delegate.merge) or the person (the code-kit pane)
22
22
  // requests [--json] open approval requests and approvals in force
23
+ // requests drop <n>|all [--reason …] [--via pane] drop requests nobody needs answered any more
24
+ // ask "<question>" [--story ST-n] [--blocks] [--why …] [--option "Label: what it does"]...
25
+ // the lead asks the person what it can't settle; ask [--json] lists them
26
+ // ask answer <id> "<answer>" [--via pane] | ask withdraw <id> --reason …
27
+ // report "<title>" --what … [--expected …] [--command …] [--refusal …] [--rule …]
28
+ // with harness.report.on, an agent drafts a bug report for code-kit
29
+ // report [--json] | report send|discard <id> the person sends a draft to code-kit's GitHub, or not
23
30
  // stops [--session id] [--json] finish checks that refused an agent's last stop, still failing
24
31
  // sent-back <branch> [--reason …] records that a review sent the branch back at its current commit
25
32
  // settings [--json] | settings set <key> <value> --reason … [--via pane]
@@ -51,6 +58,13 @@ import { diffConfigs, ownershipMoves } from '../hooks/lib/diff.mjs';
51
58
  import { CODE, importsOf, layerOf, layerProblems, targetOf } from '../hooks/lib/layers.mjs';
52
59
  import { ADAPTERS, activeAdapters, adapterFacts } from '../hooks/lib/adapters/index.mjs';
53
60
  import { nextStep } from '../hooks/lib/next.mjs';
61
+ import { discardReport, draftReport, readReports, sendReport } from '../hooks/lib/report.mjs';
62
+ import {
63
+ answerQuestion,
64
+ askQuestion,
65
+ readQuestions,
66
+ withdrawQuestion,
67
+ } from '../hooks/lib/questions.mjs';
54
68
  import { traceFile } from '../hooks/lib/trace.mjs';
55
69
  import { buildStatus, specCheckRows } from '../hooks/lib/plan.mjs';
56
70
  import { recordSentBack } from '../hooks/lib/reviews.mjs';
@@ -80,9 +94,11 @@ import {
80
94
  approvalLifetime,
81
95
  currentBranch,
82
96
  PANE,
97
+ approvalHistory,
83
98
  approvalsInForce,
84
99
  delegatedMark,
85
100
  grantApprovals,
101
+ dropRequests,
86
102
  openRequests,
87
103
  ownerOf,
88
104
  reviewerOf,
@@ -125,6 +141,15 @@ const staleDaysGiven = option('--stale-days');
125
141
  const intentsGiven = option('--intents');
126
142
  const ownersGiven = [];
127
143
  for (let o = option('--owner'); o !== undefined; o = option('--owner')) ownersGiven.push(o);
144
+ const storyGiven = option('--story');
145
+ const whyGiven = option('--why');
146
+ const whatGiven = option('--what');
147
+ const expectedGiven = option('--expected');
148
+ const commandGiven = option('--command');
149
+ const refusalGiven = option('--refusal');
150
+ const ruleGiven = option('--rule');
151
+ const optionsGiven = [];
152
+ for (let o = option('--option'); o !== undefined; o = option('--option')) optionsGiven.push(o);
128
153
  const FLAGS = [
129
154
  '--write',
130
155
  '--no-checks',
@@ -134,6 +159,7 @@ const FLAGS = [
134
159
  '--fresh',
135
160
  '--peek',
136
161
  '--off',
162
+ '--blocks',
137
163
  ];
138
164
  const flag = (name) => args.includes(name);
139
165
  const writeBaseline = flag('--write');
@@ -910,19 +936,40 @@ function story(id) {
910
936
  // Open approval requests and the approvals in force, for the person and the mod's band.
911
937
  function requests() {
912
938
  load();
939
+ // CARD-7: a request nobody needs answered any more is dropped: the person's Dismiss, or the lead.
940
+ if (rest[0] === 'drop') {
941
+ if (!rest[1])
942
+ die('code-kit requests drop <n>|all [--reason "…"]: n as `code-kit requests` numbers them.');
943
+ const r = dropRequests(resolve('.'), rest[1], {
944
+ by: via === 'pane' ? 'person' : 'lead',
945
+ reason: reason ?? null,
946
+ });
947
+ if (!r.ok) die(r.why);
948
+ return out(
949
+ `Dropped ${r.dropped.length} request${r.dropped.length === 1 ? '' : 's'}: ${r.dropped.map((d) => `${d.names.join(', ')} for ${d.lane ? `the ${d.lane} lane` : d.actor}`).join('; ')}.`,
950
+ );
951
+ }
913
952
  const open = openRequests(resolve('.'));
914
953
  const granted = approvalsInForce(resolve('.'));
915
- if (flag('--json')) return out(JSON.stringify({ open, inForce: granted }, null, 2));
954
+ // CARD-6: every approval given, newest first.
955
+ const history = approvalHistory(resolve('.')).slice(0, 100);
956
+ if (flag('--json')) return out(JSON.stringify({ open, inForce: granted, history }, null, 2));
916
957
  out(open.length ? `${open.length} approval request(s) open:` : 'No approval requests open.');
917
- for (const r of open)
958
+ open.forEach((r, i) =>
918
959
  out(
919
- ` ${r.names.join(', ')} for ${r.lane ? `the ${r.lane} lane` : r.actor}: ${r.what} (${r.why})`,
920
- );
960
+ ` ${i + 1}. ${r.names.join(', ')} for ${r.lane ? `the ${r.lane} lane` : r.actor}: ${r.what} (${r.why}; asked ${r.at.slice(0, 16).replace('T', ' ')})`,
961
+ ),
962
+ );
921
963
  out(granted.length ? `\n${granted.length} approval(s) in force:` : '\nNo approvals in force.');
922
964
  for (const a of granted)
923
965
  out(
924
966
  ` ${a.name} for ${a.lane ? `the ${a.lane} lane` : 'any agent'}, ${a.minutesLeft} min left: ${a.reason}`,
925
967
  );
968
+ out(history.length ? `\nApprovals given (newest first):` : '\nNo approvals given yet.');
969
+ for (const h of history.slice(0, 30))
970
+ out(
971
+ ` ${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}`,
972
+ );
926
973
  }
927
974
 
928
975
  /**
@@ -1605,6 +1652,121 @@ function intendCommand() {
1605
1652
  );
1606
1653
  }
1607
1654
 
1655
+ /**
1656
+ * ASK-1 to ASK-3: questions for the person. `ask "<question>"` records one (the lead's: the hooks refuse
1657
+ * it from lanes), waiting in the Decisions tab; `--blocks` holds its story until it's answered. `ask`
1658
+ * lists them; `ask answer` records the answer (the person's from the pane with --via pane, which the
1659
+ * hooks refuse from agents; otherwise the lead recording what the person said in chat); `ask withdraw`
1660
+ * takes one back once it no longer needs the person.
1661
+ */
1662
+ function askCommand() {
1663
+ const root = resolve('.');
1664
+ const [sub, id, ...words] = rest;
1665
+ if (sub === 'answer') {
1666
+ const r = answerQuestion(root, id, {
1667
+ answer: words.join(' '),
1668
+ by: via === 'pane' ? 'person' : 'lead',
1669
+ via: via === 'pane' ? 'pane' : 'chat',
1670
+ });
1671
+ if (!r.ok) die(r.why);
1672
+ return out(
1673
+ `Recorded the answer to ${id}${via === 'pane' ? '' : ' (given in chat, recorded by the lead)'}: ${r.question.answer.text}${r.question.blocks ? ` ${r.question.story} no longer waits on the person.` : ''}`,
1674
+ );
1675
+ }
1676
+ if (sub === 'withdraw') {
1677
+ if (!reason?.trim()) die('Say why it no longer needs the person, with --reason "…".');
1678
+ const r = withdrawQuestion(root, id, reason);
1679
+ if (!r.ok) die(r.why);
1680
+ return out(`Withdrew ${id}: ${reason.trim()}`);
1681
+ }
1682
+ const all = readQuestions(root);
1683
+ if (!rest.length) {
1684
+ if (flag('--json')) return out(JSON.stringify(all, null, 2));
1685
+ const open = all.filter((q) => q.state === 'open');
1686
+ out(
1687
+ open.length
1688
+ ? `${open.length} question(s) wait for the person:`
1689
+ : 'No questions wait for the person.',
1690
+ );
1691
+ for (const q of open)
1692
+ out(
1693
+ ` ${q.id}${q.story ? ` (${q.story}${q.blocks ? ', which waits on it' : ''})` : ''}: ${q.question}${q.options.length ? ` [${q.options.map((o) => o.label).join(' / ')}]` : ''}`,
1694
+ );
1695
+ const answered = all.filter((q) => q.state === 'answered').slice(-10);
1696
+ if (answered.length) out('\nAnswered lately:');
1697
+ for (const q of answered) out(` ${q.id}: ${q.question} → ${q.answer.text}`);
1698
+ return;
1699
+ }
1700
+ const question = rest.join(' ').trim();
1701
+ if (flag('--blocks') && !storyGiven) die('--blocks holds a story: name it with --story ST-n.');
1702
+ const r = askQuestion(root, {
1703
+ by: 'lead',
1704
+ question,
1705
+ why: whyGiven ?? null,
1706
+ story: storyGiven ?? null,
1707
+ blocks: flag('--blocks'),
1708
+ options: optionsGiven,
1709
+ });
1710
+ out(
1711
+ r.existed
1712
+ ? `${r.question.id} is already open: ${r.question.question}`
1713
+ : `Asked ${r.question.id}. It waits in the person's Decisions tab${r.question.blocks ? `, and ${r.question.story} waits on it` : ''}; their answer is sent to you as a prompt. Carry on with other work meanwhile.`,
1714
+ );
1715
+ }
1716
+
1717
+ /**
1718
+ * REPORT-1 to REPORT-3: bug reports for code-kit itself. An agent drafts one (with harness.report.on);
1719
+ * the person reads it and sends or discards it, from the pane or their terminal: the hooks refuse
1720
+ * `report send` and `report discard` from every agent.
1721
+ */
1722
+ function reportCommand() {
1723
+ const root = resolve('.');
1724
+ const [sub, id] = rest;
1725
+ if (sub === 'send') {
1726
+ const r = sendReport(root, id);
1727
+ if (!r.ok) die(`${r.why}${r.link ? `\nOpen this to file it yourself: ${r.link}` : ''}`);
1728
+ return out(
1729
+ r.comment ? `Someone had filed it already; added it there: ${r.url}` : `Filed ${r.url}`,
1730
+ );
1731
+ }
1732
+ if (sub === 'discard') {
1733
+ const r = discardReport(root, id);
1734
+ if (!r.ok) die(r.why);
1735
+ return out(`Discarded ${id}.`);
1736
+ }
1737
+ if (!rest.length) {
1738
+ const all = readReports(root);
1739
+ if (flag('--json')) return out(JSON.stringify(all, null, 2));
1740
+ const drafts = all.filter((r) => r.state === 'draft');
1741
+ out(
1742
+ drafts.length
1743
+ ? `${drafts.length} report(s) wait for the person to send or discard:`
1744
+ : 'No reports waiting.',
1745
+ );
1746
+ for (const r of drafts) out(` ${r.id}: ${r.title}`);
1747
+ return;
1748
+ }
1749
+ const { config } = load();
1750
+ if (!config.harness.report.on)
1751
+ die(
1752
+ "Reporting code-kit's bugs is off (harness.report.on). Tell the person what went wrong instead: what you ran, what code-kit said, and what you expected.",
1753
+ );
1754
+ const r = draftReport(root, {
1755
+ title: rest.join(' '),
1756
+ what: whatGiven,
1757
+ expected: expectedGiven,
1758
+ command: commandGiven,
1759
+ refusal: refusalGiven,
1760
+ rule: ruleGiven,
1761
+ });
1762
+ if (!r.ok) die(r.why);
1763
+ out(
1764
+ r.existed
1765
+ ? `${r.report.id} already reports this; it waits for the person.`
1766
+ : `Drafted ${r.report.id}. The person reads it in the Decisions tab and sends it to code-kit's GitHub, or discards it. Carry on: work around it if you can, and say so.`,
1767
+ );
1768
+ }
1769
+
1608
1770
  /**
1609
1771
  * The lead loop's pause (LOOP-6): `loop` says whether the person paused it; `loop pause|resume`, the
1610
1772
  * person's act from the code-kit band, records it, so other tools' background work pauses with it.
@@ -1639,6 +1801,8 @@ const commands = {
1639
1801
  queue,
1640
1802
  team: teamCommand,
1641
1803
  intend: intendCommand,
1804
+ ask: askCommand,
1805
+ report: reportCommand,
1642
1806
  story: () => (rest.length === 1 ? story(rest[0]) : usage()),
1643
1807
  map: () => (rest.length === 1 ? mapCommand(rest[0]) : usage()),
1644
1808
  trail: trailCommand,
@@ -1664,7 +1828,11 @@ function usage() {
1664
1828
  ' | security [--base ref] [--branch name] [--json]\n' +
1665
1829
  ' | next [--base ref] [--json] | adapters | trace <path>... [--json]\n' +
1666
1830
  ' | approve <name>... --reason "…" [--lane name] [--config file]\n' +
1667
- ' | merge <branch> --delegated|--person [--into branch] | requests [--json]\n' +
1831
+ ' | merge <branch> --delegated|--person [--into branch] | requests [--json] | requests drop <n>|all\n' +
1832
+ ' | ask "<question>" [--story ST-n] [--blocks] [--why "…"] [--option "Label: effect"]... | ask [--json]\n' +
1833
+ ' | ask answer <id> "<answer>" | ask withdraw <id> --reason "…"\n' +
1834
+ ' | report "<title>" --what "…" [--expected …] [--command …] [--refusal …] [--rule …]\n' +
1835
+ ' | report [--json] | report send|discard <id>\n' +
1668
1836
  ' | stops [--session id] [--json] | sent-back <branch> [--reason "…"]\n' +
1669
1837
  ' | settings [--json] | settings set <key> <value> --reason "…" [--via pane]\n' +
1670
1838
  ' | hold <id> [--minutes N] | hold <id> --answer approved|refused\n' +
@@ -77,6 +77,8 @@ const approves = runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+approve\b`));
77
77
  const delegatedMerge = runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+(?:queue\s+)?merge\b`));
78
78
  const queues = runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+queue\s+(?:add|drop)\b`));
79
79
  const rulesOntoBase = runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+queue\s+rules\b`));
80
+ const asks = runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+ask\b\s+(?!--json\b)\S`));
81
+ const dropsRequests = runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+requests\s+drop\b`));
80
82
  const commits = runs(cmd, new RegExp(String.raw`^${GIT}commit\b`));
81
83
  if (error) {
82
84
  if (commits) block(`Blocked: ${error}\nFix the config before committing.`);
@@ -163,6 +165,21 @@ if ((delegatedMerge && /\s--person\b/.test(cmd)) || (approves && /\s--via\b/.tes
163
165
  block(
164
166
  `Blocked: merging or approving as the person is the person's own act, from the code-kit pane or their terminal. Ask them, and stop.\nCommand: ${cmd}`,
165
167
  );
168
+ // ASK-2, CARD-7: answering as the person, or dismissing their request, is theirs, from the pane.
169
+ if ((asks || dropsRequests) && /\s--via\b/.test(cmd))
170
+ block(
171
+ `Blocked: answering a question or dismissing a request in the person's name is their own act, from the code-kit pane or their terminal. Ask them, and stop.\nCommand: ${cmd}`,
172
+ );
173
+ // ASK-1: questions for the person, and taking back requests, go through the lead.
174
+ if ((asks || dropsRequests) && actor.kind !== 'lead')
175
+ block(
176
+ `Blocked: only the lead asks the person a question or drops an approval request. Write the question for the lead (what you need decided, the options and what each does), and stop.\nCommand: ${cmd}`,
177
+ );
178
+ // REPORT-3: sending a report to code-kit's GitHub, under the person's account, or discarding it, is theirs.
179
+ if (runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+report\s+(send|discard)\b`)))
180
+ block(
181
+ `Blocked: sending a bug report to code-kit's GitHub, or discarding it, is the person's own act, from the code-kit pane or their terminal. Tell them it's drafted, and carry on.\nCommand: ${cmd}`,
182
+ );
166
183
  // Pausing and resuming the lead loop is the person's, from the band or their terminal.
167
184
  if (runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+loop\s+(pause|resume)\b`)))
168
185
  block(
@@ -57,6 +57,12 @@ export const SETTINGS = {
57
57
  Number.isFinite(v) && v > 0 && v <= 100 ? null : 'must be a percentage from 1 to 100',
58
58
  about: "The plan's 5-hour use at which background agents pause",
59
59
  },
60
+ 'report.on': {
61
+ default: false,
62
+ problem: (v) => (typeof v === 'boolean' ? null : 'must be true or false'),
63
+ about:
64
+ "Agents draft a bug report for code-kit's GitHub when code-kit itself gets in their way; you read each one and send it, or not",
65
+ },
60
66
  };
61
67
 
62
68
  const get = (obj, key) =>
@@ -56,18 +56,21 @@ const hasMarkdown = (dir) => {
56
56
  };
57
57
 
58
58
  /**
59
- * { step, why, args, then, attention }. `step` is a skill (spec-design, init, check, dispatch,
59
+ * { step, why, args, then, attention, brief }. `brief`: the brief's open questions (ASK-4). `step` is a skill (spec-design, init, check, dispatch,
60
60
  * review), `merge` (the head of the merge queue, where the lead merges), `wait` (lanes are
61
61
  * building) or `done`. `base(config)` resolves the branch lanes merge into.
62
62
  */
63
63
  export function nextStep(root, base) {
64
64
  const attention = [];
65
+ // ASK-4: the brief's open questions, for the person's Decisions tab.
66
+ let briefQuestions = [];
65
67
  const result = (step, why, extra = {}) => ({
66
68
  step,
67
69
  why,
68
70
  args: '',
69
71
  then: [],
70
72
  attention,
73
+ brief: briefQuestions,
71
74
  ...extra,
72
75
  });
73
76
  const isRepo = git(root, 'rev-parse', '--is-inside-work-tree') === 'true';
@@ -82,8 +85,10 @@ export function nextStep(root, base) {
82
85
  `The config is invalid, so nothing but the config can be written: ${error.split('\n')[0]}`,
83
86
  );
84
87
  const brief = readBrief(root, config);
85
- if (brief?.openQuestions.length)
88
+ if (brief?.openQuestions.length) {
86
89
  attention.push(`${brief.openQuestions.length} open question(s) in ${brief.path}.`);
90
+ briefQuestions = brief.openQuestions.map((question) => ({ question, path: brief.path }));
91
+ }
87
92
 
88
93
  if (!config) {
89
94
  if (existsSync(join(root, DRAFT_FILE)))
@@ -187,6 +192,12 @@ export function nextStep(root, base) {
187
192
  const [review, building, blocked, fixes] = ['review', 'in progress', 'blocked', 'sent back'].map(
188
193
  ids,
189
194
  );
195
+ // ASK-3: a story whose question is open waits on the person; nothing dispatches or reviews it.
196
+ const waitingOnPerson = status.stories.filter((s) => s.state === 'waiting on you');
197
+ for (const s of waitingOnPerson)
198
+ attention.push(
199
+ `${s.id} waits on the person: ${s.waitingFor.question} (${s.waitingFor.id}, in their Decisions tab).`,
200
+ );
190
201
  // A ready story with a gap (unknown lane, a requirement no spec defines) waits for the plan's fix.
191
202
  const defined = new Set(status.requirements.map((r) => r.id));
192
203
  const buildable = (s) =>
@@ -302,6 +313,12 @@ export function nextStep(root, base) {
302
313
  'spec-design',
303
314
  `${unbuildable.join(', ')} ${unbuildable.length === 1 ? 'is' : 'are'} next, but the plan has gaps to fix first.`,
304
315
  );
316
+ // ASK-3: what's left waits on the person's answers, directly or through a dependency.
317
+ if (waitingOnPerson.length)
318
+ return result(
319
+ 'wait',
320
+ `${waitingOnPerson.map((s) => `${s.id} waits on the person (${s.waitingFor.id})`).join('; ')}${blocked.length ? `; ${blocked.join(', ')} ${blocked.length === 1 ? 'waits' : 'wait'} on ${waitingOnPerson.length === 1 ? 'it' : 'them'}` : ''}. Carry on with anything else; their answer is sent to you.`,
321
+ );
305
322
  if (blocked.length)
306
323
  return result(
307
324
  'spec-design',
@@ -7,6 +7,7 @@
7
7
  import { execFileSync } from 'node:child_process';
8
8
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
9
9
  import { join } from 'node:path';
10
+ import { waitingStories } from './questions.mjs';
10
11
  import { sentBack } from './reviews.mjs';
11
12
 
12
13
  export const REQUIREMENT = /\b(?!ST-)([A-Z][A-Z0-9]*-\d+)\b/g;
@@ -138,7 +139,8 @@ function worktrees(root) {
138
139
  * `base` is in review; one level with it is in progress, or done once the plan had it in review.
139
140
  * A branch still at the commit a review sent back is `sent back`, waiting for its lane's fix.
140
141
  * Without a branch, the plan's status stands. A story is `blocked` while a story it depends on
141
- * isn't done, whatever its branch holds; a todo story whose dependencies are done is `ready`.
142
+ * isn't done, whatever its branch holds; a todo story whose dependencies are done is `ready`. A story
143
+ * with an open question for the person that holds it (`code-kit ask --blocks`) is `waiting on you`.
142
144
  */
143
145
  export function storyStates(root, stories, base) {
144
146
  const trees = worktrees(root);
@@ -165,10 +167,16 @@ export function storyStates(root, stories, base) {
165
167
  };
166
168
  });
167
169
  const done = new Set(states.filter((s) => s.state === 'done').map((s) => s.id));
170
+ // ASK-3: a story whose question for the person is open waits on them, whatever its branch holds.
171
+ const asked = waitingStories(root);
168
172
  for (const s of states) {
169
173
  if (s.state === 'done') continue;
170
174
  s.waitingOn = s.dependsOn.filter((d) => !done.has(d));
171
- if (s.waitingOn.length) s.state = 'blocked';
175
+ s.waitingFor = asked.has(s.id)
176
+ ? { id: asked.get(s.id).id, question: asked.get(s.id).question }
177
+ : null;
178
+ if (s.waitingFor) s.state = 'waiting on you';
179
+ else if (s.waitingOn.length) s.state = 'blocked';
172
180
  else if (s.state === 'todo') s.state = 'ready';
173
181
  }
174
182
  return states;
@@ -216,7 +224,11 @@ export function buildStatus(root, config, base, files) {
216
224
  let state = 'no story';
217
225
  if (r.removed) state = 'removed';
218
226
  else if (covering.length && covering.every((s) => s.state === 'done')) state = 'done';
219
- else if (covering.some((s) => ['in progress', 'review', 'sent back', 'done'].includes(s.state)))
227
+ else if (
228
+ covering.some((s) =>
229
+ ['in progress', 'review', 'sent back', 'waiting on you', 'done'].includes(s.state),
230
+ )
231
+ )
220
232
  state = 'in progress';
221
233
  else if (covering.length) state = 'todo';
222
234
  return { ...r, state, stories: covering.map((s) => s.id), tests: tested[r.id] ?? [] };
@@ -224,7 +236,7 @@ export function buildStatus(root, config, base, files) {
224
236
  const drift = stories
225
237
  .filter(
226
238
  (s) =>
227
- !['ready', 'blocked'].includes(s.state) &&
239
+ !['ready', 'blocked', 'waiting on you'].includes(s.state) &&
228
240
  s.state !== s.status &&
229
241
  !(s.state === 'sent back' && s.status === 'in progress'),
230
242
  )