@warren-dean/code-kit 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  "name": "code-kit",
10
10
  "source": "./",
11
11
  "description": "Keeps AI coding agents inside your project's rules, from idea to merged code: spec design, lanes, layer rules, protected paths with an approval log, spec-check and design gates, proof-before-finish checks, dispatch, review, build status and CI verification, configured per project from its docs",
12
- "version": "0.20.0",
12
+ "version": "0.21.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.20.0",
3
+ "version": "0.21.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,18 @@
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.21.0 (2026-10-21)
6
+
7
+ A project can keep its engineering rules as named standards, and code-kit applies each as firmly as it can.
8
+
9
+ - `docs/standards.md` holds the project's standards, each with a category (principles, patterns, architecture, conventions or policies), a level (required or recommended) and how it's applied: enforced by a rule the project already has, checked in review, or given to agents as advice. `code-kit standards` says what's wrong with the file.
10
+ - `/code-kit:init` suggests standards after the layers: from the specs, citing where each came from, or from the code, with its evidence ("41 of 43 modules"). A pattern the code follows only partly is put to you as a choice. Nothing is written until you approve each one. An existing project gets the step on its next init.
11
+ - Each lane agent is given the standards for its own layers, one line each, and the lead sees them all. Engineering rules move out of `docs/principles.md` into the standards; principles keeps the definition of done, the commands and the privacy targets.
12
+ - A refusal from a rule a standard names now names the standard. Reviews send back a branch that breaks a required checked standard, citing its id.
13
+ - `docs/standards.md` is protected. Adding a recommended standard can be left to the lead, but removing one or weakening a required one always needs your approval, and `verify` checks it.
14
+ - A team provider can supply an organisation's standards. A project can extend or tighten them, and weaken one only with a waiver the provider confirms.
15
+ - Reports to a team provider now carry the project's standards (report version 2).
16
+
5
17
  ## 0.20.0 (2026-10-21)
6
18
 
7
19
  A team provider can now receive a report of a project's facts, and a hosted provider's adapter is in preview.
package/README.md CHANGED
@@ -210,7 +210,7 @@ spec-design works an idea into a specification with you before any code exists.
210
210
  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.
211
211
  6. **Check:** every journey maps to requirements and every requirement to a story, with no gaps left as "TBD".
212
212
 
213
- It writes `docs/brief.md` (with the decision log and open questions), `docs/specs/NN-<area>.md`, `docs/architecture.md`, `docs/principles.md` and `docs/plan.md`. Those are exactly what `/code-kit:init docs/` turns into lanes and layers, and what lanes spec-check against. Run it again to resume where it left off, to change a decision, or to spec a new feature in an existing codebase.
213
+ It writes `docs/brief.md` (with the decision log and open questions), `docs/specs/NN-<area>.md`, `docs/architecture.md`, `docs/standards.md`, `docs/principles.md` and `docs/plan.md`. Those are exactly what `/code-kit:init docs/` turns into lanes, layers and standards, and what lanes spec-check against. Run it again to resume where it left off, to change a decision, or to spec a new feature in an existing codebase.
214
214
 
215
215
  Requirements are headings (`### BOOK-4 Cancel a booking`). Stories are `### ST-12` headings in the plan with `**Lane:**`, `**Requirements:**`, `**Depends on:**` and `**Status:**` lines (`templates/plan.md`). The lane `lead` marks the lead's own stories, such as contracts and spikes, which the lead builds itself instead of dispatching; the lane `design` marks a design story, which the lead designs with you (`/code-kit:design`) before any screen that depends on it is built; `**Requirements:** none` marks a story that delivers no requirement. Those two formats are what the build loop below runs on.
216
216
 
@@ -438,7 +438,8 @@ code-kit security [--base ref] [--branch name] [--json] # whether a branch need
438
438
  code-kit status [--base ref] [--json] # stories and requirements against the spec
439
439
  code-kit next [--base ref] [--json] # the step to take now, from the project's state
440
440
  code-kit worktrees [--json] | worktrees release <branch> | worktrees prune # the project's worktrees; free one for a fresh agent on its story, or remove every merged and clean one, keeping their reports
441
- code-kit instructions [--write] [--json] | instructions show lead|lane # code-kit's part of CLAUDE.md and the lane agents: where each stands, brought up to date, or printed
441
+ code-kit instructions [--write] [--json] | instructions show lead|lane [<lane>] # code-kit's part of CLAUDE.md and the lane agents: where each stands, brought up to date, or printed
442
+ code-kit standards [--json] [--for <path>...] # the project's standards with any its organisation's provider supplies, and what's wrong with them; --for keeps the ones for those files' layers
442
443
  code-kit trace <path>... [--json] # what a file is for: requirements, lane, layer rules
