@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +12 -0
- package/README.md +31 -29
- package/bin/code-kit.mjs +95 -4
- package/hooks/guard-bash.mjs +45 -3
- package/hooks/guard-paths.mjs +3 -2
- package/hooks/lib/adapters/hub/client.mjs +15 -0
- package/hooks/lib/checks.mjs +5 -1
- package/hooks/lib/config.mjs +1 -1
- package/hooks/lib/instructions.mjs +46 -13
- package/hooks/lib/layers.mjs +11 -0
- package/hooks/lib/rules.mjs +2 -1
- package/hooks/lib/snapshot.mjs +74 -2
- package/hooks/lib/standards.mjs +450 -0
- package/hooks/lib/team.mjs +9 -2
- package/hooks/lib/verify.mjs +23 -1
- package/hooks/mod/register.test.ts +4 -0
- package/hooks/mod/review.mjs +1 -0
- package/hooks/post-edit-check.mjs +5 -1
- package/hooks/stop-check.mjs +11 -3
- package/package.json +1 -1
- package/skills/init/SKILL.md +16 -2
- package/skills/next/SKILL.md +1 -1
- package/skills/review/SKILL.md +2 -1
- package/skills/spec-design/SKILL.md +8 -6
- package/templates/agent.md +1 -0
- package/templates/claude-md.md +2 -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.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.
|
|
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
|
|
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
|
-
| `
|
|
497
|
-
| `lead.
|
|
498
|
-
| `
|
|
499
|
-
| `
|
|
500
|
-
| `
|
|
501
|
-
| `
|
|
502
|
-
| `
|
|
503
|
-
| `
|
|
504
|
-
| `
|
|
505
|
-
| `shell.
|
|
506
|
-
| `
|
|
507
|
-
| `
|
|
508
|
-
| `
|
|
509
|
-
| `
|
|
510
|
-
| `
|
|
511
|
-
| `
|
|
512
|
-
| `approvals.
|
|
513
|
-
| `
|
|
514
|
-
| `
|
|
515
|
-
| `
|
|
516
|
-
| `
|
|
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
|
-
|
|
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
|
|
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' +
|
package/hooks/guard-bash.mjs
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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') {
|
package/hooks/guard-paths.mjs
CHANGED
|
@@ -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}.`);
|