@warren-dean/code-kit 0.10.0 → 0.11.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 +18 -0
- package/README.md +33 -31
- package/bin/code-kit.mjs +99 -3
- package/hooks/guard-bash.mjs +8 -0
- package/hooks/lib/rules.mjs +214 -4
- package/hooks/lib/team.mjs +62 -7
- package/hooks/mod/decisions.mjs +5 -1
- package/hooks/mod/loop.mjs +49 -0
- package/hooks/mod/panes.mjs +42 -49
- package/hooks/mod/register.mjs +122 -14
- package/hooks/mod/register.test.ts +283 -15
- package/hooks/mod/teamview.mjs +469 -74
- package/hooks/mod/view.mjs +59 -11
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +1 -1
- package/skills/init/SKILL.md +2 -1
- package/skills/review/SKILL.md +1 -1
- package/skills/team-init/SKILL.md +59 -0
package/hooks/mod/view.mjs
CHANGED
|
@@ -630,6 +630,31 @@ export const APPROVE_ID = 'code-kit-approve';
|
|
|
630
630
|
export const RESULT_ID = 'code-kit-result';
|
|
631
631
|
|
|
632
632
|
/** An approval name as a person reads it: a dependency approval by its package. */
|
|
633
|
+
/**
|
|
634
|
+
* CARD-2: open requests as the decisions they are: one per approval and lane, with every file (or
|
|
635
|
+
* command) it was asked for, newest first. Approving it covers them all.
|
|
636
|
+
*/
|
|
637
|
+
export function approvalAsks(open) {
|
|
638
|
+
const asks = new Map();
|
|
639
|
+
for (const r of open ?? []) {
|
|
640
|
+
const key = `${[...r.names].sort().join(',')}|${r.lane ?? r.actor}`;
|
|
641
|
+
const ask = asks.get(key) ?? { request: r, files: [] };
|
|
642
|
+
if (!ask.files.includes(r.what)) ask.files.push(r.what);
|
|
643
|
+
asks.set(key, ask);
|
|
644
|
+
}
|
|
645
|
+
return [...asks.values()];
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** What an approval decision covers, in a line: "contract for the platform lane: a.ts, b.ts (2 files)". */
|
|
649
|
+
export function askWords(request, files = [request?.what]) {
|
|
650
|
+
const who = request.lane ? `the ${request.lane} lane` : request.actor;
|
|
651
|
+
const list =
|
|
652
|
+
files.length > 4
|
|
653
|
+
? `${files.slice(0, 4).join(', ')} and ${files.length - 4} more`
|
|
654
|
+
: files.join(', ');
|
|
655
|
+
return `${request.names.map(approvalLabel).join(', ')} for ${who}: ${list}${files.length > 1 ? ` (${files.length} files)` : ''}`;
|
|
656
|
+
}
|
|
657
|
+
|
|
633
658
|
export function approvalLabel(name) {
|
|
634
659
|
return name.startsWith('dep-') ? name.slice(4).replaceAll('+', '/') : name;
|
|
635
660
|
}
|
|
@@ -745,12 +770,14 @@ export function bandLines({
|
|
|
745
770
|
actions: [{ id: 'go', label: 'Go' }],
|
|
746
771
|
});
|
|
747
772
|
// A held call's request is asked on its own line above, not again here.
|
|
748
|
-
|
|
749
|
-
|
|
773
|
+
// CARD-2: counted as decisions, not files: one approval for a lane covers every file it was asked for.
|
|
774
|
+
const asks = approvalAsks((requests?.open ?? []).filter((r) => !held.some((h) => sameAsk(h, r))));
|
|
775
|
+
if (asks.length)
|
|
750
776
|
lines.push({
|
|
751
777
|
kind: 'approvals',
|
|
752
|
-
text: `${plural(
|
|
753
|
-
request:
|
|
778
|
+
text: `${plural(asks.length, 'approval', 'approvals')} waiting`,
|
|
779
|
+
request: asks[0].request,
|
|
780
|
+
files: asks[0].files,
|
|
754
781
|
actions: [{ id: 'approve', label: 'Approve…' }],
|
|
755
782
|
});
|
|
756
783
|
// DECIDE-1: reviewing a lane's work is the lead's. Only with the loop off, where the person runs the
|
|
@@ -856,10 +883,22 @@ export function band(lines, { Box, Text, Button }, onAction) {
|
|
|
856
883
|
};
|
|
857
884
|
const LIVE = { quiet: 'yellow', stalled: 'red' };
|
|
858
885
|
const LABEL = 8;
|
|
886
|
+
// Two lines can share a kind or an action (two drift findings, each with Team): the second and later
|
|
887
|
+
// get a number, so every row and button keeps its own key.
|
|
888
|
+
const counter = () => {
|
|
889
|
+
const seen = new Map();
|
|
890
|
+
return (key) => {
|
|
891
|
+
const n = (seen.get(key) ?? 0) + 1;
|
|
892
|
+
seen.set(key, n);
|
|
893
|
+
return n === 1 ? key : `${key}-${n}`;
|
|
894
|
+
};
|
|
895
|
+
};
|
|
896
|
+
const uniqueButton = counter();
|
|
897
|
+
const uniqueRow = counter();
|
|
859
898
|
const buttons = (line) =>
|
|
860
899
|
line.actions.map((a) =>
|
|
861
900
|
Button({
|
|
862
|
-
key: `band-${a.id}
|
|
901
|
+
key: uniqueButton(`band-${a.id}`),
|
|
863
902
|
label: a.hotkey ? `${a.hotkey} ${a.label}` : a.label,
|
|
864
903
|
hotkey: a.hotkey,
|
|
865
904
|
onPress: () => onAction(a.id, line),
|
|
@@ -900,7 +939,7 @@ export function band(lines, { Box, Text, Button }, onAction) {
|
|
|
900
939
|
const label = line.mission ? line.label : note ? 'note' : saidYou ? '' : 'you';
|
|
901
940
|
if (!line.mission && !note) saidYou = true;
|
|
902
941
|
return Box({
|
|
903
|
-
key: `band-${line.kind}
|
|
942
|
+
key: uniqueRow(`band-${line.kind}`),
|
|
904
943
|
flexDirection: 'row',
|
|
905
944
|
columnGap: 1,
|
|
906
945
|
children: [
|
|
@@ -1117,12 +1156,13 @@ export function refusalView(card, expanded, { Box, Text, Button }, { onApprove,
|
|
|
1117
1156
|
export function approvalsText(requests) {
|
|
1118
1157
|
const open = requests?.open ?? [];
|
|
1119
1158
|
const inForce = requests?.inForce ?? [];
|
|
1159
|
+
const history = requests?.history ?? [];
|
|
1160
|
+
const asks = approvalAsks(open);
|
|
1120
1161
|
const lines = [
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
),
|
|
1162
|
+
asks.length
|
|
1163
|
+
? `${plural(asks.length, 'approval', 'approvals')} waiting:`
|
|
1164
|
+
: 'No requests waiting.',
|
|
1165
|
+
...asks.map((a) => ` ${askWords(a.request, a.files)}`),
|
|
1126
1166
|
inForce.length
|
|
1127
1167
|
? `${plural(inForce.length, 'approval', 'approvals')} in force:`
|
|
1128
1168
|
: 'No approvals in force.',
|
|
@@ -1130,6 +1170,14 @@ export function approvalsText(requests) {
|
|
|
1130
1170
|
(a) =>
|
|
1131
1171
|
` ${approvalLabel(a.name)} for ${a.lane ? `the ${a.lane} lane` : 'any agent'}, ${a.minutesLeft} min left: ${a.reason}`,
|
|
1132
1172
|
),
|
|
1173
|
+
// CARD-6: what was given, newest first, whether or not its branch has merged.
|
|
1174
|
+
history.length ? 'Approvals given, newest first:' : 'No approvals given yet.',
|
|
1175
|
+
...history
|
|
1176
|
+
.slice(0, 20)
|
|
1177
|
+
.map(
|
|
1178
|
+
(h) =>
|
|
1179
|
+
` ${new Date(h.at).toLocaleString('en-AU', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })} ${approvalLabel(h.name)} for ${h.lane ? `the ${h.lane} lane` : 'any agent'}, by ${h.by}${h.files.length ? ` (${h.files.length} file${h.files.length === 1 ? '' : 's'})` : ''}: ${h.reason}`,
|
|
1180
|
+
),
|
|
1133
1181
|
];
|
|
1134
1182
|
return lines.join('\n');
|
|
1135
1183
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warren-dean/code-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Keeps AI coding agents inside your project's rules: lanes, layer rules, protected paths and proof before finish, enforced by Claude Code hooks and in CI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Warren Langeveldt",
|
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -48,7 +48,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" team claim ST-<n> --lease "<glob>,
|
|
|
48
48
|
|
|
49
49
|
The lease is the paths the story will create or change: its lane's folders for this feature, and any contract it changes. A claim someone else holds is refused with who holds it; pick another story or ask them. Stories in a lane another person owns are theirs to dispatch. Making the story's branch also claims it, and each commit on it adds what it changed to the lease; merging it ends the claim.
|
|
50
50
|
|
|
51
|
-
Start each story with the Agent tool: `subagent_type` is the lane's agent, `isolation: "worktree"`, and run them in the background. Start every story in the batch in the same message so they run in parallel.
|
|
51
|
+
Start each story with the Agent tool: `subagent_type` is the lane's agent, `isolation: "worktree"`, and run them in the background, with a `description` that starts with the story's id ("ST-12 booking form"): the loop reads it to know the story is in flight, and won't ask for it again while the agent works. Start every story in the batch in the same message so they run in parallel.
|
|
52
52
|
|
|
53
53
|
The brief is the agent's whole context. Write it so the agent needs nothing else:
|
|
54
54
|
|
package/skills/init/SKILL.md
CHANGED
|
@@ -138,7 +138,8 @@ Once they approve (with any changes):
|
|
|
138
138
|
- tell the person to make the `code-kit` check required on the protected branches.
|
|
139
139
|
7. Run `check` and `unowned` again, without `--config`.
|
|
140
140
|
8. Commit the setup straight onto the base branch (usually `main`), not a branch: while the base has no `.claude/code-kit.json` committed, the hooks let the lead make that one commit, so the rules the person just approved are in force on `main` and the lead can merge reviewed work from the start, with no first merge for anyone to make. The kit records the protected files in `.claude/approval-log.jsonl`. If the base already has rules (an update, section 4), commit on a branch as usual.
|
|
141
|
-
9.
|
|
141
|
+
9. **A team, only if there is one.** Not every developer works in a team. Ask once: "Do other developers work in this repository with their own Claude Code sessions?" If yes, run `/code-kit:team-init` now (it asks who the tech lead is and who owns which lanes, and writes the `team` section). If no, or not yet, leave it out and say `/code-kit:team-init` sets it up any time.
|
|
142
|
+
10. Set up each companion the config now wants (`"adapters": { "<name>": true }`) by running its init skill, for example `/context-graph:init`, which asks nothing. Do it before reporting, so it's in place before the coding loop starts; `code-kit next` also gives `setup` until it is.
|
|
142
143
|
|
|
143
144
|
Report what was set up, what's enforced from now on, how many existing violations the baseline holds, and the open questions. Say who does what from here: the person owns the intent and the rules; the lead owns delivery, so it dispatches, reviews, sends back and merges, and approves within the delegated limits, and only a real decision (scope, something outside those limits, a problem it couldn't solve) comes to the person.
|
|
144
145
|
|
package/skills/review/SKILL.md
CHANGED
|
@@ -57,7 +57,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" security --base <base> --branch <b
|
|
|
57
57
|
|
|
58
58
|
It does when the diff touches sign-in, permissions, input from outside, personal data, encryption, payments, secrets, or queries and commands built from strings, adds dependencies, or the story cites a security requirement; it says which, and where. When it does:
|
|
59
59
|
|
|
60
|
-
- Run the `security-reviewer` agent (Agent tool, `subagent_type` `security-reviewer`) with the branch, the base, the story, its requirements, the spec files it touches (their `## Threat model` and `## Security requirements`) and the reasons `security` gave. It reads only, and ends with the same findings block as code-kit's reviewer: each finding graded blocker, concern or nit, with where, the attack and the fix.
|
|
60
|
+
- Run the `security-reviewer` agent (Agent tool, `subagent_type` `security-reviewer`, its `description` naming the branch, such as "Security review web/st-12", so the loop knows the story is under review) with the branch, the base, the story, its requirements, the spec files it touches (their `## Threat model` and `## Security requirements`) and the reasons `security` gave. It reads only, and ends with the same findings block as code-kit's reviewer: each finding graded blocker, concern or nit, with where, the attack and the fix.
|
|
61
61
|
- **A blocker fails the review**, like a failing test: the branch goes back to its lane with the finding (where, the attack, the fix). Concerns are yours to weigh: send back the ones the threat model says matter, and put the rest in the report. Nits go in the report.
|
|
62
62
|
- No `security-reviewer` agent in the project: `security` says so. Review the diff yourself against the same list in `${CLAUDE_PLUGIN_ROOT}/templates/security-reviewer.md`, and tell the person that re-running `/code-kit:init` adds the agent.
|
|
63
63
|
- A background reviewer can't start agents: it lists the security review as due, as a concern with the reasons, and the lead runs it.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: team-init
|
|
3
|
+
description: Set up a code-kit project for a team of developers, change its team setup, or make it one developer's again. Asks who the tech lead is, who owns which lanes, when a claim goes stale and where intents go, shows the team section before writing it, checks GitHub is ready (gh signed in, the shared repository, the intents' discussion category), and offers CODEOWNERS, the Project board and a hosted team provider. Use when the person says they work in a team, wants teammates to share the project, wants to change the tech lead or lane owners, wants to stop working as a team, or runs /code-kit:team-init. Not every developer works in a team: never set one up unasked.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Set up the team
|
|
7
|
+
|
|
8
|
+
A team is several people in one repository, each with their own lead and agents (spec 16). code-kit then claims a story before anyone builds it, leases the files it changes, has intents raised before shared things change, and shows it all in `/team`. One developer needs none of this, and the project works the same without it.
|
|
9
|
+
|
|
10
|
+
The CLI is `node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs"`, run from the project root. Its `team init` shows the team section it would write; with `--write` it writes it, once the person agrees. Only the lead writes it: the hooks refuse it from a lane agent.
|
|
11
|
+
|
|
12
|
+
## 1. Where things stand
|
|
13
|
+
|
|
14
|
+
1. `.claude/code-kit.json` must exist and be valid (`check`). If it doesn't, stop: the person runs `/code-kit:init` first.
|
|
15
|
+
2. Run `check --json` and read `team` (null when the project isn't a team's), `me` (who's signed in to gh here) and `lanes`.
|
|
16
|
+
3. The shared repository: `git remote get-url origin` must be on GitHub, and `gh auth status` signed in. Without either, say what's missing; code-kit's own team provider works through both.
|
|
17
|
+
|
|
18
|
+
Say which applies: **new team**, **change the team**, or **back to one developer** (the person asked to stop working as a team).
|
|
19
|
+
|
|
20
|
+
## 2. Ask, one question at a time
|
|
21
|
+
|
|
22
|
+
Ask only what the config can't answer, with the current value as the default on a change.
|
|
23
|
+
|
|
24
|
+
- **Is this a team?** On a new setup, unless the person already said so: "Do other developers work in this repository with their own Claude Code sessions?" No → stop, and say nothing changed.
|
|
25
|
+
- **Tech lead:** whose GitHub handle owns the plan, specs, contracts and principles, agrees or drops intents, and may release or reassign anyone's claim. Default: `me`.
|
|
26
|
+
- **Lane owners:** for each lane, whose sessions build its stories (GitHub handles), or nobody, in which case whoever claims a story first builds it. `git log --format='%an' -- <lane paths> | sort | uniq -c` and CODEOWNERS suggest people, but ask for handles: names in commits aren't handles.
|
|
27
|
+
- **Stale after:** how many working days without a push before a claim is stale (default 3).
|
|
28
|
+
- **Intents category:** the GitHub Discussions category intents go in (default `Intents`).
|
|
29
|
+
- **Back to one developer:** confirm that claims, owners and intents stop applying, and that teammates' sessions lose them too once it's merged.
|
|
30
|
+
|
|
31
|
+
## 3. Show, then write
|
|
32
|
+
|
|
33
|
+
Run it without `--write` and show the person exactly what it prints:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" team init --tech-lead <handle> --owner <lane>=<handle>,<handle> [--stale-days <n>] [--intents <category>]
|
|
37
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" team init --off
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Give `--owner <lane>=` (nothing after `=`) to take a lane's owners out. Once they agree, run the same command with `--write`. It prints what GitHub still needs; tell the person each, in their words:
|
|
41
|
+
|
|
42
|
+
- **gh isn't signed in:** they run `! gh auth login`.
|
|
43
|
+
- **Discussions are off, or there's no intents category:** GitHub doesn't let a tool create a category. They turn Discussions on (Settings › Features) and add an "Open-ended discussion" category with the intents' name (Settings › Discussions), once.
|
|
44
|
+
|
|
45
|
+
## 4. Also offer
|
|
46
|
+
|
|
47
|
+
Each is optional; ask, and do only what they want:
|
|
48
|
+
|
|
49
|
+
- **CODEOWNERS** (TEAM-11): `team codeowners --write` writes `.github/CODEOWNERS` from the config, so GitHub asks the tech lead and lane owners to review. Then they require code owners' review in a ruleset on the protected branch.
|
|
50
|
+
- **A Project board** (TEAM-12): with a GitHub Project's owner and number, add `"board": { "owner": "<user or organisation>", "number": <n> }` to `team` in the config; the mod then mirrors the plan onto it.
|
|
51
|
+
- **A hosted team provider:** if a plugin for one is installed (its connect skill is in your skills list, such as `/code-kit-teams:connect`), offer it: it makes claims, leases and intents live, shows who's at work where, and checks each push for drift. Its connect skill sets `team.provider`, with GitHub as the fallback. If none is installed, don't mention one.
|
|
52
|
+
|
|
53
|
+
## 5. Finish
|
|
54
|
+
|
|
55
|
+
1. Run `check` and `team` to confirm.
|
|
56
|
+
2. Commit `.claude/code-kit.json` (and CODEOWNERS, if written) on a branch, open a pull request, and merge it as any change to the kit's rules.
|
|
57
|
+
3. Tell the person what each teammate does once: install code-kit, `gh auth login`, pull the change. Their sessions then share the claims, and `/team` shows the team.
|
|
58
|
+
|
|
59
|
+
Report in three lines: the tech lead and owners as written (or that the project is one developer's again), what GitHub still needs, and what each teammate does.
|