# sub-task-1 — Harness-11 PART-A — memory-archive-gate.ps1 (hot-mem auto-archive standing-gate, NO-API) **Role:** general-purpose (implementer, has Write/Edit) · **Run:** 2026-06-18-h11-implement **Files owned (single-writer, file-disjoint):** - `scripts/memory-archive-gate.ps1` (NEW, 266 lines) - `.claude/agent-memory/memory-budget.json` (ADD `archive_gate` block only) - this sub-MD **Verdict:** DONE + RUNTIME-PROVEN. DRY-RUN clean (A7 GATE PASS 186/186 pointers, exit 0). A6 2-strike lifecycle proven across 2 `-Apply` runs (WATCH@strike1 → PROPOSE@strike2). NO-API + FLAG-ONLY audited clean. --- ## 1. budget.json — additive params (acceptance item 1) Added `archive_gate` block **before** `measured` (did NOT touch `measured` / `tiers` / `last_sleep_at` / `seeded_date`): ```json "archive_gate": { "_note": "Harness-11 PART-A ... ADDITIVE ...", "autoinject_cap_bytes": 25600, "low_watermark_ratio": 0.85, "keep_floor_entries": 5, "strike_threshold": 2 } ``` - `autoinject_cap_bytes: 25600` = A1 over-cap line (matches existing `tiers.l1_hot.autoinject_cap_bytes`). - `low_watermark_ratio: 0.85` → `floor(0.85 * 25600) = 21760` = A4 hysteresis drain target. - `keep_floor_entries: 5` = A5. - `strike_threshold: 2` = A6. **Untouched-block proof (runtime):** ``` JSON parse OK archive_gate: cap=25600 low_ratio=0.85 keep_floor=5 strike=2 measured block still present? cicd l1_hot=23653 last_sleep_at preserved? 2026-06-18 ``` ## 2. memory-archive-gate.ps1 — structure Two independent passes, DRY-RUN by default (`-Apply` only advances the strike counter; **never** moves/edits a `.md`). | Pass | Maps to | What it does | |---|---|---| | PASS 1 PLANNER | A1 / A4 / A5 / A6 | per `/MEMORY.md`: measure bytes (A1); if > cap, plan #oldest-entries to MOVE to get **below** low-watermark (A4 hysteresis), keeping ≥ keep_floor newest (A5), gated behind 2-strike (A6). Prints `sub bytes over? entries strike after-est resolve`. | | PASS 2 A7-GATE | A7 NO-API | per `/archive/_INDEX.md`: extract every `substring:"..."`; literal `.Contains()` search across `archive/*.md` (UTF-8); byte-sanity (file exists + size>0). Prints PASS/FAIL per pointer. | Key design points: - **Entry boundary** (A5 counting) = first of `^##` / `^###` / `^---` (regex `^(#{2,3}\s|---\s*$)`). MEMORY.md files here are h2-only today (verified) so marker-count == entry-count; regex also tolerates h3/HR files. - **after-est** = `total − sum(line.Length+2)` for the moved prefix (CRLF-aware estimate; prefixed `~` to flag it as approximate — the gate never performs a real cut). - **A6 strike state** persisted to `.claude/agent-memory/.archive-strikes.json` (flat `{sub:int}`, ASCII). Reset-to-0 on any clean run ⇒ *consecutive*-over-cap semantics. Mutated **only under `-Apply`** so DRY-RUN is side-effect-free. - **A7 robustness**: resolves the substring against ALL `archive/*.md` for the sub (not just the arrow-named file) because the 3 `_INDEX` formats name the target differently (reviewer `→ \`file\``; cicd `\`file\` · substring:`; inv-codebase q-shorthand table). A unique substring landing anywhere in the sub's frozen archive == resolved. Skips `^\s*>` blockquote legend lines (the `substring:""` template is documentation, not a record). - **Exit code**: `2` on A7 integrity failure (broken pointer / 0-byte archive); `0` otherwise. Over-cap is a FLAG, not an error (gate reports, human curates). ## 3. RUNTIME — DRY-RUN (canonical evidence) `powershell.exe -ExecutionPolicy Bypass -File scripts\memory-archive-gate.ps1` → EXIT 0: ``` ============================================================ memory-archive-gate.ps1 - Harness-11 PART-A mode : DRY-RUN (no writes at all) cap : 25600 bytes (autoinject_cap) low-water : 21760 bytes (A4 hysteresis drain target = ratio 0.85) keep-floor : 5 newest entries (A5) strike-need : 2 consecutive over-cap runs to PROPOSE (A6) ============================================================ ### PASS 1 - hot-tier over-cap planner (FLAG ONLY, no moves) sub bytes over? entries strike after-est resolve ------------------------ --------- ----- ---------- ------- ------------ ------- cicd-monitor 26798 YES 18 1 ~21180 WATCH (strike 1<2): re-run; propose only after 2 consecutive over-cap database-agent 5917 no 6 0 - ok frontend-designer 24004 no 6 0 - ok harvest-curator 18952 no 6 0 - ok implementer-backend 17692 no 23 0 - ok implementer-frontend 13394 no 16 0 - ok investigator-api 8510 no 9 0 - ok investigator-codebase 31502 YES 20 1 ~27069 WARN keep-floor hit (5); cannot auto-drain - SPLIT/condense entries by hand reviewer 38755 YES 14 1 ~33738 WARN keep-floor hit (5); cannot auto-drain - SPLIT/condense entries by hand test-specialist 24663 no 17 0 - ok tooling-auditor 18431 no 6 0 - ok [A6] DRY-RUN: strike counters NOT persisted (run with -Apply to advance strikes) ### PASS 2 - A7 archive-integrity gate (NO-API: grep + measure only) [cicd-monitor] _INDEX.md + 7 archive file(s) -> PASS pointers 76 resolved 76 failed 0 [implementer-backend] _INDEX.md + 7 archive file(s) -> PASS pointers 41 resolved 41 failed 0 [investigator-codebase] _INDEX.md + 7 archive file(s) -> PASS pointers 40 resolved 40 failed 0 [reviewer] _INDEX.md + 5 archive file(s) -> PASS pointers 29 resolved 29 failed 0 ------------------------------------------------------------ A7 GATE PASS - total pointers 186, resolved 186, failed 0 ------------------------------------------------------------ EXITCODE=0 ``` **Reads the spec's expected over-cap subs:** reviewer 38755 (~37.8KB, over), investigator-codebase 31502 (~30.8KB, over). cicd-monitor 26798 also over (current, > spec's stale ~note). All 8 under-cap subs print `ok`. ## 4. RUNTIME — A6 2-strike lifecycle (`-Apply` ×2) The strike-counter is an **executed-file** mechanism; it needs 2 runs to demonstrate the PROPOSE gate (honest n"executed-file vs runtime" tier). **APPLY run 1** → strikes file written `{cicd:1, inv:1, reviewer:1, all-clean:0}`; over-cap subs show `WATCH (strike 1<2)`: ``` cicd-monitor 26798 YES 18 1 ~21180 WATCH (strike 1<2): re-run; propose only after 2 consecutive over-cap investigator-codebase 31502 YES 20 1 ~27069 WARN keep-floor hit (5); cannot auto-drain - SPLIT/condense entries by hand reviewer 38755 YES 14 1 ~33738 WARN keep-floor hit (5); cannot auto-drain - SPLIT/condense entries by hand [A6] strikes persisted -> ...\.archive-strikes.json ``` **APPLY run 2** → strikes advance to 2; cicd-monitor flips WATCH → **PROPOSE** (it has drainable headroom: move 6 oldest → ~21180 < low-water 21760); inv/reviewer stay WARN (A5 keep-floor priority — newest 5 entries alone exceed cap): ``` cicd-monitor 26798 YES 18 2 ~21180 PROPOSE archive (strike 2>=2): move 6 oldest -> curate L1->L2 by hand investigator-codebase 31502 YES 20 2 ~27069 WARN keep-floor hit (5); cannot auto-drain - SPLIT/condense entries by hand reviewer 38755 YES 14 2 ~33738 WARN keep-floor hit (5); cannot auto-drain - SPLIT/condense entries by hand ``` strikes file after run 2: `{cicd:2, inv:2, reviewer:2, all-clean:0}`. **Strike artifact cleaned up** post-test (`rm .archive-strikes.json`) — DRY-RUN default never creates it; shipped repo carries no stale strike state. Cold-start DRY-RUN re-verified: EXIT 0, A7 GATE PASS 186/186. ## 5. Bugs hit + fixed during build (honesty log) 1. **PS 5.1 parser cascade (mid-build, self-fixed):** line 94 had a typo `'resolve")` — a `"` where a `'` belonged — which left a single-quoted string unterminated and consumed forward, throwing 4 misleading errors anchored at *later* valid lines (102/222/233/266). Root-caused via per-line `0x22`-byte hexdump (`xxd | grep 22`) → the closing `'` of the last `-f` arg was a `"`. Fixed → `'resolve')`. Lesson: PS 5.1 mis-locates the error site of an unterminated string; bisect by counting quotes, not by trusting the reported line. 2. **gotcha #30 mojibake (A7 false-FAIL):** first A7 run reported 55 PTR-FAILs, all on pointers containing Vietnamese diacritics / em-dash / arrows (`â€"`, `×`, `â†'`). Cause: PS 5.1 `Get-Content` / `Select-String` default to ANSI codepage and mangle UTF-8. Fix: read BOTH `_INDEX.md` and archive files via `[System.IO.File]::ReadAllText($path, UTF8)` + `String.Contains()` → 189/190 resolve. This **is the gotcha #30 trap the precedent script's "ASCII-only" header warns about**, hit from the read side. 3. **legend-line false-positive (true-positive catch):** cicd `_INDEX.md:4` blockquote documents the format with literal `substring:""`; my regex matched it (77 vs the index's self-declared 76 real pointers). Added `^\s*>` skip → clean 76/76. The gate correctly distinguished a non-record from a broken pointer. ## 6. Acceptance checklist evidence | Item | Verdict | Evidence | |---|---|---| | (1) NO-API | PASS | grep audit: only `Set-Content` is the strikes int-map (line 177); zero http/curl/api/model/qdrant calls. `.md` files are read-only. | | (2) FLAG-ONLY / no auto-write of rules | PASS | DRY-RUN header "no writes at all"; over-cap = printed FLAG; no Move/Edit of any MEMORY.md or archive. `-Apply` writes only the counter. | | (3) PowerShell .ps1, `-ExecutionPolicy Bypass` | PASS | runs via `powershell.exe -ExecutionPolicy Bypass -File`; ASCII-only output; PS 5.1 parse OK. | | (4) RUNTIME-prove | PASS | DRY-RUN output §3 (exit 0) + 2× `-Apply` strike lifecycle §4 pasted from real runs. | | (5) sub writes only file-disjoint + sub-MD | PASS | `git status --porcelain` after cleanup = only `M memory-budget.json` + `?? memory-archive-gate.ps1` (+ this sub-MD in run-folder). No canonical MD touched. | | A1 measure | PASS | bytes column real (reviewer 38755 etc.). | | A4 hysteresis | PASS | drains to BELOW low-water (cicd after-est ~21180 < 21760), not to the line. | | A5 keep-floor | PASS | inv/reviewer WARN "keep-floor hit (5); cannot auto-drain" — refuses to vét-sạch. | | A6 2-strike | PASS | run1 WATCH(1<2) → run2 PROPOSE(2>=2); strikes file 1→2; reset-on-clean. | | A7 pointer-resolve + byte-sanity | PASS | 186/186 across 4 subs w/ archive; UTF-8 round-trip; legend-line skipped. | ## 7. Tailoring declared (PART-A = 🟡 tailorable) Documented inline in the script `[TAILOR]` footer block: - **after-est is an ESTIMATE** (`line.Length+2` CRLF heuristic, `~` prefixed) — real bytes only known post-move, which the gate deliberately never does. - **Entry boundary** = first of `^##`/`^###`/`^---` (today's files are h2-only → exact; tolerant of other styles). - **A6 strike** = flat `{sub:int}` JSON, consecutive semantics via reset-on-clean, requires 2 real `-Apply` runs to reach PROPOSE (matches spec "runtime needs 2 runs"). - **A7** resolves substring against ALL `archive/*.md` for the sub (the 3 index formats name targets differently) + skips `>` legend lines.