@warren-dean/code-kit 0.18.0 → 0.20.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.18.0",
12
+ "version": "0.20.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.18.0",
3
+ "version": "0.20.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,29 @@
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.20.0 (2026-10-21)
6
+
7
+ A team provider can now receive a report of a project's facts, and a hosted provider's adapter is in preview.
8
+
9
+ - A team provider can receive a report of the project: each story's stage and when it changed, milestones and their targets, send-backs, the requirement trail, approvals, token use by kind of agent, and the project's rules. A report never holds code, diffs or prompts; its fields are a fixed list, and anything else is refused before it's sent. With GitHub as the provider, nothing is sent anywhere.
10
+ - `code-kit team report --dry-run` shows exactly what would be sent, and `team.report: false` turns reports off.
11
+ - A milestone in the plan can have a target date: `**Target:** 2026-11-30` under its heading.
12
+ - A hosted provider's adapter is in preview. It runs only for a project whose config names one, connects through code-kit's own hooks with GitHub as its fallback, and doesn't need a Discussions category for intents.
13
+ - A decision record now names whoever agreed the intent, not always the tech lead.
14
+
15
+ ## 0.19.0 (2026-10-19)
16
+
17
+ A build uses far fewer tokens. In two days of two builds, output was under 1% of the tokens: nearly all of it was agents rereading large contexts on every request.
18
+
19
+ - Each lane agent builds one story. A send-back, or a story carried on, goes to a fresh agent, never to the one that built it. Lane agents had reached 900k to 970k tokens of context over hundreds of requests.
20
+ - A lane agent past `agents.lanes.contextLimit` (250,000 tokens by default) is told to finish its step, commit, write its report and hand back. The loop then has the lead carry the story on with a fresh agent that reads that report first.
21
+ - `code-kit worktrees` lists the project's worktrees. `worktrees release <branch>` frees one for a fresh agent, keeping the branch's commits and moving its report to the main checkout, and `worktrees prune` removes every one that's merged and clean. One project had 11 such leftovers, about 4.5 GB; `code-kit next` now points them out.
22
+ - The lead's context is compacted between turns once it's past `compact.at` (40% of its window by default), without asking. Waiting for a milestone wasn't enough: one lead reached 968k tokens first.
23
+ - The loop reads the lead's replies with Sonnet, from the reply itself. Before, it forked the lead's whole conversation on the lead's own model at the end of every turn.
24
+ - Lane agents read every file they change in full, but of its imports and callers only what the change needs; a fresh Context Graph card counts as reading a file. They run the narrowest tests with failures only in the output, and the full suite once before committing. The lead's review starts from the lane's report, the reviewer's findings and the diff's summary.
25
+ - `agents.reviewer.model` now defaults to Sonnet and covers the security reviewer too, unless its agent file names a model.
26
+ - The lane rules changed, so `code-kit next` asks the lead to bring code-kit's instructions up to date.
27
+
5
28
  ## 0.18.0 (2026-10-17)
6
29
 
7
30
  code-kit's instructions to your agents now keep up with code-kit, it tells you when something needs you, and the fixes found by checking two live builds.
package/README.md CHANGED
@@ -337,6 +337,7 @@ The loop keeps the build moving between your prompts, under `harness.autonomy`.
337
337
  - **Stories waiting on other work.** When the lead says a story isn't due yet because it waits on work still going (a paired story a lane is building, a rebuild, a check running), the loop parks that story and leaves it out of every step, so a step naming it with others prompts only the others. It wakes when its own branch or state moves, or when a story it waits on (one the lead named, or an unfinished one building on it) changes state; that story's commits along the way don't wake it, nor do other lanes'. A story the lead's turn left exactly where it stood is parked too, whatever the reply said. After 30 minutes with nothing moving, the lead is asked once more.
338
338
  - **A step that doesn't move.** One the lead was prompted with twice in a row that is still next goes to you instead: "Merge lead/st-1: still next after 2 prompts. Ask the lead again?" with **Ask again**.
339
339
  - **A lean lead.** A lane writes its full report, with its commands and their output, to `.claude/state/reports/<branch>.md`, and hands the lead a few lines naming it; the stop hook won't let a lane on a story branch finish without it, or with a hand-back of more than ten lines, and the lead reads the file when it reviews. Lanes don't message the lead while they work. When a milestone finishes, the lead's context is compacted before the next one (`harness.compact.milestone`: you're asked by default, or it's done at once): the summary keeps what's in flight and what waits on you, and drops the finished stories' detail.
340
+ - **Token use.** A long-lived agent rereads its whole context on every request, which is most of what a build costs. So each lane agent builds one story; one past `agents.lanes.contextLimit` is told to wrap up, and the lead carries its story on with a fresh agent that reads its report (`code-kit worktrees release` frees the branch). The lead's context is compacted between turns once it's past `compact.at`. The loop reads the lead's replies with Sonnet, and the reviewers run on Sonnet by default.
340
341
  - **Designs.** A design story is handed to the lead once a session; while you and the lead design it, the loop leaves it to you.
