@paths.design/caws-cli 9.3.2 → 10.1.0
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 +71 -32
- package/dist/budget-derivation.js +221 -74
- package/dist/commands/archive.js +67 -28
- package/dist/commands/burnup.js +20 -11
- package/dist/commands/diagnose.js +34 -22
- package/dist/commands/evaluate.js +41 -15
- package/dist/commands/gates.js +149 -0
- package/dist/commands/init.js +150 -19
- package/dist/commands/iterate.js +81 -4
- package/dist/commands/parallel.js +4 -0
- package/dist/commands/plan.js +9 -19
- package/dist/commands/provenance.js +53 -17
- package/dist/commands/quality-monitor.js +64 -45
- package/dist/commands/scope.js +264 -0
- package/dist/commands/sidecar.js +74 -0
- package/dist/commands/specs.js +381 -45
- package/dist/commands/status.js +117 -9
- package/dist/commands/templates.js +0 -8
- package/dist/commands/tutorial.js +10 -9
- package/dist/commands/validate.js +70 -6
- package/dist/commands/verify-acs.js +48 -76
- package/dist/commands/waivers.js +212 -13
- package/dist/commands/worktree.js +131 -26
- package/dist/error-handler.js +2 -13
- package/dist/gates/budget-limit.js +121 -0
- package/dist/gates/feedback.js +260 -0
- package/dist/gates/format.js +179 -0
- package/dist/gates/god-object.js +117 -0
- package/dist/gates/pipeline.js +167 -0
- package/dist/gates/scope-boundary.js +93 -0
- package/dist/gates/spec-completeness.js +109 -0
- package/dist/gates/todo-detection.js +205 -0
- package/dist/index.js +157 -151
- package/dist/parallel/parallel-manager.js +3 -3
- package/dist/policy/PolicyManager.js +51 -17
- package/dist/scaffold/claude-hooks.js +24 -1
- package/dist/scaffold/git-hooks.js +45 -102
- package/dist/scaffold/index.js +4 -3
- package/dist/session/session-manager.js +105 -14
- package/dist/sidecars/index.js +33 -0
- package/dist/sidecars/listeners.js +40 -0
- package/dist/sidecars/provenance-summary.js +238 -0
- package/dist/sidecars/quality-gaps.js +258 -0
- package/dist/sidecars/schema.js +149 -0
- package/dist/sidecars/spec-drift.js +151 -0
- package/dist/sidecars/waiver-draft.js +176 -0
- package/dist/templates/.caws/schemas/policy.schema.json +112 -0
- package/dist/templates/.caws/schemas/scope.schema.json +3 -3
- package/dist/templates/.caws/schemas/waivers.schema.json +96 -20
- package/dist/templates/.caws/schemas/working-spec.schema.json +264 -57
- package/dist/templates/.caws/schemas/worktrees.schema.json +3 -1
- package/dist/templates/.caws/templates/working-spec.template.yml +10 -4
- package/dist/templates/.caws/tools/scope-guard.js +66 -15
- package/dist/templates/.claude/README.md +1 -1
- package/dist/templates/.claude/hooks/audit.sh +0 -0
- package/dist/templates/.claude/hooks/block-dangerous.sh +52 -11
- package/dist/templates/.claude/hooks/classify_command.py +592 -0
- package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
- package/dist/templates/.claude/hooks/protected-paths.sh +39 -0
- package/dist/templates/.claude/hooks/quality-check.sh +23 -10
- package/dist/templates/.claude/hooks/scope-guard.sh +136 -55
- package/dist/templates/.claude/hooks/session-caws-status.sh +2 -2
- package/dist/templates/.claude/hooks/session-log.sh +76 -3
- package/dist/templates/.claude/hooks/stop-worktree-check.sh +1 -1
- package/dist/templates/.claude/hooks/test_classify_command.py +370 -0
- package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
- package/dist/templates/.claude/hooks/worktree-guard.sh +2 -2
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +97 -4
- package/dist/templates/.claude/settings.json +31 -0
- package/dist/templates/.cursor/hooks/caws-quality-check.sh +4 -4
- package/dist/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
- package/dist/templates/.cursor/hooks/session-log.sh +924 -0
- package/dist/templates/.cursor/hooks.json +25 -0
- package/dist/templates/.cursor/rules/02-quality-gates.mdc +3 -5
- package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
- package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
- package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
- package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
- package/dist/templates/.github/copilot-instructions.md +5 -5
- package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
- package/dist/templates/.junie/guidelines.md +2 -2
- package/dist/templates/.vscode/settings.json +3 -1
- package/dist/templates/.windsurf/rules/caws-quality-standards.md +2 -2
- package/dist/templates/.windsurf/workflows/caws-guided-development.md +3 -3
- package/dist/templates/CLAUDE.md +77 -8
- package/dist/templates/agents.md +50 -9
- package/dist/templates/docs/README.md +8 -7
- package/dist/templates/scripts/new_feature.sh +80 -0
- package/dist/test-analysis.js +43 -30
- package/dist/tool-loader.js +1 -1
- package/dist/utils/agent-session.js +202 -0
- package/dist/utils/detection.js +8 -2
- package/dist/utils/event-log.js +584 -0
- package/dist/utils/event-renderer.js +521 -0
- package/dist/utils/finalization.js +7 -6
- package/dist/utils/gitignore-updater.js +3 -0
- package/dist/utils/lifecycle-events.js +94 -0
- package/dist/utils/quality-gates-utils.js +29 -44
- package/dist/utils/schema-validator.js +50 -0
- package/dist/utils/spec-resolver.js +93 -21
- package/dist/utils/working-state.js +530 -0
- package/dist/validation/spec-validation.js +191 -31
- package/dist/waivers-manager.js +144 -6
- package/dist/worktree/worktree-manager.js +598 -95
- package/package.json +9 -8
- package/templates/.caws/schemas/policy.schema.json +112 -0
- package/templates/.caws/schemas/scope.schema.json +3 -3
- package/templates/.caws/schemas/waivers.schema.json +96 -20
- package/templates/.caws/schemas/working-spec.schema.json +264 -57
- package/templates/.caws/schemas/worktrees.schema.json +3 -1
- package/templates/.caws/templates/working-spec.template.yml +10 -4
- package/templates/.caws/tools/scope-guard.js +66 -15
- package/templates/.claude/README.md +1 -1
- package/templates/.claude/hooks/block-dangerous.sh +52 -11
- package/templates/.claude/hooks/classify_command.py +592 -0
- package/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
- package/templates/.claude/hooks/protected-paths.sh +39 -0
- package/templates/.claude/hooks/quality-check.sh +23 -10
- package/templates/.claude/hooks/scope-guard.sh +136 -55
- package/templates/.claude/hooks/session-caws-status.sh +2 -2
- package/templates/.claude/hooks/session-log.sh +76 -3
- package/templates/.claude/hooks/stop-worktree-check.sh +1 -1
- package/templates/.claude/hooks/test_classify_command.py +370 -0
- package/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
- package/templates/.claude/hooks/worktree-guard.sh +2 -2
- package/templates/.claude/hooks/worktree-write-guard.sh +97 -4
- package/templates/.claude/settings.json +31 -0
- package/templates/.cursor/hooks/caws-quality-check.sh +4 -4
- package/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
- package/templates/.cursor/hooks/session-log.sh +924 -0
- package/templates/.cursor/hooks.json +25 -0
- package/templates/.cursor/rules/02-quality-gates.mdc +3 -5
- package/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
- package/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
- package/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
- package/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
- package/templates/.github/copilot-instructions.md +5 -5
- package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
- package/templates/.junie/guidelines.md +2 -2
- package/templates/.vscode/settings.json +3 -1
- package/templates/.windsurf/rules/caws-quality-standards.md +2 -2
- package/templates/.windsurf/workflows/caws-guided-development.md +3 -3
- package/templates/CLAUDE.md +77 -8
- package/templates/{AGENTS.md → agents.md} +50 -9
- package/templates/docs/README.md +8 -7
- package/templates/scripts/new_feature.sh +80 -0
- package/dist/budget-derivation.d.ts +0 -74
- package/dist/budget-derivation.d.ts.map +0 -1
- package/dist/cicd-optimizer.d.ts +0 -142
- package/dist/cicd-optimizer.d.ts.map +0 -1
- package/dist/commands/archive.d.ts +0 -51
- package/dist/commands/archive.d.ts.map +0 -1
- package/dist/commands/burnup.d.ts +0 -6
- package/dist/commands/burnup.d.ts.map +0 -1
- package/dist/commands/diagnose.d.ts +0 -52
- package/dist/commands/diagnose.d.ts.map +0 -1
- package/dist/commands/evaluate.d.ts +0 -8
- package/dist/commands/evaluate.d.ts.map +0 -1
- package/dist/commands/init.d.ts +0 -5
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/iterate.d.ts +0 -8
- package/dist/commands/iterate.d.ts.map +0 -1
- package/dist/commands/mode.d.ts +0 -25
- package/dist/commands/mode.d.ts.map +0 -1
- package/dist/commands/parallel.d.ts +0 -7
- package/dist/commands/parallel.d.ts.map +0 -1
- package/dist/commands/plan.d.ts +0 -49
- package/dist/commands/plan.d.ts.map +0 -1
- package/dist/commands/provenance.d.ts +0 -32
- package/dist/commands/provenance.d.ts.map +0 -1
- package/dist/commands/quality-gates.d.ts +0 -6
- package/dist/commands/quality-gates.d.ts.map +0 -1
- package/dist/commands/quality-gates.js +0 -444
- package/dist/commands/quality-monitor.d.ts +0 -17
- package/dist/commands/quality-monitor.d.ts.map +0 -1
- package/dist/commands/session.d.ts +0 -7
- package/dist/commands/session.d.ts.map +0 -1
- package/dist/commands/specs.d.ts +0 -77
- package/dist/commands/specs.d.ts.map +0 -1
- package/dist/commands/status.d.ts +0 -44
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/templates.d.ts +0 -74
- package/dist/commands/templates.d.ts.map +0 -1
- package/dist/commands/tool.d.ts +0 -13
- package/dist/commands/tool.d.ts.map +0 -1
- package/dist/commands/troubleshoot.d.ts +0 -8
- package/dist/commands/troubleshoot.d.ts.map +0 -1
- package/dist/commands/troubleshoot.js +0 -104
- package/dist/commands/tutorial.d.ts +0 -55
- package/dist/commands/tutorial.d.ts.map +0 -1
- package/dist/commands/validate.d.ts +0 -15
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/waivers.d.ts +0 -8
- package/dist/commands/waivers.d.ts.map +0 -1
- package/dist/commands/workflow.d.ts +0 -85
- package/dist/commands/workflow.d.ts.map +0 -1
- package/dist/commands/worktree.d.ts +0 -7
- package/dist/commands/worktree.d.ts.map +0 -1
- package/dist/config/index.d.ts +0 -29
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/lite-scope.d.ts +0 -33
- package/dist/config/lite-scope.d.ts.map +0 -1
- package/dist/config/modes.d.ts +0 -264
- package/dist/config/modes.d.ts.map +0 -1
- package/dist/constants/spec-types.d.ts +0 -93
- package/dist/constants/spec-types.d.ts.map +0 -1
- package/dist/error-handler.d.ts +0 -151
- package/dist/error-handler.d.ts.map +0 -1
- package/dist/generators/jest-config-generator.d.ts +0 -32
- package/dist/generators/jest-config-generator.d.ts.map +0 -1
- package/dist/generators/jest-config.d.ts +0 -32
- package/dist/generators/jest-config.d.ts.map +0 -1
- package/dist/generators/jest-config.js +0 -242
- package/dist/generators/working-spec.d.ts +0 -13
- package/dist/generators/working-spec.d.ts.map +0 -1
- package/dist/index-new.d.ts +0 -5
- package/dist/index-new.d.ts.map +0 -1
- package/dist/index-new.js +0 -317
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.backup +0 -4711
- package/dist/minimal-cli.d.ts +0 -3
- package/dist/minimal-cli.d.ts.map +0 -1
- package/dist/parallel/parallel-manager.d.ts +0 -67
- package/dist/parallel/parallel-manager.d.ts.map +0 -1
- package/dist/policy/PolicyManager.d.ts +0 -104
- package/dist/policy/PolicyManager.d.ts.map +0 -1
- package/dist/scaffold/claude-hooks.d.ts +0 -28
- package/dist/scaffold/claude-hooks.d.ts.map +0 -1
- package/dist/scaffold/cursor-hooks.d.ts +0 -7
- package/dist/scaffold/cursor-hooks.d.ts.map +0 -1
- package/dist/scaffold/git-hooks.d.ts +0 -38
- package/dist/scaffold/git-hooks.d.ts.map +0 -1
- package/dist/scaffold/index.d.ts +0 -17
- package/dist/scaffold/index.d.ts.map +0 -1
- package/dist/session/session-manager.d.ts +0 -94
- package/dist/session/session-manager.d.ts.map +0 -1
- package/dist/spec/SpecFileManager.d.ts +0 -146
- package/dist/spec/SpecFileManager.d.ts.map +0 -1
- package/dist/templates/.cursor/hooks/caws-tool-validation.sh +0 -121
- package/dist/templates/.github/copilot/instructions.md +0 -311
- package/dist/test-analysis.d.ts +0 -231
- package/dist/test-analysis.d.ts.map +0 -1
- package/dist/tool-interface.d.ts +0 -236
- package/dist/tool-interface.d.ts.map +0 -1
- package/dist/tool-loader.d.ts +0 -77
- package/dist/tool-loader.d.ts.map +0 -1
- package/dist/tool-validator.d.ts +0 -72
- package/dist/tool-validator.d.ts.map +0 -1
- package/dist/utils/async-utils.d.ts +0 -73
- package/dist/utils/async-utils.d.ts.map +0 -1
- package/dist/utils/command-wrapper.d.ts +0 -66
- package/dist/utils/command-wrapper.d.ts.map +0 -1
- package/dist/utils/detection.d.ts +0 -14
- package/dist/utils/detection.d.ts.map +0 -1
- package/dist/utils/error-categories.d.ts +0 -52
- package/dist/utils/error-categories.d.ts.map +0 -1
- package/dist/utils/finalization.d.ts +0 -17
- package/dist/utils/finalization.d.ts.map +0 -1
- package/dist/utils/git-lock.d.ts +0 -13
- package/dist/utils/git-lock.d.ts.map +0 -1
- package/dist/utils/gitignore-updater.d.ts +0 -39
- package/dist/utils/gitignore-updater.d.ts.map +0 -1
- package/dist/utils/ide-detection.d.ts +0 -89
- package/dist/utils/ide-detection.d.ts.map +0 -1
- package/dist/utils/project-analysis.d.ts +0 -34
- package/dist/utils/project-analysis.d.ts.map +0 -1
- package/dist/utils/promise-utils.d.ts +0 -30
- package/dist/utils/promise-utils.d.ts.map +0 -1
- package/dist/utils/quality-gates-utils.d.ts +0 -49
- package/dist/utils/quality-gates-utils.d.ts.map +0 -1
- package/dist/utils/quality-gates.d.ts +0 -49
- package/dist/utils/quality-gates.d.ts.map +0 -1
- package/dist/utils/quality-gates.js +0 -402
- package/dist/utils/spec-resolver.d.ts +0 -80
- package/dist/utils/spec-resolver.d.ts.map +0 -1
- package/dist/utils/typescript-detector.d.ts +0 -66
- package/dist/utils/typescript-detector.d.ts.map +0 -1
- package/dist/utils/yaml-validation.d.ts +0 -32
- package/dist/utils/yaml-validation.d.ts.map +0 -1
- package/dist/validation/spec-validation.d.ts +0 -43
- package/dist/validation/spec-validation.d.ts.map +0 -1
- package/dist/waivers-manager.d.ts +0 -167
- package/dist/waivers-manager.d.ts.map +0 -1
- package/dist/worktree/worktree-manager.d.ts +0 -54
- package/dist/worktree/worktree-manager.d.ts.map +0 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Document Frontmatter Check Hook for Claude Code
|
|
3
|
+
# Warns when docs/**/*.md files are written/edited without proper frontmatter.
|
|
4
|
+
# Advisory only — does not block.
|
|
5
|
+
#
|
|
6
|
+
# Validates YAML frontmatter with required fields, authority/status enums,
|
|
7
|
+
# governs requirements for high-authority docs, and verified_at_commit for
|
|
8
|
+
# implementation-state claims.
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
INPUT=$(cat)
|
|
13
|
+
|
|
14
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
15
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
16
|
+
|
|
17
|
+
# Only check Write and Edit tools
|
|
18
|
+
if [[ "$TOOL_NAME" != "Write" ]] && [[ "$TOOL_NAME" != "Edit" ]]; then
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
if [[ -z "$FILE_PATH" ]]; then
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Only check .md files under docs/
|
|
27
|
+
if [[ ! "$FILE_PATH" =~ docs/.*\.md$ ]]; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Skip exempt filenames
|
|
32
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
33
|
+
if [[ "$BASENAME" == "README.md" ]] || [[ "$BASENAME" == "INDEX.md" ]] || [[ "$BASENAME" == "index.md" ]] || [[ "$BASENAME" == "00_INDEX.md" ]]; then
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Skip archive and templates directories
|
|
38
|
+
if [[ "$FILE_PATH" =~ docs/archive/ ]] || [[ "$FILE_PATH" =~ docs/templates/ ]]; then
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Skip ephemeral (gitignored, not governed)
|
|
43
|
+
if [[ "$FILE_PATH" =~ docs/ephemeral/ ]]; then
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Check if file exists (Write creates it, Edit modifies it)
|
|
48
|
+
if [[ ! -f "$FILE_PATH" ]]; then
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# --- Frontmatter validation ---
|
|
53
|
+
|
|
54
|
+
# V1: Check for frontmatter delimiters
|
|
55
|
+
FIRST_LINE=$(head -1 "$FILE_PATH" 2>/dev/null || echo "")
|
|
56
|
+
if [[ "$FIRST_LINE" != "---" ]]; then
|
|
57
|
+
echo '{
|
|
58
|
+
"hookSpecificOutput": {
|
|
59
|
+
"hookEventName": "PostToolUse",
|
|
60
|
+
"additionalContext": "Doc governance (V1): '"$FILE_PATH"' is missing YAML frontmatter. All docs under docs/ (except README.md, archive/, templates/) must start with --- delimiters containing doc_id, authority, status, title, owner, and updated fields."
|
|
61
|
+
}
|
|
62
|
+
}'
|
|
63
|
+
exit 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Extract frontmatter block (between first and second ---)
|
|
67
|
+
FRONTMATTER=$(awk 'NR==1 && /^---$/{found=1; next} found && /^---$/{exit} found{print}' "$FILE_PATH")
|
|
68
|
+
|
|
69
|
+
if [[ -z "$FRONTMATTER" ]]; then
|
|
70
|
+
echo '{
|
|
71
|
+
"hookSpecificOutput": {
|
|
72
|
+
"hookEventName": "PostToolUse",
|
|
73
|
+
"additionalContext": "Doc governance (V1): '"$FILE_PATH"' has opening --- but no closing --- for frontmatter block."
|
|
74
|
+
}
|
|
75
|
+
}'
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# V2: Check required fields
|
|
80
|
+
MISSING=""
|
|
81
|
+
for field in doc_id authority status title owner updated; do
|
|
82
|
+
if ! echo "$FRONTMATTER" | grep -q "^${field}:"; then
|
|
83
|
+
MISSING="${MISSING} ${field}"
|
|
84
|
+
fi
|
|
85
|
+
done
|
|
86
|
+
|
|
87
|
+
if [[ -n "$MISSING" ]]; then
|
|
88
|
+
echo '{
|
|
89
|
+
"hookSpecificOutput": {
|
|
90
|
+
"hookEventName": "PostToolUse",
|
|
91
|
+
"additionalContext": "Doc governance (V2): '"$FILE_PATH"' is missing required frontmatter fields:'"$MISSING"'."
|
|
92
|
+
}
|
|
93
|
+
}'
|
|
94
|
+
exit 0
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# V2: Check authority value
|
|
98
|
+
AUTHORITY=$(echo "$FRONTMATTER" | grep "^authority:" | head -1 | sed 's/^authority: *//' | tr -d '"' | tr -d "'")
|
|
99
|
+
case "$AUTHORITY" in
|
|
100
|
+
canonical|policy|architecture|adr|spec|roadmap|reference|working|ephemeral)
|
|
101
|
+
;;
|
|
102
|
+
*)
|
|
103
|
+
echo '{
|
|
104
|
+
"hookSpecificOutput": {
|
|
105
|
+
"hookEventName": "PostToolUse",
|
|
106
|
+
"additionalContext": "Doc governance (V2): '"$FILE_PATH"' has invalid authority '"'"''"$AUTHORITY"''"'"'. Must be one of: canonical, policy, architecture, adr, spec, roadmap, reference, working, ephemeral."
|
|
107
|
+
}
|
|
108
|
+
}'
|
|
109
|
+
exit 0
|
|
110
|
+
;;
|
|
111
|
+
esac
|
|
112
|
+
|
|
113
|
+
# V2: Check status value
|
|
114
|
+
STATUS=$(echo "$FRONTMATTER" | grep "^status:" | head -1 | sed 's/^status: *//' | tr -d '"' | tr -d "'")
|
|
115
|
+
case "$STATUS" in
|
|
116
|
+
draft|active|implemented|proven|superseded|archived)
|
|
117
|
+
;;
|
|
118
|
+
*)
|
|
119
|
+
echo '{
|
|
120
|
+
"hookSpecificOutput": {
|
|
121
|
+
"hookEventName": "PostToolUse",
|
|
122
|
+
"additionalContext": "Doc governance (V2): '"$FILE_PATH"' has invalid status '"'"''"$STATUS"''"'"'. Must be one of: draft, active, implemented, proven, superseded, archived."
|
|
123
|
+
}
|
|
124
|
+
}'
|
|
125
|
+
exit 0
|
|
126
|
+
;;
|
|
127
|
+
esac
|
|
128
|
+
|
|
129
|
+
# V3: Check governs for high-authority docs
|
|
130
|
+
case "$AUTHORITY" in
|
|
131
|
+
canonical|architecture|adr|spec)
|
|
132
|
+
if ! echo "$FRONTMATTER" | grep -q "^governs:"; then
|
|
133
|
+
echo '{
|
|
134
|
+
"hookSpecificOutput": {
|
|
135
|
+
"hookEventName": "PostToolUse",
|
|
136
|
+
"additionalContext": "Doc governance (V3): '"$FILE_PATH"' has authority '"'"''"$AUTHORITY"''"'"' but no governs section. Docs with authority canonical/architecture/adr/spec must declare what they govern (modules, schemas, or specs)."
|
|
137
|
+
}
|
|
138
|
+
}'
|
|
139
|
+
exit 0
|
|
140
|
+
fi
|
|
141
|
+
;;
|
|
142
|
+
esac
|
|
143
|
+
|
|
144
|
+
# V4: Check verified_at_commit for implementation-state claims
|
|
145
|
+
case "$STATUS" in
|
|
146
|
+
implemented|proven)
|
|
147
|
+
if ! echo "$FRONTMATTER" | grep -q "^verified_at_commit:"; then
|
|
148
|
+
echo '{
|
|
149
|
+
"hookSpecificOutput": {
|
|
150
|
+
"hookEventName": "PostToolUse",
|
|
151
|
+
"additionalContext": "Doc governance (V4): '"$FILE_PATH"' has status '"'"''"$STATUS"''"'"' but no verified_at_commit. Docs claiming implementation state must declare the commit SHA where claims were verified."
|
|
152
|
+
}
|
|
153
|
+
}'
|
|
154
|
+
exit 0
|
|
155
|
+
fi
|
|
156
|
+
;;
|
|
157
|
+
esac
|
|
158
|
+
|
|
159
|
+
# V5: Check superseded_by for superseded docs
|
|
160
|
+
if [[ "$STATUS" == "superseded" ]]; then
|
|
161
|
+
if ! echo "$FRONTMATTER" | grep -q "^superseded_by:"; then
|
|
162
|
+
echo '{
|
|
163
|
+
"hookSpecificOutput": {
|
|
164
|
+
"hookEventName": "PostToolUse",
|
|
165
|
+
"additionalContext": "Doc governance (V5): '"$FILE_PATH"' has status '"'"'superseded'"'"' but no superseded_by. Superseded docs must declare their replacement doc_id."
|
|
166
|
+
}
|
|
167
|
+
}'
|
|
168
|
+
exit 0
|
|
169
|
+
fi
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# All checks passed
|
|
173
|
+
exit 0
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS Protected Paths Guard for Claude Code
|
|
3
|
+
# Blocks direct Write/Edit access to guard code and guard state.
|
|
4
|
+
# @author @darianrosebrook
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
INPUT=$(cat)
|
|
9
|
+
|
|
10
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
11
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
12
|
+
|
|
13
|
+
case "$TOOL_NAME" in
|
|
14
|
+
Write|Edit) ;;
|
|
15
|
+
*) exit 0 ;;
|
|
16
|
+
esac
|
|
17
|
+
|
|
18
|
+
if [[ -z "$FILE_PATH" ]]; then
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# If you are reading this because a write was blocked, do not edit hook files or
|
|
23
|
+
# strike-state files to bypass a guard. Switch into the correct worktree, fix the
|
|
24
|
+
# active spec scope, or ask the user if the guard itself is wrong.
|
|
25
|
+
case "$FILE_PATH" in
|
|
26
|
+
*/.claude/hooks/*)
|
|
27
|
+
echo "BLOCKED: $FILE_PATH is protected." >&2
|
|
28
|
+
echo "Ask the user for permission before editing Claude hook scripts." >&2
|
|
29
|
+
exit 1
|
|
30
|
+
;;
|
|
31
|
+
*/.claude/logs/guard-strikes-*.json)
|
|
32
|
+
echo "BLOCKED: $FILE_PATH is protected guard state." >&2
|
|
33
|
+
echo "Do not reset or edit strike counters to bypass enforcement." >&2
|
|
34
|
+
echo "Switch into the correct worktree, update the active CAWS spec scope, or ask the user for direction instead." >&2
|
|
35
|
+
exit 2
|
|
36
|
+
;;
|
|
37
|
+
esac
|
|
38
|
+
|
|
39
|
+
exit 0
|
|
@@ -29,25 +29,39 @@ fi
|
|
|
29
29
|
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
30
30
|
|
|
31
31
|
# Check if we're in a CAWS project
|
|
32
|
-
if [[ ! -f "$PROJECT_DIR/.caws/working-spec.yaml" ]]; then
|
|
32
|
+
if [[ ! -f "$PROJECT_DIR/.caws/working-spec.yaml" ]] && [[ ! -d "$PROJECT_DIR/.caws/specs" ]]; then
|
|
33
33
|
exit 0
|
|
34
34
|
fi
|
|
35
35
|
|
|
36
36
|
# Check if CAWS CLI is available
|
|
37
37
|
if ! command -v caws &> /dev/null; then
|
|
38
|
-
# Suggest installing CAWS
|
|
39
38
|
echo '{
|
|
40
39
|
"hookSpecificOutput": {
|
|
41
40
|
"hookEventName": "PostToolUse",
|
|
42
|
-
"additionalContext": "CAWS CLI not available. Consider installing with: npm install -g @caws
|
|
41
|
+
"additionalContext": "CAWS CLI not available. Consider installing with: npm install -g @paths.design/caws-cli"
|
|
43
42
|
}
|
|
44
43
|
}'
|
|
45
44
|
exit 0
|
|
46
45
|
fi
|
|
47
46
|
|
|
48
|
-
# Run
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
# Run quality gates via the unified pipeline
|
|
48
|
+
RESULT=$(caws gates run --context=edit --file "$FILE_PATH" --json --quiet 2>&1) || GATE_EXIT=$?
|
|
49
|
+
|
|
50
|
+
if [ -z "$RESULT" ]; then
|
|
51
|
+
# No output — gates command not available or errored
|
|
52
|
+
echo '{
|
|
53
|
+
"hookSpecificOutput": {
|
|
54
|
+
"hookEventName": "PostToolUse",
|
|
55
|
+
"additionalContext": "Quality gates did not produce output (exit '"${GATE_EXIT:-0}"'). Run '\''caws gates run'\'' for details."
|
|
56
|
+
}
|
|
57
|
+
}'
|
|
58
|
+
exit 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Check if gates passed
|
|
62
|
+
PASSED=$(echo "$RESULT" | jq -r '.passed // true' 2>/dev/null)
|
|
63
|
+
|
|
64
|
+
if [ "$PASSED" = "true" ]; then
|
|
51
65
|
echo '{
|
|
52
66
|
"hookSpecificOutput": {
|
|
53
67
|
"hookEventName": "PostToolUse",
|
|
@@ -55,13 +69,12 @@ if caws quality-gates --context=commit --quiet 2>/dev/null; then
|
|
|
55
69
|
}
|
|
56
70
|
}'
|
|
57
71
|
else
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
VIOLATIONS=$(caws quality-gates --context=commit --json 2>/dev/null | jq -r '.violations[:3] | .[] | "- \(.gate): \(.message)"' 2>/dev/null || echo "Run 'caws quality-gates' for details")
|
|
72
|
+
# Extract top 3 gate failure messages
|
|
73
|
+
VIOLATIONS=$(echo "$RESULT" | jq -r '[.gates[] | select(.status == "fail") | "- \(.name): \(.messages[0] // "failed")"] | .[0:3] | .[]' 2>/dev/null || echo "Run 'caws gates run' for details")
|
|
61
74
|
|
|
62
75
|
echo '{
|
|
63
76
|
"decision": "block",
|
|
64
|
-
"reason": "Quality gate violations detected. Please address the following issues before continuing:\n'"$VIOLATIONS"'\n\nRun '\''caws
|
|
77
|
+
"reason": "Quality gate violations detected. Please address the following issues before continuing:\n'"$VIOLATIONS"'\n\nRun '\''caws gates run'\'' for full details."
|
|
65
78
|
}'
|
|
66
79
|
fi
|
|
67
80
|
|
|
@@ -123,27 +123,28 @@ if [[ ! -f "$SPEC_FILE" ]] && [[ -f "$SCOPE_FILE" ]]; then
|
|
|
123
123
|
}
|
|
124
124
|
" 2>&1)
|
|
125
125
|
|
|
126
|
+
|
|
127
|
+
if [[ "$LITE_CHECK" == error:* ]]; then
|
|
128
|
+
ERROR_MSG="${LITE_CHECK#error:}"
|
|
129
|
+
echo "BLOCKED: Scope check failed — cannot verify file is in scope" >&2
|
|
130
|
+
echo " Error: $ERROR_MSG" >&2
|
|
131
|
+
exit 2
|
|
132
|
+
fi
|
|
133
|
+
|
|
126
134
|
if [[ "$LITE_CHECK" == banned:* ]]; then
|
|
127
135
|
PATTERN="${LITE_CHECK#banned:}"
|
|
128
|
-
echo
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"permissionDecisionReason": "This file ('"$REL_PATH"') matches a banned pattern ('"$PATTERN"') in .caws/scope.json. Creating files with this pattern is blocked to prevent file sprawl."
|
|
133
|
-
}
|
|
134
|
-
}'
|
|
135
|
-
exit 0
|
|
136
|
+
echo "BLOCKED: $REL_PATH matches banned pattern ($PATTERN) in .caws/scope.json"
|
|
137
|
+
echo " Scope allows: files not matching banned patterns"
|
|
138
|
+
echo " To modify scope, update bannedPatterns in .caws/scope.json"
|
|
139
|
+
exit 2
|
|
136
140
|
fi
|
|
137
141
|
|
|
138
142
|
if [[ "$LITE_CHECK" == "not_allowed" ]]; then
|
|
139
|
-
echo
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
}'
|
|
146
|
-
exit 0
|
|
143
|
+
ALLOWED_DIRS=$(node -e "const s=JSON.parse(require('fs').readFileSync('$SCOPE_FILE','utf8')); console.log((s.allowedDirectories||[]).join(', '))" 2>/dev/null || echo "unknown")
|
|
144
|
+
echo "BLOCKED: $REL_PATH is outside allowed directories"
|
|
145
|
+
echo " Scope allows: $ALLOWED_DIRS"
|
|
146
|
+
echo " To modify scope, update allowedDirectories in .caws/scope.json"
|
|
147
|
+
exit 2
|
|
147
148
|
fi
|
|
148
149
|
|
|
149
150
|
# File is allowed - exit normally
|
|
@@ -203,31 +204,83 @@ if command -v node >/dev/null 2>&1; then
|
|
|
203
204
|
process.exit(0);
|
|
204
205
|
}
|
|
205
206
|
|
|
206
|
-
|
|
207
|
-
const specs = [];
|
|
207
|
+
const projectDir = '$PROJECT_DIR';
|
|
208
208
|
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
209
|
+
// --- Authoritative spec detection ---
|
|
210
|
+
// If we are inside a worktree with a bound specId, ONLY check that spec.
|
|
211
|
+
// This prevents unrelated specs from blocking writes via broad scope.out.
|
|
212
|
+
let authoritativeSpec = null;
|
|
213
|
+
let mode = 'union';
|
|
214
|
+
|
|
215
|
+
const registryPath = path.join(projectDir, '.caws', 'worktrees.json');
|
|
216
|
+
const cwd = process.cwd();
|
|
217
|
+
const worktreesBase = path.join(projectDir, '.caws', 'worktrees');
|
|
218
|
+
|
|
219
|
+
if (cwd.startsWith(worktreesBase + '/')) {
|
|
220
|
+
const relative = cwd.slice(worktreesBase.length + 1);
|
|
221
|
+
const worktreeName = relative.split('/')[0];
|
|
222
|
+
|
|
223
|
+
if (worktreeName && fs.existsSync(registryPath)) {
|
|
224
|
+
try {
|
|
225
|
+
const reg = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
226
|
+
const entry = reg.worktrees && reg.worktrees[worktreeName];
|
|
227
|
+
|
|
228
|
+
if (entry && entry.specId) {
|
|
229
|
+
// Try to load the bound spec
|
|
230
|
+
const specsDir = '$SPECS_DIR';
|
|
231
|
+
const specCandidates = [
|
|
232
|
+
path.join(specsDir, entry.specId + '.yaml'),
|
|
233
|
+
path.join(specsDir, entry.specId + '.yml'),
|
|
234
|
+
];
|
|
235
|
+
for (const candidate of specCandidates) {
|
|
236
|
+
if (fs.existsSync(candidate)) {
|
|
237
|
+
try {
|
|
238
|
+
const s = yaml.load(fs.readFileSync(candidate, 'utf8'));
|
|
239
|
+
if (s && !TERMINAL.has(s.status)) {
|
|
240
|
+
// Verify mutual binding: spec must also reference this worktree
|
|
241
|
+
if (s.worktree === worktreeName) {
|
|
242
|
+
authoritativeSpec = { source: path.basename(candidate), spec: s };
|
|
243
|
+
mode = 'authoritative';
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} catch (_) {}
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch (_) {}
|
|
252
|
+
}
|
|
218
253
|
}
|
|
219
254
|
|
|
220
|
-
//
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
255
|
+
// --- Collect specs based on mode ---
|
|
256
|
+
const specs = [];
|
|
257
|
+
|
|
258
|
+
if (authoritativeSpec) {
|
|
259
|
+
// Authoritative: only the bound spec matters
|
|
260
|
+
specs.push(authoritativeSpec);
|
|
261
|
+
} else {
|
|
262
|
+
// Union: load all active specs
|
|
263
|
+
const mainSpec = '$SPEC_FILE';
|
|
264
|
+
if (fs.existsSync(mainSpec)) {
|
|
224
265
|
try {
|
|
225
|
-
const s = yaml.load(fs.readFileSync(
|
|
266
|
+
const s = yaml.load(fs.readFileSync(mainSpec, 'utf8'));
|
|
226
267
|
if (s && !TERMINAL.has(s.status)) {
|
|
227
|
-
specs.push({ source:
|
|
268
|
+
specs.push({ source: 'working-spec', spec: s });
|
|
228
269
|
}
|
|
229
270
|
} catch (_) {}
|
|
230
271
|
}
|
|
272
|
+
|
|
273
|
+
const specsDir = '$SPECS_DIR';
|
|
274
|
+
if (fs.existsSync(specsDir)) {
|
|
275
|
+
for (const f of fs.readdirSync(specsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))) {
|
|
276
|
+
try {
|
|
277
|
+
const s = yaml.load(fs.readFileSync(path.join(specsDir, f), 'utf8'));
|
|
278
|
+
if (s && !TERMINAL.has(s.status)) {
|
|
279
|
+
specs.push({ source: f, spec: s });
|
|
280
|
+
}
|
|
281
|
+
} catch (_) {}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
231
284
|
}
|
|
232
285
|
|
|
233
286
|
// No active specs — allow everything
|
|
@@ -236,18 +289,18 @@ if command -v node >/dev/null 2>&1; then
|
|
|
236
289
|
process.exit(0);
|
|
237
290
|
}
|
|
238
291
|
|
|
239
|
-
// Check scope.out
|
|
292
|
+
// Check scope.out — any match blocks
|
|
240
293
|
for (const { source, spec } of specs) {
|
|
241
294
|
for (const pattern of (spec.scope?.out || [])) {
|
|
242
295
|
const regex = globToRegex(pattern);
|
|
243
296
|
if (regex.test(filePath)) {
|
|
244
|
-
console.log('out_of_scope:' + source + ':' + pattern);
|
|
297
|
+
console.log('out_of_scope:' + mode + ':' + source + ':' + pattern);
|
|
245
298
|
process.exit(0);
|
|
246
299
|
}
|
|
247
300
|
}
|
|
248
301
|
}
|
|
249
302
|
|
|
250
|
-
//
|
|
303
|
+
// scope.in — file must match at least one pattern
|
|
251
304
|
const allInScope = specs.flatMap(({ spec }) => spec.scope?.in || []);
|
|
252
305
|
if (allInScope.length > 0) {
|
|
253
306
|
let found = false;
|
|
@@ -259,7 +312,7 @@ if command -v node >/dev/null 2>&1; then
|
|
|
259
312
|
}
|
|
260
313
|
}
|
|
261
314
|
if (!found) {
|
|
262
|
-
console.log('not_in_scope');
|
|
315
|
+
console.log('not_in_scope:' + mode);
|
|
263
316
|
process.exit(0);
|
|
264
317
|
}
|
|
265
318
|
}
|
|
@@ -270,29 +323,57 @@ if command -v node >/dev/null 2>&1; then
|
|
|
270
323
|
}
|
|
271
324
|
" 2>&1)
|
|
272
325
|
|
|
326
|
+
|
|
327
|
+
if [[ "$SCOPE_CHECK" == error:* ]]; then
|
|
328
|
+
ERROR_MSG="${SCOPE_CHECK#error:}"
|
|
329
|
+
echo "BLOCKED: Scope check failed — cannot verify file is in scope" >&2
|
|
330
|
+
echo " Error: $ERROR_MSG" >&2
|
|
331
|
+
echo " Fix the spec file or scope configuration before editing files" >&2
|
|
332
|
+
exit 2
|
|
333
|
+
fi
|
|
334
|
+
|
|
273
335
|
if [[ "$SCOPE_CHECK" == out_of_scope:* ]]; then
|
|
274
336
|
DETAIL="${SCOPE_CHECK#out_of_scope:}"
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
337
|
+
# Format: mode:source:pattern
|
|
338
|
+
MODE="${DETAIL%%:*}"
|
|
339
|
+
REST="${DETAIL#*:}"
|
|
340
|
+
SOURCE="${REST%%:*}"
|
|
341
|
+
PATTERN="${REST#*:}"
|
|
342
|
+
echo "BLOCKED: $REL_PATH is excluded by scope.out in $SOURCE (pattern: $PATTERN)"
|
|
343
|
+
if [[ "$MODE" == "union" ]]; then
|
|
344
|
+
echo " Mode: union (no authoritative spec bound to this worktree)"
|
|
345
|
+
echo " The scope guard is checking ALL active specs because the worktree<->spec"
|
|
346
|
+
echo " binding is missing. An unrelated spec may be blocking this edit."
|
|
347
|
+
echo " Fix: caws worktree bind <your-spec-id>"
|
|
348
|
+
echo " Diagnose: caws scope show"
|
|
349
|
+
else
|
|
350
|
+
echo " Mode: authoritative (checking only your bound spec)"
|
|
351
|
+
echo " To modify scope, update the spec's scope.out field"
|
|
352
|
+
fi
|
|
353
|
+
exit 2
|
|
285
354
|
fi
|
|
286
355
|
|
|
356
|
+
if [[ "$SCOPE_CHECK" == not_in_scope:* ]]; then
|
|
357
|
+
MODE="${SCOPE_CHECK#not_in_scope:}"
|
|
358
|
+
echo "BLOCKED: $REL_PATH is not in the defined scope.in of any active spec"
|
|
359
|
+
if [[ "$MODE" == "union" ]]; then
|
|
360
|
+
echo " Mode: union (no authoritative spec bound to this worktree)"
|
|
361
|
+
echo " The scope guard is checking ALL active specs because the worktree<->spec"
|
|
362
|
+
echo " binding is missing. Your file may be in a scope that no spec covers."
|
|
363
|
+
echo " Fix: caws worktree bind <your-spec-id>"
|
|
364
|
+
echo " Diagnose: caws scope show"
|
|
365
|
+
else
|
|
366
|
+
echo " Mode: authoritative (checking only your bound spec)"
|
|
367
|
+
echo " To modify scope, update the spec's scope.in field"
|
|
368
|
+
fi
|
|
369
|
+
exit 2
|
|
370
|
+
fi
|
|
371
|
+
|
|
372
|
+
# Legacy fallback for unqualified not_in_scope (shouldn't happen with updated logic)
|
|
287
373
|
if [[ "$SCOPE_CHECK" == "not_in_scope" ]]; then
|
|
288
|
-
echo
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
"permissionDecision": "ask",
|
|
292
|
-
"permissionDecisionReason": "This file ('"$REL_PATH"') is not in the defined scope of any active spec. Editing it may cause scope creep. Please confirm this edit is intentional."
|
|
293
|
-
}
|
|
294
|
-
}'
|
|
295
|
-
exit 0
|
|
374
|
+
echo "BLOCKED: $REL_PATH is not in the defined scope.in of any active spec"
|
|
375
|
+
echo " Diagnose: caws scope show"
|
|
376
|
+
exit 2
|
|
296
377
|
fi
|
|
297
378
|
fi
|
|
298
379
|
|
|
@@ -40,7 +40,7 @@ if [ -f "$CAWS_ROOT/.caws/worktrees.json" ] && command -v node >/dev/null 2>&1;
|
|
|
40
40
|
WT_INFO=$(node -e "
|
|
41
41
|
try {
|
|
42
42
|
var reg = JSON.parse(require('fs').readFileSync('$CAWS_ROOT/.caws/worktrees.json', 'utf8'));
|
|
43
|
-
var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active'; });
|
|
43
|
+
var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active' || w.status === 'fresh' || w.status === 'merged'; });
|
|
44
44
|
if (active.length > 0) {
|
|
45
45
|
var names = active.map(function(w) { return w.name + ' (' + w.branch + ')'; });
|
|
46
46
|
console.log(active.length + ':' + names.join(', '));
|
|
@@ -58,7 +58,7 @@ if [ -f "$CAWS_ROOT/.caws/worktrees.json" ] && command -v node >/dev/null 2>&1;
|
|
|
58
58
|
BASE_BRANCH=$(node -e "
|
|
59
59
|
try {
|
|
60
60
|
var reg = JSON.parse(require('fs').readFileSync('$CAWS_ROOT/.caws/worktrees.json', 'utf8'));
|
|
61
|
-
var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active'; });
|
|
61
|
+
var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active' || w.status === 'fresh' || w.status === 'merged'; });
|
|
62
62
|
if (active.length > 0) console.log(active[0].baseBranch || '');
|
|
63
63
|
else console.log('');
|
|
64
64
|
} catch(e) { console.log(''); }
|
|
@@ -548,13 +548,86 @@ handle_pre_compact() {
|
|
|
548
548
|
generate_session_output "$(resolve_transcript)"
|
|
549
549
|
}
|
|
550
550
|
|
|
551
|
+
# ============================================================
|
|
552
|
+
# Agent registry heartbeat — register this agent with CAWS
|
|
553
|
+
# ============================================================
|
|
554
|
+
AGENTS_REGISTRY="${CWD}/.caws/agents.json"
|
|
555
|
+
|
|
556
|
+
heartbeat_agent() {
|
|
557
|
+
[ "$SESSION_ID" = "unknown" ] && return
|
|
558
|
+
[ ! -d "${CWD}/.caws" ] && return
|
|
559
|
+
|
|
560
|
+
local model_val
|
|
561
|
+
model_val=$(echo "$INPUT" | jq -r '.model // "unknown"' 2>/dev/null)
|
|
562
|
+
|
|
563
|
+
local registry
|
|
564
|
+
if [ -f "$AGENTS_REGISTRY" ]; then
|
|
565
|
+
registry=$(cat "$AGENTS_REGISTRY" 2>/dev/null || echo '{"version":1,"agents":{}}')
|
|
566
|
+
else
|
|
567
|
+
registry='{"version":1,"agents":{}}'
|
|
568
|
+
fi
|
|
569
|
+
|
|
570
|
+
registry=$(echo "$registry" | python3 -c "
|
|
571
|
+
import json, sys
|
|
572
|
+
from datetime import datetime, timedelta, timezone
|
|
573
|
+
|
|
574
|
+
TTL = timedelta(minutes=30)
|
|
575
|
+
now = datetime.now(timezone.utc)
|
|
576
|
+
sid = '$SESSION_ID'
|
|
577
|
+
model = '$model_val'
|
|
578
|
+
|
|
579
|
+
data = json.load(sys.stdin)
|
|
580
|
+
agents = data.get('agents', {})
|
|
581
|
+
|
|
582
|
+
pruned = {}
|
|
583
|
+
for k, entry in agents.items():
|
|
584
|
+
try:
|
|
585
|
+
last = datetime.fromisoformat(entry['lastSeen'].replace('Z', '+00:00'))
|
|
586
|
+
if now - last < TTL:
|
|
587
|
+
pruned[k] = entry
|
|
588
|
+
except (KeyError, ValueError):
|
|
589
|
+
pass
|
|
590
|
+
|
|
591
|
+
existing = pruned.get(sid, {})
|
|
592
|
+
pruned[sid] = {
|
|
593
|
+
'sessionId': sid,
|
|
594
|
+
'platform': 'claude-code',
|
|
595
|
+
'model': model if model != 'unknown' else existing.get('model'),
|
|
596
|
+
'specId': existing.get('specId'),
|
|
597
|
+
'ttl': 1800000,
|
|
598
|
+
'firstSeen': existing.get('firstSeen', now.strftime('%Y-%m-%dT%H:%M:%SZ')),
|
|
599
|
+
'lastSeen': now.strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
data['agents'] = pruned
|
|
603
|
+
json.dump(data, sys.stdout, indent=2)
|
|
604
|
+
" 2>/dev/null)
|
|
605
|
+
|
|
606
|
+
[ -n "$registry" ] && echo "$registry" > "$AGENTS_REGISTRY"
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
remove_agent() {
|
|
610
|
+
[ "$SESSION_ID" = "unknown" ] && return
|
|
611
|
+
[ ! -f "$AGENTS_REGISTRY" ] && return
|
|
612
|
+
|
|
613
|
+
python3 -c "
|
|
614
|
+
import json
|
|
615
|
+
sid = '$SESSION_ID'
|
|
616
|
+
with open('$AGENTS_REGISTRY', 'r') as f:
|
|
617
|
+
data = json.load(f)
|
|
618
|
+
data.get('agents', {}).pop(sid, None)
|
|
619
|
+
with open('$AGENTS_REGISTRY', 'w') as f:
|
|
620
|
+
json.dump(data, f, indent=2)
|
|
621
|
+
" 2>/dev/null || true
|
|
622
|
+
}
|
|
623
|
+
|
|
551
624
|
# ============================================================
|
|
552
625
|
# DISPATCH
|
|
553
626
|
# ============================================================
|
|
554
627
|
case "$HOOK_EVENT" in
|
|
555
|
-
SessionStart) handle_session_start ;;
|
|
556
|
-
Stop) handle_stop ;;
|
|
557
|
-
PreCompact) handle_pre_compact ;;
|
|
628
|
+
SessionStart) handle_session_start; heartbeat_agent ;;
|
|
629
|
+
Stop) handle_stop; remove_agent ;;
|
|
630
|
+
PreCompact) handle_pre_compact; heartbeat_agent ;;
|
|
558
631
|
*) ;; # Other events: no-op
|
|
559
632
|
esac
|
|
560
633
|
|
|
@@ -25,7 +25,7 @@ if [[ -f "$PROJECT_DIR/.caws/worktrees.json" ]] && command -v node >/dev/null 2>
|
|
|
25
25
|
ACTIVE_INFO=$(node -e "
|
|
26
26
|
try {
|
|
27
27
|
var reg = JSON.parse(require('fs').readFileSync('$PROJECT_DIR/.caws/worktrees.json', 'utf8'));
|
|
28
|
-
var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active'; });
|
|
28
|
+
var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active' || w.status === 'fresh' || w.status === 'merged'; });
|
|
29
29
|
if (active.length > 0) {
|
|
30
30
|
console.log(active.length + ':' + active.map(function(w) { return w.name; }).join(', '));
|
|
31
31
|
} else {
|