@paths.design/caws-cli 11.1.6 → 11.1.7

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.
Files changed (93) hide show
  1. package/README.md +1 -1
  2. package/dist/init/hook-packs/manifest-claude-code.d.ts +1 -1
  3. package/dist/init/hook-packs/manifest-claude-code.d.ts.map +1 -1
  4. package/dist/init/hook-packs/manifest-claude-code.js +59 -6
  5. package/dist/init/hook-packs/manifest-claude-code.js.map +1 -1
  6. package/dist/init/hook-packs/types.js +1 -1
  7. package/dist/init/hook-packs/types.js.map +1 -1
  8. package/dist/shell/commands/agents.d.ts +49 -0
  9. package/dist/shell/commands/agents.d.ts.map +1 -0
  10. package/dist/shell/commands/agents.js +577 -0
  11. package/dist/shell/commands/agents.js.map +1 -0
  12. package/dist/shell/commands/claim.d.ts.map +1 -1
  13. package/dist/shell/commands/claim.js +3 -4
  14. package/dist/shell/commands/claim.js.map +1 -1
  15. package/dist/shell/commands/status.d.ts +12 -0
  16. package/dist/shell/commands/status.d.ts.map +1 -1
  17. package/dist/shell/commands/status.js +236 -21
  18. package/dist/shell/commands/status.js.map +1 -1
  19. package/dist/shell/commands/worktree.d.ts +9 -0
  20. package/dist/shell/commands/worktree.d.ts.map +1 -1
  21. package/dist/shell/commands/worktree.js +302 -0
  22. package/dist/shell/commands/worktree.js.map +1 -1
  23. package/dist/shell/index.d.ts +4 -2
  24. package/dist/shell/index.d.ts.map +1 -1
  25. package/dist/shell/index.js +12 -1
  26. package/dist/shell/index.js.map +1 -1
  27. package/dist/shell/register.d.ts.map +1 -1
  28. package/dist/shell/register.js +150 -0
  29. package/dist/shell/register.js.map +1 -1
  30. package/dist/shell/render/status.d.ts +7 -1
  31. package/dist/shell/render/status.d.ts.map +1 -1
  32. package/dist/shell/render/status.js +72 -0
  33. package/dist/shell/render/status.js.map +1 -1
  34. package/dist/store/agents-store.d.ts.map +1 -1
  35. package/dist/store/agents-store.js +9 -0
  36. package/dist/store/agents-store.js.map +1 -1
  37. package/dist/store/apply-patch.d.ts.map +1 -1
  38. package/dist/store/apply-patch.js +15 -0
  39. package/dist/store/apply-patch.js.map +1 -1
  40. package/dist/store/doctor-snapshot.d.ts.map +1 -1
  41. package/dist/store/doctor-snapshot.js +143 -3
  42. package/dist/store/doctor-snapshot.js.map +1 -1
  43. package/dist/store/git-sparse-checkout.d.ts +25 -0
  44. package/dist/store/git-sparse-checkout.d.ts.map +1 -0
  45. package/dist/store/git-sparse-checkout.js +101 -0
  46. package/dist/store/git-sparse-checkout.js.map +1 -0
  47. package/dist/store/index.d.ts +2 -0
  48. package/dist/store/index.d.ts.map +1 -1
  49. package/dist/store/index.js +10 -1
  50. package/dist/store/index.js.map +1 -1
  51. package/dist/store/leases-store.d.ts +89 -0
  52. package/dist/store/leases-store.d.ts.map +1 -0
  53. package/dist/store/leases-store.js +369 -0
  54. package/dist/store/leases-store.js.map +1 -0
  55. package/dist/store/lifecycle-transaction.d.ts.map +1 -1
  56. package/dist/store/lifecycle-transaction.js +34 -1
  57. package/dist/store/lifecycle-transaction.js.map +1 -1
  58. package/dist/store/rules.d.ts +21 -1
  59. package/dist/store/rules.d.ts.map +1 -1
  60. package/dist/store/rules.js +22 -0
  61. package/dist/store/rules.js.map +1 -1
  62. package/dist/store/types.d.ts +25 -1
  63. package/dist/store/types.d.ts.map +1 -1
  64. package/dist/store/worktrees-migration.d.ts +141 -0
  65. package/dist/store/worktrees-migration.d.ts.map +1 -0
  66. package/dist/store/worktrees-migration.js +356 -0
  67. package/dist/store/worktrees-migration.js.map +1 -0
  68. package/dist/store/worktrees-writer.d.ts.map +1 -1
  69. package/dist/store/worktrees-writer.js +37 -1
  70. package/dist/store/worktrees-writer.js.map +1 -1
  71. package/package.json +2 -2
  72. package/templates/hook-packs/claude-code/CLAUDE.md +5 -5
  73. package/templates/hook-packs/claude-code/agent-heartbeat.sh +131 -0
  74. package/templates/hook-packs/claude-code/agent-register.sh +62 -0
  75. package/templates/hook-packs/claude-code/agent-stop.sh +51 -0
  76. package/templates/hook-packs/claude-code/audit.sh +1 -1
  77. package/templates/hook-packs/claude-code/block-dangerous.sh +1 -1
  78. package/templates/hook-packs/claude-code/classify_command.py +1 -1
  79. package/templates/hook-packs/claude-code/dispatch/post_tool_use.sh +1 -1
  80. package/templates/hook-packs/claude-code/dispatch/pre_tool_use.sh +11 -2
  81. package/templates/hook-packs/claude-code/dispatch/session_start.sh +6 -2
  82. package/templates/hook-packs/claude-code/dispatch/stop.sh +7 -2
  83. package/templates/hook-packs/claude-code/guard-strikes.sh +1 -1
  84. package/templates/hook-packs/claude-code/lib/parse-input.sh +1 -1
  85. package/templates/hook-packs/claude-code/lib/run-handlers.sh +1 -1
  86. package/templates/hook-packs/claude-code/reset-danger-latch.sh +1 -1
  87. package/templates/hook-packs/claude-code/reset-strikes.sh +1 -1
  88. package/templates/hook-packs/claude-code/runtime-paths.sh +1 -1
  89. package/templates/hook-packs/claude-code/scope-guard.sh +1 -1
  90. package/templates/hook-packs/claude-code/session-caws-status.sh +7 -1
  91. package/templates/hook-packs/claude-code/session-log.sh +1 -1
  92. package/templates/hook-packs/claude-code/worktree-guard.sh +130 -4
  93. package/templates/hook-packs/claude-code/worktree-write-guard.sh +133 -18
