@warren-dean/code-kit 0.7.0 → 0.8.1
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 +33 -0
- package/README.md +15 -3
- package/bin/code-kit.mjs +171 -3
- package/hooks/guard-bash.mjs +29 -2
- package/hooks/lib/adapters/context-graph.mjs +18 -19
- package/hooks/lib/journal.mjs +224 -0
- package/hooks/lib/loop-state.mjs +26 -0
- package/hooks/lib/next.mjs +13 -3
- package/hooks/lib/plan.mjs +8 -1
- package/hooks/lib/storymap.mjs +155 -0
- package/hooks/mod/codemap.mjs +328 -0
- package/hooks/mod/decisions.mjs +146 -0
- package/hooks/mod/loop.mjs +7 -3
- package/hooks/mod/mission.mjs +116 -0
- package/hooks/mod/panes.mjs +469 -34
- package/hooks/mod/register.mjs +196 -2
- package/hooks/mod/register.test.ts +456 -25
- package/hooks/mod/trail.mjs +152 -0
- package/hooks/mod/view.mjs +324 -116
- package/package.json +1 -1
- package/skills/init/SKILL.md +3 -3
|
@@ -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.8.1",
|
|
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.8.1",
|
|
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,39 @@
|
|
|
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.8.1 (2026-10-11)
|
|
6
|
+
|
|
7
|
+
Fixes for projects set up before 0.8.0.
|
|
8
|
+
|
|
9
|
+
- **No first merge for you here either.** A project set up before 0.8.0 has no code-kit rules on `main`, so its first merge still waited on you. Now the lead puts the rules you approved onto `main` itself (`code-kit queue rules`, a step the loop takes), without switching branches, and then merges the queue under them.
|
|
10
|
+
- **Waiting on the merge queue isn't a broken plan.** Stories that depend on branches waiting to merge were reported as a dependency cycle, sending the build back to spec design. `next` now says they wait for the merge.
|
|
11
|
+
- The band's key for decisions reads "d: Why, and what each choice does", not "d: d Why".
|
|
12
|
+
|
|
13
|
+
## 0.8.0 (2026-10-11)
|
|
14
|
+
|
|
15
|
+
You decide what's built and the rules; the lead delivers it. Only real decisions reach you, each with the story behind it, and the whole build reads as one story from your request to now.
|
|
16
|
+
|
|
17
|
+
**Who decides what**
|
|
18
|
+
|
|
19
|
+
- **No first merge.** Setting up commits the rules you approved straight onto `main`, so the lead can merge reviewed work from the start. Under autonomy, setup also drafts limits within which the lead merges and approves new packages itself; you adjust them before approving.
|
|
20
|
+
- **Lane work is the lead's.** While the loop runs, a lane's finished work never waits for you: the lead reviews, sends back and merges it. With the loop off, Review and Merge are offered to you as before.
|
|
21
|
+
- **Decisions come with their story.** Each one above the prompt says which story it's for ("for ST-4 Booking form (BOOK-1)"), and **d** opens it as a card in the new Decisions tab: what you asked for, the story and its requirements, what the agent was doing, what's already settled, why the lead couldn't settle it (the rule, and the limit it missed), and what each answer does. Usage and pauses are labelled "note", not "you".
|
|
22
|
+
- **The Journal.** A Journal tab, and `code-kit journal`, tell the build by day: your request and the decisions in your brief, the specification, each story handed out, sent back and merged, each approval and who gave it, each milestone finished, and the loop's steps this session.
|
|
23
|
+
|
|
24
|
+
**Seeing the build**
|
|
25
|
+
|
|
26
|
+
- **The mission band.** Above the prompt, while the build moves: the milestone being built and how far along it is, what the lead is doing, the loop's next step, and each agent at work with its current tool.
|
|
27
|
+
- **The Lanes pane** opens on the loop as a card (its mode, its steps, the next one), keeps each lane on fixed columns, shows a lane's stories as cards, and reads "orchestrating" while the lead works.
|
|
28
|
+
- **The code map.** A story's files in a column per layer with the imports between them; a file fills with a lane's colour while that lane reads or edits it, and says whether its why is written. `code-kit map <story>`.
|
|
29
|
+
- **The trail.** Each requirement drawn to its stories, code and tests, with a missing link in red. `code-kit trail`.
|
|
30
|
+
- Tabs are now Lanes 1, Decisions 2, Journal 3, Code map 4, Trail 5, Queue 6, Usage 7.
|
|
31
|
+
|
|
32
|
+
**Fixes**
|
|
33
|
+
|
|
34
|
+
- Pausing the loop is recorded (`code-kit loop`), so Context Graph's background work pauses with it. A new session starts unpaused.
|
|
35
|
+
- The stall check no longer nudges or restarts agents whose work is done, and until you choose how hands-off the loop is, a restart is offered, not taken.
|
|
36
|
+
- With Context Graph, agents can add the guidance they find without your approval; only its settings stay yours.
|
|
37
|
+
|
|
5
38
|
## 0.7.0 (2026-10-10)
|
|
6
39
|
|
|
7
40
|
The rest of the harness, in the mod (Claude Code 2.1.287 or later), and fixes from building a whole app with it.
|
package/README.md
CHANGED
|
@@ -17,6 +17,12 @@ It also carries the work from idea to merged code: spec an idea into requirement
|
|
|
17
17
|
<img src="https://raw.githubusercontent.com/warrendeanlangeveldt/code-kit/main/assets/workflow.svg" alt="How code-kit works: the specs create the agents, skills and rules; the lead agent delegates stories to lane agents and takes decisions and approvals to you; lanes send scope and dependency requests back to the lead; hooks enforce ownership, layers, approvals and spec-check while each lane implements, tests and commits; the lead reviews against the requirements and CI runs verify." width="820">
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
|
+
<p align="center">
|
|
21
|
+
<a href="https://warrendeanlangeveldt.github.io"><img src="https://warrendeanlangeveldt.github.io/assets/media/harness.gif" alt="code-kit and Context Graph in a Claude Code session: the band above the prompt, the lanes, the code map lighting up as agents read and edit, and the module graph" width="900"></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
**See it in action at [warrendeanlangeveldt.github.io](https://warrendeanlangeveldt.github.io)**, with a live replay of a real build.
|
|
25
|
+
|
|
20
26
|
## Quick start
|
|
21
27
|
|
|
22
28
|
You need Claude Code, Node 22 or later, and git. Then, in Claude Code:
|
|
@@ -266,9 +272,9 @@ Editing an agent file can't loosen a rule. And the agent files are kit files (`.
|
|
|
266
272
|
|
|
267
273
|
With Claude Code 2.1.287 or later, code-kit's mod shows the project in the session, in the terminal (VS Code's terminal included) and the Desktop app. Older versions skip it and keep enforcing everything through the hooks.
|
|
268
274
|
|
|
269
|
-
- **The band** sits above the prompt
|
|
275
|
+
- **The band** sits above the prompt. While the build is moving (the lead mid-turn, an agent at work, the loop started) it opens with the mission: **goal**, the milestone being built and how many of its stories are done; **now**, what the lead is doing and the loop's state, with **Pause** or **Resume**; **next**, the step the loop takes next; and **agents**, a line per agent at work with its current tool and how long since. With Context Graph installed, a line says when a lane edited files without understanding them, and only then. Beneath, labelled **you**, what only you can decide, each with the story it's for ("for ST-4 Booking form (BOOK-1)"), and **d** ("Why, and what each choice does") opens it as a card in the Decisions tab; usage and the plan's pause are labelled **note**. While the loop runs, a lane's finished work is the lead's to review and merge, so it never waits here; with the loop off, Review and Merge are offered to you. With nothing happening and nothing waiting, the band is gone. It shows:
|
|
270
276
|
- an agent's call held for you: installing a new package, writing or committing a protected file, or a lane changing the kit. Rather than stopping the agent, the call waits while the band asks ("web wants to install dayjs · 1:53"). **Approve** opens the confirmation and, once you confirm, the same call runs; **Refuse**, or no answer within `harness.hold.minutes` (2 unless set), refuses it as before. Under `autonomy: autonomous`, a call within `approvals.delegate` is approved as delegated without asking. Nothing is held under `claude -p` or the SDK, where nobody can answer;
|
|
271
|
-
- the lead loop, under `harness.autonomy`. `/code-kit:init` asks you how hands-off you want it. Until you've chosen, the loop's first step waits in the band ("code-kit can keep the build moving between your prompts") with **Keep going**, **Ask each time** and **Off**, and your answer is recorded as your setting. Once the lead has finished a turn, the loop keeps the build moving: with the lead idle and nothing typed in the prompt, it asks `code-kit next` and prompts the lead to dispatch the ready stories, review a finished one, or build its own, never the same step twice while nothing has changed. A step the lead was prompted with twice in a row and that is still next goes to you instead, in the band: "Merge lead/st-1: still next after 2 prompts. Ask the lead again?" with **Go**. **Pause** and **Resume** stop and start it. Under `propose`, each step waits in the band for **Go**; under `off`, the loop does nothing. A lane agent with no tool call for `harness.stall.nudgeMinutes` (5) is messaged; at `stall.restartMinutes` (10) the lead is asked to stop it and dispatch its story again, at most `stall.maxRestarts` (2) times, and then the band flags the lane. When every story is done, the band says so and the loop stops. With `harness.agents.reviewer.on`, a finished branch is first reviewed in the background by code-kit's reviewer agent, which the lead starts: it runs `verify` and the review checklist, read-only, and grades each finding blocker, concern or nit. The lead's review prompt then carries the findings, and a blocker means the branch is sent back. The pane shows each review, running with its clock or what it found. If the reviewer hasn't reported within 15 minutes, or the plan's 5-hour use is past `harness.background.pauseAtPercent`, the lead reviews without it. A branch the lead's review passes joins the merge queue (`code-kit queue`). Branches merge one at a time, in the order they passed, each verified against the base as it is then: the lead merges where `approvals.delegate.merge` is on under `autonomous`, otherwise the band shows the head with **Merge** for you. Who may merge is read from the config committed on the base, so a branch can't grant itself merges
|
|
277
|
+
- the lead loop, under `harness.autonomy`. `/code-kit:init` asks you how hands-off you want it. Until you've chosen, the loop's first step waits in the band ("code-kit can keep the build moving between your prompts") with **Keep going**, **Ask each time** and **Off**, and your answer is recorded as your setting. Once the lead has finished a turn, the loop keeps the build moving: with the lead idle and nothing typed in the prompt, it asks `code-kit next` and prompts the lead to dispatch the ready stories, review a finished one, or build its own, never the same step twice while nothing has changed. A step the lead was prompted with twice in a row and that is still next goes to you instead, in the band: "Merge lead/st-1: still next after 2 prompts. Ask the lead again?" with **Go**. **Pause** and **Resume** stop and start it. Under `propose`, each step waits in the band for **Go**; under `off`, the loop does nothing. A lane agent with no tool call for `harness.stall.nudgeMinutes` (5) is messaged, unless its work is done (it handed back, or its branch is in review or the merge queue); at `stall.restartMinutes` (10) the lead is asked to stop it and dispatch its story again, at most `stall.maxRestarts` (2) times, and then the band flags the lane. When every story is done, the band says so and the loop stops. With `harness.agents.reviewer.on`, a finished branch is first reviewed in the background by code-kit's reviewer agent, which the lead starts: it runs `verify` and the review checklist, read-only, and grades each finding blocker, concern or nit. The lead's review prompt then carries the findings, and a blocker means the branch is sent back. The pane shows each review, running with its clock or what it found. If the reviewer hasn't reported within 15 minutes, or the plan's 5-hour use is past `harness.background.pauseAtPercent`, the lead reviews without it. A branch the lead's review passes joins the merge queue (`code-kit queue`). Branches merge one at a time, in the order they passed, each verified against the base as it is then: the lead merges where `approvals.delegate.merge` is on under `autonomous`, otherwise the band shows the head with **Merge** for you. Who may merge is read from the config committed on the base, so a branch can't grant itself merges. There's no first merge for you: setting up commits the rules you approved straight onto the base, and in a project set up before that, the lead puts them there itself (`code-kit queue rules`) before its first merge. A merged lane's worktree is removed when it holds nothing uncommitted. A head that conflicts or fails verify goes back to its lane. The pane lists the queue, and lanes whose branch waits in it read `queued`;
|
|
272
278
|
- approvals agents have asked for: **Approve…** opens a confirmation with a reason prefilled from the request, which you keep or rewrite;
|
|
273
279
|
- stories finished and waiting for review: **Review** asks the lead to run `/code-kit:review` on the branch, and **Merge** runs `verify`, checks included, then merges if it passes;
|
|
274
280
|
- a finish check failing in this session: **Lanes** opens the pane;
|
|
@@ -277,7 +283,7 @@ With Claude Code 2.1.287 or later, code-kit's mod shows the project in the sessi
|
|
|
277
283
|
|
|
278
284
|
Each button has a digit: type it in an empty prompt to press it.
|
|
279
285
|
|
|
280
|
-
- **`/lanes`** opens the Lanes pane. Its first line counts the stories by state (building, in review, queued, sent back, blocked, ready, merged), with the loop's state and the plan's 5-hour use; press a count to show only those lanes. Beneath it, whatever needs you (held calls, approvals, reviews, merges, stalled lanes, outliers) is pinned with its key. Tabs on **1** to **
|
|
286
|
+
- **`/lanes`** opens the Lanes pane. Its first line counts the stories by state (building, in review, queued, sent back, blocked, ready, merged), with the loop's state and the plan's 5-hour use; press a count to show only those lanes. Beneath it, whatever needs you (held calls, approvals, reviews, merges, stalled lanes, outliers) is pinned with its key. Tabs on **1** to **7**: **Lanes** opens on the loop as a card: its mode, the steps it took, the one running and the next. Then each lane on fixed columns: its name, state and story, its agent's current or last tool call with the time since beneath (amber once quiet, red when stalled), and its tokens; the lead reads orchestrating while its turn runs. A selected lane shows its stories as cards (state, requirements, what each waits on or its branch), then its agent and the paths it owns. **Decisions**, each decision that needs you as a card: what you asked for, the story and its requirements, what was happening, what's already settled, why the lead couldn't settle it, and each option with what it does; **Journal**, the build from your request to now in plain sentences, by day: the request and the decisions in the brief, the specification, each story handed out, sent back and merged, each approval and who gave it, each milestone finished, and the loop's steps this session, yours in yellow. Each lane has its own small pixel character, tinted in the lane's colour, whose pose shows its state: working (animated while its agent works), waiting on you, quiet, stalled, done or resting; the lead's wears a crown. Under each lane, its story's timeline on a shared time axis: building, then in review, with send-backs (↩), approvals (◆) and the merge (✓) marked where they fell; **Code map**, a story's files in a column per layer with the imports between them: a file fills with a lane's colour while that lane reads or edits it, and says whether its why (its Context Graph card) is written, out of date or not written yet; **j**/**k** select a file to see its card and what it imports and what imports it, and **o** opens it in Context Graph (box-drawing text in the terminal, an SVG on Desktop); **Trail**, every requirement in the specs as a cell by spec, its glyph and colour saying whether it's done and tested, done but untested, in progress, todo, without a story or removed, with a legend counting each. **h**/**l** and **j**/**k** move between cells and specs (or pick one from the list), showing the requirement's stories, their branches, the code they changed and the tests that name it; beneath the cells, the spec's trail drawn as a flow, each requirement to its stories, code and tests, with a missing link dashed in red; **Queue**, the merge queue; and **Usage**. **j** and **k** move between lanes, and the selected lane's acts are on **a** (approve), **r** (review), **s** (send back, with your reason), **m** (merge), **n** (nudge its agent) and **x** (stop it); **p** pauses or resumes the loop, and **f** filters the lanes by name, agent or story. **Enter** (or **o**) opens the selected lane's story: its requirements beside its spec-check report, the reviewer's findings by severity (each a way into the diff), `verify`'s problems, its usage and loop steps, and its diff against the base, file by file, with the acts its state allows. **b** or Escape goes back to the lanes. Inline (when the terminal can't dock it beside the transcript), it shows the counts, what needs you and a line per lane. It stays current within 2 seconds of a commit, a branch change, a plan edit or an agent starting or stopping. `/lanes` again, or Escape, closes it.
|
|
281
287
|
- **Refusals** a code-kit hook gives an agent are drawn as cards: what was refused, the rule, what to do, and Approve… where your approval would allow it. The full text is a press away.
|
|
282
288
|
- **`/harness`** opens the harness settings: how the lead loop acts, how long a call waiting on your approval is held, when a stalled lane is nudged and restarted, the reviewer agent and its model, and the use at which background agents pause. Changing one asks your reason, writes `.claude/code-kit.json` and logs a `kit` approval. Lanes, paths and rules still change through `/code-kit:init`.
|
|
283
289
|
- **`/approvals`** lists the requests waiting and the approvals in force, with minutes left. **`/verify-branch`** runs `code-kit verify` on this branch. Neither calls the model.
|
|
@@ -340,9 +346,15 @@ code-kit settings [--json] # the harness setti
|
|
|
340
346
|
code-kit settings set <key> <value> --reason "…" # change one, as the person: written to the config and logged as a kit approval
|
|
341
347
|
code-kit queue [--json] # the merge queue: branches whose review passed, in order
|
|
342
348
|
code-kit queue add|drop <branch> # the lead queues a branch its review passed, or takes it out
|
|
349
|
+
code-kit queue rules # the lead puts the approved rules onto a base that has none yet (a project set up before 0.8.0)
|
|
343
350
|
code-kit queue merge --delegated|--person # merge the head, verified against the base as it is now; a conflict or failed verify sends it back to its lane
|
|
351
|
+
code-kit loop [--json] # whether the person paused the lead loop; other tools' background work pauses with it
|
|
352
|
+
code-kit loop pause|resume # the person's act, as Pause and Resume in the band
|
|
344
353
|
code-kit story <id> [--json] # one story's requirements, spec-check report, diff against the base and verify (without checks), for the drill-down
|
|
345
354
|
code-kit timeline [--json] # when each story's work happened: its branch made, last commit, merge, send-backs and approvals
|
|
355
|
+
code-kit journal [--json] # the build's lineage: the request, its decisions, the spec, each story handed out, sent back and merged, each approval and who gave it
|
|
356
|
+
code-kit map <story> [--json] # a story's code: its files by layer, the imports between them, and what adapters know of each
|
|
357
|
+
code-kit trail [--json] # each requirement to its stories, the code they changed and the tests that name it
|
|
346
358
|
code-kit adapters [--json --session id] # the adapters in use; with --json, what they know of each lane's work in a session
|
|
347
359
|
code-kit hold <id> [--minutes N] # used by the mod: a held call waits here for the band's answer (--answer approved|refused)
|
|
348
360
|
code-kit approve <name>... --reason "…" [--lane name] [--delegated | --via pane|<tool>] # record a person's approval from chat, or (--delegated) grant one within the delegated rules; a package name stands for its dep- approval
|
package/bin/code-kit.mjs
CHANGED
|
@@ -53,8 +53,11 @@ import { traceFile } from '../hooks/lib/trace.mjs';
|
|
|
53
53
|
import { buildStatus, specCheckRows } from '../hooks/lib/plan.mjs';
|
|
54
54
|
import { recordSentBack } from '../hooks/lib/reviews.mjs';
|
|
55
55
|
import { storyTimes } from '../hooks/lib/timeline.mjs';
|
|
56
|
+
import { storyMap, trail } from '../hooks/lib/storymap.mjs';
|
|
57
|
+
import { journal } from '../hooks/lib/journal.mjs';
|
|
56
58
|
import { dequeue, enqueue, lastMerged, readQueue, settle, waiting } from '../hooks/lib/queue.mjs';
|
|
57
59
|
import { SETTINGS, parseSetting, settingOf, withSetting } from '../hooks/lib/harness.mjs';
|
|
60
|
+
import { readLoop, writeLoop } from '../hooks/lib/loop-state.mjs';
|
|
58
61
|
import {
|
|
59
62
|
APPROVAL_MINUTES,
|
|
60
63
|
approvalLifetime,
|
|
@@ -333,9 +336,82 @@ function configAt(ref) {
|
|
|
333
336
|
return config ?? null;
|
|
334
337
|
}
|
|
335
338
|
|
|
339
|
+
/**
|
|
340
|
+
* SETUP-2: a project set up before setup committed its rules onto the base has none there, so nothing
|
|
341
|
+
* could be merged by the lead. `queue rules` commits the rules as committed on this branch (the ones the
|
|
342
|
+
* person approved) onto the base, as setup now does, without checking the base out: a commit of the base
|
|
343
|
+
* with those files changed, the base moved to it. Only while the base has no rules, and only the lead.
|
|
344
|
+
*/
|
|
345
|
+
function rulesOntoBase(root, config) {
|
|
346
|
+
const into = intoName ?? config.branches.protected[0];
|
|
347
|
+
const git = (args, opts = {}) =>
|
|
348
|
+
execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', ...opts }).trim();
|
|
349
|
+
const head = git(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
350
|
+
if (head === into)
|
|
351
|
+
die(
|
|
352
|
+
`You're on ${into}: commit ${file} here as setup does (\`git add ${file} && git commit\`), which the hooks allow while ${into} has no rules.`,
|
|
353
|
+
);
|
|
354
|
+
if (configAt(into))
|
|
355
|
+
return out(
|
|
356
|
+
`${into} already has code-kit's rules committed; the merge queue merges under them.`,
|
|
357
|
+
);
|
|
358
|
+
if (!configAt('HEAD'))
|
|
359
|
+
die(
|
|
360
|
+
`${head} has no ${file} committed. Commit the rules the person approved first, then run this again.`,
|
|
361
|
+
);
|
|
362
|
+
const used = git(['worktree', 'list', '--porcelain'])
|
|
363
|
+
.split('\n')
|
|
364
|
+
.some((l) => l === `branch refs/heads/${into}`);
|
|
365
|
+
if (used)
|
|
366
|
+
die(
|
|
367
|
+
`${into} is checked out in a worktree, so moving it here would leave that checkout behind. Commit ${file} in that checkout instead (the hooks allow it while ${into} has no rules).`,
|
|
368
|
+
);
|
|
369
|
+
// The rules, and the baseline of existing violations they were approved with.
|
|
370
|
+
const files = [file, BASELINE_FILE].filter((f) => {
|
|
371
|
+
try {
|
|
372
|
+
execFileSync('git', ['-C', root, 'cat-file', '-e', `HEAD:${f}`], { stdio: 'ignore' });
|
|
373
|
+
return true;
|
|
374
|
+
} catch {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
const index = join(tmpdir(), `code-kit-rules-${process.pid}`);
|
|
379
|
+
const env = { ...process.env, GIT_INDEX_FILE: index };
|
|
380
|
+
try {
|
|
381
|
+
git(['read-tree', into], { env });
|
|
382
|
+
for (const f of files)
|
|
383
|
+
git(
|
|
384
|
+
['update-index', '--add', '--cacheinfo', `100644,${git(['rev-parse', `HEAD:${f}`])},${f}`],
|
|
385
|
+
{
|
|
386
|
+
env,
|
|
387
|
+
},
|
|
388
|
+
);
|
|
389
|
+
const tree = git(['write-tree'], { env });
|
|
390
|
+
const before = git(['rev-parse', into]);
|
|
391
|
+
const commit = git(
|
|
392
|
+
[
|
|
393
|
+
'commit-tree',
|
|
394
|
+
tree,
|
|
395
|
+
'-p',
|
|
396
|
+
before,
|
|
397
|
+
'-m',
|
|
398
|
+
`code-kit's rules onto ${into}, as the person approved them on ${head}\n\nFrom now on ${into}'s committed rules decide who merges into it.`,
|
|
399
|
+
],
|
|
400
|
+
{ env },
|
|
401
|
+
);
|
|
402
|
+
git(['update-ref', `refs/heads/${into}`, commit, before]);
|
|
403
|
+
} finally {
|
|
404
|
+
rmSync(index, { force: true });
|
|
405
|
+
}
|
|
406
|
+
const now = configAt(into);
|
|
407
|
+
out(
|
|
408
|
+
`Committed ${files.join(' and ')} onto ${into}, from ${head}. ${now?.approvals.delegate?.merge ? 'The lead merges the queue from now on (code-kit queue merge --delegated).' : `${into}'s rules don't delegate merges, so the person merges the queue.`}`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
336
412
|
/** Why the first merge into `into`, which has no config committed yet, is the person's. */
|
|
337
413
|
const firstMerge = (into, branch) =>
|
|
338
|
-
`${into} has no ${file} committed yet, so it delegates nothing
|
|
414
|
+
`${into} has no ${file} committed yet, so it delegates nothing. Put the approved rules onto it first: \`node "${process.argv[1]}" queue rules\`. Then merge ${branch} again; from then on ${into}'s committed rules decide who merges.`;
|
|
339
415
|
|
|
340
416
|
/** Why a merge didn't happen: `refused` (the rules), `verify` (problems) or `conflict`. */
|
|
341
417
|
class MergeStop extends Error {
|
|
@@ -524,6 +600,7 @@ function queue() {
|
|
|
524
600
|
const place = waiting(readQueue(root)).length;
|
|
525
601
|
return out(`Queued ${branch} to merge${place > 1 ? `, ${place - 1} ahead of it` : ', next'}.`);
|
|
526
602
|
}
|
|
603
|
+
if (sub === 'rules') return rulesOntoBase(root, config);
|
|
527
604
|
if (sub === 'merge') {
|
|
528
605
|
const byPerson = mergeMode();
|
|
529
606
|
const entries = readQueue(root);
|
|
@@ -558,7 +635,7 @@ function queue() {
|
|
|
558
635
|
}
|
|
559
636
|
if (sub !== undefined)
|
|
560
637
|
die(
|
|
561
|
-
'code-kit queue [--json] | queue add <branch> | queue drop <branch> | queue merge --delegated|--person',
|
|
638
|
+
'code-kit queue [--json] | queue add <branch> | queue drop <branch> | queue rules | queue merge --delegated|--person',
|
|
562
639
|
);
|
|
563
640
|
const entries = readQueue(root);
|
|
564
641
|
if (flag('--json')) return out(JSON.stringify(entries, null, 2));
|
|
@@ -568,6 +645,69 @@ function queue() {
|
|
|
568
645
|
left.forEach((e, i) => out(` ${i + 1}. ${e.branch} (passed review ${e.passed})`));
|
|
569
646
|
}
|
|
570
647
|
|
|
648
|
+
/**
|
|
649
|
+
* MAP-1: a story's code: the files it changed by layer, with the imports between them, and what the
|
|
650
|
+
* active adapters know of each (its card, its rules). `map <id> --json` is what the pane draws.
|
|
651
|
+
*/
|
|
652
|
+
function mapCommand(id) {
|
|
653
|
+
const { config, raw } = load();
|
|
654
|
+
const { specs, plan } = config.docs ?? {};
|
|
655
|
+
if (!specs || !plan)
|
|
656
|
+
die('map needs docs.specs and docs.plan in the config. The spec-design skill writes both.');
|
|
657
|
+
const from = base(config, false);
|
|
658
|
+
const { stories } = buildStatus('.', config, from, tracked());
|
|
659
|
+
const s = stories.find((x) => x.id === id.toUpperCase());
|
|
660
|
+
if (!s) die(`There is no story ${id} in ${plan}.`);
|
|
661
|
+
const map = storyMap(resolve('.'), s, config, from);
|
|
662
|
+
const facts = adapterFacts(raw, resolve('.'), { paths: map.files.map((f) => f.path) });
|
|
663
|
+
for (const f of map.files)
|
|
664
|
+
f.adapters = Object.fromEntries(
|
|
665
|
+
facts.filter((a) => a.files?.[f.path]).map((a) => [a.name, a.files[f.path]]),
|
|
666
|
+
);
|
|
667
|
+
map.open = facts.find((a) => a.open)?.open ?? null;
|
|
668
|
+
if (flag('--json')) return out(JSON.stringify(map, null, 2));
|
|
669
|
+
out(`${s.id} ${s.title} (${map.files.length} file(s)):`);
|
|
670
|
+
for (const layer of map.layers) {
|
|
671
|
+
out(` ${layer}`);
|
|
672
|
+
for (const f of map.files.filter((x) => x.layer === layer))
|
|
673
|
+
out(` ${f.path}${f.imports.length ? ` → ${f.imports.join(', ')}` : ''}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** JOURNAL-1: the build's lineage, from the request to each merge, in plain sentences. */
|
|
678
|
+
function journalCommand() {
|
|
679
|
+
const { config } = load();
|
|
680
|
+
const { specs, plan } = config.docs ?? {};
|
|
681
|
+
if (!specs || !plan)
|
|
682
|
+
die('journal needs docs.specs and docs.plan in the config. The spec-design skill writes both.');
|
|
683
|
+
const from = base(config, false);
|
|
684
|
+
const events = journal(
|
|
685
|
+
resolve('.'),
|
|
686
|
+
buildStatus('.', config, from, tracked()),
|
|
687
|
+
from,
|
|
688
|
+
config.docs,
|
|
689
|
+
);
|
|
690
|
+
if (flag('--json')) return out(JSON.stringify({ base: from, events }, null, 2));
|
|
691
|
+
for (const e of events)
|
|
692
|
+
out(`${new Date(e.at).toISOString().slice(0, 16).replace('T', ' ')} ${e.text}`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** TRAIL-1: each requirement to its stories, the code they changed and the tests that name it. */
|
|
696
|
+
function trailCommand() {
|
|
697
|
+
const { config } = load();
|
|
698
|
+
const { specs, plan } = config.docs ?? {};
|
|
699
|
+
if (!specs || !plan)
|
|
700
|
+
die('trail needs docs.specs and docs.plan in the config. The spec-design skill writes both.');
|
|
701
|
+
const from = base(config, false);
|
|
702
|
+
const status = buildStatus('.', config, from, tracked());
|
|
703
|
+
const rows = trail(resolve('.'), status, from);
|
|
704
|
+
if (flag('--json')) return out(JSON.stringify({ base: from, requirements: rows }, null, 2));
|
|
705
|
+
for (const r of rows)
|
|
706
|
+
out(
|
|
707
|
+
`${pad(r.id, 10)} ${pad(r.stories.map((s) => s.id).join(', ') || '-', 14)} ${pad(`${r.files.length} file(s)`, 10)} ${r.tests.length ? r.tests.join(', ') : 'no test names it'}`,
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
571
711
|
/** When each story's work happened, for the mod's timelines (VIEW-2). */
|
|
572
712
|
function timeline() {
|
|
573
713
|
const { config } = load();
|
|
@@ -1069,8 +1209,31 @@ async function approve(given) {
|
|
|
1069
1209
|
);
|
|
1070
1210
|
}
|
|
1071
1211
|
|
|
1212
|
+
/**
|
|
1213
|
+
* The lead loop's pause (LOOP-6): `loop` says whether the person paused it; `loop pause|resume`, the
|
|
1214
|
+
* person's act from the code-kit band, records it, so other tools' background work pauses with it.
|
|
1215
|
+
*/
|
|
1216
|
+
function loopCommand() {
|
|
1217
|
+
const root = resolve('.');
|
|
1218
|
+
const [sub] = rest;
|
|
1219
|
+
if (sub === 'pause' || sub === 'resume') {
|
|
1220
|
+
writeLoop(root, sub === 'pause');
|
|
1221
|
+
return out(sub === 'pause' ? 'The lead loop is paused.' : 'The lead loop is running.');
|
|
1222
|
+
}
|
|
1223
|
+
if (sub !== undefined) die('code-kit loop [--json] | loop pause | loop resume');
|
|
1224
|
+
const state = readLoop(root);
|
|
1225
|
+
out(
|
|
1226
|
+
flag('--json')
|
|
1227
|
+
? JSON.stringify(state)
|
|
1228
|
+
: state.paused
|
|
1229
|
+
? `The lead loop is paused (since ${state.at}).`
|
|
1230
|
+
: 'The lead loop is running.',
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1072
1234
|
const commands = {
|
|
1073
1235
|
check,
|
|
1236
|
+
loop: loopCommand,
|
|
1074
1237
|
approve: () => (rest.length ? approve(rest) : usage()),
|
|
1075
1238
|
merge: () => (rest.length === 1 ? merge(rest[0]) : usage()),
|
|
1076
1239
|
requests,
|
|
@@ -1079,6 +1242,9 @@ const commands = {
|
|
|
1079
1242
|
hold: () => (rest.length === 1 ? hold(rest[0]) : usage()),
|
|
1080
1243
|
queue,
|
|
1081
1244
|
story: () => (rest.length === 1 ? story(rest[0]) : usage()),
|
|
1245
|
+
map: () => (rest.length === 1 ? mapCommand(rest[0]) : usage()),
|
|
1246
|
+
trail: trailCommand,
|
|
1247
|
+
journal: journalCommand,
|
|
1082
1248
|
timeline,
|
|
1083
1249
|
'sent-back': () => (rest.length === 1 ? sentBackCommand(rest[0]) : usage()),
|
|
1084
1250
|
trace: () => (rest.length ? trace(rest) : usage()),
|
|
@@ -1102,7 +1268,9 @@ function usage() {
|
|
|
1102
1268
|
' | stops [--session id] [--json] | sent-back <branch> [--reason "…"]\n' +
|
|
1103
1269
|
' | settings [--json] | settings set <key> <value> --reason "…" [--via pane]\n' +
|
|
1104
1270
|
' | hold <id> [--minutes N] | hold <id> --answer approved|refused\n' +
|
|
1105
|
-
' | queue [--json] | queue add|drop <branch> | queue merge --delegated|--person\n' +
|
|
1271
|
+
' | queue [--json] | queue add|drop <branch> | queue rules | queue merge --delegated|--person\n' +
|
|
1272
|
+
' | loop [--json] | loop pause|resume\n' +
|
|
1273
|
+
' | map <story> [--json] | trail [--json] | journal [--json]\n' +
|
|
1106
1274
|
' | story <id> [--json] | timeline [--json]',
|
|
1107
1275
|
);
|
|
1108
1276
|
}
|
package/hooks/guard-bash.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { join, resolve } from 'node:path';
|
|
|
11
11
|
import { addedPackages, dependencyApproval, isDependencyFile } from './lib/dependencies.mjs';
|
|
12
12
|
import { CODE_KIT, GIT, codeOf, commandsOf, runs } from './lib/shell.mjs';
|
|
13
13
|
import { block, start } from './lib/hook.mjs';
|
|
14
|
+
import { CONFIG_FILE } from './lib/config.mjs';
|
|
14
15
|
import { noteBefore } from './lane-audit.mjs';
|
|
15
16
|
import {
|
|
16
17
|
APPROVAL_LOG,
|
|
@@ -73,6 +74,7 @@ const packages = addedPackages(cmd);
|
|
|
73
74
|
const approves = runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+approve\b`));
|
|
74
75
|
const delegatedMerge = runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+(?:queue\s+)?merge\b`));
|
|
75
76
|
const queues = runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+queue\s+(?:add|drop)\b`));
|
|
77
|
+
const rulesOntoBase = runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+queue\s+rules\b`));
|
|
76
78
|
const commits = runs(cmd, new RegExp(String.raw`^${GIT}commit\b`));
|
|
77
79
|
if (error) {
|
|
78
80
|
if (commits) block(`Blocked: ${error}\nFix the config before committing.`);
|
|
@@ -89,6 +91,14 @@ const branches = config.branches.protected
|
|
|
89
91
|
// The branch is read before the command runs, so a command that switches to a protected branch and then
|
|
90
92
|
// commits or merges in the same breath is judged by what it does, not by where it starts. Moving a
|
|
91
93
|
// protected branch directly (branch -f, update-ref) is a commit on it by another name.
|
|
94
|
+
/** Whether `file` is committed at HEAD in the repository at `root`. */
|
|
95
|
+
function committedOnHead(root, file) {
|
|
96
|
+
return (
|
|
97
|
+
spawnSync('git', ['-C', root, 'cat-file', '-e', `HEAD:${file}`], { stdio: 'ignore' }).status ===
|
|
98
|
+
0
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
92
102
|
const writesHistory = runs(
|
|
93
103
|
cmd,
|
|
94
104
|
new RegExp(String.raw`^${GIT}(commit|merge|cherry-pick|rebase|revert|am)\b`),
|
|
@@ -151,6 +161,11 @@ if ((delegatedMerge && /\s--person\b/.test(cmd)) || (approves && /\s--via\b/.tes
|
|
|
151
161
|
block(
|
|
152
162
|
`Blocked: merging or approving as the person is the person's own act, from the code-kit pane or their terminal. Ask them, and stop.\nCommand: ${cmd}`,
|
|
153
163
|
);
|
|
164
|
+
// Pausing and resuming the lead loop is the person's, from the band or their terminal.
|
|
165
|
+
if (runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+loop\s+(pause|resume)\b`)))
|
|
166
|
+
block(
|
|
167
|
+
`Blocked: pausing or resuming the lead loop is the person's own act, from the code-kit band or their terminal. Ask them, and stop.\nCommand: ${cmd}`,
|
|
168
|
+
);
|
|
154
169
|
// The harness settings are the person's to change, from the pane or their terminal.
|
|
155
170
|
if (runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+settings\s+set\b`)))
|
|
156
171
|
block(
|
|
@@ -160,6 +175,10 @@ if (queues && actor.kind !== 'lead')
|
|
|
160
175
|
block(
|
|
161
176
|
`Blocked: only the lead puts branches in the merge queue, after its review passes them. Finish your story and commit it on your branch; the lead reviews it.\nCommand: ${cmd}`,
|
|
162
177
|
);
|
|
178
|
+
if (rulesOntoBase && actor.kind !== 'lead')
|
|
179
|
+
block(
|
|
180
|
+
`Blocked: only the lead puts code-kit's rules onto the protected branch. Finish your story and commit it on your branch; the lead takes it from there.\nCommand: ${cmd}`,
|
|
181
|
+
);
|
|
163
182
|
if (delegatedMerge && actor.kind !== 'lead')
|
|
164
183
|
block(
|
|
165
184
|
`Blocked: only the lead merges into a protected branch. Finish your story and commit it on your branch; the lead reviews and merges it.\nCommand: ${cmd}`,
|
|
@@ -233,9 +252,17 @@ try {
|
|
|
233
252
|
} catch {
|
|
234
253
|
// not a git repository yet
|
|
235
254
|
}
|
|
236
|
-
|
|
255
|
+
// SETUP-1: the one exception is setting up. Until the protected branch has the rules committed, the
|
|
256
|
+
// lead commits the setup the person approved straight onto it, so no first merge needs anyone; from
|
|
257
|
+
// that commit on, the branch is protected by the rules it now holds.
|
|
258
|
+
const settingUp =
|
|
259
|
+
actor.kind === 'lead' &&
|
|
260
|
+
runs(cmd, new RegExp(String.raw`^${GIT}commit\b`)) &&
|
|
261
|
+
!runs(cmd, new RegExp(String.raw`^${GIT}(merge|cherry-pick|rebase|revert|am)\b`)) &&
|
|
262
|
+
!committedOnHead(root, CONFIG_FILE);
|
|
263
|
+
if (writesHistory && config.branches.protected.includes(branch) && !settingUp) {
|
|
237
264
|
block(
|
|
238
|
-
`Blocked: never commit or merge on ${branch}. Work on a branch;
|
|
265
|
+
`Blocked: never commit or merge on ${branch}. Work on a branch; the lead merges it after review (or the person, where merges aren't delegated).`,
|
|
239
266
|
);
|
|
240
267
|
}
|
|
241
268
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
// Context Graph (ctx) keeps a project's engineering context in `.ctx/`:
|
|
2
|
-
//
|
|
3
|
-
// each file's card (its why) in cards.ctx.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// lead act on what the person says in chat (`approvals.lead`). A
|
|
8
|
-
// `Ctx-Ratified-By: <ratifier> (delegated)`, is the lead's own act under
|
|
9
|
-
// rules: only the lead may write that trailer
|
|
1
|
+
// Context Graph (ctx) keeps a project's engineering context in `.ctx/`: rules and concepts in graph.ctx
|
|
2
|
+
// (proposals in proposals.ctx), settings in config.toml, the decisions agents record as they work in
|
|
3
|
+
// decisions.ctx, and each file's card (its why) in cards.ctx. It runs in the background, so every agent
|
|
4
|
+
// writes its cards, its decisions and the guidance it finds; the settings are the lead's, and a change
|
|
5
|
+
// to them goes through the approval log. A rule that blocks edits, or a concept, needs a person's
|
|
6
|
+
// signature, a commit trailer, and Context Graph's own gate checks it, so no Claude actor may write that
|
|
7
|
+
// trailer, unless the project lets the lead act on what the person says in chat (`approvals.lead`). A
|
|
8
|
+
// delegated ratification, signed `Ctx-Ratified-By: <ratifier> (delegated)`, is the lead's own act under
|
|
9
|
+
// the repository's [delegate] rules: only the lead may write that trailer.
|
|
10
10
|
// Ratifying with `--commit`, dropping a proposal and changing the harness settings are the person's acts
|
|
11
11
|
// in ctx too, so no actor runs
|
|
12
12
|
// them; only a command the line runs counts, so a heredoc or quoted text that mentions one isn't refused.
|
|
@@ -41,16 +41,12 @@ export default {
|
|
|
41
41
|
detect: (root) => existsSync(join(root, '.ctx')),
|
|
42
42
|
config: {
|
|
43
43
|
lead: ['.ctx/**'],
|
|
44
|
-
|
|
44
|
+
// Context Graph runs in the background: every agent writes its cards and decisions, and the guidance
|
|
45
|
+
// rules it finds (which inform and never block). A rule that blocks edits, or a concept, still needs
|
|
46
|
+
// a person's signed commit, and Context Graph's own gate checks that on every pull request.
|
|
47
|
+
anyActor: ['.ctx/decisions.ctx', '.ctx/cards.ctx', '.ctx/graph.ctx', '.ctx/proposals.ctx'],
|
|
45
48
|
protected: [
|
|
46
|
-
|
|
47
|
-
glob: '.ctx/graph.ctx',
|
|
48
|
-
approval: 'ctx',
|
|
49
|
-
why: "Context Graph's rules and concepts",
|
|
50
|
-
// A person's ratification (`ctx ratify --commit`, `ctx drop --commit`) is signed, not logged.
|
|
51
|
-
signedBy: PERSON_SIGNED,
|
|
52
|
-
},
|
|
53
|
-
// Setting Context Graph up is ratified the same way: the person commits it from their terminal.
|
|
49
|
+
// Its settings are the person's: a change is logged, or signed when they commit it themselves.
|
|
54
50
|
{
|
|
55
51
|
glob: '.ctx/config.toml',
|
|
56
52
|
approval: 'ctx',
|
|
@@ -100,7 +96,7 @@ export default {
|
|
|
100
96
|
}
|
|
101
97
|
return byType;
|
|
102
98
|
},
|
|
103
|
-
/** Per file: the rules that apply to it, agreed and proposed. */
|
|
99
|
+
/** Per file: the rules that apply to it, agreed and proposed, and its card: current, stale or missing, with its text. */
|
|
104
100
|
files: (root, paths) =>
|
|
105
101
|
Object.fromEntries(
|
|
106
102
|
paths.map((path) => {
|
|
@@ -108,6 +104,9 @@ export default {
|
|
|
108
104
|
return [
|
|
109
105
|
path,
|
|
110
106
|
{
|
|
107
|
+
card: f?.card
|
|
108
|
+
? { state: f.card.fresh === false ? 'stale' : 'current', text: f.card.text ?? '' }
|
|
109
|
+
: { state: 'missing', text: '' },
|
|
111
110
|
rules: (f?.rules ?? []).map((r) => ({
|
|
112
111
|
id: r.id,
|
|
113
112
|
text: r.text,
|