341
342
  - **One session runs it.** The first session whose loop acts runs it for the project. Another session you open there, for any reason, never prompts its lead: its band reads "the loop runs in another session", with **Lead from here** to move the loop to it.
342
343
  - **Modes.** **Pause** and **Resume** stop and start it. Under `propose`, each step waits for **Go**; under `off`, the loop does nothing. When every story is done, the band says so and the loop stops.
@@ -436,6 +437,7 @@ code-kit verify [--base ref] [--branch name] [--no-checks] [--json] # a branch'
436
437
  code-kit security [--base ref] [--branch name] [--json] # whether a branch needs the security review, and why: sign-in, permissions, outside input, personal data, payments, secrets, dependencies
437
438
  code-kit status [--base ref] [--json] # stories and requirements against the spec
438
439
  code-kit next [--base ref] [--json] # the step to take now, from the project's state
440
+ code-kit worktrees [--json] | worktrees release <branch> | worktrees prune # the project's worktrees; free one for a fresh agent on its story, or remove every merged and clean one, keeping their reports
439
441
  code-kit instructions [--write] [--json] | instructions show lead|lane # code-kit's part of CLAUDE.md and the lane agents: where each stands, brought up to date, or printed
440
442
  code-kit trace <path>... [--json] # what a file is for: requirements, lane, layer rules
441
443
  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
@@ -466,6 +468,9 @@ code-kit team claim <story> [--lease globs] | team release <story> # claim a s
466
468
  code-kit team keep <story> | team reassign <story> <handle> # keep your stale claim (its clock starts again); the tech lead hands a claim to someone else
467
469
  code-kit team codeowners [--write] # .github/CODEOWNERS from the config: the tech lead for their paths, each lane's owners for its paths
468
470
  code-kit team board # mirror the plan onto the team's GitHub Project: each story with Lane, Claimed by, Stale and State
471
+ code-kit team report [--dry-run] [--json] # report the project's facts to the team provider; --dry-run prints exactly what would be sent (spec 21)
472
+ code-kit team provider signin|signout|status|connect # a hosted team provider the config names (preview): sign this machine in, connect the repository
473
+ code-kit team pulse --session <id> # used by the mod with a hosted provider: the session's agents and token use, on stdin, and the report when something changed
469
474
  code-kit intend "<what and why>" [--touches globs] # raise a change of direction before any code, as a discussion in the repository's Intents category
470
475
  code-kit intend agree|drop|duplicate <id> [--reason "…"] # the tech lead's answer, from their terminal or the pane
471
476
  code-kit intend record <id> # write an agreed intent as a numbered decision record in docs.decisions, for a pull request
