@warren-dean/code-kit 0.15.0 → 0.15.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.15.0",
12
+ "version": "0.15.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.15.0",
3
+ "version": "0.15.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,13 @@
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.15.1 (2026-10-16)
6
+
7
+ Lanes stop less often to wait for you.
8
+
9
+ - A lane reading `.claude/code-kit.json` with an inline script, such as `python3 -c "json.load(open(...))"`, no longer waits for your approval. The bash guard used to count any Python command that named a kit file as a change to the kit, so each lane that read its spec folders this way filed a kit request and stopped: 11 of one project's 16 approval requests. An inline Python or Node script is now judged by its code, and only one that writes the kit, or whose output is redirected into it, is refused. A Python script run from a file still counts as a change, and the check after each shell call still catches any real write. The spec-check skill tells lanes to open the config with the Read tool.
10
+ - A call Claude Code refused that later goes through, the same tool from the same agent (for a shell command, the same command), leaves the band. Before, every refusal stayed in the band until you pressed Done it or Not now, even after a retry had worked, so the band could show calls refused that no longer needed you.
11
+
5
12
  ## 0.15.0 (2026-10-16)
6
13
 
7
14
  The lane agents can run on a cheaper model than the lead.
@@ -368,8 +368,21 @@ if (team) {
368
368
  const KIT_PATH = /\.claude\/(hooks|agents|skills|settings[^/]*\.json|README\.md|code-kit\.json)/;
369
369
  // a real redirect into a file; `2>&1` and `>/dev/null` are not writes
370
370
  const WRITES =
371
- /((?<![0-9&])>>?\s*(?!&|\/dev\/null)\S|\btee\b|\bsed\s+-i|\bperl\s+-i|\bmv\b|\bcp\b|\brm\b|\bpython3?\b|\bnode\s+-e|\btruncate\b|\bchmod\b|\bln\b)/;
372
- const kitWrite = commandsOf(cmd).some((c) => KIT_PATH.test(c.text) && WRITES.test(c.text));
371
+ /((?<![0-9&])>>?\s*(?!&|\/dev\/null)\S|\btee\b|\bsed\s+-i|\bperl\s+-i|\bmv\b|\bcp\b|\brm\b|\btruncate\b|\bchmod\b|\bln\b)/;
372
+ // An inline script (`python3 -c`, `node -e`) is judged by its code: lanes read the config this way,
373
+ // and a read isn't a change to the kit. A Python script run from a file can't be read here, so it counts.
374
+ const SCRIPT = /\bpython3?\b/;
375
+ const INLINE =
376
+ /(?:^|\s)(?:python3?\s+(?:-\w+\s+)*-c|node\s+(?:-\w+\s+)*(?:-e|--eval|-p|--print))\s/;
377
+ const SCRIPT_WRITES =
378
+ /\bopen(?:Sync)?\s*\([^)]*['"][rwax]*[wax+][rwax+b]*['"]|\.write_(?:text|bytes)\b|\.(?:unlink|rename|touch|rmdir|chmod|symlink_to)\s*\(|\bos\.(?:remove|unlink|rename|replace|rmdir|chmod|symlink|system|popen)\b|\bshutil\.|\bsubprocess\.|\b(?:writeFile|appendFile|rm|rmdir|unlink|rename|copyFile|cp|chmod|truncate|symlink|createWriteStream)(?:Sync)?\s*\(|\bexecSync\b|\bspawnSync\b|\bchild_process\b/;
379
+ const writesKit = ({ text, code }) => {
380
+ if (!KIT_PATH.test(text)) return false;
381
+ // the script's own text is quoted, so a `>` in it isn't a redirect; one outside it still is
382
+ if (INLINE.test(text)) return WRITES.test(code) || SCRIPT_WRITES.test(text);
383
+ return WRITES.test(text) || SCRIPT.test(text);
384
+ };
385
+ const kitWrite = commandsOf(cmd).some(writesKit);
373
386
  if (kitWrite && actor.kind !== 'lead' && !approval(root, 'kit', actor)) {
374
387
  recordRequest(
375
388
  root,
@@ -1684,6 +1684,7 @@ export function register(on) {
1684
1684
  const lane = e.agent_id ? activity.get(e.agent_id)?.lane : null;
1685
1685
  denied.set(e.tool_use_id ?? `${e.tool_name}-${denied.size}`, {
1686
1686
  id: e.tool_use_id ?? `${e.tool_name}-${denied.size}`,
1687
+ agentId: e.agent_id ?? null,
1687
1688
  who: lane ? `the ${lane} lane` : (e.agent_type ?? (e.agent_id ? 'an agent' : 'the lead')),
1688
1689
  tool: e.tool_name,
1689
1690
  command,
@@ -1733,6 +1734,19 @@ export function register(on) {
1733
1734
  try {
1734
1735
  const res = await next(e);
1735
1736
  const text = res?.deny ?? (res?.isError ? (res.text ?? String(res.result ?? '')) : null);
1737
+ // PERM-1: a refused call that later goes through (retried, or allowed) no longer waits for the person.
1738
+ if (!text && denied.size) {
1739
+ const command = e.tool === 'Bash' ? (e.command ?? null) : null;
1740
+ const before = denied.size;
1741
+ for (const [id, d] of denied)
1742
+ if (
1743
+ d.tool === e.tool &&
1744
+ d.agentId === (e.agentId ?? null) &&
1745
+ (!d.command || d.command === command)
1746
+ )
1747
+ denied.delete(id);
1748
+ if (denied.size < before) $.ui.invalidate('ui.render');
1749
+ }
1736
1750
  if (text && refusalCard(text)) {
1737
1751
  refusals.set(e.tool_use_id, text);
1738
1752
  if (refusals.size > REFUSALS_KEPT) refusals.delete(refusals.keys().next().value);
@@ -1707,6 +1707,38 @@ test("PERM-1 a call Claude Code's own permission check refused waits for the per
1707
1707
  await after.unmount();
1708
1708
  });
1709
1709
 
1710
+ test('PERM-1 a refused call that later goes through no longer waits for the person', async ($, on) => {
1711
+ const w = project();
1712
+ await start($, on, w);
1713
+ const refused = (id: string, command: string) =>
1714
+ $.classic.PermissionDenied({
1715
+ tool_name: 'Bash',
1716
+ tool_input: { command },
1717
+ tool_use_id: id,
1718
+ reason: 'Permission for this action was denied by the Claude Code auto mode classifier.',
1719
+ });
1720
+ await refused('tu-1', 'pnpm db:generate');
1721
+ await refused('tu-2', 'pnpm db:migrate');
1722
+ // another command going through leaves both waiting
1723
+ await $.tool.call({ tool: 'Bash', tool_use_id: 'tu-3', command: 'pnpm test' } as any);
1724
+ let band = await bandUi($);
1725
+ expect(await band.find({ type: 'Text', text: /^2 calls refused by Claude Code/ })).toBeDefined();
1726
+ await band.unmount();
1727
+ // the lead retries one and it goes through: only the other still waits
1728
+ await $.tool.call({ tool: 'Bash', tool_use_id: 'tu-4', command: 'pnpm db:generate' } as any);
1729
+ band = await bandUi($);
1730
+ expect(
1731
+ await band.find({ type: 'Text', text: "Claude Code refused the lead's `pnpm db:migrate`" }),
1732
+ ).toBeDefined();
1733
+ await band.unmount();
1734
+ await $.tool.call({ tool: 'Bash', tool_use_id: 'tu-5', command: 'pnpm db:migrate' } as any);
1735
+ band = await bandUi($);
1736
+ expect(
1737
+ await band.find({ type: 'Text', text: /Claude Code refused|refused by Claude Code/ }),
1738
+ ).toBeUndefined();
1739
+ await band.unmount();
1740
+ });
1741
+
1710
1742
  test('CARD-8 two approvals waiting are two decisions, each approved or dropped on its own', async ($, on) => {
1711
1743
  const w = project();
1712
1744
  w.requests = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warren-dean/code-kit",
3
- "version": "0.15.0",
3
+ "version": "0.15.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",
@@ -5,7 +5,7 @@ description: Check a task against the project's specs before implementing it, an
5
5
 
6
6
  # Spec check
7
7
 
8
- The project's specs are in the folder named by `docs.specs` in `.claude/code-kit.json`. The engineering principles are in `docs.principles`, and the plan in `docs.plan`.
8
+ The project's specs are in the folder named by `docs.specs` in `.claude/code-kit.json`. The engineering principles are in `docs.principles`, and the plan in `docs.plan`. Open the config with the Read tool. A shell script that reads it works too, but one that could write to it waits for a person's approval.
9
9
 
10
10
  ## Steps
11
11