443
444
  code-kit merge <branch> --delegated|--person [--into branch] # merge a branch that passes verify: the lead where approvals.delegate.merge is on, or the person
444
445
  code-kit requests [--json] # approval requests still waiting, numbered, each the lead's or the person's to decide, the approvals in force, and every approval given
@@ -487,33 +488,34 @@ code-kit approve <name>... --reason "…" [--lane name] [--delegated [--story ST
487
488
 
488
489
  `.claude/code-kit.json`. Globs use `*` (one segment), `**` (any depth), `**/` (zero or more folders), `?` and `{a,b}`. Patterns under `shell` are regular expressions.
489
490
 
490
- | Field | Required | What it is |
491
- | ------------------------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
492
- | `version` | yes | `1` |
493
- | `docs.specs` | no | Folder of specs. With it, a lane needs a spec-check report before writing code on a branch |
494
- | `docs.principles`, `docs.plan` | no | The engineering principles and the implementation plan |
495
- | `docs.decisions` | no | Where agreed intents are recorded as numbered decision records (`code-kit intend record`); default `docs/decisions` |
496
- | `lead.paths` | yes | What the main session writes. `.claude/**` is always added. |
497
- | `lead.outside` | no | Paths outside the repository the lead may write, e.g. `../shared-lib/**`. Claude's memory and scratchpad are always included. |
498
- | `anyActor` | no | Paths every writer may change (e.g. the lockfile), read-only agents included. `.claude/state/**` is always included. An entry may be `{ "glob", "reviewer" }` to name the lane (or `"lead"`) that reviews changes to it; otherwise the lead reviews them, and `who` and `trace` say so. |
499
- | `lanes.<name>` | yes (may be `{}`) | `{ agent, paths, exclude?, owners? }`: the agent name maps a subagent to its lane; `owners` (GitHub handles) are the people whose sessions work it, in a team (needs `team`). A worktree with a `.lane` file holding the lane name makes the main session that lane. |
500
- | `protected[]` | no | `{ glob, approval, why }`: logged on every commit. Anyone but the lead needs an approval: the lead's where `approvals.delegate.protected` names it, otherwise a person's (see [Approvals](#approvals)). `.claude/**` is always protected as `kit`. |
501
- | `designGate` | no | `{ register, screens: [{ glob, not? }], howTo }`: screen files wait until the register (`{ "screens": [{ "file": … }] }`) lists them. Init sets it for every lane that owns screens; `/code-kit:design` adds approved designs |
502
- | `layers[]` | no | `{ name, paths, mayImport, denyPackages? }`: the first layer whose paths match a file owns it. A layer may always import itself. |
503
- | `importAliases` | no | `{ "@app/domain": "packages/domain/src" }`: resolves non-relative imports to repository paths |
504
- | `shell.block[]` | no | `{ pattern, why, person?, command? }`: never run. `person: true` marks a person's act, which the lead may do for them with `approvals.lead` on. `command: true` matches the pattern from the start of each command the line runs, so a heredoc or quoted text that mentions it isn't refused |
505
- | `shell.restricted[]` | no | `{ pattern, lanes, why }`: only the lead and these lanes |
506
- | `postEdit[]` | no | `{ files, exclude?, run }`: after each edit of a matching file. `{file}` is the file. Exit 127 (tool not installed) is skipped. |
507
- | `tests[]` | no | Globs naming the test files, for the weakened-test and fix rules. Default: the usual conventions (`*.test.*`, `*.spec.*`, `__tests__/`, `test/`, `tests/`, `e2e/`, `*_test.go`, `test_*.py`). |
508
- | `regression.prepare` | no | A command run in a fresh checkout before a fix's tests run there without the fix, such as `npm ci`. Without it, the checkout's `node_modules` and virtualenvs are linked in. |
509
- | `checks[]` | no | `{ name, files, exclude?, run[], each?, owners?, ifMissing?, timeoutSeconds? }`: before finishing, when matching files changed. `each` runs the commands in every changed folder matching that glob. `owners` limits who may change the files (the lead always may). `ifMissing: "fail"` makes a missing tool an error (the default is to skip the check, which `verify` and a merge list as not run). |
510
- | `adapters` | no | `{ "<adapter>": false }` switches off an adapter that would otherwise be detected; `true` says the project wants that tool, so until it's set up here `next` gives its setup before any code. See [Adapters](#adapters). |
511
- | `approvals.lead` | no | `true` lets the lead act on what the person says in chat, for when `!` isn't available (on a phone, say): it records their approvals with `code-kit approve`, and runs `shell.block` commands marked `person`. Default `false`. See [Approvals](#approvals). |
512
- | `approvals.delegate` | no | `{ protected?: [approval names], merge?: true, dependencies?: { licences?, minWeeklyDownloads?, maxMonthsSinceRelease?, allowInstallScripts?: true \| false \| [kinds] } }`: what the lead decides, inside the guardrails you keep: the protected approvals it grants, and packages it approves for a story, with `code-kit approve --delegated`; with `merge`, merges into a protected branch with `code-kit merge --delegated`. Lanes ask the lead for these, never you. npm packages are also checked against the registry; the defaults are MIT, Apache-2.0, BSD and ISC, 10,000 weekly downloads, a release within 18 months, and no install scripts. See [Approvals](#approvals). |
513
- | `setup` | no | The version of init's setup the project has (3 now). Without it, or lower, `next` names what the project is missing (the design gate, design stories before screens, a QA lane, a security reviewer) and starts no new story until re-running init adds it |
514
- | `team` | no | Preview. For several people in one repository: `{ techLead, staleDays?, intents?, board?, provider?, report? }`. `techLead` is the GitHub handle of the person who owns the plan, specs and contracts; `staleDays` is how many working days without a push make a claim stale (default 3); `intents` is the discussion category intents go in (default `Intents`; create it once in the repository's Settings › Discussions); `board` is `{ owner, number }`, a GitHub Project the plan is mirrored onto; `provider` is `"github"` (default), `{ command, fallback? }` for another team provider, or, in preview, a hosted provider's adapter as `{ "<name>": "<https address>" }`, which keeps the intents (Discussions only when `intents` is set) and has GitHub as its fallback; with `fallback: "github"`, claims go through GitHub whenever that provider can't be reached; `report` (default true) is whether the project reports its facts (stories' stages, send-backs, the requirement trail, approvals, token use and the rules; never code) to a provider that isn't GitHub. See docs/specs/16-teams.md and docs/specs/21-hub-adapter-and-reports.md. |
515
- | `branches.protected` | no | Default `["main", "master"]` |
516
- | `harness` | no | What the mod's harness does in the session: `autonomy` (`autonomous`, `propose` or `off`; default `autonomous`), `hold.minutes` (how long a call waiting on your approval is held, 0 to 10; default 2), `stall.nudgeMinutes` and `stall.restartMinutes` (default 5 and 10), `stall.maxRestarts` (default 2), `agents.reviewer.on` (default `false`) and `agents.reviewer.model` (code-kit's reviewer and the security reviewer; default `sonnet`), `agents.lanes.model` (the model each lane agent starts on, unless the lead's call or the lane's agent file names one; default the session's), `background.pauseAtPercent` (default 80), `compact.at` (compact the lead's context between turns once it's past this share of its window; default 40, 0 for never), `compact.milestone` (when a milestone finishes, compact the lead's context: `ask`, `auto` or `off`; default `ask`), `agents.lanes.contextLimit` (the context a lane agent may reach before it hands its story to a fresh agent; default 250,000 tokens), `notify.on` (a system notification when something new needs you; default `true`) and `report.on` (agents draft bug reports for code-kit, which you send or discard; default `false`). Change them with `/harness` or `code-kit settings set`. |
491
+ | Field | Required | What it is |
492
+ | ------------------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
493
+ | `version` | yes | `1` |
494
+ | `docs.specs` | no | Folder of specs. With it, a lane needs a spec-check report before writing code on a branch |
495
+ | `docs.principles`, `docs.plan` | no | The engineering principles and the implementation plan |
496
+ | `docs.decisions` | no | Where agreed intents are recorded as numbered decision records (`code-kit intend record`); default `docs/decisions` |
497
+ | `docs.standards` | no | The project's standards (spec 22), default `docs/standards.md`: each a `### <id> <title>` heading with `level` (required or recommended), `applied` (enforced, checked or advice), `layers`, `why`, `source` and, when enforced, `enforced by` naming the layer rule, protected path, check or shell rule that refuses a breach. The id's prefix is its category: `PRI-` principles, `PAT-` patterns, `ARC-` architecture, `CON-` conventions, `POL-` policies. Each lane agent is given the ones for its layers; a refusal from an enforcing rule names the standard; reviews check the checked ones. Protect the file with the `standards` approval: removing a standard, or weakening a required one, always needs the person's, whatever is delegated |
498
+ | `lead.paths` | yes | What the main session writes. `.claude/**` is always added. |
499
+ | `lead.outside` | no | Paths outside the repository the lead may write, e.g. `../shared-lib/**`. Claude's memory and scratchpad are always included. |
500
+ | `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. |
501
+ | `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. |
502
+ | `protected[]` | no | `{ glob, approval, why }`: logged on every commit. Anyone but the lead needs an approval: the lead's where `approvals.delegate.protected` names it, otherwise a person's (see [Approvals](#approvals)). `.claude/**` is always protected as `kit`. |
503
+ | `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 |
504
+ | `layers[]` | no | `{ name, paths, mayImport, denyPackages? }`: the first layer whose paths match a file owns it. A layer may always import itself. |
505
+ | `importAliases` | no | `{ "@app/domain": "packages/domain/src" }`: resolves non-relative imports to repository paths |
506
+ | `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 |
507
+ | `shell.restricted[]` | no | `{ pattern, lanes, why }`: only the lead and these lanes |
508
+ | `postEdit[]` | no | `{ files, exclude?, run }`: after each edit of a matching file. `{file}` is the file. Exit 127 (tool not installed) is skipped. |
509
+ | `tests[]` | no | Globs naming the test files, for the weakened-test and fix rules. Default: the usual conventions (`*.test.*`, `*.spec.*`, `__tests__/`, `test/`, `tests/`, `e2e/`, `*_test.go`, `test_*.py`). |
510
+ | `regression.prepare` | no | A command run in a fresh checkout before a fix's tests run there without the fix, such as `npm ci`. Without it, the checkout's `node_modules` and virtualenvs are linked in. |
511
+ | `checks[]` | no | `{ name, files, exclude?, run[], each?, owners?, ifMissing?, timeoutSeconds? }`: before finishing, when matching files changed. `each` runs the commands in every changed folder matching that glob. `owners` limits who may change the files (the lead always may). `ifMissing: "fail"` makes a missing tool an error (the default is to skip the check, which `verify` and a merge list as not run). |
512
+ | `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). |
513
+ | `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). |
514
+ | `approvals.delegate` | no | `{ protected?: [approval names], merge?: true, dependencies?: { licences?, minWeeklyDownloads?, maxMonthsSinceRelease?, allowInstallScripts?: true \| false \| [kinds] } }`: what the lead decides, inside the guardrails you keep: the protected approvals it grants, and packages it approves for a story, with `code-kit approve --delegated`; with `merge`, merges into a protected branch with `code-kit merge --delegated`. Lanes ask the lead for these, never you. npm packages are also checked against the registry; the defaults are MIT, Apache-2.0, BSD and ISC, 10,000 weekly downloads, a release within 18 months, and no install scripts. See [Approvals](#approvals). |
515
+ | `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 |
516
+ | `team` | no | Preview. For several people in one repository: `{ techLead, staleDays?, intents?, board?, provider?, report? }`. `techLead` is the GitHub handle of the person who owns the plan, specs and contracts; `staleDays` is how many working days without a push make a claim stale (default 3); `intents` is the discussion category intents go in (default `Intents`; create it once in the repository's Settings › Discussions); `board` is `{ owner, number }`, a GitHub Project the plan is mirrored onto; `provider` is `"github"` (default), `{ command, fallback? }` for another team provider, or, in preview, a hosted provider's adapter as `{ "<name>": "<https address>" }`, which keeps the intents (Discussions only when `intents` is set) and has GitHub as its fallback; with `fallback: "github"`, claims go through GitHub whenever that provider can't be reached; `report` (default true) is whether the project reports its facts (stories' stages, send-backs, the requirement trail, approvals, token use and the rules; never code) to a provider that isn't GitHub. A provider may also supply an organisation's standards (`standards`, answering `{ standards, waivers, version }`, each standard in `docs.standards`' fields, each waiver `{ standard, paths?, expires?, reason?, approvedBy? }`): they apply as if in the project's file, which may extend or tighten one but weaken a required one only under a waiver. See docs/specs/16-teams.md, docs/specs/21-hub-adapter-and-reports.md and docs/specs/22-standards.md. |
517
+ | `branches.protected` | no | Default `["main", "master"]` |
518
+ | `harness` | no | What the mod's harness does in the session: `autonomy` (`autonomous`, `propose` or `off`; default `autonomous`), `hold.minutes` (how long a call waiting on your approval is held, 0 to 10; default 2), `stall.nudgeMinutes` and `stall.restartMinutes` (default 5 and 10), `stall.maxRestarts` (default 2), `agents.reviewer.on` (default `false`) and `agents.reviewer.model` (code-kit's reviewer and the security reviewer; default `sonnet`), `agents.lanes.model` (the model each lane agent starts on, unless the lead's call or the lane's agent file names one; default the session's), `background.pauseAtPercent` (default 80), `compact.at` (compact the lead's context between turns once it's past this share of its window; default 40, 0 for never), `compact.milestone` (when a milestone finishes, compact the lead's context: `ask`, `auto` or `off`; default `ask`), `agents.lanes.contextLimit` (the context a lane agent may reach before it hands its story to a fresh agent; default 250,000 tokens), `notify.on` (a system notification when something new needs you; default `true`) and `report.on` (agents draft bug reports for code-kit, which you send or discard; default `false`). Change them with `/harness` or `code-kit settings set`. |
517
519
 
518
520
  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.
519
521
 
package/bin/code-kit.mjs CHANGED
@@ -29,6 +29,8 @@
29
29
  // report [--json] | report send|discard <id> the person sends a draft to code-kit's GitHub, or not
30
30
  // stops [--session id] [--json] finish checks that refused an agent's last stop, still failing
31
31
  // sent-back <branch> [--reason …] records that a review sent the branch back at its current commit
32
+ // standards [--json] [--for <path>...]
33
+ // the project's standards, with an organisation's, and their problems
32
34
  // settings [--json] | settings set <key> <value> --reason … [--via pane]
33
35
  // the harness settings; set is the person's own change
34
36
  // record approvals a person gave in chat (with approvals.lead on, the lead runs it)
@@ -131,6 +133,13 @@ import { dependencyRuleProblems, npmFacts, packageOf } from '../hooks/lib/regist
131
133
  import { heldTo, laneOfBranch, verifyBranch } from '../hooks/lib/verify.mjs';
132
134
  import { skippedLine } from '../hooks/lib/checks.mjs';
133
135
  import { TESTS_APPROVAL } from '../hooks/lib/regression.mjs';
136
+ import {
137
+ appliesTo,
138
+ projectStandards,
139
+ readInherited,
140
+ standardLine,
141
+ writeInherited,
142
+ } from '../hooks/lib/standards.mjs';
134
143
 
135
144
  const out = (s) => process.stdout.write(`${s}\n`);
136
145
  // Piped into `head` and the like: stop quietly when the reader closes.
@@ -187,6 +196,7 @@ const FLAGS = [
187
196
  '--blocks',
188
197
  '--take',
189
198
  '--dry-run',
199
+ '--for',
190
200
  ];
191
201
  const flag = (name) => args.includes(name);
192
202
  const writeBaseline = flag('--write');
@@ -427,6 +437,7 @@ const VERIFY_GROUPS = {
427
437
  design: 'Designs before screens',
428
438
  tests: 'Weakened tests',
429
439
  regression: 'Fixes and their tests',
440
+ standards: 'Standards',
430
441
  checks: 'Checks',
431
442
  };
432
443
 
@@ -1725,6 +1736,12 @@ async function pulseCommand(raw, team, provider) {
1725
1736
  `${JSON.stringify({ at: Date.now(), entries: input.usage })}\n`,
1726
1737
  );
1727
1738
  }
1739
+ // STD-6: the organisation's standards, for the hooks and instructions, read every ten minutes.
1740
+ const inherited = readInherited(root);
1741
+ if (!inherited.at || Date.now() - Date.parse(inherited.at) >= REPORT_MINUTES * 60_000) {
1742
+ const r = provider.standards();
1743
+ if (r && r.ok !== false && Array.isArray(r.standards)) writeInherited(root, r);
1744
+ }
1728
1745
  // Building the snapshot reads git: at most once a minute.
1729
1746
  const built = join(mainCheckout(root), '.claude/state/report-built.json');
1730
1747
  let at = 0;
@@ -2041,8 +2058,11 @@ function instructionsCommand() {
2041
2058
  const { config } = load();
2042
2059
  const root = resolve('.');
2043
2060
  if (rest[0] === 'show') {
2044
- if (!['lead', 'lane'].includes(rest[1])) die('code-kit instructions show lead|lane');
2045
- return out(rest[1] === 'lead' ? leadSection(root, config) : agentRules(root, config));
2061
+ if (!['lead', 'lane'].includes(rest[1])) die('code-kit instructions show lead|lane [<lane>]');
2062
+ if (rest[2] && !config.lanes[rest[2]])
2063
+ die(`No lane is called ${rest[2]}: ${Object.keys(config.lanes).join(', ') || 'none'}.`);
2064
+ // A named lane's rules carry the standards for its layers (spec 22, STD-3).
2065
+ return out(rest[1] === 'lead' ? leadSection(root, config) : agentRules(root, config, rest[2]));
2046
2066
  }
2047
2067
  const changed = flag('--write') ? writeInstructions(root, config) : [];
2048
2068
  const files = instructionsState(root, config);
@@ -2068,7 +2088,7 @@ function instructionsCommand() {
2068
2088
  for (const f of files) out(` ${f.file}: ${said[f.state]}`);
2069
2089
  if (files.some((f) => f.state === 'legacy'))
2070
2090
  out(
2071
- '\nTo merge one: replace its old code-kit part (in CLAUDE.md from `## Enforced, not advised` to the line about changing the rules; in an agent file from `Always:` to the end of its list) with what `code-kit instructions show lead` or `show lane` prints. Keep every line the project wrote itself (its own rules, its layer order, its docs) outside the markers, under a heading of its own, so a later refresh never touches them.',
2091
+ '\nTo merge one: replace its old code-kit part (in CLAUDE.md from `## Enforced, not advised` to the line about changing the rules; in an agent file from `Always:` to the end of its list) with what `code-kit instructions show lead` or `show lane <lane>` prints. Keep every line the project wrote itself (its own rules, its layer order, its docs) outside the markers, under a heading of its own, so a later refresh never touches them.',
2072
2092
  );
2073
2093
  }
2074
2094
 
@@ -2147,8 +2167,78 @@ function worktreesCommand() {
2147
2167
  );
2148
2168
  }
2149
2169
 
2170
+ /**
2171
+ * Spec 22: `standards` lists the standards that apply (the project's file, merged with what an
2172
+ * organisation's team provider supplies) and what's wrong with them (STD-1, STD-6); `--for <path>...`
2173
+ * keeps the ones for those files' layers, as a review reads them (STD-4). Exits 1 on a problem.
2174
+ */
2175
+ function standardsCommand() {
2176
+ const { raw, config } = load();
2177
+ const root = resolve('.');
2178
+ const team = teamOf(raw);
2179
+ let fetched = null;
2180
+ if (team && team.provider !== 'github') {
2181
+ const r = teamProvider(root, team).standards();
2182
+ if (r && r.ok !== false && Array.isArray(r.standards)) writeInherited(root, r);
2183
+ else fetched = r?.why ?? "The team provider didn't answer.";
2184
+ }
2185
+ const found = projectStandards(root, config);
2186
+ const { waivers } = readInherited(root);
2187
+ const files = flag('--for') ? rest : [];
2188
+ const layersOf = (f) =>
2189
+ config.layers.length ? [layerOf(f, config.layers)?.name].filter(Boolean) : null;
2190
+ const standards = files.length
2191
+ ? found.standards.filter((s) => files.some((f) => appliesTo(s, layersOf(f))))
2192
+ : found.standards;
2193
+ if (flag('--json')) {
2194
+ out(
2195
+ JSON.stringify(
2196
+ {
2197
+ file: found.file,
2198
+ exists: found.exists,
2199
+ version: found.version,
2200
+ ...(files.length ? { for: files } : {}),
2201
+ standards,
2202
+ waivers,
2203
+ problems: found.problems,
2204
+ ...(fetched ? { provider: fetched } : {}),
2205
+ },
2206
+ null,
2207
+ 2,
2208
+ ),
2209
+ );
2210
+ if (found.problems.length) process.exit(1);
2211
+ return;
2212
+ }
2213
+ if (!found.exists && !found.standards.length)
2214
+ return out(
2215
+ `This project has no standards yet: ${found.file} doesn't exist. /code-kit:init suggests them from the specs or the code, and the person approves each.`,
2216
+ );
2217
+ if (fetched)
2218
+ out(`The organisation's standards weren't refreshed (${fetched}); using the last ones read.`);
2219
+ const inherited = found.standards.filter((s) => s.origin !== 'own').length;
2220
+ out(
2221
+ `${found.standards.length} standard${found.standards.length === 1 ? '' : 's'} apply${files.length ? `; ${standards.length} to ${files.join(', ')}` : ''}: ${found.standards.length - inherited} from ${found.file}${inherited ? `, ${inherited} from the organisation${found.version ? ` (version ${found.version})` : ''}` : ''}.`,
2222
+ );
2223
+ for (const s of standards) {
2224
+ const rule = s.applied === 'enforced' && s.enforcedBy ? ` · enforced by ${s.enforcedBy}` : '';
2225
+ const where = s.layers?.length ? ` · ${s.layers.join(', ')}` : '';
2226
+ const origin = s.origin === 'own' ? '' : ` · ${s.origin}`;
2227
+ out(` ${standardLine({ ...s, origin: 'own', waiver: null })}${where}${rule}${origin}`);
2228
+ if (s.waiver)
2229
+ out(
2230
+ ` waived${s.waiver.paths?.length ? ` for ${s.waiver.paths.join(', ')}` : ''}${s.waiver.expires ? ` until ${s.waiver.expires}` : ''}${s.waiver.approvedBy ? `, approved by ${s.waiver.approvedBy}` : ''}${s.waiver.reason ? `: ${s.waiver.reason}` : ''}`,
2231
+ );
2232
+ }
2233
+ if (!found.problems.length) return;
2234
+ out(`\nProblems:`);
2235
+ for (const p of found.problems) out(` ${p}`);
2236
+ process.exit(1);
2237
+ }
2238
+
2150
2239
  const commands = {
2151
2240
  check,
2241
+ standards: standardsCommand,
2152
2242
  worktrees: worktreesCommand,
2153
2243
  instructions: instructionsCommand,
2154
2244
  loop: loopCommand,
@@ -2187,7 +2277,8 @@ function usage() {
2187
2277
  ' | verify [--base ref] [--branch name] [--no-checks] | status [--base ref] [--json]\n' +
2188
2278
  ' | security [--base ref] [--branch name] [--json]\n' +
2189
2279
  ' | next [--base ref] [--json] | adapters | trace <path>... [--json]\n' +
2190
- ' | instructions [--write] [--json] | instructions show lead|lane\n' +
2280
+ ' | instructions [--write] [--json] | instructions show lead|lane [<lane>]\n' +
2281
+ ' | standards [--json] [--for <path>...]\n' +
2191
2282
  ' | worktrees [--json] | worktrees release <branch> | worktrees prune\n' +
2192
2283
  ' | approve <name>... --reason "…" [--lane name] [--config file]\n' +
2193
2284
  ' | merge <branch> --delegated|--person [--into branch] | requests [--json] | requests drop <n>|all\n' +
@@ -21,6 +21,12 @@ import { laneOfBranch, storyOfBranch, teamOf, teamProvider, whoami } from './lib
21
21
  import { matchesAny } from './lib/glob.mjs';
22
22
  import { noteBefore } from './lane-audit.mjs';
23
23
  import { TESTS_APPROVAL, commitWeakenings } from './lib/regression.mjs';
24
+ import {
25
+ STANDARDS_APPROVAL,
26
+ noteFor,
27
+ standardsFile,
28
+ standardsWeakenings,
29
+ } from './lib/standards.mjs';
24
30
  import {
25
31
  APPROVAL_LOG,
26
32
  actorFor,
@@ -30,6 +36,7 @@ import {
30
36
  askHowTo,
31
37
  currentBranch,
32
38
  decidedBy,
39
+ delegatedMark,
33
40
  dependencyChange,
34
41
  dependencyGrants,
35
42
  useApproval,
@@ -176,7 +183,9 @@ for (const { pattern, why, person, command } of config.shell.block) {
176
183
  person && actor.kind === 'lead'
177
184
  ? '\nTo let the lead run it when the person says so in chat, a person sets "approvals": { "lead": true } in the config.'
178
185
  : '';
179
- block(`Blocked: ${why}${hint}\nCommand: ${cmd}`);
186
+ block(
187
+ `Blocked: ${why}${noteFor(project, config, { kind: 'shell', name: why })}${hint}\nCommand: ${cmd}`,
188
+ );
180
189
  }
181
190
 
182
191
  if (approves && actor.kind !== 'lead')
@@ -471,16 +480,47 @@ function recordApprovals() {
471
480
  session: input.session_id ?? null,
472
481
  }),
473
482
  );
483
+ // STD-5: removing a standard, or weakening a required one, is the person's own call, never the lead's,
484
+ // delegated or not. The commit waits for their `standards` approval, which records what it weakened.
485
+ const stdFile = standardsFile(config);
486
+ const stdWeak =
487
+ files.includes(stdFile) && !arrivesWithMerge(root, stdFile, { staged: true })
488
+ ? standardsWeakenings(
489
+ git('show', `HEAD:${stdFile}`),
490
+ existsSync(join(root, stdFile)) ? readFileSync(join(root, stdFile), 'utf8') : '',
491
+ )
492
+ : [];
493
+ if (stdWeak.length) {
494
+ const granted = approval(root, STANDARDS_APPROVAL, actor);
495
+ if (!granted || granted.reason.includes(delegatedMark('').slice(0, -1))) {
496
+ recordRequest(
497
+ root,
498
+ actor,
499
+ STANDARDS_APPROVAL,
500
+ stdFile,
501
+ `Standards weakened: ${stdWeak.join('; ')}.`,
502
+ );
503
+ const personOnly = { ...config, approvals: { ...config.approvals, delegate: null } };
504
+ block(
505
+ `Blocked: this commit weakens the project's standards in ${stdFile}: ${stdWeak.join('; ')}. Removing a standard, or weakening a required one, is the person's call, never the lead's. Ask them, with what the standard costs the project and your recommendation; they approve it with\n${approvalHowTo(STANDARDS_APPROVAL, actor, root, 'weakening the standards in one commit', personOnly)}`,
506
+ );
507
+ }
508
+ }
474
509
  for (const file of files) {
475
510
  // Concluding a merge: what arrives as the other branch has it was logged there, with its approvals.
476
511
  if (arrivesWithMerge(root, file, { staged: true })) continue;
477
- const entry = protectedEntry(file, config);
512
+ // A weakened standards file is the person's even where the config doesn't protect it.
513
+ const entry =
514
+ protectedEntry(file, config) ??
515
+ (file === stdFile && stdWeak.length
516
+ ? { approval: STANDARDS_APPROVAL, why: "the project's standards" }
517
+ : null);
478
518
  const granted = entry && approval(root, entry.approval, actor);
479
519
  const byDependency = actor.kind === 'lane' && dependencyChange(actor, file, root, config);
480
520
  if (entry && !granted && actor.kind !== 'lead' && !byDependency) {
481
521
  recordRequest(root, actor, entry.approval, file, entry.why);
482
522
  block(
483
- `Blocked: ${file} is ${entry.why}; committing it needs ${decidedBy(entry.approval, config) === 'lead' ? "the lead's" : "a person's"} approval in force. ${askHowTo(entry.approval, actor, root, config)}`,
523
+ `Blocked: ${file} is ${entry.why}; committing it needs ${decidedBy(entry.approval, config) === 'lead' ? "the lead's" : "a person's"} approval in force.${noteFor(root, config, { kind: 'protected', name: entry.approval, glob: entry.glob })} ${askHowTo(entry.approval, actor, root, config)}`,
484
524
  );
485
525
  }
486
526
  if (granted)
@@ -488,6 +528,7 @@ function recordApprovals() {
488
528
  approval: entry.approval,
489
529
  reason: granted.reason,
490
530
  grantedAt: granted.grantedAt,
531
+ ...(file === stdFile && stdWeak.length ? { weakened: stdWeak } : {}),
491
532
  });
492
533
  else if (byDependency && (entry || !owns(actor, file, config))) {
493
534
  const grants = dependencyGrants(root, actor);
@@ -538,6 +579,7 @@ ${approvalHowTo(TESTS_APPROVAL, actor, root, 'weakening tests in one commit', pe
538
579
  });
539
580
  useApproval(root, TESTS_APPROVAL, actor);
540
581
  }
582
+ if (stdWeak.length) useApproval(root, STANDARDS_APPROVAL, actor);
541
583
  // A dependency approval is used up by the commit that adds its package to a manifest or lockfile.
542
584
  const manifests = files.filter((f) => isDependencyFile(f));
543
585
  if (manifests.length && actor.kind !== 'readonly') {
@@ -6,7 +6,8 @@ import { existsSync, readFileSync } from 'node:fs';
6
6
  import { join } from 'node:path';
7
7
  import { loadBaseline, newProblems } from './lib/baseline.mjs';
8
8
  import { CONFIG_FILE } from './lib/config.mjs';
9
- import { layerProblems } from './lib/layers.mjs';
9
+ import { brokenLayerRule, layerProblems } from './lib/layers.mjs';
10
+ import { noteFor } from './lib/standards.mjs';
10
11
  import { block, start } from './lib/hook.mjs';
11
12
  import { teamOf, teamProvider, teamRefusal, whoami } from './lib/team.mjs';
12
13
  import { spawnSync } from 'node:child_process';
@@ -83,7 +84,7 @@ if (after !== null) {
83
84
  );
84
85
  if (added.length)
85
86
  block(
86
- `Blocked: this edit would break the layer rules (.claude/code-kit.json › layers):\n ${added.join('\n ')}\nDepend only on the layers listed, or ask the lead to change the rule.`,
87
+ `Blocked: this edit would break the layer rules (.claude/code-kit.json › layers):\n ${added.join('\n ')}\nDepend only on the layers listed, or ask the lead to change the rule.${noteFor(root, config, added.map(brokenLayerRule))}`,
87
88
  );
88
89
  }
89
90
  process.exit(0);
@@ -40,6 +40,7 @@ const OPS = [
40
40
  'intent',
41
41
  'since',
42
42
  'report',
43
+ 'standards',
43
44
  ];
44
45
  /** What to run again to sign in: code-kit's own command for the client. */
45
46
  const SIGN_IN = 'code-kit team provider signin';
@@ -158,6 +159,20 @@ export async function provide(op, input, root = process.cwd()) {
158
159
  }
159
160
  const { team: _team, ...body } = input;
160
161
  try {
162
+ // STD-6: the organisation's standards are read, not posted. A hub without them answers none.
163
+ if (op === 'standards') {
164
+ const { status, json } = await hub('GET', `/v1/repos/${repo}/standards`);
165
+ markUp(root);
166
+ if (status === 401) return unreachable(json.why ?? `Sign in to the hub again: ${SIGN_IN}.`);
167
+ if (status === 404 || status === 405 || !Array.isArray(json?.standards))
168
+ return { ok: true, standards: [], waivers: [] };
169
+ return {
170
+ ok: true,
171
+ version: json.version ?? null,
172
+ standards: json.standards,
173
+ waivers: Array.isArray(json.waivers) ? json.waivers : [],
174
+ };
175
+ }
161
176
  const { status, json } = await hub('POST', `/v1/repos/${repo}/${op}`, body);
162
177
  markUp(root);
163
178
  if (status === 401) return unreachable(json.why ?? `Sign in to the hub again: ${SIGN_IN}.`);