@warren-dean/code-kit 0.8.0 → 0.9.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 +29 -0
- package/README.md +65 -38
- package/bin/code-kit.mjs +355 -7
- package/hooks/guard-bash.mjs +69 -0
- package/hooks/guard-paths.mjs +19 -0
- package/hooks/lib/adapters/context-graph.mjs +5 -0
- package/hooks/lib/adapters/index.mjs +12 -0
- package/hooks/lib/config.mjs +15 -1
- package/hooks/lib/next.mjs +88 -6
- package/hooks/lib/plan.mjs +3 -1
- package/hooks/lib/storymap.mjs +62 -0
- package/hooks/lib/team.mjs +851 -0
- package/hooks/lib/teamfacts.mjs +187 -0
- package/hooks/mod/codemap.mjs +16 -1
- package/hooks/mod/decisions.mjs +25 -2
- package/hooks/mod/loop.mjs +23 -3
- package/hooks/mod/panes.mjs +99 -50
- package/hooks/mod/register.mjs +70 -1
- package/hooks/mod/register.test.ts +251 -9
- package/hooks/mod/teamview.mjs +243 -0
- package/hooks/mod/view.mjs +10 -8
- package/package.json +1 -1
- package/skills/design/SKILL.md +56 -0
- package/skills/dispatch/SKILL.md +9 -1
- package/skills/init/SKILL.md +19 -6
- package/skills/next/SKILL.md +3 -0
- package/skills/review/SKILL.md +1 -0
- package/skills/spec-design/SKILL.md +9 -3
- package/templates/plan.md +1 -1
package/hooks/lib/next.mjs
CHANGED
|
@@ -5,7 +5,9 @@ import { execFileSync } from 'node:child_process';
|
|
|
5
5
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
6
6
|
import { join, posix } from 'node:path';
|
|
7
7
|
import { loadConfig, mergeDelegation } from './config.mjs';
|
|
8
|
-
import { buildStatus, readStories, LEAD } from './plan.mjs';
|
|
8
|
+
import { buildStatus, readStories, DESIGN, LEAD } from './plan.mjs';
|
|
9
|
+
import { pendingSetup } from './adapters/index.mjs';
|
|
10
|
+
import { CONFIG_FILE, SETUP_VERSION } from './config.mjs';
|
|
9
11
|
import { readQueue, waiting } from './queue.mjs';
|
|
10
12
|
|
|
11
13
|
export const DRAFT_FILE = '.claude/code-kit.draft.json';
|
|
@@ -138,6 +140,23 @@ export function nextStep(root, base) {
|
|
|
138
140
|
`${plan} has no stories in the \`### ST-n\` form yet; spec-design writes the plan.`,
|
|
139
141
|
);
|
|
140
142
|
|
|
143
|
+
// ONB-3: a companion the config wants is set up before any coding starts.
|
|
144
|
+
let raw = null;
|
|
145
|
+
try {
|
|
146
|
+
raw = JSON.parse(readFileSync(join(root, CONFIG_FILE), 'utf8'));
|
|
147
|
+
} catch {
|
|
148
|
+
// read and validated above; nothing to add
|
|
149
|
+
}
|
|
150
|
+
const [setup] = pendingSetup(raw, root);
|
|
151
|
+
if (setup) return result('setup', setup.why, { args: setup.skill });
|
|
152
|
+
// ONB-5: a project set up before setup did all of this is told what re-running init adds.
|
|
153
|
+
if ((raw?.setup ?? 1) < SETUP_VERSION)
|
|
154
|
+
attention.push(
|
|
155
|
+
"This project's code-kit setup predates what init now sets up: project skills with each lane agent, designs approved before screens are built, a QA lane for end-to-end tests, and companion tools set up before coding. Re-run /code-kit:init to add what's missing; it keeps everything that's still right and shows you the difference first.",
|
|
156
|
+
);
|
|
157
|
+
// ONB-1: a lane agent naming a skill that doesn't exist works without the recipe it was given.
|
|
158
|
+
for (const missing of missingSkills(root)) attention.push(missing);
|
|
159
|
+
|
|
141
160
|
const from = base(config);
|
|
142
161
|
if (!from)
|
|
143
162
|
attention.push('The base branch has no commits yet, so story branches are read against HEAD.');
|
|
@@ -163,13 +182,17 @@ export function nextStep(root, base) {
|
|
|
163
182
|
// A ready story with a gap (unknown lane, a requirement no spec defines) waits for the plan's fix.
|
|
164
183
|
const defined = new Set(status.requirements.map((r) => r.id));
|
|
165
184
|
const buildable = (s) =>
|
|
166
|
-
(Boolean(config.lanes[s.lane]) || s.lane === LEAD) &&
|
|
185
|
+
(Boolean(config.lanes[s.lane]) || s.lane === LEAD || s.lane === DESIGN) &&
|
|
167
186
|
(s.requirements.length > 0 || s.noRequirements) &&
|
|
168
187
|
s.requirements.every((r) => defined.has(r));
|
|
169
188
|
const readyStories = status.stories.filter((s) => s.state === 'ready');
|
|
170
189
|
// The lead's own stories are built by the lead, not dispatched to a lane agent.
|
|
171
190
|
const forLead = readyStories.filter((s) => buildable(s) && s.lane === LEAD).map((s) => s.id);
|
|
172
|
-
const ready = readyStories
|
|
191
|
+
const ready = readyStories
|
|
192
|
+
.filter((s) => buildable(s) && s.lane !== LEAD && s.lane !== DESIGN)
|
|
193
|
+
.map((s) => s.id);
|
|
194
|
+
// ONB-2: design stories, designed with the person before any screen that depends on them is built.
|
|
195
|
+
const forDesign = readyStories.filter((s) => buildable(s) && s.lane === DESIGN).map((s) => s.id);
|
|
173
196
|
const unbuildable = readyStories.filter((s) => !buildable(s)).map((s) => s.id);
|
|
174
197
|
if (unbuildable.length)
|
|
175
198
|
attention.push(
|
|
@@ -180,11 +203,15 @@ export function nextStep(root, base) {
|
|
|
180
203
|
// the person does, and always the first merge, which brings code-kit's rules onto the base.
|
|
181
204
|
const delegation = queued.length ? mergeDelegation(root, config) : { delegated: false };
|
|
182
205
|
const leadMerges = delegation.delegated && config.harness.autonomy === 'autonomous';
|
|
206
|
+
// SETUP-2: a base with no rules committed (a project set up before setup committed them there): the
|
|
207
|
+
// lead puts the approved rules onto it, and the queue merges under them from then on.
|
|
183
208
|
if (queued.length && delegation.first)
|
|
184
|
-
|
|
185
|
-
|
|
209
|
+
return result(
|
|
210
|
+
'rules',
|
|
211
|
+
`${queued[0]} passed review and is next to merge, but ${delegation.into} has no code-kit rules committed yet. Put the rules the person approved onto it (code-kit queue rules); then the queue merges under them.`,
|
|
212
|
+
{ args: delegation.into },
|
|
186
213
|
);
|
|
187
|
-
|
|
214
|
+
if (queued.length && !leadMerges)
|
|
188
215
|
attention.push(
|
|
189
216
|
`${queued.join(', ')} passed review and ${queued.length === 1 ? 'waits' : 'wait'} for the person to merge (code-kit queue merge --person, or Merge in the code-kit band).`,
|
|
190
217
|
);
|
|
@@ -197,6 +224,7 @@ export function nextStep(root, base) {
|
|
|
197
224
|
const then = [];
|
|
198
225
|
// A sent-back story goes back to its lane with the review's findings, before new work starts.
|
|
199
226
|
const toDispatch = [...fixes, ...ready];
|
|
227
|
+
if (review.length && forDesign.length) then.push({ step: 'design', args: forDesign.join(' ') });
|
|
200
228
|
if (review.length && toDispatch.length)
|
|
201
229
|
then.push({ step: 'dispatch', args: toDispatch.join(' ') });
|
|
202
230
|
if (forLead.length) then.push({ step: 'lead', args: forLead.join(' ') });
|
|
@@ -205,6 +233,18 @@ export function nextStep(root, base) {
|
|
|
205
233
|
args: review.join(' '),
|
|
206
234
|
then,
|
|
207
235
|
});
|
|
236
|
+
if (forDesign.length)
|
|
237
|
+
return result(
|
|
238
|
+
'design',
|
|
239
|
+
`${forDesign.join(', ')} ${forDesign.length === 1 ? 'designs' : 'design'} the screens before any code is written for them; the person approves each design.`,
|
|
240
|
+
{
|
|
241
|
+
args: forDesign.join(' '),
|
|
242
|
+
then: [
|
|
243
|
+
...(toDispatch.length ? [{ step: 'dispatch', args: toDispatch.join(' ') }] : []),
|
|
244
|
+
...(forLead.length ? [{ step: 'lead', args: forLead.join(' ') }] : []),
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
);
|
|
208
248
|
if (fixes.length)
|
|
209
249
|
return result(
|
|
210
250
|
'dispatch',
|
|
@@ -227,6 +267,12 @@ export function nextStep(root, base) {
|
|
|
227
267
|
'wait',
|
|
228
268
|
`${building.join(', ')} ${building.length === 1 ? 'is' : 'are'} being built; nothing else can start until one finishes. A lane that stopped without finishing is dispatched again.`,
|
|
229
269
|
);
|
|
270
|
+
// Stories waiting on branches in the merge queue wait for the merge, not on a broken plan.
|
|
271
|
+
if (queued.length)
|
|
272
|
+
return result(
|
|
273
|
+
'wait',
|
|
274
|
+
`${queued.join(', ')} passed review and ${queued.length === 1 ? 'waits' : 'wait'} to be merged; the stories that depend on ${queued.length === 1 ? 'it' : 'them'} start after.`,
|
|
275
|
+
);
|
|
230
276
|
if (unbuildable.length)
|
|
231
277
|
return result(
|
|
232
278
|
'spec-design',
|
|
@@ -247,3 +293,39 @@ export function nextStep(root, base) {
|
|
|
247
293
|
`All ${status.stories.length} stories are done; spec-design plans the next milestone or feature.`,
|
|
248
294
|
);
|
|
249
295
|
}
|
|
296
|
+
|
|
297
|
+
/** The code-kit skills every agent may name without a project copy. */
|
|
298
|
+
const KIT_SKILLS = (() => {
|
|
299
|
+
try {
|
|
300
|
+
return new Set(readdirSync(new URL('../../skills/', import.meta.url)));
|
|
301
|
+
} catch {
|
|
302
|
+
return new Set();
|
|
303
|
+
}
|
|
304
|
+
})();
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* ONB-1: lane agents naming skills under **Skills to use** that don't exist, in words: ["web-engineer
|
|
308
|
+
* names a skill that doesn't exist: new-screen"]. Plugin skills (`plugin:skill`) and code-kit's own are
|
|
309
|
+
* left alone.
|
|
310
|
+
*/
|
|
311
|
+
export function missingSkills(root) {
|
|
312
|
+
const dir = join(root, '.claude/agents');
|
|
313
|
+
if (!existsSync(dir)) return [];
|
|
314
|
+
const out = [];
|
|
315
|
+
for (const file of readdirSync(dir).filter((f) => f.endsWith('.md'))) {
|
|
316
|
+
const line = readFileSync(join(dir, file), 'utf8').match(/^\*\*Skills to use:\*\*(.*)$/m)?.[1];
|
|
317
|
+
if (!line) continue;
|
|
318
|
+
const missing = line
|
|
319
|
+
.split(',')
|
|
320
|
+
.map((n) => n.replace(/[`*]/g, '').trim())
|
|
321
|
+
.filter((n) => /^[a-z0-9][a-z0-9-]*$/.test(n))
|
|
322
|
+
.filter(
|
|
323
|
+
(n) => !KIT_SKILLS.has(n) && !existsSync(join(root, '.claude/skills', n, 'SKILL.md')),
|
|
324
|
+
);
|
|
325
|
+
if (missing.length)
|
|
326
|
+
out.push(
|
|
327
|
+
`${file.replace(/\.md$/, '')} names ${missing.length === 1 ? "a skill that doesn't" : "skills that don't"} exist: ${missing.join(', ')}. Write ${missing.length === 1 ? 'it' : 'them'} from the architecture's recipes (re-run init), or take ${missing.length === 1 ? 'it' : 'them'} off the agent.`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
331
|
+
}
|
package/hooks/lib/plan.mjs
CHANGED
|
@@ -14,6 +14,8 @@ const STORY_HEADING = /^###\s+(ST-\d+)\b\s*(.*)$/;
|
|
|
14
14
|
const REQUIREMENT_HEADING = /^###\s+((?!ST-)[A-Z][A-Z0-9]*-\d+)\b\s*(.*)$/;
|
|
15
15
|
export const STATES = ['todo', 'in progress', 'review', 'done'];
|
|
16
16
|
export const LEAD = 'lead';
|
|
17
|
+
/** ONB-2: the pseudo-lane for design stories, which the lead designs with the person before any screen is built. */
|
|
18
|
+
export const DESIGN = 'design';
|
|
17
19
|
// Test files, by the usual conventions. Tests name the requirement IDs they prove.
|
|
18
20
|
const TEST =
|
|
19
21
|
/(^|\/)(tests?|__tests__|e2e)\/|\.(test|spec)\.[^/]+$|_test\.(go|py)$|(^|\/)test_[^/]+\.py$/;
|
|
@@ -185,7 +187,7 @@ export function traceProblems(requirements, stories, lanes) {
|
|
|
185
187
|
if (storyIds.has(s.id)) problems.push(`${s.id} is in the plan more than once`);
|
|
186
188
|
storyIds.add(s.id);
|
|
187
189
|
if (!s.lane) problems.push(`${s.id} has no **Lane:**`);
|
|
188
|
-
else if (s.lane !== LEAD && !lanes.includes(s.lane))
|
|
190
|
+
else if (s.lane !== LEAD && s.lane !== DESIGN && !lanes.includes(s.lane))
|
|
189
191
|
problems.push(`${s.id} names an unknown lane "${s.lane}"`);
|
|
190
192
|
if (!s.requirements.length && !s.noRequirements)
|
|
191
193
|
problems.push(`${s.id} cites no requirements (write "none" if it delivers none)`);
|
package/hooks/lib/storymap.mjs
CHANGED
|
@@ -153,3 +153,65 @@ export function trail(root, status, from) {
|
|
|
153
153
|
};
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
|
+
|
|
157
|
+
/** The code map of no story in particular: the whole project. */
|
|
158
|
+
export const PROJECT = 'project';
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* MAP-5: the whole project's code, for when no story is at work: its folders (`depth` deep, shallower
|
|
162
|
+
* when that makes more than `most`) by the layer most of their files sit in, with the imports between
|
|
163
|
+
* folders. The same shape as a story's map, each node a folder with its file count.
|
|
164
|
+
*/
|
|
165
|
+
export function projectMap(root, config, tracked, { depth = 3, most = 60 } = {}) {
|
|
166
|
+
const code = tracked.filter(isCode);
|
|
167
|
+
const set = new Set(code);
|
|
168
|
+
const aliases = config.importAliases ?? {};
|
|
169
|
+
const folderAt = (d) => (rel) => {
|
|
170
|
+
const parts = rel.split('/');
|
|
171
|
+
return parts.length === 1 ? '.' : parts.slice(0, Math.min(d, parts.length - 1)).join('/');
|
|
172
|
+
};
|
|
173
|
+
let d = depth;
|
|
174
|
+
while (d > 1 && new Set(code.map(folderAt(d))).size > most) d -= 1;
|
|
175
|
+
const folderOf = folderAt(d);
|
|
176
|
+
const nodes = new Map();
|
|
177
|
+
for (const rel of code) {
|
|
178
|
+
const path = folderOf(rel);
|
|
179
|
+
const node = nodes.get(path) ?? {
|
|
180
|
+
path,
|
|
181
|
+
lane: laneOfFile(rel, config),
|
|
182
|
+
layers: new Map(),
|
|
183
|
+
imports: new Set(),
|
|
184
|
+
files: 0,
|
|
185
|
+
};
|
|
186
|
+
nodes.set(path, node);
|
|
187
|
+
const layer = TEST.test(rel) ? 'tests' : (layerOf(rel, config.layers)?.name ?? 'other');
|
|
188
|
+
node.layers.set(layer, (node.layers.get(layer) ?? 0) + 1);
|
|
189
|
+
node.files += 1;
|
|
190
|
+
let source = '';
|
|
191
|
+
try {
|
|
192
|
+
source = readFileSync(join(root, rel), 'utf8');
|
|
193
|
+
} catch {
|
|
194
|
+
// unreadable: no imports
|
|
195
|
+
}
|
|
196
|
+
for (const spec of importsOf(source)) {
|
|
197
|
+
const target = resolveIn(spec, rel, aliases, set);
|
|
198
|
+
if (target && folderOf(target) !== path) node.imports.add(folderOf(target));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const files = [...nodes.values()]
|
|
202
|
+
.map((n) => ({
|
|
203
|
+
path: n.path,
|
|
204
|
+
layer: [...n.layers.entries()].sort((a, b) => b[1] - a[1])[0][0],
|
|
205
|
+
lane: n.lane,
|
|
206
|
+
imports: [...n.imports].sort(),
|
|
207
|
+
files: n.files,
|
|
208
|
+
}))
|
|
209
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
210
|
+
const used = new Set(files.map((f) => f.layer));
|
|
211
|
+
const layers = [...config.layers.map((l) => l.name), 'tests', 'other'].filter((l) => used.has(l));
|
|
212
|
+
return {
|
|
213
|
+
story: { id: PROJECT, title: 'The whole project', lane: null, state: null },
|
|
214
|
+
layers,
|
|
215
|
+
files,
|
|
216
|
+
};
|
|
217
|
+
}
|