From 42d6912f572d536f19c811cecef47901cc520183 Mon Sep 17 00:00:00 2001 From: WAR Test Date: Mon, 27 Jul 2026 16:17:13 -0700 Subject: [PATCH 1/9] fix(plan): re-anchor Task 1.2 tighten-lesson path to docs/learnings/archive/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tighten lesson was archived on 2026-07-27 (PR #1167, operator-directed RESOLVED sweep), so Task 1.2's declared path no longer resolves. Archiving is a move, not a deletion — the lesson stays FTS-queryable, so its inaccurate zsh-fuse mechanism prose still misleads and still needs correcting. Re-anchored in place rather than restoring the lesson to the hot set: restoring would add its row back and push the projection over the 17,000 B advisory that the same sweep just cleared (16,797 B, verdict ok). The sibling cmdquery lesson is unaffected — still hot at its declared path. Lead adjudication at the campaign decompose gate. Co-Authored-By: Claude Opus 5 --- docs/plans/2026-07-26-war-memory-cli-correctness.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/plans/2026-07-26-war-memory-cli-correctness.md b/docs/plans/2026-07-26-war-memory-cli-correctness.md index 88de166..dac9b2a 100644 --- a/docs/plans/2026-07-26-war-memory-cli-correctness.md +++ b/docs/plans/2026-07-26-war-memory-cli-correctness.md @@ -46,7 +46,8 @@ Phase edge rationale: the release bump is its own trailing phase per the decompo ### Task 1.2: lesson prose truth-up (#1147 + D9) -- Files: `docs/learnings/tighten-target-flag-has-three-independent-silent-degradation-paths.md`, `docs/learnings/cmdquery-topk-budget-share-tighten-targets-pre-fix-truthy-ternary-shape.md` +- Files: `docs/learnings/archive/tighten-target-flag-has-three-independent-silent-degradation-paths.md`, `docs/learnings/cmdquery-topk-budget-share-tighten-targets-pre-fix-truthy-ternary-shape.md` +- **Path note (Lead adjudication, 2026-07-27):** the tighten lesson was archived on 2026-07-27 (PR #1167, operator-directed `RESOLVED` sweep) and now lives under `docs/learnings/archive/`. Archiving is a move, not a deletion — the lesson stays queryable via FTS, so its mechanism prose still misleads and still needs this correction. Edit it **in place at the archive path**; do NOT restore it to the hot set (that would re-breach the 17,000 B projection advisory this repo just cleared). The sibling `cmdquery-…` lesson is still hot at its declared path. - Plan slice: Two committed-lesson corrections, sourced from the red-team report (`docs/red-team/2026-07-24-memory-tooling-hardening.md`, Minors, "Purpose, zsh threading") and spec §4 — not re-derived: - **Tighten lesson, failure-mode-3 item:** replace the "collapses to one argv token and the flag is dropped entirely" sentence pair with the fuse mechanism (zsh, lacking `SH_WORD_SPLIT`, does not word-split the unquoted `${TIGHTEN_TARGET:+…}` expansion → one fused argv word `--target 2000` → `parseArgv` keys `{"target 2000": true}` → `argv.target` stays `undefined`; observable end state coincides with never setting the variable). Keep the "same silent outcome" clause — it was the accurate part. - **Tighten lesson, closing paragraph:** rewrite "Not yet extended to siblings" to past tense recording that the extension landed in Task 1.1's `cmdQuery` fix. From 4453863222a55cf4fa09fd56a8b0362d67aed129 Mon Sep 17 00:00:00 2001 From: RT Probe Date: Mon, 27 Jul 2026 16:44:36 -0700 Subject: [PATCH 2/9] =?UTF-8?q?fix(plan):=20red-team=20round=200=20?= =?UTF-8?q?=E2=80=94=2012=20blocking=20findings=20patched=20in=20place?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - End state 1 + Purpose narrowed to space-separated flag values; the =-attached residual (D10, frozen parseArgv) lifted into the Intent as a named still-silent out-of-scope path instead of a Notes-only mention. - Task 1.2 sweep + End state 9 greps made case-insensitive (grep -in), token-anchored — a re-cased straggler could previously false-negate. - ADR 0028 quotes the hub-WARN verbatim: added to Task 1.1 Files with a dated append-only amendment; per-medium gate (NEW-present on the ADR, OLD-absent on live skills/ surfaces) added to End state 8. - The existing spawn-the-CLI hub-WARN test pins the message bytes and reds on the noun flip: named as a required in-task edit; End state 8's stay-green clause narrowed to the inboundCiters unit tests. - cmdQuery guard triple gets coupling comments both ways (cmdTightenPlan's --target block named as reference implementation). - End state 3's explicit-defaults invocation built from imported constants, not hand-typed 10/4096 literals. - End state 6 re-filed HARD (task gate runs the recursive live-tree lint pre-merge); backstop 1 narrowed to the post-gate window. - Backstop 2 rewritten: in-run runner = Phase 1 servitor (recorded resolved-lesson-stamp convention); Lead Gate-2 duty to exclude the archived tighten slug from promotion (flat-copy would duplicate it hot). - Contention note corrected: three siblings, not one; plan 4's README + version-slots.test.mjs Phase-1 overlap recorded with mitigation. - Doc-sweep note widened to the WARN message string. Co-Authored-By: Claude Opus 5 --- .../2026-07-26-war-memory-cli-correctness.md | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/plans/2026-07-26-war-memory-cli-correctness.md b/docs/plans/2026-07-26-war-memory-cli-correctness.md index dac9b2a..076cc2c 100644 --- a/docs/plans/2026-07-26-war-memory-cli-correctness.md +++ b/docs/plans/2026-07-26-war-memory-cli-correctness.md @@ -5,18 +5,18 @@ Issues addressed: #1145, #1135, #1154, #1147 (theme: memory-tooling). ## Commander's Intent -- Purpose: Close the memory CLI's last silent-degradation flag pair (`cmdQuery`'s `--top-k`/`--budget`), make the one shared redaction-lint invocation cover the committed `archive/` subtree on every surface, converge `cmdArchive`'s concept-hub WARN onto the slug-deduped counting its two sibling surfaces already use, and true up the two committed lessons that describe these defects — so no memory-CLI surface degrades silently and no lesson misstates a live mechanism. +- Purpose: Close the memory CLI's last silent-degradation flag pair (`cmdQuery`'s `--top-k`/`--budget`), make the one shared redaction-lint invocation cover the committed `archive/` subtree on every surface, converge `cmdArchive`'s concept-hub WARN onto the slug-deduped counting its two sibling surfaces already use, and true up the two committed lessons that describe these defects — so no memory-CLI surface degrades silently on a space-separated flag value and no lesson misstates a live mechanism (the `=`-attached flag form remains a known-silent, deliberately frozen residual — D10, named in End state 1). - Method: Bind each flag guard at `cmdQuery`'s own argv boundary by copying the ratified `--target` three-way resolution from `cmdTightenPlan` (shape copy, no shared helper; `parseArgv` is frozen); refusal = exit 1, stderr naming the flag and the received token, placed before the `walkCorpus` call so a refused invocation writes nothing (no `appendQueryLog` line, no stdout block). Land archive coverage inside `cmdLint`'s directory branch via `fs.readdirSync(t, { recursive: true })` so CI (`memory-audit.yml`) and the gate wrapper (`war-memory-lint.test.sh`) inherit it with zero edits — both stay byte-unchanged. Slug-dedupe the hub WARN (`new Set(hotInbound.map((r) => r.slug)).size` drives both the `>= 2` predicate and the printed count, whose noun becomes `inbound citers`) and update the adjacent comment to name the three-surface convergence. Correct lesson prose from the red-team-measured fuse mechanism (sourced from `docs/red-team/2026-07-24-memory-tooling-hardening.md`, not re-derived); the sibling lesson's RESOLVED stamp lands with-or-after the `cmdQuery` fix, never before (deps edge enforces this). Every mandated grep is a floor: pair it with a same-scope hand survey and list each straggler as a survey-derived correction (or state zero stragglers explicitly). New refusal test cases get their own banner — do not edit the existing `refusal (#1059)` block's narrative. `cmdQueriesBatch` and the Lead prefetch stay byte-identical; no `parseArgv` change; the `=`-attached-flag residual stays frozen. - End state: - 1. A supplied-but-invalid `--top-k` or `--budget` on `query` (bare flag, non-numeric, zero, negative — eight cases across the two flags) exits 1 with empty stdout and stderr naming the flag and the received token; spawn-the-CLI regressions cover all eight and are red pre-fix. + 1. A supplied-but-invalid space-separated `--top-k` or `--budget` value on `query` (bare flag, non-numeric, zero, negative — eight cases across the two flags) exits 1 with empty stdout and stderr naming the flag and the received token; spawn-the-CLI regressions cover all eight and are red pre-fix. The `=`-attached form (`--top-k=abc`) never reaches these guards — the frozen `parseArgv` keys it as a bare boolean flag and the query silently runs on defaults; that is a NAMED, still-silent, deliberately frozen residual (D10), out of scope by ratified spec decision. 2. A refused `query` invocation appends no line to the query log (asserted on a fixture local root; delete-the-guard must red the test). - 3. Unchanged paths hold: flagless `query` output equals an explicit `--top-k 10 --budget 4096` invocation's output on the same fixture (defaults unchanged — `DEFAULT_TOP_K` 10 / `DEFAULT_BUDGET` 4096); `--top-k 3` yields ≤ 3 lessons; a fractional `--top-k 3.5` binds (D3 — `slice` truncates to 3); a valid `--budget` still caps the block; no diff hunk in the change set touches `cmdQueriesBatch` (checked from the task diff at audit/merge). + 3. Unchanged paths hold: flagless `query` output equals an explicit-defaults invocation built from the imported constants (`--top-k ${DEFAULT_TOP_K} --budget ${DEFAULT_BUDGET}` — never hand-typed literals) on the same fixture (defaults unchanged); `--top-k 3` yields ≤ 3 lessons; a fractional `--top-k 3.5` binds (D3 — `slice` truncates to 3); a valid `--budget` still caps the block; no diff hunk in the change set touches `cmdQueriesBatch` (checked from the task diff at audit/merge). 4. Truthy-ternary retirement sweep: a grep of `skills/_shared/war-memory.mjs` for the `? Number(`-on-an-`argv`-read shape finds exactly the three `typeof`-gated guarded arms (`--target`, `--top-k`, `--budget`) and zero unguarded truthy ternaries (the guarded arms are sanctioned substrings, not false reds); the mandated same-scope hand survey (`cmdQuery`/`cmdQueriesBatch` comments, `war-memory.test.mjs` titles naming these flags, sibling verbs' flag reads) is reported with each straggler corrected or an explicit zero-straggler statement. 5. `lint ` walks recursively: a fixture with a credential-shaped violation nested under `/archive/` exits 1 naming file + pattern, the printed hit path carrying the `archive/` subdir prefix (red pre-fix — today `lint: clean`, exit 0); a clean nested fixture exits 0; the wrapper meta-test in `war-memory.test.mjs` shows `war-memory-lint.test.sh ` propagating exit 1 for the nested violation. - 6. Live tree: `node skills/_shared/war-memory.mjs lint docs/learnings/` exits 0 with the recursive walk in place (re-verified at land per the backstop; a violation surfacing there is redacted in the same change set, never waived in prose). + 6. Live tree: `node skills/_shared/war-memory.mjs lint docs/learnings/` exits 0 with the recursive walk in place — a HARD, gate-confirmable check, not a deferred one: the gate-discovered `war-memory-lint.test.sh` lints the live tree with recursion active inside Task 1.1's own merge gate, and its `== gate(bash): … ==` banner in the captured gate log is the audit evidence (a violation surfacing there is redacted in the same change set, never waived in prose; the backstop covers only the post-gate window). 7. `.github/workflows/memory-audit.yml` and `skills/_shared/war-memory-lint.test.sh` are byte-unchanged by the change set (checked from the task diff: neither file appears in `git diff --name-only` against the phase base). - 8. `cmdArchive`'s hub WARN is slug-deduped: a citer slug hot in both roots citing a hub produces no WARN (deduped count 1); two distinct hot citers produce a WARN printing count 2; one dupe citer plus one distinct citer produces a WARN printing 2, not 3 (the mixed case — pins predicate and printed count together); the WARN noun reads `inbound citers` (the deduped count counts slugs, so the old `inbound refs` noun would be false); the adjacent comment says the count is slug-deduped to match the `inbound` verb and `tightenPlan` Floor 2; existing `inboundCiters` hotOnly tests stay green. - 9. The tighten lesson's failure-mode-3 item states the fuse mechanism (zsh does not word-split the unquoted expansion → one fused argv word `--target 2000` → `parseArgv` keys `{"target 2000": true}` → `argv.target` stays `undefined`; outcome coincides with never setting the variable, "same silent outcome" clause kept); a grep of the file for `dropped`/`collapses` finds zero mechanism-position claims; the "Not yet extended to siblings" paragraph is past-tense and cites the landed fix; the same-scope hand survey (full body plus the sibling lesson) is reported. + 8. `cmdArchive`'s hub WARN is slug-deduped: a citer slug hot in both roots citing a hub produces no WARN (deduped count 1); two distinct hot citers produce a WARN printing count 2; one dupe citer plus one distinct citer produces a WARN printing 2, not 3 (the mixed case — pins predicate and printed count together); the WARN noun reads `inbound citers` (the deduped count counts slugs, so the old `inbound refs` noun would be false); the adjacent comment says the count is slug-deduped to match the `inbound` verb and `tightenPlan` Floor 2; the existing `inboundCiters` hotOnly UNIT tests stay green unmodified, while the existing spawn-the-CLI hub-WARN test is updated in-task (title + regex — it pins the message bytes, so the noun flip reds it by design); OLD-absent: a repo grep for the retired WARN literal (`inbound refs`) over live surfaces (`skills/`, `hooks/`, `agents/`, `CONTEXT.md`, `README.md`) returns zero hits — frozen artifacts (`docs/plans/`, `docs/specs/`, `docs/red-team/`, `docs/learnings/archive/`) and ADR 0028's historical quote (superseded by its dated amendment, which must be present) are exempt. + 9. The tighten lesson's failure-mode-3 item states the fuse mechanism (zsh does not word-split the unquoted expansion → one fused argv word `--target 2000` → `parseArgv` keys `{"target 2000": true}` → `argv.target` stays `undefined`; outcome coincides with never setting the variable, "same silent outcome" clause kept); a case-insensitive grep (`grep -in`) of the file for `dropped`/`collapses` finds zero mechanism-position claims (case-insensitive so a re-cased straggler cannot slip the floor); the "Not yet extended to siblings" paragraph is past-tense and cites the landed fix; the same-scope hand survey (full body plus the sibling lesson) is reported. 10. The `cmdquery-topk-budget-share-tighten-targets-pre-fix-truthy-ternary-shape` lesson's description is RESOLVED-prefixed and its body carries a `## RESOLVED` section citing this fix and its tests; the redaction lint passes on both edited lessons. 11. All four version slots carry the next free patch above the live integration base; `version-slots.test.mjs` green. 12. `node --test 'skills/**/*.test.mjs'` fully green. @@ -32,12 +32,13 @@ Phase edge rationale: the release bump is its own trailing phase per the decompo ### Task 1.1: cmdQuery flag guards, cmdLint recursion, cmdArchive WARN dedupe -- Files: `skills/_shared/war-memory.mjs`, `skills/_shared/war-memory.test.mjs` +- Files: `skills/_shared/war-memory.mjs`, `skills/_shared/war-memory.test.mjs`, `docs/adr/0028-memory-store-integrity-tool-enforced.md` - Plan slice: Three fixes in `war-memory.mjs`, each bound at its owning verb's boundary, plus their regressions: - - **`cmdQuery` (#1145, D1–D3):** at function entry, before the `walkCorpus` call, replace the two truthy ternaries (`argv['top-k'] ? Number(...) : DEFAULT_TOP_K`, `argv.budget ? Number(...) : DEFAULT_BUDGET`) with two instances of the ratified three-way resolution, copying the `cmdTightenPlan` `--target` block's shape, in the existing declaration order (`--top-k` first, then `--budget` — a both-invalid invocation reports `--top-k`; source order is the pin, no dedicated ordering test): absent flag → default (byte-identical flagless path); `typeof === 'string'` and `Number.isFinite(t) && t > 0` → binds; anything else → stderr `war-memory query: --top-k requires a positive count (got '')` (resp. `--budget requires a positive byte count`), exit 1. No integer check (D3 — mirror `--target`'s predicate exactly). Guard placement makes no-log-on-refusal hold for free; assert it anyway. + - **`cmdQuery` (#1145, D1–D3):** at function entry, before the `walkCorpus` call, replace the two truthy ternaries (`argv['top-k'] ? Number(...) : DEFAULT_TOP_K`, `argv.budget ? Number(...) : DEFAULT_BUDGET`) with two instances of the ratified three-way resolution, copying the `cmdTightenPlan` `--target` block's shape, in the existing declaration order (`--top-k` first, then `--budget` — a both-invalid invocation reports `--top-k`; source order is the pin, no dedicated ordering test): absent flag → default (byte-identical flagless path); `typeof === 'string'` and `Number.isFinite(t) && t > 0` → binds; anything else → stderr `war-memory query: --top-k requires a positive count (got '')` (resp. `--budget requires a positive byte count`), exit 1. No integer check (D3 — mirror `--target`'s predicate exactly). Add a one-line coupling comment at each new guard arm naming `cmdTightenPlan`'s `--target` block as the ratified reference implementation, and extend that block's existing rationale comment to name its two new copies — three sites, comment-coupled both ways, no shared helper (`parseArgv` frozen). Guard placement makes no-log-on-refusal hold for free; assert it anyway. - **`cmdLint` (#1135, D5):** in the directory branch, swap the one-level `fs.readdirSync(t)` for `fs.readdirSync(t, { recursive: true })`; keep the `.endsWith('.md')` filter and `path.join` unchanged (correct on the subdir-prefixed relative paths recursion returns); preserve the `catch { continue }` fail-open (the wrapper is the loud surface for an unreadable target). - **`cmdArchive` (#1154, D7):** in the advisory concept-hub WARN, compute `const hubCiters = new Set(hotInbound.map((r) => r.slug)).size` and use it for both the `>= 2` predicate and the printed count; change the WARN's count noun from `inbound refs` to `inbound citers` (the deduped count counts slugs — one word, not a message redesign; see Notes); `hotOnly: true` stays; update the adjacent comment to say the count is slug-deduped to match the `inbound` verb and `tightenPlan` Floor 2. - - **Tests (`war-memory.test.mjs`):** spawn-the-CLI refusal regressions for both `query` flags mirroring the `refusal (#1059)` family shape, per-case stderr token fidelity included (bare → `'true'`, non-numeric, zero, negative × both flags — new banner, do not edit the #1059 block's narrative); unchanged-path cases (flagless output equals an explicit-defaults invocation on the same fixture; valid values bind and cap; `--top-k 3.5` binds, D3); no-query-log-on-refusal assertion (this also proves `appendQueryLog`'s `mkdirSync` never ran — both live behind the same call); lint fixtures with an `archive/`-nested violation (CLI case red pre-fix; assert the printed hit path carries the `archive/` prefix) and a clean nested case; the existing wrapper meta-test family extended to the nested-violation fixture; archive hub-WARN dedupe regression driven by spawning `archive ` on a two-root fixture with stderr capture (cross-root-dupe citer ⇒ no WARN; two distinct citers ⇒ WARN counting 2; one dupe + one distinct citer ⇒ WARN counting 2, not 3), each case also asserting the archive operation itself succeeds around the WARN (exit 0, file moved, re-render completes). + - **ADR 0028 amendment (red-team 2026-07-27):** `docs/adr/0028-memory-store-integrity-tool-enforced.md` quotes the hub-WARN message verbatim (`inbound refs` noun, undeduped count). Append a dated amendment note recording that as of this fix the WARN prints a slug-deduped count with the `inbound citers` noun — append-only, never editing the historical quote (ADR convention: superseded, not rewritten). Per-medium gate (plan-1 precedent): the ADR asserts NEW-present (the amendment) only; OLD-absent runs on live surfaces (End state 8). + - **Tests (`war-memory.test.mjs`):** spawn-the-CLI refusal regressions for both `query` flags mirroring the `refusal (#1059)` family shape, per-case stderr token fidelity included (bare → `'true'`, non-numeric, zero, negative × both flags — new banner, do not edit the #1059 block's narrative); unchanged-path cases (flagless output equals an explicit-defaults invocation on the same fixture; valid values bind and cap; `--top-k 3.5` binds, D3); no-query-log-on-refusal assertion (this also proves `appendQueryLog`'s `mkdirSync` never ran — both live behind the same call); lint fixtures with an `archive/`-nested violation (CLI case red pre-fix; assert the printed hit path carries the `archive/` prefix) and a clean nested case; the existing wrapper meta-test family extended to the nested-violation fixture; archive hub-WARN dedupe regression driven by spawning `archive ` on a two-root fixture with stderr capture (cross-root-dupe citer ⇒ no WARN; two distinct citers ⇒ WARN counting 2; one dupe + one distinct citer ⇒ WARN counting 2, not 3), each case also asserting the archive operation itself succeeds around the WARN (exit 0, file moved, re-render completes). The EXISTING spawn-the-CLI hub-WARN test (title `archive: concept-hub WARN on ≥2 hot inbound refs…`) pins the message bytes and MUST be updated in-task (title noun + assertion regex → `inbound citers`, deduped count) — it is the forced-red surface for the noun flip; the `refusal (#1059)` narrative stays untouched. The explicit-defaults equality case is built from the imported constants (`--top-k ${DEFAULT_TOP_K} --budget ${DEFAULT_BUDGET}`), never hand-typed literals (the test file already imports `DEFAULT_TOP_K`). - **Retirement sweep (floor):** grep `war-memory.mjs` for `? Number(` on `argv` reads — expected survivors: exactly the three `typeof`-gated guarded arms (`--target`, `--top-k`, `--budget`), zero unguarded; then hand-scan the same scope (comments, test titles naming these flags, sibling verbs' flag reads) and list each straggler as a survey-derived correction. `spec.topK ?? DEFAULT` reads in `cmdQueriesBatch` are out of scope (constraint 6). - requiresTest: true - requiresPackaging: false @@ -52,7 +53,7 @@ Phase edge rationale: the release bump is its own trailing phase per the decompo - **Tighten lesson, failure-mode-3 item:** replace the "collapses to one argv token and the flag is dropped entirely" sentence pair with the fuse mechanism (zsh, lacking `SH_WORD_SPLIT`, does not word-split the unquoted `${TIGHTEN_TARGET:+…}` expansion → one fused argv word `--target 2000` → `parseArgv` keys `{"target 2000": true}` → `argv.target` stays `undefined`; observable end state coincides with never setting the variable). Keep the "same silent outcome" clause — it was the accurate part. - **Tighten lesson, closing paragraph:** rewrite "Not yet extended to siblings" to past tense recording that the extension landed in Task 1.1's `cmdQuery` fix. - **Sibling cmdquery lesson (D9):** stamp RESOLVED — description prefix + appended `## RESOLVED` section citing Task 1.1's fix and its tests **by construct name** (plan-slug/task + issue: `war-memory-cli-correctness/1.1, #1145`, the guarded `cmdQuery` resolution, the new refusal-test banner — never a commit SHA or line number; SHAs don't exist stably until land and line anchors rot across the merge queue), mirroring the tighten lesson's landed RESOLVED shape. Per the ratified stamp convention the body stays frozen otherwise: the `deliberately left open` H1 and the pre-fix code snippet remain (the appended section is what corrects the record). - - **Stale-wording sweep (floor):** grep the tighten lesson for `dropped` and `collapses` and handle every match — mechanism-position claims are defects; negated/coincides-with mentions in the corrected wording are exempt. Then hand-scan the same scope (frontmatter description, all three failure-mode items, "Why this matters", `## RESOLVED`, residual paragraphs, plus the sibling lesson's full body) for paraphrase stragglers describing the zsh failure as flag loss, listing each as a survey-derived correction. (Spec-time survey: the sibling lesson carries no zsh claim.) + - **Stale-wording sweep (floor):** grep the tighten lesson case-insensitively for `dropped` and `collapses` (`grep -in 'dropped\|collapses'`) and handle every match — anchored on the tokens, never on line position (a re-cased or re-positioned straggler must still hit) — mechanism-position claims are defects; negated/coincides-with mentions in the corrected wording are exempt. Then hand-scan the same scope (frontmatter description, all three failure-mode items, "Why this matters", `## RESOLVED`, residual paragraphs, plus the sibling lesson's full body) for paraphrase stragglers describing the zsh failure as flag loss, listing each as a survey-derived correction. (Spec-time survey: the sibling lesson carries no zsh claim.) - Run the redaction lint on both edited files (`node skills/_shared/war-memory.mjs lint ` per file) and confirm clean. - requiresTest: false - requiresPackaging: false @@ -72,8 +73,8 @@ Phase edge rationale: the release bump is its own trailing phase per the decompo ## Deferred validations (backstops) -- Live-tree recursive lint re-run at land: `node skills/_shared/war-memory.mjs lint docs/learnings/` exits 0 with the recursive walk in place · why deferred: the 113-archived-files-clean claim is time-of-spec; the archive can pick up a violation before land · earliest loud surface: Task 1.1's own merge gate (the gate-discovered `war-memory-lint.test.sh` lints the live `docs/learnings/`, recursion active) — a hit there routes as a fix round on Task 1.1 with Lead-adjudicated scope widening to redact the offending archive file inside Phase 1 (fail-closed, never waived in prose, ADR 0017; the archive file joins the task diff as a survey-derived correction) · runner: Task 1.1 gate → refiner/Lead at Phase 1 land → CI `memory-audit.yml` on the PR. -- Local-root recurrence copies of BOTH edited lessons still need the same corrections carried forward: the tighten lesson's local copy (same slug) carries the stale "silently dropping the flag" wording, and the cmdquery lesson's local copy (same slug, currently shadowing the repo row in the local MEMORY.md) carries no RESOLVED stamp · why deferred: the untracked local memory root is outside every worker's write scope (spec constraint 7 / §9) · runner: the next `/lessons-learned` pass / Gate-2 servitor cycle — which overwrites repo copies from local ones, so it must carry the fuse wording AND the RESOLVED stamp forward, never regress the repo copies corrected in Task 1.2. +- Live-tree recursive lint re-run at land: `node skills/_shared/war-memory.mjs lint docs/learnings/` exits 0 with the recursive walk in place · why deferred (NARROWED at red-team 2026-07-27 — the task gate already runs this check pre-merge and blocking, so End state 6 is HARD there, not deferred; only the window between that gate and the PR merge is uncovered): archive drift landing after Task 1.1's merge gate — a gate hit routes as a fix round on Task 1.1 with Lead-adjudicated scope widening to redact the offending archive file inside Phase 1 (fail-closed, never waived in prose, ADR 0017; the archive file joins the task diff as a survey-derived correction) · runner: Task 1.1 gate → refiner/Lead at Phase 1 land → CI `memory-audit.yml` on the PR. +- Local-root recurrence copies of BOTH edited lessons still need the same corrections carried forward: the tighten lesson's local copy (in the local `archive/` since the 2026-07-27 sweep) carries the stale flag-dropped wording, and the cmdquery lesson's local HOT copy carries no RESOLVED stamp — a later Gate-2 promotion of that stale local copy would overwrite-on-promote the Task 1.2-corrected repo copy · why deferred FROM THE WORKER only: the untracked local root is outside every worker's write scope (spec constraint 7 / §9) — but not outside the run's: · in-run runner (primary): this run's Phase 1 **servitor**, whose sole write path IS the local memory root; the recorded `resolved-lesson-stamp-freezes-body-so-it-can-contradict-the-new-description` convention ("How to apply" #2) sanctions mirroring a landed RESOLVED stamp onto the editable local copy · residual runner: the next `/lessons-learned` pass, for whatever the servitor does not reach (e.g. the archived local tighten copy, if the provenance hook denies archive-path writes) · **Lead duty at this run's Gate 2 (fail-closed):** exclude the tighten slug — and any slug whose repo copy lives under `docs/learnings/archive/` — from the promotion selection: promotion copies FLAT into `docs/learnings/` and would land the slug HOT, duplicating the archived copy and re-breaching the 17,000 B projection advisory. ## Notes / conscious deviations @@ -90,10 +91,10 @@ Phase edge rationale: the release bump is its own trailing phase per the decompo - Red-pre-fix claims (End states 1, 5, 8) and the grep + hand-survey results (End states 4, 9) are worker done-report evidence — SOFT at gate-audit per the recorded convention, accepted; no committed drift guard on the survey results (survey is a floor, by design). - The no-log-on-refusal assertion subsumes a dir-not-created assertion: `mkdirSync` lives inside `appendQueryLog`, so an absent log proves the mkdir never ran. - Issue closure: #1145/#1135/#1154/#1147 are survey-swept (mapped in the 2026-07-26 survey manifest) — `/aftermath` Class-4 closes them against this plan's merged PR; not a `/war` checkpoint duty. - - Campaign contention (computed, not assumed): the one sibling plan (`2026-07-26-dispatch-args-and-floor-coverage`) shares only the three release-slot files — by design; the campaign is serial stack-and-plow, each release phase resolving the next free patch at its own land-time tip, so no patch-number race. No overlap on `war-memory.mjs`/`.test.mjs` or the two lessons. No in-campaign writer moves files into `docs/learnings/archive/`, so the wrapper's no-arg meta-test (which lints the live tree on every suite run) cannot red an unrelated worker mid-campaign. + - Campaign contention (RE-computed at red-team, 2026-07-27): the campaign has THREE sibling plans (roadmap: 4 plans, strict order 1 → 2 → 3 → 4; plan 1 already merged to master). All siblings share the three release-slot files by design — serial landing means each release phase resolves the next free patch at its own land-time tip, no patch-number race. No sibling touches `war-memory.mjs`/`.test.mjs` or either lesson. Real overlap beyond the slots: plan 4 (`standing-doc-and-remedy-truth-sweep`) edits `README.md` prose and `skills/war/assets/version-slots.test.mjs` in its NON-release Phase 1 — the same README `## Status` slot this plan's Task 2.1 replaces and the same arbiter suite End state 11 names. Mitigation: the strict landing order puts plan 4 after this plan, so Task 2.1 lands first and plan 4 rebases onto it; if the order ever slips, Task 2.1's blurb must satisfy whatever presence lock plan 4 has added to `version-slots.test.mjs` at the tip. The 2026-07-27 memory sweep DID move 25 lessons into `docs/learnings/archive/` before this plan's execution (145 archived files total; live-tree recursive lint re-verified clean, 0 hits) — the no-mid-campaign-archive claim holds only from this red-team forward. - Spec's "#1154 observed live 2026-07-27" is a UTC-clock date (the lesson files' own `modified` stamps read 2026-07-27T03:5xZ = 2026-07-26 evening local) — not a typo; no spec surgery. - Rollback is roll-forward only: the version-slots monotonic floor (ratified) blocks a coherent downgrade; a guard proving too strict for an exotic downstream value is a follow-up patch release. Accepted. - - Doc sweep run (comment-leader-stripped, per the recorded sweep lesson): no standing surface (ADR 0015, lessons-learned SKILL/references, seeding.md, CI header, wrapper header, `war-memory.mjs` file-top banner) describes the lint walk depth or the archive as unlinted, and no instruction surface (`skills/`, `agents/` `.md`) threads `--top-k`/`--budget` for agents to replay. Zero stragglers. + - Doc sweep run (comment-leader-stripped, per the recorded sweep lesson): no standing surface (ADR 0015, lessons-learned SKILL/references, seeding.md, CI header, wrapper header, `war-memory.mjs` file-top banner) describes the lint walk depth or the archive as unlinted, and no instruction surface (`skills/`, `agents/` `.md`) threads `--top-k`/`--budget` for agents to replay. Zero stragglers on those two scopes. Red-team widening (2026-07-27): that sweep never covered the WARN message STRING — a repo grep for it found `docs/adr/0028-memory-store-integrity-tool-enforced.md` quoting the WARN verbatim (`inbound refs` noun included); handled in Task 1.1 via the dated append-only ADR amendment, per-medium gated (NEW-present on the ADR, OLD-absent on live `skills/` surfaces — End state 8). - `/red-team` runs inside `/war-campaign` step 3 before execution; expected report path `docs/red-team/2026-07-26-war-memory-cli-correctness.md` (convention-derived from the kept Source spec line). ## Open decisions From ebdcc84b52f43962da6c3614287e4bd1feb97bf8 Mon Sep 17 00:00:00 2001 From: RT Probe Date: Mon, 27 Jul 2026 16:47:46 -0700 Subject: [PATCH 3/9] =?UTF-8?q?docs(red-team):=20war-memory-cli-correctnes?= =?UTF-8?q?s=20CLEARED=20=E2=80=94=2012=20blocking=20findings=20patched,?= =?UTF-8?q?=20sandbox=20escape=20contained?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 0: 14 probes on-target (6 executed / 8 analyzed), 20 agents, opus/high. 8 gate blockers + 4 needsDecision patched into the plan at 4453863; round-1 re-verify confirms all 12 RESOLVED. 5 Minors absorbed by the same patches. Incident: the ff-topology probe built its synthetic fixture in the shared sandbox worktree (5 rogue commits + working-tree edits). Contained to the disposable sandbox — real repo clean, remote tips unchanged; taint audit shows every load-bearing finding anchored on unmutated surfaces; sandbox restored and escape guard re-run exit 0 before the verdict. Co-Authored-By: Claude Opus 5 --- .../2026-07-26-war-memory-cli-correctness.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/red-team/2026-07-26-war-memory-cli-correctness.md diff --git a/docs/red-team/2026-07-26-war-memory-cli-correctness.md b/docs/red-team/2026-07-26-war-memory-cli-correctness.md new file mode 100644 index 0000000..257d7b4 --- /dev/null +++ b/docs/red-team/2026-07-26-war-memory-cli-correctness.md @@ -0,0 +1,54 @@ +# Red-team report — war-memory CLI correctness & lint completeness + +- **Plan:** `docs/plans/2026-07-26-war-memory-cli-correctness.md` (patched in place; verified tip `4453863`) +- **Source spec:** `docs/specs/2026-07-26-war-memory-cli-correctness-design.md` +- **Run:** `wf_53228ce5-e83`, 2026-07-27, `artifactKind: impl-plan`, opus/high (config `agents.redteam`) +- **Rounds:** round 0 (14 probes, 20 agents) → plan patch `4453863` → round 1 (single-agent re-verify of all 12 blocking findings) + +## Verdict: **CLEARED** + +All 14 probes on-target, none dropped, none off-target. 12 blocking findings (8 gate blockers, 4 additional `needsDecision`) patched into the plan and re-verified RESOLVED at `4453863`. All 5 Minors were absorbed by the same patches. + +## Attack surface + +6 spine lenses + 8 bespoke probes (snippet-fidelity, baseline-repro, anchor-check, lesson-prose-source-fidelity, ff-topology, default-flip-old-absent, unguarded-new-mirror, backstop-legitimacy). Executed 6 / analyzed 8; round-0 statuses: 4 pass, 5 fail, 5 warn. + +**Baselines proved live** (baseline-repro, snippet-fidelity — both `pass`, completed pre-incident): all four quoted pre-fix snippets verbatim-present; all "red pre-fix" claims genuinely red today (invalid `--top-k`/`--budget` bind or default silently at exit 0; nested `archive/` violation lints clean; a refused-shape query still logs). + +## Findings and resolutions applied (all at `4453863`) + +| # | Probe | Finding | Resolution | +|---|-------|---------|------------| +| 1 | executable-proof | Task 1.2's `dropped`/`collapses` grep guard is case-sensitive — a re-cased straggler false-negates | Sweep + End state 9 now mandate `grep -in`, token-anchored | +| 2 | executable-proof / consistency-placeholders / default-flip-old-absent | ADR 0028 quotes the hub-WARN verbatim (`inbound refs`); no task owned it, no OLD-absent gate anywhere | ADR 0028 added to Task 1.1 Files; dated **append-only** amendment mandated; per-medium gate: NEW-present on the ADR, OLD-absent on live `skills/`/`hooks/`/`agents/`/`CONTEXT.md`/`README.md` (frozen artifacts + the historical ADR quote exempt) | +| 3 | consistency-placeholders | Existing spawn-CLI hub-WARN test pins the message bytes — the noun flip reds it, and no plan step updated it | Named as required in-task edit (title + regex); End state 8's stay-green clause narrowed to the `inboundCiters` unit tests | +| 4 | intent-vs-plan | End state 1's literal ("A supplied-but-invalid `--top-k`… exits 1") falsified by the `=`-attached residual the same plan freezes (D10) | Purpose + End state 1 narrowed to space-separated values; the `=`-attached form lifted into the Intent as a NAMED still-silent frozen residual. Widening `cmdQuery` to refuse `^(top-k|budget)=` keys was **declined** — D10 is a ratified spec freeze | +| 5 | anchor-check / claims-vs-reality | Contention note claims "the one sibling plan" — the roadmap has three siblings, and plan 4 overlaps `README.md` + `version-slots.test.mjs` in its non-release Phase 1 | Bullet rewritten: three siblings, real overlap named, landing-order mitigation recorded | +| 6 | backstop-legitimacy | Backstop 2's runner (Gate-2) is a propagation vector that would overwrite the Task 1.2-corrected repo copy and land the archived tighten slug hot | Rewritten: in-run runner = Phase 1 **servitor** (recorded `resolved-lesson-stamp…` convention); residual = next `/lessons-learned`; **Lead Gate-2 duty**: exclude the tighten slug (and any archived-repo-copy slug) from promotion | +| 7 | backstop-legitimacy | Backstop 2's justification ruled out only the worker, not the run — the servitor's sole write path IS the local root | Justification re-scoped ("FROM THE WORKER only… but not outside the run's") | +| 8 | backstop-legitimacy | Backstop 1 mis-filed as deferred — the task gate runs the recursive live-tree lint pre-merge and blocking | End state 6 re-filed HARD (gate-log banner = evidence); backstop narrowed to the post-gate window (runner: refiner/Lead at land → CI) | +| 9 | unguarded-new-mirror | The cmdQuery guard shape-copy ships with no coupling to its reference implementation | Coupling comments mandated both ways (each guard arm ↔ `cmdTightenPlan`'s `--target` block) | +| 10 | unguarded-new-mirror | End state 3 hand-types `10`/`4096` — an unguarded inline copy of two canonical exports | Explicit-defaults invocation built from the imported constants | +| 11 | executable-proof (Minor) + intent-vs-plan (Minor) | Doc-sweep "Zero stragglers" claim was scoped too narrowly to support itself | Sweep note widened to the WARN message string; ADR 0028 discovery recorded | +| 12 | — (decompose gate, pre-red-team) | Task 1.2's declared tighten-lesson path no longer resolves (archived 2026-07-27, PR #1167) | Re-anchored to `docs/learnings/archive/…` at `42d6912`; restore-to-hot declined (would re-breach the 17,000 B projection advisory) | + +## Adjudications + +| # | Delta | Ruling | Route | Moment | +|---|-------|--------|-------|--------| +| 1 | `=`-attached flag form (`--top-k=abc`) silently binds defaults even after Task 1.1 | Stays FROZEN (spec D10, ratified); End state 1/Purpose narrowed to space-separated form; residual is NAMED in the Intent, not waived in prose | in-artifact (`4453863`) | red-team round 0, 2026-07-27 | +| 2 | ADR 0028's verbatim WARN quote goes stale on the noun flip | Append-only dated amendment in Task 1.1 (never edit the historical quote); per-medium gate — NEW-present on the ADR, OLD-absent on live surfaces (plan-1 ADR 0037 precedent) | in-artifact (`4453863`) | red-team round 0, 2026-07-27 | +| 3 | Backstop 2's Gate-2 runner would regress Task 1.2's corrections and duplicate the archived tighten slug hot | In-run runner = Phase 1 servitor; **Lead MUST exclude the tighten slug (and any slug whose repo copy is archived) from this run's Gate-2 promotion selection** — fail-closed | in-artifact (`4453863`); Lead duty threaded to the run | red-team round 0, 2026-07-27 | +| 4 | Tighten lesson archived mid-campaign (PR #1167) broke Task 1.2's declared path | Edit in place at `docs/learnings/archive/…`; do NOT restore hot | in-artifact (`42d6912`) | campaign decompose gate, 2026-07-27 | +| 5 | Version literals | Phase 2 resolves the next free patch from the four slots at land tip (master shipped 0.14.64 via plan 1 ⇒ expect 0.14.65); plan/roadmap literals non-authoritative | standing (plan already states it) | red-team round 0, 2026-07-27 | + +## Incident — sandbox escape (contained, diagnosed, state restored) + +The post-run escape guard exited 1: the `ff-topology` probe built its synthetic fast-forward fixture **in the shared sandbox worktree** instead of a fresh temp repo — 5 commits on top of `42d6912` (incl. a deliberate `ROGUE2` detection-test mutation) plus working-tree edits. Self-confound gate run first (Lead actions since worktree creation were read-only ⇒ probe action). **Blast radius:** the disposable sandbox only — the real repo was clean (0 dirty files) and both remote tips unchanged. **Taint audit:** 5 probes (incl. both `pass` executed baselines) completed before the mutation window opened (16:25:51); every load-bearing round-0 finding rests on plan text or files the mutations never touched (mutations hit `war-memory.mjs`/`.test.mjs`/the cmdquery lesson; findings anchor on the plan, the roadmap, ADR 0028, and the tighten lesson). Sandbox `reset --hard` + `clean -fd` to `42d6912`; escape guard re-run **exit 0** before this verdict. `ff-topology` itself returned `pass` (0 findings): post-ff, `git diff --name-only ..HEAD` and per-task ranges remain computable — End states 3/7 evidence anchors hold. + +## Residual risk + +- The `=`-attached flag form remains silent by ratified decision (D10) — named in the Intent; closing it requires a `parseArgv` change this plan forbids. +- Worker done-report evidence (End states 1, 5, 8 red-pre-fix claims; End state 4/9 surveys) stays SOFT at gate-audit per the recorded convention — accepted in Notes. +- Recursive `readdirSync` does not descend directory symlinks; unreadable subdirectory skips fail-open (both accepted, one line each, in Notes). +- The plan's no-mid-campaign-archive claim now holds only from this red-team forward (the 2026-07-27 sweep predates it; live-tree lint re-verified clean over all 145 archived files, 0 hits). From 36e04d04793db89eae9fa16aa0360f988acb13ef Mon Sep 17 00:00:00 2001 From: RT Probe Date: Mon, 27 Jul 2026 17:06:53 -0700 Subject: [PATCH 4/9] fix(war-memory): guard query flags, recurse lint, dedupe hub WARN (#1170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three CLI correctness fixes, each bound at its owning verb's boundary: - cmdQuery (#1145): --top-k and --budget get the three-way argv-boundary resolution #1059 ratified for tighten-plan's --target — absent ⇒ default (byte-identical flagless path), a positive finite string binds, anything else refuses loud (exit 1, stderr naming the flag and the received token). The guards sit above walkCorpus, so a refusal writes nothing: no query-log line, no prompt block. No integer check (D3 — --top-k 3.5 binds, mirroring --target's predicate exactly). parseArgv is frozen, so the =-attached form (--top-k=abc) remains the named, deliberately silent residual (D10). Three comment-coupled sites, no shared helper. - cmdLint (#1135): the directory branch walks recursively, so the committed archive/ subtree is linted on every surface sharing the one invocation — CI (memory-audit.yml) and the gate wrapper (war-memory-lint.test.sh) both stay byte-unchanged. The catch { continue } fail-open is preserved. - cmdArchive (#1154): the advisory concept-hub WARN counts slug-deduped citers for both the >=2 predicate and the printed number, converging on the inbound verb and tightenPlan's Floor 2; the count noun becomes "inbound citers" (the old noun would be false on the deduped count). ADR 0028 quotes the WARN verbatim: a dated append-only amendment records the new counting and noun; the historical quote stands (superseded, not rewritten). Regressions (all red pre-fix, verified by reverting the source alone): eight spawn-the-CLI refusal cases with per-case token fidelity plus no-query-log-on- refusal, unchanged-path cases built from the imported DEFAULT_TOP_K/ DEFAULT_BUDGET constants, archive/-nested lint fixtures at both the CLI and wrapper surfaces, and three hub-WARN dedupe cases (twin ⇒ no WARN, two distinct ⇒ 2, twin + distinct ⇒ 2 not 3). The existing hub-WARN CLI test is updated in place — it pins the message bytes, so the noun flip reds it by design. Co-Authored-By: Claude Opus 5 --- ...28-memory-store-integrity-tool-enforced.md | 12 + skills/_shared/war-memory.mjs | 48 +++- skills/_shared/war-memory.test.mjs | 225 +++++++++++++++++- 3 files changed, 276 insertions(+), 9 deletions(-) diff --git a/docs/adr/0028-memory-store-integrity-tool-enforced.md b/docs/adr/0028-memory-store-integrity-tool-enforced.md index cb327d9..1cbb3b9 100644 --- a/docs/adr/0028-memory-store-integrity-tool-enforced.md +++ b/docs/adr/0028-memory-store-integrity-tool-enforced.md @@ -91,6 +91,18 @@ judgment — which hub to keep, which lesson to compress — stays human.** Conc reach — the sweep greps the current tree; the drift-guard/`/lessons-learned` posture is the standing backstop. +## Amendments + +- **2026-07-27 — the hub WARN counts slug-deduped `inbound citers`.** Decision item 2 above quotes the + WARN as first shipped: an undeduped record count with the noun `inbound refs`. A slug hot in *both* + roots is two records but one citer, so that count read `2` where the `inbound` verb (and `tightenPlan`'s + cross-root Floor 2) read `1`. As of + [`docs/plans/2026-07-26-war-memory-cli-correctness.md`](../plans/2026-07-26-war-memory-cli-correctness.md) + Task 1.1 (#1154), `cmdArchive` counts `new Set(hotInbound.map((r) => r.slug)).size` for both the ≥2 + predicate and the printed number, and the noun reads **`inbound citers`** — the deduped count counts + slugs, so the old noun would be false. Advisory, non-blocking, exit 0: unchanged. The quote in item 2 + stands as the historical record (superseded, never rewritten). + ## References - Design spec: diff --git a/skills/_shared/war-memory.mjs b/skills/_shared/war-memory.mjs index 2dd7d8a..7fcef96 100644 --- a/skills/_shared/war-memory.mjs +++ b/skills/_shared/war-memory.mjs @@ -700,8 +700,35 @@ function readQueryHits(localRoot) { function cmdQuery(argv) { const roots = resolveRoots(argv); const text = argv._.slice(1).join(' '); - const topK = argv['top-k'] ? Number(argv['top-k']) : DEFAULT_TOP_K; - const budget = argv.budget ? Number(argv.budget) : DEFAULT_BUDGET; + // Both flag guards are shape copies of cmdTightenPlan's ratified `--target` block (#1059) — + // see its rationale comment; no shared helper (`parseArgv` is frozen). They sit ABOVE the + // walkCorpus call, so a refusal writes nothing: no query-log line, no prompt block (#1145). + let topK = DEFAULT_TOP_K; // flag absent ⇒ the default (unchanged path) + if (argv['top-k'] !== undefined) { + // ratified reference implementation: cmdTightenPlan's `--target` three-way resolution + const t = typeof argv['top-k'] === 'string' ? Number(argv['top-k']) : NaN; + if (Number.isFinite(t) && t > 0) { + topK = t; + } else { + process.stderr.write( + `war-memory query: --top-k requires a positive count (got '${argv['top-k']}')\n` + ); + process.exit(1); + } + } + let budget = DEFAULT_BUDGET; // flag absent ⇒ the default (unchanged path) + if (argv.budget !== undefined) { + // ratified reference implementation: cmdTightenPlan's `--target` three-way resolution + const b = typeof argv.budget === 'string' ? Number(argv.budget) : NaN; + if (Number.isFinite(b) && b > 0) { + budget = b; + } else { + process.stderr.write( + `war-memory query: --budget requires a positive byte count (got '${argv.budget}')\n` + ); + process.exit(1); + } + } const seat = argv.seat || null; const records = walkCorpus(roots); const db = buildIndex(records); @@ -783,7 +810,10 @@ function cmdLint(argv) { try { const st = fs.statSync(t); if (st.isDirectory()) { - files = fs.readdirSync(t).filter((f) => f.endsWith('.md')).map((f) => path.join(t, f)); + // Recursive (#1135): the committed `archive/` subtree is prose too, and every lint + // surface (CI, the gate wrapper, a bare interactive run) inherits the walk from here. + // `path.join` is correct on the subdir-prefixed relative paths recursion returns. + files = fs.readdirSync(t, { recursive: true }).filter((f) => f.endsWith('.md')).map((f) => path.join(t, f)); } else { files = [t]; } @@ -837,12 +867,14 @@ function cmdArchive(argv) { continue; } // Advisory concept-hub WARN: archiving is link-safe (cold links still resolve), but a - // hub with ≥2 hot inbound refs loses its hot index row. Non-blocking — the keep-or-stub - // call stays human. Counted from the pre-move snapshot. + // hub with ≥2 hot inbound citers loses its hot index row. Non-blocking — the keep-or-stub + // call stays human. Counted from the pre-move snapshot, SLUG-DEDUPED (#1154) so a + // cross-root twin counts once — matching the `inbound` verb and tightenPlan's Floor 2. const hotInbound = inboundCiters(records, slug, { hotOnly: true }); - if (hotInbound.length >= 2) { + const hubCiters = new Set(hotInbound.map((r) => r.slug)).size; + if (hubCiters >= 2) { process.stderr.write( - `WARN: archiving concept hub '${slug}' (${hotInbound.length} inbound refs) — ` + + `WARN: archiving concept hub '${slug}' (${hubCiters} inbound citers) — ` + `its index row disappears; consider keep-compress stub\n` ); } @@ -957,6 +989,8 @@ function cmdTightenPlan(argv) { // flag refuse: `Number(true) === 1` would otherwise pass `isFinite && > 0` and silently // pre-select the entire eligible list at `target: 1`. Refusal is inline (`requireLocal`'s // shape) — `main()` has no catch, so a throw would surface as a stack trace, not a diagnostic. + // This block is the RATIFIED reference implementation: cmdQuery copies its shape twice + // (`--top-k`, `--budget`, #1145). Three comment-coupled sites, deliberately no shared helper. let target = WARN_BYTES; // flag absent ⇒ the advisory default (unchanged path) if (argv.target !== undefined) { const t = typeof argv.target === 'string' ? Number(argv.target) : NaN; diff --git a/skills/_shared/war-memory.test.mjs b/skills/_shared/war-memory.test.mjs index 8d0cf8b..67e171f 100644 --- a/skills/_shared/war-memory.test.mjs +++ b/skills/_shared/war-memory.test.mjs @@ -35,6 +35,7 @@ import { TIGHTEN_YOUNG_DAYS, TIGHTEN_SLACK_BYTES, DEFAULT_TOP_K, + DEFAULT_BUDGET, } from './war-memory.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); @@ -592,14 +593,14 @@ test('archive : explicit-slug path is unchanged by the --candidates flip ( rmSync(local, { recursive: true, force: true }); }); -test('archive: concept-hub WARN on ≥2 hot inbound refs, still exits 0; <2 stays silent (criterion 4)', () => { +test('archive: concept-hub WARN on ≥2 hot inbound citers, still exits 0; <2 stays silent (criterion 4)', () => { const local = tmpDir(); lessonFile(local, 'hub', { description: 'hub' }); lessonFile(local, 'ref-a', { description: 'a', body: 'per [[hub]]' }); lessonFile(local, 'ref-b', { description: 'b', body: 'see [[hub]]' }); const r = spawnSync('node', [CLI, 'archive', 'hub', '--local', local], { encoding: 'utf8' }); assert.equal(r.status, 0, r.stderr); // advisory only — never blocks - assert.match(r.stderr, /WARN: archiving concept hub 'hub' \(2 inbound refs\)/); + assert.match(r.stderr, /WARN: archiving concept hub 'hub' \(2 inbound citers\)/); assert.ok(existsSync(join(local, 'archive', 'hub.md')), 'archive still happened'); // one inbound ref → below the ≥2 threshold → no hub WARN. @@ -613,6 +614,74 @@ test('archive: concept-hub WARN on ≥2 hot inbound refs, still exits 0; <2 stay rmSync(local2, { recursive: true, force: true }); }); +// ============================================================================ +// (Task 1.1 / #1154) The hub WARN counts CITERS, slug-deduped — one Set drives +// both the ≥2 predicate and the printed number, converging cmdArchive onto the +// `inbound` verb and tightenPlan's cross-root Floor 2. Red pre-fix: the raw +// record count made a slug hot in BOTH roots read as two, so a single-citer +// archive printed a "concept hub (2 …)" WARN that the `inbound` verb flatly +// contradicted (the recorded ground-truth-the-hub trap). The retired noun is +// deliberately NOT spelled here — quoting it would self-match its own sweep. +// ============================================================================ + +// Two-root hub fixture: `hub` is hot in the LOCAL root (so the archive moves the +// local copy); each [slug, roots] entry writes one citing lesson per named root — +// listing a slug in both roots makes it a cross-root twin, ONE citer. +function hubFixture(citers) { + const local = tmpDir('war-hub-local-'); + const repo = tmpDir('war-hub-repo-'); + lessonFile(local, 'hub', { description: 'a concept hub' }); + for (const [slug, roots] of citers) { + for (const which of roots) { + lessonFile(which === 'repo' ? repo : local, slug, { + description: `${slug} summary`, + meta: { type: 'project' }, + body: 'builds on [[hub]] heavily', + }); + } + } + return { local, repo }; +} + +function archiveHub({ local, repo }) { + return spawnSync('node', [CLI, 'archive', 'hub', '--local', local, '--repo', repo], { encoding: 'utf8' }); +} + +// The WARN is advisory: every case must still archive and re-render around it. +function assertHubArchived(r, local) { + assert.equal(r.status, 0, `advisory WARN must never block, got ${r.status}\n${r.stderr}`); + assert.ok(existsSync(join(local, 'archive', 'hub.md')), 'hub file moved into archive/'); + assert.ok(!existsSync(join(local, 'hub.md')), 'and left the hot root'); + assert.match(r.stdout, /rendered .*MEMORY\.md/); // the trailing re-render completed +} + +test('hub WARN (#1154): a cross-root twin citer counts ONCE → below the ≥2 line, no WARN', () => { + const { local, repo } = hubFixture([['twin-citer', ['local', 'repo']]]); + const r = archiveHub({ local, repo }); + assertHubArchived(r, local); + assert.doesNotMatch(r.stderr, /concept hub/, 'two records, one citer — pre-fix this printed a false 2'); + for (const d of [local, repo]) rmSync(d, { recursive: true, force: true }); +}); + +test('hub WARN (#1154): two DISTINCT hot citers (one per root) → WARN counting 2 inbound citers', () => { + const { local, repo } = hubFixture([['citer-a', ['local']], ['citer-b', ['repo']]]); + const r = archiveHub({ local, repo }); + assertHubArchived(r, local); + assert.match(r.stderr, /WARN: archiving concept hub 'hub' \(2 inbound citers\)/); + for (const d of [local, repo]) rmSync(d, { recursive: true, force: true }); +}); + +test('hub WARN (#1154): one twin + one distinct citer → 3 records, WARN counting 2 (not 3)', () => { + // The mixed case pins predicate and printed count together: dedupe must not simply + // suppress the WARN, and the number must be the citer count, not the record count. + const { local, repo } = hubFixture([['twin-citer', ['local', 'repo']], ['citer-b', ['local']]]); + const r = archiveHub({ local, repo }); + assertHubArchived(r, local); + assert.match(r.stderr, /WARN: archiving concept hub 'hub' \(2 inbound citers\)/); + assert.doesNotMatch(r.stderr, /\(3 inbound/, 'the raw record count (3) must never reach the message'); + for (const d of [local, repo]) rmSync(d, { recursive: true, force: true }); +}); + // ============================================================================ // (8) Consolidate — near-dupe pair flagged, no auto-merge. // ============================================================================ @@ -1223,6 +1292,100 @@ test('unchanged path (#1059): the flagless invocation keeps the advisory default rmSync(local, { recursive: true, force: true }); }); +// ============================================================================ +// (Task 1.1 / #1145) `query`'s `--top-k` and `--budget` carry the SAME three-way +// argv-boundary resolution #1059 ratified for `--target` — the two flags that fix +// deliberately left open. Red pre-fix, every case exiting 0 with no diagnostic: +// a bare `--top-k` took `Number(true) === 1` (a one-lesson block), while `abc`/`0`/ +// `-500` reached `records.slice(0, NaN | 0 | -500)` and rendered an EMPTY block — +// a silently lesson-less seat prefetch. `--budget abc` (NaN) silently uncapped the +// block instead. `parseArgv` is byte-untouched, so the flagless path is unchanged. +// ============================================================================ + +// Four hot facts sharing a query token — a real corpus, so any run reaching the +// printer emits a block: an empty stdout can only come from the guard firing first. +function queryFlagFixture() { + const local = tmpDir('war-query-flags-'); + for (const s of ['alpha-fact', 'beta-fact', 'gamma-fact', 'delta-fact']) { + lessonFile(local, s, { description: `${s} ordering token summary`, meta: { provenance: 'code-verified' } }); + } + return local; +} + +const lessonLines = (stdout) => stdout.split('\n').filter((l) => l.startsWith('- [')); + +for (const [flag, noun] of [['top-k', 'positive count'], ['budget', 'positive byte count']]) { + for (const [label, valueArgs, token] of [ + ['bare (parseArgv maps a valueless flag to boolean true)', [], 'true'], + ['abc (non-numeric ⇒ NaN)', ['abc'], 'abc'], + ['0 (zero is not positive)', ['0'], '0'], + ['-500 (negative)', ['-500'], '-500'], + ]) { + test(`refusal (#1145): --${flag} ${label} → exit 1, empty stdout, stderr names the flag + the token`, () => { + const local = queryFlagFixture(); + const r = spawnSync( + 'node', + [CLI, 'query', 'ordering token', '--local', local, `--${flag}`, ...valueArgs], + { encoding: 'utf8' } + ); + assert.equal(r.status, 1, `expected the guard's exit 1, got ${r.status}\n${r.stdout}\n${r.stderr}`); + // Empty, not merely short: a refusal must not print a partial prompt block. + assert.equal(r.stdout, '', `expected no block bytes on stdout, got: ${r.stdout}`); + assert.match(r.stderr, /war-memory query:/); // the verb, per the requireLocal diagnostic shape + assert.ok(r.stderr.includes(`--${flag}`), `stderr must name the flag, got: ${r.stderr}`); + assert.ok(r.stderr.includes(noun), `stderr must name what the flag wants, got: ${r.stderr}`); + assert.ok(r.stderr.includes(`'${token}'`), `stderr must render the received token, got: ${r.stderr}`); + // The guard sits ABOVE walkCorpus, so a refusal writes nothing — and since the + // log dir's mkdirSync lives inside appendQueryLog, an absent log proves it too. + assert.equal( + existsSync(join(local, 'war-memory-queries.jsonl')), false, + 'a refused query must append no query-log line' + ); + rmSync(local, { recursive: true, force: true }); + }); + } +} + +test('unchanged path (#1145): flagless output is byte-identical to an explicit-defaults invocation', () => { + const local = queryFlagFixture(); + const args = [CLI, 'query', 'ordering token', '--local', local]; + const flagless = spawnSync('node', args, { encoding: 'utf8' }); + // built from the imported constants — a hand-typed literal would pass a changed default + const explicit = spawnSync( + 'node', + [...args, '--top-k', String(DEFAULT_TOP_K), '--budget', String(DEFAULT_BUDGET)], + { encoding: 'utf8' } + ); + assert.equal(flagless.status, 0, flagless.stderr); + assert.equal(explicit.status, 0, explicit.stderr); + assert.ok(lessonLines(flagless.stdout).length > 0, 'fixture must render a block for this to mean anything'); + assert.equal(flagless.stdout, explicit.stdout); + rmSync(local, { recursive: true, force: true }); +}); + +test('unchanged path (#1145): a valid --top-k caps the block; a fractional value BINDS (D3 — no integer check)', () => { + const local = queryFlagFixture(); // four matching lessons + const three = spawnSync('node', [CLI, 'query', 'ordering token', '--local', local, '--top-k', '3'], { encoding: 'utf8' }); + assert.equal(three.status, 0, three.stderr); + assert.equal(lessonLines(three.stdout).length, 3); + // 3.5 mirrors --target's predicate exactly (finite and > 0): it binds, and slice truncates. + const frac = spawnSync('node', [CLI, 'query', 'ordering token', '--local', local, '--top-k', '3.5'], { encoding: 'utf8' }); + assert.equal(frac.status, 0, `a fractional --top-k must NOT refuse\n${frac.stderr}`); + assert.equal(lessonLines(frac.stdout).length, 3); + rmSync(local, { recursive: true, force: true }); +}); + +test('unchanged path (#1145): a valid --budget still caps the rendered block', () => { + const local = queryFlagFixture(); + const wide = spawnSync('node', [CLI, 'query', 'ordering token', '--local', local], { encoding: 'utf8' }); + const tight = spawnSync('node', [CLI, 'query', 'ordering token', '--local', local, '--budget', '90'], { encoding: 'utf8' }); + assert.equal(tight.status, 0, tight.stderr); + const [w, t] = [lessonLines(wide.stdout).length, lessonLines(tight.stdout).length]; + assert.ok(t >= 1, 'the byte budget always emits at least one lesson'); + assert.ok(t < w, `a tight budget must cap the block (${t} vs ${w})`); + rmSync(local, { recursive: true, force: true }); +}); + // ============================================================================ // (Task 1.1 / #992) tightenPlan's returned `verdict` is the STRICTER of the advisory // projection read and the effective `--target`. buildProjection is byte-untouched, so @@ -1318,6 +1481,45 @@ test('safe-swap cross-check: buildProjection 2-col render PASSes verify; strippi rmSync(repo, { recursive: true, force: true }); }); +// ============================================================================ +// (Task 1.1 / #1135) `lint ` walks the target RECURSIVELY, so the committed +// `archive/` subtree is covered on every surface that shares the one invocation +// (CI, the gate wrapper, a bare interactive run) with zero edits to either. +// Red pre-fix: the one-level readdirSync never opened archive/, so a nested +// violation printed "lint: clean" at exit 0 — a fail-closed gate reporting green. +// Fixture hygiene: violating shapes are ASSEMBLED AT RUNTIME under this test's own +// mkdtemp dir, never committed .md, so they cannot trip the repo's own lint. +// ============================================================================ + +test('lint : an archive/-nested violation exits 1, hit path carrying the archive/ prefix (#1135)', () => { + const dir = tmpDir('war-lint-nested-red-'); + lessonFile(dir, 'clean-hot', { description: 'clean prose', body: 'The renderer refuses above either hard axis.' }); + lessonFile(join(dir, 'archive'), 'archived-violator', { + description: 'an archived fixture lesson', + body: `pasted ${'ghp_' + 'd'.repeat(36)} into the note`, // assembled here, never committed + }); + const r = spawnSync('node', [CLI, 'lint', dir], { encoding: 'utf8' }); + assert.equal(r.status, 1, `expected the fail-closed exit 1, got ${r.status}\n${r.stdout}\n${r.stderr}`); + assert.match(r.stdout, /github-token/); // names the pattern class + // ...and the FILE by its subdir-prefixed path: proof the recursion found it, not a flattened walk + assert.ok( + r.stdout.includes(join(dir, 'archive', 'archived-violator.md')), + `hit path must carry the archive/ prefix, got: ${r.stdout}` + ); + assert.doesNotMatch(r.stdout, /lint: clean/); + rmSync(dir, { recursive: true, force: true }); +}); + +test('lint : a clean nested tree still exits 0 (recursion invents no hit) (#1135)', () => { + const dir = tmpDir('war-lint-nested-green-'); + lessonFile(dir, 'clean-hot', { description: 'clean prose', body: 'Archive is a file move, never a deletion.' }); + lessonFile(join(dir, 'archive'), 'clean-cold', { description: 'clean cold prose', body: 'Cold links still resolve.' }); + const r = spawnSync('node', [CLI, 'lint', dir], { encoding: 'utf8' }); + assert.equal(r.status, 0, `expected exit 0, got ${r.status}\n${r.stdout}\n${r.stderr}`); + assert.match(r.stdout, /lint: clean/); + rmSync(dir, { recursive: true, force: true }); +}); + // ============================================================================ // (#1081) The gate-DISCOVERED redaction-lint wrapper, war-memory-lint.test.sh. // In a real gate the wrapper only ever runs the live docs/learnings/ (green), so @@ -1343,6 +1545,25 @@ test('lint wrapper: violating fixture → exit 1, offending file + pattern class rmSync(dir, { recursive: true, force: true }); }); +test('lint wrapper: archive/-NESTED violating fixture → exit 1 propagated, subdir path on stdout (#1135)', () => { + // The wrapper is byte-unchanged by #1135 — it inherits the recursive walk through the + // one shared `lint` invocation. This case is what proves the inheritance at the surface + // a real gate actually runs (CI's memory-audit.yml rides the same call). + const dir = tmpDir('war-lint-wrapper-nested-'); + const homeShaped = ['', 'Users', 'fixture-person', 'notes', 'draft.md'].join('/'); // assembled here, never committed + lessonFile(dir, 'clean-hot', { description: 'fixture lesson', body: 'no findings at the top level' }); + lessonFile(join(dir, 'archive'), 'nested-violator', { + description: 'archived fixture lesson', + body: `captured at ${homeShaped} during the run`, + }); + const r = spawnSync('bash', [LINT_WRAPPER, dir], { encoding: 'utf8' }); + assert.equal(r.status, 1, `expected the CLI's fail-closed exit 1, got ${r.status}\n${r.stdout}\n${r.stderr}`); + assert.match(r.stdout, /nested-violator\.md/); // names the offending FILE + assert.match(r.stdout, /home-path/); // ...and the pattern class + assert.ok(r.stdout.includes(join('archive', 'nested-violator.md')), 'hit path carries the archive/ prefix'); + rmSync(dir, { recursive: true, force: true }); +}); + test('lint wrapper: clean fixture → exit 0 and "lint: clean" on stdout', () => { const dir = tmpDir('war-lint-green-'); lessonFile(dir, 'clean-lesson', { description: 'clean prose', body: 'The renderer refuses above either hard axis.' }); From 42316cdfd464d6fb3ada782d05fbdedd50d0b880 Mon Sep 17 00:00:00 2001 From: RT Probe Date: Mon, 27 Jul 2026 17:31:56 -0700 Subject: [PATCH 5/9] =?UTF-8?q?docs(learnings):=20lesson=20prose=20truth-u?= =?UTF-8?q?p=20=E2=80=94=20fuse=20mechanism=20+=20RESOLVED=20stamp=20(#117?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two committed-lesson corrections, sourced from the red-team report (docs/red-team/2026-07-24-memory-tooling-hardening.md, Minors, "Purpose, zsh threading") and spec §4 of docs/specs/2026-07-26-war-memory-cli-correctness-design.md — not re-derived: - tighten-target-flag-...-degradation-paths.md (archive/, moved there by the 2026-07-27 RESOLVED sweep): failure-mode-3 now states the true fuse mechanism — zsh, lacking SH_WORD_SPLIT, does not word-split the unquoted ${TIGHTEN_TARGET:+...} expansion, so the whole replacement fuses into one argv word (`--target 2000`), which parseArgv keys as `{"target 2000": true}`; argv.target itself stays undefined. The "same silent outcome as never setting $TIGHTEN_TARGET" clause is kept verbatim (it was the accurate part). A case-insensitive grep for dropped/collapses now finds zero hits. The closing "Not yet extended to siblings" paragraph is rewritten past-tense as "Extended to siblings (war-memory-cli-correctness/1.1, #1145)", citing Task 1.1's landed cmdQuery guard by construct name. - cmdquery-topk-budget-share-tighten-targets-pre-fix-truthy-ternary-shape.md (D9): description RESOLVED-prefixed; appended `## RESOLVED` section names Task 1.1's guarded cmdQuery resolution and its `refusal (#1145): -- ...` test banner in war-memory.test.mjs. Per the ratified stamp convention the "deliberately left open" H1 and pre-fix code snippet stay frozen — the appended section is what corrects the record. Hand-scan survey (both files, full body): zero additional paraphrase stragglers beyond failure-mode-3 — matches the spec's own spec-time survey result (the sibling lesson carries no zsh claim). Redaction lint clean on both edited files. Co-Authored-By: Claude Sonnet 5 --- ...ee-independent-silent-degradation-paths.md | 20 +++++++++-------- ...en-targets-pre-fix-truthy-ternary-shape.md | 22 ++++++++++++++++++- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/docs/learnings/archive/tighten-target-flag-has-three-independent-silent-degradation-paths.md b/docs/learnings/archive/tighten-target-flag-has-three-independent-silent-degradation-paths.md index 66eacb9..12a9903 100644 --- a/docs/learnings/archive/tighten-target-flag-has-three-independent-silent-degradation-paths.md +++ b/docs/learnings/archive/tighten-target-flag-has-three-independent-silent-degradation-paths.md @@ -64,8 +64,10 @@ none of which raise a diagnostic: 3. **Shell-dialect word-splitting**, at the doc layer: `skills/lessons-learned/SKILL.md`'s step-1 fence threads the flag as `${TIGHTEN_TARGET:+--target "$TIGHTEN_TARGET"}` inside a ` ```bash ` block. Bash splits the `:+` replacement text into two argv words as expected; **zsh does not** - (no `SH_WORD_SPLIT` by default), so under zsh the same line collapses to one argv token and the - flag is dropped entirely — same silent outcome as never setting `$TIGHTEN_TARGET`. + — lacking `SH_WORD_SPLIT` by default, it does not word-split the unquoted expansion, so the + whole replacement fuses into one argv word (`--target 2000` arrives as a single token), which + `parseArgv` keys as `{"target 2000": true}`; `argv.target` itself stays `undefined` — same + silent outcome as never setting `$TIGHTEN_TARGET`. **Why this matters:** all three degrade toward one of two silent states (default 17,000 B advisory, or the "verdict always warn" pre-select) with zero diagnostic — an operator who typos `--target` @@ -126,12 +128,12 @@ Method explicitly froze as a shared blast-radius fence — not reopened by this it at `cmdTightenPlan`'s own boundary (e.g. also refuse when any argv key starts with `target=`), never by widening `parseArgv`. -**Not yet extended to siblings:** `cmdQuery`'s `--top-k`/`--budget` resolve via the identical -pre-fix truthy-ternary shape (`argv['top-k'] ? Number(...) : DEFAULT_TOP_K`, same for `budget`) at -`skills/_shared/war-memory.mjs` lines 703-704 — this task's Method deliberately bound the fix to -`cmdTightenPlan`'s boundary only ("bind each fix at its own boundary, never a shared one") and did -not extend it to `cmdQuery`. See [[cmdquery-topk-budget-share-tighten-targets-pre-fix-truthy-ternary-shape]] -for the standalone gotcha and its failure-direction difference (NaN `--top-k` empties the seat -prefetch instead of over-selecting). +**Extended to siblings (war-memory-cli-correctness/1.1, #1145):** `cmdQuery`'s `--top-k`/`--budget` +now carry the same three-way `typeof`-gated resolution described above for `--target` — a shape +copy at `cmdQuery`'s own boundary, not a shared helper (`parseArgv` stays frozen). See +[[cmdquery-topk-budget-share-tighten-targets-pre-fix-truthy-ternary-shape]] for the RESOLVED +sibling gotcha and the failure-direction difference that made it worth its own lesson (a bare/NaN +`--top-k` had silently emptied the seat prefetch instead of over-selecting, unlike `--target`'s +over-select failure mode). > archived 2026-07-27: resolved — moved to archive diff --git a/docs/learnings/cmdquery-topk-budget-share-tighten-targets-pre-fix-truthy-ternary-shape.md b/docs/learnings/cmdquery-topk-budget-share-tighten-targets-pre-fix-truthy-ternary-shape.md index 53b9040..d20af34 100644 --- a/docs/learnings/cmdquery-topk-budget-share-tighten-targets-pre-fix-truthy-ternary-shape.md +++ b/docs/learnings/cmdquery-topk-budget-share-tighten-targets-pre-fix-truthy-ternary-shape.md @@ -1,6 +1,6 @@ --- name: cmdquery-topk-budget-share-tighten-targets-pre-fix-truthy-ternary-shape -description: "cmdQuery's --top-k/--budget resolve via argv['top-k'] ? Number(...) : DEFAULT — the exact truthy-ternary silent-degradation shape #1059 closed for tighten-plan's --target, left open here on purpose (fix bound to cmdTightenPlan's boundary only); a bare/NaN --top-k silently empties the seat memory-prefetch block instead of over-selecting" +description: "RESOLVED (war-memory-cli-correctness/1.1, #1145): cmdQuery's --top-k/--budget now carry the same three-way typeof-gated resolution #1059 ratified for tighten-plan's --target, closing the truthy-ternary shape (argv['top-k'] ? Number(...) : DEFAULT) this lesson flagged as deliberately left open; a bare/NaN --top-k no longer silently empties the seat memory-prefetch block, it refuses loud instead" metadata: node_type: memory type: project @@ -69,3 +69,23 @@ three-way `typeof argv[x] === 'string'` resolution (undefined → default; strin Related: [[tighten-target-flag-has-three-independent-silent-degradation-paths]] (the sibling fix this gotcha was found alongside, in the same audit pass). + +## RESOLVED (war-memory-cli-correctness/1.1, #1145) + +`cmdQuery` now carries the same three-way `typeof`-gated resolution ratified for +`cmdTightenPlan`'s `--target` — a shape copy at `cmdQuery`'s own boundary, not a shared helper +(`parseArgv` stays frozen): flag absent → the unchanged default; `typeof === 'string'` and +`Number.isFinite(t) && t > 0` → the value binds; anything else (bare flag, non-numeric, zero, +negative) → `war-memory query: --top-k requires a positive count (got '')` (resp. +`--budget requires a positive byte count`) on stderr, exit 1 — placed above the `walkCorpus` call, +so a refusal appends no query-log line and prints no block. This closes exactly the gap this +lesson flagged: a bare `--top-k` no longer takes `Number(true) === 1` into a one-lesson block, and +a `NaN` `--top-k`/`--budget` no longer reaches `selectForBudget`'s `slice` to silently empty it. + +Covered by the `refusal (#1145): --