@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.
Files changed (197) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +222 -0
  3. package/bin/cli.js +247 -0
  4. package/obj/template/.claude/agents/swarm-worker.md +52 -0
  5. package/obj/template/.claude/bin/LICENSE +201 -0
  6. package/obj/template/.claude/bin/NOTICE +48 -0
  7. package/obj/template/.claude/commands/approve-spec.md +29 -0
  8. package/obj/template/.claude/commands/approve-swarm.md +27 -0
  9. package/obj/template/.claude/commands/grant-commit.md +19 -0
  10. package/obj/template/.claude/commands/init-project.md +191 -0
  11. package/obj/template/.claude/hooks/artifact_template_guard.sh +141 -0
  12. package/obj/template/.claude/hooks/consent_gate_grant.sh +89 -0
  13. package/obj/template/.claude/hooks/destructive_cmd_guard.sh +42 -0
  14. package/obj/template/.claude/hooks/env_guard.sh +36 -0
  15. package/obj/template/.claude/hooks/git_commit_guard.sh +93 -0
  16. package/obj/template/.claude/hooks/harness_continuation.sh +121 -0
  17. package/obj/template/.claude/hooks/lib/__pycache__/resume_writer.cpython-314.pyc +0 -0
  18. package/obj/template/.claude/hooks/lib/common.sh +328 -0
  19. package/obj/template/.claude/hooks/lib/resume_writer.py +341 -0
  20. package/obj/template/.claude/hooks/lint_runner.sh +55 -0
  21. package/obj/template/.claude/hooks/memory_pre_compact.sh +36 -0
  22. package/obj/template/.claude/hooks/memory_session_start.sh +244 -0
  23. package/obj/template/.claude/hooks/memory_stop.sh +173 -0
  24. package/obj/template/.claude/hooks/plantuml_syntax_guard.sh +161 -0
  25. package/obj/template/.claude/hooks/process_lifecycle_guard.sh +89 -0
  26. package/obj/template/.claude/hooks/setup_guard.sh +50 -0
  27. package/obj/template/.claude/hooks/spec_approval_guard.sh +81 -0
  28. package/obj/template/.claude/hooks/spec_design_calls_guard.sh +183 -0
  29. package/obj/template/.claude/hooks/spec_diagram_presence_guard.sh +141 -0
  30. package/obj/template/.claude/hooks/swarm_approval_guard.sh +39 -0
  31. package/obj/template/.claude/hooks/swarm_boundary_guard.sh +136 -0
  32. package/obj/template/.claude/hooks/tdd_order_guard.sh +176 -0
  33. package/obj/template/.claude/hooks/test_runner.sh +75 -0
  34. package/obj/template/.claude/hooks/tests/fixtures/ac008_byte_equal_reference.txt +12 -0
  35. package/obj/template/.claude/hooks/tests/memory_session_start_test.sh +285 -0
  36. package/obj/template/.claude/hooks/track_guard.sh +127 -0
  37. package/obj/template/.claude/hooks/verify_pass_guard.sh +88 -0
  38. package/obj/template/.claude/memory/README.md +108 -0
  39. package/obj/template/.claude/memory/_pending.md +15 -0
  40. package/obj/template/.claude/memory/_resume.md +12 -0
  41. package/obj/template/.claude/memory/conventions.md +26 -0
  42. package/obj/template/.claude/memory/decisions.md +29 -0
  43. package/obj/template/.claude/memory/landmarks.md +26 -0
  44. package/obj/template/.claude/memory/landmines.md +27 -0
  45. package/obj/template/.claude/memory/libraries.md +27 -0
  46. package/obj/template/.claude/memory/pending-questions.md +28 -0
  47. package/obj/template/.claude/project.json +221 -0
  48. package/obj/template/.claude/settings.json +110 -0
  49. package/obj/template/.claude/skills/archive/SKILL.md +48 -0
  50. package/obj/template/.claude/skills/archive/archive.sh +145 -0
  51. package/obj/template/.claude/skills/audit-baseline/SKILL.md +80 -0
  52. package/obj/template/.claude/skills/audit-baseline/audit.sh +919 -0
  53. package/obj/template/.claude/skills/brd/SKILL.md +44 -0
  54. package/obj/template/.claude/skills/brd/template.md +83 -0
  55. package/obj/template/.claude/skills/chore/SKILL.md +99 -0
  56. package/obj/template/.claude/skills/claude-automation-recommender/LICENSE +202 -0
  57. package/obj/template/.claude/skills/claude-automation-recommender/NOTICE +69 -0
  58. package/obj/template/.claude/skills/claude-automation-recommender/SKILL.md +358 -0
  59. package/obj/template/.claude/skills/claude-automation-recommender/references/hooks-patterns.md +226 -0
  60. package/obj/template/.claude/skills/claude-automation-recommender/references/mcp-servers.md +263 -0
  61. package/obj/template/.claude/skills/claude-automation-recommender/references/plugins-reference.md +98 -0
  62. package/obj/template/.claude/skills/claude-automation-recommender/references/skills-reference.md +408 -0
  63. package/obj/template/.claude/skills/claude-automation-recommender/references/subagent-templates.md +181 -0
  64. package/obj/template/.claude/skills/code-structure/SKILL.md +204 -0
  65. package/obj/template/.claude/skills/commit/SKILL.md +21 -0
  66. package/obj/template/.claude/skills/copywriting/SKILL.md +252 -0
  67. package/obj/template/.claude/skills/copywriting/evals/evals.json +111 -0
  68. package/obj/template/.claude/skills/copywriting/references/ai-writing-detection.md +200 -0
  69. package/obj/template/.claude/skills/copywriting/references/copy-frameworks.md +344 -0
  70. package/obj/template/.claude/skills/copywriting/references/natural-transitions.md +272 -0
  71. package/obj/template/.claude/skills/design-ui/SKILL.md +175 -0
  72. package/obj/template/.claude/skills/design-ui/references/design-vs-development.md +89 -0
  73. package/obj/template/.claude/skills/design-ui/references/intent-table.md +64 -0
  74. package/obj/template/.claude/skills/design-ui/references/orchestration.md +121 -0
  75. package/obj/template/.claude/skills/design-ui/references/state-machine.md +125 -0
  76. package/obj/template/.claude/skills/document/SKILL.md +66 -0
  77. package/obj/template/.claude/skills/documentation/SKILL.md +50 -0
  78. package/obj/template/.claude/skills/harness/SKILL.md +169 -0
  79. package/obj/template/.claude/skills/humanizer/SKILL.md +489 -0
  80. package/obj/template/.claude/skills/humanizer/references/ai-writing-detection.md +208 -0
  81. package/obj/template/.claude/skills/impeccable/PROJECT_NOTES.md +22 -0
  82. package/obj/template/.claude/skills/impeccable/SKILL.md +153 -0
  83. package/obj/template/.claude/skills/impeccable/agents/openai.yaml +4 -0
  84. package/obj/template/.claude/skills/impeccable/reference/adapt.md +190 -0
  85. package/obj/template/.claude/skills/impeccable/reference/animate.md +173 -0
  86. package/obj/template/.claude/skills/impeccable/reference/audit.md +134 -0
  87. package/obj/template/.claude/skills/impeccable/reference/bolder.md +113 -0
  88. package/obj/template/.claude/skills/impeccable/reference/brand.md +104 -0
  89. package/obj/template/.claude/skills/impeccable/reference/clarify.md +174 -0
  90. package/obj/template/.claude/skills/impeccable/reference/cognitive-load.md +106 -0
  91. package/obj/template/.claude/skills/impeccable/reference/color-and-contrast.md +105 -0
  92. package/obj/template/.claude/skills/impeccable/reference/colorize.md +154 -0
  93. package/obj/template/.claude/skills/impeccable/reference/craft.md +138 -0
  94. package/obj/template/.claude/skills/impeccable/reference/critique.md +213 -0
  95. package/obj/template/.claude/skills/impeccable/reference/delight.md +302 -0
  96. package/obj/template/.claude/skills/impeccable/reference/distill.md +111 -0
  97. package/obj/template/.claude/skills/impeccable/reference/document.md +427 -0
  98. package/obj/template/.claude/skills/impeccable/reference/extract.md +70 -0
  99. package/obj/template/.claude/skills/impeccable/reference/harden.md +347 -0
  100. package/obj/template/.claude/skills/impeccable/reference/heuristics-scoring.md +234 -0
  101. package/obj/template/.claude/skills/impeccable/reference/interaction-design.md +195 -0
  102. package/obj/template/.claude/skills/impeccable/reference/layout.md +141 -0
  103. package/obj/template/.claude/skills/impeccable/reference/live.md +513 -0
  104. package/obj/template/.claude/skills/impeccable/reference/motion-design.md +99 -0
  105. package/obj/template/.claude/skills/impeccable/reference/onboard.md +234 -0
  106. package/obj/template/.claude/skills/impeccable/reference/optimize.md +258 -0
  107. package/obj/template/.claude/skills/impeccable/reference/overdrive.md +130 -0
  108. package/obj/template/.claude/skills/impeccable/reference/personas.md +178 -0
  109. package/obj/template/.claude/skills/impeccable/reference/polish.md +232 -0
  110. package/obj/template/.claude/skills/impeccable/reference/product.md +62 -0
  111. package/obj/template/.claude/skills/impeccable/reference/quieter.md +99 -0
  112. package/obj/template/.claude/skills/impeccable/reference/responsive-design.md +114 -0
  113. package/obj/template/.claude/skills/impeccable/reference/shape.md +136 -0
  114. package/obj/template/.claude/skills/impeccable/reference/spatial-design.md +100 -0
  115. package/obj/template/.claude/skills/impeccable/reference/teach.md +137 -0
  116. package/obj/template/.claude/skills/impeccable/reference/typeset.md +124 -0
  117. package/obj/template/.claude/skills/impeccable/reference/typography.md +159 -0
  118. package/obj/template/.claude/skills/impeccable/reference/ux-writing.md +107 -0
  119. package/obj/template/.claude/skills/impeccable/scripts/cleanup-deprecated.mjs +284 -0
  120. package/obj/template/.claude/skills/impeccable/scripts/command-metadata.json +94 -0
  121. package/obj/template/.claude/skills/impeccable/scripts/design-parser.mjs +820 -0
  122. package/obj/template/.claude/skills/impeccable/scripts/detect-csp.mjs +198 -0
  123. package/obj/template/.claude/skills/impeccable/scripts/is-generated.mjs +69 -0
  124. package/obj/template/.claude/skills/impeccable/scripts/live-accept.mjs +465 -0
  125. package/obj/template/.claude/skills/impeccable/scripts/live-browser.js +4684 -0
  126. package/obj/template/.claude/skills/impeccable/scripts/live-inject.mjs +436 -0
  127. package/obj/template/.claude/skills/impeccable/scripts/live-poll.mjs +187 -0
  128. package/obj/template/.claude/skills/impeccable/scripts/live-server.mjs +679 -0
  129. package/obj/template/.claude/skills/impeccable/scripts/live-wrap.mjs +395 -0
  130. package/obj/template/.claude/skills/impeccable/scripts/live.mjs +247 -0
  131. package/obj/template/.claude/skills/impeccable/scripts/load-context.mjs +93 -0
  132. package/obj/template/.claude/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
  133. package/obj/template/.claude/skills/impeccable/scripts/pin.mjs +214 -0
  134. package/obj/template/.claude/skills/implement/SKILL.md +83 -0
  135. package/obj/template/.claude/skills/intake/SKILL.md +46 -0
  136. package/obj/template/.claude/skills/intake/template.md +61 -0
  137. package/obj/template/.claude/skills/integrate/SKILL.md +62 -0
  138. package/obj/template/.claude/skills/memory-flush/SKILL.md +172 -0
  139. package/obj/template/.claude/skills/memory-flush/sweep.py +286 -0
  140. package/obj/template/.claude/skills/memory-flush/tests/run.sh +327 -0
  141. package/obj/template/.claude/skills/prose/SKILL.md +119 -0
  142. package/obj/template/.claude/skills/rca/SKILL.md +42 -0
  143. package/obj/template/.claude/skills/rca/template.md +83 -0
  144. package/obj/template/.claude/skills/research/SKILL.md +75 -0
  145. package/obj/template/.claude/skills/scenario/SKILL.md +64 -0
  146. package/obj/template/.claude/skills/scout/SKILL.md +72 -0
  147. package/obj/template/.claude/skills/security/SKILL.md +75 -0
  148. package/obj/template/.claude/skills/simplify/SKILL.md +67 -0
  149. package/obj/template/.claude/skills/spec/SKILL.md +69 -0
  150. package/obj/template/.claude/skills/spec/template.md +274 -0
  151. package/obj/template/.claude/skills/spec-diagram-review/SKILL.md +81 -0
  152. package/obj/template/.claude/skills/spec-lint/SKILL.md +55 -0
  153. package/obj/template/.claude/skills/spec-lint/lint.sh +218 -0
  154. package/obj/template/.claude/skills/spec-render/SKILL.md +45 -0
  155. package/obj/template/.claude/skills/spec-render/render.sh +109 -0
  156. package/obj/template/.claude/skills/spec-traceability-review/SKILL.md +72 -0
  157. package/obj/template/.claude/skills/swarm-dispatch/SKILL.md +212 -0
  158. package/obj/template/.claude/skills/swarm-dispatch/swarm_merge.sh +154 -0
  159. package/obj/template/.claude/skills/swarm-plan/SKILL.md +90 -0
  160. package/obj/template/.claude/skills/swarm-plan/validate.sh +181 -0
  161. package/obj/template/.claude/skills/tdd/SKILL.md +100 -0
  162. package/obj/template/.claude/skills/technical-tutorials/SKILL.md +569 -0
  163. package/obj/template/.claude/skills/technical-tutorials/references/audience-context-README.md +53 -0
  164. package/obj/template/.claude/skills/technical-tutorials/references/audience-context.md +246 -0
  165. package/obj/template/.claude/skills/technical-tutorials/references/audience-example.md +175 -0
  166. package/obj/template/.claude/skills/technical-tutorials/references/audience-template.md +152 -0
  167. package/obj/template/.claude/skills/triage/SKILL.md +55 -0
  168. package/obj/template/.claude/skills/verify/SKILL.md +74 -0
  169. package/obj/template/.mcp.json +24 -0
  170. package/obj/template/CLAUDE.md +327 -0
  171. package/obj/template/docs/init/seed.md +585 -0
  172. package/obj/template/manifest.json +214 -0
  173. package/package.json +48 -0
  174. package/src/.mcp.template.json +24 -0
  175. package/src/.npmrc.template +2 -0
  176. package/src/CLAUDE.template.md +327 -0
  177. package/src/agents/swarm-worker.template.md +51 -0
  178. package/src/cli/conflict.js +31 -0
  179. package/src/cli/doctor.js +152 -0
  180. package/src/cli/install.js +93 -0
  181. package/src/cli/io.js +27 -0
  182. package/src/cli/manifest.js +38 -0
  183. package/src/cli/mcp.js +54 -0
  184. package/src/cli/merge.js +107 -0
  185. package/src/cli/plantuml.js +121 -0
  186. package/src/cli/util.js +10 -0
  187. package/src/memory/_pending.template.md +15 -0
  188. package/src/memory/_resume.template.md +12 -0
  189. package/src/memory/conventions.template.md +26 -0
  190. package/src/memory/decisions.template.md +29 -0
  191. package/src/memory/landmarks.template.md +26 -0
  192. package/src/memory/landmines.template.md +27 -0
  193. package/src/memory/libraries.template.md +27 -0
  194. package/src/memory/pending-questions.template.md +28 -0
  195. package/src/project.template.json +221 -0
  196. package/src/seed.template.md +585 -0
  197. package/src/settings.template.json +110 -0
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env bash
2
+ # Swarm Boundary Guard — PreToolUse(Write|Edit|MultiEdit)
3
+ #
4
+ # Enforces the swarm invariant: within an active wave, writes may only touch
5
+ # files that appear in some task's declared write_set. Because write_sets are
6
+ # pairwise disjoint within a wave (enforced by swarm-plan at plan time), any
7
+ # write uniquely maps back to exactly one task — so the guard does not need
8
+ # to identify which agent is writing. It only needs to verify the file is
9
+ # owned by SOMEONE in the active wave.
10
+ #
11
+ # Control file:
12
+ # .claude/state/swarm/active_wave.json
13
+ # Format:
14
+ # {
15
+ # "slug": "<slug>",
16
+ # "wave": <n>,
17
+ # "started_at": <epoch>,
18
+ # "write_sets": [
19
+ # {"task_id": "T-001", "files": ["src/foo.py", "tests/foo_test.py"]},
20
+ # ...
21
+ # ]
22
+ # }
23
+ #
24
+ # Semantics:
25
+ # - active_wave.json missing → not in swarm, allow.
26
+ # - file path under an exempt prefix → allow (tooling/state/vcs writes).
27
+ # - file path in enforced prefix and in union(write_sets) → allow.
28
+ # - file path in enforced prefix and NOT in any write_set → deny.
29
+ # - file path NOT in enforced prefix → allow (hook scope is narrow on purpose).
30
+ #
31
+ # Exempt / enforced prefixes come from project.json → swarm.exempt_path_prefixes
32
+ # / swarm.enforced_path_prefixes. Sensible defaults if absent.
33
+
34
+ # shellcheck source=./lib/common.sh
35
+ . "${BASH_SOURCE[0]%/*}/lib/common.sh"
36
+ read_payload
37
+
38
+ TOOL="$(payload_get .tool_name)"
39
+ case "$TOOL" in
40
+ Write|Edit|MultiEdit) ;;
41
+ *) emit_allow ;;
42
+ esac
43
+
44
+ FILE="$(payload_get .tool_input.file_path)"
45
+ [ -n "$FILE" ] || emit_allow
46
+ rel="${FILE#$CLAUDE_PROJECT_ROOT/}"
47
+
48
+ ACTIVE_WAVE="$STATE_DIR/swarm/active_wave.json"
49
+ if [ ! -f "$ACTIVE_WAVE" ]; then
50
+ # Not in a swarm — guard is dormant.
51
+ emit_allow
52
+ fi
53
+
54
+ HOOK_REL="$rel" HOOK_ACTIVE="$ACTIVE_WAVE" HOOK_PROJECT_JSON="$PROJECT_JSON" python3 <<'PY'
55
+ import json, os, sys
56
+
57
+ rel = os.environ["HOOK_REL"]
58
+ active_path = os.environ["HOOK_ACTIVE"]
59
+ pj_path = os.environ["HOOK_PROJECT_JSON"]
60
+
61
+ # Load active wave.
62
+ try:
63
+ active = json.load(open(active_path))
64
+ except Exception as e:
65
+ # Corrupt active_wave.json — fail CLOSED. If we're inside a swarm we need
66
+ # this file to be readable; if we can't read it, we cannot safely allow
67
+ # arbitrary writes.
68
+ print(json.dumps({
69
+ "hookSpecificOutput": {
70
+ "hookEventName": "PreToolUse",
71
+ "permissionDecision": "deny",
72
+ "permissionDecisionReason": (
73
+ f"Swarm Boundary Guard: active_wave.json exists but could not be parsed ({e}). "
74
+ "This is a swarm-state corruption — swarm-dispatch must clean up and re-plan."
75
+ ),
76
+ }
77
+ }))
78
+ sys.exit(0)
79
+
80
+ # Load config.
81
+ exempt_prefixes = [".claude/", ".git/"]
82
+ enforced_prefixes = None
83
+ try:
84
+ pj = json.load(open(pj_path))
85
+ sw = (pj.get("swarm") or {})
86
+ if isinstance(sw.get("exempt_path_prefixes"), list):
87
+ exempt_prefixes = sw["exempt_path_prefixes"]
88
+ if isinstance(sw.get("enforced_path_prefixes"), list):
89
+ enforced_prefixes = sw["enforced_path_prefixes"]
90
+ except Exception:
91
+ pass
92
+
93
+ # Exempt paths (tooling / vcs / deps) — never enforced.
94
+ for p in exempt_prefixes:
95
+ if rel.startswith(p):
96
+ sys.exit(0)
97
+
98
+ # If enforced_prefixes is set, only enforce within those roots.
99
+ # If absent, enforce on everything not exempt.
100
+ if enforced_prefixes is not None:
101
+ if not any(rel.startswith(p) for p in enforced_prefixes):
102
+ sys.exit(0)
103
+
104
+ # Build union of active write_sets.
105
+ write_sets = active.get("write_sets") or []
106
+ owners = {} # file -> task_id
107
+ for entry in write_sets:
108
+ tid = entry.get("task_id", "?")
109
+ for f in (entry.get("files") or []):
110
+ owners[f] = tid
111
+
112
+ if rel in owners:
113
+ sys.exit(0)
114
+
115
+ # Plan drift: file not owned by any active task in this wave.
116
+ slug = active.get("slug", "?")
117
+ wave = active.get("wave", "?")
118
+ owners_preview = ", ".join(sorted(set(owners.keys()))[:6])
119
+ if len(owners) > 6:
120
+ owners_preview += f", … ({len(owners)} total)"
121
+
122
+ msg = (
123
+ f"Swarm Boundary Guard: write to '{rel}' denied. "
124
+ f"Swarm '{slug}' wave {wave} is active; no task in this wave owns that file. "
125
+ f"Files owned by this wave: {owners_preview or '(none)'}. "
126
+ "Either (a) abort this write, (b) stop the swarm and re-plan so the file is in some task's write_set, "
127
+ "or (c) if this is a genuinely required file that was missed at plan time, surface it — do not patch mid-wave."
128
+ )
129
+ print(json.dumps({
130
+ "hookSpecificOutput": {
131
+ "hookEventName": "PreToolUse",
132
+ "permissionDecision": "deny",
133
+ "permissionDecisionReason": msg,
134
+ }
135
+ }))
136
+ PY
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env bash
2
+ # TDD Order Guard — PreToolUse(Write)
3
+ #
4
+ # When Claude creates a new source file (first write, file does not exist),
5
+ # require that a corresponding test file already exists. Enforces
6
+ # test-before-source TDD per seed.md § "TDD order guard".
7
+ #
8
+ # Applies only if .tdd.enabled is true in project.json. Skips edits to
9
+ # existing files (TDD ordering applies to file creation, not later edits).
10
+ # Honours source/test/exempt globs from project.json.
11
+
12
+ # shellcheck source=./lib/common.sh
13
+ . "${BASH_SOURCE[0]%/*}/lib/common.sh"
14
+ read_payload
15
+
16
+ TOOL="$(payload_get .tool_name)"
17
+ [ "$TOOL" = "Write" ] || emit_allow
18
+
19
+ enabled="$(project_get .tdd.enabled)"
20
+ [ "$enabled" = "True" ] || [ "$enabled" = "true" ] || emit_allow
21
+
22
+ FILE="$(payload_get .tool_input.file_path)"
23
+ [ -n "$FILE" ] || emit_allow
24
+
25
+ # Only apply on file *creation*. If file already exists, it's an edit.
26
+ [ -e "$FILE" ] && emit_allow
27
+
28
+ # Relative path for glob matching.
29
+ rel="${FILE#$CLAUDE_PROJECT_ROOT/}"
30
+
31
+ # Exempt patterns (docs, config, .claude itself, etc.).
32
+ if path_matches_globs "$rel" "$(project_get .tdd.exempt_globs)"; then
33
+ emit_allow
34
+ fi
35
+
36
+ # Not a source path → nothing to enforce.
37
+ if ! path_matches_globs "$rel" "$(project_get .tdd.source_globs)"; then
38
+ emit_allow
39
+ fi
40
+
41
+ # Never gate test files themselves.
42
+ if path_matches_globs "$rel" "$(project_get .tdd.test_globs)"; then
43
+ emit_allow
44
+ fi
45
+
46
+ # Look for a corresponding test file. Candidates are derived from
47
+ # `project.json → tdd.test_globs` so customized projects (Go-style
48
+ # `<pkg>_test.go`, Rust `tests/integration.rs`, jest `__tests__/*`,
49
+ # etc.) are honored without code changes here.
50
+ stem="$(basename "$FILE")"
51
+ name="${stem%.*}"
52
+ ext="${stem##*.}"
53
+ dir="$(dirname "$rel")"
54
+
55
+ # Candidate generation in Python — reads tdd.test_globs and produces
56
+ # combinations of {dir-root × name × suffix/prefix} plus mirrored layout.
57
+ candidates="$(python3 - "$rel" "$CLAUDE_PROJECT_ROOT" <<'PY' 2>/dev/null || true
58
+ import sys, os, re, json, pathlib
59
+
60
+ rel = sys.argv[1]
61
+ root = pathlib.Path(sys.argv[2])
62
+ proj_path = root / ".claude/project.json"
63
+ try:
64
+ cfg = json.loads(proj_path.read_text(encoding="utf-8"))
65
+ except Exception:
66
+ cfg = {}
67
+ test_globs = (cfg.get("tdd", {}) or {}).get("test_globs", []) or []
68
+
69
+ stem = os.path.basename(rel)
70
+ name, _, ext = stem.rpartition(".")
71
+ ext = ext or "py"
72
+ src_dir = os.path.dirname(rel)
73
+
74
+ # Extension family — when source is .js/.mjs/.cjs, tests may use any of
75
+ # the JS-ESM-family extensions. Same for .ts/.tsx/.mts/.cts. Without this,
76
+ # a .js source whose test is .mjs (a common node:test ESM convention)
77
+ # would fail the existence check and the guard would falsely block.
78
+ JS_FAMILY = {"js", "mjs", "cjs"}
79
+ TS_FAMILY = {"ts", "tsx", "mts", "cts"}
80
+ if ext in JS_FAMILY:
81
+ ext_variants = list(JS_FAMILY)
82
+ elif ext in TS_FAMILY:
83
+ ext_variants = list(TS_FAMILY)
84
+ else:
85
+ ext_variants = [ext]
86
+
87
+ # Strip a source-root prefix so candidates can mirror the layout under a
88
+ # parallel test-root: src/foo/bar.py → foo/bar.py.
89
+ src_subpath = rel
90
+ for r in ("src/", "lib/", "app/", "pkg/", "internal/"):
91
+ if rel.startswith(r):
92
+ src_subpath = rel[len(r):]
93
+ break
94
+ src_subpath_noext = re.sub(r"\.[^./]+$", "", src_subpath)
95
+
96
+ suffix_patterns = [] # e.g. "_test", ".test", ".spec" — appended to name
97
+ prefix_patterns = [] # e.g. "test_" — prepended to name
98
+ dir_roots = [] # e.g. "tests", "test", "spec", "__tests__"
99
+
100
+ for g in test_globs:
101
+ # **/*<sep><word>.* → suffix `<sep><word>` e.g. "_test", ".test"
102
+ m = re.match(r"^\*\*/\*([._-][^*/.]+)\.\*$", g)
103
+ if m:
104
+ suffix_patterns.append(m.group(1))
105
+ continue
106
+ # **/<word>_*.* → prefix "<word>_" e.g. "test_*"
107
+ m = re.match(r"^\*\*/([^*/.]+)_\*\.\*$", g)
108
+ if m:
109
+ prefix_patterns.append(m.group(1) + "_")
110
+ continue
111
+ # <dir>/** → directory root
112
+ m = re.match(r"^([\w._-]+)/\*\*$", g)
113
+ if m:
114
+ dir_roots.append(m.group(1))
115
+ continue
116
+
117
+ # Backstop conventions for any pattern dimension the configured globs
118
+ # don't cover. The common project.json ships directory globs only
119
+ # (`tests/**` etc.) — without these backstops we'd miss pytest's
120
+ # `test_<name>` prefix and the standard `_test`/`.test`/`.spec` suffixes.
121
+ if not suffix_patterns:
122
+ suffix_patterns = ["_test", ".test", ".spec"]
123
+ if not prefix_patterns:
124
+ prefix_patterns = ["test_"]
125
+ if not dir_roots:
126
+ dir_roots = ["tests", "test", "spec", "__tests__"]
127
+
128
+ cands = set()
129
+ def add(p): cands.add(p.lstrip("/"))
130
+
131
+ # Co-located beside the source file (across extension family)
132
+ for e in ext_variants:
133
+ for s in suffix_patterns:
134
+ add(f"{src_dir}/{name}{s}.{e}")
135
+ for p in prefix_patterns:
136
+ add(f"{src_dir}/{p}{name}.{e}")
137
+
138
+ # Under each dir-root: stem-based + mirrored-layout (across extension family)
139
+ for d in dir_roots:
140
+ add(f"{d}/{src_subpath}") # mirror layout: tests/foo/bar.py
141
+ for e in ext_variants:
142
+ add(f"{d}/{name}.{e}") # plain stem (Rust-like)
143
+ for s in suffix_patterns:
144
+ add(f"{d}/{name}{s}.{e}")
145
+ add(f"{d}/{src_subpath_noext}{s}.{e}")
146
+ for p in prefix_patterns:
147
+ add(f"{d}/{p}{name}.{e}")
148
+ add(f"{d}/{p}{src_subpath_noext}.{e}")
149
+
150
+ # Co-located inside __tests__-style subdirs (Jest)
151
+ for d in dir_roots:
152
+ if d.startswith("_") or d == "__tests__":
153
+ for e in ext_variants:
154
+ for s in suffix_patterns:
155
+ add(f"{src_dir}/{d}/{name}{s}.{e}")
156
+
157
+ print("\n".join(sorted(cands)))
158
+ PY
159
+ )"
160
+
161
+ found=""
162
+ while IFS= read -r c; do
163
+ [ -z "$c" ] && continue
164
+ if [ -f "$CLAUDE_PROJECT_ROOT/$c" ]; then
165
+ found="$c"
166
+ break
167
+ fi
168
+ done <<< "$candidates"
169
+
170
+ if [ -n "$found" ]; then
171
+ log_line tdd_order_guard "ALLOWED test exists: $found for $rel"
172
+ emit_allow
173
+ fi
174
+
175
+ log_line tdd_order_guard "BLOCKED no test for: $rel"
176
+ emit_block "TDD Order Guard: no test file found for new source '$rel'. Write the failing test first. Candidates were derived from project.json → tdd.test_globs (e.g. tests/${name}_test.${ext}, ${dir}/${name}_test.${ext}, tests/${src_subpath_noext:-${name}}.${ext}). If this file truly has no tests by design, add the path to .tdd.exempt_globs in .claude/project.json."
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ # Test Runner hook — PostToolUse(Edit|Write|MultiEdit)
3
+ #
4
+ # Runs the project-configured test command against the changed file's
5
+ # affected tests. This is a GUIDE hook: until `.claude/project.json`
6
+ # declares `test.cmd`, it emits guidance pointing at `/init-project` rather
7
+ # than failing. Once configured, it executes the command and surfaces
8
+ # failures as stderr info (PostToolUse cannot block the edit that already
9
+ # happened, but it surfaces test failures immediately so Claude reacts).
10
+ #
11
+ # Projects are free to replace this script entirely with their own logic.
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
+ case "$TOOL" in
19
+ Edit|Write|MultiEdit) ;;
20
+ *) emit_allow ;;
21
+ esac
22
+
23
+ FILE="$(payload_get .tool_input.file_path)"
24
+ [ -n "$FILE" ] || emit_allow
25
+ rel="${FILE#$CLAUDE_PROJECT_ROOT/}"
26
+
27
+ # Skip obviously non-code changes.
28
+ case "$rel" in
29
+ *.md|*.json|*.yaml|*.yml|*.toml|*.txt|docs/*|.claude/*|.config/*) emit_allow ;;
30
+ esac
31
+
32
+ configured="$(project_get .configured)"
33
+ cmd="$(project_get .test.cmd)"
34
+
35
+ if [ "$configured" != "True" ] && [ "$configured" != "true" ]; then
36
+ emit_info "Test Runner: .claude/project.json is not configured yet. Run \`/init-project\` to declare the test command for this repo. (Skipping test run for '$rel'.)"
37
+ emit_allow
38
+ fi
39
+
40
+ if [ -z "$cmd" ] || [ "$cmd" = "None" ]; then
41
+ emit_info "Test Runner: no .test.cmd set in .claude/project.json. Skipping tests for '$rel'."
42
+ emit_allow
43
+ fi
44
+
45
+ # Resolve affected tests. If a custom resolver is configured, delegate to it;
46
+ # otherwise pass the changed file path and let the test command decide.
47
+ resolver="$(project_get .test.affected_resolver)"
48
+ affected=""
49
+ if [ -n "$resolver" ] && [ "$resolver" != "None" ]; then
50
+ if [ -x "$CLAUDE_PROJECT_ROOT/$resolver" ]; then
51
+ affected="$("$CLAUDE_PROJECT_ROOT/$resolver" "$rel" 2>/dev/null || true)"
52
+ else
53
+ emit_info "Test Runner: affected_resolver '$resolver' not found or not executable."
54
+ fi
55
+ fi
56
+
57
+ timeout_s="$(project_get .test.timeout_seconds)"
58
+ [ -z "$timeout_s" ] && timeout_s=120
59
+
60
+ # Compose final command. {file} and {affected} placeholders are substituted.
61
+ final="${cmd//\{file\}/$rel}"
62
+ final="${final//\{affected\}/$affected}"
63
+
64
+ emit_info "Test Runner: running \`$final\` (timeout ${timeout_s}s)"
65
+ out="$(cd "$CLAUDE_PROJECT_ROOT" && timeout "${timeout_s}s" bash -lc "$final" 2>&1)"
66
+ rc=$?
67
+ if [ $rc -ne 0 ]; then
68
+ log_line test_runner "FAIL rc=$rc cmd=$final"
69
+ emit_info "Test Runner: FAILED (exit $rc) — output:"
70
+ emit_info "$out"
71
+ exit 2
72
+ fi
73
+
74
+ log_line test_runner "PASS cmd=$final"
75
+ emit_allow
@@ -0,0 +1,12 @@
1
+ ## Project memory — index (.claude/memory/)
2
+
3
+ HEAD: `n/a` · total entries: 32 · stale (>=30 commits old): 0
4
+
5
+ | File | Entries | Stale | Status |
6
+ |---|---:|---:|---|
7
+ | `landmarks.md` | 19 | 0 | ok |
8
+ | `libraries.md` | 3 | 0 | ok |
9
+ | `decisions.md` | 1 | 0 | ok |
10
+ | `landmines.md` | 5 | 0 | ok |
11
+ | `conventions.md` | 3 | 0 | ok |
12
+ | `pending-questions.md` | 1 | 0 | ok |
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env bash
2
+ # Fixture-based integration tests for memory_session_start.sh
3
+ # Covers AC-003, AC-005, AC-007, AC-008 from docs/specs/memory-lifecycle-closure.md
4
+ #
5
+ # Each test builds a synthetic .claude/memory/ tree under a tempdir, invokes
6
+ # the hook with PROJECT_ROOT pointed at that tempdir, and asserts on the
7
+ # emitted additionalContext JSON.
8
+
9
+ set -uo pipefail
10
+
11
+ HERE="$(cd "$(dirname "$0")" && pwd)"
12
+ REPO_ROOT="$(cd "$HERE/../../.." && pwd)"
13
+ HOOK="$REPO_ROOT/.claude/hooks/memory_session_start.sh"
14
+ FIXTURES="$HERE/fixtures"
15
+
16
+ PASS=0; FAIL=0; FAILED=()
17
+
18
+ # --- assertion helpers --------------------------------------------------------
19
+
20
+ fail() { echo " FAIL: $*"; return 1; }
21
+
22
+ assert_contains() {
23
+ local haystack="$1" needle="$2" msg="$3"
24
+ case "$haystack" in
25
+ *"$needle"*) return 0 ;;
26
+ *) fail "$msg :: expected to contain: $needle" ;;
27
+ esac
28
+ }
29
+
30
+ assert_not_contains() {
31
+ local haystack="$1" needle="$2" msg="$3"
32
+ case "$haystack" in
33
+ *"$needle"*) fail "$msg :: should NOT contain: $needle" ;;
34
+ *) return 0 ;;
35
+ esac
36
+ }
37
+
38
+ # Run the hook against a project root. $1 = project root containing .claude/memory/.
39
+ # Prints the parsed additionalContext to stdout. Returns 1 if hook crashed.
40
+ run_hook() {
41
+ local proj="$1"
42
+ CLAUDE_PROJECT_DIR="$proj" \
43
+ bash "$HOOK" <<< '{}' 2>/dev/null | python3 -c '
44
+ import json, sys
45
+ data = sys.stdin.read().strip()
46
+ if not data:
47
+ sys.exit(2)
48
+ j = json.loads(data)
49
+ print(j["hookSpecificOutput"]["additionalContext"])
50
+ '
51
+ }
52
+
53
+ # Build a minimal synthetic memory tree. $1 = root.
54
+ seed_tree() {
55
+ local root="$1"
56
+ mkdir -p "$root/.claude/memory" "$root/.claude/state/harness"
57
+ # Frontmatter-only files so hook reports 0 entries unless we add ## blocks.
58
+ for f in landmarks libraries decisions landmines conventions pending-questions; do
59
+ cat > "$root/.claude/memory/$f.md" <<'EOF'
60
+ ---
61
+ owners: [test]
62
+ size-cap: 500
63
+ key: test
64
+ ---
65
+
66
+ # Test fixture
67
+ EOF
68
+ done
69
+ # Minimal _pending.md so the hook does not emit the nag by accident.
70
+ cat > "$root/.claude/memory/_pending.md" <<'EOF'
71
+ ---
72
+ owners: [test]
73
+ ---
74
+
75
+ # Pending
76
+ EOF
77
+ }
78
+
79
+ # Add an entry block to a canonical file. $1 root, $2 file basename, $3 entry body.
80
+ add_entry() {
81
+ local root="$1" file="$2"; shift 2
82
+ printf '\n%s\n' "$*" >> "$root/.claude/memory/$file.md"
83
+ }
84
+
85
+ # Compute an ISO date N days ago (portable: GNU or BSD date).
86
+ days_ago() {
87
+ local n="$1"
88
+ if date -u -d "$n days ago" +%Y-%m-%d 2>/dev/null; then return; fi
89
+ date -u -v "-${n}d" +%Y-%m-%d
90
+ }
91
+
92
+ run() {
93
+ local name="$1"
94
+ echo "RUN $name"
95
+ if "$name"; then
96
+ PASS=$((PASS+1)); echo "PASS $name"
97
+ else
98
+ FAIL=$((FAIL+1)); FAILED+=("$name"); echo "FAIL $name"
99
+ fi
100
+ }
101
+
102
+ # --- tests --------------------------------------------------------------------
103
+
104
+ test_when_7_entries_stale_then_top_5_listed_oldest_first() {
105
+ local tmp; tmp="$(mktemp -d)"; trap "rm -rf $tmp" RETURN
106
+ seed_tree "$tmp"
107
+ # 7 entries across 3 files with ages 100/95/90/50/40/30/5 days, no closure fields.
108
+ local i=0
109
+ for spec in "landmarks:lm-a:100" "landmarks:lm-b:95" "libraries:lib-a:90" \
110
+ "libraries:lib-b:50" "decisions:dec-a:40" "decisions:dec-b:30" \
111
+ "conventions:conv-a:5"; do
112
+ local file="${spec%%:*}" key key_age age
113
+ key="$(echo "$spec" | cut -d: -f2)"
114
+ age="${spec##*:}"
115
+ add_entry "$tmp" "$file" "## $key
116
+
117
+ - role: test
118
+ - verified-at: HEAD
119
+ - last-touched: $(days_ago "$age")"
120
+ i=$((i+1))
121
+ done
122
+ local out; out="$(run_hook "$tmp")"
123
+ assert_contains "$out" "## Stale entries" "AC-003 stale block missing" || return 1
124
+ # The 5 oldest by last-touched are 100/95/90/50/40 → lm-a lm-b lib-a lib-b dec-a.
125
+ for k in lm-a lm-b lib-a lib-b dec-a; do
126
+ assert_contains "$out" "$k" "AC-003 expected key $k in stale block" || return 1
127
+ done
128
+ # 30-day-old entry IS stale (>=30 day threshold for non-git), but only top 5 listed.
129
+ # conv-a (5 days) is not stale; should be absent from the block.
130
+ # With 6 stale entries total, the 6th (dec-b) should appear via overflow.
131
+ assert_contains "$out" "and 1 more" "AC-003 overflow indicator missing" || return 1
132
+ assert_not_contains "$out" "## conv-a" "AC-003 non-stale should not appear in block" || return 1
133
+ }
134
+
135
+ test_when_8_stale_then_overflow_indicator_shows_3_more() {
136
+ local tmp; tmp="$(mktemp -d)"; trap "rm -rf $tmp" RETURN
137
+ seed_tree "$tmp"
138
+ # 8 stale entries, ages 200..130 by 10s.
139
+ local i=0
140
+ for age in 200 190 180 170 160 150 140 130; do
141
+ add_entry "$tmp" "landmarks" "## ent-$i
142
+
143
+ - role: test
144
+ - verified-at: HEAD
145
+ - last-touched: $(days_ago "$age")"
146
+ i=$((i+1))
147
+ done
148
+ local out; out="$(run_hook "$tmp")"
149
+ assert_contains "$out" "## Stale entries" "AC-003 stale block missing" || return 1
150
+ assert_contains "$out" "and 3 more" "AC-003 expected overflow '… and 3 more'" || return 1
151
+ }
152
+
153
+ test_when_5_stale_with_identical_last_touched_then_alphabetical_by_file_colon_key() {
154
+ local tmp; tmp="$(mktemp -d)"; trap "rm -rf $tmp" RETURN
155
+ seed_tree "$tmp"
156
+ local d; d="$(days_ago 120)"
157
+ # Place entries across files in non-alphabetical insertion order; expect
158
+ # alphabetical-by-file:key in the rendered block.
159
+ add_entry "$tmp" "landmines" "## zeta
160
+
161
+ - role: test
162
+ - verified-at: HEAD
163
+ - last-touched: $d"
164
+ add_entry "$tmp" "landmarks" "## alpha
165
+
166
+ - role: test
167
+ - verified-at: HEAD
168
+ - last-touched: $d"
169
+ add_entry "$tmp" "decisions" "## kappa
170
+
171
+ - role: test
172
+ - verified-at: HEAD
173
+ - last-touched: $d"
174
+ add_entry "$tmp" "libraries" "## beta
175
+
176
+ - role: test
177
+ - verified-at: HEAD
178
+ - last-touched: $d"
179
+ add_entry "$tmp" "conventions" "## mu
180
+
181
+ - role: test
182
+ - verified-at: HEAD
183
+ - last-touched: $d"
184
+ local out; out="$(run_hook "$tmp")"
185
+ # Expected lexicographic order of "<file>:<key>":
186
+ # conventions:mu < decisions:kappa < landmarks:alpha < landmines:zeta < libraries:beta
187
+ # Find the first appearance position of each key in the stale block.
188
+ local block; block="$(printf '%s\n' "$out" | awk '/## Stale entries/{flag=1;next}/^## /{flag=0}flag')"
189
+ local order; order="$(printf '%s\n' "$block" | grep -oE '(alpha|beta|kappa|mu|zeta)' | head -5 | tr '\n' ' ')"
190
+ assert_contains " $order" " mu kappa alpha zeta beta " "AC-003 alphabetical-by-file:key order wrong: got [$order]" || return 1
191
+ }
192
+
193
+ test_when_closure_field_and_stale_sha_then_not_counted_stale() {
194
+ local tmp; tmp="$(mktemp -d)"; trap "rm -rf $tmp" RETURN
195
+ seed_tree "$tmp"
196
+ # Entry stale by date but carries resolved-at on pending-questions → MUST be excluded.
197
+ add_entry "$tmp" "pending-questions" "## Q-999
198
+
199
+ - Question: stub
200
+ - verified-at: HEAD
201
+ - last-touched: $(days_ago 200)
202
+ - resolved-at: $(days_ago 1)"
203
+ # Plus a normal stale entry on landmarks (no closure) → still counted.
204
+ add_entry "$tmp" "landmarks" "## still-open
205
+
206
+ - role: test
207
+ - verified-at: HEAD
208
+ - last-touched: $(days_ago 200)"
209
+ local out; out="$(run_hook "$tmp")"
210
+ assert_not_contains "$out" "Q-999" "AC-005 closed entry must not appear in stale block" || return 1
211
+ # Header count must be 1, not 2.
212
+ if printf '%s\n' "$out" | grep -qE 'stale \(>=30 commits old\): 1\b'; then :; else
213
+ fail "AC-005 expected stale count 1 in header"
214
+ return 1
215
+ fi
216
+ assert_contains "$out" "still-open" "AC-005 sanity: unclosed stale entry should appear" || return 1
217
+ }
218
+
219
+ test_when_audit_runs_against_changed_repo_then_exit_0() {
220
+ ( cd "$REPO_ROOT" && bash .claude/skills/audit-baseline/audit.sh >/dev/null 2>&1 ) \
221
+ || { fail "AC-007 audit exited non-zero"; return 1; }
222
+ }
223
+
224
+ test_when_hook_runs_unchanged_tree_then_header_and_table_byte_equal() {
225
+ # Run hook against the real repo memory tree and compare header+table to
226
+ # the captured pre-spec reference.
227
+ local out; out="$(run_hook "$REPO_ROOT")"
228
+ local actual_block
229
+ actual_block="$(printf '%s\n' "$out" | python3 -c '
230
+ import sys
231
+ lines = sys.stdin.read().split("\n")
232
+ started = False
233
+ out = []
234
+ for ln in lines:
235
+ if ln.startswith("## Project memory"):
236
+ started = True
237
+ if not started:
238
+ continue
239
+ out.append(ln)
240
+ if ln.startswith("| `pending-questions.md`"):
241
+ break
242
+ sys.stdout.write("\n".join(out) + "\n")
243
+ ')"
244
+ local expected; expected="$(cat "$FIXTURES/ac008_byte_equal_reference.txt")"
245
+ if [ "$actual_block" = "$expected" ]; then return 0; fi
246
+ fail "AC-008 header+table not byte-equal"
247
+ diff <(printf '%s' "$expected") <(printf '%s' "$actual_block") || true
248
+ return 1
249
+ }
250
+
251
+ test_when_old_resume_snapshot_reinjected_then_hook_does_not_crash() {
252
+ local tmp; tmp="$(mktemp -d)"; trap "rm -rf $tmp" RETURN
253
+ seed_tree "$tmp"
254
+ # Old _resume.md from a pre-spec session — minimal body, no new index format.
255
+ cat > "$tmp/.claude/memory/_resume.md" <<'EOF'
256
+ ---
257
+ written_at: 2026-04-30T12:00:00Z
258
+ ---
259
+
260
+ # Resume snapshot (legacy)
261
+ Last phase: spec
262
+ EOF
263
+ local out
264
+ out="$(run_hook "$tmp")" || { fail "AC-008 hook crashed on legacy _resume"; return 1; }
265
+ assert_contains "$out" "## Project memory" "AC-008 expected index header still present" || return 1
266
+ assert_contains "$out" "Resume snapshot (legacy)" "AC-008 legacy body should be appended" || return 1
267
+ }
268
+
269
+ # --- runner -------------------------------------------------------------------
270
+
271
+ run test_when_7_entries_stale_then_top_5_listed_oldest_first
272
+ run test_when_8_stale_then_overflow_indicator_shows_3_more
273
+ run test_when_5_stale_with_identical_last_touched_then_alphabetical_by_file_colon_key
274
+ run test_when_closure_field_and_stale_sha_then_not_counted_stale
275
+ run test_when_audit_runs_against_changed_repo_then_exit_0
276
+ run test_when_hook_runs_unchanged_tree_then_header_and_table_byte_equal
277
+ run test_when_old_resume_snapshot_reinjected_then_hook_does_not_crash
278
+
279
+ echo "----"
280
+ echo "Passed: $PASS Failed: $FAIL"
281
+ if [ "$FAIL" -gt 0 ]; then
282
+ echo "Failed tests:"
283
+ for t in "${FAILED[@]}"; do echo " - $t"; done
284
+ fi
285
+ exit $((FAIL > 0))