@@ -0,0 +1,131 @@
1
+ #!/bin/bash
2
+ # CAWS-MANAGED-HOOK
3
+ # hook_pack: claude-code
4
+ # hook_pack_version: 5
5
+ # caws_min_major: 11
6
+ # lineage_refs: 19
7
+ # do_not_edit_directly: update via `caws init --agent-surface claude-code`
8
+ #
9
+ # PreToolUse handler — heartbeats the current session's lease and surfaces
10
+ # parallel-agent presence to the calling agent
11
+ # (MULTI-AGENT-ACTIVITY-REGISTRY-001).
12
+ #
13
+ # Sourcing: invoked by dispatch/pre_tool_use.sh (FIRST in the handler
14
+ # list) after parse-input.sh has populated HOOK_SESSION_ID. The dispatcher
15
+ # runs with --short-circuit-on-block; this handler must never block.
16
+ #
17
+ # Behavior:
18
+ # - Refuses on empty/unknown HOOK_SESSION_ID.
19
+ # - Invokes `caws agents heartbeat --session-id <id> --platform claude-code
20
+ # --throttle 30000 --reason pre_tool_use --json --include-active-summary`.
21
+ # - Parses CAWS-native JSON. When active_agent_count > 1, wraps the
22
+ # active_agents list into Claude Code's hookSpecificOutput.
23
+ # additionalContext envelope and emits it on stdout. When the count
24
+ # is 1 (self only), emits nothing — silent in the common case.
25
+ # - Throttled invocations still return an active_agents summary, so
26
+ # parallel-presence surfacing fires every tool call even when the
27
+ # write was skipped.
28
+ #
29
+ # IO BOUNDARY: this script is the ONLY surface that emits Claude Code's
30
+ # hookSpecificOutput.additionalContext envelope for lease state. The CLI
31
+ # emits CAWS-native JSON only. A Cursor or terminal integration would
32
+ # rewrite this script to emit its own protocol-specific output while
33
+ # reusing the same `caws agents heartbeat --json --include-active-summary`
34
+ # command verbatim.
35
+ #
36
+ # RUNTIME DEPENDENCIES: bash + node. node is already required by the CAWS
37
+ # CLI itself (which is a Node binary), so depending on it here adds no new
38
+ # runtime surface area. We do NOT depend on jq — jq is not guaranteed
39
+ # present on every install target (it is absent from many container base
40
+ # images and minimal CI runners), and a missing jq would silently drop
41
+ # every parallel-agent notice. The product goal is "agents see each
42
+ # other": that visibility cannot depend on a shell utility outside the
43
+ # CAWS toolchain.
44
+ #
45
+ # FAIL-CLOSED-NON-BLOCKING: if the CLI is absent, fails, or returns
46
+ # malformed JSON, this hook exits 0 silently. Heartbeat is observability
47
+ # and parallel-agent surfacing; a failure must never block the tool call.
48
+
49
+ set -uo pipefail
50
+
51
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
52
+
53
+ # shellcheck source=lib/parse-input.sh
54
+ source "$SCRIPT_DIR/lib/parse-input.sh" 2>/dev/null || exit 0
55
+ parse_hook_input || exit 0
56
+
57
+ if [[ -z "${HOOK_SESSION_ID:-}" || "$HOOK_SESSION_ID" == "unknown" ]]; then
58
+ exit 0
59
+ fi
60
+
61
+ CAWS_BIN="${CAWS_BIN:-caws}"
62
+ if ! command -v "$CAWS_BIN" >/dev/null 2>&1; then
63
+ exit 0
64
+ fi
65
+
66
+ # Capture both stdout (JSON) and stderr (diagnostics). On any CLI error,
67
+ # fall through to silent exit.
68
+ CLI_OUT="$(
69
+ "$CAWS_BIN" agents heartbeat \
70
+ --session-id "$HOOK_SESSION_ID" \
71
+ --platform claude-code \
72
+ --throttle 30000 \
73
+ --reason pre_tool_use \
74
+ --json \
75
+ --include-active-summary \
76
+ 2>/dev/null
77
+ )" || exit 0
78
+
79
+ if [[ -z "$CLI_OUT" ]]; then
80
+ exit 0
81
+ fi
82
+
83
+ # Parse the CAWS-native JSON and, when active_agent_count > 1, compose
84
+ # Claude Code's hookSpecificOutput.additionalContext envelope. A single
85
+ # node invocation does the whole pipeline: parse → filter peers → format
86
+ # bullet list → wrap envelope → emit. Malformed input, parse errors, or
87
+ # any thrown exception fall through to silent exit (fail-closed-non-
88
+ # blocking). Node is already a hard CAWS dependency — the CLI binary
89
+ # IS node — so this adds no new runtime surface vs. jq.
90
+ printf '%s' "$CLI_OUT" | node -e '
91
+ let raw = "";
92
+ process.stdin.setEncoding("utf8");
93
+ process.stdin.on("data", (chunk) => { raw += chunk; });
94
+ process.stdin.on("end", () => {
95
+ let parsed;
96
+ try { parsed = JSON.parse(raw); } catch { process.exit(0); }
97
+ const count = Number(parsed && parsed.active_agent_count);
98
+ if (!Number.isFinite(count) || count <= 1) process.exit(0);
99
+ const agents = Array.isArray(parsed.active_agents) ? parsed.active_agents : [];
100
+ const peers = agents.filter((a) => a && a.is_self !== true);
101
+ if (peers.length === 0) process.exit(0);
102
+ const bullets = peers.map((a) => {
103
+ const worktree = a.bound_worktree || "no worktree";
104
+ const spec = a.bound_spec_id ? " — spec " + a.bound_spec_id : "";
105
+ const kind = a.git_dir_kind || "unknown";
106
+ const branch = a.branch || "-";
107
+ const ageMs = Number(a.last_active_age_ms);
108
+ const ageSec = Number.isFinite(ageMs) ? Math.floor(ageMs / 1000) : 0;
109
+ return "• " + (a.session_id || "<unknown>") +
110
+ " (" + worktree + ")" + spec +
111
+ " — git_dir_kind=" + kind +
112
+ " — branch=" + branch +
113
+ " — last active " + ageSec + "s ago";
114
+ }).join("\n");
115
+ const ctx = "MULTI-AGENT NOTICE: " + count +
116
+ " agents active in this repo (including this session). Other active sessions:\n" +
117
+ bullets + "\n\n" +
118
+ "Coordinate via '\''caws agents list'\'' and '\''caws status'\'' before " +
119
+ "mutating shared state. Authority remains in .caws/worktrees.json " +
120
+ "(ownership) and .caws/specs/<id>.yaml (scope) — leases are " +
121
+ "visibility only.";
122
+ process.stdout.write(JSON.stringify({
123
+ hookSpecificOutput: {
124
+ hookEventName: "PreToolUse",
125
+ additionalContext: ctx,
126
+ },
127
+ }));
128
+ });
129
+ ' 2>/dev/null || exit 0
130
+
131
+ exit 0
@@ -0,0 +1,62 @@
1
+ #!/bin/bash
2
+ # CAWS-MANAGED-HOOK
3
+ # hook_pack: claude-code
4
+ # hook_pack_version: 5
5
+ # caws_min_major: 11
6
+ # lineage_refs: 19
7
+ # do_not_edit_directly: update via `caws init --agent-surface claude-code`
8
+ #
9
+ # SessionStart handler — agent self-registration into the .caws/leases/
10
+ # liveness substrate (MULTI-AGENT-ACTIVITY-REGISTRY-001).
11
+ #
12
+ # Sourcing: invoked by dispatch/session_start.sh after parse-input.sh has
13
+ # populated HOOK_SESSION_ID. Reads HOOK_INPUT_JSON from stdin (unused for
14
+ # this handler beyond the parse-input contract).
15
+ #
16
+ # Behavior:
17
+ # - Refuses to run when HOOK_SESSION_ID is empty or "unknown" (the
18
+ # parse-input.sh fallback). A lease whose filename is "unknown.json"
19
+ # would collide across every session that hits the same fallback.
20
+ # - Invokes `caws agents register --session-id <id> --platform claude-code
21
+ # --reason session_start` to write the lease through the CLI.
22
+ # - Non-blocking. Any failure of the CLI surfaces as stderr only;
23
+ # SessionStart never fails on hook errors.
24
+ #
25
+ # IO boundary: this script is the only place that knows about Claude Code's
26
+ # SessionStart payload. The CLI receives explicit flags and returns
27
+ # CAWS-native JSON. The hook script does not produce additionalContext
28
+ # at SessionStart — the parallel-agent surfacing happens at PreToolUse
29
+ # via agent-heartbeat.sh.
30
+
31
+ set -uo pipefail
32
+
33
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
34
+
35
+ # shellcheck source=lib/parse-input.sh
36
+ source "$SCRIPT_DIR/lib/parse-input.sh" 2>/dev/null || exit 0
37
+ parse_hook_input || exit 0
38
+
39
+ # Refuse on empty/unknown session id. Writing a lease at "unknown.json"
40
+ # would collide across every session that hits the parse-input fallback.
41
+ if [[ -z "${HOOK_SESSION_ID:-}" || "$HOOK_SESSION_ID" == "unknown" ]]; then
42
+ exit 0
43
+ fi
44
+
45
+ # Locate caws binary. Prefer a project-local install (consistent with the
46
+ # repo's installed CLI version); fall back to PATH.
47
+ CAWS_BIN="${CAWS_BIN:-caws}"
48
+ if ! command -v "$CAWS_BIN" >/dev/null 2>&1; then
49
+ # No CAWS binary on PATH — silent skip. Liveness is best-effort.
50
+ exit 0
51
+ fi
52
+
53
+ # Invoke register. Send stderr to a buffer; we may want to attribute it
54
+ # in dispatcher output (prefixed by run-handlers).
55
+ "$CAWS_BIN" agents register \
56
+ --session-id "$HOOK_SESSION_ID" \
57
+ --platform claude-code \
58
+ --reason session_start \
59
+ >/dev/null 2>&1 || true
60
+
61
+ # Never block SessionStart.
62
+ exit 0
@@ -0,0 +1,51 @@
1
+ #!/bin/bash
2
+ # CAWS-MANAGED-HOOK
3
+ # hook_pack: claude-code
4
+ # hook_pack_version: 5
5
+ # caws_min_major: 11
6
+ # lineage_refs: 19
7
+ # do_not_edit_directly: update via `caws init --agent-surface claude-code`
8
+ #
9
+ # Stop handler — marks the current session's lease as stopped on clean
10
+ # session exit (MULTI-AGENT-ACTIVITY-REGISTRY-001).
11
+ #
12
+ # Sourcing: invoked by dispatch/stop.sh after parse-input.sh has populated
13
+ # HOOK_SESSION_ID.
14
+ #
15
+ # Behavior:
16
+ # - Refuses on empty/unknown HOOK_SESSION_ID.
17
+ # - Invokes `caws agents stop --session-id <id> --platform claude-code`
18
+ # which writes a mark_stopped LeasePatch (status: stopped, stopped_at
19
+ # timestamp). The lease file is preserved as evidence; hard deletion
20
+ # happens only via explicit `caws agents prune`.
21
+ # - Non-blocking. Stop semantics already require all handlers to be
22
+ # best-effort; a Stop failure is a warn, not a block.
23
+ #
24
+ # Note: Stop is NOT a guaranteed signal. A SIGKILL'd or crashed Claude
25
+ # Code session never reaches Stop. The primary liveness mechanism is
26
+ # heartbeat — Stop is the clean-exit optimization that lets observers
27
+ # distinguish "stopped cleanly" from "went stale and is presumed dead."
28
+
29
+ set -uo pipefail
30
+
31
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
+
33
+ # shellcheck source=lib/parse-input.sh
34
+ source "$SCRIPT_DIR/lib/parse-input.sh" 2>/dev/null || exit 0
35
+ parse_hook_input || exit 0
36
+
37
+ if [[ -z "${HOOK_SESSION_ID:-}" || "$HOOK_SESSION_ID" == "unknown" ]]; then
38
+ exit 0
39
+ fi
40
+
41
+ CAWS_BIN="${CAWS_BIN:-caws}"
42
+ if ! command -v "$CAWS_BIN" >/dev/null 2>&1; then
43
+ exit 0
44
+ fi
45
+
46
+ "$CAWS_BIN" agents stop \
47
+ --session-id "$HOOK_SESSION_ID" \
48
+ --platform claude-code \
49
+ >/dev/null 2>&1 || true
50
+
51
+ exit 0
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
6
  # lineage_refs: 9
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
6
  # lineage_refs: 1,17
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
6
  # lineage_refs: 1,17
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
6
  # lineage_refs: 8,16
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
@@ -1,9 +1,9 @@
1
1
  #!/bin/bash
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
- # lineage_refs: 8,11,17
6
+ # lineage_refs: 8,11,17,19
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
8
8
  #
