# 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 `<sub>/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 `<sub>/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:"<unique-string>"` 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:"<unique-string>"`; 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.
