@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.0",
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.0",
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
- const live = readJson(join(stateDir(root), 'hub-stream.json'), {});
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 this session's
193
- * stream is live, and who this machine is signed in as.
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 live = readJson(join(stateDir(root), 'hub-stream.json'), {});
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: Boolean(live.connected),
237
+ live: link.up,
202
238
  account: credentials()?.login ?? null,
203
- detail: live.connected
239
+ detail: link.up
204
240
  ? 'claims, leases, intents and presence as they happen; every push checked for drift'
205
- : (live.why ?? 'not connected to its live updates; read on each refresh'),
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 = join(dir, 'hub-stream.json');
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
- writeJson(streamFile, { lastId, connected: false });
502
- try {
503
- unlinkSync(join(dir, `hub-daemon-${session}.json`));
504
- } catch {}
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];
@@ -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.0",
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 and adds its MCP server to `.mcp.json`. Commit `.mcp.json` with the config.
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