9
9
  # PreToolUse dispatcher for Claude Code hooks.
@@ -40,7 +40,16 @@ source "$HOOKS_DIR/lib/run-handlers.sh" 2>/dev/null || exit 0
40
40
 
41
41
  # Registered handlers in execution order. Each handler self-filters
42
42
  # on $HOOK_TOOL_NAME; non-matching cases return exit 0 cheaply.
43
+ #
44
+ # MULTI-AGENT-ACTIVITY-REGISTRY-001: agent-heartbeat.sh runs FIRST so the
45
+ # lease is refreshed and parallel-agent presence is surfaced even when a
46
+ # later guard short-circuits the chain with exit 2 (block). Heartbeat is
47
+ # non-blocking and never produces a "block" decision — its stdout is a
48
+ # Claude-Code additionalContext envelope (priority 1), so it does not
49
+ # outrank a real block from scope-guard / worktree-guard. The dispatcher's
50
+ # stdout-priority logic ensures a block from a later handler still wins.
43
51
  HANDLERS=(
52
+ agent-heartbeat.sh
44
53
  block-dangerous.sh
45
54
  worktree-guard.sh
46
55
  scope-guard.sh
@@ -1,9 +1,9 @@
1
1
  #!/bin/bash
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
- # lineage_refs: 10,11
6
+ # lineage_refs: 10,11,19
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
8
8
  # SessionStart dispatcher for Claude Code hooks.
9
9
  #
@@ -36,6 +36,10 @@ HANDLERS=(
36
36
  "audit.sh session-start"
37
37
  # "session-caws-status.sh session-start"
38
38
  "session-log.sh"
39
+ # MULTI-AGENT-ACTIVITY-REGISTRY-001: self-register into the leases
40
+ # substrate so other sessions can see this one. Non-blocking; refuses
41
+ # silently when HOOK_SESSION_ID is empty or "unknown".
42
+ "agent-register.sh"
39
43
  )
40
44
 
41
45
  run_handlers "${HANDLERS[@]}"
@@ -1,9 +1,9 @@
1
1
  #!/bin/bash
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
- # lineage_refs: 10
6
+ # lineage_refs: 10,19
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
8
8
  # Stop dispatcher for Claude Code hooks.
9
9
  #
@@ -32,6 +32,11 @@ HANDLERS=(
32
32
  # "stop-worktree-check.sh"
33
33
  "plan-transcript-finalize.sh"
34
34
  "session-log.sh"
35
+ # MULTI-AGENT-ACTIVITY-REGISTRY-001: mark our lease as stopped so other
36
+ # sessions can distinguish "stopped cleanly" from "went stale and is
37
+ # presumed dead." Non-blocking; refuses silently when HOOK_SESSION_ID
38
+ # is empty or "unknown".
39
+ "agent-stop.sh"
35
40
  )
36
41
 
37
42
  run_handlers "${HANDLERS[@]}"
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
6
  # lineage_refs: 8,16
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
6
  # lineage_refs: 8,16
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
6
  # lineage_refs: 8,16
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
6
  # lineage_refs: 17
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
6
  # lineage_refs: 17
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
6
  # lineage_refs: 8,16
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
6
  # lineage_refs: 8,11,12,16
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
6
  # lineage_refs: 4,11
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
@@ -109,6 +109,12 @@ if [ -f "$CAWS_ROOT/.caws/worktrees.json" ] && command -v node >/dev/null 2>&1;
109
109
  echo " Worktree lifecycle commands (create/destroy/merge) return in"
110
110
  echo " CAWS v11.1+; if you are on v11.0 they are not yet available."
111
111
  echo ""
112
+ echo " CANONICAL-CHECKOUT-WORKTREE-GUARD-001 active:"
113
+ echo " Mutating git commands from this checkout (checkout, switch,"
114
+ echo " branch -f, reset non-hard) are now BLOCKED while worktrees"
115
+ echo " are active. Read-only commands remain allowed. To act on a"
116
+ echo " worktree's branch, enter the worktree first."
117
+ echo ""
112
118
  else
113
119
  echo ""
114
120
  echo " You are on branch '$CURRENT_BRANCH' (worktree). Good."
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
6
  # lineage_refs: 10
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
@@ -1,9 +1,9 @@
1
1
  #!/bin/bash
2
2
  # CAWS-MANAGED-HOOK
3
3
  # hook_pack: claude-code
4
- # hook_pack_version: 2
4
+ # hook_pack_version: 5
5
5
  # caws_min_major: 11
6
- # lineage_refs: 4,6,11
6
+ # lineage_refs: 4,6,11,19
7
7
  # do_not_edit_directly: update via `caws init --agent-surface claude-code`
8
8
  #
9
9
  # CAWS Worktree Safety Guard for Claude Code (v11-shape).
@@ -50,11 +50,137 @@ if echo "$COMMAND" | grep -qE 'caws\s+(worktree\s+create|parallel\s+setup).*--sc
50
50
  fi
51
51
 
52
52
  if echo "$COMMAND" | grep -qE '(^|;|&&|\|)\s*git\s+sparse-checkout'; then
53
- echo "BLOCKED: git sparse-checkout is not allowed in this project." >&2
54
- echo "Use full worktrees without sparse-checkout." >&2
53
+ # WORKTREE-SPEC-CANONICAL-ACCESS-GUARD-001 A3: blanket refusal stays.
54
+ # Agent-issued git sparse-checkout commands are refused regardless of
55
+ # subcommand (disable / set / init / reapply / list / add). Recovery
56
+ # of the canonical-spec-materialization invariant in a linked CAWS
57
+ # worktree is a CAWS worktree-repair concern routed through the CLI,
58
+ # not an agent-Bash git operation.
59
+ echo "BLOCKED: agent-issued git sparse-checkout is refused in CAWS projects." >&2
60
+ echo "" >&2
61
+ echo "Sparse-checkout in a CAWS linked worktree carries the mechanical guard" >&2
62
+ echo "against the v10.2 split-brain authority class: .caws/specs/ is excluded" >&2
63
+ echo "from the worktree by design, so canonical spec authority cannot be" >&2
64
+ echo "materialized inside the worktree as a divergent private copy. Disabling" >&2
65
+ echo "sparse-checkout (or any sparse-checkout reconfiguration via agent Bash)" >&2
66
+ echo "would re-open that class. Linked worktrees must not use worktree-local" >&2
67
+ echo ".caws/specs/ files as authority; CAWS resolves spec reads through the" >&2
68
+ echo "canonical control plane regardless of cwd." >&2
69
+ echo "" >&2
70
+ echo "To read a spec from any cwd (including this worktree), use:" >&2
71
+ echo " caws specs show <id>" >&2
72
+ echo "" >&2
73
+ echo "To check scope from any cwd, use:" >&2
74
+ echo " caws scope show <path>" >&2
75
+ echo " caws scope check <path>" >&2
76
+ echo "" >&2
77
+ echo "To restore the sparse-checkout invariant on a linked worktree (e.g.," >&2
78
+ echo "after a human-authorized sparse-checkout reconfiguration left the tree" >&2
79
+ echo "with materialized .caws/specs/ files), run from the canonical checkout:" >&2
80
+ echo " caws worktree repair-sparse <name>" >&2
81
+ echo "" >&2
82
+ echo "The repair command is non-destructive: it refuses dirty .caws/specs/" >&2
83
+ echo "rather than stashing, cleaning, or deleting work." >&2
55
84
  exit 2
56
85
  fi
57
86
 
87
+ # ─── CANONICAL-CHECKOUT-WORKTREE-GUARD-001 (Entry 19) ────────────────
88
+ # Block mutating git commands from the canonical checkout while at
89
+ # least one active CAWS worktree exists. Hook-layer enforcement only:
90
+ # authority remains in worktrees.json + specs. The guard's refusal
91
+ # predicate is conjunctive: canonical + worktrees-active + mutating
92
+ # command. Any one false MUST allow.
93
+ #
94
+ # Leases (.caws/leases/*.json) are NOT consulted by this decision —
95
+ # stale-lease-is-evidence-never-authority. The block decision uses
96
+ # worktrees.json's active entries only.
97
+ canonical_guard_emit_block() {
98
+ local action="$1"
99
+ local first_active="$2"
100
+ echo "BLOCKED: $action from the canonical checkout while CAWS worktrees are active." >&2
101
+ echo "Active worktree(s) detected (e.g. '$first_active' in .caws/worktrees.json)." >&2
102
+ echo "Switch into your worktree before mutating: cd .caws/worktrees/$first_active" >&2
103
+ echo "Or destroy any worktree that is genuinely abandoned: caws worktree destroy <name>" >&2
104
+ }
105
+
106
+ # Determine whether the session's cwd is the canonical checkout.
107
+ # git_dir == git_common_dir indicates canonical; a linked worktree has
108
+ # git_dir under git_common_dir/worktrees/<name>/.
109
+ CANONICAL_GUARD_CHECK_CWD="${HOOK_CWD:-$PROJECT_DIR}"
110
+ if command -v git >/dev/null 2>&1 && [[ -d "$CANONICAL_GUARD_CHECK_CWD" ]]; then
111
+ GIT_DIR_RESOLVED=$(cd "$CANONICAL_GUARD_CHECK_CWD" && git rev-parse --git-dir 2>/dev/null | head -1 || echo "")
112
+ GIT_COMMON_RESOLVED=$(cd "$CANONICAL_GUARD_CHECK_CWD" && git rev-parse --git-common-dir 2>/dev/null | head -1 || echo "")
113
+ if [[ -n "$GIT_DIR_RESOLVED" ]] && [[ -n "$GIT_COMMON_RESOLVED" ]]; then
114
+ # Normalize to absolute paths so equality is structural, not textual.
115
+ GIT_DIR_ABS=$(cd "$CANONICAL_GUARD_CHECK_CWD" && cd "$GIT_DIR_RESOLVED" 2>/dev/null && pwd || echo "$GIT_DIR_RESOLVED")
116
+ GIT_COMMON_ABS=$(cd "$CANONICAL_GUARD_CHECK_CWD" && cd "$GIT_COMMON_RESOLVED" 2>/dev/null && pwd || echo "$GIT_COMMON_RESOLVED")
117
+ if [[ "$GIT_DIR_ABS" == "$GIT_COMMON_ABS" ]]; then
118
+ # We are in the canonical checkout. Now check for active worktrees.
119
+ WORKTREES_JSON="$PROJECT_DIR/.caws/worktrees.json"
120
+ if [[ -f "$WORKTREES_JSON" ]] && command -v node >/dev/null 2>&1; then
121
+ FIRST_ACTIVE_WT=$(node -e "
122
+ try {
123
+ var reg = JSON.parse(require('fs').readFileSync('$WORKTREES_JSON', 'utf8'));
124
+ function entriesOf(r) {
125
+ if (!r || typeof r !== 'object') return [];
126
+ if (r.worktrees && typeof r.worktrees === 'object') {
127
+ return Object.entries(r.worktrees);
128
+ }
129
+ var out = [];
130
+ for (var k in r) {
131
+ if (Object.prototype.hasOwnProperty.call(r, k)) {
132
+ var v = r[k];
133
+ if (v && typeof v === 'object') out.push([k, v]);
134
+ }
135
+ }
136
+ return out;
137
+ }
138
+ var entries = entriesOf(reg);
139
+ // 'active' is the documented status; entries without an
140
+ // explicit status (legacy/in-flight registry shapes) are
141
+ // also treated as active because the CLI's createWorktree
142
+ // does not always emit a status field.
143
+ var active = entries.filter(function(e) {
144
+ var s = e[1] && e[1].status;
145
+ return s === 'active' || s === undefined || s === null || s === '';
146
+ });
147
+ if (active.length > 0) console.log(active[0][0]);
148
+ else console.log('');
149
+ } catch(e) { console.log(''); }
150
+ " 2>/dev/null || echo "")
151
+ if [[ -n "$FIRST_ACTIVE_WT" ]]; then
152
+ # Predicate (a) canonical + (b) at least one active worktree
153
+ # is satisfied. Now check (c) mutation command keywords.
154
+ # Read-only commands (status, log, diff, show, fetch w/o --prune,
155
+ # rev-parse, ls-files, branch -v, stash list) are NOT in this
156
+ # set; they fall through to the existing guard rules.
157
+ if echo "$COMMAND" | grep -qE '(^|[[:space:];&|])git\s+checkout\s+[^[:space:]-]'; then
158
+ canonical_guard_emit_block "git checkout (branch switch)" "$FIRST_ACTIVE_WT"
159
+ exit 2
160
+ fi
161
+ if echo "$COMMAND" | grep -qE '(^|[[:space:];&|])git\s+switch\s+[^[:space:]-]'; then
162
+ canonical_guard_emit_block "git switch (branch switch)" "$FIRST_ACTIVE_WT"
163
+ exit 2
164
+ fi
165
+ if echo "$COMMAND" | grep -qE '(^|[[:space:];&|])git\s+branch\s+(-f|--force)'; then
166
+ canonical_guard_emit_block "git branch -f (force branch update)" "$FIRST_ACTIVE_WT"
167
+ exit 2
168
+ fi
169
+ # git reset variants other than --hard (already covered later in
170
+ # this file) — --keep, --merge, --soft, --mixed, or with no
171
+ # mode flag — mutate the canonical's working tree/HEAD.
172
+ if echo "$COMMAND" | grep -qE '(^|[[:space:];&|])git\s+reset\b' \
173
+ && ! echo "$COMMAND" | grep -qE 'git\s+reset\s+--hard'; then
174
+ canonical_guard_emit_block "git reset (HEAD mutation)" "$FIRST_ACTIVE_WT"
175
+ exit 2
176
+ fi
177
+ fi
178
+ fi
179
+ fi
180
+ fi
181
+ fi
182
+ # ─── /CANONICAL-CHECKOUT-WORKTREE-GUARD-001 ──────────────────────────
183
+
58
184
  # Block cross-boundary file copies (worktree → main).
59
185
  WORKTREE_BASE="$PROJECT_DIR/.caws/worktrees"
60
186
  if [[ -d "$WORKTREE_BASE" ]]; then