@paths.design/caws-cli 11.1.6 → 11.1.8
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/index.js +55 -58
- 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 +317 -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/binding/resolve-binding.d.ts.map +1 -1
- package/dist/shell/binding/resolve-binding.js +105 -1
- package/dist/shell/binding/resolve-binding.js.map +1 -1
- package/dist/shell/binding/types.d.ts +47 -3
- package/dist/shell/binding/types.d.ts.map +1 -1
- package/dist/shell/command-metadata.d.ts +93 -0
- package/dist/shell/command-metadata.d.ts.map +1 -0
- package/dist/shell/command-metadata.js +687 -0
- package/dist/shell/command-metadata.js.map +1 -0
- package/dist/shell/commands/agents.d.ts +48 -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 +16 -0
- package/dist/shell/commands/claim.d.ts.map +1 -1
- package/dist/shell/commands/claim.js +88 -30
- package/dist/shell/commands/claim.js.map +1 -1
- package/dist/shell/commands/events.d.ts +106 -0
- package/dist/shell/commands/events.d.ts.map +1 -0
- package/dist/shell/commands/events.js +510 -0
- package/dist/shell/commands/events.js.map +1 -0
- package/dist/shell/commands/gates.d.ts +2 -2
- package/dist/shell/commands/gates.d.ts.map +1 -1
- package/dist/shell/commands/gates.js +106 -25
- package/dist/shell/commands/gates.js.map +1 -1
- package/dist/shell/commands/init.d.ts.map +1 -1
- package/dist/shell/commands/init.js +26 -0
- package/dist/shell/commands/init.js.map +1 -1
- package/dist/shell/commands/prepush.d.ts +26 -0
- package/dist/shell/commands/prepush.d.ts.map +1 -0
- package/dist/shell/commands/prepush.js +373 -0
- package/dist/shell/commands/prepush.js.map +1 -0
- package/dist/shell/commands/scope.d.ts.map +1 -1
- package/dist/shell/commands/scope.js +31 -1
- package/dist/shell/commands/scope.js.map +1 -1
- package/dist/shell/commands/specs.d.ts +44 -3
- package/dist/shell/commands/specs.d.ts.map +1 -1
- package/dist/shell/commands/specs.js +411 -15
- package/dist/shell/commands/specs.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 +353 -1
- package/dist/shell/commands/worktree.js.map +1 -1
- package/dist/shell/gates/disposition.d.ts.map +1 -1
- package/dist/shell/gates/disposition.js +43 -2
- package/dist/shell/gates/disposition.js.map +1 -1
- package/dist/shell/index.d.ts +14 -6
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +32 -1
- package/dist/shell/index.js.map +1 -1
- package/dist/shell/legacy-command-map.js +832 -0
- package/dist/shell/push-range/classify-range.d.ts +99 -0
- package/dist/shell/push-range/classify-range.d.ts.map +1 -0
- package/dist/shell/push-range/classify-range.js +155 -0
- package/dist/shell/push-range/classify-range.js.map +1 -0
- package/dist/shell/push-range/scope-match.d.ts +13 -0
- package/dist/shell/push-range/scope-match.d.ts.map +1 -0
- package/dist/shell/push-range/scope-match.js +53 -0
- package/dist/shell/push-range/scope-match.js.map +1 -0
- package/dist/shell/register.d.ts.map +1 -1
- package/dist/shell/register.js +350 -165
- package/dist/shell/register.js.map +1 -1
- package/dist/shell/registered-command-groups.js +48 -0
- 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/shell/rules.d.ts +19 -0
- package/dist/shell/rules.d.ts.map +1 -1
- package/dist/shell/rules.js +27 -0
- package/dist/shell/rules.js.map +1 -1
- package/dist/shell/session/resolve-session.d.ts +29 -1
- package/dist/shell/session/resolve-session.d.ts.map +1 -1
- package/dist/shell/session/resolve-session.js +817 -11
- package/dist/shell/session/resolve-session.js.map +1 -1
- package/dist/shell/session/types.d.ts +127 -1
- package/dist/shell/session/types.d.ts.map +1 -1
- package/dist/shell/session/types.js +10 -4
- package/dist/shell/session/types.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 +169 -3
- package/dist/store/doctor-snapshot.js.map +1 -1
- package/dist/store/events-migration.d.ts +207 -0
- package/dist/store/events-migration.d.ts.map +1 -0
- package/dist/store/events-migration.js +358 -0
- package/dist/store/events-migration.js.map +1 -0
- package/dist/store/events-store.d.ts +47 -1
- package/dist/store/events-store.d.ts.map +1 -1
- package/dist/store/events-store.js +278 -0
- package/dist/store/events-store.js.map +1 -1
- package/dist/store/git-autocommit.d.ts +46 -0
- package/dist/store/git-autocommit.d.ts.map +1 -0
- package/dist/store/git-autocommit.js +198 -0
- package/dist/store/git-autocommit.js.map +1 -0
- 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 +6 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +16 -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 +427 -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 +74 -1
- package/dist/store/rules.d.ts.map +1 -1
- package/dist/store/rules.js +76 -0
- package/dist/store/rules.js.map +1 -1
- package/dist/store/specs-migration.d.ts +128 -0
- package/dist/store/specs-migration.d.ts.map +1 -0
- package/dist/store/specs-migration.js +481 -0
- package/dist/store/specs-migration.js.map +1 -0
- package/dist/store/specs-store.d.ts.map +1 -1
- package/dist/store/specs-store.js +14 -2
- package/dist/store/specs-store.js.map +1 -1
- package/dist/store/specs-writer.d.ts +130 -3
- package/dist/store/specs-writer.d.ts.map +1 -1
- package/dist/store/specs-writer.js +941 -102
- package/dist/store/specs-writer.js.map +1 -1
- package/dist/store/types.d.ts +31 -1
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/waivers-store.d.ts.map +1 -1
- package/dist/store/waivers-store.js +8 -1
- package/dist/store/waivers-store.js.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 +28 -0
- package/dist/store/worktrees-writer.d.ts.map +1 -1
- package/dist/store/worktrees-writer.js +147 -13
- package/dist/store/worktrees-writer.js.map +1 -1
- package/package.json +5 -2
- package/templates/hook-packs/claude-code/CLAUDE.md +11 -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 +1 -1
- package/templates/hook-packs/claude-code/cwd-guard.sh +30 -0
- package/templates/hook-packs/claude-code/dispatch/post_tool_use.sh +15 -4
- package/templates/hook-packs/claude-code/dispatch/pre_tool_use.sh +19 -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/duplicate-export-check.sh +156 -0
- package/templates/hook-packs/claude-code/god-object-check.sh +102 -0
- package/templates/hook-packs/claude-code/guard-strikes.sh +1 -1
- package/templates/hook-packs/claude-code/lib/parse-input.sh +115 -1
- package/templates/hook-packs/claude-code/lib/run-handlers.sh +1 -1
- package/templates/hook-packs/claude-code/loc-delta-check.sh +91 -0
- package/templates/hook-packs/claude-code/naming-check.sh +128 -0
- package/templates/hook-packs/claude-code/plan-transcript-finalize.sh +59 -0
- package/templates/hook-packs/claude-code/plan-transcript-snapshot.sh +86 -0
- package/templates/hook-packs/claude-code/protected-paths.sh +59 -0
- package/templates/hook-packs/claude-code/quiet-merge.sh +68 -0
- 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/scan-secrets.sh +98 -0
- package/templates/hook-packs/claude-code/scope-guard.sh +47 -65
- 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/session_log_renderer.py +956 -0
- package/templates/hook-packs/claude-code/shortcut-language-check.sh +147 -0
- package/templates/hook-packs/claude-code/worktree-guard.sh +130 -4
- package/templates/hook-packs/claude-code/worktree-write-guard.sh +133 -18
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS-MANAGED-HOOK
|
|
3
|
+
# hook_pack: claude-code
|
|
4
|
+
# hook_pack_version: 11
|
|
5
|
+
# caws_min_major: 11
|
|
6
|
+
# lineage_refs: 29
|
|
7
|
+
# do_not_edit_directly: update via `caws init --agent-surface claude-code`
|
|
8
|
+
#
|
|
9
|
+
# CAWS Shortcut-Language Progressive Check (QG-HOOKS-EXTRACT-001)
|
|
10
|
+
#
|
|
11
|
+
# PostToolUse hook firing on Write/Edit. Flags "shortcut" / incomplete-work
|
|
12
|
+
# language in committed-bound source — the edit-time analogue of the
|
|
13
|
+
# quality-gates `todo_detection` gate
|
|
14
|
+
# (packages/quality-gates/todo-analyzer.mjs + check-placeholders.mjs). It
|
|
15
|
+
# re-implements the practical intent in self-contained bash: catch the agent
|
|
16
|
+
# leaving a TODO/FIXME/placeholder/"not implemented" stub in a NON-test source
|
|
17
|
+
# file.
|
|
18
|
+
#
|
|
19
|
+
# Unlike the other three advisory hooks, this one escalates via the existing
|
|
20
|
+
# progressive-strike mechanism (guard-strikes.sh):
|
|
21
|
+
# strike 1 -> warn (allow)
|
|
22
|
+
# strike 2 -> ask (permission prompt)
|
|
23
|
+
# strike 3 -> block
|
|
24
|
+
# Rationale: TODO/placeholder language in committed code is the CLAUDE.md
|
|
25
|
+
# "No fake implementations" rule; repeated offenses in a session warrant
|
|
26
|
+
# escalation, matching how scope-guard treats repeated scope violations.
|
|
27
|
+
#
|
|
28
|
+
# Test files (*.test.* / *.spec.*) are NOT strike-eligible: TODO/placeholder
|
|
29
|
+
# language in tests is routine (describing pending cases, fixture stubs).
|
|
30
|
+
#
|
|
31
|
+
# Patterns (case-insensitive) — only the high-signal subset of the
|
|
32
|
+
# todo-analyzer engine, kept to single-file grep for hook-time speed:
|
|
33
|
+
# \bTODO\b \bFIXME\b \bXXX\b \bHACK\b \bTBD\b
|
|
34
|
+
# "not implemented" "implement later" "coming soon" "placeholder"
|
|
35
|
+
# stub-return shapes: return null;? // TODO ; throw new Error("not implemented")
|
|
36
|
+
#
|
|
37
|
+
# env: none (strike count fixed at 3 via guard-strikes.sh).
|
|
38
|
+
|
|
39
|
+
set -uo pipefail
|
|
40
|
+
|
|
41
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
42
|
+
# shellcheck source=lib/parse-input.sh
|
|
43
|
+
source "$SCRIPT_DIR/lib/parse-input.sh" 2>/dev/null || exit 0
|
|
44
|
+
parse_hook_input || exit 0
|
|
45
|
+
# shellcheck source=guard-strikes.sh
|
|
46
|
+
source "$SCRIPT_DIR/guard-strikes.sh" 2>/dev/null || exit 0
|
|
47
|
+
|
|
48
|
+
FILE_PATH="$HOOK_FILE_PATH"
|
|
49
|
+
TOOL_NAME="$HOOK_TOOL_NAME"
|
|
50
|
+
|
|
51
|
+
case "$TOOL_NAME" in
|
|
52
|
+
Write | Edit) ;;
|
|
53
|
+
*) exit 0 ;;
|
|
54
|
+
esac
|
|
55
|
+
|
|
56
|
+
[[ -z "$FILE_PATH" ]] && exit 0
|
|
57
|
+
|
|
58
|
+
# Skip generated / vendored / build output.
|
|
59
|
+
case "$FILE_PATH" in
|
|
60
|
+
*/node_modules/* | */dist/* | */build/* | */coverage/* | */.next/* | */out/* | */vendor/*)
|
|
61
|
+
exit 0
|
|
62
|
+
;;
|
|
63
|
+
esac
|
|
64
|
+
|
|
65
|
+
# Skip non-source artifacts: markdown/docs and lockfiles routinely contain
|
|
66
|
+
# the word "placeholder"/"TODO" as prose. The hook targets code.
|
|
67
|
+
case "$(basename "$FILE_PATH")" in
|
|
68
|
+
*.md | *.markdown | *.txt | *.lock | *-lock.json | package-lock.json | *.min.js | *.bundle.js | *.map)
|
|
69
|
+
exit 0
|
|
70
|
+
;;
|
|
71
|
+
esac
|
|
72
|
+
|
|
73
|
+
# Test files are exempt from strikes (TODO/placeholder is routine in specs).
|
|
74
|
+
case "$FILE_PATH" in
|
|
75
|
+
*.test.* | *.spec.* | */tests/* | */__tests__/* | */test/* | */fixtures/*)
|
|
76
|
+
exit 0
|
|
77
|
+
;;
|
|
78
|
+
esac
|
|
79
|
+
|
|
80
|
+
# The content to scan: prefer the tool payload (works on untracked files and
|
|
81
|
+
# is exactly what the agent just wrote). Write -> .content; Edit -> .new_string.
|
|
82
|
+
CONTENT=""
|
|
83
|
+
if [[ -n "${HOOK_TOOL_INPUT_JSON:-}" ]] && command -v jq >/dev/null 2>&1; then
|
|
84
|
+
CONTENT=$(printf '%s' "$HOOK_TOOL_INPUT_JSON" | jq -r '.content // .new_string // empty' 2>/dev/null || printf '')
|
|
85
|
+
fi
|
|
86
|
+
# Fallback: read the file from disk if the payload had no content field.
|
|
87
|
+
if [[ -z "$CONTENT" ]] && [[ -f "$FILE_PATH" ]]; then
|
|
88
|
+
CONTENT=$(cat "$FILE_PATH" 2>/dev/null || printf '')
|
|
89
|
+
fi
|
|
90
|
+
[[ -z "$CONTENT" ]] && exit 0
|
|
91
|
+
|
|
92
|
+
# Match the high-signal patterns. grep -nE on the content via a here-string;
|
|
93
|
+
# -i case-insensitive. Word-boundary keywords first, then phrases, then
|
|
94
|
+
# stub-return shapes.
|
|
95
|
+
MATCH=""
|
|
96
|
+
PATTERN_DESC=""
|
|
97
|
+
|
|
98
|
+
# 1. Keyword markers (word-boundary).
|
|
99
|
+
if hit=$(printf '%s' "$CONTENT" | grep -nE '\b(TODO|FIXME|XXX|HACK|TBD)\b' 2>/dev/null | head -1); then
|
|
100
|
+
if [[ -n "$hit" ]]; then
|
|
101
|
+
MATCH="$hit"
|
|
102
|
+
PATTERN_DESC="incomplete-work marker (TODO/FIXME/XXX/HACK/TBD)"
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# 2. Placeholder / not-implemented phrases.
|
|
107
|
+
if [[ -z "$MATCH" ]]; then
|
|
108
|
+
if hit=$(printf '%s' "$CONTENT" | grep -niE 'not implemented|implement later|coming soon|placeholder' 2>/dev/null | head -1); then
|
|
109
|
+
if [[ -n "$hit" ]]; then
|
|
110
|
+
MATCH="$hit"
|
|
111
|
+
PATTERN_DESC="placeholder / not-implemented language"
|
|
112
|
+
fi
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# 3. Stub-return shapes (throw new Error("not implemented") is caught above;
|
|
117
|
+
# catch the bare "return null; // TODO" combo and an explicit stub throw).
|
|
118
|
+
if [[ -z "$MATCH" ]]; then
|
|
119
|
+
if hit=$(printf '%s' "$CONTENT" | grep -niE 'throw new Error\(["'"'"']not implemented' 2>/dev/null | head -1); then
|
|
120
|
+
if [[ -n "$hit" ]]; then
|
|
121
|
+
MATCH="$hit"
|
|
122
|
+
PATTERN_DESC="explicit not-implemented stub throw"
|
|
123
|
+
fi
|
|
124
|
+
fi
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
[[ -z "$MATCH" ]] && exit 0
|
|
128
|
+
|
|
129
|
+
# Trim the matched line for the message (strip leading whitespace, cap length).
|
|
130
|
+
LINE_TEXT=$(printf '%s' "$MATCH" | sed 's/^[0-9]*://; s/^[[:space:]]*//' | cut -c1-120)
|
|
131
|
+
|
|
132
|
+
BASE="Shortcut-language advisory in ${FILE_PATH}: ${PATTERN_DESC} — \"${LINE_TEXT}\". CAWS doctrine (\"No fake implementations\") asks for complete code in committed source, not TODO/placeholder stubs."
|
|
133
|
+
MSG1="${BASE} (strike 1 of 3 — advisory.)"
|
|
134
|
+
MSG2="${BASE} (strike 2 of 3 — please resolve before continuing.)"
|
|
135
|
+
MSG3="${BASE} (strike 3 — blocked. Replace the placeholder/stub with a real implementation, or move the work to a tracked spec.)"
|
|
136
|
+
|
|
137
|
+
guard_enforce_progressive_strikes \
|
|
138
|
+
"${HOOK_SESSION_ID:-unknown}" \
|
|
139
|
+
"shortcut_language" \
|
|
140
|
+
"${HOOK_CWD:-}" \
|
|
141
|
+
"$MSG1" "$MSG2" "$MSG3"
|
|
142
|
+
|
|
143
|
+
# guard_enforce_progressive_strikes emits the decision JSON. For strikes 1/2
|
|
144
|
+
# it is an allow/ask (exit 0). For strike 3 it emits a block decision; exit 0
|
|
145
|
+
# is correct for PostToolUse (the tool already ran) — the block decision in
|
|
146
|
+
# the JSON is what Claude Code honors.
|
|
147
|
+
exit 0
|
|
@@ -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: 11
|
|
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: 11
|
|
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
|