@friedbotstudio/create-baseline 0.2.0 → 0.3.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/README.md +7 -3
- package/obj/template/.claude/commands/grant-push.md +19 -0
- package/obj/template/.claude/commands/init-project.md +26 -4
- package/obj/template/.claude/hooks/consent_gate_grant.mjs +107 -0
- package/obj/template/.claude/hooks/git_commit_guard.mjs +224 -0
- package/obj/template/.claude/hooks/harness_continuation.sh +101 -34
- package/obj/template/.claude/hooks/lib/common.mjs +283 -0
- package/obj/template/.claude/hooks/lib/common.sh +1 -1
- package/obj/template/.claude/hooks/memory_session_start.sh +20 -6
- package/obj/template/.claude/hooks/memory_stop.sh +161 -2
- package/obj/template/.claude/hooks/spec_approval_guard.sh +1 -1
- package/obj/template/.claude/hooks/swarm_approval_guard.sh +1 -1
- package/obj/template/.claude/hooks/tests/fixtures/ac008_byte_equal_reference.txt +7 -7
- package/obj/template/.claude/hooks/tests/fixtures/memory_stop_landmark_baseline.txt +21 -0
- package/obj/template/.claude/hooks/tests/fixtures/regenerate-ac008.sh +47 -0
- package/obj/template/.claude/hooks/tests/memory_session_start_test.sh +7 -3
- package/obj/template/.claude/hooks/tests/memory_stop_intent_test.sh +329 -0
- package/obj/template/.claude/hooks/tests/regenerate_ac008_test.sh +99 -0
- package/obj/template/.claude/memory/README.md +8 -3
- package/obj/template/.claude/memory/backlog.md +12 -0
- package/obj/template/.claude/project.json +6 -1
- package/obj/template/.claude/settings.json +3 -4
- package/obj/template/.claude/skills/audit-baseline/audit.sh +39 -21
- package/obj/template/.claude/skills/audit-baseline/tests/fixtures/_pending_opener_only.md +3 -0
- package/obj/template/.claude/skills/audit-baseline/tests/fixtures/preamble_full_empty_body.md +4 -0
- package/obj/template/.claude/skills/audit-baseline/tests/fixtures/preamble_full_with_entries.md +9 -0
- package/obj/template/.claude/skills/audit-baseline/tests/fixtures/preamble_no_opener.md +3 -0
- package/obj/template/.claude/skills/audit-baseline/tests/fixtures/preamble_opener_only.md +3 -0
- package/obj/template/.claude/skills/audit-baseline/tests/preamble_check_test.sh +147 -0
- package/obj/template/.claude/skills/chore/SKILL.md +5 -3
- package/obj/template/.claude/skills/commit/SKILL.md +5 -4
- package/obj/template/.claude/skills/copywriting/LICENSE +21 -0
- package/obj/template/.claude/skills/copywriting/NOTICE +23 -0
- package/obj/template/.claude/skills/copywriting/SKILL.md +1 -1
- package/obj/template/.claude/skills/design-ui/SKILL.md +23 -5
- package/obj/template/.claude/skills/design-ui/references/design-vs-development.md +26 -5
- package/obj/template/.claude/skills/design-ui/references/orchestration.md +1 -0
- package/obj/template/.claude/skills/design-ui/references/state-machine.md +5 -3
- package/obj/template/.claude/skills/documentation/LICENSE +202 -0
- package/obj/template/.claude/skills/documentation/NOTICE +22 -0
- package/obj/template/.claude/skills/google-analytics/SKILL.md +129 -0
- package/obj/template/.claude/skills/google-analytics/references/audiences.md +389 -0
- package/obj/template/.claude/skills/google-analytics/references/bigquery.md +470 -0
- package/obj/template/.claude/skills/google-analytics/references/custom-dimensions.md +355 -0
- package/obj/template/.claude/skills/google-analytics/references/custom-events.md +383 -0
- package/obj/template/.claude/skills/google-analytics/references/data-management.md +416 -0
- package/obj/template/.claude/skills/google-analytics/references/debugview.md +364 -0
- package/obj/template/.claude/skills/google-analytics/references/events-fundamentals.md +398 -0
- package/obj/template/.claude/skills/google-analytics/references/gtag.md +502 -0
- package/obj/template/.claude/skills/google-analytics/references/gtm-integration.md +483 -0
- package/obj/template/.claude/skills/google-analytics/references/measurement-protocol.md +519 -0
- package/obj/template/.claude/skills/google-analytics/references/privacy.md +441 -0
- package/obj/template/.claude/skills/google-analytics/references/recommended-events.md +464 -0
- package/obj/template/.claude/skills/google-analytics/references/reporting.md +397 -0
- package/obj/template/.claude/skills/google-analytics/references/setup.md +344 -0
- package/obj/template/.claude/skills/google-analytics/references/user-tracking.md +417 -0
- package/obj/template/.claude/skills/harness/SKILL.md +3 -1
- package/obj/template/.claude/skills/humanizer/LICENSE +21 -0
- package/obj/template/.claude/skills/humanizer/NOTICE +21 -0
- package/obj/template/.claude/skills/impeccable/LICENSE +202 -0
- package/obj/template/.claude/skills/impeccable/NOTICE +24 -0
- package/obj/template/.claude/skills/memory-flush/SKILL.md +20 -4
- package/obj/template/.claude/skills/memory-flush/sweep.py +74 -6
- package/obj/template/.claude/skills/memory-flush/tests/run.sh +300 -1
- package/obj/template/.claude/skills/optimize-seo/SKILL.md +313 -0
- package/obj/template/.claude/skills/optimize-seo/scripts/pagespeed.mjs +197 -0
- package/obj/template/.claude/skills/pagespeed-insights/LICENSE.md +37 -0
- package/obj/template/.claude/skills/pagespeed-insights/SKILL.md +446 -0
- package/obj/template/.claude/skills/pagespeed-insights/reference.md +50 -0
- package/obj/template/.claude/skills/tdd/SKILL.md +2 -1
- package/obj/template/.claude/skills/tdd/drift_check.py +180 -0
- package/obj/template/.claude/skills/tdd/tests/drift_check_test.sh +190 -0
- package/obj/template/.claude/skills/tdd/tests/run.sh +21 -0
- package/obj/template/.claude/skills/technical-tutorials/LICENSE +21 -0
- package/obj/template/.claude/skills/technical-tutorials/NOTICE +23 -0
- package/obj/template/.claude/skills/technical-tutorials/SKILL.md +1 -1
- package/obj/template/.claude/skills/triage/SKILL.md +8 -3
- package/obj/template/CLAUDE.md +37 -26
- package/obj/template/docs/init/seed.md +38 -23
- package/obj/template/manifest.json +80 -33
- package/package.json +1 -1
- package/src/CLAUDE.template.md +37 -26
- package/src/memory/backlog.template.md +12 -0
- package/src/project.template.json +6 -1
- package/src/seed.template.md +38 -23
- package/src/settings.template.json +3 -4
- package/obj/template/.claude/hooks/consent_gate_grant.sh +0 -89
- package/obj/template/.claude/hooks/git_commit_guard.sh +0 -93
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
impeccable
|
|
2
|
+
==========
|
|
3
|
+
|
|
4
|
+
This skill is vendored from the `impeccable` repository published by Phil
|
|
5
|
+
Bakaus (pbakaus) and is redistributed here under the terms of the Apache
|
|
6
|
+
License, Version 2.0 (see LICENSE in this directory).
|
|
7
|
+
|
|
8
|
+
Upstream:
|
|
9
|
+
Repository: https://github.com/pbakaus/impeccable
|
|
10
|
+
Author: Phil Bakaus (pbakaus)
|
|
11
|
+
|
|
12
|
+
Vendored into: .claude/skills/impeccable/
|
|
13
|
+
Vendored on: 2026-05-15 (this commit) — prior install date unknown; LICENSE
|
|
14
|
+
+ NOTICE added retroactively as part of the licensing-attribution
|
|
15
|
+
drift fix in the branch-aware-git-policy workflow.
|
|
16
|
+
|
|
17
|
+
Local changes:
|
|
18
|
+
- Frontmatter `owner: baseline` added so the audit-baseline drift check
|
|
19
|
+
tracks this file as a shipped baseline artifact. The Apache 2.0 license
|
|
20
|
+
permits redistribution under attribution; this NOTICE file fulfills the
|
|
21
|
+
notice-preservation requirement.
|
|
22
|
+
- PROJECT_NOTES.md captures local annotations made in the course of using
|
|
23
|
+
impeccable within the baseline. The vendored SKILL.md and agents/* files
|
|
24
|
+
are otherwise unmodified from upstream.
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: memory-flush
|
|
3
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.
|
|
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`, `backlog.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
5
|
---
|
|
6
6
|
|
|
7
|
+
# When invoked as Phase 10.6
|
|
8
|
+
|
|
9
|
+
This skill runs as **Phase 10.6** of every workflow track (intake / spec / tdd / chore), between `/archive` (Phase 10.5) and `/grant-commit` (Phase 11). The harness loop reads `.claude/memory/_pending.md` body, runs Step 0 canonical sweeps unconditionally, and on **empty pending** (zero `## CANDIDATE:` blocks) short-circuits the **fast-path**: Steps 1–5 are skipped, Step 6 emits a one-line "no pending candidates" report, and the skill returns success. This keeps the no-op cost bounded at ≤ 3 sweep.py invocations per Phase 10.6 invocation.
|
|
10
|
+
|
|
11
|
+
The skill is also user-invokable outside the workflow (ad-hoc curation). When invoked ad-hoc and `_pending.md` is non-empty, the full Steps 1–5 flow runs identically. The fast-path activates per-invocation based on `_pending.md` body state, not on workflow context.
|
|
12
|
+
|
|
13
|
+
`/commit` (Phase 11) refuses to proceed unless `memory-flush` is in `workflow.json → completed` (or in `exceptions`). Empty-pending fast-path still appends `"memory-flush"` to `completed` — the prereq is satisfied either way.
|
|
14
|
+
|
|
15
|
+
(See "Method" below for the full Step 0 / Steps 1–5 / Step 6 flow.)
|
|
16
|
+
|
|
7
17
|
# memory-flush — curate auto-extracted memory candidates
|
|
8
18
|
|
|
9
19
|
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.
|
|
@@ -18,13 +28,13 @@ The hook is a passive collector. **You are the curator.** Discard noise, promote
|
|
|
18
28
|
- field: value
|
|
19
29
|
- field: value
|
|
20
30
|
```
|
|
21
|
-
- The
|
|
31
|
+
- The seven canonical files at `.claude/memory/<name>.md`. Read each before deciding where a candidate lands and whether it duplicates existing content.
|
|
22
32
|
|
|
23
33
|
# Method
|
|
24
34
|
|
|
25
35
|
## Step 0 — Canonical sweep (closure semantics)
|
|
26
36
|
|
|
27
|
-
Before reviewing `_pending.md`, sweep the
|
|
37
|
+
Before reviewing `_pending.md`, sweep the seven 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
38
|
|
|
29
39
|
### Step 0a — Auto-close structured closure fields
|
|
30
40
|
|
|
@@ -55,6 +65,10 @@ For each entry without a structured closure field, the helper scans the body aga
|
|
|
55
65
|
|
|
56
66
|
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
67
|
|
|
68
|
+
### Step 0a-bis — Stamp-closure mode (invoked from /commit, not from /memory-flush)
|
|
69
|
+
|
|
70
|
+
`sweep.py` also exposes a `--mode stamp-closure --backlog-keys <csv>` mode that writes `status: picked-up` + `superseded-at: <today>` to each named `backlog.md` entry. This mode is NOT invoked by `/memory-flush` Step 0; it is invoked by `/commit` Step 6 when `workflow.json → source_backlog_keys` is non-empty. The mode is idempotent (re-running on stamped entries rewrites `superseded-at:` to today; reports them under `already_closed`). The next `/memory-flush` Step 0a auto-close sweep then deletes the stamped entries per the existing `superseded-at:` closure trigger — so `/memory-flush`'s contract is unchanged; it just sees more closures in its `auto-close` step. Report shape: `{"stamped": N, "missing": [keys], "already_closed": [keys]}`.
|
|
71
|
+
|
|
58
72
|
### Step 0c — Stale sweep
|
|
59
73
|
|
|
60
74
|
Only run when `memory_session_start.sh` reported stale > 0 this session, or the user asks. Invoke:
|
|
@@ -76,9 +90,11 @@ Read `_pending.md` in full. Then read the canonical file each candidate targets
|
|
|
76
90
|
For each `## CANDIDATE:` block, decide one of:
|
|
77
91
|
|
|
78
92
|
- **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.
|
|
93
|
+
- **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; an intent line that was a passing chat phrase rather than real future work). No canonical write.
|
|
80
94
|
- **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
95
|
|
|
96
|
+
**Backlog candidates** (`## CANDIDATE: backlog → <slug>-<4hash>`) route to `backlog.md` with the canonical entry shape plus these fields: `status: open` (the initial state; transitions to `picked-up` or `dropped` are later edits), `raised-on: <ISO>`, `raised-in-context: <slug-or-(no active workflow)>`, the verbatim blockquote of the user/assistant intent line. Provenance is `source: user-instruction` for `role: user` candidates or `source: assistant-deferral` for `role: assistant` candidates. The verbatim is REQUIRED for both — `/memory-flush` SHALL reject promotion without it (per `.claude/memory/README.md → Source provenance`).
|
|
97
|
+
|
|
82
98
|
## Step 3 — Verify before promoting
|
|
83
99
|
|
|
84
100
|
Per the project memory contract: every entry on the canonical files must have a `verified-at:` field. Verify the candidate's claim before writing:
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Deterministic Step 0
|
|
2
|
+
"""Deterministic actuator for /memory-flush Step 0 and for /commit Step 6.
|
|
3
3
|
|
|
4
4
|
Scans canonical memory files for closure fields and prose closure signals,
|
|
5
5
|
applies the matching action (auto-close / surface-and-confirm / stale-sweep),
|
|
6
|
-
and emits a JSON action report.
|
|
6
|
+
and emits a JSON action report. Also exposes a non-interactive stamp-closure
|
|
7
|
+
mode invoked by /commit (Phase 11, Step 6) to write status: picked-up +
|
|
8
|
+
superseded-at: today on backlog entries named in workflow.json →
|
|
9
|
+
source_backlog_keys. Invoked by SKILL.md Step 0 (auto-close / prose-scan /
|
|
10
|
+
stale-sweep) and by commit/SKILL.md Step 6 (stamp-closure). Exercised by
|
|
7
11
|
the fixture tests at .claude/skills/memory-flush/tests/run.sh.
|
|
8
12
|
|
|
9
13
|
CLI:
|
|
10
|
-
--mode {auto-close, prose-scan, stale-sweep}
|
|
14
|
+
--mode {auto-close, prose-scan, stale-sweep, stamp-closure}
|
|
11
15
|
--memory-dir <path>
|
|
16
|
+
--backlog-keys <csv> (required iff --mode stamp-closure)
|
|
12
17
|
|
|
13
18
|
For interactive modes (prose-scan, stale-sweep), one reply per surfaced entry
|
|
14
|
-
is read from stdin. Empty stdin / EOF defaults to "keep".
|
|
19
|
+
is read from stdin. Empty stdin / EOF defaults to "keep". stamp-closure is
|
|
20
|
+
non-interactive; --backlog-keys is the input channel.
|
|
15
21
|
"""
|
|
16
22
|
from __future__ import annotations
|
|
17
23
|
import argparse
|
|
@@ -27,9 +33,16 @@ from pathlib import Path
|
|
|
27
33
|
CANONICAL_FILES = [
|
|
28
34
|
'landmarks', 'libraries', 'decisions',
|
|
29
35
|
'landmines', 'conventions', 'pending-questions',
|
|
36
|
+
'backlog',
|
|
30
37
|
]
|
|
31
38
|
PENDING_FILE = 'pending-questions'
|
|
32
39
|
|
|
40
|
+
# Files whose entries do NOT stale-age. Backlog is intent, not a verifiable
|
|
41
|
+
# fact about code state, so commit-distance and day-count are meaningless
|
|
42
|
+
# signals. Closure is still tracked via superseded-at: per the canonical
|
|
43
|
+
# closure-field-per-file rule.
|
|
44
|
+
STALE_EXEMPT_FILES = {'backlog'}
|
|
45
|
+
|
|
33
46
|
STALE_COMMITS = 30
|
|
34
47
|
STALE_DAYS = 30
|
|
35
48
|
|
|
@@ -153,6 +166,8 @@ def prose_matches(block: str) -> bool:
|
|
|
153
166
|
return any(p.search(block) for p in PROSE_PATTERNS)
|
|
154
167
|
|
|
155
168
|
def is_stale(block: str, name: str, head: str, root: Path) -> bool:
|
|
169
|
+
if name in STALE_EXEMPT_FILES:
|
|
170
|
+
return False
|
|
156
171
|
if is_closed(block, name):
|
|
157
172
|
return False
|
|
158
173
|
stamp = read_field(block, 'verified-at')
|
|
@@ -225,6 +240,50 @@ def mode_prose_scan(memdir: Path) -> dict:
|
|
|
225
240
|
write_file(memdir, name, new_text)
|
|
226
241
|
return report
|
|
227
242
|
|
|
243
|
+
def mode_stamp_closure(memdir: Path, keys_csv: str) -> dict:
|
|
244
|
+
"""Stamp the named backlog entries with status: picked-up + superseded-at: today.
|
|
245
|
+
|
|
246
|
+
Idempotent: re-running on already-stamped entries rewrites superseded-at:
|
|
247
|
+
to today and reports them under `already_closed`. Entries the caller named
|
|
248
|
+
that aren't present in backlog.md go into `missing`. The next /memory-flush
|
|
249
|
+
Step 0a auto-close sweep deletes the stamped entries per the existing
|
|
250
|
+
superseded-at: closure-trigger contract.
|
|
251
|
+
"""
|
|
252
|
+
report = {'stamped': 0, 'missing': [], 'already_closed': []}
|
|
253
|
+
keys = [k.strip() for k in keys_csv.split(',') if k.strip()]
|
|
254
|
+
if not keys:
|
|
255
|
+
return report
|
|
256
|
+
text = read_file(memdir, 'backlog')
|
|
257
|
+
if not text:
|
|
258
|
+
report['missing'] = list(keys)
|
|
259
|
+
return report
|
|
260
|
+
new_text = text
|
|
261
|
+
today = date.today().isoformat()
|
|
262
|
+
for key in keys:
|
|
263
|
+
block = _find_entry_block(new_text, key)
|
|
264
|
+
if block is None:
|
|
265
|
+
report['missing'].append(key)
|
|
266
|
+
continue
|
|
267
|
+
was_stamped = (read_field(block, 'status') or '').strip() == 'picked-up'
|
|
268
|
+
updated = update_field(block, 'status', 'picked-up')
|
|
269
|
+
updated = update_field(updated, 'superseded-at', today)
|
|
270
|
+
new_text = new_text.replace(block, updated)
|
|
271
|
+
if was_stamped:
|
|
272
|
+
report['already_closed'].append(key)
|
|
273
|
+
else:
|
|
274
|
+
report['stamped'] += 1
|
|
275
|
+
if new_text != text:
|
|
276
|
+
write_file(memdir, 'backlog', new_text)
|
|
277
|
+
return report
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _find_entry_block(text: str, key: str):
|
|
281
|
+
for entry_key, block in split_entries(text):
|
|
282
|
+
if entry_key == key:
|
|
283
|
+
return block
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
|
|
228
287
|
def mode_stale_sweep(memdir: Path) -> dict:
|
|
229
288
|
report = {'reverified': 0, 'deleted': 0, 'mark_closed': 0, 'kept': 0}
|
|
230
289
|
root = memdir.parent.parent
|
|
@@ -267,18 +326,27 @@ MODE_DISPATCH = {
|
|
|
267
326
|
'auto-close': mode_auto_close,
|
|
268
327
|
'prose-scan': mode_prose_scan,
|
|
269
328
|
'stale-sweep': mode_stale_sweep,
|
|
329
|
+
'stamp-closure': mode_stamp_closure,
|
|
270
330
|
}
|
|
271
331
|
|
|
272
332
|
def parse_args(argv):
|
|
273
333
|
p = argparse.ArgumentParser(description='Memory Step 0 sweep helper')
|
|
274
334
|
p.add_argument('--mode', required=True, choices=list(MODE_DISPATCH))
|
|
275
335
|
p.add_argument('--memory-dir', required=True)
|
|
276
|
-
|
|
336
|
+
p.add_argument('--backlog-keys', default=None,
|
|
337
|
+
help='CSV of backlog stable keys; required when --mode stamp-closure')
|
|
338
|
+
args = p.parse_args(argv)
|
|
339
|
+
if args.mode == 'stamp-closure' and args.backlog_keys is None:
|
|
340
|
+
p.error('--backlog-keys is required when --mode stamp-closure')
|
|
341
|
+
return args
|
|
277
342
|
|
|
278
343
|
def main(argv) -> int:
|
|
279
344
|
args = parse_args(argv)
|
|
280
345
|
memdir = Path(args.memory_dir).resolve()
|
|
281
|
-
|
|
346
|
+
if args.mode == 'stamp-closure':
|
|
347
|
+
report = mode_stamp_closure(memdir, args.backlog_keys or '')
|
|
348
|
+
else:
|
|
349
|
+
report = MODE_DISPATCH[args.mode](memdir)
|
|
282
350
|
print(json.dumps(report))
|
|
283
351
|
return 0
|
|
284
352
|
|
|
@@ -62,7 +62,7 @@ sweep() {
|
|
|
62
62
|
seed_skel() {
|
|
63
63
|
local mem="$1"
|
|
64
64
|
mkdir -p "$mem"
|
|
65
|
-
for f in landmarks libraries decisions landmines conventions pending-questions; do
|
|
65
|
+
for f in landmarks libraries decisions landmines conventions pending-questions backlog; do
|
|
66
66
|
cat > "$mem/$f.md" <<EOF
|
|
67
67
|
---
|
|
68
68
|
owners: [test]
|
|
@@ -304,6 +304,289 @@ test_when_pre_spec_entry_no_source_no_verbatim_then_grandfathered() {
|
|
|
304
304
|
assert_file_contains "$mem/conventions.md" "## legacy-entry" "AC-006 grandfathered legacy entry survives" || return 1
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
# --- Phase 10.6 (memory-flush-phase) regression traps -------------------------
|
|
308
|
+
# Confirms sweep.py stays scoped to canonical files and does not touch _pending.md
|
|
309
|
+
# regardless of whether _pending.md body is empty (the fast-path case).
|
|
310
|
+
|
|
311
|
+
PENDING_SKELETON='---
|
|
312
|
+
owners: [memory_stop.sh writes; /memory-flush clears]
|
|
313
|
+
category: auto-extracted candidates awaiting curation
|
|
314
|
+
verifies-against: none
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
# Pending memory candidates
|
|
318
|
+
|
|
319
|
+
Auto-extracted by `memory_stop.sh`. Run `/memory-flush` to review.
|
|
320
|
+
|
|
321
|
+
**Content of this file is gitignored.**
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
'
|
|
325
|
+
|
|
326
|
+
seed_pending_skeleton() {
|
|
327
|
+
printf '%s' "$PENDING_SKELETON" > "$1/_pending.md"
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
test_when_pending_empty_then_sweep_does_not_touch_pending() {
|
|
331
|
+
local mem; mem="$(mktemp -d)"; trap "rm -rf $mem" RETURN
|
|
332
|
+
seed_skel "$mem"
|
|
333
|
+
seed_pending_skeleton "$mem"
|
|
334
|
+
local before; before="$(cat "$mem/_pending.md")"
|
|
335
|
+
sweep auto-close "$mem" >/dev/null || { fail "sweep crashed on empty pending"; return 1; }
|
|
336
|
+
sweep prose-scan "$mem" "" >/dev/null || { fail "sweep crashed on empty pending"; return 1; }
|
|
337
|
+
sweep stale-sweep "$mem" "" >/dev/null || { fail "sweep crashed on empty pending"; return 1; }
|
|
338
|
+
local after; after="$(cat "$mem/_pending.md")"
|
|
339
|
+
if [ "$before" = "$after" ]; then return 0; fi
|
|
340
|
+
fail "_pending.md mutated by sweep.py — sweep must stay scoped to canonical files"
|
|
341
|
+
diff <(printf '%s' "$before") <(printf '%s' "$after") || true
|
|
342
|
+
return 1
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
test_when_pending_empty_AND_q999_has_resolved_at_then_q999_is_swept() {
|
|
346
|
+
local mem; mem="$(mktemp -d)"; trap "rm -rf $mem" RETURN
|
|
347
|
+
seed_skel "$mem"
|
|
348
|
+
seed_pending_skeleton "$mem"
|
|
349
|
+
add "$mem" "pending-questions" "## Q-999
|
|
350
|
+
|
|
351
|
+
- Question: fast-path canonical-sweep regression trap
|
|
352
|
+
- verified-at: HEAD
|
|
353
|
+
- last-touched: $(today)
|
|
354
|
+
- resolved-at: 2026-05-17"
|
|
355
|
+
local report; report="$(sweep auto-close "$mem")" || { fail "sweep crashed"; return 1; }
|
|
356
|
+
assert_file_not_contains "$mem/pending-questions.md" "## Q-999" "Q-999 must be auto-closed even when _pending.md is empty" || return 1
|
|
357
|
+
assert_contains "$report" '"closed": 1' "expected closed count 1 (got: $report)" || return 1
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
# --- backlog-memory-bucket: routing + bootstrap + stale-exempt + verbatim ----
|
|
361
|
+
# Covers AC-005, AC-006, AC-007, AC-009, AC-011 from
|
|
362
|
+
# docs/specs/backlog-memory-bucket.md. All start RED until sweep.py adds
|
|
363
|
+
# 'backlog' to CANONICAL_FILES + STALE_EXEMPT_FILES, and README.md documents
|
|
364
|
+
# the new register.
|
|
365
|
+
|
|
366
|
+
test_when_promote_user_candidate_writes_canonical_entry_with_status_open_and_verbatim() {
|
|
367
|
+
local mem; mem="$(mktemp -d)"; trap "rm -rf $mem" RETURN
|
|
368
|
+
seed_skel "$mem"
|
|
369
|
+
# Curator-style entry: the shape /memory-flush would write on promotion.
|
|
370
|
+
add "$mem" "backlog" "## add-retry-to-webhook-worker-3f2a
|
|
371
|
+
|
|
372
|
+
> verbatim (user, $(today)):
|
|
373
|
+
> TODO: add retry to webhook worker
|
|
374
|
+
|
|
375
|
+
- source: user-instruction
|
|
376
|
+
- status: open
|
|
377
|
+
- raised-on: $(today)
|
|
378
|
+
- raised-in-context: backlog-memory-bucket
|
|
379
|
+
- verified-at: HEAD
|
|
380
|
+
- last-touched: $(today)"
|
|
381
|
+
# Auto-close should NOT touch this entry (no closure field present).
|
|
382
|
+
local report; report="$(sweep auto-close "$mem")" || { fail "AC-005 sweep crashed"; return 1; }
|
|
383
|
+
assert_file_contains "$mem/backlog.md" "## add-retry-to-webhook-worker-3f2a" "AC-005 open entry must survive auto-close" || return 1
|
|
384
|
+
assert_file_contains "$mem/backlog.md" "status: open" "AC-005 status:open missing" || return 1
|
|
385
|
+
assert_file_contains "$mem/backlog.md" "> verbatim (user," "AC-005 verbatim blockquote missing" || return 1
|
|
386
|
+
assert_file_contains "$mem/backlog.md" "raised-on: $(today)" "AC-005 raised-on missing" || return 1
|
|
387
|
+
assert_file_contains "$mem/backlog.md" "raised-in-context: backlog-memory-bucket" "AC-005 raised-in-context missing" || return 1
|
|
388
|
+
assert_contains "$report" '"closed": 0' "AC-005 expected closed:0 (open entry shouldn't auto-close)" || return 1
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
test_when_bootstrap_entry_has_superseded_at_today_then_auto_close_removes_it() {
|
|
392
|
+
local mem; mem="$(mktemp -d)"; trap "rm -rf $mem" RETURN
|
|
393
|
+
seed_skel "$mem"
|
|
394
|
+
add "$mem" "backlog" "## bootstrap
|
|
395
|
+
|
|
396
|
+
- source: inferred-from-code
|
|
397
|
+
- status: dropped
|
|
398
|
+
- raised-on: $(today)
|
|
399
|
+
- raised-in-context: backlog-memory-bucket
|
|
400
|
+
- verified-at: HEAD
|
|
401
|
+
- last-touched: $(today)
|
|
402
|
+
- superseded-at: $(today)"
|
|
403
|
+
local report; report="$(sweep auto-close "$mem")" || { fail "AC-006 sweep crashed"; return 1; }
|
|
404
|
+
assert_file_not_contains "$mem/backlog.md" "## bootstrap" "AC-006 bootstrap must be auto-closed" || return 1
|
|
405
|
+
assert_contains "$report" '"closed": 1' "AC-006 expected closed:1 (got: $report)" || return 1
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
test_when_backlog_entry_verified_at_old_sha_then_not_classified_stale() {
|
|
409
|
+
local mem; mem="$(mktemp -d)"; trap "rm -rf $mem" RETURN
|
|
410
|
+
seed_skel "$mem"
|
|
411
|
+
# Non-git tempdir → stale predicate falls back to days-since(last-touched).
|
|
412
|
+
# 120 days ago should be stale for any other canonical file; backlog must
|
|
413
|
+
# be stale-exempt and NOT surface.
|
|
414
|
+
add "$mem" "backlog" "## ancient-intent-aaaa
|
|
415
|
+
|
|
416
|
+
> verbatim (user, $(days_ago 120)):
|
|
417
|
+
> TODO: this is intentionally aged
|
|
418
|
+
|
|
419
|
+
- source: user-instruction
|
|
420
|
+
- status: open
|
|
421
|
+
- raised-on: $(days_ago 120)
|
|
422
|
+
- raised-in-context: legacy-workflow
|
|
423
|
+
- verified-at: HEAD
|
|
424
|
+
- last-touched: $(days_ago 120)"
|
|
425
|
+
# stale-sweep reads one reply per surfaced entry from stdin; if backlog is
|
|
426
|
+
# correctly stale-exempt, NO entry is surfaced and stdin is never consumed.
|
|
427
|
+
local report; report="$(sweep stale-sweep "$mem" "")" || { fail "AC-009 sweep crashed"; return 1; }
|
|
428
|
+
assert_file_contains "$mem/backlog.md" "## ancient-intent-aaaa" "AC-009 backlog entry must survive stale-sweep" || return 1
|
|
429
|
+
assert_contains "$report" '"reverified": 0' "AC-009 expected reverified:0 (entry never surfaced)" || return 1
|
|
430
|
+
assert_contains "$report" '"deleted": 0' "AC-009 expected deleted:0" || return 1
|
|
431
|
+
assert_contains "$report" '"mark_closed": 0' "AC-009 expected mark_closed:0" || return 1
|
|
432
|
+
assert_contains "$report" '"kept": 0' "AC-009 expected kept:0 (kept counts a surfaced entry that was skipped — backlog should never surface)" || return 1
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
test_when_readme_documents_backlog_and_assistant_deferral_then_present() {
|
|
436
|
+
# Asserts the LIVE README.md (which the implement worker edits) — not a
|
|
437
|
+
# fixture. Covers AC-007 schema-doc lockstep.
|
|
438
|
+
local readme="$REPO_ROOT/.claude/memory/README.md"
|
|
439
|
+
[ -f "$readme" ] || { fail "AC-007 README missing at $readme"; return 1; }
|
|
440
|
+
assert_file_contains "$readme" "backlog.md" "AC-007 README missing backlog.md mention" || return 1
|
|
441
|
+
assert_file_contains "$readme" "assistant-deferral" "AC-007 README missing assistant-deferral provenance value" || return 1
|
|
442
|
+
# backlog.md should be listed under both the Files table and the stable-key
|
|
443
|
+
# table; greedy substring matches the row prefix.
|
|
444
|
+
local backlog_rows; backlog_rows="$(grep -cE '^\|\s*`backlog\.md`' "$readme" 2>/dev/null || true)"
|
|
445
|
+
[ -z "$backlog_rows" ] && backlog_rows=0
|
|
446
|
+
if [ "$backlog_rows" -lt 2 ]; then
|
|
447
|
+
fail "AC-007 expected backlog.md row in BOTH Files table and stable-key table (>=2 rows); got $backlog_rows"
|
|
448
|
+
return 1
|
|
449
|
+
fi
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
# --- workflow-loop-closing-hygiene: stamp-closure mode (Goal 3) --------------
|
|
453
|
+
# Covers AC-005, AC-006, AC-007, AC-008 from
|
|
454
|
+
# docs/specs/workflow-loop-closing-hygiene.md. All start RED until sweep.py
|
|
455
|
+
# adds `mode_stamp_closure` + `--backlog-keys` arg + a 4th MODE_DISPATCH entry.
|
|
456
|
+
|
|
457
|
+
# Reuse the existing `sweep` helper for stamp-closure: pass a 4th positional
|
|
458
|
+
# argument forwarding the --backlog-keys CSV. The current helper signature
|
|
459
|
+
# `sweep mode mem replies` — we extend behavior with a sibling invoker so the
|
|
460
|
+
# existing call sites stay untouched.
|
|
461
|
+
sweep_stamp_closure() {
|
|
462
|
+
local mem="$1" keys="${2:-}"
|
|
463
|
+
if [ ! -f "$SWEEP" ]; then
|
|
464
|
+
echo "{\"error\":\"sweep.py missing\"}"
|
|
465
|
+
return 127
|
|
466
|
+
fi
|
|
467
|
+
python3 "$SWEEP" --mode stamp-closure --memory-dir "$mem" --backlog-keys "$keys"
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
# Add a backlog entry shaped like the canonical write from /memory-flush.
|
|
471
|
+
add_open_backlog_entry() {
|
|
472
|
+
local mem="$1" key="$2"
|
|
473
|
+
add "$mem" "backlog" "## $key
|
|
474
|
+
|
|
475
|
+
> verbatim (user, $(today)):
|
|
476
|
+
> stub intent line for $key
|
|
477
|
+
|
|
478
|
+
- source: user-instruction
|
|
479
|
+
- status: open
|
|
480
|
+
- raised-on: $(today)
|
|
481
|
+
- raised-in-context: test-fixture
|
|
482
|
+
- verified-at: HEAD
|
|
483
|
+
- last-touched: $(today)"
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
test_when_stamp_closure_runs_then_status_and_superseded_at_set() {
|
|
487
|
+
local mem; mem="$(mktemp -d)"; trap "rm -rf $mem" RETURN
|
|
488
|
+
seed_skel "$mem"
|
|
489
|
+
add_open_backlog_entry "$mem" "k1"
|
|
490
|
+
add_open_backlog_entry "$mem" "k2"
|
|
491
|
+
add_open_backlog_entry "$mem" "k3"
|
|
492
|
+
local report; report="$(sweep_stamp_closure "$mem" "k1,k2")" \
|
|
493
|
+
|| { fail "AC-005 sweep crashed (got: $report)"; return 1; }
|
|
494
|
+
assert_contains "$report" '"stamped": 2' "AC-005 expected stamped:2 (got: $report)" || return 1
|
|
495
|
+
# k1 and k2 must now carry status: picked-up + superseded-at: today.
|
|
496
|
+
# k3 must remain status: open (no superseded-at:).
|
|
497
|
+
python3 - "$mem/backlog.md" "$(today)" <<'PY' || return 1
|
|
498
|
+
import re, sys
|
|
499
|
+
path, today = sys.argv[1], sys.argv[2]
|
|
500
|
+
text = open(path).read()
|
|
501
|
+
def find(key):
|
|
502
|
+
m = re.search(rf'^## {re.escape(key)}$(.*?)(?=^## |\Z)', text, re.M | re.DOTALL)
|
|
503
|
+
return m.group(1) if m else ''
|
|
504
|
+
for key in ("k1", "k2"):
|
|
505
|
+
block = find(key)
|
|
506
|
+
if not block:
|
|
507
|
+
sys.exit(f"missing block for {key}")
|
|
508
|
+
if 'status: picked-up' not in block:
|
|
509
|
+
sys.exit(f"{key}: expected status: picked-up; got: {block!r}")
|
|
510
|
+
if f'superseded-at: {today}' not in block:
|
|
511
|
+
sys.exit(f"{key}: expected superseded-at: {today}; got: {block!r}")
|
|
512
|
+
k3 = find("k3")
|
|
513
|
+
if 'status: open' not in k3:
|
|
514
|
+
sys.exit(f"k3: expected status: open (untouched); got: {k3!r}")
|
|
515
|
+
if 'superseded-at:' in k3:
|
|
516
|
+
sys.exit(f"k3: expected NO superseded-at: (untouched); got: {k3!r}")
|
|
517
|
+
PY
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
test_when_stamp_closure_then_auto_close_deletes() {
|
|
521
|
+
local mem; mem="$(mktemp -d)"; trap "rm -rf $mem" RETURN
|
|
522
|
+
seed_skel "$mem"
|
|
523
|
+
add_open_backlog_entry "$mem" "k1"
|
|
524
|
+
add_open_backlog_entry "$mem" "k2"
|
|
525
|
+
sweep_stamp_closure "$mem" "k1,k2" >/dev/null \
|
|
526
|
+
|| { fail "AC-007 stamp-closure crashed"; return 1; }
|
|
527
|
+
local report; report="$(sweep auto-close "$mem")" \
|
|
528
|
+
|| { fail "AC-007 auto-close crashed"; return 1; }
|
|
529
|
+
assert_file_not_contains "$mem/backlog.md" "## k1" "AC-007 expected k1 deleted by auto-close" || return 1
|
|
530
|
+
assert_file_not_contains "$mem/backlog.md" "## k2" "AC-007 expected k2 deleted by auto-close" || return 1
|
|
531
|
+
assert_contains "$report" '"closed": 2' "AC-007 expected closed:2 (got: $report)" || return 1
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
test_when_stamp_closure_with_empty_keys_then_zero_stamped() {
|
|
535
|
+
local mem; mem="$(mktemp -d)"; trap "rm -rf $mem" RETURN
|
|
536
|
+
seed_skel "$mem"
|
|
537
|
+
add_open_backlog_entry "$mem" "k1"
|
|
538
|
+
local before; before="$(cat "$mem/backlog.md")"
|
|
539
|
+
local report; report="$(sweep_stamp_closure "$mem" "")" \
|
|
540
|
+
|| { fail "AC-008 sweep crashed on empty keys"; return 1; }
|
|
541
|
+
assert_contains "$report" '"stamped": 0' "AC-008 expected stamped:0 (got: $report)" || return 1
|
|
542
|
+
local after; after="$(cat "$mem/backlog.md")"
|
|
543
|
+
if [ "$before" = "$after" ]; then return 0; fi
|
|
544
|
+
fail "AC-008 backlog.md was modified despite empty keys"
|
|
545
|
+
diff <(printf '%s' "$before") <(printf '%s' "$after") || true
|
|
546
|
+
return 1
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
test_when_stamp_closure_with_nonexistent_key_then_missing_list() {
|
|
550
|
+
local mem; mem="$(mktemp -d)"; trap "rm -rf $mem" RETURN
|
|
551
|
+
seed_skel "$mem"
|
|
552
|
+
add_open_backlog_entry "$mem" "k1"
|
|
553
|
+
local report; report="$(sweep_stamp_closure "$mem" "nonexistent-key")" \
|
|
554
|
+
|| { fail "AC-008 sweep crashed (got: $report)"; return 1; }
|
|
555
|
+
assert_contains "$report" '"stamped": 0' "AC-008 expected stamped:0 for missing key" || return 1
|
|
556
|
+
assert_contains "$report" '"missing"' "AC-008 expected missing list in report (got: $report)" || return 1
|
|
557
|
+
assert_contains "$report" 'nonexistent-key' "AC-008 expected key in missing list" || return 1
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
test_when_stamp_closure_called_twice_then_idempotent() {
|
|
561
|
+
local mem; mem="$(mktemp -d)"; trap "rm -rf $mem" RETURN
|
|
562
|
+
seed_skel "$mem"
|
|
563
|
+
add_open_backlog_entry "$mem" "k1"
|
|
564
|
+
sweep_stamp_closure "$mem" "k1" >/dev/null \
|
|
565
|
+
|| { fail "AC-005 first stamp crashed"; return 1; }
|
|
566
|
+
sweep_stamp_closure "$mem" "k1" >/dev/null \
|
|
567
|
+
|| { fail "AC-005 second stamp crashed"; return 1; }
|
|
568
|
+
# status: picked-up should appear exactly once; superseded-at: today exactly once.
|
|
569
|
+
local picked_count; picked_count="$(grep -c 'status: picked-up' "$mem/backlog.md")"
|
|
570
|
+
local sup_count; sup_count="$(grep -c "superseded-at: $(today)" "$mem/backlog.md")"
|
|
571
|
+
if [ "$picked_count" -ne 1 ] || [ "$sup_count" -ne 1 ]; then
|
|
572
|
+
fail "AC-005 idempotency violated: status-count=$picked_count, superseded-count=$sup_count (expected 1, 1)"
|
|
573
|
+
return 1
|
|
574
|
+
fi
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
test_when_stamp_closure_missing_keys_arg_then_argparse_error() {
|
|
578
|
+
if [ ! -f "$SWEEP" ]; then
|
|
579
|
+
fail "AC-005 sweep.py missing"
|
|
580
|
+
return 1
|
|
581
|
+
fi
|
|
582
|
+
python3 "$SWEEP" --mode stamp-closure --memory-dir "$REPO_ROOT/.claude/memory" >/dev/null 2>&1
|
|
583
|
+
local ec=$?
|
|
584
|
+
if [ "$ec" -ne 2 ]; then
|
|
585
|
+
fail "AC-005 expected argparse exit 2 when --backlog-keys missing, got $ec"
|
|
586
|
+
return 1
|
|
587
|
+
fi
|
|
588
|
+
}
|
|
589
|
+
|
|
307
590
|
# --- runner -------------------------------------------------------------------
|
|
308
591
|
|
|
309
592
|
run test_when_resolved_at_present_on_pending_then_flush_removes_block
|
|
@@ -317,6 +600,22 @@ run test_when_2_stale_then_flush_offers_reverify_then_delete_prompts
|
|
|
317
600
|
run test_when_stale_mark_closed_on_pending_then_resolved_at_added_not_deleted
|
|
318
601
|
run test_when_no_closure_no_prose_no_stale_then_entry_survives_all_paths
|
|
319
602
|
run test_when_pre_spec_entry_no_source_no_verbatim_then_grandfathered
|
|
603
|
+
run test_when_pending_empty_then_sweep_does_not_touch_pending
|
|
604
|
+
run test_when_pending_empty_AND_q999_has_resolved_at_then_q999_is_swept
|
|
605
|
+
|
|
606
|
+
# backlog-memory-bucket coverage
|
|
607
|
+
run test_when_promote_user_candidate_writes_canonical_entry_with_status_open_and_verbatim
|
|
608
|
+
run test_when_bootstrap_entry_has_superseded_at_today_then_auto_close_removes_it
|
|
609
|
+
run test_when_backlog_entry_verified_at_old_sha_then_not_classified_stale
|
|
610
|
+
run test_when_readme_documents_backlog_and_assistant_deferral_then_present
|
|
611
|
+
|
|
612
|
+
# workflow-loop-closing-hygiene: stamp-closure coverage
|
|
613
|
+
run test_when_stamp_closure_runs_then_status_and_superseded_at_set
|
|
614
|
+
run test_when_stamp_closure_then_auto_close_deletes
|
|
615
|
+
run test_when_stamp_closure_with_empty_keys_then_zero_stamped
|
|
616
|
+
run test_when_stamp_closure_with_nonexistent_key_then_missing_list
|
|
617
|
+
run test_when_stamp_closure_called_twice_then_idempotent
|
|
618
|
+
run test_when_stamp_closure_missing_keys_arg_then_argparse_error
|
|
320
619
|
|
|
321
620
|
echo "----"
|
|
322
621
|
echo "Passed: $PASS Failed: $FAIL"
|