@@ -482,33 +487,33 @@ code-kit approve <name>... --reason "…" [--lane name] [--delegated [--story ST
482
487
 
483
488
  `.claude/code-kit.json`. Globs use `*` (one segment), `**` (any depth), `**/` (zero or more folders), `?` and `{a,b}`. Patterns under `shell` are regular expressions.
484
489
 
485
- | Field | Required | What it is |
486
- | ------------------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
487
- | `version` | yes | `1` |
488
- | `docs.specs` | no | Folder of specs. With it, a lane needs a spec-check report before writing code on a branch |
489
- | `docs.principles`, `docs.plan` | no | The engineering principles and the implementation plan |
490
- | `docs.decisions` | no | Where agreed intents are recorded as numbered decision records (`code-kit intend record`); default `docs/decisions` |
491
- | `lead.paths` | yes | What the main session writes. `.claude/**` is always added. |
492
- | `lead.outside` | no | Paths outside the repository the lead may write, e.g. `../shared-lib/**`. Claude's memory and scratchpad are always included. |
493
- | `anyActor` | no | Paths every writer may change (e.g. the lockfile), read-only agents included. `.claude/state/**` is always included. An entry may be `{ "glob", "reviewer" }` to name the lane (or `"lead"`) that reviews changes to it; otherwise the lead reviews them, and `who` and `trace` say so. |
494
- | `lanes.<name>` | yes (may be `{}`) | `{ agent, paths, exclude?, owners? }`: the agent name maps a subagent to its lane; `owners` (GitHub handles) are the people whose sessions work it, in a team (needs `team`). A worktree with a `.lane` file holding the lane name makes the main session that lane. |
495
- | `protected[]` | no | `{ glob, approval, why }`: logged on every commit. Anyone but the lead needs an approval: the lead's where `approvals.delegate.protected` names it, otherwise a person's (see [Approvals](#approvals)). `.claude/**` is always protected as `kit`. |
496
- | `designGate` | no | `{ register, screens: [{ glob, not? }], howTo }`: screen files wait until the register (`{ "screens": [{ "file": … }] }`) lists them. Init sets it for every lane that owns screens; `/code-kit:design` adds approved designs |
497
- | `layers[]` | no | `{ name, paths, mayImport, denyPackages? }`: the first layer whose paths match a file owns it. A layer may always import itself. |
498
- | `importAliases` | no | `{ "@app/domain": "packages/domain/src" }`: resolves non-relative imports to repository paths |
499
- | `shell.block[]` | no | `{ pattern, why, person?, command? }`: never run. `person: true` marks a person's act, which the lead may do for them with `approvals.lead` on. `command: true` matches the pattern from the start of each command the line runs, so a heredoc or quoted text that mentions it isn't refused |
500
- | `shell.restricted[]` | no | `{ pattern, lanes, why }`: only the lead and these lanes |
501
- | `postEdit[]` | no | `{ files, exclude?, run }`: after each edit of a matching file. `{file}` is the file. Exit 127 (tool not installed) is skipped. |
502
- | `tests[]` | no | Globs naming the test files, for the weakened-test and fix rules. Default: the usual conventions (`*.test.*`, `*.spec.*`, `__tests__/`, `test/`, `tests/`, `e2e/`, `*_test.go`, `test_*.py`). |
503
- | `regression.prepare` | no | A command run in a fresh checkout before a fix's tests run there without the fix, such as `npm ci`. Without it, the checkout's `node_modules` and virtualenvs are linked in. |
504
- | `checks[]` | no | `{ name, files, exclude?, run[], each?, owners?, ifMissing?, timeoutSeconds? }`: before finishing, when matching files changed. `each` runs the commands in every changed folder matching that glob. `owners` limits who may change the files (the lead always may). `ifMissing: "fail"` makes a missing tool an error (the default is to skip the check, which `verify` and a merge list as not run). |
505
- | `adapters` | no | `{ "<adapter>": false }` switches off an adapter that would otherwise be detected; `true` says the project wants that tool, so until it's set up here `next` gives its setup before any code. See [Adapters](#adapters). |
506
- | `approvals.lead` | no | `true` lets the lead act on what the person says in chat, for when `!` isn't available (on a phone, say): it records their approvals with `code-kit approve`, and runs `shell.block` commands marked `person`. Default `false`. See [Approvals](#approvals). |
507
- | `approvals.delegate` | no | `{ protected?: [approval names], merge?: true, dependencies?: { licences?, minWeeklyDownloads?, maxMonthsSinceRelease?, allowInstallScripts?: true \| false \| [kinds] } }`: what the lead decides, inside the guardrails you keep: the protected approvals it grants, and packages it approves for a story, with `code-kit approve --delegated`; with `merge`, merges into a protected branch with `code-kit merge --delegated`. Lanes ask the lead for these, never you. npm packages are also checked against the registry; the defaults are MIT, Apache-2.0, BSD and ISC, 10,000 weekly downloads, a release within 18 months, and no install scripts. See [Approvals](#approvals). |
508
- | `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 |
509
- | `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. |
510
- | `branches.protected` | no | Default `["main", "master"]` |
511
- | `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), `agents.lanes.model` (the model each lane agent starts on, unless the lead's call or the lane's agent file names one; default the session's), `background.pauseAtPercent` (default 80), `compact.milestone` (when a milestone finishes, compact the lead's context: `ask`, `auto` or `off`; default `ask`), `notify.on` (a system notification when something new needs you; default `true`) 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`. |
490
+ | Field | Required | What it is |
491
+ | ------------------------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
492
+ | `version` | yes | `1` |
493
+ | `docs.specs` | no | Folder of specs. With it, a lane needs a spec-check report before writing code on a branch |
494
+ | `docs.principles`, `docs.plan` | no | The engineering principles and the implementation plan |
495
+ | `docs.decisions` | no | Where agreed intents are recorded as numbered decision records (`code-kit intend record`); default `docs/decisions` |
496
+ | `lead.paths` | yes | What the main session writes. `.claude/**` is always added. |
497
+ | `lead.outside` | no | Paths outside the repository the lead may write, e.g. `../shared-lib/**`. Claude's memory and scratchpad are always included. |
498
+ | `anyActor` | no | Paths every writer may change (e.g. the lockfile), read-only agents included. `.claude/state/**` is always included. An entry may be `{ "glob", "reviewer" }` to name the lane (or `"lead"`) that reviews changes to it; otherwise the lead reviews them, and `who` and `trace` say so. |
499
+ | `lanes.<name>` | yes (may be `{}`) | `{ agent, paths, exclude?, owners? }`: the agent name maps a subagent to its lane; `owners` (GitHub handles) are the people whose sessions work it, in a team (needs `team`). A worktree with a `.lane` file holding the lane name makes the main session that lane. |
500
+ | `protected[]` | no | `{ glob, approval, why }`: logged on every commit. Anyone but the lead needs an approval: the lead's where `approvals.delegate.protected` names it, otherwise a person's (see [Approvals](#approvals)). `.claude/**` is always protected as `kit`. |
501
+ | `designGate` | no | `{ register, screens: [{ glob, not? }], howTo }`: screen files wait until the register (`{ "screens": [{ "file": … }] }`) lists them. Init sets it for every lane that owns screens; `/code-kit:design` adds approved designs |
502
+ | `layers[]` | no | `{ name, paths, mayImport, denyPackages? }`: the first layer whose paths match a file owns it. A layer may always import itself. |
503
+ | `importAliases` | no | `{ "@app/domain": "packages/domain/src" }`: resolves non-relative imports to repository paths |
504
+ | `shell.block[]` | no | `{ pattern, why, person?, command? }`: never run. `person: true` marks a person's act, which the lead may do for them with `approvals.lead` on. `command: true` matches the pattern from the start of each command the line runs, so a heredoc or quoted text that mentions it isn't refused |
505
+ | `shell.restricted[]` | no | `{ pattern, lanes, why }`: only the lead and these lanes |
506
+ | `postEdit[]` | no | `{ files, exclude?, run }`: after each edit of a matching file. `{file}` is the file. Exit 127 (tool not installed) is skipped. |
507
+ | `tests[]` | no | Globs naming the test files, for the weakened-test and fix rules. Default: the usual conventions (`*.test.*`, `*.spec.*`, `__tests__/`, `test/`, `tests/`, `e2e/`, `*_test.go`, `test_*.py`). |
508
+ | `regression.prepare` | no | A command run in a fresh checkout before a fix's tests run there without the fix, such as `npm ci`. Without it, the checkout's `node_modules` and virtualenvs are linked in. |
509
+ | `checks[]` | no | `{ name, files, exclude?, run[], each?, owners?, ifMissing?, timeoutSeconds? }`: before finishing, when matching files changed. `each` runs the commands in every changed folder matching that glob. `owners` limits who may change the files (the lead always may). `ifMissing: "fail"` makes a missing tool an error (the default is to skip the check, which `verify` and a merge list as not run). |
510
+ | `adapters` | no | `{ "<adapter>": false }` switches off an adapter that would otherwise be detected; `true` says the project wants that tool, so until it's set up here `next` gives its setup before any code. See [Adapters](#adapters). |
511
+ | `approvals.lead` | no | `true` lets the lead act on what the person says in chat, for when `!` isn't available (on a phone, say): it records their approvals with `code-kit approve`, and runs `shell.block` commands marked `person`. Default `false`. See [Approvals](#approvals). |
512
+ | `approvals.delegate` | no | `{ protected?: [approval names], merge?: true, dependencies?: { licences?, minWeeklyDownloads?, maxMonthsSinceRelease?, allowInstallScripts?: true \| false \| [kinds] } }`: what the lead decides, inside the guardrails you keep: the protected approvals it grants, and packages it approves for a story, with `code-kit approve --delegated`; with `merge`, merges into a protected branch with `code-kit merge --delegated`. Lanes ask the lead for these, never you. npm packages are also checked against the registry; the defaults are MIT, Apache-2.0, BSD and ISC, 10,000 weekly downloads, a release within 18 months, and no install scripts. See [Approvals](#approvals). |
513
+ | `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 |
514
+ | `team` | no | Preview. For several people in one repository: `{ techLead, staleDays?, intents?, board?, provider?, report? }`. `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), `{ command, fallback? }` for another team provider, or, in preview, a hosted provider's adapter as `{ "<name>": "<https address>" }`, which keeps the intents (Discussions only when `intents` is set) and has GitHub as its fallback; with `fallback: "github"`, claims go through GitHub whenever that provider can't be reached; `report` (default true) is whether the project reports its facts (stories' stages, send-backs, the requirement trail, approvals, token use and the rules; never code) to a provider that isn't GitHub. See docs/specs/16-teams.md and docs/specs/21-hub-adapter-and-reports.md. |
515
+ | `branches.protected` | no | Default `["main", "master"]` |
516
+ | `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` (code-kit's reviewer and the security reviewer; default `sonnet`), `agents.lanes.model` (the model each lane agent starts on, unless the lead's call or the lane's agent file names one; default the session's), `background.pauseAtPercent` (default 80), `compact.at` (compact the lead's context between turns once it's past this share of its window; default 40, 0 for never), `compact.milestone` (when a milestone finishes, compact the lead's context: `ask`, `auto` or `off`; default `ask`), `agents.lanes.contextLimit` (the context a lane agent may reach before it hands its story to a fresh agent; default 250,000 tokens), `notify.on` (a system notification when something new needs you; default `true`) 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`. |
512
517
 
513
518
  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.
514
519
 
package/bin/code-kit.mjs CHANGED
@@ -33,8 +33,9 @@
33
33
  // the harness settings; set is the person's own change
34
34
  // record approvals a person gave in chat (with approvals.lead on, the lead runs it)
35
35
  // --config <file> reads a draft (.claude/code-kit.draft.json) instead of .claude/code-kit.json.
36
- import { execFileSync } from 'node:child_process';
36
+ import { execFileSync, spawnSync } from 'node:child_process';
37
37
  import {
38
+ copyFileSync,
38
39
  existsSync,
39
40
  mkdirSync,
40
41
  mkdtempSync,
@@ -56,7 +57,20 @@ import {
56
57
  } from '../hooks/lib/config.mjs';
57
58
  import { diffConfigs, ownershipMoves } from '../hooks/lib/diff.mjs';
58
59
  import { CODE, importsOf, layerOf, layerProblems, targetOf } from '../hooks/lib/layers.mjs';
59
- import { ADAPTERS, activeAdapters, adapterFacts } from '../hooks/lib/adapters/index.mjs';
60
+ import {
61
+ ADAPTERS,
62
+ activeAdapters,
63
+ adapterFacts,
64
+ providerAdapterOf,
65
+ } from '../hooks/lib/adapters/index.mjs';
66
+ import {
67
+ EVENT_PARTS,
68
+ USAGE_DIR,
69
+ buildSnapshot,
70
+ mainCheckout,
71
+ snapshotHash,
72
+ snapshotProblems,
73
+ } from '../hooks/lib/snapshot.mjs';
60
74
  import { nextStep } from '../hooks/lib/next.mjs';
61
75
  import { discardReport, draftReport, readReports, sendReport } from '../hooks/lib/report.mjs';
62
76
  import {
@@ -91,6 +105,7 @@ import {
91
105
  claimChanges,
92
106
  storyOfBranch,
93
107
  syncBoard,
108
+ readCache,
94
109
  teamOf,
95
110
  teamProvider,
96
111
  whoami,
@@ -171,6 +186,7 @@ const FLAGS = [
171
186
  '--off',
172
187
  '--blocks',
173
188
  '--take',
189
+ '--dry-run',
174
190
  ];
175
191
  const flag = (name) => args.includes(name);
176
192
  const writeBaseline = flag('--write');
@@ -241,7 +257,7 @@ function check() {
241
257
  if (team) {
242
258
  const me = whoami(resolve('.'));
243
259
  out(
244
- `Team: ${team.techLead} is the tech lead; a claim is stale after ${team.staleDays} working day${team.staleDays === 1 ? '' : 's'} without a push; provider ${team.provider === 'github' ? 'GitHub' : team.provider.command}.`,
260
+ `Team: ${team.techLead} is the tech lead; a claim is stale after ${team.staleDays} working day${team.staleDays === 1 ? '' : 's'} without a push; provider ${team.provider === 'github' ? 'GitHub' : (team.provider.name ?? team.provider.command)}.`,
245
261
  );
246
262
  for (const [lane, owners] of Object.entries(team.owners))
247
263
  out(`Lane ${lane} is owned by ${owners.join(', ')}`);
@@ -1460,8 +1476,12 @@ function teamCommand() {
1460
1476
  die(
1461
1477
  'This project has no "team" in its config, so it has no claims or owners. Add "team": { "techLead": "<GitHub handle>" } to share them.',
1462
1478
  );
1479
+ // HUBR-6: a hosted provider's own commands (sign in, connect, status), through its adapter.
1480
+ if (rest[0] === 'provider') return providerCommand(raw);
1463
1481
  const provider = teamProvider(resolve('.'), team);
1464
1482
  const [sub, story] = rest;
1483
+ if (sub === 'report') return teamReport(team, provider);
1484
+ if (sub === 'pulse') return pulseCommand(raw, team, provider);
1465
1485
  const holderWords = (h) =>
1466
1486
  `${h.story} is claimed by ${h.by} (${ago(h.at)}). Ask them, or the tech lead (${team.techLead}), to release it.`;
1467
1487
  if (sub === 'claim') {
@@ -1576,7 +1596,7 @@ function teamCommand() {
1576
1596
  }
1577
1597
  if (sub !== undefined)
1578
1598
  die(
1579
- 'code-kit team [--json] [--fresh] | team init … | team since [--json] [--peek] | team claim|keep|release <story> | team reassign <story> <handle>',
1599
+ 'code-kit team [--json] [--fresh] | team init … | team since [--json] [--peek] | team claim|keep|release <story> | team reassign <story> <handle> | team report [--dry-run] [--json]',
1580
1600
  );
1581
1601
  const state = provider.state({ fresh: flag('--fresh') });
1582
1602
  if (flag('--json')) return out(JSON.stringify(state, null, 2));
@@ -1607,6 +1627,128 @@ function teamCommand() {
1607
1627
  out(` #${p.number} ${p.title} (${p.by}, ${p.branch}${p.draft ? ', draft' : ''})`);
1608
1628
  }
1609
1629
 
1630
+ // --- what a project reports (docs/specs/21-hub-adapter-and-reports.md) ----------------------------
1631
+
1632
+ const REPORT_SENT = '.claude/state/report-sent.json';
1633
+ const REPORT_MINUTES = 10;
1634
+
1635
+ /** HUBR-2: the snapshot, from what status, timeline, trail, the journal and the config already know. */
1636
+ function snapshotNow(team) {
1637
+ const { config } = load();
1638
+ if (!config.docs?.specs || !config.docs?.plan) return null;
1639
+ const from = base(config, false);
1640
+ const status = buildStatus('.', config, from, tracked());
1641
+ const claims = (readCache(resolve('.')) ?? {}).claims ?? [];
1642
+ return buildSnapshot({
1643
+ root: resolve('.'),
1644
+ config,
1645
+ status,
1646
+ base: from,
1647
+ claims,
1648
+ version: JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version,
1649
+ });
1650
+ }
1651
+
1652
+ /**
1653
+ * HUBR-3: sends the snapshot when what happened changed (stages, approvals, send-backs, merges), or when
1654
+ * only usage changed and ten minutes have passed; never the same snapshot twice. A provider that can't
1655
+ * be reached is tried again with the next change. `{ sent, why }`.
1656
+ */
1657
+ function sendSnapshot(team, provider, snapshot, { force = false } = {}) {
1658
+ if (team.provider === 'github') return { sent: false, why: 'GitHub keeps no reports.' };
1659
+ if (!team.report) return { sent: false, why: 'team.report is false.' };
1660
+ const problems = snapshotProblems(snapshot);
1661
+ if (problems.length) return { sent: false, why: `The report isn't valid: ${problems[0]}.` };
1662
+ const file = join(mainCheckout(resolve('.')), REPORT_SENT);
1663
+ let last = {};
1664
+ try {
1665
+ last = JSON.parse(readFileSync(file, 'utf8'));
1666
+ } catch {
1667
+ // nothing sent yet
1668
+ }
1669
+ const events = snapshotHash(snapshot, EVENT_PARTS);
1670
+ const all = snapshotHash(snapshot);
1671
+ const due =
1672
+ force ||
1673
+ events !== last.events ||
1674
+ (all !== last.all && Date.now() - (last.at ?? 0) >= REPORT_MINUTES * 60_000);
1675
+ if (!due) return { sent: false, why: 'Nothing new to report.' };
1676
+ const r = provider.report(snapshot);
1677
+ if (!r || r.ok === false)
1678
+ return { sent: false, why: r?.why ?? 'The provider refused the report.' };
1679
+ mkdirSync(join(mainCheckout(resolve('.')), '.claude/state'), { recursive: true });
1680
+ writeFileSync(file, `${JSON.stringify({ events, all, at: Date.now() })}\n`);
1681
+ return { sent: true };
1682
+ }
1683
+
1684
+ /** HUBR-2, HUBR-4: `team report` sends what's new; `--dry-run` prints exactly what would be sent. */
1685
+ function teamReport(team, provider) {
1686
+ const snapshot = snapshotNow(team);
1687
+ if (!snapshot)
1688
+ die(
1689
+ 'A report needs docs.specs and docs.plan in the config. The spec-design skill writes both.',
1690
+ );
1691
+ if (flag('--dry-run')) return out(JSON.stringify(snapshot, null, 2));
1692
+ const r = sendSnapshot(team, provider, snapshot, { force: true });
1693
+ if (flag('--json')) return out(JSON.stringify(r, null, 2));
1694
+ out(
1695
+ r.sent
1696
+ ? `Reported this project's facts to ${team.provider.name ?? 'the team provider'}.`
1697
+ : r.why,
1698
+ );
1699
+ }
1700
+
1701
+ /**
1702
+ * The mod's pulse (HUBR-3, HUBR-6), every 30 seconds while a session runs, only for a hosted provider:
1703
+ * stdin is `{ agents, usage }`, the session's agents by tool and branch and its token ledger (USE-1).
1704
+ * Keeps both for the provider's client and the report, and reports when something new happened.
1705
+ */
1706
+ async function pulseCommand(raw, team, provider) {
1707
+ const hosted = providerAdapterOf(raw.team?.provider);
1708
+ if (!hosted || hosted.problem || !sessionId) return;
1709
+ let input = {};
1710
+ try {
1711
+ let raw = '';
1712
+ for await (const chunk of process.stdin) raw += chunk;
1713
+ input = raw.trim() ? JSON.parse(raw) : {};
1714
+ } catch {
1715
+ input = {};
1716
+ }
1717
+ const root = resolve('.');
1718
+ const id = sessionId.replace(/[^\w-]/g, '');
1719
+ if (input.agents) hosted.adapter.presence(root, id, input.agents);
1720
+ if (input.usage && typeof input.usage === 'object') {
1721
+ const dir = join(mainCheckout(root), USAGE_DIR);
1722
+ mkdirSync(dir, { recursive: true });
1723
+ writeFileSync(
1724
+ join(dir, `${id}.json`),
1725
+ `${JSON.stringify({ at: Date.now(), entries: input.usage })}\n`,
1726
+ );
1727
+ }
1728
+ // Building the snapshot reads git: at most once a minute.
1729
+ const built = join(mainCheckout(root), '.claude/state/report-built.json');
1730
+ let at = 0;
1731
+ try {
1732
+ at = JSON.parse(readFileSync(built, 'utf8')).at ?? 0;
1733
+ } catch {
1734
+ // never built
1735
+ }
1736
+ if (Date.now() - at < 60_000) return;
1737
+ writeFileSync(built, `${JSON.stringify({ at: Date.now() })}\n`);
1738
+ const snapshot = snapshotNow(team);
1739
+ if (snapshot) sendSnapshot(team, provider, snapshot);
1740
+ }
1741
+
1742
+ /** HUBR-6: `team provider signin|signout|status|connect|drift-model …`, run by the provider's client. */
1743
+ function providerCommand(raw) {
1744
+ const hosted = providerAdapterOf(raw.team?.provider);
1745
+ if (!hosted || hosted.problem)
1746
+ die(
1747
+ "This team's provider isn't a hosted one: claims and intents go through GitHub, and there's nothing to sign in to.",
1748
+ );
1749
+ process.exit(hosted.adapter.run(rest.slice(1), hosted.url));
1750
+ }
1751
+
1610
1752
  /**
1611
1753
  * TEAM-13: `team init` sets the project up as a team's, or (`--off`) as one developer's again. Without
1612
1754
  * `--write` it shows the team section it would write; with it, it writes the config and says what the
@@ -1670,7 +1812,7 @@ function teamInit(raw) {
1670
1812
  if (flag('--json'))
1671
1813
  return out(JSON.stringify({ written: true, ...section, me: state.me ?? null, setup }, null, 2));
1672
1814
  out(
1673
- `Wrote the team to ${file}: ${team.techLead} is the tech lead; a claim is stale after ${team.staleDays} working day${team.staleDays === 1 ? '' : 's'} without a push; intents go in "${team.intents}".`,
1815
+ `Wrote the team to ${file}: ${team.techLead} is the tech lead; a claim is stale after ${team.staleDays} working day${team.staleDays === 1 ? '' : 's'} without a push; ${team.intents ? `intents go in "${team.intents}"` : `${team.provider.name ?? 'the provider'} keeps intents`}.`,
1674
1816
  );
1675
1817
  for (const [lane, owners] of Object.entries(team.owners))
1676
1818
  out(`Lane ${lane}: ${owners.join(', ')}`);
@@ -1930,8 +2072,84 @@ function instructionsCommand() {
1930
2072
  );
1931
2073
  }
1932
2074
 
2075
+ /** LEAN-6: a worktree of the project, with its branch, story, report and whether it's merged and clean. */
2076
+ function worktreeList(config) {
2077
+ const root = resolve('.');
2078
+ const run = (...a) => spawnSync('git', a, { cwd: root, encoding: 'utf8' }).stdout?.trim() ?? '';
2079
+ const from = findBase(config, false);
2080
+ const blocks = run('worktree', 'list', '--porcelain').split('\n\n').filter(Boolean);
2081
+ return blocks.slice(1).map((b) => {
2082
+ const path = /^worktree (.+)$/m.exec(b)?.[1];
2083
+ const branch = /^branch refs\/heads\/(.+)$/m.exec(b)?.[1] ?? null;
2084
+ const report = branch ? `.claude/state/reports/${branch.replace(/[^\w.-]+/g, '_')}.md` : null;
2085
+ const status = spawnSync('git', ['-C', path, 'status', '--porcelain'], { encoding: 'utf8' });
2086
+ return {
2087
+ path,
2088
+ branch,
2089
+ story: branch ? storyOfBranch(branch) : null,
2090
+ merged: Boolean(
2091
+ branch &&
2092
+ from &&
2093
+ spawnSync('git', ['merge-base', '--is-ancestor', branch, from], { cwd: root }).status === 0,
2094
+ ),
2095
+ clean: status.status === 0 && !status.stdout.trim(),
2096
+ report: report && existsSync(join(path, report)) ? report : null,
2097
+ };
2098
+ });
2099
+ }
2100
+
2101
+ /** LEAN-6: releases a worktree: its report kept in the main checkout, the worktree removed, its branch kept. */
2102
+ function releaseWorktree(w) {
2103
+ if (!w.clean)
2104
+ die(
2105
+ `${w.branch ?? w.path} has uncommitted changes; nothing was removed. Commit them on the branch, or have its agent do so, then release it.`,
2106
+ );
2107
+ if (w.report) {
2108
+ mkdirSync(join(resolve('.'), '.claude/state/reports'), { recursive: true });
2109
+ copyFileSync(join(w.path, w.report), join(resolve('.'), w.report));
2110
+ }
2111
+ const removed = spawnSync('git', ['worktree', 'remove', w.path], {
2112
+ cwd: resolve('.'),
2113
+ encoding: 'utf8',
2114
+ });
2115
+ if (removed.status !== 0) die(`git worktree remove failed: ${removed.stderr.trim()}`);
2116
+ return `${w.branch ?? w.path}${w.report ? ` (its report kept at ${w.report})` : ''}`;
2117
+ }
2118
+
2119
+ /**
2120
+ * LEAN-6: `worktrees` lists the project's worktrees; `release <branch>` frees one for a fresh agent on the
2121
+ * same story; `prune` releases every one whose branch is merged and that holds nothing uncommitted.
2122
+ */
2123
+ function worktreesCommand() {
2124
+ const { config } = load();
2125
+ const list = worktreeList(config);
2126
+ if (rest[0] === 'release') {
2127
+ const w = list.find((x) => x.branch === rest[1] || x.path === rest[1]);
2128
+ if (!w) die(`No worktree has ${rest[1] ?? '<branch>'} checked out.`);
2129
+ return out(`Released ${releaseWorktree(w)}. The branch and its commits stay.`);
2130
+ }
2131
+ if (rest[0] === 'prune') {
2132
+ const done = list.filter((w) => w.merged && w.clean);
2133
+ if (!done.length) return out('No worktree is both merged and clean; nothing was removed.');
2134
+ for (const w of done) out(`Released ${releaseWorktree(w)}.`);
2135
+ return;
2136
+ }
2137
+ if (flag('--json')) return out(JSON.stringify(list, null, 2));
2138
+ if (!list.length) return out('No worktrees besides the main checkout.');
2139
+ for (const w of list)
2140
+ out(
2141
+ ` ${w.branch ?? '(detached)'}${w.story ? ` · ${w.story}` : ''}: ${w.merged ? 'merged' : 'not merged'}, ${w.clean ? 'clean' : 'uncommitted changes'} ${w.path}`,
2142
+ );
2143
+ const prunable = list.filter((w) => w.merged && w.clean).length;
2144
+ if (prunable)
2145
+ out(
2146
+ `\n${prunable} merged and clean: \`code-kit worktrees prune\` removes them, keeping their reports.`,
2147
+ );
2148
+ }
2149
+
1933
2150
  const commands = {
1934
2151
  check,
2152
+ worktrees: worktreesCommand,
1935
2153
  instructions: instructionsCommand,
1936
2154
  loop: loopCommand,
1937
2155
  approve: () => (rest.length ? approve(rest) : usage()),
@@ -1970,6 +2188,7 @@ function usage() {
1970
2188
  ' | security [--base ref] [--branch name] [--json]\n' +
1971
2189
  ' | next [--base ref] [--json] | adapters | trace <path>... [--json]\n' +
1972
2190
  ' | instructions [--write] [--json] | instructions show lead|lane\n' +
2191
+ ' | worktrees [--json] | worktrees release <branch> | worktrees prune\n' +
1973
2192
  ' | approve <name>... --reason "…" [--lane name] [--config file]\n' +
1974
2193
  ' | merge <branch> --delegated|--person [--into branch] | requests [--json] | requests drop <n>|all\n' +
1975
2194
  ' | ask "<question>" [--story ST-n] [--blocks] [--why "…"] [--option "Label: effect"]... | ask [--json]\n' +
@@ -236,6 +236,14 @@ if (runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+settings\s+set\b`)))
236
236
  block(
237
237
  `Blocked: changing the harness settings is the person's own act, from the code-kit pane (/harness) or their terminal. Tell them what you'd change and why, and stop.\nCommand: ${cmd}`,
238
238
  );
239
+ // LEAN-6: releasing or pruning worktrees is the lead's, which knows which stories are still at work.
240
+ if (
241
+ runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+worktrees\s+(?:release|prune)\b`)) &&
242
+ actor.kind !== 'lead'
243
+ )
244
+ block(
245
+ `Blocked: releasing worktrees is the lead's, which knows which stories are still at work. Finish your story and hand back.\nCommand: ${cmd}`,
246
+ );
239
247
  // ONB-8: code-kit's instructions are the lead's to bring up to date, like the rest of the kit.
240
248
  if (
241
249
  runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+instructions\b.*\s--write\b`)) &&
package/hooks/hooks.json CHANGED
@@ -46,6 +46,28 @@
46
46
  ]
47
47
  }
48
48
  ],
49
+ "SessionStart": [
50
+ {
51
+ "hooks": [
52
+ {
53
+ "type": "command",
54
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/team-session.mjs\" start",
55
+ "timeout": 10
56
+ }
57
+ ]
58
+ }
59
+ ],
60
+ "SessionEnd": [
61
+ {
62
+ "hooks": [
63
+ {
64
+ "type": "command",
65
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/team-session.mjs\" end",
66
+ "timeout": 10
67
+ }
68
+ ]
69
+ }
70
+ ],
49
71
  "Stop": [
50
72
  {
51
73
  "hooks": [