@paths.design/caws-cli 11.1.5 → 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.
- package/README.md +1 -1
- package/dist/init/hook-packs/manifest-claude-code.d.ts +1 -1
- package/dist/init/hook-packs/manifest-claude-code.d.ts.map +1 -1
- package/dist/init/hook-packs/manifest-claude-code.js +59 -6
- package/dist/init/hook-packs/manifest-claude-code.js.map +1 -1
- package/dist/init/hook-packs/types.js +1 -1
- package/dist/init/hook-packs/types.js.map +1 -1
- package/dist/shell/commands/agents.d.ts +49 -0
- package/dist/shell/commands/agents.d.ts.map +1 -0
- package/dist/shell/commands/agents.js +577 -0
- package/dist/shell/commands/agents.js.map +1 -0
- package/dist/shell/commands/claim.d.ts.map +1 -1
- package/dist/shell/commands/claim.js +3 -4
- package/dist/shell/commands/claim.js.map +1 -1
- package/dist/shell/commands/status.d.ts +12 -0
- package/dist/shell/commands/status.d.ts.map +1 -1
- package/dist/shell/commands/status.js +236 -21
- package/dist/shell/commands/status.js.map +1 -1
- package/dist/shell/commands/worktree.d.ts +9 -0
- package/dist/shell/commands/worktree.d.ts.map +1 -1
- package/dist/shell/commands/worktree.js +302 -0
- package/dist/shell/commands/worktree.js.map +1 -1
- package/dist/shell/index.d.ts +4 -2
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +12 -1
- package/dist/shell/index.js.map +1 -1
- package/dist/shell/register.d.ts.map +1 -1
- package/dist/shell/register.js +150 -0
- package/dist/shell/register.js.map +1 -1
- package/dist/shell/render/status.d.ts +7 -1
- package/dist/shell/render/status.d.ts.map +1 -1
- package/dist/shell/render/status.js +72 -0
- package/dist/shell/render/status.js.map +1 -1
- package/dist/store/agents-store.d.ts.map +1 -1
- package/dist/store/agents-store.js +9 -0
- package/dist/store/agents-store.js.map +1 -1
- package/dist/store/apply-patch.d.ts.map +1 -1
- package/dist/store/apply-patch.js +15 -0
- package/dist/store/apply-patch.js.map +1 -1
- package/dist/store/doctor-snapshot.d.ts.map +1 -1
- package/dist/store/doctor-snapshot.js +143 -3
- package/dist/store/doctor-snapshot.js.map +1 -1
- package/dist/store/git-sparse-checkout.d.ts +25 -0
- package/dist/store/git-sparse-checkout.d.ts.map +1 -0
- package/dist/store/git-sparse-checkout.js +101 -0
- package/dist/store/git-sparse-checkout.js.map +1 -0
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +10 -1
- package/dist/store/index.js.map +1 -1
- package/dist/store/leases-store.d.ts +89 -0
- package/dist/store/leases-store.d.ts.map +1 -0
- package/dist/store/leases-store.js +369 -0
- package/dist/store/leases-store.js.map +1 -0
- package/dist/store/lifecycle-transaction.d.ts.map +1 -1
- package/dist/store/lifecycle-transaction.js +34 -1
- package/dist/store/lifecycle-transaction.js.map +1 -1
- package/dist/store/rules.d.ts +21 -1
- package/dist/store/rules.d.ts.map +1 -1
- package/dist/store/rules.js +22 -0
- package/dist/store/rules.js.map +1 -1
- package/dist/store/types.d.ts +25 -1
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/worktrees-migration.d.ts +141 -0
- package/dist/store/worktrees-migration.d.ts.map +1 -0
- package/dist/store/worktrees-migration.js +356 -0
- package/dist/store/worktrees-migration.js.map +1 -0
- package/dist/store/worktrees-writer.d.ts.map +1 -1
- package/dist/store/worktrees-writer.js +37 -1
- package/dist/store/worktrees-writer.js.map +1 -1
- package/package.json +2 -2
- package/templates/hook-packs/claude-code/CLAUDE.md +5 -5
- package/templates/hook-packs/claude-code/agent-heartbeat.sh +131 -0
- package/templates/hook-packs/claude-code/agent-register.sh +62 -0
- package/templates/hook-packs/claude-code/agent-stop.sh +51 -0
- package/templates/hook-packs/claude-code/audit.sh +1 -1
- package/templates/hook-packs/claude-code/block-dangerous.sh +1 -1
- package/templates/hook-packs/claude-code/classify_command.py +454 -12
- package/templates/hook-packs/claude-code/dispatch/post_tool_use.sh +1 -1
- package/templates/hook-packs/claude-code/dispatch/pre_tool_use.sh +11 -2
- package/templates/hook-packs/claude-code/dispatch/session_start.sh +6 -2
- package/templates/hook-packs/claude-code/dispatch/stop.sh +7 -2
- package/templates/hook-packs/claude-code/guard-strikes.sh +1 -1
- package/templates/hook-packs/claude-code/lib/parse-input.sh +1 -1
- package/templates/hook-packs/claude-code/lib/run-handlers.sh +1 -1
- package/templates/hook-packs/claude-code/reset-danger-latch.sh +1 -1
- package/templates/hook-packs/claude-code/reset-strikes.sh +1 -1
- package/templates/hook-packs/claude-code/runtime-paths.sh +1 -1
- package/templates/hook-packs/claude-code/scope-guard.sh +1 -1
- package/templates/hook-packs/claude-code/session-caws-status.sh +7 -1
- package/templates/hook-packs/claude-code/session-log.sh +1 -1
- package/templates/hook-packs/claude-code/worktree-guard.sh +130 -4
- package/templates/hook-packs/claude-code/worktree-write-guard.sh +133 -18
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# CAWS-MANAGED-HOOK
|
|
3
3
|
# hook_pack: claude-code
|
|
4
|
-
# hook_pack_version:
|
|
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,9 +1,9 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# CAWS-MANAGED-HOOK
|
|
3
3
|
# hook_pack: claude-code
|
|
4
|
-
# hook_pack_version:
|
|
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
|
-
|
|
54
|
-
|
|
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
|
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# CAWS-MANAGED-HOOK
|
|
3
3
|
# hook_pack: claude-code
|
|
4
|
-
# hook_pack_version:
|
|
4
|
+
# hook_pack_version: 5
|
|
5
5
|
# caws_min_major: 11
|
|
6
6
|
# lineage_refs: 4,8,13
|
|
7
7
|
# do_not_edit_directly: update via `caws init --agent-surface claude-code`
|
|
8
8
|
#
|
|
9
|
-
# CAWS Worktree Write Guard for Claude Code
|
|
10
|
-
# fail-open for v11.1).
|
|
9
|
+
# CAWS Worktree Write Guard for Claude Code.
|
|
11
10
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
11
|
+
# Two responsibilities:
|
|
12
|
+
#
|
|
13
|
+
# 1. Canonical-spec-materialization refusal
|
|
14
|
+
# (WORKTREE-SPEC-CANONICAL-ACCESS-GUARD-001 A1/A2).
|
|
15
|
+
# From inside a linked worktree (git rev-parse --git-common-dir !=
|
|
16
|
+
# git rev-parse --git-dir, after realpath normalization), refuse
|
|
17
|
+
# Read/Write/Edit tool calls whose file_path resolves under
|
|
18
|
+
# <linked-worktree>/.caws/specs/*. Such files would be private
|
|
19
|
+
# materialized copies of canonical spec authority, divergent from
|
|
20
|
+
# the canonical .caws/specs bytes, silently consulted by anything
|
|
21
|
+
# that walks cwd upward. The refusal MUST fire BEFORE the broad
|
|
22
|
+
# .caws/* allowlist below, otherwise the allowlist would exit 0
|
|
23
|
+
# first and the slice would appear implemented while the target
|
|
24
|
+
# path still bypassed the guard. The canonical checkout itself
|
|
25
|
+
# (git_common_dir == git_dir) IS spec authority and is allowed
|
|
26
|
+
# through this predicate; this refusal targets the linked-worktree
|
|
27
|
+
# materialization class only.
|
|
28
|
+
#
|
|
29
|
+
# 2. Base-branch write enforcement (intentionally fail-open for
|
|
30
|
+
# v11.1, restored in CLI-WORKTREE-001). The hook serves as the
|
|
31
|
+
# managed-install seat for the worktree-write enforcement surface
|
|
32
|
+
# and asserts the always-allowed allowlist so .caws/, .claude/,
|
|
33
|
+
# docs/, scripts/, tmp/, and tests/ writes are never inadvertently
|
|
34
|
+
# blocked by a future enforcement pass that forgets the allowlist.
|
|
19
35
|
#
|
|
20
36
|
# Worktree-active enforcement (when restored) must read the worktrees
|
|
21
37
|
# registry under both shapes:
|
|
@@ -34,23 +50,122 @@ TOOL_NAME="$HOOK_TOOL_NAME"
|
|
|
34
50
|
FILE_PATH="$HOOK_FILE_PATH"
|
|
35
51
|
|
|
36
52
|
case "$TOOL_NAME" in
|
|
37
|
-
Write|Edit) ;;
|
|
53
|
+
Read|Write|Edit) ;;
|
|
38
54
|
*) exit 0 ;;
|
|
39
55
|
esac
|
|
40
56
|
|
|
41
|
-
|
|
42
|
-
|
|
57
|
+
# WORKTREE_ROOT: where the agent is operating from. This is the cwd
|
|
58
|
+
# whose .caws/specs/* path is the refusal target. Kept distinct from
|
|
59
|
+
# CANONICAL_ROOT below — these MUST NOT be conflated for the spec-path
|
|
60
|
+
# predicate.
|
|
61
|
+
WORKTREE_ROOT="${CLAUDE_PROJECT_DIR:-.}"
|
|
62
|
+
WORKTREE_ROOT="$(cd "$WORKTREE_ROOT" 2>/dev/null && pwd -P || printf '%s\n' "$WORKTREE_ROOT")"
|
|
43
63
|
|
|
64
|
+
# _realpath: best-effort realpath. macOS lacks `readlink -f` by default;
|
|
65
|
+
# python3 is available on every supported runner (CI matrix verified).
|
|
66
|
+
# Falls back to the original path if realpath cannot resolve.
|
|
67
|
+
_realpath() {
|
|
68
|
+
local p="$1"
|
|
69
|
+
if [[ -z "$p" ]]; then
|
|
70
|
+
printf '%s\n' ""
|
|
71
|
+
return 0
|
|
72
|
+
fi
|
|
73
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
74
|
+
python3 -c "import os, sys; print(os.path.realpath(sys.argv[1]))" "$p" 2>/dev/null || printf '%s\n' "$p"
|
|
75
|
+
else
|
|
76
|
+
printf '%s\n' "$p"
|
|
77
|
+
fi
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Linked-worktree detection via git as primary signal. CAWS registry
|
|
81
|
+
# (.caws/worktrees.json) is consulted ONLY for diagnostic enrichment;
|
|
82
|
+
# a registry desync MUST NOT suppress the refusal (I3).
|
|
83
|
+
IS_LINKED_WORKTREE=0
|
|
84
|
+
CANONICAL_ROOT=""
|
|
44
85
|
if command -v git >/dev/null 2>&1; then
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
86
|
+
GIT_COMMON_DIR_RAW="$(cd "$WORKTREE_ROOT" 2>/dev/null && git rev-parse --git-common-dir 2>/dev/null || printf '')"
|
|
87
|
+
GIT_DIR_RAW="$(cd "$WORKTREE_ROOT" 2>/dev/null && git rev-parse --git-dir 2>/dev/null || printf '')"
|
|
88
|
+
if [[ -n "$GIT_COMMON_DIR_RAW" ]] && [[ -n "$GIT_DIR_RAW" ]]; then
|
|
89
|
+
# Resolve relative paths against WORKTREE_ROOT before realpath.
|
|
90
|
+
case "$GIT_COMMON_DIR_RAW" in
|
|
91
|
+
/*) GIT_COMMON_DIR_ABS="$GIT_COMMON_DIR_RAW" ;;
|
|
92
|
+
*) GIT_COMMON_DIR_ABS="$WORKTREE_ROOT/$GIT_COMMON_DIR_RAW" ;;
|
|
93
|
+
esac
|
|
94
|
+
case "$GIT_DIR_RAW" in
|
|
95
|
+
/*) GIT_DIR_ABS="$GIT_DIR_RAW" ;;
|
|
96
|
+
*) GIT_DIR_ABS="$WORKTREE_ROOT/$GIT_DIR_RAW" ;;
|
|
97
|
+
esac
|
|
98
|
+
GIT_COMMON_DIR="$(_realpath "$GIT_COMMON_DIR_ABS")"
|
|
99
|
+
GIT_DIR="$(_realpath "$GIT_DIR_ABS")"
|
|
100
|
+
if [[ -n "$GIT_COMMON_DIR" ]] && [[ "$GIT_COMMON_DIR" != "$GIT_DIR" ]]; then
|
|
101
|
+
IS_LINKED_WORKTREE=1
|
|
102
|
+
# CANONICAL_ROOT = parent of GIT_COMMON_DIR. Used for allowlist
|
|
103
|
+
# rewriting only; NOT used for the spec-path refusal predicate.
|
|
104
|
+
CANONICAL_CANDIDATE="$(_realpath "$GIT_COMMON_DIR/..")"
|
|
105
|
+
if [[ -n "$CANONICAL_CANDIDATE" ]] && [[ -d "$CANONICAL_CANDIDATE/.caws" ]]; then
|
|
106
|
+
CANONICAL_ROOT="$CANONICAL_CANDIDATE"
|
|
107
|
+
fi
|
|
50
108
|
fi
|
|
51
109
|
fi
|
|
52
110
|
fi
|
|
53
111
|
|
|
112
|
+
# Canonical-spec-materialization refusal (I1: BEFORE the allowlist).
|
|
113
|
+
#
|
|
114
|
+
# Predicate: tool_name in {Read,Write,Edit} (already gated above)
|
|
115
|
+
# AND is_linked_worktree (via git signal)
|
|
116
|
+
# AND FILE_PATH resolves under <WORKTREE_ROOT>/.caws/specs/.
|
|
117
|
+
#
|
|
118
|
+
# WORKTREE_ROOT is the cwd-as-resolved-via-CLAUDE_PROJECT_DIR. NOT
|
|
119
|
+
# CANONICAL_ROOT, NOT a PROJECT_DIR that has been rewritten upward. The
|
|
120
|
+
# refused path lives under the LINKED worktree's tree.
|
|
121
|
+
if [[ "$IS_LINKED_WORKTREE" == "1" ]] && [[ -n "$FILE_PATH" ]]; then
|
|
122
|
+
# WORKTREE_ROOT is already realpath-normalized (pwd -P above), so
|
|
123
|
+
# SPEC_ROOT inherits that normalization. We MUST also normalize
|
|
124
|
+
# FILE_PATH_ABS through _realpath so the comparison is symlink-
|
|
125
|
+
# immune. On macOS, /tmp -> /private/tmp; without normalization, an
|
|
126
|
+
# agent-supplied /tmp/.../.caws/specs/X.yaml would NOT prefix-match
|
|
127
|
+
# SPEC_ROOT=/private/tmp/.../.caws/specs because the literal strings
|
|
128
|
+
# diverge. python3 os.path.realpath resolves the existing prefix
|
|
129
|
+
# even when the leaf does not exist (Write tool case).
|
|
130
|
+
SPEC_ROOT="$WORKTREE_ROOT/.caws/specs"
|
|
131
|
+
case "$FILE_PATH" in
|
|
132
|
+
/*) FILE_PATH_ABS="$FILE_PATH" ;;
|
|
133
|
+
*) FILE_PATH_ABS="$WORKTREE_ROOT/$FILE_PATH" ;;
|
|
134
|
+
esac
|
|
135
|
+
FILE_PATH_ABS="$(_realpath "$FILE_PATH_ABS")"
|
|
136
|
+
case "$FILE_PATH_ABS" in
|
|
137
|
+
"$SPEC_ROOT"/*|"$SPEC_ROOT")
|
|
138
|
+
echo "[worktree-write-guard.sh] BLOCKED: $FILE_PATH" >&2
|
|
139
|
+
echo "[worktree-write-guard.sh] Refusing $TOOL_NAME against a linked-worktree .caws/specs/ path." >&2
|
|
140
|
+
echo "[worktree-write-guard.sh]" >&2
|
|
141
|
+
echo "[worktree-write-guard.sh] Linked worktrees must not use worktree-local .caws/specs/ files as authority." >&2
|
|
142
|
+
echo "[worktree-write-guard.sh] That path would be a private materialized copy, not canonical spec authority." >&2
|
|
143
|
+
echo "[worktree-write-guard.sh] CAWS resolves spec reads through the canonical control plane regardless of cwd." >&2
|
|
144
|
+
echo "[worktree-write-guard.sh]" >&2
|
|
145
|
+
echo "[worktree-write-guard.sh] To read a spec from any cwd (including this worktree), use:" >&2
|
|
146
|
+
echo "[worktree-write-guard.sh] caws specs show <id>" >&2
|
|
147
|
+
echo "[worktree-write-guard.sh]" >&2
|
|
148
|
+
echo "[worktree-write-guard.sh] To check scope from any cwd, use:" >&2
|
|
149
|
+
echo "[worktree-write-guard.sh] caws scope show <path>" >&2
|
|
150
|
+
echo "[worktree-write-guard.sh] caws scope check <path>" >&2
|
|
151
|
+
echo "[worktree-write-guard.sh]" >&2
|
|
152
|
+
echo "[worktree-write-guard.sh] If sparse-checkout was disabled in this worktree and you need to restore" >&2
|
|
153
|
+
echo "[worktree-write-guard.sh] the canonical-only invariant, run from the canonical checkout:" >&2
|
|
154
|
+
echo "[worktree-write-guard.sh] caws worktree repair-sparse <name>" >&2
|
|
155
|
+
exit 2
|
|
156
|
+
;;
|
|
157
|
+
esac
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# Legacy allowlist preserved from v11.1 fail-open base-branch enforcement.
|
|
161
|
+
# For the allowlist, use PROJECT_DIR rewritten toward the canonical checkout
|
|
162
|
+
# (the historical behavior) so that .caws/ etc. paths under canonical also
|
|
163
|
+
# match when the agent is operating from inside a linked worktree.
|
|
164
|
+
PROJECT_DIR="$WORKTREE_ROOT"
|
|
165
|
+
if [[ -n "$CANONICAL_ROOT" ]]; then
|
|
166
|
+
PROJECT_DIR="$CANONICAL_ROOT"
|
|
167
|
+
fi
|
|
168
|
+
|
|
54
169
|
# Always-allowed paths bypass any future enforcement.
|
|
55
170
|
# User-global Claude state lives outside the repo; .caws/, .claude/, docs/,
|
|
56
171
|
# scripts/, tmp/, .archive/, and .githooks/ are coordination/governance
|