@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,172 @@
1
+ ---
2
+ name: memory-flush
3
+ owner: baseline
4
+ description: Review the auto-extracted candidates in `.claude/memory/_pending.md` and commit keepers to the canonical memory files (`landmarks.md`, `libraries.md`, `decisions.md`, `landmines.md`, `conventions.md`, `pending-questions.md`). Invoke at session start when the SessionStart hook reports pending candidates, or any time `_pending.md` has accumulated entries you want to curate. Reset the pending body after flushing.
5
+ ---
6
+
7
+ # memory-flush — curate auto-extracted memory candidates
8
+
9
+ The `memory_stop.sh` hook appends candidates to `.claude/memory/_pending.md` after every turn. This skill reviews them in main context (where conversation richness is preserved), commits the keepers to the right canonical file with proper metadata, and resets the pending body.
10
+
11
+ The hook is a passive collector. **You are the curator.** Discard noise, promote signal, deduplicate against existing canonical entries.
12
+
13
+ # Inputs
14
+
15
+ - `.claude/memory/_pending.md` — the pending body. Each block looks like:
16
+ ```
17
+ ## CANDIDATE: <key> → <target-file>.md
18
+ - field: value
19
+ - field: value
20
+ ```
21
+ - The six canonical files at `.claude/memory/<name>.md`. Read each before deciding where a candidate lands and whether it duplicates existing content.
22
+
23
+ # Method
24
+
25
+ ## Step 0 — Canonical sweep (closure semantics)
26
+
27
+ Before reviewing `_pending.md`, sweep the six canonical files for closed entries and stale entries. The `sweep.py` helper at `.claude/skills/memory-flush/sweep.py` is the deterministic actuator; this SOP composes the three modes.
28
+
29
+ ### Step 0a — Auto-close structured closure fields
30
+
31
+ Invoke:
32
+
33
+ ```
34
+ python3 .claude/skills/memory-flush/sweep.py --mode auto-close --memory-dir .claude/memory
35
+ ```
36
+
37
+ Behavior:
38
+
39
+ - For each entry on `pending-questions.md`: if `- resolved-at: <ISO>` is present and valid, delete the entry block.
40
+ - For each entry on the other five canonical files: if `- superseded-at: <ISO>` is present and valid, delete the entry block.
41
+ - Per-file invariant violations (`resolved-at:` on a non-pending file, `superseded-at:` on `pending-questions.md`) are flagged in the report and the block is **kept**.
42
+ - Malformed ISO dates are flagged and the block is **kept**.
43
+
44
+ Report shape: `{"closed": N, "malformed": [...], "invariant_violation": [...]}`. Surface counts in the Step 6 report.
45
+
46
+ ### Step 0b — Surface prose closure phrases
47
+
48
+ Invoke (one reply per surfaced entry, piped from stdin):
49
+
50
+ ```
51
+ python3 .claude/skills/memory-flush/sweep.py --mode prose-scan --memory-dir .claude/memory
52
+ ```
53
+
54
+ For each entry without a structured closure field, the helper scans the body against three anchored, case-insensitive regexes (R1 `Resolution path taken|by|date`, R2 `Superseded by|at|on`, R3 `Resolved by|on|at`). On a match the helper reads one line from stdin and applies the reply: `y` deletes the block, `n` keeps and does-not-resurface-this-run, `skip` keeps and defers for next-run reconsideration.
55
+
56
+ You drive this step interactively: ask the user `Close <key> from <file>? (y / n / skip)` for each entry the helper surfaces, then feed the answers to the helper one per line.
57
+
58
+ ### Step 0c — Stale sweep
59
+
60
+ Only run when `memory_session_start.sh` reported stale > 0 this session, or the user asks. Invoke:
61
+
62
+ ```
63
+ python3 .claude/skills/memory-flush/sweep.py --mode stale-sweep --memory-dir .claude/memory
64
+ ```
65
+
66
+ The helper re-derives the stale set using the same predicate as the hook (verified-at ≥ 30 commits behind HEAD in git, or last-touched ≥ 30 days in non-git). For each stale entry, prompt the user `Stale: <key> in <file>. re-verify / delete / mark-closed / skip?` and feed the reply. `re-verify` restamps `verified-at:` + `last-touched:` to today; `delete` removes the block; `mark-closed` inserts the register-correct closure field (`resolved-at:` on pending-questions, `superseded-at:` elsewhere) and leaves the block in place so Step 0a auto-closes it next run; `skip` keeps it and resurfaces next session.
67
+
68
+ After Step 0 completes, proceed to Step 1.
69
+
70
+ ## Step 1 — Read everything
71
+
72
+ Read `_pending.md` in full. Then read the canonical file each candidate targets (don't read all six; read only what's referenced in pending). For each canonical file you'll write to, check the existing entries' stable keys.
73
+
74
+ ## Step 2 — Decide per candidate
75
+
76
+ For each `## CANDIDATE:` block, decide one of:
77
+
78
+ - **Promote.** The candidate is signal. Build the canonical entry shape (see `.claude/memory/README.md`) and append to the right file. If the candidate's stable key already exists in the canonical file → **replace** that entry; do not duplicate.
79
+ - **Discard.** The candidate is noise (touched-once file with no clear role; a context7 query that resolved nothing useful; a path under generated/vendored code). No canonical write.
80
+ - **Defer.** Useful but you don't have enough context to write a clean entry. Move the candidate verbatim to `pending-questions.md` as a `Q-NNN` entry phrased as "Should X be a landmark?" so the next session can decide. The pending body still gets reset at the end.
81
+
82
+ ## Step 3 — Verify before promoting
83
+
84
+ Per the project memory contract: every entry on the canonical files must have a `verified-at:` field. Verify the candidate's claim before writing:
85
+
86
+ - **Landmark candidate** → confirm the file exists at the named path (Read tool). If the candidate has no line number, find the relevant symbol and add `:line` to the key. If the file is missing, **discard**.
87
+ - **Library candidate** → confirm the version against the project's lockfile (or its stack equivalent). If lockfile absent or version mismatched, mark `verified-at: unverified` and add a caveat instead of a SHA.
88
+ - **Decision / landmine / convention candidate** → confirm the cited file/line still exists; if not, surface and discard.
89
+
90
+ Stamp `verified-at: <short HEAD SHA>` on every promoted entry. If the project isn't a git repo or HEAD isn't reachable, use `verified-at: HEAD` (the SessionStart hook treats `HEAD` as fresh).
91
+
92
+ ## Step 4 — Write canonical entries
93
+
94
+ For each promoted candidate:
95
+
96
+ 1. Read the target canonical file's body.
97
+ 2. If a stable-key match exists → use Edit to replace that block in place.
98
+ 3. If no match → use Edit to insert the new entry at the bottom of the body (above any trailing blank lines).
99
+ 4. After each write, re-read the file and confirm the new/updated entry is present and well-formed.
100
+
101
+ Apply the canonical entry shape (from `.claude/memory/README.md`):
102
+
103
+ ```markdown
104
+ ## <stable key>
105
+
106
+ - <field>: <value>
107
+ - ...
108
+ - verified-at: <short SHA>
109
+ - last-touched: <ISO date YYYY-MM-DD>
110
+ - caveat: <optional>
111
+ ```
112
+
113
+ ## Step 5 — Reset the pending body
114
+
115
+ After all promotion/discard/defer decisions are written, **rewrite `_pending.md`** to the empty skeleton:
116
+
117
+ ```markdown
118
+ ---
119
+ owners: [memory_stop.sh writes; /memory-flush clears]
120
+ category: auto-extracted candidates awaiting curation
121
+ verifies-against: none
122
+ ---
123
+
124
+ # Pending memory candidates
125
+
126
+ Auto-extracted by `memory_stop.sh` at end of each turn. Run `/memory-flush` to review and commit keepers to the canonical files.
127
+
128
+ **Content of this file is gitignored.** The file itself (with this header) is committed; everything below the `---` separator below is per-session and not staged.
129
+
130
+ ---
131
+ ```
132
+
133
+ Use `Write` to overwrite the file (not `Edit`, since you're truncating).
134
+
135
+ ## Step 6 — Report
136
+
137
+ Tell the user what happened:
138
+
139
+ ```
140
+ memory-flush — <date>
141
+
142
+ Closed (P):
143
+ - <key> → <file> (auto-close | confirmed)
144
+ - ...
145
+
146
+ Stale handled (Q):
147
+ - <key> → <file> (re-verified | deleted | marked-closed | skipped)
148
+ - ...
149
+
150
+ Promoted (N):
151
+ - <key> → <file> (new | replaced)
152
+ - ...
153
+
154
+ Discarded (M):
155
+ - <key> — <reason>
156
+ - ...
157
+
158
+ Deferred (K) → pending-questions.md:
159
+ - Q-NNN: <question>
160
+ - ...
161
+
162
+ Pending body reset.
163
+ ```
164
+
165
+ # Constraints
166
+
167
+ - **Never write directly from `_pending.md` to canonical without verification.** The whole point of the curation step is to add the verified-at stamp. Auto-promotion would defeat self-healing.
168
+ - **Never duplicate entries.** Stable-key match → replace, don't append a second copy.
169
+ - **Never grow a canonical file past its `size-cap`** (default 500 lines). If a write would exceed, prune the oldest unverified entries in the same write — and surface what you pruned in the report.
170
+ - **The pending body is gitignored content, but the file itself is committed.** Always reset to the skeleton (don't delete the file).
171
+ - **Do not write to `_pending.md` outside this skill.** The hook owns appends; this skill owns clears.
172
+ - **Do not write to `.claude/memory/README.md`.** It's documentation, not a memory file.
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministic Step 0 helper for /memory-flush.
3
+
4
+ Scans canonical memory files for closure fields and prose closure signals,
5
+ applies the matching action (auto-close / surface-and-confirm / stale-sweep),
6
+ and emits a JSON action report. Invoked by SKILL.md Step 0 and exercised by
7
+ the fixture tests at .claude/skills/memory-flush/tests/run.sh.
8
+
9
+ CLI:
10
+ --mode {auto-close, prose-scan, stale-sweep}
11
+ --memory-dir <path>
12
+
13
+ For interactive modes (prose-scan, stale-sweep), one reply per surfaced entry
14
+ is read from stdin. Empty stdin / EOF defaults to "keep".
15
+ """
16
+ from __future__ import annotations
17
+ import argparse
18
+ import json
19
+ import re
20
+ import subprocess
21
+ import sys
22
+ from datetime import date, datetime
23
+ from pathlib import Path
24
+
25
+ # --- constants (Foundation) ---------------------------------------------------
26
+
27
+ CANONICAL_FILES = [
28
+ 'landmarks', 'libraries', 'decisions',
29
+ 'landmines', 'conventions', 'pending-questions',
30
+ ]
31
+ PENDING_FILE = 'pending-questions'
32
+
33
+ STALE_COMMITS = 30
34
+ STALE_DAYS = 30
35
+
36
+ ISO_DATE_RE = re.compile(r'^\d{4}-\d{2}-\d{2}$')
37
+
38
+ PROSE_PATTERNS = [
39
+ re.compile(r'^(\s*-\s*)?\*\*?Resolution\s+(path\s+taken|by|date)\b', re.I | re.M),
40
+ re.compile(r'^Superseded\s+(by|at|on)\b', re.I | re.M),
41
+ re.compile(r'^Resolved\s+(by|on|at)\b', re.I | re.M),
42
+ ]
43
+
44
+ # --- Foundation: filesystem + entry parsing -----------------------------------
45
+
46
+ def file_path(memdir: Path, name: str) -> Path:
47
+ return memdir / f'{name}.md'
48
+
49
+ def read_file(memdir: Path, name: str) -> str:
50
+ p = file_path(memdir, name)
51
+ return p.read_text(encoding='utf-8', errors='replace') if p.is_file() else ''
52
+
53
+ def write_file(memdir: Path, name: str, text: str) -> None:
54
+ file_path(memdir, name).write_text(text, encoding='utf-8')
55
+
56
+ def split_entries(text: str):
57
+ """Return [(key, block_text)] for each `## ` heading + body in the file."""
58
+ body = text.split('---', 2)[-1] if text.startswith('---') else text
59
+ parts = re.split(r'(?m)^(##\s+\S.*)$', body)
60
+ entries = []
61
+ for i in range(1, len(parts), 2):
62
+ heading = parts[i]
63
+ tail = parts[i + 1] if i + 1 < len(parts) else ''
64
+ key = heading[2:].strip().split()[0] if heading[2:].strip() else ''
65
+ entries.append((key, heading + tail))
66
+ return entries
67
+
68
+ def read_field(block: str, name: str):
69
+ pat = re.compile(rf'^\s*-\s*{re.escape(name)}\s*:\s*(.+?)\s*$', re.M | re.I)
70
+ m = pat.search(block)
71
+ return m.group(1) if m else None
72
+
73
+ def has_field(block: str, name: str) -> bool:
74
+ return read_field(block, name) is not None
75
+
76
+ def valid_iso(s) -> bool:
77
+ if not s or not ISO_DATE_RE.match(s):
78
+ return False
79
+ try:
80
+ datetime.strptime(s, '%Y-%m-%d')
81
+ return True
82
+ except ValueError:
83
+ return False
84
+
85
+ def delete_block(text: str, block: str) -> str:
86
+ idx = text.find(block)
87
+ if idx < 0:
88
+ return text
89
+ before = text[:idx].rstrip('\n')
90
+ after = text[idx + len(block):].lstrip('\n')
91
+ if before and after:
92
+ return before + '\n\n' + after
93
+ if before:
94
+ return before + '\n'
95
+ return after
96
+
97
+ def update_field(block: str, name: str, value: str) -> str:
98
+ pat = re.compile(rf'(^\s*-\s*{re.escape(name)}\s*:\s*).+$', re.M | re.I)
99
+ if pat.search(block):
100
+ return pat.sub(lambda m: f'{m.group(1)}{value}', block, count=1)
101
+ return _append_field(block, name, value)
102
+
103
+ def _append_field(block: str, name: str, value: str) -> str:
104
+ lines = block.rstrip('\n').split('\n')
105
+ insert_at = len(lines)
106
+ for i in range(len(lines) - 1, -1, -1):
107
+ if lines[i].strip().startswith('-'):
108
+ insert_at = i + 1
109
+ break
110
+ lines.insert(insert_at, f'- {name}: {value}')
111
+ return '\n'.join(lines) + '\n'
112
+
113
+ # --- Foundation: git + dates --------------------------------------------------
114
+
115
+ def head_sha(root: Path) -> str:
116
+ try:
117
+ return subprocess.check_output(
118
+ ['git', '-C', str(root), 'rev-parse', '--short', 'HEAD'],
119
+ stderr=subprocess.DEVNULL, text=True,
120
+ ).strip()
121
+ except Exception:
122
+ return ''
123
+
124
+ def commit_distance(root: Path, stamp: str):
125
+ try:
126
+ d = subprocess.check_output(
127
+ ['git', '-C', str(root), 'rev-list', '--count', f'{stamp}..HEAD'],
128
+ stderr=subprocess.DEVNULL, text=True,
129
+ ).strip()
130
+ return int(d) if d.isdigit() else None
131
+ except Exception:
132
+ return None
133
+
134
+ def days_since(iso: str):
135
+ try:
136
+ d = datetime.strptime(iso, '%Y-%m-%d').date()
137
+ return (date.today() - d).days
138
+ except Exception:
139
+ return None
140
+
141
+ # --- Domain: closure semantics ------------------------------------------------
142
+
143
+ def closure_field_for(name: str) -> str:
144
+ return 'resolved-at' if name == PENDING_FILE else 'superseded-at'
145
+
146
+ def invariant_field_for(name: str) -> str:
147
+ return 'superseded-at' if name == PENDING_FILE else 'resolved-at'
148
+
149
+ def is_closed(block: str, name: str) -> bool:
150
+ return has_field(block, closure_field_for(name))
151
+
152
+ def prose_matches(block: str) -> bool:
153
+ return any(p.search(block) for p in PROSE_PATTERNS)
154
+
155
+ def is_stale(block: str, name: str, head: str, root: Path) -> bool:
156
+ if is_closed(block, name):
157
+ return False
158
+ stamp = read_field(block, 'verified-at')
159
+ if head and stamp and stamp != 'HEAD':
160
+ dist = commit_distance(root, stamp)
161
+ return dist is None or dist >= STALE_COMMITS
162
+ if not head:
163
+ touched = read_field(block, 'last-touched')
164
+ days = days_since(touched) if touched else None
165
+ return days is not None and days >= STALE_DAYS
166
+ return False
167
+
168
+ # --- Domain: per-mode sweepers ------------------------------------------------
169
+
170
+ def mode_auto_close(memdir: Path) -> dict:
171
+ report = {'closed': 0, 'malformed': [], 'invariant_violation': []}
172
+ for name in CANONICAL_FILES:
173
+ text = read_file(memdir, name)
174
+ if not text:
175
+ continue
176
+ valid = closure_field_for(name)
177
+ wrong = invariant_field_for(name)
178
+ new_text = text
179
+ for key, block in split_entries(text):
180
+ if has_field(block, wrong):
181
+ report['invariant_violation'].append(
182
+ {'file': f'{name}.md', 'key': key, 'field': wrong}
183
+ )
184
+ continue
185
+ value = read_field(block, valid)
186
+ if value is None:
187
+ continue
188
+ if valid_iso(value):
189
+ new_text = delete_block(new_text, block)
190
+ report['closed'] += 1
191
+ else:
192
+ report['malformed'].append(
193
+ {'file': f'{name}.md', 'key': key, 'value': value}
194
+ )
195
+ if new_text != text:
196
+ write_file(memdir, name, new_text)
197
+ return report
198
+
199
+ def _next_reply() -> str:
200
+ line = sys.stdin.readline()
201
+ return line.strip().lower() if line else ''
202
+
203
+ def mode_prose_scan(memdir: Path) -> dict:
204
+ report = {'surfaced': 0, 'closed_by_confirm': 0, 'kept': 0, 'deferred': 0}
205
+ for name in CANONICAL_FILES:
206
+ text = read_file(memdir, name)
207
+ if not text:
208
+ continue
209
+ new_text = text
210
+ for key, block in split_entries(text):
211
+ if is_closed(block, name):
212
+ continue
213
+ if not prose_matches(block):
214
+ continue
215
+ report['surfaced'] += 1
216
+ reply = _next_reply()
217
+ if reply == 'y':
218
+ new_text = delete_block(new_text, block)
219
+ report['closed_by_confirm'] += 1
220
+ elif reply == 'skip':
221
+ report['deferred'] += 1
222
+ else:
223
+ report['kept'] += 1
224
+ if new_text != text:
225
+ write_file(memdir, name, new_text)
226
+ return report
227
+
228
+ def mode_stale_sweep(memdir: Path) -> dict:
229
+ report = {'reverified': 0, 'deleted': 0, 'mark_closed': 0, 'kept': 0}
230
+ root = memdir.parent.parent
231
+ head = head_sha(root)
232
+ today = date.today().isoformat()
233
+ for name in CANONICAL_FILES:
234
+ text = read_file(memdir, name)
235
+ if not text:
236
+ continue
237
+ new_text = text
238
+ for key, block in split_entries(text):
239
+ if not is_stale(block, name, head, root):
240
+ continue
241
+ reply = _next_reply()
242
+ new_text = _apply_stale_action(new_text, block, name, reply, head, today, report)
243
+ if new_text != text:
244
+ write_file(memdir, name, new_text)
245
+ return report
246
+
247
+ def _apply_stale_action(text, block, name, reply, head, today, report):
248
+ if reply == 're-verify':
249
+ updated = update_field(block, 'verified-at', head or 'HEAD')
250
+ updated = update_field(updated, 'last-touched', today)
251
+ report['reverified'] += 1
252
+ return text.replace(block, updated)
253
+ if reply == 'delete':
254
+ report['deleted'] += 1
255
+ return delete_block(text, block)
256
+ if reply == 'mark-closed':
257
+ field = closure_field_for(name)
258
+ updated = update_field(block, field, today)
259
+ report['mark_closed'] += 1
260
+ return text.replace(block, updated)
261
+ report['kept'] += 1
262
+ return text
263
+
264
+ # --- Orchestration ------------------------------------------------------------
265
+
266
+ MODE_DISPATCH = {
267
+ 'auto-close': mode_auto_close,
268
+ 'prose-scan': mode_prose_scan,
269
+ 'stale-sweep': mode_stale_sweep,
270
+ }
271
+
272
+ def parse_args(argv):
273
+ p = argparse.ArgumentParser(description='Memory Step 0 sweep helper')
274
+ p.add_argument('--mode', required=True, choices=list(MODE_DISPATCH))
275
+ p.add_argument('--memory-dir', required=True)
276
+ return p.parse_args(argv)
277
+
278
+ def main(argv) -> int:
279
+ args = parse_args(argv)
280
+ memdir = Path(args.memory_dir).resolve()
281
+ report = MODE_DISPATCH[args.mode](memdir)
282
+ print(json.dumps(report))
283
+ return 0
284
+
285
+ if __name__ == '__main__':
286
+ sys.exit(main(sys.argv[1:]))