@warren-dean/code-kit 0.9.1 → 0.10.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 +8 -0
- package/README.md +8 -4
- package/bin/code-kit.mjs +43 -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/mod/loop.mjs +14 -1
- package/package.json +1 -1
- package/skills/init/SKILL.md +11 -4
- package/skills/next/SKILL.md +19 -17
- package/skills/review/SKILL.md +19 -4
- package/skills/spec-design/SKILL.md +5 -2
- 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.10.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.10.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,14 @@
|
|
|
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.10.0 (2026-10-12)
|
|
6
|
+
|
|
7
|
+
An older setup is brought up to date before new work starts, and a security reviewer checks the changes that need one.
|
|
8
|
+
|
|
9
|
+
- **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.
|
|
10
|
+
- **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.
|
|
11
|
+
- **Setup version 3.** Re-running init records `"setup": 3`.
|
|
12
|
+
|
|
5
13
|
## 0.9.1 (2026-10-12)
|
|
6
14
|
|
|
7
15
|
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.
|
|
@@ -349,6 +352,7 @@ code-kit diff --config .claude/code-kit.draft.json # what a draft changes
|
|
|
349
352
|
code-kit baseline [--write] # layer violations in the code as it is
|
|
350
353
|
code-kit graph [--depth N] # which folders import which
|
|
351
354
|
code-kit verify [--base ref] [--branch name] [--no-checks] [--json] # a branch's changes, for CI and review
|
|
355
|
+
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
356
|
code-kit status [--base ref] [--json] # stories and requirements against the spec
|
|
353
357
|
code-kit next [--base ref] [--json] # the step to take now, from the project's state
|
|
354
358
|
code-kit trace <path>... [--json] # what a file is for: requirements, lane, layer rules
|
|
@@ -410,7 +414,7 @@ code-kit approve <name>... --reason "…" [--lane name] [--delegated | --via pan
|
|
|
410
414
|
| `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
415
|
| `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
416
|
| `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 (
|
|
417
|
+
| `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 |
|
|
414
418
|
| `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
419
|
| `branches.protected` | no | Default `["main", "master"]` |
|
|
416
420
|
| `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`. |
|
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';
|
|
@@ -1063,6 +1071,39 @@ function verify() {
|
|
|
1063
1071
|
out('No problems.');
|
|
1064
1072
|
}
|
|
1065
1073
|
|
|
1074
|
+
/**
|
|
1075
|
+
* ONB-7: whether a branch's changes need the security reviewer, and why: sign-in, permissions, outside
|
|
1076
|
+
* input, personal data, encryption, payments, secrets, commands built from strings, new dependencies,
|
|
1077
|
+
* or a security requirement the story cites.
|
|
1078
|
+
*/
|
|
1079
|
+
function security() {
|
|
1080
|
+
const { config } = load();
|
|
1081
|
+
const from = base(config, false);
|
|
1082
|
+
let branch = branchName;
|
|
1083
|
+
if (!branch) {
|
|
1084
|
+
try {
|
|
1085
|
+
branch = currentBranch('.');
|
|
1086
|
+
} catch {
|
|
1087
|
+
die('This checkout has no branch; pass --branch <name>.');
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (!refExists(branch)) die(`--branch ${branch} is not a known branch.`);
|
|
1091
|
+
const found = securityReview(resolve('.'), { base: from, branch, config });
|
|
1092
|
+
if (flag('--json')) return out(JSON.stringify({ base: from, ...found }, null, 2));
|
|
1093
|
+
const what = found.story ? `${found.story} on ${branch}` : branch;
|
|
1094
|
+
if (!found.needed)
|
|
1095
|
+
return out(
|
|
1096
|
+
`${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.`,
|
|
1097
|
+
);
|
|
1098
|
+
out(`${what} needs the security review. Against ${from}, it touches:`);
|
|
1099
|
+
for (const r of found.reasons) out(` ${reasonsInWords([r])}`);
|
|
1100
|
+
out(
|
|
1101
|
+
found.agent
|
|
1102
|
+
? `\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.`
|
|
1103
|
+
: `\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.`,
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1066
1107
|
const pad = (s, n) => String(s).padEnd(n);
|
|
1067
1108
|
|
|
1068
1109
|
function status() {
|
|
@@ -1521,6 +1562,7 @@ const commands = {
|
|
|
1521
1562
|
adapters,
|
|
1522
1563
|
next,
|
|
1523
1564
|
verify,
|
|
1565
|
+
security,
|
|
1524
1566
|
status,
|
|
1525
1567
|
unowned,
|
|
1526
1568
|
diff,
|
|
@@ -1532,6 +1574,7 @@ function usage() {
|
|
|
1532
1574
|
die(
|
|
1533
1575
|
'Usage: code-kit check | who <path>... | unowned | diff | baseline [--write] | graph [--depth N]\n' +
|
|
1534
1576
|
' | verify [--base ref] [--branch name] [--no-checks] | status [--base ref] [--json]\n' +
|
|
1577
|
+
' | security [--base ref] [--branch name] [--json]\n' +
|
|
1535
1578
|
' | next [--base ref] [--json] | adapters | trace <path>... [--json]\n' +
|
|
1536
1579
|
' | approve <name>... --reason "…" [--lane name] [--config file]\n' +
|
|
1537
1580
|
' | merge <branch> --delegated|--person [--into branch] | requests [--json]\n' +
|
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) {
|
package/hooks/lib/next.mjs
CHANGED
|
@@ -9,6 +9,8 @@ import { buildStatus, readStories, DESIGN, LEAD } from './plan.mjs';
|
|
|
9
9
|
import { pendingSetup } from './adapters/index.mjs';
|
|
10
10
|
import { CONFIG_FILE, SETUP_VERSION } from './config.mjs';
|
|
11
11
|
import { readQueue, waiting } from './queue.mjs';
|
|
12
|
+
import { laneOf } from './rules.mjs';
|
|
13
|
+
import { SECURITY_AGENT_FILE } from './security.mjs';
|
|
12
14
|
|
|
13
15
|
export const DRAFT_FILE = '.claude/code-kit.draft.json';
|
|
14
16
|
export const BRIEF_STATES = ['framed', 'explored', 'scoped', 'specified', 'planned', 'ready'];
|
|
@@ -149,11 +151,17 @@ export function nextStep(root, base) {
|
|
|
149
151
|
}
|
|
150
152
|
const [setup] = pendingSetup(raw, root);
|
|
151
153
|
if (setup) return result('setup', setup.why, { args: setup.skill });
|
|
152
|
-
// ONB-5: a project set up before setup did all of this is told what re-running init adds
|
|
153
|
-
|
|
154
|
+
// ONB-5: a project set up before setup did all of this is told what re-running init adds; when
|
|
155
|
+
// ONB-6 finds what it's missing, no new story starts until init has added it.
|
|
156
|
+
const older = (raw?.setup ?? 1) < SETUP_VERSION;
|
|
157
|
+
const gaps = older ? setupGaps(config, readStories(root, plan), files) : [];
|
|
158
|
+
if (older && !gaps.length)
|
|
154
159
|
attention.push(
|
|
155
|
-
"This project's code-kit setup predates what init now sets up: project skills with each lane agent, designs approved before screens are built, a QA lane for end-to-end tests, and companion tools set up before coding. Re-run /code-kit:init to add what's missing; it keeps everything that's still right and shows you the difference first.",
|
|
160
|
+
"This project's code-kit setup predates what init now sets up: project skills with each lane agent, designs approved before screens are built, a QA lane for end-to-end tests, a security reviewer that checks each sensitive change as an attacker would, and companion tools set up before coding. Re-run /code-kit:init to add what's missing; it keeps everything that's still right and shows you the difference first.",
|
|
156
161
|
);
|
|
162
|
+
const missingWords = gaps.length
|
|
163
|
+
? `This project's setup is missing what init now sets up: ${gaps.join('; ')}.`
|
|
164
|
+
: '';
|
|
157
165
|
// ONB-1: a lane agent naming a skill that doesn't exist works without the recipe it was given.
|
|
158
166
|
for (const missing of missingSkills(root)) attention.push(missing);
|
|
159
167
|
|
|
@@ -194,6 +202,16 @@ export function nextStep(root, base) {
|
|
|
194
202
|
// ONB-2: design stories, designed with the person before any screen that depends on them is built.
|
|
195
203
|
const forDesign = readyStories.filter((s) => buildable(s) && s.lane === DESIGN).map((s) => s.id);
|
|
196
204
|
const unbuildable = readyStories.filter((s) => !buildable(s)).map((s) => s.id);
|
|
205
|
+
// ONB-6: work already started finishes; nothing new starts until init has filled the gaps.
|
|
206
|
+
const held = gaps.length
|
|
207
|
+
? [...ready, ...forDesign, ...forLead].sort((a, b) => a.slice(3) - b.slice(3))
|
|
208
|
+
: [];
|
|
209
|
+
if (held.length) {
|
|
210
|
+
ready.length = forDesign.length = forLead.length = 0;
|
|
211
|
+
attention.push(
|
|
212
|
+
`${missingWords} ${held.join(', ')} ${held.length === 1 ? 'waits' : 'wait'} until /code-kit:init has added it.`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
197
215
|
if (unbuildable.length)
|
|
198
216
|
attention.push(
|
|
199
217
|
`${unbuildable.join(', ')} can't be dispatched until the plan's gaps are fixed.`,
|
|
@@ -262,6 +280,12 @@ export function nextStep(root, base) {
|
|
|
262
280
|
`${forLead.join(', ')} ${forLead.length === 1 ? 'is' : 'are'} the lead's own: build ${forLead.length === 1 ? 'it' : 'them'} on ${forLead.map((id) => `lead/${id.toLowerCase()}`).join(', ')}, then review like any branch.`,
|
|
263
281
|
{ args: forLead.join(' ') },
|
|
264
282
|
);
|
|
283
|
+
// ONB-6: with nothing in flight, the setup is brought up to date before the next story.
|
|
284
|
+
if (gaps.length && !building.length && !queued.length)
|
|
285
|
+
return result(
|
|
286
|
+
'update',
|
|
287
|
+
`${missingWords} Re-run /code-kit:init before the next story: it shows the person the difference before anything changes.`,
|
|
288
|
+
);
|
|
265
289
|
if (building.length)
|
|
266
290
|
return result(
|
|
267
291
|
'wait',
|
|
@@ -294,6 +318,66 @@ export function nextStep(root, base) {
|
|
|
294
318
|
);
|
|
295
319
|
}
|
|
296
320
|
|
|
321
|
+
const SCREEN_LANE = /(^|-)(web|mobile|frontend|front-end|ui|desktop|ios|android|admin|portal)(-|$)/;
|
|
322
|
+
const SCREEN_FILE = /\.(tsx|jsx|vue|svelte|swift|dart)$/;
|
|
323
|
+
const TEST_FILE = /(^|\/)(__tests__|e2e|tests?)\/|\.(test|spec|stories)\.\w+$/;
|
|
324
|
+
const QA_LANE = /(^|-)(qa|e2e|tests?)(-|$)/;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* ONB-6: what a setup older than init's is missing, in words, from the config, the plan and the code.
|
|
328
|
+
* A lane owns screens when its name or agent says so (web, mobile, desktop…) or it holds screen
|
|
329
|
+
* files. It's missing the design gate, design stories before the screen stories not yet started, and a
|
|
330
|
+
* QA lane for the end-to-end suites; and, with or without screens, the security reviewer (ONB-7), when
|
|
331
|
+
* its agent file isn't among the tracked files.
|
|
332
|
+
*/
|
|
333
|
+
export function setupGaps(config, stories, files = []) {
|
|
334
|
+
const gaps = screenGaps(config, stories, files);
|
|
335
|
+
if (!files.includes(SECURITY_AGENT_FILE))
|
|
336
|
+
gaps.push(
|
|
337
|
+
`a security reviewer (${SECURITY_AGENT_FILE}) that checks each change touching sign-in, permissions, personal data, payments, secrets or dependencies as an attacker would`,
|
|
338
|
+
);
|
|
339
|
+
return gaps;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** ONB-6: the gaps of a setup whose lanes own screens: the design gate, design stories, a QA lane. */
|
|
343
|
+
function screenGaps(config, stories, files) {
|
|
344
|
+
const lanes = Object.entries(config.lanes);
|
|
345
|
+
const holdsScreens = new Set(
|
|
346
|
+
files.filter((f) => SCREEN_FILE.test(f) && !TEST_FILE.test(f)).map((f) => laneOf(f, config)),
|
|
347
|
+
);
|
|
348
|
+
const screens = lanes
|
|
349
|
+
.filter(
|
|
350
|
+
([name, l]) => SCREEN_LANE.test(name) || SCREEN_LANE.test(l.agent) || holdsScreens.has(name),
|
|
351
|
+
)
|
|
352
|
+
.map(([name]) => name);
|
|
353
|
+
if (!screens.length) return [];
|
|
354
|
+
const gaps = [];
|
|
355
|
+
const list = (names) => names.join(', ').replace(/, ([^,]*)$/, ' and $1');
|
|
356
|
+
if (!config.designGate)
|
|
357
|
+
gaps.push(
|
|
358
|
+
`a design gate for ${list(screens)}, so screens are built only from approved designs`,
|
|
359
|
+
);
|
|
360
|
+
// A screen story not yet started, with no design story among everything it depends on.
|
|
361
|
+
const byId = new Map(stories.map((s) => [s.id, s]));
|
|
362
|
+
const designed = (s, seen = new Set()) =>
|
|
363
|
+
s.dependsOn.some((d) => {
|
|
364
|
+
if (seen.has(d)) return false;
|
|
365
|
+
seen.add(d);
|
|
366
|
+
const dep = byId.get(d);
|
|
367
|
+
return dep && (dep.lane === DESIGN || designed(dep, seen));
|
|
368
|
+
});
|
|
369
|
+
const undesigned = stories
|
|
370
|
+
.filter((s) => screens.includes(s.lane) && ['todo', 'ready'].includes(s.status) && !designed(s))
|
|
371
|
+
.map((s) => s.id);
|
|
372
|
+
if (undesigned.length)
|
|
373
|
+
gaps.push(
|
|
374
|
+
`design stories before the screens: ${list(undesigned)} ${undesigned.length === 1 ? 'is' : 'are'} planned with no design to build from`,
|
|
375
|
+
);
|
|
376
|
+
if (!lanes.some(([name, l]) => QA_LANE.test(name) || QA_LANE.test(l.agent)))
|
|
377
|
+
gaps.push('a QA lane with a test-engineer for the end-to-end tests of the journeys');
|
|
378
|
+
return gaps;
|
|
379
|
+
}
|
|
380
|
+
|
|
297
381
|
/** The code-kit skills every agent may name without a project copy. */
|
|
298
382
|
const KIT_SKILLS = (() => {
|
|
299
383
|
try {
|
package/hooks/lib/plan.mjs
CHANGED
|
@@ -20,7 +20,8 @@ export const DESIGN = 'design';
|
|
|
20
20
|
const TEST =
|
|
21
21
|
/(^|\/)(tests?|__tests__|e2e)\/|\.(test|spec)\.[^/]+$|_test\.(go|py)$|(^|\/)test_[^/]+\.py$/;
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/** The markdown files at `path`: the file itself, or every one under the folder, sorted. */
|
|
24
|
+
export const markdownFiles = (root, path) => {
|
|
24
25
|
const full = join(root, path);
|
|
25
26
|
if (!existsSync(full)) return [];
|
|
26
27
|
if (statSync(full).isFile()) return path.endsWith('.md') ? [path] : [];
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// ONB-7: which branches the security reviewer looks at. Security is a reviewer, not a lane: it owns no
|
|
2
|
+
// paths and checks everyone's work. A story's change gets the security review when it touches sign-in,
|
|
3
|
+
// permissions, outside input, personal data, encryption, payments, secrets or commands built from
|
|
4
|
+
// strings, adds dependencies, or cites a requirement from a spec's `## Security requirements`. Read
|
|
5
|
+
// from git (base...branch): the files changed and the lines added, tests and docs left out.
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { isDependencyFile } from './dependencies.mjs';
|
|
10
|
+
import { markdownFiles, readStories } from './plan.mjs';
|
|
11
|
+
import { storyOfBranch } from './teamfacts.mjs';
|
|
12
|
+
|
|
13
|
+
export const SECURITY_AGENT = 'security-reviewer';
|
|
14
|
+
export const SECURITY_AGENT_FILE = `.claude/agents/${SECURITY_AGENT}.md`;
|
|
15
|
+
|
|
16
|
+
// Tests, docs and design files describe the change; the review looks at the code that ships.
|
|
17
|
+
const NOT_SHIPPED =
|
|
18
|
+
/(^|\/)(tests?|__tests__|e2e|docs?|design)\/|\.(test|spec|stories)\.[^/]+$|_test\.(go|py)$|(^|\/)test_[^/]+\.py$|\.(md|mdx|txt|png|jpe?g|gif|svg|webp)$/i;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* What makes a change sensitive, by kind: `path` matches a changed file's path, `code` (one pattern or
|
|
22
|
+
* several) an added line.
|
|
23
|
+
* Each kind reads as the reason a person is given.
|
|
24
|
+
*/
|
|
25
|
+
export const SENSITIVE = [
|
|
26
|
+
{
|
|
27
|
+
kind: 'sign-in',
|
|
28
|
+
why: 'sign-in or sessions',
|
|
29
|
+
path: /(^|[/_.-])(auth\w*|login|logout|sign-?in|sign-?up|sessions?|oauth|sso|jwt|passwords?|mfa|otp)([/_.-]|$)/i,
|
|
30
|
+
code: /\b(signIn|signOut|login|logout|getSession|setSession|jwt\.(sign|verify)|jsonwebtoken|passport\.|bcrypt|argon2|verifyPassword|hashPassword|Set-Cookie|cookies\(\)|httpOnly)\b/,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
kind: 'permissions',
|
|
34
|
+
why: 'permissions',
|
|
35
|
+
path: /(^|[/_.-])(permissions?|roles?|rbac|acl|polic(y|ies)|guards?|middleware|authori[sz]\w*)([/_.-]|$)/i,
|
|
36
|
+
code: [
|
|
37
|
+
/\b(isAdmin|hasRole|hasPermission|can[A-Z]\w*|[Aa]uthori[sz]e\w*|requireRole)\b/,
|
|
38
|
+
/\b(enable row level security|create policy|grant\s+\w+\s+on)\b/i,
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
kind: 'input',
|
|
43
|
+
why: 'input from outside (requests, forms, uploads, webhooks)',
|
|
44
|
+
path: /(^|[/_.-])(webhooks?|uploads?|handlers?)([/_.-]|$)|(^|\/)(api|routes?)\/|(^|\/)route\.[jt]s$/i,
|
|
45
|
+
code: /\b(req\.(body|query|params|files?)|request\.(json|form|args|body)\(?|formData\(\)|searchParams|FormData|multer|busboy|ctx\.request\.body|@Body\(|@Query\(|@Param\()/,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
kind: 'personal data',
|
|
49
|
+
why: 'personal data',
|
|
50
|
+
path: /(^|[/_.-])(users?|profiles?|accounts?|customers?|patients?|members?|children|students?|contacts?)([/_.-]|$)/i,
|
|
51
|
+
code: /\b(email|phone(_?number)?|date_?of_?birth|dob|home_?address|street_?address|passport|tax_?file|national_?id|medicare|ssn|first_?name|last_?name|full_?name)\b/i,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
kind: 'encryption',
|
|
55
|
+
why: 'encryption, hashing or random values',
|
|
56
|
+
path: /(^|[/_.-])(crypto|encrypt\w*|decrypt\w*|ciphers?|hash(ing)?|keys?|kms|signing)([/_.-]|$)/i,
|
|
57
|
+
code: /\b(createCipheriv|createDecipheriv|createHash|createHmac|randomBytes|Math\.random|crypto\.subtle|md5|sha1|AES|RSA|hashlib|Fernet)\b/,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
kind: 'payments',
|
|
61
|
+
why: 'payments',
|
|
62
|
+
path: /(^|[/_.-])(payments?|billing|checkout|invoices?|stripe|subscriptions?|refunds?|payouts?|wallet)([/_.-]|$)/i,
|
|
63
|
+
code: /\b(stripe|paymentIntent|checkout\.sessions|charge|refund|payout|amount_?cents)\b/i,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
kind: 'secrets',
|
|
67
|
+
why: 'secrets or credentials',
|
|
68
|
+
path: /(^|\/)\.env(\.|$)|(^|[/_.-])(secrets?|credentials?|tokens?)([/_.-]|$)/i,
|
|
69
|
+
code: /\b(api_?key|secret_?key|client_?secret|private_?key|access_?token|refresh_?token|password\s*[:=])/i,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
kind: 'injection',
|
|
73
|
+
why: 'queries, commands, templates or redirects built from strings',
|
|
74
|
+
code: /(\$queryRawUnsafe|\$executeRawUnsafe|\.raw\(|sql\.unsafe|execute\(\s*[`'"]?\s*(select|insert|update|delete)\b.*(\$\{|\+)|child_process|\bexec(Sync)?\(|\bspawn(Sync)?\(|subprocess\.|os\.system|\beval\(|new Function\(|dangerouslySetInnerHTML|\.innerHTML\s*=|v-html|\{\{\{|pickle\.loads|yaml\.load\(|unserialize\(|res\.redirect\(|redirect\(\s*(req|request|params|searchParams)|fetch\(\s*(req|request|params|body|url)\b)/i,
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The requirement IDs of each spec's `## Security requirements` section (spec-design writes a spec's
|
|
80
|
+
* security requirements there, as `### AREA-n` headings like any other requirement).
|
|
81
|
+
*/
|
|
82
|
+
export function securityRequirementIds(texts) {
|
|
83
|
+
const ids = new Set();
|
|
84
|
+
for (const text of texts) {
|
|
85
|
+
let inSecurity = false;
|
|
86
|
+
for (const line of text.split('\n')) {
|
|
87
|
+
if (/^##\s/.test(line)) inSecurity = /^##\s+Security requirements\b/i.test(line);
|
|
88
|
+
const m = inSecurity && line.match(/^###\s+((?!ST-)[A-Z][A-Z0-9]*-\d+)\b/);
|
|
89
|
+
if (m) ids.add(m[1]);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return ids;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Added lines of a unified diff (`git diff -U0`): [{ file, line, text }]. */
|
|
96
|
+
export function addedLines(diff) {
|
|
97
|
+
const added = [];
|
|
98
|
+
let file = null;
|
|
99
|
+
let line = 0;
|
|
100
|
+
for (const l of diff.split('\n')) {
|
|
101
|
+
if (l.startsWith('+++ ')) {
|
|
102
|
+
file = l === '+++ /dev/null' ? null : l.slice(4).replace(/^b\//, '');
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const hunk = l.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
106
|
+
if (hunk) {
|
|
107
|
+
line = Number(hunk[1]);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!file) continue;
|
|
111
|
+
if (l.startsWith('+')) added.push({ file, line: line++, text: l.slice(1) });
|
|
112
|
+
else if (l.startsWith(' ')) line++;
|
|
113
|
+
}
|
|
114
|
+
return added;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const examples = (list) =>
|
|
118
|
+
`${list.slice(0, 3).join(', ')}${list.length > 3 ? ` and ${list.length - 3} more` : ''}`;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Why a change needs the security review, in words, one per kind: [{ kind, why, where: [...] }].
|
|
122
|
+
* Empty when it touches nothing sensitive. `files` are the changed files, `added` the added lines,
|
|
123
|
+
* `cites` the story's requirements and `security` the IDs of the specs' security requirements.
|
|
124
|
+
*/
|
|
125
|
+
export function securityReasons({ files = [], added = [], cites = [], security = new Set() }) {
|
|
126
|
+
const reasons = [];
|
|
127
|
+
const shipped = files.filter((f) => !NOT_SHIPPED.test(f));
|
|
128
|
+
const lines = added.filter((a) => !NOT_SHIPPED.test(a.file));
|
|
129
|
+
for (const s of SENSITIVE) {
|
|
130
|
+
const code = [s.code].flat();
|
|
131
|
+
const where = [
|
|
132
|
+
...(s.path ? shipped.filter((f) => s.path.test(f)) : []),
|
|
133
|
+
...lines.filter((a) => code.some((c) => c.test(a.text))).map((a) => `${a.file}:${a.line}`),
|
|
134
|
+
];
|
|
135
|
+
if (where.length) reasons.push({ kind: s.kind, why: s.why, where: [...new Set(where)] });
|
|
136
|
+
}
|
|
137
|
+
const deps = files.filter(isDependencyFile);
|
|
138
|
+
if (deps.length) reasons.push({ kind: 'dependencies', why: 'dependencies', where: deps });
|
|
139
|
+
const cited = cites.filter((id) => security.has(id));
|
|
140
|
+
if (cited.length)
|
|
141
|
+
reasons.push({ kind: 'requirements', why: 'security requirements it cites', where: cited });
|
|
142
|
+
return reasons;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The reasons as one sentence: "sign-in or sessions (src/auth/login.ts), dependencies (package.json)". */
|
|
146
|
+
export const reasonsInWords = (reasons) =>
|
|
147
|
+
reasons.map((r) => `${r.why} (${examples(r.where)})`).join('; ');
|
|
148
|
+
|
|
149
|
+
const git = (root, ...args) =>
|
|
150
|
+
execFileSync('git', ['-C', root, ...args], {
|
|
151
|
+
encoding: 'utf8',
|
|
152
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
153
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Whether `branch` needs the security review against `base`, read from git:
|
|
158
|
+
* { branch, story, needed, reasons, agent } where `agent` says whether the project has the
|
|
159
|
+
* security-reviewer agent.
|
|
160
|
+
*/
|
|
161
|
+
export function securityReview(root, { base, branch, config }) {
|
|
162
|
+
const range = `${base}...${branch}`;
|
|
163
|
+
const files = git(root, 'diff', '--name-only', '--no-renames', range).split('\n').filter(Boolean);
|
|
164
|
+
const added = addedLines(git(root, 'diff', '-U0', '--no-renames', '--no-color', range));
|
|
165
|
+
const story = storyOfBranch(branch);
|
|
166
|
+
const { specs, plan } = config.docs ?? {};
|
|
167
|
+
const cites =
|
|
168
|
+
(plan ? readStories(root, plan) : []).find((s) => s.id === story)?.requirements ?? [];
|
|
169
|
+
const texts = specs
|
|
170
|
+
? markdownFiles(root, specs).map((f) => readFileSync(join(root, f), 'utf8'))
|
|
171
|
+
: [];
|
|
172
|
+
const reasons = securityReasons({ files, added, cites, security: securityRequirementIds(texts) });
|
|
173
|
+
return {
|
|
174
|
+
branch,
|
|
175
|
+
story,
|
|
176
|
+
needed: reasons.length > 0,
|
|
177
|
+
reasons,
|
|
178
|
+
agent: existsSync(join(root, SECURITY_AGENT_FILE)),
|
|
179
|
+
};
|
|
180
|
+
}
|
package/hooks/mod/loop.mjs
CHANGED
|
@@ -3,7 +3,16 @@
|
|
|
3
3
|
// existing skills, and the hooks judge every call as before.
|
|
4
4
|
|
|
5
5
|
/** The steps the loop hands the lead; the rest (spec-design, init, check) wait for the person. */
|
|
6
|
-
const LEAD_STEPS = new Set([
|
|
6
|
+
const LEAD_STEPS = new Set([
|
|
7
|
+
'dispatch',
|
|
8
|
+
'review',
|
|
9
|
+
'lead',
|
|
10
|
+
'merge',
|
|
11
|
+
'rules',
|
|
12
|
+
'design',
|
|
13
|
+
'setup',
|
|
14
|
+
'update',
|
|
15
|
+
]);
|
|
7
16
|
|
|
8
17
|
/**
|
|
9
18
|
* The prompt for a step from `code-kit next --json` (LOOP-1), or null when it isn't the lead's to
|
|
@@ -27,6 +36,9 @@ export function leadPrompt(step, { cli = 'code-kit' } = {}) {
|
|
|
27
36
|
return ids.length
|
|
28
37
|
? `Before any code is written, set up ${ids[0].split(':')[0]}: run /${ids[0]}. ${step.why ?? ''}`.trim()
|
|
29
38
|
: null;
|
|
39
|
+
// ONB-6: an older setup brought up to date before the next story; init shows the person the diff.
|
|
40
|
+
if (step.step === 'update')
|
|
41
|
+
return `Before the next story, bring code-kit's setup up to date: run /code-kit:init. ${step.why ?? ''}`.trim();
|
|
30
42
|
// ONB-2: the screens designed, and approved by the person, before they're built.
|
|
31
43
|
if (step.step === 'design')
|
|
32
44
|
return ids.length
|
|
@@ -61,6 +73,7 @@ export function stepLabel(step) {
|
|
|
61
73
|
rules: 'Rules onto',
|
|
62
74
|
design: 'Design',
|
|
63
75
|
setup: 'Set up',
|
|
76
|
+
update: 'Update the setup',
|
|
64
77
|
}[step.step] ?? step.step;
|
|
65
78
|
return ids ? `${verb} ${ids}` : verb;
|
|
66
79
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warren-dean/code-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Keeps AI coding agents inside your project's rules: lanes, layer rules, protected paths and proof before finish, enforced by Claude Code hooks and in CI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Warren Langeveldt",
|
package/skills/init/SKILL.md
CHANGED
|
@@ -60,7 +60,7 @@ Write `.claude/code-kit.draft.json`. Follow `${CLAUDE_PLUGIN_ROOT}/README.md`
|
|
|
60
60
|
- `agent`: a kebab-case agent name, for example `api-engineer`;
|
|
61
61
|
- `paths` and `exclude`: every path must have exactly one owner.
|
|
62
62
|
- **A QA lane:** when lanes own screens, or the plan's journeys cross lanes, a `qa` lane with a `test-engineer` agent owning the end-to-end suites (the plan's work split names them; brownfield: the folders the existing end-to-end tests live in), even if the docs forgot it. Its agent's proving command runs the suites. Add a `checks` entry for them scoped by `files` to the suites, so verify runs them when they change. Unit tests stay with each lane.
|
|
63
|
-
- **`setup`:** `
|
|
63
|
+
- **`setup`:** `3`, the version of this setup, so `code-kit next` can tell an older project what re-running init adds.
|
|
64
64
|
- **`lead.paths`:** contracts, specs, design, root config, and anything the docs reserve for the lead.
|
|
65
65
|
- **`anyActor`:** lockfiles, if lanes may update them.
|
|
66
66
|
- **`protected`:** registers and contracts that need a person's sign-off, each with an `approval` name and a `why`.
|
|
@@ -102,6 +102,12 @@ Draft, alongside the config:
|
|
|
102
102
|
- its lane rules.
|
|
103
103
|
- If an agent already exists, keep its content and add only what's missing.
|
|
104
104
|
- Drop the spec-check lines if the config has no `docs.specs`.
|
|
105
|
+
- **A security reviewer:** write `.claude/agents/security-reviewer.md` from `${CLAUDE_PLUGIN_ROOT}/templates/security-reviewer.md`, in the same approval as the lane agents, whenever the product handles sign-in, personal data, payments, input from outside (forms, APIs, webhooks, uploads) or secrets. That's almost every product; leave it out only for one with none of these, such as a static site or a local tool with no accounts, and say why. It is a reviewer, not a lane: no entry in `lanes`, no paths, and only read-only tools (Read, Grep, Glob, and Bash for commands that change nothing), so the hooks treat it as read-only too. Fill it in from the docs (or the code, for brownfield):
|
|
106
|
+
- what's sensitive, who might attack it and the trust boundaries, from the specs' `## Threat model` sections (brownfield, or specs without one: from the data the code stores and the routes it exposes, as a question to the person);
|
|
107
|
+
- the spec files it reads;
|
|
108
|
+
- the privacy and data residency targets in the principles;
|
|
109
|
+
- the project's audit commands (for example `npm audit --omit=dev`, `pip-audit`), each run once now; leave out one that isn't installed, and say so.
|
|
110
|
+
- If the agent already exists, keep its content and add only what's missing.
|
|
105
111
|
- **CLAUDE.md:** add the section from `${CLAUDE_PLUGIN_ROOT}/templates/claude-md.md`.
|
|
106
112
|
- **Project skills, with the agents** (recipes like "new screen", "new migration" or "new endpoint"): one per recipe in the architecture's `## Recipes`, or any other doc that describes one step by step. Draft them now, alongside the agents, and show them in the same approval; the agents and their skills are one piece of work. Brownfield code without recipes in its docs: propose recipes from the patterns the code already follows (how its existing screens, endpoints or migrations are built), as drafts the person approves. Never write an empty one.
|
|
107
113
|
- Write each as `.claude/skills/<name>/SKILL.md`, saying in its description which recipe in which doc it comes from, so an update can find it again.
|
|
@@ -110,7 +116,7 @@ Draft, alongside the config:
|
|
|
110
116
|
Show the person:
|
|
111
117
|
|
|
112
118
|
1. the layers and what each may depend on, and for brownfield the existing violations the baseline will record;
|
|
113
|
-
2. the lanes, their agents and their paths;
|
|
119
|
+
2. the lanes, their agents and their paths, and the security reviewer (or why it's left out);
|
|
114
120
|
3. protected paths, blocked and restricted commands, and checks (including any left out because they fail today);
|
|
115
121
|
and whether the lead may record approvals given in chat (`approvals.lead`), and how hands-off the build is (`harness.autonomy`);
|
|
116
122
|
4. the project skills, and which lane agents use each;
|
|
@@ -144,10 +150,11 @@ Re-run whenever the build has moved on: new apps or packages, folders that moved
|
|
|
144
150
|
|
|
145
151
|
1. **Re-read** the docs and the repository as in section 1, including the brownfield steps if the code has grown past the docs.
|
|
146
152
|
2. **Draft** the whole config again into `.claude/code-kit.draft.json`, starting from the live one. Keep everything that's still right, so the diff shows only real changes. If the live config has no `harness.autonomy`, ask the person about it as in section 2.
|
|
147
|
-
A config without `"setup":
|
|
153
|
+
A config without `"setup": 3` predates what setup now includes; add whichever of these the project is missing, as section 2 and 3 describe, and set `"setup": 3`:
|
|
148
154
|
- project skills for the recipes in the docs (or, without recipes, the patterns the code follows), listed on the lane agents;
|
|
149
155
|
- the design gate for every lane that owns screens, with its register; and design stories in the plan for screens not yet built (ask the person before adding stories);
|
|
150
156
|
- the QA lane, its agent and its checks, when there are screens or journeys across lanes;
|
|
157
|
+
- the security reviewer (`.claude/agents/security-reviewer.md`), unless the product handles no sign-in, personal data, payments, outside input or secrets; and, as a proposal for the person, a threat model and security requirements for each spec that lacks them (spec-design's section 4);
|
|
151
158
|
- `"adapters": { "<name>": true }` for a companion that's installed but not set up, which is then set up before any code.
|
|
152
159
|
3. **Show the difference:**
|
|
153
160
|
|
|
@@ -159,7 +166,7 @@ Re-run whenever the build has moved on: new apps or packages, folders that moved
|
|
|
159
166
|
`diff` lists lanes, layers, checks and rules that were added (`+`), removed (`-`) or changed (`~`), and how many files change owner, route by route. Explain each change in a sentence, with where it came from: the doc section, or the folder in the repository.
|
|
160
167
|
|
|
161
168
|
4. **Also propose:**
|
|
162
|
-
- agent files for new lanes, and updates to agents whose paths changed;
|
|
169
|
+
- agent files for new lanes, and updates to agents whose paths changed, and to the security reviewer when the specs' threat models changed;
|
|
163
170
|
- **project skills**, by reading the recipes in the docs again and comparing them with `.claude/skills/`:
|
|
164
171
|
- a new skill for each recipe the docs now describe that has none;
|
|
165
172
|
- an update to each skill whose recipe changed, showing what changed;
|
package/skills/next/SKILL.md
CHANGED
|
@@ -15,23 +15,24 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" next --json
|
|
|
15
15
|
|
|
16
16
|
It returns `step`, `why`, `args`, `then` (steps that can follow straight after) and `attention` (things the person should know). The order it applies:
|
|
17
17
|
|
|
18
|
-
| State
|
|
19
|
-
|
|
|
20
|
-
| Invalid config
|
|
21
|
-
| No config, a draft waiting
|
|
22
|
-
| No config, a spec still being shaped (brief not `ready`)
|
|
23
|
-
| No config, a ready spec
|
|
24
|
-
| No config, existing code or docs
|
|
25
|
-
| Nothing at all
|
|
26
|
-
| Config, but no specs, plan or stories
|
|
27
|
-
| A companion the config wants, not set up yet
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
18
|
+
| State | Step |
|
|
19
|
+
| ------------------------------------------------------------ | --------------------------------------------------------- |
|
|
20
|
+
| Invalid config | `check`, then fix the config |
|
|
21
|
+
| No config, a draft waiting | `init`, to show it for approval |
|
|
22
|
+
| No config, a spec still being shaped (brief not `ready`) | `spec-design`, resuming |
|
|
23
|
+
| No config, a ready spec | `init <docs folder>` |
|
|
24
|
+
| No config, existing code or docs | `init` |
|
|
25
|
+
| Nothing at all | `spec-design` |
|
|
26
|
+
| Config, but no specs, plan or stories | `spec-design` |
|
|
27
|
+
| A companion the config wants, not set up yet | `setup`: run that tool's init skill (`args`), before code |
|
|
28
|
+
| An older setup missing what init now adds, nothing in flight | `update`: re-run init, before any new story |
|
|
29
|
+
| Stories finished on their branches | `review`, then `dispatch` what's ready |
|
|
30
|
+
| Design stories ready (`**Lane:** design`) | `design`, then `dispatch` what needs no design |
|
|
31
|
+
| Stories ready | `dispatch` |
|
|
32
|
+
| The lead's own stories ready (`**Lane:** lead`) | `lead`: the lead builds them itself |
|
|
33
|
+
| Stories being built, nothing else can start | wait |
|
|
34
|
+
| Ready stories with gaps in the plan, or a dependency cycle | `spec-design`, to fix the plan |
|
|
35
|
+
| Every story done | done: `status`, then `spec-design` for the next milestone |
|
|
35
36
|
|
|
36
37
|
Tell the person in one or two lines what you found and what you're about to run, with anything under `attention`. If `attention` says the folder isn't a git repository, run `git init` before init (spec-design doesn't need one).
|
|
37
38
|
|
|
@@ -40,6 +41,7 @@ Tell the person in one or two lines what you found and what you're about to run,
|
|
|
40
41
|
Run the named skill with the Skill tool: `code-kit:<step>`, with `args` (and the person's argument, if they gave one). Follow that skill in full. This skill only chooses; it never does a step's work itself.
|
|
41
42
|
|
|
42
43
|
- **`setup`:** run the skill named in `args` (for example `context-graph:init`); it's another plugin's, so call it by that full name.
|
|
44
|
+
- **`update`:** run `code-kit:init`. `why` names what the setup is missing (the design gate, design stories, a QA lane, a security reviewer); init adds those, shows the person the difference, and sets `"setup": 3`. Until then no new story starts; work already started goes on to review and merge.
|
|
43
45
|
- **`lead`:** there's no skill to run. Build each story yourself, on the branch `lead/st-<n>`: spec-check it first if it cites requirements, do the work, commit, then run `code-kit:review` on the branch like any other. A spike's result goes in the docs it names.
|
|
44
46
|
- **`wait`:** say which stories are being built and on which branches, then stop. When a lane agent finishes, its notification arrives in this session; run this skill again then. If a story has been in progress with no agent running (the session ended, or the agent stopped without committing), dispatch it again.
|
|
45
47
|
- **`done`:** give the `status` summary in a few lines, then offer to run spec-design for the next milestone or feature.
|
package/skills/review/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: review
|
|
3
|
-
description: The lead's review of a lane's finished branch before it merges. Runs code-kit verify on the branch, checks the work against its story's requirements and acceptance criteria, the spec-check report, the approval log and the tests, and ends with merge or send back. Use when a lane agent or person finishes a story, when the person asks to review a branch or pull request, or runs /code-kit:review [branch, story ID or PR number].
|
|
3
|
+
description: The lead's review of a lane's finished branch before it merges. Runs code-kit verify on the branch, checks the work against its story's requirements and acceptance criteria, the spec-check report, the approval log and the tests, runs the security reviewer on sensitive changes, and ends with merge or send back. Use when a lane agent or person finishes a story, when the person asks to review a branch or pull request, or runs /code-kit:review [branch, story ID or PR number].
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Review a lane's branch
|
|
@@ -47,12 +47,27 @@ Read the story, every requirement it cites, and the full diff (`git diff <base>.
|
|
|
47
47
|
- **Only the story:** changes beyond the story (refactors elsewhere, other stories' requirements, new dependencies) are called out. They may be fine, but they're the person's call.
|
|
48
48
|
- **Contracts respected:** the code uses the contracts as written. A lane that needed a contract change should have asked, not worked around it.
|
|
49
49
|
|
|
50
|
-
## 4.
|
|
50
|
+
## 4. The security review
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Ask whether the change needs it:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" security --base <base> --branch <branch>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
It does when the diff touches sign-in, permissions, input from outside, personal data, encryption, payments, secrets, or queries and commands built from strings, adds dependencies, or the story cites a security requirement; it says which, and where. When it does:
|
|
59
|
+
|
|
60
|
+
- Run the `security-reviewer` agent (Agent tool, `subagent_type` `security-reviewer`) with the branch, the base, the story, its requirements, the spec files it touches (their `## Threat model` and `## Security requirements`) and the reasons `security` gave. It reads only, and ends with the same findings block as code-kit's reviewer: each finding graded blocker, concern or nit, with where, the attack and the fix.
|
|
61
|
+
- **A blocker fails the review**, like a failing test: the branch goes back to its lane with the finding (where, the attack, the fix). Concerns are yours to weigh: send back the ones the threat model says matter, and put the rest in the report. Nits go in the report.
|
|
62
|
+
- No `security-reviewer` agent in the project: `security` says so. Review the diff yourself against the same list in `${CLAUDE_PLUGIN_ROOT}/templates/security-reviewer.md`, and tell the person that re-running `/code-kit:init` adds the agent.
|
|
63
|
+
- A background reviewer can't start agents: it lists the security review as due, as a concern with the reasons, and the lead runs it.
|
|
64
|
+
|
|
65
|
+
## 5. Verdict
|
|
66
|
+
|
|
67
|
+
Report the table, `verify`'s result, the security review's findings (or that none was needed) and one of:
|
|
53
68
|
|
|
54
69
|
- **Files any agent may write** (`anyActor`, such as a lockfile or recorded decisions) have no owning lane, but they have a reviewer: `trace <file>` names it (the lead unless the config names a lane). Review those changes as that reviewer would, or ask that lane's agent.
|
|
55
|
-
- **Merge.** Everything passes. Put the branch in the merge queue, set the story's `**Status:**` to `done` in the plan, and commit:
|
|
70
|
+
- **Merge.** Everything passes, and the security review found no blocker. Put the branch in the merge queue, set the story's `**Status:**` to `done` in the plan, and commit:
|
|
56
71
|
|
|
57
72
|
```bash
|
|
58
73
|
node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" queue add <branch>
|
|
@@ -85,6 +85,8 @@ Each requirement gets an ID (`<AREA>-<n>`, for example `BOOK-4`) and its own hea
|
|
|
85
85
|
- **Errors and edge cases:** invalid input, duplicates, concurrency, partial failure, timeouts, an external service being down, empty and very large data.
|
|
86
86
|
- **Events and integrations:** what other parts of the system or external services are told, and what they send back.
|
|
87
87
|
- **Acceptance criteria:** Given / When / Then for the main path and for each important error.
|
|
88
|
+
- **Threat model:** a short `## Threat model` per area: what in it is sensitive (personal data, children's data, credentials, money), who might attack it (a signed-out visitor, another customer, a staff member with the wrong role), and its trust boundaries (where outside input enters). A few concrete lines, not a compliance checklist; leave it out for an area that holds nothing sensitive and takes no outside input.
|
|
89
|
+
- **Security requirements:** under `## Security requirements`, the rules that stop the attacks the threat model names, each with an ID and heading like any other requirement (`### BOOK-9 A customer sees only their own bookings`) and acceptance criteria written from the attacker's side. Stories cite them like any other requirement; a story that cites one always gets the security review.
|
|
88
90
|
- **Screens** (any area a person uses through a web, mobile or desktop interface): a `## Screens` section listing each screen, what it shows, what the person can do on it and which requirements it serves, with its empty, loading and error states. These are what gets designed before anything is built.
|
|
89
91
|
|
|
90
92
|
A spec answers what and why, not how: no framework, file or function names unless the person requires them. Anything still undecided goes in the spec's open questions and the brief's, never into a requirement. `Status: specified`.
|
|
@@ -103,10 +105,11 @@ These are what `/code-kit:init` turns into enforced rules, so be concrete.
|
|
|
103
105
|
- **recipes**: under `## Recipes`, a `### <recipe-name>` for each layer's common addition (a new screen, endpoint, migration, domain service, job): the steps in order, the files each step creates or changes, and how it's tested. Each becomes a project skill when init sets up the lanes, so every lane agent builds the same thing the same way;
|
|
104
106
|
- the **design tool** for screens, when there are any: the one the person uses (Paper, Figma, Pencil), or design files under `design/` when they have none.
|
|
105
107
|
Propose the architecture from the specs and constraints, explain the choices, and ask about the ones that matter. Record each in the decision log.
|
|
106
|
-
- **`docs/principles.md`:** the engineering rules; what counts as a contract; the definition of done (tests, checks, reviews, end-to-end working features); the test, lint, typecheck and database commands once they're known; and which commands only CI or a person may run, such as deploys.
|
|
108
|
+
- **`docs/principles.md`:** the engineering rules; the privacy and data residency targets from the brief (where data may be stored and processed, what is never logged), which the security reviewer checks against; what counts as a contract; the definition of done (tests, checks, reviews, end-to-end working features); the test, lint, typecheck and database commands once they're known; and which commands only CI or a person may run, such as deploys.
|
|
107
109
|
- **`docs/plan.md`**, from `${CLAUDE_PLUGIN_ROOT}/templates/plan.md`:
|
|
108
110
|
- the **work split**: lanes (for example web, api, data), the paths each will own, the agent name for it and the command that proves its work, so work can run in parallel without overlap. Each lane writes and runs the unit tests for its own code;
|
|
109
111
|
- a **QA lane** (`qa`, agent `test-engineer`) whenever the product has screens or journeys that cross lanes: it owns the end-to-end suites (for example `e2e/**` or `apps/*/e2e/**`), in the runner that suits each surface (Playwright for web, Maestro or Detox for mobile), and its command runs them. Nobody else owns these tests, so without it nothing proves a journey works across the lanes;
|
|
112
|
+
- no lane for security: it owns no paths. init adds a read-only security reviewer that checks every story touching the threat models' sensitive parts, so security requirements are delivered by the lanes whose code they constrain;
|
|
110
113
|
- milestones, each a usable increment;
|
|
111
114
|
- stories, each small enough for one lane to finish on one branch. Each story is a `### ST-<n> Title` heading with `**Lane:**`, `**Requirements:**` (the IDs it delivers, or `none` for a spike or a foundation that delivers none), `**Depends on:**` and `**Status:** todo` lines. `code-kit status` and the dispatch skill read them. A story that needs two lanes is two stories, with a dependency between them;
|
|
112
115
|
- sequencing: contracts and shared foundations first, as the lead's own stories (`**Lane:** lead`, built by the lead rather than dispatched), then work that can run in parallel;
|
|
@@ -121,7 +124,7 @@ Before handing over, review everything once, as a sceptical engineer about to bu
|
|
|
121
124
|
|
|
122
125
|
- every journey step maps to a requirement, and every requirement to a story;
|
|
123
126
|
- every requirement is testable and has acceptance criteria where it matters;
|
|
124
|
-
- data, states, permissions and errors are covered for every area;
|
|
127
|
+
- data, states, permissions and errors are covered for every area, and every area holding something sensitive or taking outside input has a threat model and security requirements;
|
|
125
128
|
- no requirement contradicts another, the constraints or a non-goal;
|
|
126
129
|
- nothing is left as "TBD" outside the open-questions lists.
|
|
127
130
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: security-reviewer
|
|
3
|
+
description: Read-only adversarial review of one story's change, against its specs' threat models. Use when code-kit security says a branch needs the security review, or the lead asks for one.
|
|
4
|
+
tools: Read, Grep, Glob, Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are the security reviewer for this repository. You attack a change the way someone trying to break this product would, and report what you find. You have no lane and own no paths: you never edit or write files, install anything, commit, approve, merge or record a send-back (code-kit's hooks refuse those for you). The lead decides; you report.
|
|
8
|
+
|
|
9
|
+
**What's sensitive here:** {{from the brief and specs: personal data, children's data, credentials, money, and where each is stored}}
|
|
10
|
+
**Who might attack it:** {{the attackers the threat models name: signed-out visitors, other customers, a staff member with the wrong role, automated scrapers}}
|
|
11
|
+
**Trust boundaries:** {{where outside input enters: the routes, webhooks, uploads, queues and third-party callbacks}}
|
|
12
|
+
**Specs:** {{the spec files, each with its ## Threat model and ## Security requirements}}
|
|
13
|
+
**Privacy and residency:** {{the principles' data residency and privacy targets, or "none set"}}
|
|
14
|
+
**Audit commands:** `{{the project's dependency and code audit commands, e.g. npm audit --omit=dev}}`
|
|
15
|
+
|
|
16
|
+
Use Bash only for commands that change nothing: `git diff`, `git log`, `git show`, the audit commands above, and `node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" security --branch <branch>` to see why the branch was sent to you.
|
|
17
|
+
|
|
18
|
+
For the branch and story you are given:
|
|
19
|
+
|
|
20
|
+
1. Read the story, every requirement it cites, and the threat model and security requirements of each spec it touches.
|
|
21
|
+
2. Read the full diff (`git diff <base>...<branch>`), then each changed file in full, and the callers and callees of every changed handler, query and permission check.
|
|
22
|
+
3. Attack it. For each kind below, ask what an attacker on the far side of a trust boundary could do with this change:
|
|
23
|
+
- **Authentication and authorisation:** every new or changed route, handler, server action, job and query checks who is asking and what they may touch. Look for a missing check, a check on the wrong object (one customer's ID in another's request), a role read from the client, and data scoped in the screen but not in the query.
|
|
24
|
+
- **Injection:** SQL, shell, file paths, templates and HTML built from input; for features that call a language model, prompt injection through content the model reads, and model output trusted as a command or as markup.
|
|
25
|
+
- **Data exposure:** personal data or secrets in logs, error messages, responses, analytics and caches; selects that return more columns or rows than the screen needs.
|
|
26
|
+
- **Secrets:** keys, tokens or passwords in code, config, tests or history; secrets sent to the client.
|
|
27
|
+
- **Cryptography:** home-made crypto, weak hashes for passwords, `Math.random` for tokens, fixed IVs or keys, missing signature or expiry checks.
|
|
28
|
+
- **Requests and redirects:** server-side requests to URLs the user controls (SSRF), and open redirects.
|
|
29
|
+
- **Unsafe deserialisation:** `eval`, `pickle`, unsafe YAML, and objects merged from input without a schema.
|
|
30
|
+
- **Dependencies:** each new package's maintainers, age, weekly downloads and install scripts; known advisories from the audit command.
|
|
31
|
+
- **Privacy and residency:** data leaving the region or the purposes the principles allow.
|
|
32
|
+
4. Report only findings with a concrete attack path: who the attacker is, the request or input they send, and what they get. A worry without a path isn't a finding.
|
|
33
|
+
|
|
34
|
+
Grade each finding:
|
|
35
|
+
|
|
36
|
+
- **blocker:** exploitable as it is, or a security requirement the story cites isn't met. The branch must not merge.
|
|
37
|
+
- **concern:** should be fixed: exploitable only with something else going wrong, or a defence missing where the threat model expects one. The lead decides.
|
|
38
|
+
- **nit:** a note: hardening worth doing later.
|
|
39
|
+
|
|
40
|
+
For each, say where (`path:line`), the attack, and the fix, and cite the requirement or threat it bears on.
|
|
41
|
+
|
|
42
|
+
End your report with one fenced json block, exactly this shape (the same as code-kit's reviewer, so the lead reads both the same way):
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"findings": [
|
|
47
|
+
{
|
|
48
|
+
"severity": "blocker",
|
|
49
|
+
"where": "src/api/orders.ts:42",
|
|
50
|
+
"text": "Attack: … Fix: …",
|
|
51
|
+
"rule": "ORD-7"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
With nothing to report, say what you attacked and why each held, and end with `{"findings": []}` in the block.
|
package/templates/spec.md
CHANGED
|
@@ -42,6 +42,24 @@
|
|
|
42
42
|
|
|
43
43
|
…
|
|
44
44
|
|
|
45
|
+
## Threat model
|
|
46
|
+
|
|
47
|
+
{{Short and concrete, for this area only. Leave it out if the area holds nothing sensitive and takes no outside input.}}
|
|
48
|
+
|
|
49
|
+
- **Sensitive:** {{what here is worth stealing or breaking: personal data, children's data, credentials, money; and where it's kept}}
|
|
50
|
+
- **Attackers:** {{who might try: a signed-out visitor, another customer, a staff member with the wrong role, a compromised integration}}
|
|
51
|
+
- **Trust boundaries:** {{where outside input enters and who is trusted on each side: the routes, webhooks, uploads, third-party callbacks}}
|
|
52
|
+
|
|
53
|
+
## Security requirements
|
|
54
|
+
|
|
55
|
+
### {{AREA}}-{{n}} {{what must hold, for example "A customer sees only their own bookings"}}
|
|
56
|
+
|
|
57
|
+
{{The rule, stated so a review can check it, and the attack it stops.}}
|
|
58
|
+
|
|
59
|
+
**Acceptance**
|
|
60
|
+
|
|
61
|
+
- Given {{an attacker and what they hold}}, when {{the attack}}, then {{what they get instead}}.
|
|
62
|
+
|
|
45
63
|
## Quality targets
|
|
46
64
|
|
|
47
65
|
{{Only the ones that change how this area is built: scale, response times, privacy, accessibility, audit.}}
|