@friedbotstudio/create-baseline 0.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/LICENSE +202 -0
- package/README.md +222 -0
- package/bin/cli.js +247 -0
- package/obj/template/.claude/agents/swarm-worker.md +52 -0
- package/obj/template/.claude/bin/LICENSE +201 -0
- package/obj/template/.claude/bin/NOTICE +48 -0
- package/obj/template/.claude/commands/approve-spec.md +29 -0
- package/obj/template/.claude/commands/approve-swarm.md +27 -0
- package/obj/template/.claude/commands/grant-commit.md +19 -0
- package/obj/template/.claude/commands/init-project.md +191 -0
- package/obj/template/.claude/hooks/artifact_template_guard.sh +141 -0
- package/obj/template/.claude/hooks/consent_gate_grant.sh +89 -0
- package/obj/template/.claude/hooks/destructive_cmd_guard.sh +42 -0
- package/obj/template/.claude/hooks/env_guard.sh +36 -0
- package/obj/template/.claude/hooks/git_commit_guard.sh +93 -0
- package/obj/template/.claude/hooks/harness_continuation.sh +121 -0
- package/obj/template/.claude/hooks/lib/__pycache__/resume_writer.cpython-314.pyc +0 -0
- package/obj/template/.claude/hooks/lib/common.sh +328 -0
- package/obj/template/.claude/hooks/lib/resume_writer.py +341 -0
- package/obj/template/.claude/hooks/lint_runner.sh +55 -0
- package/obj/template/.claude/hooks/memory_pre_compact.sh +36 -0
- package/obj/template/.claude/hooks/memory_session_start.sh +244 -0
- package/obj/template/.claude/hooks/memory_stop.sh +173 -0
- package/obj/template/.claude/hooks/plantuml_syntax_guard.sh +161 -0
- package/obj/template/.claude/hooks/process_lifecycle_guard.sh +89 -0
- package/obj/template/.claude/hooks/setup_guard.sh +50 -0
- package/obj/template/.claude/hooks/spec_approval_guard.sh +81 -0
- package/obj/template/.claude/hooks/spec_design_calls_guard.sh +183 -0
- package/obj/template/.claude/hooks/spec_diagram_presence_guard.sh +141 -0
- package/obj/template/.claude/hooks/swarm_approval_guard.sh +39 -0
- package/obj/template/.claude/hooks/swarm_boundary_guard.sh +136 -0
- package/obj/template/.claude/hooks/tdd_order_guard.sh +176 -0
- package/obj/template/.claude/hooks/test_runner.sh +75 -0
- package/obj/template/.claude/hooks/tests/fixtures/ac008_byte_equal_reference.txt +12 -0
- package/obj/template/.claude/hooks/tests/memory_session_start_test.sh +285 -0
- package/obj/template/.claude/hooks/track_guard.sh +127 -0
- package/obj/template/.claude/hooks/verify_pass_guard.sh +88 -0
- package/obj/template/.claude/memory/README.md +108 -0
- package/obj/template/.claude/memory/_pending.md +15 -0
- package/obj/template/.claude/memory/_resume.md +12 -0
- package/obj/template/.claude/memory/conventions.md +26 -0
- package/obj/template/.claude/memory/decisions.md +29 -0
- package/obj/template/.claude/memory/landmarks.md +26 -0
- package/obj/template/.claude/memory/landmines.md +27 -0
- package/obj/template/.claude/memory/libraries.md +27 -0
- package/obj/template/.claude/memory/pending-questions.md +28 -0
- package/obj/template/.claude/project.json +221 -0
- package/obj/template/.claude/settings.json +110 -0
- package/obj/template/.claude/skills/archive/SKILL.md +48 -0
- package/obj/template/.claude/skills/archive/archive.sh +145 -0
- package/obj/template/.claude/skills/audit-baseline/SKILL.md +80 -0
- package/obj/template/.claude/skills/audit-baseline/audit.sh +919 -0
- package/obj/template/.claude/skills/brd/SKILL.md +44 -0
- package/obj/template/.claude/skills/brd/template.md +83 -0
- package/obj/template/.claude/skills/chore/SKILL.md +99 -0
- package/obj/template/.claude/skills/claude-automation-recommender/LICENSE +202 -0
- package/obj/template/.claude/skills/claude-automation-recommender/NOTICE +69 -0
- package/obj/template/.claude/skills/claude-automation-recommender/SKILL.md +358 -0
- package/obj/template/.claude/skills/claude-automation-recommender/references/hooks-patterns.md +226 -0
- package/obj/template/.claude/skills/claude-automation-recommender/references/mcp-servers.md +263 -0
- package/obj/template/.claude/skills/claude-automation-recommender/references/plugins-reference.md +98 -0
- package/obj/template/.claude/skills/claude-automation-recommender/references/skills-reference.md +408 -0
- package/obj/template/.claude/skills/claude-automation-recommender/references/subagent-templates.md +181 -0
- package/obj/template/.claude/skills/code-structure/SKILL.md +204 -0
- package/obj/template/.claude/skills/commit/SKILL.md +21 -0
- package/obj/template/.claude/skills/copywriting/SKILL.md +252 -0
- package/obj/template/.claude/skills/copywriting/evals/evals.json +111 -0
- package/obj/template/.claude/skills/copywriting/references/ai-writing-detection.md +200 -0
- package/obj/template/.claude/skills/copywriting/references/copy-frameworks.md +344 -0
- package/obj/template/.claude/skills/copywriting/references/natural-transitions.md +272 -0
- package/obj/template/.claude/skills/design-ui/SKILL.md +175 -0
- package/obj/template/.claude/skills/design-ui/references/design-vs-development.md +89 -0
- package/obj/template/.claude/skills/design-ui/references/intent-table.md +64 -0
- package/obj/template/.claude/skills/design-ui/references/orchestration.md +121 -0
- package/obj/template/.claude/skills/design-ui/references/state-machine.md +125 -0
- package/obj/template/.claude/skills/document/SKILL.md +66 -0
- package/obj/template/.claude/skills/documentation/SKILL.md +50 -0
- package/obj/template/.claude/skills/harness/SKILL.md +169 -0
- package/obj/template/.claude/skills/humanizer/SKILL.md +489 -0
- package/obj/template/.claude/skills/humanizer/references/ai-writing-detection.md +208 -0
- package/obj/template/.claude/skills/impeccable/PROJECT_NOTES.md +22 -0
- package/obj/template/.claude/skills/impeccable/SKILL.md +153 -0
- package/obj/template/.claude/skills/impeccable/agents/openai.yaml +4 -0
- package/obj/template/.claude/skills/impeccable/reference/adapt.md +190 -0
- package/obj/template/.claude/skills/impeccable/reference/animate.md +173 -0
- package/obj/template/.claude/skills/impeccable/reference/audit.md +134 -0
- package/obj/template/.claude/skills/impeccable/reference/bolder.md +113 -0
- package/obj/template/.claude/skills/impeccable/reference/brand.md +104 -0
- package/obj/template/.claude/skills/impeccable/reference/clarify.md +174 -0
- package/obj/template/.claude/skills/impeccable/reference/cognitive-load.md +106 -0
- package/obj/template/.claude/skills/impeccable/reference/color-and-contrast.md +105 -0
- package/obj/template/.claude/skills/impeccable/reference/colorize.md +154 -0
- package/obj/template/.claude/skills/impeccable/reference/craft.md +138 -0
- package/obj/template/.claude/skills/impeccable/reference/critique.md +213 -0
- package/obj/template/.claude/skills/impeccable/reference/delight.md +302 -0
- package/obj/template/.claude/skills/impeccable/reference/distill.md +111 -0
- package/obj/template/.claude/skills/impeccable/reference/document.md +427 -0
- package/obj/template/.claude/skills/impeccable/reference/extract.md +70 -0
- package/obj/template/.claude/skills/impeccable/reference/harden.md +347 -0
- package/obj/template/.claude/skills/impeccable/reference/heuristics-scoring.md +234 -0
- package/obj/template/.claude/skills/impeccable/reference/interaction-design.md +195 -0
- package/obj/template/.claude/skills/impeccable/reference/layout.md +141 -0
- package/obj/template/.claude/skills/impeccable/reference/live.md +513 -0
- package/obj/template/.claude/skills/impeccable/reference/motion-design.md +99 -0
- package/obj/template/.claude/skills/impeccable/reference/onboard.md +234 -0
- package/obj/template/.claude/skills/impeccable/reference/optimize.md +258 -0
- package/obj/template/.claude/skills/impeccable/reference/overdrive.md +130 -0
- package/obj/template/.claude/skills/impeccable/reference/personas.md +178 -0
- package/obj/template/.claude/skills/impeccable/reference/polish.md +232 -0
- package/obj/template/.claude/skills/impeccable/reference/product.md +62 -0
- package/obj/template/.claude/skills/impeccable/reference/quieter.md +99 -0
- package/obj/template/.claude/skills/impeccable/reference/responsive-design.md +114 -0
- package/obj/template/.claude/skills/impeccable/reference/shape.md +136 -0
- package/obj/template/.claude/skills/impeccable/reference/spatial-design.md +100 -0
- package/obj/template/.claude/skills/impeccable/reference/teach.md +137 -0
- package/obj/template/.claude/skills/impeccable/reference/typeset.md +124 -0
- package/obj/template/.claude/skills/impeccable/reference/typography.md +159 -0
- package/obj/template/.claude/skills/impeccable/reference/ux-writing.md +107 -0
- package/obj/template/.claude/skills/impeccable/scripts/cleanup-deprecated.mjs +284 -0
- package/obj/template/.claude/skills/impeccable/scripts/command-metadata.json +94 -0
- package/obj/template/.claude/skills/impeccable/scripts/design-parser.mjs +820 -0
- package/obj/template/.claude/skills/impeccable/scripts/detect-csp.mjs +198 -0
- package/obj/template/.claude/skills/impeccable/scripts/is-generated.mjs +69 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-accept.mjs +465 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-browser.js +4684 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-inject.mjs +436 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-poll.mjs +187 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-server.mjs +679 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-wrap.mjs +395 -0
- package/obj/template/.claude/skills/impeccable/scripts/live.mjs +247 -0
- package/obj/template/.claude/skills/impeccable/scripts/load-context.mjs +93 -0
- package/obj/template/.claude/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
- package/obj/template/.claude/skills/impeccable/scripts/pin.mjs +214 -0
- package/obj/template/.claude/skills/implement/SKILL.md +83 -0
- package/obj/template/.claude/skills/intake/SKILL.md +46 -0
- package/obj/template/.claude/skills/intake/template.md +61 -0
- package/obj/template/.claude/skills/integrate/SKILL.md +62 -0
- package/obj/template/.claude/skills/memory-flush/SKILL.md +172 -0
- package/obj/template/.claude/skills/memory-flush/sweep.py +286 -0
- package/obj/template/.claude/skills/memory-flush/tests/run.sh +327 -0
- package/obj/template/.claude/skills/prose/SKILL.md +119 -0
- package/obj/template/.claude/skills/rca/SKILL.md +42 -0
- package/obj/template/.claude/skills/rca/template.md +83 -0
- package/obj/template/.claude/skills/research/SKILL.md +75 -0
- package/obj/template/.claude/skills/scenario/SKILL.md +64 -0
- package/obj/template/.claude/skills/scout/SKILL.md +72 -0
- package/obj/template/.claude/skills/security/SKILL.md +75 -0
- package/obj/template/.claude/skills/simplify/SKILL.md +67 -0
- package/obj/template/.claude/skills/spec/SKILL.md +69 -0
- package/obj/template/.claude/skills/spec/template.md +274 -0
- package/obj/template/.claude/skills/spec-diagram-review/SKILL.md +81 -0
- package/obj/template/.claude/skills/spec-lint/SKILL.md +55 -0
- package/obj/template/.claude/skills/spec-lint/lint.sh +218 -0
- package/obj/template/.claude/skills/spec-render/SKILL.md +45 -0
- package/obj/template/.claude/skills/spec-render/render.sh +109 -0
- package/obj/template/.claude/skills/spec-traceability-review/SKILL.md +72 -0
- package/obj/template/.claude/skills/swarm-dispatch/SKILL.md +212 -0
- package/obj/template/.claude/skills/swarm-dispatch/swarm_merge.sh +154 -0
- package/obj/template/.claude/skills/swarm-plan/SKILL.md +90 -0
- package/obj/template/.claude/skills/swarm-plan/validate.sh +181 -0
- package/obj/template/.claude/skills/tdd/SKILL.md +100 -0
- package/obj/template/.claude/skills/technical-tutorials/SKILL.md +569 -0
- package/obj/template/.claude/skills/technical-tutorials/references/audience-context-README.md +53 -0
- package/obj/template/.claude/skills/technical-tutorials/references/audience-context.md +246 -0
- package/obj/template/.claude/skills/technical-tutorials/references/audience-example.md +175 -0
- package/obj/template/.claude/skills/technical-tutorials/references/audience-template.md +152 -0
- package/obj/template/.claude/skills/triage/SKILL.md +55 -0
- package/obj/template/.claude/skills/verify/SKILL.md +74 -0
- package/obj/template/.mcp.json +24 -0
- package/obj/template/CLAUDE.md +327 -0
- package/obj/template/docs/init/seed.md +585 -0
- package/obj/template/manifest.json +214 -0
- package/package.json +48 -0
- package/src/.mcp.template.json +24 -0
- package/src/.npmrc.template +2 -0
- package/src/CLAUDE.template.md +327 -0
- package/src/agents/swarm-worker.template.md +51 -0
- package/src/cli/conflict.js +31 -0
- package/src/cli/doctor.js +152 -0
- package/src/cli/install.js +93 -0
- package/src/cli/io.js +27 -0
- package/src/cli/manifest.js +38 -0
- package/src/cli/mcp.js +54 -0
- package/src/cli/merge.js +107 -0
- package/src/cli/plantuml.js +121 -0
- package/src/cli/util.js +10 -0
- package/src/memory/_pending.template.md +15 -0
- package/src/memory/_resume.template.md +12 -0
- package/src/memory/conventions.template.md +26 -0
- package/src/memory/decisions.template.md +29 -0
- package/src/memory/landmarks.template.md +26 -0
- package/src/memory/landmines.template.md +27 -0
- package/src/memory/libraries.template.md +27 -0
- package/src/memory/pending-questions.template.md +28 -0
- package/src/project.template.json +221 -0
- package/src/seed.template.md +585 -0
- package/src/settings.template.json +110 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# process_lifecycle_guard — PreToolUse / Bash
|
|
3
|
+
#
|
|
4
|
+
# Advisory hook. Detects process-management Bash patterns (kill, pkill, lsof,
|
|
5
|
+
# fuser, dev-server spawns) and surfaces relevant memory entries inline so
|
|
6
|
+
# Claude reads them at the moment of action rather than relying on
|
|
7
|
+
# session-start salience to persist across turns.
|
|
8
|
+
#
|
|
9
|
+
# Closes the gap diagnosed 2026-04-30: phase skills pull memory just-in-time
|
|
10
|
+
# via their `read on demand` contract, but ad-hoc main-context Bash has no
|
|
11
|
+
# skill firing — so no entry was ever read at the action point. This hook
|
|
12
|
+
# fires the read structurally.
|
|
13
|
+
#
|
|
14
|
+
# Output: prints the matched memory entries to stderr (Claude Code surfaces
|
|
15
|
+
# stderr in the tool transcript). Always emits `allow` — never blocks.
|
|
16
|
+
# Cross-references CLAUDE.md Article IX clauses 6 + 7.
|
|
17
|
+
|
|
18
|
+
# shellcheck source=./lib/common.sh
|
|
19
|
+
. "${BASH_SOURCE[0]%/*}/lib/common.sh"
|
|
20
|
+
read_payload
|
|
21
|
+
|
|
22
|
+
CMD="$(payload_get .tool_input.command)"
|
|
23
|
+
|
|
24
|
+
# Empty commands or non-Bash payloads — nothing to inspect.
|
|
25
|
+
[ -n "$CMD" ] || { emit_allow; }
|
|
26
|
+
|
|
27
|
+
# Trigger detection. Patterns chosen to match the dev-server-ownership and
|
|
28
|
+
# lsof-port-kill-takes-firefox-with-it surfaces. Case-sensitive on the binary
|
|
29
|
+
# name; lower-case is canonical for these tools.
|
|
30
|
+
should_surface=0
|
|
31
|
+
case "$CMD" in
|
|
32
|
+
*"kill "*|*"kill\""*|*"kill$"*|kill" "*|*" kill "*) should_surface=1 ;;
|
|
33
|
+
*"pkill "*|*" pkill "*|pkill" "*) should_surface=1 ;;
|
|
34
|
+
*"killall "*|*" killall "*) should_surface=1 ;;
|
|
35
|
+
*"lsof "*|*" lsof "*|lsof" "*) should_surface=1 ;;
|
|
36
|
+
*"fuser "*|*" fuser "*) should_surface=1 ;;
|
|
37
|
+
*"npm run "*"serve"*|*"npm run "*"dev"*) should_surface=1 ;;
|
|
38
|
+
*"yarn dev"*|*"pnpm dev"*) should_surface=1 ;;
|
|
39
|
+
*"eleventy --serve"*|*"eleventy serve"*) should_surface=1 ;;
|
|
40
|
+
*"vite"*|*"next dev"*|*"astro dev"*|*"http.server"*) should_surface=1 ;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
[ "$should_surface" = 1 ] || { emit_allow; }
|
|
44
|
+
|
|
45
|
+
# Surface the relevant memory entries. The body of each entry (verbatim block
|
|
46
|
+
# first, structured fields after) is read directly so Claude sees the user's
|
|
47
|
+
# actual words, not Claude's prior paraphrase.
|
|
48
|
+
MEM="$CLAUDE_DOTDIR/memory"
|
|
49
|
+
|
|
50
|
+
excerpts="$(MEM="$MEM" python3 <<'PY'
|
|
51
|
+
import os, pathlib, re, sys
|
|
52
|
+
mem = pathlib.Path(os.environ["MEM"])
|
|
53
|
+
targets = [
|
|
54
|
+
("conventions.md", "dev-server-ownership"),
|
|
55
|
+
("landmines.md", "lsof-port-kill-takes-firefox-with-it"),
|
|
56
|
+
]
|
|
57
|
+
chunks = []
|
|
58
|
+
for fname, anchor in targets:
|
|
59
|
+
p = mem / fname
|
|
60
|
+
if not p.exists():
|
|
61
|
+
continue
|
|
62
|
+
text = p.read_text()
|
|
63
|
+
# Capture from "## <anchor>" up to the next "## " (or EOF).
|
|
64
|
+
m = re.search(
|
|
65
|
+
rf"^##\s+{re.escape(anchor)}\b.*?(?=^##\s|\Z)",
|
|
66
|
+
text, re.M | re.S
|
|
67
|
+
)
|
|
68
|
+
if m:
|
|
69
|
+
chunks.append(f"--- {fname} ---\n{m.group(0).rstrip()}")
|
|
70
|
+
print("\n\n".join(chunks))
|
|
71
|
+
PY
|
|
72
|
+
)"
|
|
73
|
+
|
|
74
|
+
if [ -z "$excerpts" ]; then
|
|
75
|
+
# No matching memory entries on disk — emit a softer notice so the absence
|
|
76
|
+
# is itself surfaced. Curator should re-flush memory if this fires.
|
|
77
|
+
emit_info "process_lifecycle_guard: command matched a process-management pattern, but no memory entries (\`conventions.md → dev-server-ownership\`, \`landmines.md → lsof-port-kill-takes-firefox-with-it\`) were found. Consider \`/memory-flush\` or restoring the entries before proceeding."
|
|
78
|
+
log_line "process_lifecycle_guard" "fired with empty memory: $CMD"
|
|
79
|
+
emit_allow
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
emit_info "process_lifecycle_guard — process-management memory surfaced (verbatim then interpretation):
|
|
83
|
+
|
|
84
|
+
$excerpts
|
|
85
|
+
|
|
86
|
+
This advisory fires whenever a Bash command matches a process-management pattern. CLAUDE.md Article IX clause 7: read the verbatim above, treat it as binding for the current operation, and prefer verbatim over interpretation when they conflict."
|
|
87
|
+
|
|
88
|
+
log_line "process_lifecycle_guard" "surfaced: ${CMD:0:120}"
|
|
89
|
+
emit_allow
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Setup Guard — PreToolUse(Write|Edit|MultiEdit)
|
|
3
|
+
#
|
|
4
|
+
# Advisory only. When `.claude/project.json` reports `configured: false`,
|
|
5
|
+
# this hook emits a one-time-per-period info message reminding the user
|
|
6
|
+
# that the baseline is in project-agnostic mode (test/lint runners are
|
|
7
|
+
# in guide mode, no stack-specific tailoring). It does NOT block writes.
|
|
8
|
+
#
|
|
9
|
+
# Bypass is intentionally allowed — the user gets baseline-only behaviour
|
|
10
|
+
# until they run `/init-project`. Other guards (commit, env, spec-approval,
|
|
11
|
+
# verify-pass, track) remain hard.
|
|
12
|
+
#
|
|
13
|
+
# Deduplication: the warning prints only when no warn marker has been
|
|
14
|
+
# touched in the last 600s. This keeps editing-heavy sessions from
|
|
15
|
+
# spamming. Re-warns at the start of each new session naturally
|
|
16
|
+
# (file mtime ages out).
|
|
17
|
+
|
|
18
|
+
# shellcheck source=./lib/common.sh
|
|
19
|
+
. "${BASH_SOURCE[0]%/*}/lib/common.sh"
|
|
20
|
+
read_payload
|
|
21
|
+
|
|
22
|
+
TOOL="$(payload_get .tool_name)"
|
|
23
|
+
case "$TOOL" in
|
|
24
|
+
Write|Edit|MultiEdit) ;;
|
|
25
|
+
*) emit_allow ;;
|
|
26
|
+
esac
|
|
27
|
+
|
|
28
|
+
FILE="$(payload_get .tool_input.file_path)"
|
|
29
|
+
[ -n "$FILE" ] || emit_allow
|
|
30
|
+
rel="${FILE#$CLAUDE_PROJECT_ROOT/}"
|
|
31
|
+
|
|
32
|
+
# Already configured → no-op.
|
|
33
|
+
configured="$(project_get .configured)"
|
|
34
|
+
if [ "$configured" = "True" ] || [ "$configured" = "true" ]; then
|
|
35
|
+
emit_allow
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Configured=false. Emit a one-time-per-period advisory and allow the write.
|
|
39
|
+
WARN_MARKER="$STATE_DIR/setup_guard_last_warn"
|
|
40
|
+
NOW="$(date +%s 2>/dev/null || echo 0)"
|
|
41
|
+
LAST="$(stat -f %m "$WARN_MARKER" 2>/dev/null || stat -c %Y "$WARN_MARKER" 2>/dev/null || echo 0)"
|
|
42
|
+
SINCE=$((NOW - LAST))
|
|
43
|
+
|
|
44
|
+
if [ "$SINCE" -ge 600 ] || [ "$LAST" = "0" ]; then
|
|
45
|
+
emit_info "Setup Guard (advisory): \`.claude/project.json\` reports configured=false. The baseline is running in project-agnostic mode — test_runner and lint_runner hooks are in guide mode and no stack-specific tailoring has been applied. Run \`/init-project\` to scout the codebase, invoke the recommender, and generate a tailored config. (This warning is rate-limited to once per 10 minutes.)"
|
|
46
|
+
: > "$WARN_MARKER" 2>/dev/null || true
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
log_line setup_guard "advisory pre-init write to $rel (warned=$([ "$SINCE" -ge 600 ] && echo yes || echo no))"
|
|
50
|
+
emit_allow
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Spec Approval Guard — PreToolUse(Write|Edit|MultiEdit)
|
|
3
|
+
#
|
|
4
|
+
# Three enforcement modes:
|
|
5
|
+
#
|
|
6
|
+
# 1. Approval artifacts (.claude/state/spec_approvals/*.approval) — only
|
|
7
|
+
# writable when a fresh slug-matched consent marker exists at
|
|
8
|
+
# .claude/state/.spec_approval_grant. The marker is written by
|
|
9
|
+
# consent_gate_grant.sh on /approve-spec invocation, OUTSIDE Claude's
|
|
10
|
+
# tool boundary. Validated and consumed via validate_consent_marker.
|
|
11
|
+
#
|
|
12
|
+
# 2. The marker file itself — Claude SHALL NEVER write it via tool. The
|
|
13
|
+
# marker is the structural source of consent; allowing Claude to write
|
|
14
|
+
# it would defeat the gate.
|
|
15
|
+
#
|
|
16
|
+
# 3. Spec files (docs/specs/*.md) — block writes that add/modify an
|
|
17
|
+
# "Approved" / "Status: Approved" line. The user must run /approve-spec.
|
|
18
|
+
|
|
19
|
+
# shellcheck source=./lib/common.sh
|
|
20
|
+
. "${BASH_SOURCE[0]%/*}/lib/common.sh"
|
|
21
|
+
read_payload
|
|
22
|
+
|
|
23
|
+
TOOL="$(payload_get .tool_name)"
|
|
24
|
+
case "$TOOL" in
|
|
25
|
+
Write|Edit|MultiEdit) ;;
|
|
26
|
+
*) emit_allow ;;
|
|
27
|
+
esac
|
|
28
|
+
|
|
29
|
+
FILE="$(payload_get .tool_input.file_path)"
|
|
30
|
+
[ -n "$FILE" ] || emit_allow
|
|
31
|
+
rel="$(canonical_rel "$FILE")"
|
|
32
|
+
[ -n "$rel" ] || emit_allow
|
|
33
|
+
|
|
34
|
+
block_marker_self_write "$rel" "$CONSENT_MARKER_SPEC_REL" "Spec Approval Guard" "/approve-spec <path>"
|
|
35
|
+
|
|
36
|
+
case "$rel" in
|
|
37
|
+
.claude/state/spec_approvals/*.approval)
|
|
38
|
+
# Strip .approval, then canonical_slug to fold legacy `<slug>.md.approval`
|
|
39
|
+
# and current `<slug>.approval` to the same bare slug as the marker.
|
|
40
|
+
expected_slug="$(canonical_slug "$(basename "$rel" .approval)")"
|
|
41
|
+
validate_consent_marker "$CONSENT_MARKER_SPEC" "Spec Approval Guard" "/approve-spec <slug|path>" "$expected_slug"
|
|
42
|
+
emit_allow
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
|
|
46
|
+
case "$rel" in
|
|
47
|
+
docs/specs/*.md) ;;
|
|
48
|
+
*) emit_allow ;;
|
|
49
|
+
esac
|
|
50
|
+
|
|
51
|
+
content=""
|
|
52
|
+
case "$TOOL" in
|
|
53
|
+
Write) content="$(payload_get .tool_input.content)" ;;
|
|
54
|
+
Edit) content="$(payload_get .tool_input.new_string)" ;;
|
|
55
|
+
MultiEdit)
|
|
56
|
+
content="$(python3 -c '
|
|
57
|
+
import json, os
|
|
58
|
+
raw = os.environ.get("HOOK_PAYLOAD","")
|
|
59
|
+
d = json.loads(raw) if raw else {}
|
|
60
|
+
edits = (d.get("tool_input") or {}).get("edits") or []
|
|
61
|
+
print("\n".join(e.get("new_string","") for e in edits))
|
|
62
|
+
')"
|
|
63
|
+
;;
|
|
64
|
+
esac
|
|
65
|
+
|
|
66
|
+
if python3 -c "
|
|
67
|
+
import re, sys
|
|
68
|
+
c = sys.argv[1]
|
|
69
|
+
for ln in c.splitlines():
|
|
70
|
+
s = ln.strip().lstrip('-').lstrip('*').strip()
|
|
71
|
+
if re.match(r'(status|state|approval)\s*[:=]\s*approved\b', s, re.I):
|
|
72
|
+
sys.exit(0)
|
|
73
|
+
if re.fullmatch(r'approved\s*[:=]\s*true', s, re.I):
|
|
74
|
+
sys.exit(0)
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
" "$content"; then
|
|
77
|
+
log_line spec_approval_guard "BLOCKED self-approval in: $rel"
|
|
78
|
+
emit_block "Spec Approval Guard: Claude cannot mark a spec as Approved. The user must run \`/approve-spec $rel\`, which produces the consent marker that allows the approval token to be written. Remove the 'Approved' line from this edit."
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
emit_allow
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Spec Design Calls Guard — PreToolUse(Write|Edit|MultiEdit)
|
|
3
|
+
#
|
|
4
|
+
# When a spec's write_set intersects `project.json → tdd.ui_globs`, the spec
|
|
5
|
+
# MUST declare a `## Design calls` section with at least one populated row.
|
|
6
|
+
# This hook denies writes to docs/specs/*.md that violate the rule.
|
|
7
|
+
#
|
|
8
|
+
# The rule is structurally tied to CLAUDE.md Article X.2: every UI design
|
|
9
|
+
# task in a workflow phase routes through `design-ui`. /tdd Step 6 reads the
|
|
10
|
+
# spec's design_calls rows and invokes design-ui per row. Without those
|
|
11
|
+
# rows, the design lane is silently skipped — the rule prevents that.
|
|
12
|
+
#
|
|
13
|
+
# Conditional firing:
|
|
14
|
+
# - SKIP (allow): `tdd.ui_globs` empty or missing.
|
|
15
|
+
# - SKIP (allow): write_set ∩ ui_globs is empty (no UI files in the spec).
|
|
16
|
+
# - DENY: write_set has UI files AND no `## Design calls` section / empty body.
|
|
17
|
+
# - ALLOW: write_set has UI files AND `## Design calls` has a populated row.
|
|
18
|
+
#
|
|
19
|
+
# Template files (_TEMPLATE_*) are exempt — they declare the section shape.
|
|
20
|
+
|
|
21
|
+
# shellcheck source=./lib/common.sh
|
|
22
|
+
. "${BASH_SOURCE[0]%/*}/lib/common.sh"
|
|
23
|
+
read_payload
|
|
24
|
+
|
|
25
|
+
TOOL="$(payload_get .tool_name)"
|
|
26
|
+
case "$TOOL" in
|
|
27
|
+
Write|Edit|MultiEdit) ;;
|
|
28
|
+
*) emit_allow ;;
|
|
29
|
+
esac
|
|
30
|
+
|
|
31
|
+
FILE="$(payload_get .tool_input.file_path)"
|
|
32
|
+
[ -n "$FILE" ] || emit_allow
|
|
33
|
+
rel="${FILE#$CLAUDE_PROJECT_ROOT/}"
|
|
34
|
+
|
|
35
|
+
case "$rel" in
|
|
36
|
+
docs/specs/*.md) ;;
|
|
37
|
+
*) emit_allow ;;
|
|
38
|
+
esac
|
|
39
|
+
|
|
40
|
+
base="${rel##*/}"
|
|
41
|
+
case "$base" in
|
|
42
|
+
_TEMPLATE_*|*TEMPLATE*.md) emit_allow ;;
|
|
43
|
+
esac
|
|
44
|
+
|
|
45
|
+
ui_globs_json="$(project_get .tdd.ui_globs)"
|
|
46
|
+
if [ -z "$ui_globs_json" ] || [ "$ui_globs_json" = "None" ] || [ "$ui_globs_json" = "[]" ]; then
|
|
47
|
+
emit_allow
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
HOOK_FILE="$FILE" HOOK_TOOL="$TOOL" HOOK_REL="$rel" HOOK_UI_GLOBS="$ui_globs_json" python3 <<'PY'
|
|
51
|
+
import json, os, pathlib, re, sys
|
|
52
|
+
|
|
53
|
+
payload = json.loads(os.environ.get("HOOK_PAYLOAD", "") or "{}")
|
|
54
|
+
tool = os.environ["HOOK_TOOL"]
|
|
55
|
+
file_ = os.environ["HOOK_FILE"]
|
|
56
|
+
rel = os.environ["HOOK_REL"]
|
|
57
|
+
try:
|
|
58
|
+
ui_globs = json.loads(os.environ["HOOK_UI_GLOBS"])
|
|
59
|
+
except Exception:
|
|
60
|
+
sys.exit(0)
|
|
61
|
+
if not isinstance(ui_globs, list) or not ui_globs:
|
|
62
|
+
sys.exit(0)
|
|
63
|
+
|
|
64
|
+
ti = payload.get("tool_input") or {}
|
|
65
|
+
|
|
66
|
+
def current():
|
|
67
|
+
try:
|
|
68
|
+
return pathlib.Path(file_).read_text(encoding="utf-8")
|
|
69
|
+
except Exception:
|
|
70
|
+
return ""
|
|
71
|
+
|
|
72
|
+
if tool == "Write":
|
|
73
|
+
content = ti.get("content") or ""
|
|
74
|
+
elif tool == "Edit":
|
|
75
|
+
base = current()
|
|
76
|
+
old = ti.get("old_string") or ""
|
|
77
|
+
new = ti.get("new_string") or ""
|
|
78
|
+
if ti.get("replace_all"):
|
|
79
|
+
content = base.replace(old, new)
|
|
80
|
+
else:
|
|
81
|
+
content = base.replace(old, new, 1) if old in base else (base + new)
|
|
82
|
+
elif tool == "MultiEdit":
|
|
83
|
+
content = current()
|
|
84
|
+
for edit in (ti.get("edits") or []):
|
|
85
|
+
old = edit.get("old_string") or ""
|
|
86
|
+
new = edit.get("new_string") or ""
|
|
87
|
+
if edit.get("replace_all"):
|
|
88
|
+
content = content.replace(old, new)
|
|
89
|
+
else:
|
|
90
|
+
content = content.replace(old, new, 1) if old in content else (content + new)
|
|
91
|
+
else:
|
|
92
|
+
sys.exit(0)
|
|
93
|
+
|
|
94
|
+
if not content.strip():
|
|
95
|
+
sys.exit(0)
|
|
96
|
+
|
|
97
|
+
def expand_brace_globs(globs):
|
|
98
|
+
out = []
|
|
99
|
+
for g in globs:
|
|
100
|
+
if "{" not in g:
|
|
101
|
+
out.append(g); continue
|
|
102
|
+
i = g.index("{"); j = g.index("}", i)
|
|
103
|
+
prefix, alts, suffix = g[:i], g[i+1:j].split(","), g[j+1:]
|
|
104
|
+
for a in alts:
|
|
105
|
+
out.append(prefix + a.strip() + suffix)
|
|
106
|
+
return out
|
|
107
|
+
|
|
108
|
+
def glob_to_regex(g):
|
|
109
|
+
out, i = [], 0
|
|
110
|
+
while i < len(g):
|
|
111
|
+
c = g[i]
|
|
112
|
+
if c == "*":
|
|
113
|
+
if i + 1 < len(g) and g[i+1] == "*":
|
|
114
|
+
out.append(".*"); i += 2
|
|
115
|
+
else:
|
|
116
|
+
out.append("[^/]*"); i += 1
|
|
117
|
+
elif c == "?":
|
|
118
|
+
out.append("[^/]"); i += 1
|
|
119
|
+
elif c in ".+()|^$\\[]{}":
|
|
120
|
+
out.append(re.escape(c)); i += 1
|
|
121
|
+
else:
|
|
122
|
+
out.append(c); i += 1
|
|
123
|
+
return "^" + "".join(out) + "$"
|
|
124
|
+
|
|
125
|
+
def matches_any_glob(path, globs):
|
|
126
|
+
for g in expand_brace_globs(globs):
|
|
127
|
+
if re.fullmatch(glob_to_regex(g), path):
|
|
128
|
+
return True
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
# Extract write_set paths from the spec body. Accept a leading "write_set:"
|
|
132
|
+
# line or paths inside a `## Design calls` table.
|
|
133
|
+
write_set_paths = set()
|
|
134
|
+
for line in content.splitlines():
|
|
135
|
+
m = re.search(r'write[_\s]set\s*:\s*(.+)$', line, re.IGNORECASE)
|
|
136
|
+
if m:
|
|
137
|
+
for tok in re.split(r'[`,\s|]+', m.group(1)):
|
|
138
|
+
tok = tok.strip().strip("*").strip()
|
|
139
|
+
if tok and "/" in tok and not tok.startswith("#"):
|
|
140
|
+
write_set_paths.add(tok)
|
|
141
|
+
|
|
142
|
+
ui_hits = [p for p in write_set_paths if matches_any_glob(p, ui_globs)]
|
|
143
|
+
if not ui_hits:
|
|
144
|
+
sys.exit(0)
|
|
145
|
+
|
|
146
|
+
# Find the `## Design calls` section and verify it has a populated row.
|
|
147
|
+
dc_section = re.search(
|
|
148
|
+
r'^##\s+Design\s+calls\s*$([\s\S]*?)(?=^##\s|\Z)',
|
|
149
|
+
content, re.MULTILINE | re.IGNORECASE,
|
|
150
|
+
)
|
|
151
|
+
body = dc_section.group(1).strip() if dc_section else ""
|
|
152
|
+
|
|
153
|
+
def is_populated(body):
|
|
154
|
+
# A populated body has at least one table row that isn't the header / separator.
|
|
155
|
+
# A row like `| --- | --- | ... |` is the separator (not a real row).
|
|
156
|
+
rows = [
|
|
157
|
+
ln for ln in body.splitlines()
|
|
158
|
+
if re.match(r'^\s*\|', ln) and not re.match(r'^\s*\|[\s:-]+\|', ln)
|
|
159
|
+
]
|
|
160
|
+
if len(rows) < 2:
|
|
161
|
+
return False
|
|
162
|
+
# The first row is the column header; the rest are data rows.
|
|
163
|
+
return any(not re.search(r'^\s*-?\s*\*?\(?none\)?\*?\s*$', r.strip("|").strip(), re.IGNORECASE) for r in rows[1:])
|
|
164
|
+
|
|
165
|
+
if dc_section and is_populated(body):
|
|
166
|
+
sys.exit(0)
|
|
167
|
+
|
|
168
|
+
reason_lines = [
|
|
169
|
+
f"Spec Design Calls Guard: '{rel}' has UI files in its write_set but lacks a populated `## Design calls` section.",
|
|
170
|
+
f" UI files detected: {', '.join(sorted(ui_hits))}",
|
|
171
|
+
" The `## Design calls` section is required when the spec's write_set intersects `project.json → tdd.ui_globs`.",
|
|
172
|
+
" See `.claude/skills/spec/template.md` for the canonical Design calls table shape.",
|
|
173
|
+
" See CLAUDE.md Article X.2 for the routing rule.",
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
print(json.dumps({
|
|
177
|
+
"hookSpecificOutput": {
|
|
178
|
+
"hookEventName": "PreToolUse",
|
|
179
|
+
"permissionDecision": "deny",
|
|
180
|
+
"permissionDecisionReason": "\n".join(reason_lines),
|
|
181
|
+
}
|
|
182
|
+
}))
|
|
183
|
+
PY
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Spec Diagram Presence Guard — PreToolUse(Write|Edit|MultiEdit)
|
|
3
|
+
#
|
|
4
|
+
# Enforces that docs/specs/*.md contains the diagram kinds required by the
|
|
5
|
+
# spec template. Complements artifact_template_guard (which checks headings)
|
|
6
|
+
# and plantuml_syntax_guard (which checks each block's syntax): this one
|
|
7
|
+
# ensures the right *kinds* of diagrams exist.
|
|
8
|
+
#
|
|
9
|
+
# Config source: .claude/project.json → artifacts.required_diagrams.spec
|
|
10
|
+
#
|
|
11
|
+
# Each entry is an object of the form:
|
|
12
|
+
# "<kind>": {
|
|
13
|
+
# "min": <int>, # required occurrences; default 1
|
|
14
|
+
# "marker": "<literal>", # optional literal substring to look for
|
|
15
|
+
# "any_of": ["<regex>", ...] # optional list; a block matching ANY counts
|
|
16
|
+
# }
|
|
17
|
+
#
|
|
18
|
+
# A fenced ```plantuml``` block counts toward <kind> if it contains the literal
|
|
19
|
+
# `marker` OR if any line matches any regex in `any_of`. The guard scans only
|
|
20
|
+
# inside plantuml fences — prose mentions don't satisfy the requirement.
|
|
21
|
+
#
|
|
22
|
+
# Template files (_TEMPLATE_*) are exempt. So is empty/whitespace-only content.
|
|
23
|
+
|
|
24
|
+
# shellcheck source=./lib/common.sh
|
|
25
|
+
. "${BASH_SOURCE[0]%/*}/lib/common.sh"
|
|
26
|
+
read_payload
|
|
27
|
+
|
|
28
|
+
TOOL="$(payload_get .tool_name)"
|
|
29
|
+
case "$TOOL" in
|
|
30
|
+
Write|Edit|MultiEdit) ;;
|
|
31
|
+
*) emit_allow ;;
|
|
32
|
+
esac
|
|
33
|
+
|
|
34
|
+
FILE="$(payload_get .tool_input.file_path)"
|
|
35
|
+
[ -n "$FILE" ] || emit_allow
|
|
36
|
+
rel="${FILE#$CLAUDE_PROJECT_ROOT/}"
|
|
37
|
+
|
|
38
|
+
case "$rel" in
|
|
39
|
+
docs/specs/*.md) ;;
|
|
40
|
+
*) emit_allow ;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
base="${rel##*/}"
|
|
44
|
+
case "$base" in
|
|
45
|
+
_TEMPLATE_*|*TEMPLATE*.md) emit_allow ;;
|
|
46
|
+
esac
|
|
47
|
+
|
|
48
|
+
required_json="$(project_get .artifacts.required_diagrams.spec)"
|
|
49
|
+
if [ -z "$required_json" ] || [ "$required_json" = "None" ]; then
|
|
50
|
+
emit_allow
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
HOOK_FILE="$FILE" HOOK_TOOL="$TOOL" HOOK_REL="$rel" HOOK_REQ_JSON="$required_json" python3 <<'PY'
|
|
54
|
+
import json, os, pathlib, re, sys
|
|
55
|
+
|
|
56
|
+
payload = json.loads(os.environ.get("HOOK_PAYLOAD", "") or "{}")
|
|
57
|
+
tool = os.environ["HOOK_TOOL"]
|
|
58
|
+
file_ = os.environ["HOOK_FILE"]
|
|
59
|
+
rel = os.environ["HOOK_REL"]
|
|
60
|
+
try:
|
|
61
|
+
required = json.loads(os.environ["HOOK_REQ_JSON"])
|
|
62
|
+
except Exception:
|
|
63
|
+
sys.exit(0)
|
|
64
|
+
if not isinstance(required, dict):
|
|
65
|
+
sys.exit(0)
|
|
66
|
+
|
|
67
|
+
ti = payload.get("tool_input") or {}
|
|
68
|
+
|
|
69
|
+
def current():
|
|
70
|
+
try:
|
|
71
|
+
return pathlib.Path(file_).read_text(encoding="utf-8")
|
|
72
|
+
except Exception:
|
|
73
|
+
return ""
|
|
74
|
+
|
|
75
|
+
if tool == "Write":
|
|
76
|
+
content = ti.get("content") or ""
|
|
77
|
+
elif tool == "Edit":
|
|
78
|
+
base = current()
|
|
79
|
+
old = ti.get("old_string") or ""
|
|
80
|
+
new = ti.get("new_string") or ""
|
|
81
|
+
if ti.get("replace_all"):
|
|
82
|
+
content = base.replace(old, new)
|
|
83
|
+
else:
|
|
84
|
+
content = base.replace(old, new, 1) if old in base else (base + new)
|
|
85
|
+
elif tool == "MultiEdit":
|
|
86
|
+
content = current()
|
|
87
|
+
for edit in (ti.get("edits") or []):
|
|
88
|
+
old = edit.get("old_string") or ""
|
|
89
|
+
new = edit.get("new_string") or ""
|
|
90
|
+
if edit.get("replace_all"):
|
|
91
|
+
content = content.replace(old, new)
|
|
92
|
+
else:
|
|
93
|
+
content = content.replace(old, new, 1) if old in content else (content + new)
|
|
94
|
+
else:
|
|
95
|
+
sys.exit(0)
|
|
96
|
+
|
|
97
|
+
if not content.strip():
|
|
98
|
+
sys.exit(0)
|
|
99
|
+
|
|
100
|
+
fence_re = re.compile(r'^[ \t]*```[ \t]*plantuml[ \t]*$(.*?)^[ \t]*```[ \t]*$',
|
|
101
|
+
re.DOTALL | re.IGNORECASE | re.MULTILINE)
|
|
102
|
+
blocks = [m.group(1) for m in fence_re.finditer(content)]
|
|
103
|
+
|
|
104
|
+
def block_matches(body, rule):
|
|
105
|
+
marker = rule.get("marker")
|
|
106
|
+
if marker and marker in body:
|
|
107
|
+
return True
|
|
108
|
+
for pat in rule.get("any_of") or []:
|
|
109
|
+
try:
|
|
110
|
+
if re.search(pat, body, re.MULTILINE):
|
|
111
|
+
return True
|
|
112
|
+
except re.error:
|
|
113
|
+
continue
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
missing = []
|
|
117
|
+
for kind, rule in required.items():
|
|
118
|
+
if not isinstance(rule, dict):
|
|
119
|
+
continue
|
|
120
|
+
need = int(rule.get("min", 1))
|
|
121
|
+
found = sum(1 for b in blocks if block_matches(b, rule))
|
|
122
|
+
if found < need:
|
|
123
|
+
missing.append((kind, need, found))
|
|
124
|
+
|
|
125
|
+
if not missing:
|
|
126
|
+
sys.exit(0)
|
|
127
|
+
|
|
128
|
+
lines = [f"Spec Diagram Presence Guard: '{rel}' is missing required diagram kinds. Each kind must appear inside a ```plantuml``` fence."]
|
|
129
|
+
for kind, need, found in missing:
|
|
130
|
+
lines.append(f" - {kind}: need {need}, found {found}")
|
|
131
|
+
lines.append("See .claude/skills/spec/template.md for the canonical diagram skeletons (C4 Context/Container/Component, class, sequence, dependency graph).")
|
|
132
|
+
lines.append("Required kinds are configured at .claude/project.json → artifacts.required_diagrams.spec.")
|
|
133
|
+
|
|
134
|
+
print(json.dumps({
|
|
135
|
+
"hookSpecificOutput": {
|
|
136
|
+
"hookEventName": "PreToolUse",
|
|
137
|
+
"permissionDecision": "deny",
|
|
138
|
+
"permissionDecisionReason": "\n".join(lines),
|
|
139
|
+
}
|
|
140
|
+
}))
|
|
141
|
+
PY
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Swarm Approval Guard — PreToolUse(Write|Edit|MultiEdit)
|
|
3
|
+
#
|
|
4
|
+
# Symmetric to spec_approval_guard for gate B (/approve-swarm). Two modes:
|
|
5
|
+
#
|
|
6
|
+
# 1. Approval artifacts (.claude/state/swarm_approvals/<slug>.approval) —
|
|
7
|
+
# writable only when a fresh slug-matched marker at
|
|
8
|
+
# .claude/state/.swarm_approval_grant exists. Marker is written by
|
|
9
|
+
# consent_gate_grant.sh on /approve-swarm. Validated + consumed via
|
|
10
|
+
# validate_consent_marker.
|
|
11
|
+
#
|
|
12
|
+
# 2. The marker file itself — Claude SHALL NEVER write it via tool.
|
|
13
|
+
|
|
14
|
+
# shellcheck source=./lib/common.sh
|
|
15
|
+
. "${BASH_SOURCE[0]%/*}/lib/common.sh"
|
|
16
|
+
read_payload
|
|
17
|
+
|
|
18
|
+
TOOL="$(payload_get .tool_name)"
|
|
19
|
+
case "$TOOL" in
|
|
20
|
+
Write|Edit|MultiEdit) ;;
|
|
21
|
+
*) emit_allow ;;
|
|
22
|
+
esac
|
|
23
|
+
|
|
24
|
+
FILE="$(payload_get .tool_input.file_path)"
|
|
25
|
+
[ -n "$FILE" ] || emit_allow
|
|
26
|
+
rel="$(canonical_rel "$FILE")"
|
|
27
|
+
[ -n "$rel" ] || emit_allow
|
|
28
|
+
|
|
29
|
+
block_marker_self_write "$rel" "$CONSENT_MARKER_SWARM_REL" "Swarm Approval Guard" "/approve-swarm <slug>"
|
|
30
|
+
|
|
31
|
+
case "$rel" in
|
|
32
|
+
.claude/state/swarm_approvals/*.approval)
|
|
33
|
+
expected_slug="$(canonical_slug "$(basename "$rel" .approval)")"
|
|
34
|
+
validate_consent_marker "$CONSENT_MARKER_SWARM" "Swarm Approval Guard" "/approve-swarm <slug>" "$expected_slug"
|
|
35
|
+
emit_allow
|
|
36
|
+
;;
|
|
37
|
+
esac
|
|
38
|
+
|
|
39
|
+
emit_allow
|