@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,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:]))
|