@warren-dean/code-kit 0.22.0 → 0.22.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.
|
@@ -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.22.
|
|
12
|
+
"version": "0.22.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.22.
|
|
3
|
+
"version": "0.22.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,14 @@
|
|
|
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.22.1 (2026-10-23)
|
|
6
|
+
|
|
7
|
+
For projects on the hosted team provider, which is in preview:
|
|
8
|
+
|
|
9
|
+
- A session shows as live only when its own link to the provider is up. Before, one session's link made every session on the checkout look live.
|
|
10
|
+
- `code-kit team provider status` checks each part of the setup: this machine's sign-in, the config in the checkout and on the base branch, the MCP server, whether the provider sees the repository, and whether this session is linked. Each part that fails says what to do, and the last line says whether the repository is connected.
|
|
11
|
+
- `/code-kit:team-init` links the session it runs in once it connects, so there's no restart.
|
|
12
|
+
|
|
5
13
|
## 0.22.0 (2026-10-23)
|
|
6
14
|
|
|
7
15
|
A team's roles are settled by default: the lead decides what's technical, a lane runs one story at a time, and several sessions can work one project without their decisions crossing.
|
package/bin/code-kit.mjs
CHANGED
|
@@ -191,6 +191,8 @@ const via = option('--via');
|
|
|
191
191
|
const sessionId = option('--session');
|
|
192
192
|
// SESS-1: the session calling, as Claude Code tells every command an agent runs, unless --session says.
|
|
193
193
|
const callerSession = sessionId ?? process.env.CLAUDE_CODE_SESSION_ID ?? null;
|
|
194
|
+
// A provider's command hears which session is calling, for that session's own live link.
|
|
195
|
+
if (callerSession) process.env.CODE_KIT_SESSION = callerSession;
|
|
194
196
|
const minutesGiven = option('--minutes');
|
|
195
197
|
const answerGiven = option('--answer');
|
|
196
198
|
const leaseGiven = option('--lease');
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
chmodSync,
|
|
18
18
|
existsSync,
|
|
19
19
|
mkdirSync,
|
|
20
|
+
readdirSync,
|
|
20
21
|
readFileSync,
|
|
21
22
|
renameSync,
|
|
22
23
|
statSync,
|
|
@@ -104,6 +105,40 @@ const mainRoot = (cwd) => {
|
|
|
104
105
|
const stateDir = (root) => join(mainRoot(root), '.claude', 'state');
|
|
105
106
|
const hubDown = (root) => readJson(join(stateDir(root), 'hub-down.json'), {});
|
|
106
107
|
|
|
108
|
+
/** The Claude Code session calling: code-kit passes it on, and Claude Code tells every command an agent runs. */
|
|
109
|
+
const sessionOf = () =>
|
|
110
|
+
String(process.env.CODE_KIT_SESSION ?? process.env.CLAUDE_CODE_SESSION_ID ?? '').replace(
|
|
111
|
+
/[^\w-]/g,
|
|
112
|
+
'',
|
|
113
|
+
) || null;
|
|
114
|
+
const streamFileOf = (root, session) => join(stateDir(root), `hub-stream-${session}.json`);
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A session's live link: up while its daemon runs and its event stream is connected. Each session holds
|
|
118
|
+
* its own, so one session's link never makes another look live.
|
|
119
|
+
*/
|
|
120
|
+
function linkOf(root, session) {
|
|
121
|
+
const daemon = readJson(join(stateDir(root), `hub-daemon-${session}.json`));
|
|
122
|
+
const stream = readJson(streamFileOf(root, session), {});
|
|
123
|
+
return {
|
|
124
|
+
up: Boolean(daemon?.pid && alive(daemon.pid) && stream.connected),
|
|
125
|
+
why: stream.why ?? null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The sessions on this checkout whose live link is up. */
|
|
130
|
+
function linkedSessions(root) {
|
|
131
|
+
let names = [];
|
|
132
|
+
try {
|
|
133
|
+
names = readdirSync(stateDir(root));
|
|
134
|
+
} catch {
|
|
135
|
+
// no state yet
|
|
136
|
+
}
|
|
137
|
+
return names
|
|
138
|
+
.map((n) => n.match(/^hub-daemon-([\w-]+)\.json$/)?.[1])
|
|
139
|
+
.filter((s) => s && linkOf(root, s).up);
|
|
140
|
+
}
|
|
141
|
+
|
|
107
142
|
// --- talking to the hub ------------------------------------------------------------------------
|
|
108
143
|
|
|
109
144
|
async function hub(method, path, body, { timeoutMs = 15_000 } = {}) {
|
|
@@ -153,8 +188,7 @@ export async function provide(op, input, root = process.cwd()) {
|
|
|
153
188
|
// The daemon keeps the state fresh from the stream: read it from disk unless asked for fresh.
|
|
154
189
|
if (op === 'state' && !input.fresh) {
|
|
155
190
|
const kept = readJson(join(stateDir(root), 'team.json'));
|
|
156
|
-
|
|
157
|
-
if (kept?.hub && live.connected && Date.now() - Date.parse(kept.at) < 5 * 60_000)
|
|
191
|
+
if (kept?.hub && linkedSessions(root).length && Date.now() - Date.parse(kept.at) < 5 * 60_000)
|
|
158
192
|
return named(kept, root);
|
|
159
193
|
}
|
|
160
194
|
const { team: _team, ...body } = input;
|
|
@@ -189,20 +223,23 @@ export async function provide(op, input, root = process.cwd()) {
|
|
|
189
223
|
}
|
|
190
224
|
|
|
191
225
|
/**
|
|
192
|
-
* The state names what coordinates the team, for code-kit's Team pane: the hub, whether
|
|
193
|
-
*
|
|
226
|
+
* The state names what coordinates the team, for code-kit's Team pane: the hub, whether the calling
|
|
227
|
+
* session's own link is live (any session's, when no session is calling), and who this machine is
|
|
228
|
+
* signed in as.
|
|
194
229
|
*/
|
|
195
230
|
function named(state, root) {
|
|
196
|
-
const
|
|
231
|
+
const session = sessionOf();
|
|
232
|
+
const link = session ? linkOf(root, session) : { up: linkedSessions(root).length > 0 };
|
|
197
233
|
return {
|
|
198
234
|
...state,
|
|
199
235
|
provider: {
|
|
200
236
|
name: 'code-kit hub',
|
|
201
|
-
live:
|
|
237
|
+
live: link.up,
|
|
202
238
|
account: credentials()?.login ?? null,
|
|
203
|
-
detail:
|
|
239
|
+
detail: link.up
|
|
204
240
|
? 'claims, leases, intents and presence as they happen; every push checked for drift'
|
|
205
|
-
: (
|
|
241
|
+
: (link.why ??
|
|
242
|
+
"this session has no live link, so the team's state is read on each refresh; a session links as it starts"),
|
|
206
243
|
},
|
|
207
244
|
};
|
|
208
245
|
}
|
|
@@ -449,6 +486,132 @@ async function connect(root) {
|
|
|
449
486
|
console.log(
|
|
450
487
|
`This repository now coordinates through the hub (${state.claims.length} claims, ${state.intents.length} open intents). Commit .claude/code-kit.json and .mcp.json so the team does too.`,
|
|
451
488
|
);
|
|
489
|
+
// The session that ran connect links now, rather than at its next start.
|
|
490
|
+
const session = sessionOf();
|
|
491
|
+
if (session) {
|
|
492
|
+
startDaemon(root, session, claudeOf(), url);
|
|
493
|
+
console.log(
|
|
494
|
+
'This session is linked to the hub now; other sessions link as they start, or run connect in them.',
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** The Claude Code process this command runs under, for the link to end with it; null when there's none. */
|
|
500
|
+
function claudeOf() {
|
|
501
|
+
let pid = process.ppid;
|
|
502
|
+
for (let i = 0; i < 8 && pid > 1; i += 1) {
|
|
503
|
+
const line = psOf(pid);
|
|
504
|
+
if (!line) return null;
|
|
505
|
+
if (/(^|\/)claude(\s|$)/.test(line.command)) return pid;
|
|
506
|
+
pid = line.ppid;
|
|
507
|
+
}
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
function psOf(pid) {
|
|
511
|
+
try {
|
|
512
|
+
const out = execFileSync('ps', ['-o', 'ppid=,command=', '-p', String(pid)], {
|
|
513
|
+
encoding: 'utf8',
|
|
514
|
+
}).trim();
|
|
515
|
+
const m = out.match(/^(\d+)\s+(.*)$/);
|
|
516
|
+
return m ? { ppid: Number(m[1]), command: m[2] } : null;
|
|
517
|
+
} catch {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* `team provider status`: whether this repository is connected to the hub, one check a line, each failing
|
|
524
|
+
* one saying what to do, then the answer. Connected means this machine is signed in, the config here and
|
|
525
|
+
* on the base branch name the hub, and the hub can see the repository with nothing left to set up; live
|
|
526
|
+
* means the calling session's own link is up. True when connected.
|
|
527
|
+
*/
|
|
528
|
+
async function status(root) {
|
|
529
|
+
const lines = [];
|
|
530
|
+
const check = (ok, text) => (lines.push(` ${ok ? '✓' : '✗'} ${text}`), ok);
|
|
531
|
+
const repo = repoOf(originOf(root));
|
|
532
|
+
const cred = credentials();
|
|
533
|
+
const url = (() => {
|
|
534
|
+
try {
|
|
535
|
+
return hubUrl();
|
|
536
|
+
} catch {
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
})();
|
|
540
|
+
const names = (config) => Boolean(url) && config?.team?.provider?.hub?.replace(/\/$/, '') === url;
|
|
541
|
+
const signed = check(
|
|
542
|
+
Boolean(cred?.token),
|
|
543
|
+
cred?.token
|
|
544
|
+
? `this machine is signed in as ${cred.login}`
|
|
545
|
+
: "this machine isn't signed in: run /code-kit:team-init (it signs in through GitHub)",
|
|
546
|
+
);
|
|
547
|
+
const here = check(
|
|
548
|
+
names(readJson(join(root, '.claude', 'code-kit.json'))),
|
|
549
|
+
names(readJson(join(root, '.claude', 'code-kit.json')))
|
|
550
|
+
? 'the config here names the hub'
|
|
551
|
+
: 'the config here doesn\'t name the hub: set "team": { "provider": { "hub": "' +
|
|
552
|
+
(url ?? '<the hub>') +
|
|
553
|
+
'" } } in .claude/code-kit.json',
|
|
554
|
+
);
|
|
555
|
+
const base =
|
|
556
|
+
git(root, 'symbolic-ref', '--short', 'refs/remotes/origin/HEAD') ??
|
|
557
|
+
(git(root, 'rev-parse', '--verify', '-q', 'origin/main') ? 'origin/main' : null);
|
|
558
|
+
const committed = (() => {
|
|
559
|
+
if (!base) return false;
|
|
560
|
+
try {
|
|
561
|
+
return names(JSON.parse(git(root, 'show', `${base}:.claude/code-kit.json`) ?? ''));
|
|
562
|
+
} catch {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
})();
|
|
566
|
+
check(
|
|
567
|
+
committed,
|
|
568
|
+
committed
|
|
569
|
+
? `${base}'s config names the hub, so the team's sessions use it`
|
|
570
|
+
: `${base ?? 'the base branch'}'s config doesn't name the hub yet (as last fetched): commit the team section and merge it, so the hub knows the tech lead and every teammate's session uses it`,
|
|
571
|
+
);
|
|
572
|
+
const mcp = readJson(join(root, '.mcp.json'), {})?.mcpServers?.['code-kit-hub'];
|
|
573
|
+
check(
|
|
574
|
+
Boolean(url) && mcp?.url === `${url}/mcp`,
|
|
575
|
+
mcp
|
|
576
|
+
? "the project's MCP servers include the hub's"
|
|
577
|
+
: "the project's MCP servers don't include the hub's: run /code-kit:team-init, and commit .mcp.json",
|
|
578
|
+
);
|
|
579
|
+
let seen = false;
|
|
580
|
+
if (signed && repo && url) {
|
|
581
|
+
const state = await provide('state', { fresh: true }, root);
|
|
582
|
+
const setup = Array.isArray(state.setup) ? state.setup : [];
|
|
583
|
+
seen = check(
|
|
584
|
+
!state.unreachable && state.ok !== false,
|
|
585
|
+
state.unreachable || state.ok === false
|
|
586
|
+
? `the hub can't use ${repo}: ${state.why}`
|
|
587
|
+
: `the hub sees ${repo}`,
|
|
588
|
+
);
|
|
589
|
+
// What the hub says is left to set up doesn't stop the connection: it's shown, not failed.
|
|
590
|
+
for (const line of seen ? setup : []) lines.push(` ! ${line}`);
|
|
591
|
+
} else if (!repo)
|
|
592
|
+
check(false, "this repository has no GitHub origin, so the hub can't be used here");
|
|
593
|
+
const session = sessionOf();
|
|
594
|
+
const linked = linkedSessions(root);
|
|
595
|
+
const live = session ? linkOf(root, session).up : linked.length > 0;
|
|
596
|
+
check(
|
|
597
|
+
live,
|
|
598
|
+
session
|
|
599
|
+
? live
|
|
600
|
+
? 'this session is linked: claims, intents and presence arrive as they happen'
|
|
601
|
+
: "this session isn't linked yet: run /code-kit:team-init, or start a new session, once the checks above pass"
|
|
602
|
+
: `${linked.length} session${linked.length === 1 ? '' : 's'} on this checkout linked`,
|
|
603
|
+
);
|
|
604
|
+
const connected = signed && here && committed && seen;
|
|
605
|
+
console.log(`code-kit hub${url ? ` (${url})` : ''}${repo ? `, for ${repo}` : ''}:`);
|
|
606
|
+
for (const line of lines) console.log(line);
|
|
607
|
+
console.log(
|
|
608
|
+
connected
|
|
609
|
+
? live || !session
|
|
610
|
+
? 'Connected.'
|
|
611
|
+
: "Connected, but this session isn't linked yet."
|
|
612
|
+
: 'Not connected yet: fix each ✗ above, in order.',
|
|
613
|
+
);
|
|
614
|
+
return connected;
|
|
452
615
|
}
|
|
453
616
|
|
|
454
617
|
// --- the daemon (HUB-5): the stream, the team's state on disk, presence --------------------------
|
|
@@ -457,7 +620,7 @@ async function daemon({ root, session, watch }) {
|
|
|
457
620
|
const dir = stateDir(root);
|
|
458
621
|
const repo = repoOf(originOf(root));
|
|
459
622
|
if (!repo || !credentials()?.token) return;
|
|
460
|
-
const streamFile =
|
|
623
|
+
const streamFile = streamFileOf(root, session);
|
|
461
624
|
const eventsFile = join(dir, 'team-events.jsonl');
|
|
462
625
|
writeJson(join(dir, `hub-daemon-${session}.json`), { pid: process.pid, started: Date.now() });
|
|
463
626
|
let lastId = readJson(streamFile, {}).lastId ?? 0;
|
|
@@ -498,10 +661,10 @@ async function daemon({ root, session, watch }) {
|
|
|
498
661
|
stopping = true;
|
|
499
662
|
clearInterval(tick);
|
|
500
663
|
await hub('POST', `/v1/repos/${repo}/leave`, { session }, { timeoutMs: 3000 }).catch(() => {});
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
664
|
+
for (const file of [streamFile, join(dir, `hub-daemon-${session}.json`)])
|
|
665
|
+
try {
|
|
666
|
+
unlinkSync(file);
|
|
667
|
+
} catch {}
|
|
505
668
|
process.exit(0);
|
|
506
669
|
};
|
|
507
670
|
process.on('SIGTERM', stop);
|
|
@@ -670,18 +833,7 @@ async function main(argv) {
|
|
|
670
833
|
console.log('Signed out of code-kit hub on this machine.');
|
|
671
834
|
return 0;
|
|
672
835
|
}
|
|
673
|
-
if (cmd === 'status')
|
|
674
|
-
const cred = credentials();
|
|
675
|
-
console.log(
|
|
676
|
-
cred?.token ? `Signed in to ${cred.url} as ${cred.login}.` : `Not signed in: run ${SIGN_IN}.`,
|
|
677
|
-
);
|
|
678
|
-
const repo = repoOf(originOf(process.cwd()));
|
|
679
|
-
if (repo && cred?.token) {
|
|
680
|
-
const live = readJson(join(stateDir(process.cwd()), 'hub-stream.json'), {});
|
|
681
|
-
console.log(`${repo}: ${live.connected ? 'live' : (live.why ?? 'not connected')}`);
|
|
682
|
-
}
|
|
683
|
-
return 0;
|
|
684
|
-
}
|
|
836
|
+
if (cmd === 'status') return status(process.cwd()).then((ok) => (ok ? 0 : 1));
|
|
685
837
|
if (cmd === 'connect') return connect(process.cwd()).then(() => 0);
|
|
686
838
|
if (cmd === 'drift-model') {
|
|
687
839
|
const choice = args[0];
|
package/hooks/mod/register.mjs
CHANGED
|
@@ -325,7 +325,7 @@ export function register(on) {
|
|
|
325
325
|
? ((await json('adapters', '--session', session)) ?? [])
|
|
326
326
|
: [];
|
|
327
327
|
// TEAM-7: the team's claims, intents and pull requests, where the project is a team's.
|
|
328
|
-
const team = valid && check.team ? await json('team') : null;
|
|
328
|
+
const team = valid && check.team ? await json('team', '--session', session) : null;
|
|
329
329
|
// ASK-2: the lead's questions waiting for the person; REPORT-3: bug reports waiting to be sent.
|
|
330
330
|
const questions = !valid
|
|
331
331
|
? []
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warren-dean/code-kit",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.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",
|
|
@@ -48,7 +48,7 @@ Each is optional; ask, and do only what they want:
|
|
|
48
48
|
|
|
49
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
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, only when the config already names one:** hosted providers are in preview, so never offer or suggest one. When `team.provider` is `{ "<name>": "<https address>" }` (set by hand), connect this machine: run `team provider status`; if it isn't signed in, run `team provider signin` in the background, give the person the code and the GitHub address it prints, and wait (they enter the code in their browser; never ask for a password or token); then `team provider connect`, which checks the provider can see the repository
|
|
51
|
+
- **A hosted team provider, only when the config already names one:** hosted providers are in preview, so never offer or suggest one. When `team.provider` is `{ "<name>": "<https address>" }` (set by hand), connect this machine: run `team provider status`; if it isn't signed in, run `team provider signin` in the background, give the person the code and the GitHub address it prints, and wait (they enter the code in their browser; never ask for a password or token); then `team provider connect`, which checks the provider can see the repository, adds its MCP server to `.mcp.json` and links this session (no restart). Commit `.mcp.json` with the config, on the base branch: the hub reads the team from there, and a session only links where the checked-out config names the hub. Then run `team provider status` and give the person its last line; for each ✗, say what it asks for.
|
|
52
52
|
|
|
53
53
|
## 5. Finish
|
|
54
54
|
|