codebyplan 1.13.30 → 1.13.32
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/dist/cli.js +36 -11
- package/package.json +1 -1
- package/templates/hooks/cbp-mcp-caller-worktree-inject.sh +79 -0
- package/templates/hooks/cbp-test-hooks.sh +81 -0
- package/templates/hooks/hooks.json +9 -0
- package/templates/skills/cbp-checkpoint-start/SKILL.md +2 -2
- package/templates/skills/cbp-task-complete/SKILL.md +1 -1
- package/templates/skills/cbp-task-start/SKILL.md +2 -2
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ var VERSION, PACKAGE_NAME;
|
|
|
14
14
|
var init_version = __esm({
|
|
15
15
|
"src/lib/version.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
VERSION = "1.13.
|
|
17
|
+
VERSION = "1.13.32";
|
|
18
18
|
PACKAGE_NAME = "codebyplan";
|
|
19
19
|
}
|
|
20
20
|
});
|
|
@@ -1493,6 +1493,23 @@ function mergeBaseSettingsIntoSettings(settings, base) {
|
|
|
1493
1493
|
if (base.permissions.skipDangerousModePermissionPrompt !== void 0 && existing.skipDangerousModePermissionPrompt === void 0) {
|
|
1494
1494
|
existing.skipDangerousModePermissionPrompt = base.permissions.skipDangerousModePermissionPrompt;
|
|
1495
1495
|
}
|
|
1496
|
+
const baseEntryTier = /* @__PURE__ */ new Map();
|
|
1497
|
+
for (const key of ["deny", "ask", "allow"]) {
|
|
1498
|
+
const baseArr = base.permissions[key];
|
|
1499
|
+
if (!baseArr) continue;
|
|
1500
|
+
for (const entry of baseArr) {
|
|
1501
|
+
baseEntryTier.set(entry, key);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
if (baseEntryTier.size > 0) {
|
|
1505
|
+
for (const key of ["deny", "ask", "allow"]) {
|
|
1506
|
+
const current = existing[key];
|
|
1507
|
+
if (!current) continue;
|
|
1508
|
+
existing[key] = current.filter(
|
|
1509
|
+
(entry) => !baseEntryTier.has(entry) || baseEntryTier.get(entry) === key
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1496
1513
|
for (const key of ["deny", "ask", "allow"]) {
|
|
1497
1514
|
const incoming = base.permissions[key];
|
|
1498
1515
|
if (!incoming || incoming.length === 0) continue;
|
|
@@ -1507,6 +1524,11 @@ function mergeBaseSettingsIntoSettings(settings, base) {
|
|
|
1507
1524
|
}
|
|
1508
1525
|
existing[key] = merged;
|
|
1509
1526
|
}
|
|
1527
|
+
for (const key of ["deny", "ask", "allow"]) {
|
|
1528
|
+
if (existing[key]?.length === 0) {
|
|
1529
|
+
delete existing[key];
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1510
1532
|
if (settings.permissions !== void 0 || Object.keys(existing).length > 0) {
|
|
1511
1533
|
settings.permissions = existing;
|
|
1512
1534
|
}
|
|
@@ -1550,17 +1572,20 @@ function stripBaseSettingsFromSettings(settings, base) {
|
|
|
1550
1572
|
if (base.permissions.skipDangerousModePermissionPrompt !== void 0 && perms.skipDangerousModePermissionPrompt === base.permissions.skipDangerousModePermissionPrompt) {
|
|
1551
1573
|
delete perms.skipDangerousModePermissionPrompt;
|
|
1552
1574
|
}
|
|
1575
|
+
const baseOwned = /* @__PURE__ */ new Set();
|
|
1553
1576
|
for (const key of ["deny", "ask", "allow"]) {
|
|
1554
|
-
const
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1577
|
+
for (const entry of base.permissions[key] ?? []) baseOwned.add(entry);
|
|
1578
|
+
}
|
|
1579
|
+
if (baseOwned.size > 0) {
|
|
1580
|
+
for (const key of ["deny", "ask", "allow"]) {
|
|
1581
|
+
const current = perms[key];
|
|
1582
|
+
if (!current) continue;
|
|
1583
|
+
const filtered = current.filter((x) => !baseOwned.has(x));
|
|
1584
|
+
if (filtered.length === 0) {
|
|
1585
|
+
delete perms[key];
|
|
1586
|
+
} else {
|
|
1587
|
+
perms[key] = filtered;
|
|
1588
|
+
}
|
|
1564
1589
|
}
|
|
1565
1590
|
}
|
|
1566
1591
|
if (Object.keys(perms).length === 0) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# @scope: org-shared
|
|
3
|
+
# @hook: PreToolUse mcp__codebyplan__(update_checkpoint|complete_checkpoint|update_task|complete_task|add_round|update_round|complete_round|create_standalone_task|update_standalone_task|complete_standalone_task|add_standalone_round|update_standalone_round|complete_standalone_round|update_standalone_file_change)
|
|
4
|
+
# Hook: PreToolUse for MCP write tools
|
|
5
|
+
#
|
|
6
|
+
# Purpose: Inject caller_worktree_id into MCP mutation tool inputs when the
|
|
7
|
+
# field is absent. Reads the worktree.local.json branch-keyed cache
|
|
8
|
+
# first (fast path); falls back to `codebyplan resolve-worktree --cache`.
|
|
9
|
+
#
|
|
10
|
+
# Fail-open: ALL exit paths exit 0. A hook failure must never block a tool call.
|
|
11
|
+
# Use explicit guards rather than set -euo pipefail (which would exit
|
|
12
|
+
# non-zero on the first failing command before the final exit 0).
|
|
13
|
+
|
|
14
|
+
# C0 — require jq; if absent, emit nothing and exit 0 (fail-open).
|
|
15
|
+
if ! command -v jq > /dev/null 2>&1; then
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Read stdin once into a variable.
|
|
20
|
+
INPUT=$(cat)
|
|
21
|
+
|
|
22
|
+
# C6 — if caller_worktree_id is already a non-empty string, do not overwrite.
|
|
23
|
+
# (jq '// empty' already maps JSON null to an empty string, so a plain -n test suffices.)
|
|
24
|
+
EXISTING=$(echo "$INPUT" | jq -r '.tool_input.caller_worktree_id // empty' 2>/dev/null)
|
|
25
|
+
if [ -n "$EXISTING" ]; then
|
|
26
|
+
# Already populated — plain allow (exit 0 with no output).
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# C5 — resolve worktree id, fast path first.
|
|
31
|
+
RESOLVED_WT=""
|
|
32
|
+
|
|
33
|
+
# Determine repo root: prefer $CLAUDE_PROJECT_DIR, fall back to PWD.
|
|
34
|
+
REPO_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
35
|
+
CACHE_FILE="$REPO_ROOT/.codebyplan/worktree.local.json"
|
|
36
|
+
|
|
37
|
+
if [ -f "$CACHE_FILE" ]; then
|
|
38
|
+
CACHED_WT=$(jq -r '.worktree_id // empty' "$CACHE_FILE" 2>/dev/null)
|
|
39
|
+
CACHED_BRANCH=$(jq -r '.branch // empty' "$CACHE_FILE" 2>/dev/null)
|
|
40
|
+
|
|
41
|
+
if [ -n "$CACHED_WT" ] && [ "$CACHED_WT" != "null" ] && \
|
|
42
|
+
[ -n "$CACHED_BRANCH" ] && [ "$CACHED_BRANCH" != "null" ]; then
|
|
43
|
+
# Validate branch matches current git branch.
|
|
44
|
+
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
45
|
+
if [ -n "$CURRENT_BRANCH" ] && [ "$CURRENT_BRANCH" = "$CACHED_BRANCH" ]; then
|
|
46
|
+
RESOLVED_WT="$CACHED_WT"
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Fallback to CLI resolution if cache miss or branch mismatch.
|
|
52
|
+
if [ -z "$RESOLVED_WT" ]; then
|
|
53
|
+
RESOLVED_WT=$(codebyplan resolve-worktree --cache 2>/dev/null \
|
|
54
|
+
|| npx --no-install codebyplan resolve-worktree --cache 2>/dev/null \
|
|
55
|
+
|| true)
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# UUID guard — accept only a canonical UUID (8-4-4-4-12 hex).
|
|
59
|
+
UUID_PATTERN='^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
|
60
|
+
if [ -z "$RESOLVED_WT" ] || ! echo "$RESOLVED_WT" | grep -qE "$UUID_PATTERN"; then
|
|
61
|
+
# Unresolved or invalid — plain allow, no updatedInput.
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# C3 — emit updatedInput as the FULL tool_input with caller_worktree_id added.
|
|
66
|
+
# Claude Code's PreToolUse updatedInput REPLACES tool_input wholesale (it is not a
|
|
67
|
+
# partial merge), so we must echo back every existing field merged with the new
|
|
68
|
+
# caller_worktree_id — otherwise the tool loses round_id/duration_minutes/etc.
|
|
69
|
+
echo "$INPUT" | jq \
|
|
70
|
+
--arg wt "$RESOLVED_WT" \
|
|
71
|
+
'{
|
|
72
|
+
hookSpecificOutput: {
|
|
73
|
+
hookEventName: "PreToolUse",
|
|
74
|
+
permissionDecision: "allow",
|
|
75
|
+
updatedInput: (.tool_input + { caller_worktree_id: $wt })
|
|
76
|
+
}
|
|
77
|
+
}'
|
|
78
|
+
|
|
79
|
+
exit 0
|
|
@@ -374,6 +374,87 @@ fi
|
|
|
374
374
|
|
|
375
375
|
echo ""
|
|
376
376
|
|
|
377
|
+
# ===== HOOK SMOKE TESTS — cbp-mcp-caller-worktree-inject =====
|
|
378
|
+
echo "## Hook Smoke Tests — cbp-mcp-caller-worktree-inject (CHK-198)"
|
|
379
|
+
|
|
380
|
+
INJECT_HOOK="$HOOKS_DIR/cbp-mcp-caller-worktree-inject.sh"
|
|
381
|
+
# Absolute path — the fail-open test runs the hook from a temp cwd (to isolate it
|
|
382
|
+
# from this repo's git context), where the relative "$HOOKS_DIR" no longer resolves.
|
|
383
|
+
INJECT_HOOK_ABS="$(cd "$HOOKS_DIR" 2>/dev/null && pwd)/cbp-mcp-caller-worktree-inject.sh"
|
|
384
|
+
|
|
385
|
+
if [ ! -f "$INJECT_HOOK" ]; then
|
|
386
|
+
test_result "cbp-mcp-caller-worktree-inject.sh present" "passed" "missing"
|
|
387
|
+
else
|
|
388
|
+
test_result "cbp-mcp-caller-worktree-inject.sh present" "passed" "passed"
|
|
389
|
+
|
|
390
|
+
FIRST_LINE=$(head -1 "$INJECT_HOOK")
|
|
391
|
+
if echo "$FIRST_LINE" | grep -q '^#!/'; then
|
|
392
|
+
test_result "cbp-mcp-caller-worktree-inject.sh has shebang" "passed" "passed"
|
|
393
|
+
else
|
|
394
|
+
test_result "cbp-mcp-caller-worktree-inject.sh has shebang" "passed" "missing"
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
if grep -q '@scope: org-shared' "$INJECT_HOOK"; then
|
|
398
|
+
test_result "cbp-mcp-caller-worktree-inject.sh has @scope: org-shared" "passed" "passed"
|
|
399
|
+
else
|
|
400
|
+
test_result "cbp-mcp-caller-worktree-inject.sh has @scope: org-shared" "passed" "missing"
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
# Fail-open: run from a non-repo temp dir with no worktree cache and no
|
|
404
|
+
# CLAUDE_PROJECT_DIR — neither the cache nor the CLI fallback can resolve a
|
|
405
|
+
# worktree, so the hook must exit 0 with empty stdout (no updatedInput).
|
|
406
|
+
ISO=$(mktemp -d)
|
|
407
|
+
OUTPUT=$( (cd "$ISO" && env -u CLAUDE_PROJECT_DIR bash "$INJECT_HOOK_ABS" <<< '{"tool_input":{"task_id":"x"}}') 2>/dev/null )
|
|
408
|
+
EXIT_CODE=$?
|
|
409
|
+
if [ "$EXIT_CODE" = "0" ] && [ -z "$OUTPUT" ]; then
|
|
410
|
+
test_result "cbp-mcp-caller-worktree-inject.sh fail-open (unresolvable) exits 0 + empty stdout" "passed" "passed"
|
|
411
|
+
else
|
|
412
|
+
test_result "cbp-mcp-caller-worktree-inject.sh fail-open (unresolvable) exits 0 + empty stdout" "passed" "failed (exit=$EXIT_CODE)"
|
|
413
|
+
fi
|
|
414
|
+
rm -rf "$ISO"
|
|
415
|
+
|
|
416
|
+
# C6 — input already carries a non-empty caller_worktree_id → never overwrite;
|
|
417
|
+
# early-return with exit 0 and empty stdout (no resolution attempted).
|
|
418
|
+
OUTPUT=$(echo '{"tool_input":{"caller_worktree_id":"11111111-1111-1111-1111-111111111111"}}' | bash "$INJECT_HOOK" 2>/dev/null)
|
|
419
|
+
EXIT_CODE=$?
|
|
420
|
+
if [ "$EXIT_CODE" = "0" ] && [ -z "$OUTPUT" ]; then
|
|
421
|
+
test_result "cbp-mcp-caller-worktree-inject.sh C6 keeps existing caller_worktree_id (exit 0 + empty stdout)" "passed" "passed"
|
|
422
|
+
else
|
|
423
|
+
test_result "cbp-mcp-caller-worktree-inject.sh C6 keeps existing caller_worktree_id (exit 0 + empty stdout)" "passed" "failed (exit=$EXIT_CODE)"
|
|
424
|
+
fi
|
|
425
|
+
|
|
426
|
+
# Injection — a worktree.local.json whose .branch matches the current git branch
|
|
427
|
+
# makes the cache fast-path resolve. Use a synthetic UUID so the assertion proves
|
|
428
|
+
# the cache value (not the live CLI) was injected. Skipped when no concrete git
|
|
429
|
+
# branch resolves (detached HEAD / non-git checkout) or jq is unavailable.
|
|
430
|
+
CUR_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
431
|
+
if [ -n "$CUR_BRANCH" ] && [ "$CUR_BRANCH" != "HEAD" ] && command -v jq >/dev/null 2>&1; then
|
|
432
|
+
ISO=$(mktemp -d)
|
|
433
|
+
mkdir -p "$ISO/.codebyplan"
|
|
434
|
+
FAKE_WT="abcdef01-2345-6789-abcd-ef0123456789"
|
|
435
|
+
jq -n --arg b "$CUR_BRANCH" --arg w "$FAKE_WT" \
|
|
436
|
+
'{worktree_id:$w, branch:$b}' > "$ISO/.codebyplan/worktree.local.json"
|
|
437
|
+
OUTPUT=$(CLAUDE_PROJECT_DIR="$ISO" bash "$INJECT_HOOK" <<< '{"tool_input":{"task_id":"x"}}' 2>/dev/null)
|
|
438
|
+
EXIT_CODE=$?
|
|
439
|
+
INJECTED=$(echo "$OUTPUT" | jq -r '.hookSpecificOutput.updatedInput.caller_worktree_id // empty' 2>/dev/null)
|
|
440
|
+
# Sibling-key survival — CC's updatedInput REPLACES tool_input wholesale (it is
|
|
441
|
+
# not a partial merge), so the hook must echo back every original field merged
|
|
442
|
+
# with caller_worktree_id. Assert the non-target sibling key (task_id) survives;
|
|
443
|
+
# this is the assertion gap that let the replace-vs-merge bug ship in round 2.
|
|
444
|
+
PRESERVED=$(echo "$OUTPUT" | jq -r '.hookSpecificOutput.updatedInput.task_id // empty' 2>/dev/null)
|
|
445
|
+
if [ "$EXIT_CODE" = "0" ] && [ "$INJECTED" = "$FAKE_WT" ] && [ "$PRESERVED" = "x" ]; then
|
|
446
|
+
test_result "cbp-mcp-caller-worktree-inject.sh injects caller_worktree_id AND preserves sibling keys" "passed" "passed"
|
|
447
|
+
else
|
|
448
|
+
test_result "cbp-mcp-caller-worktree-inject.sh injects caller_worktree_id AND preserves sibling keys" "passed" "failed (exit=$EXIT_CODE injected=$INJECTED preserved=$PRESERVED)"
|
|
449
|
+
fi
|
|
450
|
+
rm -rf "$ISO"
|
|
451
|
+
else
|
|
452
|
+
test_result "cbp-mcp-caller-worktree-inject.sh injection test (no branch resolvable — skipped)" "passed" "passed"
|
|
453
|
+
fi
|
|
454
|
+
fi
|
|
455
|
+
|
|
456
|
+
echo ""
|
|
457
|
+
|
|
377
458
|
# ===== SUMMARY =====
|
|
378
459
|
echo "=== TEST SUMMARY ==="
|
|
379
460
|
echo -e "Passed: ${GREEN}$PASSED${NC}"
|
|
@@ -45,6 +45,15 @@
|
|
|
45
45
|
"command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/cbp-mcp-migration-guard.sh"
|
|
46
46
|
}
|
|
47
47
|
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"matcher": "mcp__codebyplan__(update_checkpoint|complete_checkpoint|update_task|complete_task|add_round|update_round|complete_round|create_standalone_task|update_standalone_task|complete_standalone_task|add_standalone_round|update_standalone_round|complete_standalone_round|update_standalone_file_change)",
|
|
51
|
+
"hooks": [
|
|
52
|
+
{
|
|
53
|
+
"type": "command",
|
|
54
|
+
"command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/cbp-mcp-caller-worktree-inject.sh"
|
|
55
|
+
}
|
|
56
|
+
]
|
|
48
57
|
}
|
|
49
58
|
],
|
|
50
59
|
"PostToolUse": [
|
|
@@ -57,7 +57,7 @@ This mirrors the CHK-104 hard-lock model — never wrest a checkpoint from a liv
|
|
|
57
57
|
|
|
58
58
|
If the checkpoint is already `active` AND `worktree_id` already equals `CALLER_WT` (the Step 3 no-op row), skip this step entirely and proceed to Step 5 — nothing to write.
|
|
59
59
|
|
|
60
|
-
Otherwise set the checkpoint `active` via MCP `update_checkpoint(checkpoint_id, status: "active"`, plus `worktree_id: CALLER_WT` when claiming per Step 3.
|
|
60
|
+
Otherwise set the checkpoint `active` via MCP `update_checkpoint(checkpoint_id, status: "active"`, plus `worktree_id: CALLER_WT` when claiming per Step 3. `caller_worktree_id` (CHK-140 TASK-7) identifies the calling worktree and is auto-injected by the `cbp-mcp-caller-worktree-inject.sh` PreToolUse hook (CHK-198 TASK-2); the server falls back to the repo `main` worktree only when it is absent. If the checkpoint was already `active` but a claim is still needed, skip the status write and only write `worktree_id`.
|
|
61
61
|
|
|
62
62
|
### Step 5: Route
|
|
63
63
|
|
|
@@ -78,7 +78,7 @@ Show a one-line confirmation before routing:
|
|
|
78
78
|
## Integration
|
|
79
79
|
|
|
80
80
|
- **Reads**: MCP `get_checkpoints`, `get_tasks`; `npx codebyplan resolve-worktree`
|
|
81
|
-
- **Writes**: MCP `update_checkpoint` (status + worktree_id;
|
|
81
|
+
- **Writes**: MCP `update_checkpoint` (status + worktree_id; `caller_worktree_id` auto-injected by the cbp-mcp-caller-worktree-inject.sh hook, CHK-198 TASK-2; server falls back to repo `main` only when absent)
|
|
82
82
|
- **Triggered by**: `/cbp-checkpoint-plan` (auto when claimed at create), `/cbp-todo` (planned-but-pending gate), or user directly
|
|
83
83
|
- **Triggers**: `/cbp-task-start` (auto when claimed), or `/cbp-checkpoint-plan` (when the checkpoint is unplanned)
|
|
84
84
|
- **Never**: plans or creates tasks — that is `/cbp-checkpoint-plan`
|
|
@@ -136,7 +136,7 @@ Skip the push only when nothing was committed in Step 5 AND `/cbp-merge-main` re
|
|
|
136
136
|
|
|
137
137
|
### Step 7: Complete Task
|
|
138
138
|
|
|
139
|
-
Call `complete_task(task_id)`.
|
|
139
|
+
Call `complete_task(task_id)`. `caller_worktree_id` (CHK-140 TASK-7) identifies the calling worktree and is auto-injected by the `cbp-mcp-caller-worktree-inject.sh` PreToolUse hook (CHK-198 TASK-2); the server falls back to the repo `main` worktree only when it is absent, then enforces the mutate-lock. The server auto-clears `assigned_user_id` + `assigned_worktree_id` on the task; if this was the last sibling task, it also clears the parent checkpoint's assignment. (Per CHK-104 hard-lock.)
|
|
140
140
|
|
|
141
141
|
### Step 8: Run Cleanup + Migration (inline)
|
|
142
142
|
|
|
@@ -171,7 +171,7 @@ See `dependency-vulnerability-fixes.md` "Audit Sweep at Task Start" for the sour
|
|
|
171
171
|
|
|
172
172
|
### Step 3.5: Worktree-Match Verification
|
|
173
173
|
|
|
174
|
-
Before activating the task, verify the caller's worktree matches the assigned worktree on the target row. (Per CHK-104 — DB-level hard-lock prevents cross-worktree mutations
|
|
174
|
+
Before activating the task, verify the caller's worktree matches the assigned worktree on the target row. (Per CHK-104 — DB-level hard-lock prevents cross-worktree mutations.)
|
|
175
175
|
|
|
176
176
|
1. Read caller worktree: `CALLER_WT=$(npx codebyplan resolve-worktree 2>/dev/null)`.
|
|
177
177
|
2. Determine target worktree:
|
|
@@ -232,7 +232,7 @@ Display context summary:
|
|
|
232
232
|
|
|
233
233
|
Use MCP `update_task(task_id, status: "in_progress")`.
|
|
234
234
|
|
|
235
|
-
If worktree_id present, include `claim_worktree_id` to auto-claim the checkpoint.
|
|
235
|
+
If worktree_id present, include `claim_worktree_id` to auto-claim the checkpoint. `caller_worktree_id` (CHK-140 TASK-7) identifies the calling worktree and is auto-injected by the `cbp-mcp-caller-worktree-inject.sh` PreToolUse hook (CHK-198 TASK-2); the server falls back to the repo `main` worktree only when it is absent.
|
|
236
236
|
|
|
237
237
|
### Step 6: Auto-trigger Round Start
|
|
238
238
|
|