@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
package/hooks/lib/checks.mjs
CHANGED
|
@@ -16,13 +16,14 @@ function directories(check, files, root) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* The first failure as { title, output }, or null. `actor` decides `owners`; the lead always may. A
|
|
19
|
+
* The first failure as { check, title, output }, or null. `actor` decides `owners`; the lead always may. A
|
|
20
20
|
* command that isn't installed skips the rest of the check, unless `ifMissing` is "fail", and is
|
|
21
21
|
* added to `skipped` as { check, command, where }, so a pass never hides a check that didn't run.
|
|
22
22
|
*/
|
|
23
23
|
function runCheck(check, files, root, actor, skipped) {
|
|
24
24
|
if (check.owners && !(actor.kind === 'lead' || check.owners.includes(actor.lane))) {
|
|
25
25
|
return {
|
|
26
|
+
check: check.name,
|
|
26
27
|
title: `The ${actor.label} changed ${check.name} files it does not own:`,
|
|
27
28
|
output: files.join('\n'),
|
|
28
29
|
};
|
|
@@ -39,6 +40,7 @@ function runCheck(check, files, root, actor, skipped) {
|
|
|
39
40
|
if (res.status === 127) {
|
|
40
41
|
if (check.ifMissing === 'fail')
|
|
41
42
|
return {
|
|
43
|
+
check: check.name,
|
|
42
44
|
title: `${check.name}: \`${command}\` is not installed${where}. Install it so the check can run.`,
|
|
43
45
|
};
|
|
44
46
|
skipped.push({ check: check.name, command, where: where.replace(/^ in /, '') || null });
|
|
@@ -46,10 +48,12 @@ function runCheck(check, files, root, actor, skipped) {
|
|
|
46
48
|
}
|
|
47
49
|
if (res.error || res.status === null)
|
|
48
50
|
return {
|
|
51
|
+
check: check.name,
|
|
49
52
|
title: `${check.name}: \`${command}\` did not finish${where} (${res.error?.code ?? res.signal}).`,
|
|
50
53
|
};
|
|
51
54
|
if (res.status !== 0)
|
|
52
55
|
return {
|
|
56
|
+
check: check.name,
|
|
53
57
|
title: `${check.name}: \`${command}\` fails${where}. Fix it:`,
|
|
54
58
|
output: res.stdout + res.stderr,
|
|
55
59
|
};
|
package/hooks/lib/config.mjs
CHANGED
|
@@ -174,7 +174,7 @@ export function validate(c) {
|
|
|
174
174
|
);
|
|
175
175
|
// A brownfield project may have no specs yet: then there is no spec-check gate.
|
|
176
176
|
need(c.docs === undefined || isObj(c.docs), '"docs" must be an object');
|
|
177
|
-
for (const k of ['specs', 'principles', 'plan', 'decisions'])
|
|
177
|
+
for (const k of ['specs', 'principles', 'plan', 'decisions', 'standards'])
|
|
178
178
|
need(c.docs?.[k] === undefined || isStr(c.docs[k]), `"docs.${k}" must be a path`);
|
|
179
179
|
need(isObj(c.lead) && isList(c.lead?.paths), '"lead.paths" must be a list of globs');
|
|
180
180
|
need(
|
|
@@ -5,9 +5,16 @@
|
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import {
|
|
9
|
+
appliesTo,
|
|
10
|
+
laneLayers,
|
|
11
|
+
projectStandards,
|
|
12
|
+
standardLine,
|
|
13
|
+
standardsFile,
|
|
14
|
+
} from './standards.mjs';
|
|
8
15
|
|
|
9
16
|
/** The version of code-kit's instructions: raised whenever the templates' rules change. */
|
|
10
|
-
export const INSTRUCTIONS_VERSION =
|
|
17
|
+
export const INSTRUCTIONS_VERSION = 6;
|
|
11
18
|
|
|
12
19
|
const TEMPLATES = fileURLToPath(new URL('../../templates/', import.meta.url));
|
|
13
20
|
const BEGIN = (v) => `<!-- code-kit:begin v${v} -->`;
|
|
@@ -25,8 +32,20 @@ function fill(text, facts) {
|
|
|
25
32
|
.replace(/\n{3,}/g, '\n\n');
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
/**
|
|
29
|
-
function
|
|
35
|
+
/** The standards that apply (spec 22), or none when the file can't be read. */
|
|
36
|
+
function standardsOf(root, config) {
|
|
37
|
+
try {
|
|
38
|
+
return projectStandards(root, config).standards;
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What the templates need from the project: everything the config and the repository say. With `lane`,
|
|
46
|
+
* the standards are the ones for that lane's layers (STD-3).
|
|
47
|
+
*/
|
|
48
|
+
function factsOf(root, config, lane = null) {
|
|
30
49
|
const delegated = new Set(config.approvals?.delegate?.protected ?? []);
|
|
31
50
|
const kept = [];
|
|
32
51
|
for (const p of config.protected ?? []) {
|
|
@@ -45,9 +64,24 @@ function factsOf(root, config) {
|
|
|
45
64
|
'docs.specs': config.docs?.specs ?? 'the specs',
|
|
46
65
|
'docs.plan': config.docs?.plan ?? 'the plan',
|
|
47
66
|
'docs.principles': config.docs?.principles ?? "the project's principles",
|
|
67
|
+
'docs.standards': standardsFile(config),
|
|
68
|
+
standards: laneStandards(root, config, lane),
|
|
69
|
+
'standards.all': standardsOf(root, config)
|
|
70
|
+
.map((s) => `${s.id} ${s.title}`)
|
|
71
|
+
.join('; '),
|
|
48
72
|
};
|
|
49
73
|
}
|
|
50
74
|
|
|
75
|
+
/** A lane's standards as the lines of a nested list, or '' when none apply. */
|
|
76
|
+
function laneStandards(root, config, lane) {
|
|
77
|
+
if (!lane) return '';
|
|
78
|
+
const layers = (config.layers ?? []).length ? laneLayers(config.lanes?.[lane], config) : null;
|
|
79
|
+
const lines = standardsOf(root, config)
|
|
80
|
+
.filter((s) => appliesTo(s, layers))
|
|
81
|
+
.map((s) => `\n - ${standardLine(s)}`);
|
|
82
|
+
return lines.join('');
|
|
83
|
+
}
|
|
84
|
+
|
|
51
85
|
const marked = (body) => `${BEGIN(INSTRUCTIONS_VERSION)}\n${body.trim()}\n${END}`;
|
|
52
86
|
|
|
53
87
|
/** The lead's section of CLAUDE.md, between its markers. */
|
|
@@ -55,10 +89,10 @@ export function leadSection(root, config) {
|
|
|
55
89
|
return marked(fill(readFileSync(join(TEMPLATES, 'claude-md.md'), 'utf8'), factsOf(root, config)));
|
|
56
90
|
}
|
|
57
91
|
|
|
58
|
-
/** A lane agent's "Always:" rules, between their markers. */
|
|
59
|
-
export function agentRules(root, config) {
|
|
92
|
+
/** A lane agent's "Always:" rules, between their markers: with `lane`, its layers' standards too. */
|
|
93
|
+
export function agentRules(root, config, lane = null) {
|
|
60
94
|
const template = readFileSync(join(TEMPLATES, 'agent.md'), 'utf8');
|
|
61
|
-
return marked(fill(template.slice(template.indexOf('Always:')), factsOf(root, config)));
|
|
95
|
+
return marked(fill(template.slice(template.indexOf('Always:')), factsOf(root, config, lane)));
|
|
62
96
|
}
|
|
63
97
|
|
|
64
98
|
/** Whether two renderings say the same, whatever their whitespace and list indentation. */
|
|
@@ -87,17 +121,16 @@ function stateOf(text, block, legacy) {
|
|
|
87
121
|
*/
|
|
88
122
|
export function instructionsState(root, config) {
|
|
89
123
|
const lead = leadSection(root, config);
|
|
90
|
-
const rules = agentRules(root, config);
|
|
91
124
|
const files = [
|
|
92
125
|
{ file: 'CLAUDE.md', kind: 'lead', block: lead, legacy: LEAD_LEGACY },
|
|
93
|
-
...
|
|
94
|
-
.
|
|
95
|
-
|
|
126
|
+
...Object.entries(config.lanes ?? {})
|
|
127
|
+
.filter(([, l]) => existsSync(join(root, `.claude/agents/${l.agent}.md`)))
|
|
128
|
+
.map(([name, l]) => ({
|
|
129
|
+
file: `.claude/agents/${l.agent}.md`,
|
|
96
130
|
kind: 'lane',
|
|
97
|
-
block:
|
|
131
|
+
block: agentRules(root, config, name),
|
|
98
132
|
legacy: AGENT_LEGACY,
|
|
99
|
-
}))
|
|
100
|
-
.filter((f) => existsSync(join(root, f.file))),
|
|
133
|
+
})),
|
|
101
134
|
];
|
|
102
135
|
return files.map(({ file, kind, block, legacy }) => {
|
|
103
136
|
const path = join(root, file);
|
package/hooks/lib/layers.mjs
CHANGED
|
@@ -55,3 +55,14 @@ export function layerProblems(rel, source, config) {
|
|
|
55
55
|
}
|
|
56
56
|
return problems;
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The rule a layer problem broke, read back from its sentence (as layerProblems words it):
|
|
61
|
+
* { kind: 'layers', from, to } or { kind: 'layers', from, package }. Null for any other text.
|
|
62
|
+
*/
|
|
63
|
+
export function brokenLayerRule(problem) {
|
|
64
|
+
const pkg = String(problem).match(/ \(([^()]+)\) may not import the package (\S+)\.$/);
|
|
65
|
+
if (pkg) return { kind: 'layers', from: pkg[1], package: pkg[2] };
|
|
66
|
+
const layer = String(problem).match(/ \(([^()]+)\) imports \S+ \(([^()]+)\); /);
|
|
67
|
+
return layer ? { kind: 'layers', from: layer[1], to: layer[2] } : null;
|
|
68
|
+
}
|
package/hooks/lib/rules.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
|
31
31
|
import { fileURLToPath } from 'node:url';
|
|
32
32
|
import { DEPENDENCY_APPROVAL, isDependencyFile } from './dependencies.mjs';
|
|
33
33
|
import { globToRegExp, matchesAny } from './glob.mjs';
|
|
34
|
+
import { noteFor } from './standards.mjs';
|
|
34
35
|
|
|
35
36
|
export const APPROVAL_LOG = '.claude/approval-log.jsonl';
|
|
36
37
|
export const APPROVALS_DIR = '.claude/approvals';
|
|
@@ -763,7 +764,7 @@ export function writeProblem(actor, rel, root, config) {
|
|
|
763
764
|
decidedBy(guarded.approval, config) === 'lead'
|
|
764
765
|
? "the lead's approval"
|
|
765
766
|
: "a person's approval";
|
|
766
|
-
return `${rel} is ${guarded.why} and needs ${who}. ${askHowTo(guarded.approval, actor, root, config)}`;
|
|
767
|
+
return `${rel} is ${guarded.why} and needs ${who}.${noteFor(root, config, { kind: 'protected', name: guarded.approval, glob: guarded.glob })} ${askHowTo(guarded.approval, actor, root, config)}`;
|
|
767
768
|
}
|
|
768
769
|
if (matchesAny(rel, config.anyActor)) return null;
|
|
769
770
|
if (actor.kind === 'readonly') return readonlyProblem(actor, rel);
|
package/hooks/lib/snapshot.mjs
CHANGED
|
@@ -14,8 +14,18 @@ import { sendBacks } from './reviews.mjs';
|
|
|
14
14
|
import { readMilestones, specCheckRows } from './plan.mjs';
|
|
15
15
|
import { storyTimes } from './timeline.mjs';
|
|
16
16
|
import { trail } from './storymap.mjs';
|
|
17
|
+
import {
|
|
18
|
+
APPLIED,
|
|
19
|
+
CATEGORIES,
|
|
20
|
+
LEVELS,
|
|
21
|
+
ORIGINS,
|
|
22
|
+
SOURCES,
|
|
23
|
+
projectStandards,
|
|
24
|
+
sourceOf,
|
|
25
|
+
} from './standards.mjs';
|
|
17
26
|
|
|
18
|
-
|
|
27
|
+
/** 2 adds the project's standards (code-kit spec 22). */
|
|
28
|
+
export const SNAPSHOT_VERSION = 2;
|
|
19
29
|
|
|
20
30
|
/** A story's stage, as a report names it. */
|
|
21
31
|
export const STAGES = [
|
|
@@ -87,8 +97,28 @@ export const SNAPSHOT_SCHEMA = {
|
|
|
87
97
|
delegated: { protected: ['name'], dependencies: 'bool', merge: 'bool' },
|
|
88
98
|
designGate: 'bool',
|
|
89
99
|
},
|
|
100
|
+
// Spec 22: each standard that applies, by id and title, how it's applied and where it came from;
|
|
101
|
+
// its why in one line at most, and its evidence only as counts.
|
|
102
|
+
standards: [
|
|
103
|
+
{
|
|
104
|
+
id: 'id',
|
|
105
|
+
title: 'title',
|
|
106
|
+
category: `enum:${Object.values(CATEGORIES).join('|')}`,
|
|
107
|
+
level: `enum:${LEVELS.join('|')}`,
|
|
108
|
+
applied: `enum:${APPLIED.join('|')}`,
|
|
109
|
+
layers: ['name'],
|
|
110
|
+
source: `enum:${SOURCES.join('|')}`,
|
|
111
|
+
evidenceCount: 'count?',
|
|
112
|
+
evidenceOf: 'count?',
|
|
113
|
+
origin: `enum:${ORIGINS.join('|')}`,
|
|
114
|
+
why: 'line?',
|
|
115
|
+
},
|
|
116
|
+
],
|
|
90
117
|
};
|
|
91
118
|
|
|
119
|
+
/** Parts a report from an older code-kit doesn't have yet: valid when missing, checked when there. */
|
|
120
|
+
export const LATER_PARTS = ['standards'];
|
|
121
|
+
|
|
92
122
|
const ONE_LINE = (max) => (v) => typeof v === 'string' && v.length <= max && !/[\r\n]/.test(v);
|
|
93
123
|
const KINDS = {
|
|
94
124
|
id: (v) => typeof v === 'string' && /^[A-Za-z][A-Za-z0-9]*-\d+$/.test(v),
|
|
@@ -122,7 +152,9 @@ export function snapshotProblems(value, shape = SNAPSHOT_SCHEMA, at = 'snapshot'
|
|
|
122
152
|
const extra = Object.keys(value).filter((k) => !(k in shape));
|
|
123
153
|
return [
|
|
124
154
|
...extra.map((k) => `${at}.${k}: not a field a report may hold`),
|
|
125
|
-
...Object.entries(shape)
|
|
155
|
+
...Object.entries(shape)
|
|
156
|
+
.filter(([k]) => !(shape === SNAPSHOT_SCHEMA && LATER_PARTS.includes(k) && !(k in value)))
|
|
157
|
+
.flatMap(([k, s]) => snapshotProblems(value[k], s, `${at}.${k}`)),
|
|
126
158
|
];
|
|
127
159
|
}
|
|
128
160
|
|
|
@@ -143,6 +175,7 @@ export const EVENT_PARTS = [
|
|
|
143
175
|
'requirements',
|
|
144
176
|
'approvals',
|
|
145
177
|
'rules',
|
|
178
|
+
'standards',
|
|
146
179
|
];
|
|
147
180
|
|
|
148
181
|
const git = (root, ...args) => {
|
|
@@ -347,6 +380,45 @@ export function buildSnapshot({
|
|
|
347
380
|
},
|
|
348
381
|
designGate: Boolean(config.designGate),
|
|
349
382
|
},
|
|
383
|
+
standards: reportedStandards(root, config, line),
|
|
350
384
|
};
|
|
351
385
|
return snapshot;
|
|
352
386
|
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Spec 22: the standards that apply, as a report holds them. One that the file can't make sense of (an
|
|
390
|
+
* unknown category, no level) is left out: `code-kit standards` tells the project what's wrong with it.
|
|
391
|
+
*/
|
|
392
|
+
function reportedStandards(root, config, line) {
|
|
393
|
+
let all = [];
|
|
394
|
+
try {
|
|
395
|
+
all = projectStandards(root, config).standards;
|
|
396
|
+
} catch {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
return all
|
|
400
|
+
.filter(
|
|
401
|
+
(s) =>
|
|
402
|
+
KINDS.id(s.id) &&
|
|
403
|
+
CATEGORIES[s.id.split('-')[0]] &&
|
|
404
|
+
LEVELS.includes(s.level) &&
|
|
405
|
+
APPLIED.includes(s.applied) &&
|
|
406
|
+
KINDS.title(line(s.title) ?? ''),
|
|
407
|
+
)
|
|
408
|
+
.map((s) => {
|
|
409
|
+
const source = s.origin === 'inherited' ? { kind: 'organisation' } : sourceOf(s.source);
|
|
410
|
+
return {
|
|
411
|
+
id: s.id,
|
|
412
|
+
title: line(s.title),
|
|
413
|
+
category: CATEGORIES[s.id.split('-')[0]],
|
|
414
|
+
level: s.level,
|
|
415
|
+
applied: s.applied,
|
|
416
|
+
layers: (s.layers ?? []).filter((l) => KINDS.name(l)),
|
|
417
|
+
source: source.kind,
|
|
418
|
+
evidenceCount: source.count ?? null,
|
|
419
|
+
evidenceOf: source.of ?? null,
|
|
420
|
+
origin: ORIGINS.includes(s.origin) ? s.origin : 'own',
|
|
421
|
+
why: line(s.why),
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
}
|