@warren-dean/code-kit 0.9.1 → 0.11.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +17 -0
- package/README.md +37 -31
- package/bin/code-kit.mjs +133 -2
- package/hooks/guard-bash.mjs +8 -0
- package/hooks/lib/config.mjs +4 -3
- package/hooks/lib/next.mjs +87 -3
- package/hooks/lib/plan.mjs +2 -1
- package/hooks/lib/security.mjs +180 -0
- package/hooks/lib/team.mjs +62 -7
- package/hooks/mod/decisions.mjs +3 -0
- package/hooks/mod/loop.mjs +14 -1
- package/hooks/mod/panes.mjs +42 -49
- package/hooks/mod/register.mjs +104 -12
- package/hooks/mod/register.test.ts +255 -15
- package/hooks/mod/teamview.mjs +469 -74
- package/hooks/mod/view.mjs +14 -2
- package/package.json +1 -1
- package/skills/init/SKILL.md +13 -5
- package/skills/next/SKILL.md +19 -17
- package/skills/review/SKILL.md +19 -4
- package/skills/spec-design/SKILL.md +5 -2
- package/skills/team-init/SKILL.md +59 -0
- package/templates/security-reviewer.md +57 -0
- package/templates/spec.md +18 -0
|
@@ -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.
|
|
12
|
+
"version": "0.11.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.
|
|
3
|
+
"version": "0.11.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,23 @@
|
|
|
2
2
|
|
|
3
3
|
What changed in each release of code-kit, newest first. Versions follow the plugin manifest and the npm package `@warren-dean/code-kit`. Each release is also on [GitHub Releases](https://github.com/warrendeanlangeveldt/code-kit/releases).
|
|
4
4
|
|
|
5
|
+
## 0.11.0 (2026-10-12)
|
|
6
|
+
|
|
7
|
+
Teams get their own pane and their own setup, and a hosted team provider can stand down to GitHub.
|
|
8
|
+
|
|
9
|
+
- **`/team` opens the Team pane.** It replaces the Lanes pane's Team tab. Its header names the tech lead, you, and what coordinates the team (GitHub, or a hosted provider, and whether it's live). Its tabs: People (who's at work on what), Since you looked, Claims (Keep and Release on yours; Release and Reassign for the tech lead), Intents (Agree and Drop for the tech lead), Drift (with a provider that checks for it) and Setup (the team's config and what GitHub still needs). A Team button in the band opens it on the tab its line is about.
|
|
10
|
+
- **Set up a team only when there is one.** `/code-kit:team-init` asks for the tech lead, each lane's owners, when a claim goes stale and where intents go, shows the team section, and writes it once you agree; it also changes a team or takes it out. `/code-kit:init` asks once whether other developers work in the repository and runs it only if they do. Behind it, `code-kit team init` (`--write` to write, `--off` for one developer again) says what GitHub still needs: gh signed in, the repository reachable, and the intents' discussion category. A lane agent can't write it.
|
|
11
|
+
- **A hosted provider can fall back to GitHub.** With `team.provider: { command, fallback: "github" }`, whatever the provider can't reach goes through GitHub's claim refs instead, and the band says GitHub is standing in. A live provider's state refreshes the band and the pane as it changes, and its presence and drift findings show in the pane, with drift on your own work in the band.
|
|
12
|
+
- **Two band lines with the same button no longer clash.** Each button and row keeps its own key.
|
|
13
|
+
|
|
14
|
+
## 0.10.0 (2026-10-12)
|
|
15
|
+
|
|
16
|
+
An older setup is brought up to date before new work starts, and a security reviewer checks the changes that need one.
|
|
17
|
+
|
|
18
|
+
- **The gaps stop new work until init fills them.** For a project set up by an older init, `code-kit next` names what it's missing: a design gate for the lanes that build screens, design stories before the screen stories not yet started, a QA lane, and a security reviewer. Work already started goes on through review and merge; nothing new starts. Once nothing is in flight, `next` gives `update`, and the loop has the lead re-run `/code-kit:init`, which shows you the difference first.
|
|
19
|
+
- **An adversarial security reviewer.** spec-design writes a short threat model for each spec (what's sensitive, who might attack it, the trust boundaries) and security requirements stories can cite. init writes a read-only `security-reviewer` agent. `code-kit security` says whether a branch touches sign-in, permissions, outside input, personal data, encryption, payments, secrets or dependencies, and where; when it does, the review runs the security reviewer, which reports only findings with a concrete attack path. A blocker sends the branch back like a failing test.
|
|
20
|
+
- **Setup version 3.** Re-running init records `"setup": 3`.
|
|
21
|
+
|
|
5
22
|
## 0.9.1 (2026-10-12)
|
|
6
23
|
|
|
7
24
|
Team mode, run against a real GitHub repository. It's still a preview.
|
package/README.md
CHANGED
|
@@ -184,7 +184,7 @@ spec-design works an idea into a specification with you before any code exists.
|
|
|
184
184
|
1. **Frame:** the problem, the users, the outcome, the constraints and the non-goals.
|
|
185
185
|
2. **Explore:** two or three different shapes of solution, the main user journeys, and the riskiest assumptions. You choose.
|
|
186
186
|
3. **Scope:** capability areas, and what the first release has to deliver end to end.
|
|
187
|
-
4. **Specify:** one spec per area, with numbered, testable requirements covering data, states, rules, permissions, errors and acceptance criteria.
|
|
187
|
+
4. **Specify:** one spec per area, with numbered, testable requirements covering data, states, rules, permissions, errors and acceptance criteria, and a short threat model with the security requirements that answer it.
|
|
188
188
|
5. **Architecture, principles and plan:** the layers, contracts, recipes for each layer's common additions (they become the lane agents' skills), definition of done, and stories split into lanes, with a design story before the screens of each area that has any.
|
|
189
189
|
6. **Check:** every journey maps to requirements and every requirement to a story, with no gaps left as "TBD".
|
|
190
190
|
|
|
@@ -200,7 +200,7 @@ Requirements are headings (`### BOOK-4 Cancel a booking`). Stories are `### ST-1
|
|
|
200
200
|
|
|
201
201
|
- **A new build:** init reads the docs in full: the architecture and layer map, the plan or work split, and the principles. It drafts the rules from them.
|
|
202
202
|
- **An existing codebase (brownfield):** the docs can be thin or missing. Init works the layers out from the code: which folders import which, and who works where according to the git history. It also runs each check once and flags any that already fail, so you decide what to do with them.
|
|
203
|
-
- **Either way:** you get a draft (`.claude/code-kit.draft.json`), one agent per lane with its project skills (one per recipe, in the same approval), the design gate for lanes that own screens, a CLAUDE.md section and a list of questions (see [The agents and skills it creates](#the-agents-and-skills-it-creates)). Nothing is enforced until you approve and the draft becomes `.claude/code-kit.json`.
|
|
203
|
+
- **Either way:** you get a draft (`.claude/code-kit.draft.json`), one agent per lane with its project skills (one per recipe, in the same approval), a read-only security reviewer, the design gate for lanes that own screens, a CLAUDE.md section and a list of questions (see [The agents and skills it creates](#the-agents-and-skills-it-creates)). Nothing is enforced until you approve and the draft becomes `.claude/code-kit.json`.
|
|
204
204
|
|
|
205
205
|
**Existing violations.** Brownfield code usually breaks some layer rules already. On approval, `baseline --write` records them in `.claude/code-kit.baseline.json`:
|
|
206
206
|
|
|
@@ -220,7 +220,8 @@ code-kit ships no fixed team. Init writes one for each project, from its docs or
|
|
|
220
220
|
├── agents/
|
|
221
221
|
│ ├── web-engineer.md # one lane agent per lane
|
|
222
222
|
│ ├── api-engineer.md
|
|
223
|
-
│
|
|
223
|
+
│ ├── db-engineer.md
|
|
224
|
+
│ └── security-reviewer.md # reads every sensitive change as an attacker would; owns nothing
|
|
224
225
|
└── skills/
|
|
225
226
|
└── new-migration/ # project recipes, only the ones you approve
|
|
226
227
|
└── SKILL.md
|
|
@@ -266,6 +267,8 @@ Editing an agent file can't loosen a rule. And the agent files are kit files (`.
|
|
|
266
267
|
5. The stop hook runs the checks before the agent can finish.
|
|
267
268
|
6. Review compares the branch with the requirements and the report, and `verify` checks the rules again in CI.
|
|
268
269
|
|
|
270
|
+
**The security reviewer** (`.claude/agents/security-reviewer.md`, from `templates/security-reviewer.md`) is a reviewer, not a lane: it owns no paths and checks everyone's work. Init writes it whenever the product handles sign-in, personal data, payments, input from outside or secrets, which is nearly always. It only reads (Read, Grep, Glob, and Bash for `git diff` and the project's audit commands), and the hooks hold it to that like any subagent outside the lanes. spec-design gives each spec a short threat model (what's sensitive, who might attack it, the trust boundaries) and security requirements stories cite. When `code-kit security` says a story's change touches something sensitive, the review runs the agent on the diff against those threat models: it attacks authentication and authorisation, injection (prompt injection too), data exposure, secrets, cryptography, SSRF and redirects, deserialisation, new dependencies and the privacy targets, and grades each finding blocker, concern or nit, with where, the attack and the fix. A blocker sends the branch back like a failing test.
|
|
271
|
+
|
|
269
272
|
**Project skills** (`.claude/skills/<recipe>/SKILL.md`) are the step-by-step recipes your docs describe, like "add a migration" or "add an endpoint". Init lists the ones it finds and writes them only if you say yes, never as empty stubs. Each is listed in the owning lane agent's "Skills to use". Like the agents, they're kit files: the lead maintains them, and changes are logged.
|
|
270
273
|
|
|
271
274
|
**Kept in step.** When the project changes (a new app, a new lane, moved folders), re-run `/code-kit:init`. It drafts new agents for new lanes and updates the paths of existing ones, keeping anything written by hand. You see the difference and approve it before anything changes.
|
|
@@ -285,7 +288,7 @@ With Claude Code 2.1.287 or later, code-kit's mod shows the project in the sessi
|
|
|
285
288
|
|
|
286
289
|
Each button has a digit: type it in an empty prompt to press it.
|
|
287
290
|
|
|
288
|
-
- **`/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 **
|
|
291
|
+
- **`/lanes`** opens the Lanes pane. Its first line counts the stories by state (building, in review, queued, sent back, blocked, ready, merged), with the loop's state and the plan's 5-hour use; press a count to show only those lanes. Beneath it, whatever needs you (held calls, approvals, reviews, merges, stalled lanes, outliers) is pinned with its key. Tabs on **1** to **7**: **Lanes** opens on the loop as a card: its mode, the steps it took, the one running and the next. Then each lane on fixed columns: its name, state and story, its agent's current or last tool call with the time since beneath (amber once quiet, red when stalled), and its tokens; the lead reads orchestrating while its turn runs. A selected lane shows its stories as cards (state, requirements, what each waits on or its branch), then its agent and the paths it owns. **Decisions**, each decision that needs you as a card: what you asked for, the story and its requirements, what was happening, what's already settled, why the lead couldn't settle it, and each option with what it does; **Journal**, the build from your request to now in plain sentences, by day: the request and the decisions in the brief, the specification, each story handed out, sent back and merged, each approval and who gave it, each milestone finished, and the loop's steps this session, yours in yellow. Each lane has its own small pixel character, tinted in the lane's colour, whose pose shows its state: working (animated while its agent works), waiting on you, quiet, stalled, done or resting; the lead's wears a crown. Under each lane, its story's timeline on a shared time axis: building, then in review, with send-backs (↩), approvals (◆) and the merge (✓) marked where they fell; **Code map**, the whole project's folders by layer when no story is at work, or a story's files in a column per layer with the imports between them: a file fills with a lane's colour while that lane reads or edits it, and says whether its why (its Context Graph card) is written, out of date or not written yet; **j**/**k** select a file to see its card and what it imports and what imports it, and **o** opens it in Context Graph (box-drawing text in the terminal, an SVG on Desktop); **Trail**, every requirement in the specs as a cell by spec, its glyph and colour saying whether it's done and tested, done but untested, in progress, todo, without a story or removed, with a legend counting each. **h**/**l** and **j**/**k** move between cells and specs (or pick one from the list), showing the requirement's stories, their branches, the code they changed and the tests that name it; beneath the cells, the spec's trail drawn as a flow, each requirement to its stories, code and tests, with a missing link dashed in red; **Queue**, the merge queue; and **Usage**. **j** and **k** move between lanes, and the selected lane's acts are on **a** (approve), **r** (review), **s** (send back, with your reason), **m** (merge), **n** (nudge its agent) and **x** (stop it); **p** pauses or resumes the loop, and **f** filters the lanes by name, agent or story. **Enter** (or **o**) opens the selected lane's story: its requirements beside its spec-check report, the reviewer's findings by severity (each a way into the diff), `verify`'s problems, its usage and loop steps, and its diff against the base, file by file, with the acts its state allows. **b** or Escape goes back to the lanes. Inline (when the terminal can't dock it beside the transcript), it shows the counts, what needs you and a line per lane. It stays current within 2 seconds of a commit, a branch change, a plan edit or an agent starting or stopping. `/lanes` again, or Escape, closes it.
|
|
289
292
|
- **Refusals** a code-kit hook gives an agent are drawn as cards: what was refused, the rule, what to do, and Approve… where your approval would allow it. The full text is a press away.
|
|
290
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`.
|
|
291
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.
|
|
@@ -308,11 +311,12 @@ Once init has switched the rules on, the lead runs the build in a loop:
|
|
|
308
311
|
|
|
309
312
|
## Teams (preview)
|
|
310
313
|
|
|
311
|
-
For several people in one repository, each with their own agents.
|
|
314
|
+
For several people in one repository, each with their own agents. Not every developer works in a team, so it's off until you set it up: `/code-kit:init` asks whether others work in the repository, and `/code-kit:team-init` sets the team up (or changes it, or takes it out) at any time, asking for the tech lead and each lane's owners and showing what it writes first. Everything goes through GitHub; there's no server:
|
|
312
315
|
|
|
313
316
|
- **Owners and claims.** A lane's owners are the only ones whose agents work it. Making a story's branch claims the story, through a ref in the shared repository, so two people can't both take it. The lead says what files the story will change (`team claim --lease`), and each commit adds what it changed; another person's agent editing a leased file is refused with who holds it.
|
|
314
317
|
- **The tech lead's paths.** Only the tech lead's sessions change the plan, specs and contracts. Anyone else raises an **intent** (`code-kit intend`), a discussion in the repository's Intents category, before any code. The tech lead agrees, drops or marks it a duplicate; an agreed one becomes a numbered decision record (`code-kit intend record`) through a pull request, where every agent reads it.
|
|
315
|
-
- **Since you last looked.** `code-kit team since` says, by person, what was pushed, opened, merged, reviewed, claimed and intended. The Team
|
|
318
|
+
- **Since you last looked.** `code-kit team since` says, by person, what was pushed, opened, merged, reviewed, claimed and intended. The Team pane shows it.
|
|
319
|
+
- **`/team`** opens the Team pane: the tech lead, who you are, and what coordinates the team (GitHub, or a hosted provider, and whether it's live). Its tabs: **People**, each person with their lanes, where they're at work (with a live provider), claims, pull requests and intents; **Since you looked**; **Claims**, with Keep and Release on yours, and Release and Reassign for the tech lead; **Intents**, with Agree and Drop for the tech lead; **Drift**, when the provider checks for it; and **Setup**, the team's config and what GitHub still needs.
|
|
316
320
|
- **Stale claims** (nothing pushed for three working days) are flagged, never released on their own: the holder keeps or releases it, and the tech lead can reassign it.
|
|
317
321
|
- **GitHub does its part.** `code-kit team codeowners --write` writes CODEOWNERS from the config, and `"board": { "owner", "number" }` mirrors the plan onto a GitHub Project.
|
|
318
322
|
|
|
@@ -349,6 +353,7 @@ code-kit diff --config .claude/code-kit.draft.json # what a draft changes
|
|
|
349
353
|
code-kit baseline [--write] # layer violations in the code as it is
|
|
350
354
|
code-kit graph [--depth N] # which folders import which
|
|
351
355
|
code-kit verify [--base ref] [--branch name] [--no-checks] [--json] # a branch's changes, for CI and review
|
|
356
|
+
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
|
|
352
357
|
code-kit status [--base ref] [--json] # stories and requirements against the spec
|
|
353
358
|
code-kit next [--base ref] [--json] # the step to take now, from the project's state
|
|
354
359
|
code-kit trace <path>... [--json] # what a file is for: requirements, lane, layer rules
|
|
@@ -369,6 +374,7 @@ code-kit timeline [--json] # when each story'
|
|
|
369
374
|
code-kit journal [--json] # the build's lineage: the request, its decisions, the spec, each story handed out, sent back and merged, each approval and who gave it
|
|
370
375
|
code-kit team [--json] [--fresh] # the team (preview): tech lead, owners, claims, intents and open pull requests (spec 16)
|
|
371
376
|
code-kit team since [--json] [--peek] # what everyone pushed, opened, merged, reviewed, claimed and intended since you last looked
|
|
377
|
+
code-kit team init --tech-lead <handle> [--owner <lane>=<handles>] [--stale-days n] [--intents category] [--write] | team init --off [--write] # show, then write, the team's setup; or make the project one developer's again
|
|
372
378
|
code-kit team claim <story> [--lease globs] | team release <story> # claim a story for yourself, leasing what it changes; release it (yours, or any as the tech lead)
|
|
373
379
|
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
|
|
374
380
|
code-kit team codeowners [--write] # .github/CODEOWNERS from the config: the tech lead for their paths, each lane's owners for its paths
|
|
@@ -389,31 +395,31 @@ code-kit approve <name>... --reason "…" [--lane name] [--delegated | --via pan
|
|
|
389
395
|
|
|
390
396
|
`.claude/code-kit.json`. Globs use `*` (one segment), `**` (any depth), `**/` (zero or more folders), `?` and `{a,b}`. Patterns under `shell` are regular expressions.
|
|
391
397
|
|
|
392
|
-
| Field | Required | What it is
|
|
393
|
-
| ------------------------------ | ----------------- |
|
|
394
|
-
| `version` | yes | `1`
|
|
395
|
-
| `docs.specs` | no | Folder of specs. With it, a lane needs a spec-check report before writing code on a branch
|
|
396
|
-
| `docs.principles`, `docs.plan` | no | The engineering principles and the implementation plan
|
|
397
|
-
| `docs.decisions` | no | Where agreed intents are recorded as numbered decision records (`code-kit intend record`); default `docs/decisions`
|
|
398
|
-
| `lead.paths` | yes | What the main session writes. `.claude/**` is always added.
|
|
399
|
-
| `lead.outside` | no | Paths outside the repository the lead may write, e.g. `../shared-lib/**`. Claude's memory and scratchpad are always included.
|
|
400
|
-
| `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.
|
|
401
|
-
| `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.
|
|
402
|
-
| `protected[]` | no | `{ glob, approval, why }`: logged on every commit. Anyone but the lead needs a person's approval (see [Approvals](#approvals)). `.claude/**` is always protected as `kit`.
|
|
403
|
-
| `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
|
|
404
|
-
| `layers[]` | no | `{ name, paths, mayImport, denyPackages? }`: the first layer whose paths match a file owns it. A layer may always import itself.
|
|
405
|
-
| `importAliases` | no | `{ "@app/domain": "packages/domain/src" }`: resolves non-relative imports to repository paths
|
|
406
|
-
| `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
|
|
407
|
-
| `shell.restricted[]` | no | `{ pattern, lanes, why }`: only the lead and these lanes
|
|
408
|
-
| `postEdit[]` | no | `{ files, exclude?, run }`: after each edit of a matching file. `{file}` is the file. Exit 127 (tool not installed) is skipped.
|
|
409
|
-
| `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).
|
|
410
|
-
| `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).
|
|
411
|
-
| `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).
|
|
412
|
-
| `approvals.delegate` | no | `{ protected?: [approval names], merge?: true, dependencies?: { licences?, minWeeklyDownloads?, maxMonthsSinceRelease?, allowInstallScripts?: true \| false \| [kinds] } }`: approvals the lead may grant on its own, with `code-kit approve --delegated`, and with `merge`, merges into a protected branch with `code-kit merge --delegated`. Dependencies are checked against the npm 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).
|
|
413
|
-
| `setup` | no | The version of init's setup the project has (
|
|
414
|
-
| `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 }` for another team provider. See docs/specs/16-teams.md. |
|
|
415
|
-
| `branches.protected` | no | Default `["main", "master"]`
|
|
416
|
-
| `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`.
|
|
398
|
+
| Field | Required | What it is |
|
|
399
|
+
| ------------------------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
400
|
+
| `version` | yes | `1` |
|
|
401
|
+
| `docs.specs` | no | Folder of specs. With it, a lane needs a spec-check report before writing code on a branch |
|
|
402
|
+
| `docs.principles`, `docs.plan` | no | The engineering principles and the implementation plan |
|
|
403
|
+
| `docs.decisions` | no | Where agreed intents are recorded as numbered decision records (`code-kit intend record`); default `docs/decisions` |
|
|
404
|
+
| `lead.paths` | yes | What the main session writes. `.claude/**` is always added. |
|
|
405
|
+
| `lead.outside` | no | Paths outside the repository the lead may write, e.g. `../shared-lib/**`. Claude's memory and scratchpad are always included. |
|
|
406
|
+
| `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. |
|
|
407
|
+
| `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. |
|
|
408
|
+
| `protected[]` | no | `{ glob, approval, why }`: logged on every commit. Anyone but the lead needs a person's approval (see [Approvals](#approvals)). `.claude/**` is always protected as `kit`. |
|
|
409
|
+
| `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 |
|
|
410
|
+
| `layers[]` | no | `{ name, paths, mayImport, denyPackages? }`: the first layer whose paths match a file owns it. A layer may always import itself. |
|
|
411
|
+
| `importAliases` | no | `{ "@app/domain": "packages/domain/src" }`: resolves non-relative imports to repository paths |
|
|
412
|
+
| `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 |
|
|
413
|
+
| `shell.restricted[]` | no | `{ pattern, lanes, why }`: only the lead and these lanes |
|
|
414
|
+
| `postEdit[]` | no | `{ files, exclude?, run }`: after each edit of a matching file. `{file}` is the file. Exit 127 (tool not installed) is skipped. |
|
|
415
|
+
| `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). |
|
|
416
|
+
| `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). |
|
|
417
|
+
| `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). |
|
|
418
|
+
| `approvals.delegate` | no | `{ protected?: [approval names], merge?: true, dependencies?: { licences?, minWeeklyDownloads?, maxMonthsSinceRelease?, allowInstallScripts?: true \| false \| [kinds] } }`: approvals the lead may grant on its own, with `code-kit approve --delegated`, and with `merge`, merges into a protected branch with `code-kit merge --delegated`. Dependencies are checked against the npm 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). |
|
|
419
|
+
| `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
|
+
| `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
|
+
| `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`. |
|
|
417
423
|
|
|
418
424
|
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.
|
|
419
425
|
|
package/bin/code-kit.mjs
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// graph [--depth N] which folders import which (to find the layers in existing code)
|
|
9
9
|
// verify [--base ref] [--branch name] [--no-checks]
|
|
10
10
|
// the rules for everything this branch changed since base (for CI and review)
|
|
11
|
+
// security [--base ref] [--branch name] [--json]
|
|
12
|
+
// whether a branch's changes need the security reviewer, and why (ONB-7)
|
|
11
13
|
// status [--base ref] [--json]
|
|
12
14
|
// each story and requirement in the plan: built, in review, ready, blocked, tested
|
|
13
15
|
// adapters other tools detected in the project, what each adds to the rules, and its setup
|
|
@@ -52,6 +54,12 @@ import { nextStep } from '../hooks/lib/next.mjs';
|
|
|
52
54
|
import { traceFile } from '../hooks/lib/trace.mjs';
|
|
53
55
|
import { buildStatus, specCheckRows } from '../hooks/lib/plan.mjs';
|
|
54
56
|
import { recordSentBack } from '../hooks/lib/reviews.mjs';
|
|
57
|
+
import {
|
|
58
|
+
SECURITY_AGENT,
|
|
59
|
+
SECURITY_AGENT_FILE,
|
|
60
|
+
reasonsInWords,
|
|
61
|
+
securityReview,
|
|
62
|
+
} from '../hooks/lib/security.mjs';
|
|
55
63
|
import { storyTimes } from '../hooks/lib/timeline.mjs';
|
|
56
64
|
import { PROJECT, projectMap, storyMap, trail } from '../hooks/lib/storymap.mjs';
|
|
57
65
|
import { journal } from '../hooks/lib/journal.mjs';
|
|
@@ -112,7 +120,21 @@ const minutesGiven = option('--minutes');
|
|
|
112
120
|
const answerGiven = option('--answer');
|
|
113
121
|
const leaseGiven = option('--lease');
|
|
114
122
|
const touchesGiven = option('--touches');
|
|
115
|
-
const
|
|
123
|
+
const techLeadGiven = option('--tech-lead');
|
|
124
|
+
const staleDaysGiven = option('--stale-days');
|
|
125
|
+
const intentsGiven = option('--intents');
|
|
126
|
+
const ownersGiven = [];
|
|
127
|
+
for (let o = option('--owner'); o !== undefined; o = option('--owner')) ownersGiven.push(o);
|
|
128
|
+
const FLAGS = [
|
|
129
|
+
'--write',
|
|
130
|
+
'--no-checks',
|
|
131
|
+
'--json',
|
|
132
|
+
'--delegated',
|
|
133
|
+
'--person',
|
|
134
|
+
'--fresh',
|
|
135
|
+
'--peek',
|
|
136
|
+
'--off',
|
|
137
|
+
];
|
|
116
138
|
const flag = (name) => args.includes(name);
|
|
117
139
|
const writeBaseline = flag('--write');
|
|
118
140
|
const [command, ...rest] = args.filter((a) => !FLAGS.includes(a));
|
|
@@ -1063,6 +1085,39 @@ function verify() {
|
|
|
1063
1085
|
out('No problems.');
|
|
1064
1086
|
}
|
|
1065
1087
|
|
|
1088
|
+
/**
|
|
1089
|
+
* ONB-7: whether a branch's changes need the security reviewer, and why: sign-in, permissions, outside
|
|
1090
|
+
* input, personal data, encryption, payments, secrets, commands built from strings, new dependencies,
|
|
1091
|
+
* or a security requirement the story cites.
|
|
1092
|
+
*/
|
|
1093
|
+
function security() {
|
|
1094
|
+
const { config } = load();
|
|
1095
|
+
const from = base(config, false);
|
|
1096
|
+
let branch = branchName;
|
|
1097
|
+
if (!branch) {
|
|
1098
|
+
try {
|
|
1099
|
+
branch = currentBranch('.');
|
|
1100
|
+
} catch {
|
|
1101
|
+
die('This checkout has no branch; pass --branch <name>.');
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (!refExists(branch)) die(`--branch ${branch} is not a known branch.`);
|
|
1105
|
+
const found = securityReview(resolve('.'), { base: from, branch, config });
|
|
1106
|
+
if (flag('--json')) return out(JSON.stringify({ base: from, ...found }, null, 2));
|
|
1107
|
+
const what = found.story ? `${found.story} on ${branch}` : branch;
|
|
1108
|
+
if (!found.needed)
|
|
1109
|
+
return out(
|
|
1110
|
+
`${what} needs no security review: against ${from}, it touches no sign-in, permissions, outside input, personal data, encryption, payments or secrets, adds no dependencies and cites no security requirement.`,
|
|
1111
|
+
);
|
|
1112
|
+
out(`${what} needs the security review. Against ${from}, it touches:`);
|
|
1113
|
+
for (const r of found.reasons) out(` ${reasonsInWords([r])}`);
|
|
1114
|
+
out(
|
|
1115
|
+
found.agent
|
|
1116
|
+
? `\nRun the ${SECURITY_AGENT} agent on the diff (git diff ${from}...${branch}) with the story's specs and their threat models. A blocker it finds sends the branch back, like a failing test.`
|
|
1117
|
+
: `\nThis project has no ${SECURITY_AGENT} agent (${SECURITY_AGENT_FILE}). Re-run /code-kit:init to add it; until then, review the diff as an attacker would yourself, against the specs' threat models.`,
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1066
1121
|
const pad = (s, n) => String(s).padEnd(n);
|
|
1067
1122
|
|
|
1068
1123
|
function status() {
|
|
@@ -1269,6 +1324,7 @@ function ago(iso) {
|
|
|
1269
1324
|
*/
|
|
1270
1325
|
function teamCommand() {
|
|
1271
1326
|
const { raw } = load();
|
|
1327
|
+
if (rest[0] === 'init') return teamInit(raw);
|
|
1272
1328
|
const team = teamOf(raw);
|
|
1273
1329
|
if (!team)
|
|
1274
1330
|
die(
|
|
@@ -1390,7 +1446,7 @@ function teamCommand() {
|
|
|
1390
1446
|
}
|
|
1391
1447
|
if (sub !== undefined)
|
|
1392
1448
|
die(
|
|
1393
|
-
'code-kit team [--json] [--fresh] | team since [--json] [--peek] | team claim|keep|release <story> | team reassign <story> <handle>',
|
|
1449
|
+
'code-kit team [--json] [--fresh] | team init … | team since [--json] [--peek] | team claim|keep|release <story> | team reassign <story> <handle>',
|
|
1394
1450
|
);
|
|
1395
1451
|
const state = provider.state({ fresh: flag('--fresh') });
|
|
1396
1452
|
if (flag('--json')) return out(JSON.stringify(state, null, 2));
|
|
@@ -1421,6 +1477,78 @@ function teamCommand() {
|
|
|
1421
1477
|
out(` #${p.number} ${p.title} (${p.by}, ${p.branch}${p.draft ? ', draft' : ''})`);
|
|
1422
1478
|
}
|
|
1423
1479
|
|
|
1480
|
+
/**
|
|
1481
|
+
* TEAM-13: `team init` sets the project up as a team's, or (`--off`) as one developer's again. Without
|
|
1482
|
+
* `--write` it shows the team section it would write; with it, it writes the config and says what the
|
|
1483
|
+
* team still needs on GitHub (gh signed in, origin reachable, the intents' discussion category).
|
|
1484
|
+
*/
|
|
1485
|
+
function teamInit(raw) {
|
|
1486
|
+
const usage =
|
|
1487
|
+
'code-kit team init --tech-lead <GitHub handle> [--owner <lane>=<handle>,<handle>]… [--stale-days <n>] [--intents <category>] [--write] [--json] | team init --off [--write]';
|
|
1488
|
+
const changed = structuredClone(raw);
|
|
1489
|
+
if (flag('--off')) {
|
|
1490
|
+
delete changed.team;
|
|
1491
|
+
for (const lane of Object.values(changed.lanes ?? {})) delete lane.owners;
|
|
1492
|
+
} else {
|
|
1493
|
+
const techLead = techLeadGiven ?? raw.team?.techLead;
|
|
1494
|
+
if (!techLead) die(usage);
|
|
1495
|
+
changed.team = { ...(raw.team ?? {}), techLead };
|
|
1496
|
+
if (staleDaysGiven !== undefined) changed.team.staleDays = Number(staleDaysGiven);
|
|
1497
|
+
if (intentsGiven !== undefined) changed.team.intents = intentsGiven;
|
|
1498
|
+
for (const given of ownersGiven) {
|
|
1499
|
+
const [lane, people = ''] = given.split('=');
|
|
1500
|
+
if (!changed.lanes?.[lane])
|
|
1501
|
+
die(
|
|
1502
|
+
`There's no lane "${lane}" in the config. The lanes are: ${Object.keys(changed.lanes ?? {}).join(', ') || 'none'}.`,
|
|
1503
|
+
);
|
|
1504
|
+
const handles = people
|
|
1505
|
+
.split(',')
|
|
1506
|
+
.map((h) => h.trim().replace(/^@/, ''))
|
|
1507
|
+
.filter(Boolean);
|
|
1508
|
+
if (handles.length) changed.lanes[lane].owners = handles;
|
|
1509
|
+
else delete changed.lanes[lane].owners;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
const problems = validate(changed);
|
|
1513
|
+
if (problems.length) die(`Nothing changed: ${problems.join('; ')}`);
|
|
1514
|
+
const team = teamOf(changed);
|
|
1515
|
+
const section = team
|
|
1516
|
+
? {
|
|
1517
|
+
team: changed.team,
|
|
1518
|
+
owners: team.owners,
|
|
1519
|
+
}
|
|
1520
|
+
: null;
|
|
1521
|
+
if (!flag('--write')) {
|
|
1522
|
+
if (flag('--json')) return out(JSON.stringify({ written: false, ...section }, null, 2));
|
|
1523
|
+
if (!team)
|
|
1524
|
+
return out(
|
|
1525
|
+
'This would take the team out of the config: one developer, no claims or owners. Run again with --write to do it.',
|
|
1526
|
+
);
|
|
1527
|
+
out(JSON.stringify({ team: changed.team }, null, 2));
|
|
1528
|
+
for (const [lane, owners] of Object.entries(team.owners))
|
|
1529
|
+
out(`lanes.${lane}.owners: ${owners.join(', ')}`);
|
|
1530
|
+
return out('Nothing is written yet: run again with --write.');
|
|
1531
|
+
}
|
|
1532
|
+
writeFileSync(resolve(file), `${JSON.stringify(changed, null, 2)}\n`);
|
|
1533
|
+
if (!team)
|
|
1534
|
+
return out(
|
|
1535
|
+
`Took the team out of ${file}: claims, owners and intents no longer apply. Commit it on a branch.`,
|
|
1536
|
+
);
|
|
1537
|
+
// What GitHub says now: the provider's own look at the repository.
|
|
1538
|
+
const state = teamProvider(resolve('.'), team).state({ fresh: true });
|
|
1539
|
+
const setup = state.setup ?? [];
|
|
1540
|
+
if (flag('--json'))
|
|
1541
|
+
return out(JSON.stringify({ written: true, ...section, me: state.me ?? null, setup }, null, 2));
|
|
1542
|
+
out(
|
|
1543
|
+
`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}".`,
|
|
1544
|
+
);
|
|
1545
|
+
for (const [lane, owners] of Object.entries(team.owners))
|
|
1546
|
+
out(`Lane ${lane}: ${owners.join(', ')}`);
|
|
1547
|
+
out(setup.length ? 'Still to set up:' : 'GitHub is ready for the team.');
|
|
1548
|
+
for (const s of setup) out(` ${s}`);
|
|
1549
|
+
out(`Commit ${file} on a branch; each teammate then needs code-kit and gh signed in.`);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1424
1552
|
/**
|
|
1425
1553
|
* TEAM-5: `intend "<what and why>" [--touches globs]` raises an intent before any code, so the team sees
|
|
1426
1554
|
* a change of direction first; `intend agree|drop|duplicate <id> [--reason "…"]` is the tech lead's answer.
|
|
@@ -1521,6 +1649,7 @@ const commands = {
|
|
|
1521
1649
|
adapters,
|
|
1522
1650
|
next,
|
|
1523
1651
|
verify,
|
|
1652
|
+
security,
|
|
1524
1653
|
status,
|
|
1525
1654
|
unowned,
|
|
1526
1655
|
diff,
|
|
@@ -1532,6 +1661,7 @@ function usage() {
|
|
|
1532
1661
|
die(
|
|
1533
1662
|
'Usage: code-kit check | who <path>... | unowned | diff | baseline [--write] | graph [--depth N]\n' +
|
|
1534
1663
|
' | verify [--base ref] [--branch name] [--no-checks] | status [--base ref] [--json]\n' +
|
|
1664
|
+
' | security [--base ref] [--branch name] [--json]\n' +
|
|
1535
1665
|
' | next [--base ref] [--json] | adapters | trace <path>... [--json]\n' +
|
|
1536
1666
|
' | approve <name>... --reason "…" [--lane name] [--config file]\n' +
|
|
1537
1667
|
' | merge <branch> --delegated|--person [--into branch] | requests [--json]\n' +
|
|
@@ -1543,6 +1673,7 @@ function usage() {
|
|
|
1543
1673
|
' | map <story>|project [--json] | trail [--json] | journal [--json]\n' +
|
|
1544
1674
|
' | story <id> [--json] | timeline [--json]\n' +
|
|
1545
1675
|
' | team [--json] [--fresh] | team since [--json] [--peek]\n' +
|
|
1676
|
+
' | team init --tech-lead <handle> [--owner <lane>=<handles>] [--write] | team init --off [--write]\n' +
|
|
1546
1677
|
' | team claim <story> [--lease globs] | team keep|release <story>\n' +
|
|
1547
1678
|
' | team reassign <story> <handle> | team codeowners [--write] | team board\n' +
|
|
1548
1679
|
' | intend "<what and why>" [--touches globs] | intend agree|drop|duplicate <id> [--reason "…"] | intend record <id>',
|
package/hooks/guard-bash.mjs
CHANGED
|
@@ -178,6 +178,14 @@ if (runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+intend\s+(agree|drop|duplicat
|
|
|
178
178
|
block(
|
|
179
179
|
`Blocked: agreeing or dropping an intent is the tech lead's own decision, from their terminal or the code-kit pane. Tell them what you'd recommend and why, and stop.\nCommand: ${cmd}`,
|
|
180
180
|
);
|
|
181
|
+
// TEAM-13: setting the team up, or taking it out, rewrites the config: the lead's, once the person agrees.
|
|
182
|
+
if (
|
|
183
|
+
runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+team\s+init\b.*\s--write\b`)) &&
|
|
184
|
+
actor.kind !== 'lead'
|
|
185
|
+
)
|
|
186
|
+
block(
|
|
187
|
+
`Blocked: the team's setup is the lead's to write, once the person agrees (/code-kit:team-init). Tell the lead what you'd change, and stop.\nCommand: ${cmd}`,
|
|
188
|
+
);
|
|
181
189
|
// The harness settings are the person's to change, from the pane or their terminal.
|
|
182
190
|
if (runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+settings\s+set\b`)))
|
|
183
191
|
block(
|
package/hooks/lib/config.mjs
CHANGED
|
@@ -143,10 +143,11 @@ const isRegExp = (v) => {
|
|
|
143
143
|
|
|
144
144
|
/**
|
|
145
145
|
* ONB-5: the version of init's setup. 2 adds project skills with the lane agents, the design gate for
|
|
146
|
-
* lanes with screens, a QA lane for end-to-end tests, and companions set up before coding
|
|
147
|
-
* project set up before (no `setup`, or a lower one) is told what
|
|
146
|
+
* lanes with screens, a QA lane for end-to-end tests, and companions set up before coding; 3 adds the
|
|
147
|
+
* security reviewer (ONB-7, spec 17). A project set up before (no `setup`, or a lower one) is told what
|
|
148
|
+
* re-running init adds.
|
|
148
149
|
*/
|
|
149
|
-
export const SETUP_VERSION =
|
|
150
|
+
export const SETUP_VERSION = 3;
|
|
150
151
|
|
|
151
152
|
/** Every problem with a parsed config, as readable sentences; empty when it is valid. */
|
|
152
153
|
export function validate(c) {
|