@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,141 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Artifact Template Guard — PreToolUse(Write|Edit|MultiEdit)
|
|
3
|
+
#
|
|
4
|
+
# Enforces that writes to docs/{intake,brd,specs,rca}/*.md include every
|
|
5
|
+
# required section heading for that artifact type. Required sections come
|
|
6
|
+
# from .claude/project.json → artifacts.required_sections.<type>.
|
|
7
|
+
#
|
|
8
|
+
# Template files (any basename starting with "_TEMPLATE_") are exempt — they
|
|
9
|
+
# ARE the canonical structure, and edits to them shouldn't self-check.
|
|
10
|
+
# Also exempt: writes where the proposed content is empty/whitespace only
|
|
11
|
+
# (the guard intervenes on substantive writes, not touch/clear operations).
|
|
12
|
+
#
|
|
13
|
+
# The guard inspects *proposed content* (what the tool is about to write),
|
|
14
|
+
# not the file on disk. For Edit: checks the resulting content by merging
|
|
15
|
+
# old_string → new_string into the existing file. For MultiEdit: same,
|
|
16
|
+
# applied sequentially. For Write: checks the new content directly.
|
|
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
|
+
# Identify artifact type from path.
|
|
33
|
+
artifact_type=""
|
|
34
|
+
case "$rel" in
|
|
35
|
+
docs/intake/*.md) artifact_type="intake" ;;
|
|
36
|
+
docs/brd/*.md) artifact_type="brd" ;;
|
|
37
|
+
docs/specs/*.md) artifact_type="spec" ;;
|
|
38
|
+
docs/rca/*.md) artifact_type="rca" ;;
|
|
39
|
+
*) emit_allow ;;
|
|
40
|
+
esac
|
|
41
|
+
|
|
42
|
+
# Exempt templates.
|
|
43
|
+
base="$(/usr/bin/basename "$rel" 2>/dev/null || echo "${rel##*/}")"
|
|
44
|
+
case "$base" in
|
|
45
|
+
_TEMPLATE_*|*TEMPLATE*.md) emit_allow ;;
|
|
46
|
+
esac
|
|
47
|
+
|
|
48
|
+
# Fetch required sections for this artifact type.
|
|
49
|
+
required_json="$(project_get ".artifacts.required_sections.$artifact_type")"
|
|
50
|
+
if [ -z "$required_json" ] || [ "$required_json" = "None" ]; then
|
|
51
|
+
# No requirements configured → don't enforce.
|
|
52
|
+
emit_allow
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Compute the resulting content the write would produce. For Write, that's
|
|
56
|
+
# tool_input.content. For Edit/MultiEdit, we apply the edits against the
|
|
57
|
+
# existing file (if any) to get the post-write content.
|
|
58
|
+
HOOK_FILE="$FILE" HOOK_TOOL="$TOOL" HOOK_REQ_JSON="$required_json" HOOK_REL="$rel" HOOK_ARTIFACT="$artifact_type" python3 <<'PY'
|
|
59
|
+
import json, os, re, sys, pathlib
|
|
60
|
+
|
|
61
|
+
payload = json.loads(os.environ.get("HOOK_PAYLOAD", "") or "{}")
|
|
62
|
+
tool = os.environ["HOOK_TOOL"]
|
|
63
|
+
file_ = os.environ["HOOK_FILE"]
|
|
64
|
+
rel = os.environ["HOOK_REL"]
|
|
65
|
+
atype = os.environ["HOOK_ARTIFACT"]
|
|
66
|
+
req = json.loads(os.environ["HOOK_REQ_JSON"])
|
|
67
|
+
if not isinstance(req, list):
|
|
68
|
+
# Malformed config — don't enforce, fail open.
|
|
69
|
+
sys.exit(0)
|
|
70
|
+
|
|
71
|
+
ti = payload.get("tool_input") or {}
|
|
72
|
+
|
|
73
|
+
def current_file_content():
|
|
74
|
+
try:
|
|
75
|
+
return pathlib.Path(file_).read_text(encoding="utf-8")
|
|
76
|
+
except Exception:
|
|
77
|
+
return ""
|
|
78
|
+
|
|
79
|
+
if tool == "Write":
|
|
80
|
+
content = ti.get("content") or ""
|
|
81
|
+
elif tool == "Edit":
|
|
82
|
+
base = current_file_content()
|
|
83
|
+
old = ti.get("old_string") or ""
|
|
84
|
+
new = ti.get("new_string") or ""
|
|
85
|
+
if ti.get("replace_all"):
|
|
86
|
+
content = base.replace(old, new)
|
|
87
|
+
else:
|
|
88
|
+
# Apply single replacement.
|
|
89
|
+
content = base.replace(old, new, 1) if old in base else (base + new)
|
|
90
|
+
elif tool == "MultiEdit":
|
|
91
|
+
content = current_file_content()
|
|
92
|
+
for edit in (ti.get("edits") or []):
|
|
93
|
+
old = edit.get("old_string") or ""
|
|
94
|
+
new = edit.get("new_string") or ""
|
|
95
|
+
if edit.get("replace_all"):
|
|
96
|
+
content = content.replace(old, new)
|
|
97
|
+
else:
|
|
98
|
+
content = content.replace(old, new, 1) if old in content else (content + new)
|
|
99
|
+
else:
|
|
100
|
+
sys.exit(0)
|
|
101
|
+
|
|
102
|
+
# Don't enforce on empty/whitespace-only content (touch or clear).
|
|
103
|
+
if not content.strip():
|
|
104
|
+
sys.exit(0)
|
|
105
|
+
|
|
106
|
+
# Collect heading text (## and ### levels) from the content.
|
|
107
|
+
headings = set()
|
|
108
|
+
for ln in content.splitlines():
|
|
109
|
+
m = re.match(r'^\s{0,3}#{2,4}\s+(.+?)\s*$', ln)
|
|
110
|
+
if m:
|
|
111
|
+
# Normalize: lowercase, strip trailing punctuation, collapse whitespace.
|
|
112
|
+
h = re.sub(r'\s+', ' ', m.group(1).strip()).lower()
|
|
113
|
+
h = h.rstrip(':').rstrip('.')
|
|
114
|
+
headings.add(h)
|
|
115
|
+
|
|
116
|
+
missing = []
|
|
117
|
+
for r in req:
|
|
118
|
+
r_norm = re.sub(r'\s+', ' ', str(r).strip()).lower().rstrip(':').rstrip('.')
|
|
119
|
+
if r_norm not in headings:
|
|
120
|
+
missing.append(r)
|
|
121
|
+
|
|
122
|
+
if not missing:
|
|
123
|
+
sys.exit(0)
|
|
124
|
+
|
|
125
|
+
msg = (
|
|
126
|
+
f"Artifact Template Guard: '{rel}' ({atype}) is missing required section(s): "
|
|
127
|
+
f"{', '.join(missing)}. "
|
|
128
|
+
f"Use the `{atype}` skill at .claude/skills/{atype}/SKILL.md (template at "
|
|
129
|
+
f".claude/skills/{atype}/template.md) to produce a compliant document. "
|
|
130
|
+
f"Every required heading must appear as a ## or ### heading."
|
|
131
|
+
)
|
|
132
|
+
print(json.dumps({
|
|
133
|
+
"hookSpecificOutput": {
|
|
134
|
+
"hookEventName": "PreToolUse",
|
|
135
|
+
"permissionDecision": "deny",
|
|
136
|
+
"permissionDecisionReason": msg,
|
|
137
|
+
}
|
|
138
|
+
}))
|
|
139
|
+
PY
|
|
140
|
+
|
|
141
|
+
exit 0
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Consent Gate Grant — UserPromptSubmit
|
|
3
|
+
#
|
|
4
|
+
# When the user types one of the three consent-gate slash commands —
|
|
5
|
+
# /approve-spec, /approve-swarm, /grant-commit — this hook fires BEFORE the
|
|
6
|
+
# model is invoked. It writes a short-lived consent marker to
|
|
7
|
+
# .claude/state/.<gate>_grant.
|
|
8
|
+
#
|
|
9
|
+
# The marker is what makes the corresponding approval-token write succeed:
|
|
10
|
+
# the gate-specific PreToolUse guard (spec_approval_guard, swarm_approval_guard,
|
|
11
|
+
# git_commit_guard) reads the marker and allows Claude's write only if a
|
|
12
|
+
# fresh, slug-matched marker is on disk.
|
|
13
|
+
#
|
|
14
|
+
# Why the marker is unforgeable by Claude:
|
|
15
|
+
# - This hook runs on UserPromptSubmit, OUTSIDE Claude's tool boundary.
|
|
16
|
+
# - The PreToolUse guards block Claude from writing the marker file.
|
|
17
|
+
# - Markers expire after consent.gate_marker_ttl_seconds (default 60).
|
|
18
|
+
#
|
|
19
|
+
# Marker shapes (also documented in lib/common.sh validate_consent_marker):
|
|
20
|
+
# .spec_approval_grant line 1: basename of spec path (slug)
|
|
21
|
+
# line 2: epoch
|
|
22
|
+
# line 3: absolute spec path
|
|
23
|
+
# .swarm_approval_grant line 1: slug · line 2: epoch
|
|
24
|
+
# .commit_consent_grant line 1: epoch · line 2: optional note
|
|
25
|
+
|
|
26
|
+
# shellcheck source=./lib/common.sh
|
|
27
|
+
. "${BASH_SOURCE[0]%/*}/lib/common.sh"
|
|
28
|
+
read_payload
|
|
29
|
+
|
|
30
|
+
# Fast-path: glob-match against the raw payload to rule out 99% of prompts
|
|
31
|
+
# before any json/regex parsing. False positives are tolerated; the regex
|
|
32
|
+
# dispatch below would no-op anyway.
|
|
33
|
+
case "$HOOK_PAYLOAD" in
|
|
34
|
+
*'"prompt":'*/approve-spec*) ;;
|
|
35
|
+
*'"prompt":'*/approve-swarm*) ;;
|
|
36
|
+
*'"prompt":'*/grant-commit*) ;;
|
|
37
|
+
*) exit 0 ;;
|
|
38
|
+
esac
|
|
39
|
+
|
|
40
|
+
PROMPT="$(payload_get .prompt)"
|
|
41
|
+
[ -n "$PROMPT" ] || exit 0
|
|
42
|
+
|
|
43
|
+
first_line="${PROMPT%%$'\n'*}"
|
|
44
|
+
trimmed="${first_line#"${first_line%%[![:space:]]*}"}"
|
|
45
|
+
|
|
46
|
+
NOW="$(date +%s)"
|
|
47
|
+
|
|
48
|
+
write_marker_atomic() {
|
|
49
|
+
local marker="$1"
|
|
50
|
+
shift
|
|
51
|
+
local tmp="${marker}.tmp.$$"
|
|
52
|
+
if printf '%s\n' "$@" >"$tmp" 2>/dev/null && mv -f "$tmp" "$marker" 2>/dev/null; then
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
55
|
+
rm -f "$tmp" 2>/dev/null || true
|
|
56
|
+
return 1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if [[ "$trimmed" =~ ^/approve-spec[[:space:]]+([^[:space:]]+) ]]; then
|
|
60
|
+
arg="${BASH_REMATCH[1]}"
|
|
61
|
+
slug="$(canonical_slug "$arg")"
|
|
62
|
+
case "$arg" in
|
|
63
|
+
/*) abs_path="$arg" ;;
|
|
64
|
+
*/*) abs_path="$CLAUDE_PROJECT_ROOT/$arg" ;;
|
|
65
|
+
*) abs_path="$CLAUDE_PROJECT_ROOT/docs/specs/$slug.md" ;;
|
|
66
|
+
esac
|
|
67
|
+
if write_marker_atomic "$CONSENT_MARKER_SPEC" "$slug" "$NOW" "$abs_path"; then
|
|
68
|
+
log_line consent_gate_grant "wrote spec_approval_grant slug=$slug path=$abs_path"
|
|
69
|
+
else
|
|
70
|
+
log_line consent_gate_grant "FAILED write spec_approval_grant slug=$slug"
|
|
71
|
+
fi
|
|
72
|
+
elif [[ "$trimmed" =~ ^/approve-swarm[[:space:]]+([^[:space:]]+) ]]; then
|
|
73
|
+
slug="$(canonical_slug "${BASH_REMATCH[1]}")"
|
|
74
|
+
if write_marker_atomic "$CONSENT_MARKER_SWARM" "$slug" "$NOW"; then
|
|
75
|
+
log_line consent_gate_grant "wrote swarm_approval_grant slug=$slug"
|
|
76
|
+
else
|
|
77
|
+
log_line consent_gate_grant "FAILED write swarm_approval_grant slug=$slug"
|
|
78
|
+
fi
|
|
79
|
+
elif [[ "$trimmed" =~ ^/grant-commit([[:space:]].*)?$ ]]; then
|
|
80
|
+
note="${BASH_REMATCH[1]:-}"
|
|
81
|
+
note="${note#"${note%%[![:space:]]*}"}"
|
|
82
|
+
if write_marker_atomic "$CONSENT_MARKER_COMMIT" "$NOW" "$note"; then
|
|
83
|
+
log_line consent_gate_grant "wrote commit_consent_grant note=$note"
|
|
84
|
+
else
|
|
85
|
+
log_line consent_gate_grant "FAILED write commit_consent_grant"
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
exit 0
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Destructive Command Guard — PreToolUse(Bash)
|
|
3
|
+
#
|
|
4
|
+
# Two tiers:
|
|
5
|
+
# - hard_block_patterns: block outright, cannot be overridden here (user
|
|
6
|
+
# would need to remove the pattern from project.json).
|
|
7
|
+
# - ask_patterns: emit an "ask" decision so the user is prompted each time.
|
|
8
|
+
#
|
|
9
|
+
# Patterns come from .destructive.hard_block_patterns / .destructive.ask_patterns
|
|
10
|
+
# in .claude/project.json. Mode selector .destructive.mode is "ask" (default)
|
|
11
|
+
# or "block" — block upgrades ask_patterns to deny.
|
|
12
|
+
|
|
13
|
+
# shellcheck source=./lib/common.sh
|
|
14
|
+
. "${BASH_SOURCE[0]%/*}/lib/common.sh"
|
|
15
|
+
read_payload
|
|
16
|
+
|
|
17
|
+
TOOL="$(payload_get .tool_name)"
|
|
18
|
+
[ "$TOOL" = "Bash" ] || emit_allow
|
|
19
|
+
|
|
20
|
+
CMD="$(payload_get .tool_input.command)"
|
|
21
|
+
[ -n "$CMD" ] || emit_allow
|
|
22
|
+
|
|
23
|
+
hard="$(project_get .destructive.hard_block_patterns)"
|
|
24
|
+
if [ -n "$hard" ] && cmd_matches_any "$CMD" "$hard"; then
|
|
25
|
+
log_line destructive_cmd_guard "HARD BLOCK: $CMD"
|
|
26
|
+
emit_block "Destructive Command Guard: '$CMD' matches a hard-block pattern (catastrophic/irreversible). This is not overridable by confirmation. If this is genuinely necessary, edit .claude/project.json .destructive.hard_block_patterns."
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
mode="$(project_get .destructive.mode)"
|
|
30
|
+
[ -z "$mode" ] && mode="ask"
|
|
31
|
+
|
|
32
|
+
ask="$(project_get .destructive.ask_patterns)"
|
|
33
|
+
if [ -n "$ask" ] && cmd_matches_any "$CMD" "$ask"; then
|
|
34
|
+
if [ "$mode" = "block" ]; then
|
|
35
|
+
log_line destructive_cmd_guard "BLOCK (mode=block): $CMD"
|
|
36
|
+
emit_block "Destructive Command Guard: '$CMD' matches a destructive pattern and mode=block. Ask the user to run this themselves, or set .destructive.mode to 'ask' in project.json."
|
|
37
|
+
fi
|
|
38
|
+
log_line destructive_cmd_guard "ASK: $CMD"
|
|
39
|
+
emit_ask "Destructive Command Guard: '$CMD' looks destructive (matches an ask pattern). Confirm this is intentional before proceeding."
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
emit_allow
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# .env file guard — PreToolUse(Edit|Write|MultiEdit|NotebookEdit)
|
|
3
|
+
#
|
|
4
|
+
# Blocks any write to files matching .env patterns that are likely to hold
|
|
5
|
+
# secrets. Allows .env.example / .env.sample (template files that don't hold
|
|
6
|
+
# real secrets).
|
|
7
|
+
|
|
8
|
+
# shellcheck source=./lib/common.sh
|
|
9
|
+
. "${BASH_SOURCE[0]%/*}/lib/common.sh"
|
|
10
|
+
read_payload
|
|
11
|
+
|
|
12
|
+
TOOL="$(payload_get .tool_name)"
|
|
13
|
+
case "$TOOL" in
|
|
14
|
+
Edit|Write|MultiEdit|NotebookEdit) ;;
|
|
15
|
+
*) emit_allow ;;
|
|
16
|
+
esac
|
|
17
|
+
|
|
18
|
+
FILE="$(payload_get .tool_input.file_path)"
|
|
19
|
+
[ -n "$FILE" ] || emit_allow
|
|
20
|
+
|
|
21
|
+
base="$(basename "$FILE")"
|
|
22
|
+
|
|
23
|
+
# Allow clearly-safe template files.
|
|
24
|
+
case "$base" in
|
|
25
|
+
.env.example|.env.sample|.env.template|.env.dist|.env.defaults) emit_allow ;;
|
|
26
|
+
esac
|
|
27
|
+
|
|
28
|
+
# Block anything else matching .env*.
|
|
29
|
+
case "$base" in
|
|
30
|
+
.env|.env.*|*.env)
|
|
31
|
+
log_line env_guard "BLOCKED $FILE"
|
|
32
|
+
emit_block ".env file guard: '$FILE' looks like a secrets file. seed.md forbids edits to .env files. If this is a template, rename to .env.example."
|
|
33
|
+
;;
|
|
34
|
+
esac
|
|
35
|
+
|
|
36
|
+
emit_allow
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Git Commit Guard — PreToolUse(Bash) and PreToolUse(Write|Edit|MultiEdit)
|
|
3
|
+
#
|
|
4
|
+
# Two roles:
|
|
5
|
+
#
|
|
6
|
+
# 1. Bash matcher (run-boundary) — gates `git commit` invocations on a
|
|
7
|
+
# fresh consent token at .claude/state/commit_consent (default TTL 5
|
|
8
|
+
# min). Hard-blocks forbidden git operations regardless of consent.
|
|
9
|
+
#
|
|
10
|
+
# 2. Write matcher (write-boundary) — gates Claude's writes to the consent
|
|
11
|
+
# files themselves:
|
|
12
|
+
# - .claude/state/.commit_consent_grant — the marker. Written only by
|
|
13
|
+
# consent_gate_grant.sh (UserPromptSubmit) on /grant-commit.
|
|
14
|
+
# - .claude/state/commit_consent — the consent token. Writable by
|
|
15
|
+
# Claude only when a fresh marker is on disk (consumed on success).
|
|
16
|
+
#
|
|
17
|
+
# This makes gate C structurally symmetric with gates A and B.
|
|
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
|
+
Bash)
|
|
26
|
+
: # fall through to run-boundary logic below
|
|
27
|
+
;;
|
|
28
|
+
Write|Edit|MultiEdit)
|
|
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_COMMIT_REL" "Git Commit Guard" "/grant-commit"
|
|
35
|
+
|
|
36
|
+
case "$rel" in
|
|
37
|
+
.claude/state/commit_consent)
|
|
38
|
+
validate_consent_marker "$CONSENT_MARKER_COMMIT" "Git Commit Guard" "/grant-commit"
|
|
39
|
+
emit_allow
|
|
40
|
+
;;
|
|
41
|
+
*)
|
|
42
|
+
emit_allow
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
;;
|
|
46
|
+
*)
|
|
47
|
+
emit_allow
|
|
48
|
+
;;
|
|
49
|
+
esac
|
|
50
|
+
|
|
51
|
+
CMD="$(payload_get .tool_input.command)"
|
|
52
|
+
[ -n "$CMD" ] || emit_allow
|
|
53
|
+
|
|
54
|
+
case "$CMD" in
|
|
55
|
+
*git\ *|git) ;;
|
|
56
|
+
*) emit_allow ;;
|
|
57
|
+
esac
|
|
58
|
+
|
|
59
|
+
# Hard-blocks (apply always, consent cannot override).
|
|
60
|
+
FORBIDDEN_RE='(\bgit\s+push\b|\bgit\s+commit\b[^|&;]*--amend|--no-verify|--no-gpg-sign|\bgit\s+reset\s+--hard\b|\bgit\s+clean\s+-[a-zA-Z]*f\b|\bgit\s+checkout\s+--\s|\bgit\s+branch\s+-D\b|\bgit\s+config\b|\bgit\s+rebase\s+-i\b|\bgit\s+add\s+-i\b|\bgit\s+add\s+(-A|\.)(?![A-Za-z0-9_/.\-]))'
|
|
61
|
+
if python3 -c "import re,sys; sys.exit(0 if re.search(r'''$FORBIDDEN_RE''', sys.argv[1]) else 1)" "$CMD"; then
|
|
62
|
+
log_line git_commit_guard "BLOCKED forbidden git op: $CMD"
|
|
63
|
+
emit_block "Git Commit Guard: forbidden git operation detected. seed.md forbids git push / --amend / --no-verify / reset --hard / clean -f / checkout -- / branch -D / config / rebase -i / add -A|. unless the user explicitly names the operation. Ask the user to approve by stating the exact command."
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
if ! python3 -c "import re,sys; sys.exit(0 if re.search(r'\bgit\s+commit\b', sys.argv[1]) else 1)" "$CMD"; then
|
|
67
|
+
emit_allow
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
CONSENT_FILE="$STATE_DIR/commit_consent"
|
|
71
|
+
COMMIT_TTL="$(project_get .consent.commit_ttl_seconds)"
|
|
72
|
+
[ -z "$COMMIT_TTL" ] && COMMIT_TTL=300
|
|
73
|
+
|
|
74
|
+
if [ ! -f "$CONSENT_FILE" ]; then
|
|
75
|
+
log_line git_commit_guard "BLOCKED no consent file: $CMD"
|
|
76
|
+
emit_block "Git Commit Guard: no consent granted. The user must run \`/grant-commit\` before a commit is allowed. Consent is valid for ${COMMIT_TTL}s."
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
read -r granted_at < "$CONSENT_FILE" 2>/dev/null
|
|
80
|
+
now="$(date +%s)"
|
|
81
|
+
if ! [[ "$granted_at" =~ ^[0-9]+$ ]]; then
|
|
82
|
+
log_line git_commit_guard "BLOCKED malformed consent file"
|
|
83
|
+
emit_block "Git Commit Guard: consent file is malformed. Ask the user to re-run \`/grant-commit\`."
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
age=$(( now - granted_at ))
|
|
87
|
+
if [ "$age" -gt "$COMMIT_TTL" ]; then
|
|
88
|
+
log_line git_commit_guard "BLOCKED consent expired age=${age}s ttl=${COMMIT_TTL}s"
|
|
89
|
+
emit_block "Git Commit Guard: consent expired (${age}s old, TTL ${COMMIT_TTL}s). Ask the user to re-run \`/grant-commit\`."
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
log_line git_commit_guard "ALLOWED age=${age}s cmd=$CMD"
|
|
93
|
+
emit_allow
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Harness Continuation — Stop event
|
|
3
|
+
#
|
|
4
|
+
# Auto-continues multi-phase workflows across non-gated phase boundaries.
|
|
5
|
+
# Reads .claude/state/harness_state (written by the harness skill on every
|
|
6
|
+
# tick) and decides whether to re-fire harness on the same turn or stay
|
|
7
|
+
# silent.
|
|
8
|
+
#
|
|
9
|
+
# Three-rung gate (plus sanity rail) — ALL three must pass to emit a block:
|
|
10
|
+
# 1. stop_hook_active flag absent on payload (avoids in-turn recursion).
|
|
11
|
+
# 2. .claude/state/.harness_active exists (session-scoped in-the-loop marker;
|
|
12
|
+
# the harness skill creates it on continue, deletes it on yielded/done;
|
|
13
|
+
# memory_session_start.sh deletes it on session boundary).
|
|
14
|
+
# 3. harness_state.state equals "continue".
|
|
15
|
+
#
|
|
16
|
+
# Sanity rail: if the marker's slug content disagrees with workflow.json.slug,
|
|
17
|
+
# log one WARN line to harness_continuation.log; the decision is unchanged.
|
|
18
|
+
#
|
|
19
|
+
# If all three pass, emit {"decision":"block","reason":"..."} to stdout.
|
|
20
|
+
# Otherwise: exit 0 silent. Internal failures are treated as silence.
|
|
21
|
+
|
|
22
|
+
# shellcheck source=./lib/common.sh
|
|
23
|
+
. "${BASH_SOURCE[0]%/*}/lib/common.sh"
|
|
24
|
+
read_payload
|
|
25
|
+
|
|
26
|
+
# Rung 1: stop_hook_active prevents recursive re-firing inside a single turn.
|
|
27
|
+
STOP_ACTIVE="$(payload_get .stop_hook_active)"
|
|
28
|
+
case "$STOP_ACTIVE" in
|
|
29
|
+
true|True|TRUE)
|
|
30
|
+
log_line harness_continuation "silent: rung1 stop_hook_active=true"
|
|
31
|
+
exit 0
|
|
32
|
+
;;
|
|
33
|
+
esac
|
|
34
|
+
|
|
35
|
+
# Rung 2: active marker presence — session-scoped "in the loop" signal.
|
|
36
|
+
MARKER="$STATE_DIR/.harness_active"
|
|
37
|
+
if [ ! -f "$MARKER" ]; then
|
|
38
|
+
log_line harness_continuation "silent: rung2 marker missing ($MARKER)"
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Rung 3 (plus sanity rail + emit) — delegate to python for JSON parsing.
|
|
43
|
+
HARNESS_STATE="$STATE_DIR/harness_state"
|
|
44
|
+
if [ ! -r "$HARNESS_STATE" ]; then
|
|
45
|
+
log_line harness_continuation "silent: rung3a harness_state missing or unreadable"
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
HARNESS_STATE="$HARNESS_STATE" \
|
|
50
|
+
WORKFLOW_JSON="$STATE_DIR/workflow.json" \
|
|
51
|
+
MARKER_PATH="$MARKER" \
|
|
52
|
+
LOG_PATH="$LOG_DIR/harness_continuation.log" \
|
|
53
|
+
python3 <<'PY' || exit 0
|
|
54
|
+
import json, os, sys, time
|
|
55
|
+
|
|
56
|
+
state_path = os.environ['HARNESS_STATE']
|
|
57
|
+
workflow_path = os.environ.get('WORKFLOW_JSON', '')
|
|
58
|
+
marker_path = os.environ.get('MARKER_PATH', '')
|
|
59
|
+
log_path = os.environ.get('LOG_PATH', '')
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _log(level, message):
|
|
63
|
+
if not log_path:
|
|
64
|
+
return
|
|
65
|
+
try:
|
|
66
|
+
with open(log_path, 'a') as f:
|
|
67
|
+
ts = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
|
68
|
+
f.write(f'{ts} {level} {message}\n')
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _warn(message):
|
|
74
|
+
_log('WARN', message)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Rung 3: parse harness_state and check state field.
|
|
78
|
+
try:
|
|
79
|
+
with open(state_path) as f:
|
|
80
|
+
data = json.load(f)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
_log('INFO', f'silent: rung3b harness_state unparseable ({e!s})')
|
|
83
|
+
sys.exit(0)
|
|
84
|
+
|
|
85
|
+
state_value = data.get('state')
|
|
86
|
+
if state_value != 'continue':
|
|
87
|
+
_log('INFO', f'silent: rung3c state={state_value!r} (expected "continue")')
|
|
88
|
+
sys.exit(0)
|
|
89
|
+
|
|
90
|
+
# Sanity rail: marker slug should match workflow.json slug.
|
|
91
|
+
# Mismatch is a WARN log line; the decision is unchanged.
|
|
92
|
+
marker_slug = ''
|
|
93
|
+
if marker_path and os.path.exists(marker_path):
|
|
94
|
+
try:
|
|
95
|
+
with open(marker_path) as f:
|
|
96
|
+
marker_slug = f.read().strip()
|
|
97
|
+
except Exception:
|
|
98
|
+
marker_slug = ''
|
|
99
|
+
|
|
100
|
+
workflow_slug = ''
|
|
101
|
+
if workflow_path and os.path.exists(workflow_path):
|
|
102
|
+
try:
|
|
103
|
+
with open(workflow_path) as f:
|
|
104
|
+
wf = json.load(f)
|
|
105
|
+
workflow_slug = wf.get('slug') or ''
|
|
106
|
+
except Exception:
|
|
107
|
+
workflow_slug = ''
|
|
108
|
+
|
|
109
|
+
if marker_slug and workflow_slug and marker_slug != workflow_slug:
|
|
110
|
+
_warn(f'slug mismatch: marker={marker_slug} workflow={workflow_slug}')
|
|
111
|
+
|
|
112
|
+
# All rungs passed — emit the block decision.
|
|
113
|
+
decision = {
|
|
114
|
+
'decision': 'block',
|
|
115
|
+
'reason': 'Workflow continuing per harness_state. Invoke Skill(harness) to advance to the next phase.',
|
|
116
|
+
}
|
|
117
|
+
print(json.dumps(decision))
|
|
118
|
+
_log('INFO', 'emit: decision=block (all rungs passed)')
|
|
119
|
+
PY
|
|
120
|
+
|
|
121
|
+
exit 0
|