@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.
|
|
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.
|
|
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.
|
package/hooks/guard-bash.mjs
CHANGED
|
@@ -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|\
|
|
372
|
-
|
|
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,
|
package/hooks/mod/register.mjs
CHANGED
|
@@ -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.
|
|
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
|
|