[CLAUDE] Workflow: adopt presence-not-age selector guardrail + rec-3 hmw.js STOP-HARD (S115)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m23s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m23s
Adopt AI_INFRA 2026-07-13 broadcast ab6c387e (presence-not-age selector, type=update) + directed 6c32df89 rec-3, via /fable-clone 5-lane reviewer ensemble (wf_b621aac4-f0b) -> spec -> /fable-real deep-pass (PASS-WITH-FIXES, M1+M2 applied) -> HMW execute (em-main solo; governance single-writer D9). D2 (hmw.js): unknown-role fail-soft-WARN -> up-front STOP-HARD throw (before parallel; preserves null/'' role-less inherit-lead path). node --check + stub 7/7. Doc-sync 5 live lines (ultra-on/README/harness-11-engine/runbook). D3 (memory-archive-gate.ps1): value_protect advisory-flag -> pre-selection HARD-SKIP (value-primary; heading-only spans; non-contiguous byte accum) + value-floor WARN. Fault-inject ALL PASS + real regression A7 242/242. DRY-RUN. D1 (reinject-ledger.md): presence re-verify stamp + 3 honest-notes + BUILD-GAP. Re-verify: reinject (i) + MFE age-band (iii) already COMPLIANT; only the archive-gate age-trace needed hardening. D3 = defense-in-depth codify, NOT leak-closure (9-token grep unchanged; em-main value-scan stays the guarantee). adap-report + email AI_INFRA (8b9dc5165d5a); inbox STAGE-2 processed. No new User-Mark (codify-only). #53 garble x1 (lane-4) recovered from disk, 0 loss. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -36,7 +36,7 @@
|
||||
"keep_floor_entries": 5,
|
||||
"strike_threshold": 2,
|
||||
"value_protect": {
|
||||
"_note": "Harness-15 B(b) value-gated archival (S81, 2026-06-20): keep_floor_entries protects NEWEST-n (recency/age axis); value_protect protects HIGH-VALUE entries regardless of age (value axis, orthogonal). Recurring-bug / anti-pattern / gotcha / root-cause entries STAY in L1-hot even when old -- archival cuts LOW-VALUE, NOT FIFO-by-date. This is mark RC-...10-29-11 (time/age=false-proxy) applied to the memory layer. Mechanism = CONVENTION (em-main judgement when condensing L1->L2); the patterns below are an advisory grep-hint for the DRY-RUN planner to FLAG protected entries, NOT an enforced auto-exclude (no overclaim: archive-gate stays DRY-RUN, em-main decides).",
|
||||
"_note": "Harness-15 B(b) value-gated archival (S81, 2026-06-20): keep_floor_entries protects NEWEST-n (recency/age axis); value_protect protects HIGH-VALUE entries regardless of age (value axis, orthogonal). Recurring-bug / anti-pattern / gotcha / root-cause entries STAY in L1-hot even when old -- archival cuts LOW-VALUE, NOT FIFO-by-date. This is mark RC-...10-29-11 (time/age=false-proxy) applied to the memory layer. Mechanism (D3 REC-3 2026-07-13): the planner now HARD-SKIPS value-protected LOGICAL entries (heading-only spans) from the drain-candidate pool BEFORE the size-drain, accumulating INDIVIDUAL non-contiguous low-value bytes -- value is PRIMARY, position is only a within-low-value tiebreak; a value-floor WARN fires when every drainable entry is protected. So the printed PROPOSAL is value-gated by construction, NOT FIFO-by-date (was: advisory post-hoc flag on an oldest-first prefix-cut). HONEST (no overclaim): the patterns are a 9-token grep = LOWER-BOUND signal -- paraphrase/spine entries lacking a literal token still leak, so em-main MUST value-scan the WHOLE proposed set; the hard-skip is defense-in-depth codify, NOT leak-closure. Archive-gate stays DRY-RUN (em-main = final decision).",
|
||||
"patterns": ["gotcha #", "anti-pattern", "recurring", "lost-update", "race", "bai hoc", "lesson", "guard", "root-cause", "silent-fail"]
|
||||
}
|
||||
},
|
||||
|
||||
@ -74,6 +74,7 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod
|
||||
|
||||
## 📅 Recent activity (compressed — full verbatim → `archive/2026-06.md` + `archive/2026-07.md` via `archive/_INDEX.md`)
|
||||
|
||||
- **S115 (2026-07-13) `/fable-real` spec-review presence-not-age adopt (D1 docs + D2 hmw.js STOP-HARD + D3 archive-gate) — PASS-WITH-FIXES (0C/2M/5m):** sub = `runs/2026-07-13-presence-not-age-adopt/spec-review-fable-real.md`. **M1: acceptance-grep can't detect its OWN deliverable's miss** — D2 crit-4 regex `fail-soft.*default subagent\|degrade về default subagent` catches only 1/3 doc-lines (misses ultra-on:21 "cảnh báo" + README:208 order fail-soft-AFTER) → 0-hits=false-PASS; fix = bare-token grep + human-classify vs frozen-history, NOT narrow ordered-pattern. **M2: "skip element" bolted on contiguous-prefix cut = correctness inversion** — naive `if protected continue` leaves skipped entry ABOVE cutLine → STILL archived while code thinks excluded; needs non-contiguous per-entry byte-sum; DRY-RUN bounds harm but 0 acceptance tests after-est. **Lessons: (1) `parallel()` = runtime-builtin NOT in-repo → premise unverifiable-from-source but design robust-under-BOTH-truth-values = endorse-with-caveat; (2) grep-acceptance must bare-token+classify; (3) verify EACH regex-alt vs EACH real target-line by hand; (4) node --check HAS teeth + top-level return/await pass via CJS wrapSafe (empirical > theory).** Spec faithful (3 floors+rec-3+3 honest-notes verified on-disk), scope-clean, honest-caveated; no Smart-Friend lower. → `archive/2026-07.md`.
|
||||
- **S101 H19 fable-clone WF2 Lane-A (toggle) — PWC, CONCERN closed @S102:** marker WRITE-ONLY dead artifact — persist-claim cần CẢ reader-side (verify consumer exists, not just producer); reader BƯỚC 0.5b wired S101-cuối. → `archive/2026-07.md`.
|
||||
- **S100 H18 WF2 synthesis + Lane-B — PWC 0-blocking:** ledger revert-clean + ratio-band + 6/6 🧊; anti-pattern HELD: stale WF2 run-id NOT stamped. → `archive/2026-07.md`.
|
||||
- **S98 (2026-07-02) 3 verify-lane — PASS (minor):** fidelity-gate 3-lớp (verbatim+pointer-arith+ground-truth-code); Windows byte-verify = .NET ReadAllBytes/`wc -c`/`od -c` (MSYS strip `\r` báo sai). → `archive/2026-07.md`.
|
||||
|
||||
@ -205,7 +205,7 @@ All 12 agent có **4 RAG-READ MCP**: `search_memory` + `search_code` (BM25, pref
|
||||
- **Keyword = QUYỀN, KHÔNG lệnh (T4):** "workflow"/"ultracode" mở quyền hỏi, KHÔNG auto-run. Mode-OFF + "chạy workflow" → TỪ CHỐI + nhắc `/ultra-on`. CẤM native `/effort ultracode`. *(Bài học 515K-token false-trigger.)*
|
||||
- **Scope (S1):** Workflow fan-out CHỈ repo SOLUTION_ERP — KHÔNG fan-out repo/corpus khác.
|
||||
- **Checkpoint (S2):** `hmw.js` **throw** nếu `checkpointApproved≠true` (mechanized tripwire anti-accidental). Em main BÁO {số agent·vai·task} @inform → set cờ → fan-out (KHÔNG chờ confirm từng lần; marker-ON=consent). Sub KHÔNG spawn sub (S3).
|
||||
- **VALID_ROLES (12 — whitelist `hmw.js`, đủ roster):** `investigator-codebase` · `investigator-api` · `implementer-backend` · `implementer-frontend` · `test-specialist` · `reviewer` · `cicd-monitor` · `frontend-designer` · `database-agent` (+S57 — S56 đã dùng 3× qua fail-soft WARN) · `office-document` (+S107 — Office WRITE OD1–OD10) · `tooling-auditor` + `harvest-curator` (+S110 anh-directed — H21 roster-parity lệnh-B; monitor vẫn INFORM-only, lane RETURN-only). Role lạ → default subagent + WARN (fail-soft, S4c).
|
||||
- **VALID_ROLES (12 — whitelist `hmw.js`, đủ roster):** `investigator-codebase` · `investigator-api` · `implementer-backend` · `implementer-frontend` · `test-specialist` · `reviewer` · `cicd-monitor` · `frontend-designer` · `database-agent` (+S57 — S56 đã dùng 3× qua fail-soft WARN) · `office-document` (+S107 — Office WRITE OD1–OD10) · `tooling-auditor` + `harvest-curator` (+S110 anh-directed — H21 roster-parity lệnh-B; monitor vẫn INFORM-only, lane RETURN-only). Role lạ ∉ VALID_ROLES → `hmw.js` THROW (STOP-HARD) + báo owner (S4c, DIRECTED REC-3 2026-07-13); role-less null → inherit lead.
|
||||
- **Memory governance (M1–M5 + R1):** B1 slice-inject (agent ← slice MEMORY của đúng vai qua `args`) · M2 return-delta-only (`memoryDelta{task,verdict,learned,surprise}`) · B3 lead single-writer VERIFY→APPEND-only (no-overwrite-unverified) · B2 harvest-LIỀN sau mỗi workflow vào `agent-memory/<role>` · M5 `store_memory` strip (đã S47). **Containment = defense-in-depth** (git-diff + Qdrant chunk-count post-P2), KHÔNG allowlist đơn-độc (G-015: sub vẫn giữ Bash/Write — KHÔNG "read-only").
|
||||
|
||||
> Floor T/S/M/R đầy đủ → `.claude/commands/ultra-on.md`. adap-report → `docs/governance/adap-reports/2026-06-03-Agent-ultracode-hmw-mem-governance.md`.
|
||||
|
||||
@ -18,12 +18,12 @@ argument-hint: (trống = bật mode · hoặc kèm task lớn đầu tiên)
|
||||
- 🟢 **H6.1 (governed-ultracode, adopt S63) — mode-ON = auto-HMW:** mode ON → task **SUBSTANTIVE** (≥2 bước độc-lập · multi-file · sweep/audit/review/migration/research/verify-heavy) em main **TỰ author+chạy Workflow** (KHÔNG cần anh gõ "workflow"; marker-ON = standing consent — vẫn checkpoint INFORM `{số agent·vai·task}` rồi chạy NGAY). Task **TRIVIAL / governance-authoring single-writer** → solo (chống token-nổ). Ranh giới = "workflow có giúp THẬT không"; nghi-ngờ nghiêng workflow, KHÔNG workflow 1-câu-hỏi-đáp. = lấy sự-tiện native ultracode + GIỮ guard HMW. (H6.7 role+memory-fidelity ĐÃ là floor sẵn: `agentType ∈ VALID_ROLES` + `memoryDelta`→agent-memory single-writer B3.)
|
||||
|
||||
## Phân loại (em main mỗi task)
|
||||
- **HMW (LỚN):** fan-out nhiều file/nguồn — sweep · audit · cross-stack review · mass migration · multi-source research. Số task THOẢI MÁI (harness queue theo slot, KHÔNG cap cứng) · spawn **ĐÚNG VAI** (`agentType` ∈ VALID_ROLES; role lạ → default subagent + cảnh báo).
|
||||
- **HMW (LỚN):** fan-out nhiều file/nguồn — sweep · audit · cross-stack review · mass migration · multi-source research. Số task THOẢI MÁI (harness queue theo slot, KHÔNG cap cứng) · spawn **ĐÚNG VAI** (`agentType` ∈ VALID_ROLES; role lạ ∉ VALID_ROLES → `hmw.js` THROW STOP-HARD + báo owner (REC-3 2026-07-13); role-less null → inherit lead).
|
||||
- **Thường (nhỏ):** <30min · 1–2 file · hỏi-đáp → Agent-tool spawn lẻ / solo theo `.claude/agents/README.md` decision-tree, KHÔNG HMW.
|
||||
|
||||
## VALID_ROLES (roster SOLUTION_ERP — 12 sub, đủ roster từ S110)
|
||||
`investigator-codebase` · `investigator-api` · `implementer-backend` · `implementer-frontend` · `test-specialist` · `reviewer` · `cicd-monitor` · `frontend-designer` · `database-agent` · `office-document` (+S107) · `tooling-auditor` · `harvest-curator` (+S110 anh-directed — monitor vẫn INFORM-only, lane RETURN-only)
|
||||
> Role lạ ∉ list → `hmw.js` degrade về default subagent + WARN (fail-soft, KHÔNG crash). Windows MAX_PATH (Dropbox nested) → KHÔNG `isolation:worktree`.
|
||||
> Role lạ ∉ VALID_ROLES → `hmw.js` **THROW (STOP-HARD)** + báo owner thêm vai (REC-3 2026-07-13; fail-soft cũ "đứt âm-thầm" đã bỏ); role-less null → inherit lead. Windows MAX_PATH (Dropbox nested) → KHÔNG `isolation:worktree`.
|
||||
|
||||
## Quy trình HMW — vai trò từng phase
|
||||
| Phase | Ai | Làm |
|
||||
|
||||
@ -36,3 +36,22 @@
|
||||
---
|
||||
|
||||
> **Nấc honest:** ledger = **convention** (em-main append tay, git-tracked audit-trail — KHÔNG OS-hook auto-write, nhất-quán CAVEAT engine "no-OS-hook"). Tín-hiệu floor-rot feed = **mechanized** (`mfe-eval.ps1` age-band + `memory-selfimprove-audit.ps1`). Detection per-item-L1-presence hiện **partial** (MFE age-band chỉ đo marks-có-date; AS/guard/gotcha mang session-ref chưa có pass so-từng-item-trong-L1 — xem §I honest nấc + CAVEAT C4 self-blind-spot).
|
||||
|
||||
---
|
||||
|
||||
## Re-verify: presence-not-age selector (broadcast `ab6c387e`, adopt S115 · 2026-07-13)
|
||||
|
||||
> AI_INFRA broadcast `ab6c387e` (type=**update**) — re-verify the memory refine-step **selector** decides by PRESENCE/COVERAGE, not AGE. Verdict per floor:
|
||||
|
||||
- **(i) Reinject = MET (presence-based).** Trigger = **floor-rot** `"ĐÃ-từng-ở-L1 ∩ CÒN-giá-trị ∩ RỚT-khỏi-L1"` (coverage-gap, this file `:12`) = the broadcast (i) condition "should be present but is missing." Age plays no part in *what-to-reinject*. The CG-1 `reinjected` session column is a **reinject-event rate-limiter / loop-breaker (≤1 per N=3 sessions), NOT an age-rank** → OK per broadcast (iii) carve-out ("date used only for last-seen/loop-breaking = OK"). Drop-date test: dropping the column changes only the rate-limit/termination count, never the selection.
|
||||
- **(iii) MFE age-band = FLAG-only (COMPLIANT).** `mfe-eval.ps1` `$oldN` is computed then `Write-Host`-printed ("KEPT status-driven age-blind") with **no downstream consumer**; the denominator is gated by STATUS, not date. Drop-date test: changes one diagnostic count, not the denominator / FIT / Goodhart / any selection.
|
||||
- **(ii)+(iii) Archive-gate = RED-FLAG surfaced → HARDENED (D3, S115).** The planner's oldest-by-position base ordering tripped the (iii) self-check; fixed in `scripts/memory-archive-gate.ps1` by promoting `value_protect` from an advisory post-hoc flag to a **pre-selection HARD-SKIP** (value-primary; position = within-low-value tiebreak) + a value-floor WARN. Fault-injection-verified (protected-not-drained + permutation-invariant + value-floor). See `memory-budget.json:value_protect._note`.
|
||||
|
||||
**BUILD-GAP (honest, disclosed):** mechanized **per-item-L1-presence** detection is still PARTIAL — the MFE age-band counts *present* marks (by date), not per-item *drops*; AS/guard/gotcha carrying session-refs have no per-item-in-L1 pass yet. So reinject-detection stays **convention + em-main judgement**, not full mechanization (do NOT overclaim). Consistent with §I honest nấc + CAVEAT C4.
|
||||
|
||||
**3 honest-notes (broadcast §5 — MANDATORY):**
|
||||
1. Basis = **ONE** occurrence already fixed elsewhere → **proactive-prevention**, NOT a spreading SE incident.
|
||||
2. **SPECIFIC-APPLICATION**, not a new rule — SE reinject + MFE age-band were already presence/flag-based (quick re-verify, no change); only the archive-gate needed hardening.
|
||||
3. Floor = **FUNCTION not FORM** — implemented in SE's own script shape; no hub structure/filenames copied.
|
||||
|
||||
> **Provenance:** `/fable-clone reviewer` 5-lane ensemble (`wf_b621aac4-f0b`) → spec `runs/2026-07-13-presence-not-age-adopt/spec-presence-not-age-adopt-13-07-2026.md` → `/fable-real reviewer` deep-pass (PASS-WITH-FIXES; M1+M2 applied) → HMW execute D1/D2/D3. Applies existing mark `RC-…10-29-11` (age=false-proxy) to the selector layer; codify-only (no new mark).
|
||||
|
||||
@ -43,8 +43,8 @@ const VALID_ROLES = [
|
||||
function resolveModel(role, rawRole, tier, i) {
|
||||
if (tier === 'fable' || tier === 'opus') return tier
|
||||
if (tier) log(`⚠️ hmw: tier "${tier}" lạ (task #${i}) → bỏ qua, dùng mặc định H8 (inherit top-tier)`)
|
||||
// Invalid-role (typo ∉ VALID_ROLES, WARN đã log ở caller) → fail-UP inherit Fable 5 — H4.5 "chưa-phân-loại
|
||||
// → nghiêng quality" (KHÔNG rơi 'opus': task có thể là gate-class gõ nhầm tên role).
|
||||
// Role-less (null) → inherit lead; invalid non-empty role đã THROW up-front (STOP-HARD, DIRECTED REC-3 2026-07-13)
|
||||
// → nhánh này giờ CHỈ đạt bởi role-less thật (rawRole null/''); giữ làm defensive no-op.
|
||||
if (!role && rawRole) return undefined
|
||||
if (!role) { log(`hmw: task #${i} role-less → inherit lead-model (H6.2 governed-ultracode; per-task tier:'opus' = escape-hatch sweep/cost)`); return undefined }
|
||||
return undefined // role có frontmatter: tất cả `inherit` (H8 all-inherit top-tier) — KHÔNG override
|
||||
@ -88,6 +88,24 @@ if (A.taskList.length > 16) {
|
||||
log(`hmw: taskList=${A.taskList.length} (>16) → harness queue theo slot (thoải mái, không cap cứng).`)
|
||||
}
|
||||
|
||||
// ─── STOP-HARD role-whitelist (DIRECTED 6c32df89 REC-3, 2026-07-13 · S115) ────
|
||||
// Vai ∉ VALID_ROLES (typo / rename-drift) = DỪNG HẲN + báo owner — KHÔNG fail-soft
|
||||
// về default subagent (fail-soft = "đứt âm-thầm" đã gây lỗi thật hub-side; sim 5-ca).
|
||||
// PHÂN-BIỆT (backward-compat H6.2): role null/omitted/'' = HỢP-LỆ role-less (inherit
|
||||
// lead) → KHÔNG throw. CHỈ non-empty-string ∉ whitelist mới throw. Validate UP-FRONT
|
||||
// (trước parallel) — halt KHÔNG phụ-thuộc semantics parallel()/.filter(Boolean) (per-lane
|
||||
// throw có thể bị nuốt thành null = STOP-SILENT, tệ hơn). Mirror checkpointApproved tripwire.
|
||||
const badRoles = A.taskList
|
||||
.map((t, i) => ({ i, raw: t && t.role }))
|
||||
.filter(x => x.raw != null && x.raw !== '' && !VALID_ROLES.includes(x.raw))
|
||||
if (badRoles.length > 0) {
|
||||
const detail = badRoles.map(x => `#${x.i}="${x.raw}"`).join(', ')
|
||||
throw new Error(
|
||||
`hmw: vai ∉ VALID_ROLES (${detail}) — DỪNG HẲN (STOP-HARD). ` +
|
||||
`Sửa typo HOẶC báo owner thêm vai vào VALID_ROLES [${VALID_ROLES.join(', ')}]. ` +
|
||||
`KHÔNG fail-soft về default subagent (DIRECTED 6c32df89 REC-3 2026-07-13).`)
|
||||
}
|
||||
|
||||
const memoryPack = A.memoryPack || {}
|
||||
const spec = A.spec || ''
|
||||
|
||||
@ -103,8 +121,8 @@ log(`HMW P2: fan-out ${A.taskList.length} task (${wave ? 'RUN-TRACE' : 'return-d
|
||||
|
||||
const results = await parallel(A.taskList.map((t, i) => () => {
|
||||
const raw = t && t.role
|
||||
const role = VALID_ROLES.includes(raw) ? raw : undefined // S4c whitelist; invalid → default subagent (fail-soft)
|
||||
if (raw && !role) log(`⚠️ hmw: agentType "${raw}" ∉ VALID_ROLES → default subagent cho task #${i}`)
|
||||
// STOP-HARD validate up-front (DIRECTED REC-3 2026-07-13): raw ở đây CHỈ có thể ∈ VALID_ROLES HOẶC null/'' (role-less H6.2).
|
||||
const role = VALID_ROLES.includes(raw) ? raw : undefined // role-less (null/'') → undefined → inherit lead
|
||||
|
||||
const mem = role && memoryPack[role] ? memoryPack[role] : ''
|
||||
const subMd = wave ? `${wave.dir}/sub-${role || 'task'}-${i}.md` : null // Harness-10 FLAT (h10-refine 2026-06-18): sub-<role>-<i>.md phẳng cùng cấp dưới runs/<run-id>/ — KHÔNG sub-md/ subdir
|
||||
@ -149,6 +167,6 @@ const results = await parallel(A.taskList.map((t, i) => () => {
|
||||
})
|
||||
}))
|
||||
|
||||
// trả mảng kết quả (lọc null nếu agent lỗi/null — invalid-role vẫn chạy default subagent, KHÔNG skip)
|
||||
// trả mảng kết quả (lọc null nếu agent lỗi/null — invalid-role đã STOP-HARD up-front; .filter(Boolean) chỉ lọc null-lane do agent lỗi runtime)
|
||||
// về em main → P3 VERIFY + harvest + P4 checklist
|
||||
return results.filter(Boolean)
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
# Run: 2026-07-13-presence-not-age-adopt — fable-clone reviewer ensemble (5-lane)
|
||||
|
||||
- **Engine:** `/fable-clone reviewer` (H21 ENGINE-ĐẮT LỆNH-B, ensemble tier-2, per-invocation, owner-directed S115)
|
||||
- **Lead:** Opus 4.8 (1M) Max (Fable outage → tier-2 ensemble lanes = Opus inherit)
|
||||
- **Đề-bài:** review AI_INFRA 2026-07-13 updates → propose SE adoption spec
|
||||
- Broadcast `ab6c387e` — presence-not-age selector guardrail (3 floor points i/ii/iii)
|
||||
- Directed `6c32df89` — approve H22 + H21/MTv3 (VERIFIED, SE=first-in-fleet H22) + rec-3 fail-soft→stop-hard + alias-sync
|
||||
- **Mode:** RUN-TRACE (5 lanes ≥3 → H22 sàn S111). Lanes Write `sub-reviewer-<n>.md` (full detail) + return lean schema (garble-safe).
|
||||
- **Pipeline (H21 spec-file):** engine propose → lead verify+refute+synthesize → lead writes `spec-presence-not-age-adopt-13-07-2026.md` → `/fable-real reviewer` reviews spec → HMW Opus executes.
|
||||
|
||||
## taskList snapshot (5 reviewer lenses, same role, multi-lens)
|
||||
|
||||
| # | lens | floor-point | sub-file |
|
||||
|---|---|---|---|
|
||||
| 1 | reinject-by-absence | (i) | sub-reviewer-1.md |
|
||||
| 2 | archive value-gate vs FIFO-by-date | (ii) — CRUX | sub-reviewer-2.md |
|
||||
| 3 | self-check drop-date-column | (iii) | sub-reviewer-3.md |
|
||||
| 4 | engine whitelist fail-soft→stop-hard + alias | Part B (rec-3) | sub-reviewer-4.md |
|
||||
| 5 | scope-discipline / honest-notes / anti-over-eng | meta | sub-reviewer-5.md |
|
||||
|
||||
## Status
|
||||
- [x] ensemble launched — `wf_b621aac4-f0b` (5 reviewer lanes, background, effort=max, inherit=Opus)
|
||||
- [x] 5 lanes returned + sub-files written — 4 via return, **lane-4 return garbled (#53) → recovered from `sub-reviewer-4.md` (0 work lost)**
|
||||
- [x] lead synthesized → spec written — `spec-presence-not-age-adopt-13-07-2026.md` (3 deliverables: D2 hmw.js MUST · D3 archive-gate SHOULD · D1 docs MUST)
|
||||
- [x] /fable-real review of spec — `spec-review-fable-real.md` **PASS-WITH-FIXES** (0C/2M/5m); M1 D2-grep-too-narrow + **M2 D3-contiguity-inversion (real bug caught)**; D2/D1 positively validated on disk
|
||||
- [x] execute D1/D2/D3 (owner chose ALL-3, S115) — em-main solo (governance single-writer, D9): **D2** hmw.js STOP-HARD (node --check exit0 + stub 7/7 + 5-doc sync) · **D3** archive-gate value-first hard-skip (fault-inject ALL PASS + real-regression clean + A7 242/242) · **D1** reinject-ledger stamp (detectors 0 new HIGH). `reviewer/MEMORY.md` fable-real self-write verified-faithful → KEPT (AS-10 residual, git-diff backstop).
|
||||
- [x] adap-report (`adap-reports/2026-07-13-…presence-not-age-reinject.md`) + send-email AI_INFRA (`8b9dc5165d5a` self-verify MATCH) + inbox STAGE-2 processed
|
||||
- [ ] commit + push — **awaiting owner go** (governance/engine changes; push triggers CI per path-filter since `scripts/*.ps1` + `.claude/workflows/hmw.js` are non-docs)
|
||||
@ -0,0 +1,128 @@
|
||||
# SPEC — Adopt AI_INFRA 2026-07-13 updates (presence-not-age selector guardrail + rec-3 STOP-HARD)
|
||||
|
||||
> **Run:** `2026-07-13-presence-not-age-adopt` · **Engine:** `/fable-clone reviewer` (H21 ensemble, `wf_b621aac4-f0b`) → lead verify+synthesize → this spec (H21 propose→verify→write→execute).
|
||||
> **Lead:** Opus 4.8 (1M) Max (Fable outage). **Provenance:** broadcast `ab6c387e` (presence-not-age, type=update) + directed `6c32df89` (approve + rec-3). Sub-traces: `sub-reviewer-{1..5}.md`.
|
||||
> **Fable-real spec-review:** `spec-review-fable-real.md` — PASS-WITH-FIXES (0C/2M/5m); **M1 + M2 + minors APPLIED in this revision** (see change-log at bottom). **Owner scope decision (S115): execute ALL 3 (D1+D2+D3).**
|
||||
> ⚠️ Line numbers below are ANCHORS-first (match by surrounding code, not hard line#); files drift.
|
||||
|
||||
---
|
||||
|
||||
## ① Tính-năng / đề-bài
|
||||
|
||||
Adopt two AI_INFRA 2026-07-13 items into SOLUTION_ERP:
|
||||
|
||||
1. **Broadcast `ab6c387e` — "presence-not-age selector guardrail"** (type=**update**, NOT a new rule). Re-verify ONLY the selector of the memory refine-step against a 3-point FUNCTION-floor: **(i)** reinject by ABSENCE/coverage-gap (not "old"); **(ii)** archive via VALUE-GATE (not FIFO-by-date; recurring-bug/anti-pattern kept regardless of age); **(iii)** self-check for age-sorting red flags.
|
||||
2. **Directed `6c32df89` rec-3** — raise SE `hmw.js` fail-soft-WARN (unknown role → default subagent) to **STOP-HARD + ask owner** (fail-soft = the silent-break that caused a real hub-side error; owner fixed hub-side today, 5-case sim test). Also approves SE H22 + H21/MTv3 at VERIFIED (SE = first-in-fleet full H22).
|
||||
|
||||
**Ensemble verdict (5 reviewer lanes, convergent):** SE selector is **already presence-based on 2 of 3 floors** (reinject + MFE age-band); the one real selector red-flag is the **archive-gate's oldest-by-position base ordering** (SHOULD-fix); the one real code MUST is **Part B (hmw.js STOP-HARD)**. Matches the broadcast's own framing: small delta, already-mostly-compliant, function-not-form.
|
||||
|
||||
### 3 honest-notes (MANDATORY — keep verbatim-in-spirit in SE docs, broadcast §5)
|
||||
1. Basis = **ONE** occurrence already fixed elsewhere → **proactive-prevention**, NOT a spreading SE incident.
|
||||
2. **SPECIFIC-APPLICATION**, not a new rule — SE reinject + MFE age-band were already presence/flag-based (quick re-verify, no change); only the archive-gate needs hardening.
|
||||
3. Floor = **FUNCTION not FORM** — implement in SE's own script shape; no obligation to copy hub structure/filenames.
|
||||
|
||||
---
|
||||
|
||||
## ② Cách implement — 3 deliverables (tiered by necessity)
|
||||
|
||||
### D1 — Presence re-verify STAMP (MUST · docs-only · owner: em-main)
|
||||
|
||||
Append an add-only re-verify block to `.claude/governance/reinject-ledger.md` (below the ledger table, above the honest-note footer):
|
||||
- **Floor (i) reinject = MET.** Trigger = floor-rot `"ĐÃ-từng-ở-L1 ∩ CÒN-giá-trị ∩ RỚT-khỏi-L1"` (presence-gap, `reinject-ledger.md:12`). CG-1 N=3 uses the `reinjected` session column as a **reinject-event rate-limiter / loop-breaker (max 1 reinject per N=3 sessions), NOT an age-rank** → OK per broadcast (iii) carve-out ("date used only for last-seen/loop-breaking = OK"; fable-real m5: label is rate-limiter not literal last-seen — verdict unchanged). Drop-date test: changes only the rate-limit/termination count, never the *what-to-reinject* decision.
|
||||
- **MFE age-band = FLAG-only** (`mfe-eval.ps1` `$oldN` computed then `Write-Host` "KEPT status-driven age-blind"; no downstream consumer; denominator gated by STATUS not date). Drop-date test: changes one diagnostic count, not denominator/FIT/Goodhart/any selection.
|
||||
- **Archive-gate (iii) = RED-FLAG surfaced → hardened by D3** (cross-ref).
|
||||
- **Known BUILD-GAP disclosed:** mechanized per-item-L1-presence detection is still PARTIAL (age-band counts present marks, not per-item drops) — reinject detection stays convention + em-main judgement. Do NOT overclaim full mechanization.
|
||||
- Embed the 3 honest-notes.
|
||||
|
||||
### D2 — hmw.js fail-soft → up-front STOP-HARD (MUST · code · owner: em-main solo)
|
||||
|
||||
**File:** `.claude/workflows/hmw.js`. **Decisive design constraint:** the throw MUST be an **up-front validation pass BEFORE `parallel(...)`** — chosen for **halt-independence** from unverifiable runtime semantics. A `throw` in the INNER thunk `(t,i)=>()=>{…}` *may* be caught by `parallel()` and swallowed to `null` by the tail `return results.filter(Boolean)` (`~:152-154`, comment "lọc null nếu agent lỗi/null") = STOP-SILENT. ⚠️ **fable-real m1:** `parallel()` is a Workflow-runtime builtin, NOT defined in-repo → swallow-to-null is an INFERENCE from the `:152` comment, not source-proven; and only the INNER thunk (not the outer synchronous map callback) is subject to it. Up-front validation is robust under BOTH truth-values (swallows → avoids drop; propagates → harmlessly earlier), so it does not depend on that inference. Mirror the existing up-front `checkpointApproved` tripwire (`~:83-85`).
|
||||
|
||||
**D2a (the MUST) — insert up-front block** after the `taskList.length > 16` notice (`~:89`), before `const memoryPack` (`~:91`):
|
||||
```js
|
||||
// ─── STOP-HARD role-whitelist (DIRECTED 6c32df89 REC-3, 2026-07-13) ──────────
|
||||
// Vai ∉ VALID_ROLES (typo / rename-drift) = DỪNG HẲN + báo owner — KHÔNG fail-soft
|
||||
// về default subagent (fail-soft = "đứt âm-thầm" đã gây lỗi thật hub-side; sim 5-ca).
|
||||
// PHÂN-BIỆT (backward-compat H6.2): role null/omitted/'' = HỢP-LỆ role-less (inherit
|
||||
// lead) → KHÔNG throw. CHỈ non-empty-string ∉ whitelist mới throw. Validate UP-FRONT
|
||||
// (trước parallel) — halt KHÔNG phụ-thuộc semantics parallel()/.filter(Boolean).
|
||||
const badRoles = A.taskList
|
||||
.map((t, i) => ({ i, raw: t && t.role }))
|
||||
.filter(x => x.raw != null && x.raw !== '' && !VALID_ROLES.includes(x.raw))
|
||||
if (badRoles.length > 0) {
|
||||
const detail = badRoles.map(x => `#${x.i}="${x.raw}"`).join(', ')
|
||||
throw new Error(
|
||||
`hmw: vai ∉ VALID_ROLES (${detail}) — DỪNG HẲN (STOP-HARD). ` +
|
||||
`Sửa typo HOẶC báo owner thêm vai vào VALID_ROLES [${VALID_ROLES.join(', ')}]. ` +
|
||||
`KHÔNG fail-soft về default subagent (DIRECTED 6c32df89 REC-3 2026-07-13).`)
|
||||
}
|
||||
```
|
||||
**D2b (cleanup) — inner map** (`~:105-107`): keep `const role = VALID_ROLES.includes(raw) ? raw : undefined` but change the comment to "role-less (null/'') → undefined → inherit lead"; **delete** the now-dead `if (raw && !role) log('⚠️ … default subagent …')` WARN line (invalid non-empty role already threw up-front).
|
||||
**D2c (comment-only, no behavior change)** — `resolveModel` (`~:46-48`) comment "Invalid-role → fail-UP inherit" → "role-less (null) → inherit lead; invalid-role đã throw up-front"; tail comment (`~:152`) "invalid-role vẫn chạy default subagent" → "invalid-role đã STOP-HARD up-front; .filter(Boolean) chỉ lọc null-lane do agent lỗi runtime".
|
||||
|
||||
**Doc-sync (LIVE-behavior docs — MUST update to STOP-HARD; FROZEN adap-reports/sent-broadcasts/session-logs/diaries excluded):**
|
||||
- `.claude/commands/ultra-on.md` (`~:21` "role lạ → default subagent + cảnh báo"; `~:26` "degrade về default subagent + WARN (fail-soft, KHÔNG crash)") → "role lạ ∉ VALID_ROLES → hmw.js THROW (STOP-HARD) + báo owner; role-less null → inherit lead".
|
||||
- `.claude/agents/README.md` (`~:208` "Role lạ → default subagent + WARN (fail-soft, S4c)") → STOP-HARD + báo owner; role-less null → inherit lead.
|
||||
- `docs/governance/harness-11-engine.md` §K.E (`~:345`) — update current-behavior clause to STOP-HARD (keep the 🧊 saga note as history).
|
||||
- `docs/governance/fable-real-runbook.md` (`~:520`, `~:74`, `~:191`) — fail-soft mentions → STOP-HARD (`:74` caveat: typo-guard is NOW code-enforced at hmw.js layer; command-layer "hỏi anh" stays convention = defense-in-depth).
|
||||
- **NO change to `session-start.md`** (fable-real independently re-confirmed: no fail-soft rule there; BƯỚC 0.5b already "thiếu/sai vai → hỏi anh").
|
||||
|
||||
**Alias-sync:** N/A for SE — `/fable-real` (single) vs `/fable-clone` (ensemble) = deliberate H21 **depth-toggle**, not a conversational-vs-ensemble alias pair. (Optional N/A-severity: drop the vestigial `args.wave→args.run` data-alias — NOT required by rec-3; defer.)
|
||||
|
||||
### D3 — archive-gate value_protect advisory → PRE-selection hard-skip (SHOULD · code · owner: em-main solo)
|
||||
|
||||
**File:** `scripts/memory-archive-gate.ps1`, PASS-1 drain block (`~:141-191`). **Problem:** base drain ordering is OLDEST-by-position (age-proxy; `~:137-138,153-162`, resolve-string `~:182` "move N oldest"); `value_protect` (`~:167-191`) is an **advisory post-hoc flag** that annotates the already-chosen move-set but never re-selects it. Worse for SE: structured `MEMORY.md` → top-of-file = evergreen spine (Role-baseline/Recurring-bug/Anti-patterns), so age-proxy is **anti-correlated with value**; spine + paraphrased anti-patterns carry none of the 9 value-tokens → drain **unflagged**.
|
||||
|
||||
**Change (keep DRY-RUN + keep_floor + strike-gating + em-main-final intact) — CORRECTED per fable-real M2:** the existing drain is a CONTIGUOUS top-prefix cut (`$prefixBytes = Σ lines[0..cutLine-1]`; `$afterEst = $bytes - $prefixBytes`). Skipping a *middle* protected entry makes the move-set NON-CONTIGUOUS, which that model CANNOT express — a naive `if ($protected) continue` leaves the protected entry ABOVE the cut = **still archived while the code believes it's excluded (silent inversion)**. D3 therefore MUST (a) exclude protected entries from the CUT (not just flag), (b) replace prefix-sum with **per-entry byte accumulation over the chosen (non-contiguous) low-value set**, (c) define protection over **LOGICAL entries (heading markers only)** for `---`-robustness:
|
||||
```powershell
|
||||
# (1) LOGICAL entries = HEADING markers ONLY (^#{2,3}\s) — NOT the '---' separators that
|
||||
# Get-EntryMarkerLineNumbers also counts (:80). A '---' inside one logical entry must NOT
|
||||
# split it, else a value token after the '---' would flag the wrong (pseudo) span.
|
||||
# span(k) = [ headingLine[k] .. headingLine[k+1]-1 ] (last -> EOF); bytes(k) = Σ (len+2).
|
||||
# (2) protected(k) = ANY line in span(k) matches any $valPatterns token (age-blind, whole span).
|
||||
# (3) movablePool = logical entries NOT protected AND NOT in keep_floor (newest-N).
|
||||
# (4) NON-CONTIGUOUS size-drain — accumulate INDIVIDUAL chosen movable bytes:
|
||||
# $afterEst = $bytes
|
||||
# foreach $e in movablePool (position-order = WITHIN-low-value tiebreak only):
|
||||
# choose $e; $afterEst -= bytes($e); if ($afterEst -lt $lowMark) { break }
|
||||
# A protected / keep_floor entry is NEVER chosen, regardless of file position.
|
||||
# (5) if movablePool exhausted AND $afterEst -ge $cap -> $warnValueFloor = $true
|
||||
# "over-cap but every drainable entry is value-protected/keep-floor -> condense by hand"
|
||||
# (mirror keep_floor WARN ~:180; propose ZERO protected entries).
|
||||
```
|
||||
The move-set is now a SET of entry-indices (not a top-prefix); the printed proposal lists the specific low-value entries and `$afterEst` sums only their bytes → coherent under interleaving.
|
||||
- Resolve-string (`~:182`): "move N oldest" → "move N lowest-value (position tiebreak) → curate by hand". Add `WARN value-floor hit` beside the keep-floor WARN.
|
||||
- **`---` double-count (fable-real M2c — NOW REQUIRED, not deferred):** step (1) uses heading-only markers, so the `---`-as-marker double-count in the legacy `$markers` cannot corrupt `$protected`/byte-spans. If an implementer reuses legacy `$markers` instead, they MUST prove `---`-robustness — heading-only is the simpler correct path.
|
||||
- `budget.json` note (`value_protect._note ~:39` + `mfe age_band ~:67`): selector is now value-PRIMARY (pre-selection hard-skip), position only a within-low-value tiebreak; the 9-token grep is a **lower-bound** signal → em-main must value-scan the WHOLE proposed set. **Do NOT claim "now fully value-gated"** — grep unchanged so the spine/paraphrase leak (fable-real m2) REMAINS; procedural em-main-scan stays the true guarantee. D3 = defense-in-depth codify, NOT leak-closure.
|
||||
|
||||
---
|
||||
|
||||
## ③ Checklist (vai + deliverable + measurable acceptance)
|
||||
|
||||
| # | Deliverable | Necessity | Owner-vai | Measurable acceptance (fault-injection, not happy-path) |
|
||||
|---|---|---|---|---|
|
||||
| D1 | reinject-ledger re-verify stamp + 3 honest-notes | **MUST** | em-main | git-diff = add-only append to `reinject-ledger.md`; block names floor(i)=MET + MFE=FLAG-only + (iii)→D3 + BUILD-GAP disclosed + 3 honest-notes present; `governance-detectors.ps1` 0 new HIGH. |
|
||||
| D2 | hmw.js up-front STOP-HARD + doc-sync | **MUST** | em-main solo | (1) `node --check hmw.js` exit-class identical pre/post. (2) Stub test: `role:'reviewr'`→**throws** (msg `/STOP-HARD/` + `"reviewr"` + VALID_ROLES); `role:null`→**no throw**; `role:''`→**no throw**; `role:'reviewer'`→**no throw**. (3) Fail-fast: typo role → `agent()` stub counter stays **0** (throw before parallel). (4) **bare-token** `rg -i "default subagent"` across {ultra-on.md, agents/README.md, harness-11-engine.md, fable-real-runbook.md} → human-classify EVERY hit as (a) synced STOP-HARD line OR (b) the known frozen 🧊 saga note (`harness-11-engine.md ~:345`); PASS = zero un-synced current-behavior "default subagent + WARN/fail-soft" lines (fable-real M1: a narrow ordered regex false-PASSes `ultra-on.md:21` "cảnh báo" + `README.md:208`). (5) Regression: `role:'reviewer'` taskList still fans out. |
|
||||
| D3 | archive-gate value_protect → pre-selection hard-skip + budget.json note | **SHOULD** | em-main solo | Use `-RepoRoot <temp-tree>` fixtures (`feedback_faultinjection_proves_teeth`): (1) high-value entry (**MUST carry a `value_protect` token** — fable-real m3) at TOP (oldest) is **NOT** in proposed move-set; a low-value entry is. (2) Permutation-invariance: same entries, positions shuffled → same value-CLASS proposed. (3) **Interleaved-protected (fable-real M2b):** protected entry BETWEEN two low-value entries → protected NOT in move-set AND `$afterEst` reflects ONLY the moved low-value bytes (proves non-contiguous accounting, not top-prefix). (4) value-floor WARN fires when every non-keep_floor entry protected (moveCount = 0). (5) DRY-RUN preserved (no file mutated); keep_floor never proposed; over-cap exits 0; A7 PASS-2 (exit-2-on-pointer-fail) untouched. (6) `git diff` touches only PASS-1 block + resolve-string + budget.json note. |
|
||||
| D4 | adap-report + send-email AI_INFRA | MUST (report) | em-main | `adap-reports/2026-07-13-Governance-presence-not-age-reinject.md` per REPORT-FORMAT LOCK (evidence + tailored/skip + honest-caveat); send-email selftest hash MATCH; report states re-verify-verdict (i/MFE compliant, archive hardened, Part B stop-hard done) + keeps the 3 honest-notes. |
|
||||
|
||||
### Sequencing (step-by-step)
|
||||
1. **D2 first** (MUST engine change) — verify stub test + node --check BEFORE anything else.
|
||||
2. **D3** (archive-gate) — fault-injection acceptance in temp-tree; DRY-RUN so zero live-memory risk.
|
||||
3. **D1** (docs stamp) — after D3 lands (cross-ref the hardening).
|
||||
4. **D4** (adap-report + email) — closeout.
|
||||
|
||||
### Scope guard (lane 5 — SKIP as over-engineering)
|
||||
- Do **NOT** rewrite the reinject predicate (compliant) or remove `keep_floor` / `value_protect` / MFE age-band (all legitimate).
|
||||
- D3 is a **bounded reorder** (no value-SCORING system, no new file, no hub-org mirroring). Position survives only as a within-low-value tiebreak. The 9-token grep is unchanged (leak stays procedural).
|
||||
- No new User-Mark (applies existing mark `RC-…10-29-11` age=false-proxy to the selector layer; codify-only, like H16/H17/H18).
|
||||
|
||||
---
|
||||
|
||||
## Change-log (fable-real fixes applied to this revision)
|
||||
- **M1** (D2 accept 4): narrow ordered regex → bare-token `default subagent` grep + human-classify (caught false-PASS of ultra-on:21 + README:208).
|
||||
- **M2** (D3 impl): contiguous-prefix "skip" = silent-inversion bug → non-contiguous per-entry byte accumulation + heading-only spans (`---`-robust) + interleaved-protected acceptance case (D3 accept 3).
|
||||
- **m1** (D2 premise): relabeled the swallow-to-null as an inference (parallel() not in-repo); up-front chosen for halt-independence.
|
||||
- **m3** (D3 accept 1): fixture high-value entry must carry a value_protect token.
|
||||
- **m5** (D1): CG-1 relabeled reinject-event rate-limiter/loop-breaker (not "last-seen debounce").
|
||||
- **m2/m4**: already honestly caveated (D3 = defense-in-depth not leak-closure; stub proves predicate not integration) — kept prominent.
|
||||
@ -0,0 +1,78 @@
|
||||
# spec-review-fable-real — adversarial deep-pass of `spec-presence-not-age-adopt-13-07-2026.md`
|
||||
|
||||
> **Engine:** `/fable-real reviewer` (single top-model deep-pass, Opus 1M; Fable outage → single Opus).
|
||||
> **Target:** `.claude/workflows/runs/2026-07-13-presence-not-age-adopt/spec-presence-not-age-adopt-13-07-2026.md`
|
||||
> **Provenance verified against disk:** broadcast `ab6c387e`, directed `6c32df89`, sub-reviewer-{1..5}, `hmw.js`, `memory-archive-gate.ps1`, `memory-budget.json`, `reinject-ledger.md`, `mfe-eval.ps1`, `ultra-on.md`, `session-start.md`, `agents/README.md`.
|
||||
|
||||
---
|
||||
|
||||
## VERDICT: PASS-WITH-FIXES (critical 0 / major 2 / minor 5)
|
||||
|
||||
The spec is faithful to the broadcast's 3 function-floors + directed rec-3, correctly classifies necessity per lane (D1/D2 MUST, D3 SHOULD, alias N/A), and is honest about its own residual limits. All D1 factual claims and all D2/D3 anchors verify against disk. Two MAJOR items are **verification-completeness gaps** (not code defects) that could let an incomplete deliverable pass its own acceptance gate; both are fixable by tightening the acceptance criteria, not by redesign. No blocker.
|
||||
|
||||
---
|
||||
|
||||
## MAJOR (fix before HMW execution)
|
||||
|
||||
### M1 — D2 acceptance criterion (4) grep MISSES 2 of the 3 doc lines it must guard [spec:106 vs spec:63-67]
|
||||
The spec's D2 doc-sync (spec:64-65) explicitly requires editing three current-behavior lines. Its acceptance criterion (4) verifies the sync with:
|
||||
```
|
||||
rg -i "fail-soft.*default subagent\|degrade về default subagent"
|
||||
```
|
||||
Tested against the three actual target lines on disk:
|
||||
|
||||
| Doc line (verified on disk) | matches spec regex? |
|
||||
|---|---|
|
||||
| `.claude/commands/ultra-on.md:21` — "role lạ → default subagent + cảnh báo" | **NO** (no "fail-soft"; not "degrade về…") |
|
||||
| `.claude/commands/ultra-on.md:26` — "degrade về default subagent + WARN (fail-soft…)" | yes |
|
||||
| `.claude/agents/README.md:208` — "…default subagent + WARN (fail-soft, S4c)." | **NO** ("fail-soft" is AFTER "default subagent", so `fail-soft.*default subagent` fails; no "degrade") |
|
||||
|
||||
So the criterion catches only **1 of 3**. It actually **narrowed** lane-4's already-imperfect regex (lane 4 had a 3rd alt `default subagent.*(fail-soft|WARN)` that caught README:208 and ultra-on:26 — but even lane 4 missed ultra-on:21's "cảnh báo" phrasing). Consequence: the grep can return **"0 hits = PASS"** while `ultra-on.md:21` and/or `README.md:208` remain un-synced → the acceptance criterion cannot detect a miss of its own MUST deliverable (a false-negative gate).
|
||||
|
||||
**Fix (acceptance-only, no redesign):** verify by grepping the bare token `-i "default subagent"` across the live-doc set {ultra-on.md, agents/README.md, harness-11-engine.md, fable-real-runbook.md}, then human-classify every hit as either (a) a synced STOP-HARD line, or (b) the KNOWN frozen-history hit the spec deliberately keeps — `harness-11-engine.md:~345` 🧊 saga note (spec:66). A narrow ordered-pattern regex cannot distinguish current-behavior from history and cannot enumerate all phrasings ("cảnh báo" vs "WARN"); the criterion must be "bare-token grep + classify," not a single pattern.
|
||||
|
||||
### M2 — D3 "skip protected" is incompatible with the existing contiguous-prefix byte model; spec under-specifies the required refactor + no acceptance tests it [spec:76-95 vs memory-archive-gate.ps1:149-165]
|
||||
The current drain (memory-archive-gate.ps1:153-162) is a **contiguous top-prefix cut**: it walks `for ($move=1; $move -le ($entryCount-$keepFloor); $move++)`, cuts at `$cutLine = $markers[$move]`, and estimates `$afterEst = $bytes - $prefixBytes` where `$prefixBytes = Σ lines[0..cutLine-1]` (a single contiguous top region). D3 (spec:89-91) says "walk position-order **BUT SKIP protected entries**; only LOW-VALUE can enter the move-set."
|
||||
|
||||
Skipping a *middle* protected entry makes the move-set **non-contiguous**, which the contiguous `$prefixBytes`/`$cutLine` model cannot express. Concrete trap: if an implementer adds a naive `if ($protected[$move]) { continue }` to the existing loop, the cut still happens at `$markers[$moveCount]`, so the protected entry — sitting **above** the cut line — is **still archived** while the code believes it was excluded. That is a silent correctness inversion (D3 would archive exactly what it set out to protect).
|
||||
|
||||
Coherent skip requires replacing the prefix-sum with **per-entry byte accumulation** (`afterEst = bytes − Σ bytes(each low-value entry actually chosen to move)`), summed over a possibly non-contiguous set — which the spec's pseudocode gestures at ("accumulate until est < lowMark") but never states as a refactor of the `$prefixBytes` logic. Compounding factor: the `---`-double-count (memory-archive-gate.ps1:80 counts `---` separators as markers; [TAILOR] note :305-306 concedes marker-count only ≈ entry-count) means `$markers[$k+1]-1` spans and the `keep_floor`/`entryCount` arithmetic are already loose, and the new `$protected` hashtable is keyed on those same double-counted indices — a value token after a `---` inside one logical entry flags the `---`-started pseudo-span, not the `##`-headed span, so the head can be proposed while the tail is "protected."
|
||||
|
||||
**Bounded:** the script is DRY-RUN (never cuts; `afterEst` is already flagged approximate with the `~` prefix and the [TAILOR] estimate note), so worst case is a *misleading proposal number*, not data loss. **But** none of the D3 acceptance criteria (spec:107) test after-est coherence or the interleaved-protected case, so a broken estimate would pass all listed checks.
|
||||
|
||||
**Fix (spec + acceptance):** (a) state explicitly that protected entries are excluded **from the cut**, not just from a flag, and that `afterEst` must sum the bytes of the individually-chosen low-value entries (non-contiguous), not a top-prefix; (b) add an acceptance case: a fixture where a protected entry is **interleaved between two low-value entries** must show the protected entry is NOT in the move-set AND `afterEst` reflects only the moved low-value bytes; (c) either fix or explicitly account for the `---` double-count in the `$protected` span mapping (the spec currently defers it to "future code-quality pass," spec:97 — acceptable only if D3's partition is proven `---`-robust, which it is not shown to be).
|
||||
|
||||
---
|
||||
|
||||
## MINOR
|
||||
|
||||
### m1 — D2 load-bearing premise is an unverifiable inference (conclusion still correct) [spec:39]
|
||||
The spec states a per-lane throw inside `taskList.map` "risks being swallowed to `null` by the tail `return results.filter(Boolean)`." **`parallel()` is a Workflow-runtime builtin — it is NOT defined anywhere in the repo** (grep across the tree returns 0 real definitions). So the swallow-to-null premise **cannot be verified from source**; it rests entirely on the `hmw.js:152` comment "lọc null nếu agent lỗi/null." The up-front design decision is nonetheless **correct and robust under both truth-values**: if `parallel()` swallows → up-front avoids the silent drop; if `parallel()` propagates → up-front is harmlessly redundant (throws even earlier, identical halt). The spec hedges appropriately ("risks"). Also a wording imprecision: a throw in the **outer** map callback `(t,i)=>()=>{…}` runs synchronously during array construction, **before** `parallel()` — it would NOT be swallowed; only a throw in the **inner thunk** is subject to `parallel()`. The spec conflates the two, but the practical target (the inner thunk, where today's fail-soft lives at :106-107) and the conclusion (validate up-front) are right. Recommend: relabel the premise "inference from the :152 comment, not proven — up-front chosen for halt-independence from unverifiable runtime semantics."
|
||||
|
||||
### m2 — D3 does NOT close the leak that justifies it over COMPLIANT (fully disclosed) [spec:96 / lane 2 §3]
|
||||
The lane-2/lane-3 case for D3 being more than "quick re-verify" is the **spine + paraphrased-anti-pattern silent leak**: high-value entries carrying none of the 9 `value_protect` tokens drain UNFLAGGED. D3's hard-skip reuses the **identical 9-token grep**, so it leaves that exact leak open — it only upgrades already-token-flagged entries from advisory→hard-skip. The residual guarantee stays 100% procedural (em-main value-scan), the same as pre-D3. The spec is explicitly honest about this (spec:96 "do NOT claim 'now fully value-gated' — paraphrase leak remains; procedural em-main-scan is still the true guarantee"), so this is **not a defect** — but it means D3 is closer to "safe defense-in-depth codify" than "closes a real gap," which strengthens lane-5's position that D3 is legitimately SHOULD / deferrable. No action required beyond keeping the honest caveat prominent.
|
||||
|
||||
### m3 — D3 acceptance (1) is only meaningful with a token-bearing fixture [spec:107]
|
||||
"high-value entry at TOP is NOT in proposed move-set" only holds if the fixture entry carries a `value_protect` token (D3 keys on the grep). A spine-style untokenized "high-value" fixture would still be proposed (the un-fixable-by-D3 case, see m2) and the test would fail for the wrong reason. Spec should require the D3(1) fixture's high-value entry to contain a `value_protect` pattern.
|
||||
|
||||
### m4 — D2 stub test proves the predicate, not the in-context integration [spec:106 crit (2)]
|
||||
Because `hmw.js` is not node-runnable, the stub (lane 4:154-160) replicates the `badRoles` filter standalone. It proves the predicate is correct but not that `A.taskList`/`VALID_ROLES` resolve at the insertion point. I manually confirmed both are in scope (VALID_ROLES `hmw.js:22`, `A` `hmw.js:75`, insertion after `:89`), so it is fine in practice — but the acceptance does not mechanically cover integration. `node --check` (crit 1) is syntax-only and does not catch a ReferenceError. Note only.
|
||||
|
||||
### m5 — D1 mislabels the CG-1 session column (verdict still correct) [spec:31]
|
||||
CG-1's `reinjected` column enforces "max 1 reinject per N=3 sessions" — a reinject-**event rate-limiter / loop-breaker**, which the spec calls a "last-seen debounce." It is not literally a last-seen-presence mark. But the drop-date test the spec applies yields the correct answer (dropping the column changes only the debounce/termination count, never the *what-to-reinject* decision → not an age-rank selector → OK per broadcast:68 carve-out). Label imprecise; the "OK / not a red-flag" verdict is sound.
|
||||
|
||||
---
|
||||
|
||||
## Positive validations (claims that resisted adversarial scrutiny)
|
||||
|
||||
- **D1 factual base fully verified on disk:** reinject predicate is presence-only (`reinject-ledger.md:12` "ĐÃ-từng-tồn-tại ∩ CÒN-giá-trị ∩ RỚT-khỏi-L1", no date term); `mfe-eval.ps1:183-187` age-band is FLAG-only (`$oldN` computed :185, printed :187 "KEPT status-driven age-blind", **no downstream consumer** — grep of `$oldN`/`markDates`/`age-band`/`KEPT` returns only the compute+print sites); **`Sort-Object` sweep = exactly 3 hits** (archive-gate:106 `Name`, measure:10 `Name`, selfimprove-audit:292 `LastWriteTime` = latest-run presence) → **zero date-rank in any reinject/archive DECISION path**, matching lanes 1+3.
|
||||
- **D2 doc-sync anchors all exist at cited lines:** `ultra-on.md:21`+`:26` ✓, `README.md:208` ✓; and **`session-start.md` correctly has NO fail-soft rule** — line 32 already says "thiếu/sai vai → em main hỏi anh, KHÔNG tự chọn" (roster 12). The spec's "NO change to session-start (grep-verified)" (spec:68) is independently confirmed; the original task premise that session-start had a fail-soft rule is correctly rejected.
|
||||
- **D2 `badRoles` filter is correct** (`x.raw != null && x.raw !== '' && !VALID_ROLES.includes(x.raw)`): preserves null / undefined(omitted) / '' as legitimate role-less inherit-lead (H6.2) and throws only on non-empty invalid — verified across all 4 cases. Insertion point (after `:89`, before `:91`) is genuinely BEFORE `parallel()` (:104) and AFTER the `checkpointApproved` tripwire (:83-85), mirroring the existing up-front guard. D2b deletion of the `:107` WARN is correct (dead after the up-front throw). `node --check` was empirically confirmed to have teeth (broken.js→exit 1) and hmw.js + an edited copy both pass (exit 0), so acceptance crit (1) "exit-class identical" is satisfiable and meaningful.
|
||||
- **Scope discipline intact:** spec forbids removing `keep_floor`/`value_protect`/mfe-age-band (spec:117) and forbids rewriting the reinject predicate — matches lane-5's anti-regression guard (row G). No scope drift beyond the 4 deliverables. D3 stays a bounded reorder with no value-scoring numeric (spec:118).
|
||||
- **3 mandatory honest-notes present** (spec:19-23) and required by D1+D4 acceptance: basis = ONE occurrence proactively fixed; SPECIFIC-APPLICATION not new rule; FUNCTION not FORM. Faithful to broadcast §5.
|
||||
- **Necessity tiering matches the convergent ensemble:** D1 MUST (docs), D2 MUST (code, directed), D3 SHOULD (bounded), alias N/A (`/fable-real` vs `/fable-clone` = depth-toggle, not a conversational-vs-ensemble alias). No Smart-Friend lowering: the two MAJORs are raised despite the spec's high overall quality.
|
||||
|
||||
---
|
||||
|
||||
## Bottom line for HMW
|
||||
Proceed. Apply M1 (replace the D2 crit-4 regex with bare-token grep + classify) and M2 (specify D3 non-contiguous byte-accounting + protected-exclusion-from-the-cut + add the interleaved-protected acceptance case) before running the D2/D3 acceptance gates. The 5 minors are notes/relabels, not blockers.
|
||||
@ -0,0 +1,79 @@
|
||||
# sub-reviewer-1 — LENS 1 / FLOOR (i) reinject-by-ABSENCE
|
||||
|
||||
**Run:** 2026-07-13-presence-not-age-adopt · **Lane:** 1 of 5 · **Lens:** reinject-by-absence
|
||||
**Broadcast:** ab6c387e (presence-not-age, type=update) · **Reviewer:** adversarial, presence-vs-age lens
|
||||
|
||||
---
|
||||
|
||||
## VERDICT: COMPLIANT (already presence-based) — necessity = SHOULD (doc-stamp only, no mechanism change)
|
||||
|
||||
SE's reinject selector (floor-rot → CG-1) already decides by **PRESENCE / coverage-gap** (dropped-from-L1), not by AGE. The `age-band` in `mfe-eval.ps1` is a **FLAG that feeds no reinject decision** — it does not rank or cut by age. Floor (i) is MET. The only actionable is an add-only re-verify stamp + surfacing the already-disclosed build-gap (mechanized per-item presence-detection is PARTIAL).
|
||||
|
||||
---
|
||||
|
||||
## Evidence (file:line + short quote)
|
||||
|
||||
### 1. The reinject TRIGGER is defined by PRESENCE, explicitly
|
||||
|
||||
- `.claude/governance/reinject-ledger.md:3` — "một **mục-sàn floor-rot** (đã-từng-ở-hot-mem nay **RỚT**)". Trigger = *was in hot-mem, now dropped* = a coverage-gap, not an age.
|
||||
- `.claude/governance/reinject-ledger.md:12` — "item **ĐÃ-từng-tồn-tại ∩ CÒN-giá-trị ∩ RỚT-khỏi-L1**" (already-existed ∩ still-has-value ∩ dropped-from-L1). This is **verbatim** the broadcast floor (i) "should be present but is missing".
|
||||
- Same line disambiguates the two non-reinject cases exactly as the broadcast demands: "Item chưa-từng-dựng = **build-gap** (KHÔNG reinject). Item hết-giá-trị = để **cold-archive** (rớt đúng-đắn)." → old-but-valueless drops correctly; only present-needed-but-missing reinjects. Age is nowhere in the predicate.
|
||||
|
||||
Broadcast floor (i) check — "Old-but-present = no reinject; new-but-dropped = reinject; age plays no part": SE ledger satisfies all three (old-but-present stays PRESENT so never qualifies; a newly-dropped high-value item qualifies regardless of its age; the predicate has no date term). **MET.**
|
||||
|
||||
### 2. The `age-band` does NOT rank or cut by age — it is an advisory FLAG
|
||||
|
||||
- `scripts/mfe-eval.ps1:184-185` — `$oldN=0; foreach ($d in $markDates) { if (($Today-$d).TotalDays -gt 30) { $oldN++ } }`. This **counts** marks >30d old. There is **no `Sort-Object`**, no ordering, no threshold-cut.
|
||||
- `scripts/mfe-eval.ps1:186-188` — the ONLY consumer of `$oldN` is a `Write-Host`: ">30d old but still Active=$oldN -> **KEPT (status-driven, age-blind)**" and "drop on status-change, **never by age**". `$oldN` feeds no branch, no filter, no reinject/cool-down decision (verified by full-file read + grep: `$oldN` appears only at :184 and :187).
|
||||
- Header comment `scripts/mfe-eval.ps1:183` — "age-band : FLAG old-but-still-required, **NEVER cut**". The design intent is explicit and matches the broadcast.
|
||||
|
||||
### 3. No date-sort anywhere in the reinject path (grep-confirmed)
|
||||
|
||||
- `Sort-Object` in `scripts/` (grep): only 3 hits — `memory-selfimprove-audit.ps1:292` (Sort `LastWriteTime` to pick the **latest** run for the HCV eval-arm = date-used-for-PRESENCE/last-seen = the broadcast's explicit **OK** case, and out of scope for reinject), `measure-agent-memory.ps1:10` (Sort by **Name**), `memory-archive-gate.ps1:106` (Sort by **Name**). **Zero** date-rank in mfe-eval.ps1 or in any reinject decision.
|
||||
|
||||
### 4. The audit script does NOT own the reinject decision
|
||||
|
||||
- `scripts/memory-selfimprove-audit.ps1:327` — "It does NOT verify ... **B2 CG-1 termination decisions** ... (those are separate arms: mfe-eval.ps1 + the reinject-ledger + em-main judgement)." So reinject is decided by the ledger's presence-test + em-main, not by any age-based checker.
|
||||
|
||||
### Broadcast self-check (iii) applied to the age-band (drop-the-date-column test)
|
||||
|
||||
Drop `$markDates` (the date column) from the age-band input → `$oldN` becomes 0/uncomputable → the **printed count changes**, BUT no reinject or cool-down **behavior** changes, because `$oldN` drives nothing. Per the broadcast rubric this is NOT the "date used for age-RANK → fix" case; the age-band is not even a selector (it selects nothing). **Not a red flag.**
|
||||
|
||||
---
|
||||
|
||||
## Self-refutation (strongest case AGAINST my COMPLIANT verdict)
|
||||
|
||||
**The attack:** `reinject-ledger.md:38` itself names the age-band as the floor-rot **detection signal feed** ("Tín-hiệu floor-rot feed = mechanized (`mfe-eval.ps1` age-band ...)"). If the only mechanized signal a human reads before reinjecting is the age-band — which thresholds at **>30d** — then in PRACTICE reinject could be smuggled-in as age-ranked (human-in-the-loop age bias). That would make "presence-based" true on paper but age-driven in operation.
|
||||
|
||||
**Why it still fails (rebuttal):** The age-band counts marks that are **still Active / still present** in `ACTIVE-MARKS.md` (it iterates `$markDates` built only from marks that passed the Active-status filter at :95-98). Still-present items are **by definition not floor-rot** (floor-rot requires DROPPED). So the age-band literally cannot surface a dropped-item reinject candidate — it counts the *opposite* set and labels it "KEPT". The genuine per-item-L1-presence detector (the thing that would actually detect a drop) is **admittedly PARTIAL** — `reinject-ledger.md:38`: "Detection per-item-L1-presence hiện **partial** ... chưa có pass so-từng-item-trong-L1". That is a **BUILD-GAP** (a missing presence-detector), **not** an age-rank being used to decide. A missing detector under-fires reinject; it does not convert the criterion to age. The broadcast pre-classifies exactly this situation: "already-presence-based → **quick re-verify only**" and "floor = FUNCTION not FORM". So COMPLIANT holds; the residual is a coverage/build-gap to note, not a presence-vs-age violation.
|
||||
|
||||
---
|
||||
|
||||
## Concrete change (necessity = SHOULD — add-only doc-stamp; mechanism unchanged)
|
||||
|
||||
No code change to `mfe-eval.ps1` (already FLAG-only) and no change to the reinject predicate (already presence-based). Propose appending a short re-verify stamp to `reinject-ledger.md` after the existing honest-nấc (current last line :38), recording the presence-not-age adoption + the self-check (iii) outcome + the still-open build-gap. Suggested text (lead may reword — FORM is self-determined per broadcast note #3):
|
||||
|
||||
```markdown
|
||||
## Presence-not-age re-verify (adap broadcast ab6c387e, 2026-07-13)
|
||||
- Floor (i) MET: reinject trigger decides by ABSENCE (đã-từng-ở-L1 ∩ CÒN-giá-trị ∩ RỚT-khỏi-L1, §CG-1 line 12), never by age. Old-but-present → cold-archive; new-but-dropped → reinject. Age has no term in the predicate.
|
||||
- Self-check (iii) PASS: `mfe-eval.ps1` age-band computes `$oldN` (count >30d) and ONLY prints it ("KEPT, status-driven, age-blind"); it feeds no reinject/cool-down branch. Drop the date column → printed count changes, reinject behavior does NOT. No `Sort-Object`-by-date in the reinject path.
|
||||
- HONEST (basis = ONE proactive occurrence, SPECIFIC-APPLICATION not new rule): mechanized per-item-L1-presence detection is STILL PARTIAL (age-band counts present marks, not drops) → BUILD-GAP, not an age-rank. Escalate to build a presence-diff pass when AS/guard/gotcha in-L1 detection matures (tracked here, §I honest nấc + CAVEAT C4).
|
||||
```
|
||||
|
||||
**Scope guard:** add-only, ~6 lines, no existing line edited/deleted (ledger is append-only single-writer em-main, `reinject-ledger.md:5`). Do NOT touch mfe-eval.ps1. Do NOT introduce any new filename/structure mirroring the hub (broadcast note #3: floor = FUNCTION not FORM).
|
||||
|
||||
---
|
||||
|
||||
## Measurable acceptance criteria
|
||||
|
||||
1. `reinject-ledger.md` contains a presence-not-age re-verify block that states floor (i) decides by absence and age has no term in the predicate. (Verify: grep `presence` / `age has no term` in the file.)
|
||||
2. The block records the self-check (iii) result for the age-band (FLAG-only, `$oldN` feeds no decision, no date-sort in reinject path). (Verify: block references `$oldN` / `age-band` / "no Sort-Object-by-date".)
|
||||
3. The block carries all 3 broadcast honest-notes for this floor: basis=ONE-occurrence, SPECIFIC-APPLICATION-not-new-rule, FUNCTION-not-FORM (or the lead-synthesized equivalent placed once across lanes). (Verify: presence of the three phrases or a single cross-lane honest-note section.)
|
||||
4. `mfe-eval.ps1` is byte-unchanged (no over-engineering; already compliant). (Verify: `git diff --stat` shows no change to `scripts/mfe-eval.ps1`.)
|
||||
5. No new date/timestamp `Sort-Object` introduced in any reinject-decision path. (Verify: grep `Sort-Object` in scripts unchanged from baseline: audit:292 LastWriteTime [presence/last-seen, OK], measure:10 Name, archive-gate:106 Name.)
|
||||
|
||||
---
|
||||
|
||||
## One-line finding
|
||||
|
||||
SE reinject floor-rot already triggers by PRESENCE (dropped-from-L1, ledger:12), the age-band is a print-only FLAG that decides nothing (mfe-eval.ps1:184-188) — COMPLIANT; only add-only doc-stamp + surface the already-disclosed partial presence-detector build-gap.
|
||||
@ -0,0 +1,123 @@
|
||||
# sub-reviewer-2 — LENS 2 / FLOOR (ii) archive VALUE-GATE vs FIFO-by-date [CRUX]
|
||||
|
||||
**Run:** 2026-07-13-presence-not-age-adopt · **Lane:** 2 of 5 · **Lens:** archive value-gate vs FIFO-by-date
|
||||
**Broadcast:** ab6c387e (presence-not-age, type=update) · **Directed:** 6c32df89 · **Reviewer:** adversarial, value-vs-age lens
|
||||
|
||||
---
|
||||
|
||||
## VERDICT: NEEDS-FIX — necessity = SHOULD (minimal ~10-line value-primary drain reorder; DRY-RUN preserved)
|
||||
|
||||
SE's `memory-archive-gate.ps1` PASS-1 planner is functionally a **FIFO-queue-with-a-value-overlay**, NOT a genuine value-gate. The **base drain ordering is OLDEST-by-position** (file position used as an age-proxy), and `value_protect` is bolted on as an **ADVISORY flag that never changes the proposed move-set**. The exact (iii) red-flag signature ("oldest-first to decide cool-down") is present in the code, its comments, and its resolve-string. DRY-RUN + `keep_floor` + em-main-final + the advisory flag together form a *functional backstop* (no automatic age-cut ever happens), which is why this is NEEDS-FIX/SHOULD and not RED-FLAG/MUST — but the floor asks the **decision-step ordering itself** to be value-first, and today it is age-first. The minimal fix promotes `value_protect` from advisory-flag to a **hard-skip in the drain-candidate loop**, making the printed PROPOSAL value-gated by construction (position demoted to a within-low-value tiebreak).
|
||||
|
||||
---
|
||||
|
||||
## Evidence (file:line + short quote)
|
||||
|
||||
### 1. The base drain ordering is OLDEST-first (age-proxy = file position)
|
||||
|
||||
- `scripts/memory-archive-gate.ps1:137` — comment: **"how many OLDEST entries to move?"**
|
||||
- `scripts/memory-archive-gate.ps1:138` — comment: **"Move oldest entries one-by-one"**
|
||||
- `scripts/memory-archive-gate.ps1:153-162` — the loop walks `for ($move = 1; $move -le ($entryCount - $keepFloor); $move++)` and cuts `$prefixBytes` = `lines[0..cutLine-1]` (the **TOP** of the file). It accumulates the move-set strictly **top-downward** until `$est -lt $lowMark`. There is no value term in this loop; the ONLY ranking key is position (`markers[$move]`).
|
||||
- `scripts/memory-archive-gate.ps1:182` — the human-facing resolution literally reads **"move $moveCount oldest -> curate L1->L2 by hand"**.
|
||||
- `scripts/memory-archive-gate.ps1:50` — `keep_floor` protects the mirror-image age axis: **"A5: never auto-drain below N newest"** (protect newest-5 = a pure recency protection). budget.json:38 `keep_floor_entries: 5`.
|
||||
|
||||
→ Both ends of the base mechanism are AGE-based: drain-oldest (top) + protect-newest (bottom-5). Position is the age-proxy (the convention = newest appended at bottom). This is *exactly* the (iii) pattern: "any place that SORTS/FILTERS by ... 'oldest-first' to DECIDE ... cool-down = RED FLAG."
|
||||
|
||||
### 2. `value_protect` is ADVISORY ONLY — it does not change the move-set
|
||||
|
||||
- `scripts/memory-archive-gate.ps1:53-56` — "If a planned MOVE would archive one OUT of L1-hot regardless of age, FLAG it ... **ADVISORY FLAG ONLY - em-main decides (no auto-exclude); keep_floor stays the recency axis.**"
|
||||
- `scripts/memory-archive-gate.ps1:167-176` — the value scan runs **AFTER** `$moveCount` is already computed (lines 149-165). It reads the *already-chosen* moved prefix (`$cutLine = $markers[$moveCount]`) and only appends `$valHits`. It does **not** feed back into `$moveCount` or the drain loop.
|
||||
- `scripts/memory-archive-gate.ps1:188-190` — the flag is a `Write-Output` only: **"-> KEEP in L1 regardless of age (do NOT age-archive); em-main decides"**. No exclusion.
|
||||
- budget.json:39 confirms by design: "the patterns below are an **advisory grep-hint** for the DRY-RUN planner to FLAG protected entries, **NOT an enforced auto-exclude**".
|
||||
|
||||
→ So the PROPOSAL (the thing printed and handed to em-main) is generated by pure FIFO-by-age; value is a *post-hoc annotation* on that FIFO set, not the gate that selects it.
|
||||
|
||||
### 3. The advisory value-detector has coverage holes that the age-base can drain through
|
||||
|
||||
`value_protect.patterns` (budget.json:40) = `["gotcha #","anti-pattern","recurring","lost-update","race","bai hoc","lesson","guard","root-cause","silent-fail"]` — a 9-token substring grep. Two concrete leak paths where a high-value OLD entry is proposed for drain **without a flag** (so em-main gets no signal):
|
||||
|
||||
- **Spine sections carry no token.** In a reviewer/lead `MEMORY.md` the TOP-of-file spine is `## 🎯 Role baseline`, `## 📋 6-category checklist`, `## 🧠 ... review essentials`. These are the highest-value keep-forever sections but contain **none** of the 9 tokens. Being at the TOP (oldest position) they are the FIRST drain candidates, and they are **UNFLAGGED** → the FIFO proposal targets the spine and the advisory backstop is silent. (Verify: the injected reviewer MEMORY.md "🎯 Role baseline" body has no `gotcha #`/`race`/`lesson`/etc.)
|
||||
- **Paraphrased anti-patterns.** An entry describing a lost-update as "hai lượt lưu liên tiếp đè nhau" without the literal `race`/`lost-update`/`lesson` token is high-value but UNFLAGGED → drainable by age. This is the precise "silent age-creep" the broadcast warns of at ab6c387e:44 ("'sắp theo ngày' ... lặng-lẽ đưa một tiêu-chí đã-bị-cấm quay trở lại đúng khâu ra-quyết-định").
|
||||
|
||||
→ The functional backstop (em-main honoring flags) can therefore **silently fail** for exactly the class floor (ii) protects. The value-gate must be the PRIMARY filter, not an opt-in-by-human-attention overlay.
|
||||
|
||||
### Broadcast self-check (iii) applied — the drop-the-date-column test
|
||||
|
||||
The archive-gate has no literal date column; **file position IS the age-column** (drain top-first, protect bottom-5). Run the broadcast's test: remove position-as-age from the selector input. Today the drain has *nothing else* to rank by — behavior collapses (it can't choose a move-set). Per the rubric ab6c387e:68, position is being used for **age-RANK** (which entry to cool), NOT merely for PRESENCE/last-seen → **"date used for age-RANK = fix."** This is a genuine (iii) hit on the *base ordering*. (Contrast lane-1's age-band, which is print-only and passes the same test.)
|
||||
|
||||
---
|
||||
|
||||
## Self-refutation (I must beat BOTH directions before settling)
|
||||
|
||||
### Direction A — "I'm UNDER-fixing; this should be COMPLIANT / quick re-verify only"
|
||||
|
||||
**The case for COMPLIANT:** The script is `FLAG-ONLY, DRY-RUN` (header :5-8, :86) and **never cuts anything** — "auto-WRITE of rules = top hazard" (:7). The real *selector* per the broadcast is em-main, who reads the `value_protect` flags and decides by value; the script is a byte-budget smoke-detector, not the selector. SE **already adopted** the 2026-06-20 principle this broadcast references — budget.json:39 explicitly cites "mark RC-...10-29-11 (time/age=false-proxy) applied to the memory layer" and :67 states "age is a FLAG, never a cut ... drops only on status-change ... NOT by age." honest-note-3 (ab6c387e:94) says floor = FUNCTION not FORM; the FUNCTION (high-value kept regardless of age) is achieved when em-main honors the flags. Broadcast pre-classifies this: "already-presence-based → quick re-verify only" (honest-note-2). Forcing a code reorder on a DRY-RUN planner is FORM-policing → over-engineering a type=UPDATE.
|
||||
|
||||
**Why it fails (rebuttal):** The stated *policy* (budget.json:67 "never by age") is presence-based, but the *mechanism that implements it* (the PASS-1 drain loop) is age-first with value as advisory — a policy/mechanism gap. Floor (ii)+(iii) do not merely ask "is high-value ever auto-cut?" (answer: no, DRY-RUN); they ask the **decision-step ordering itself** to not be oldest-first (ab6c387e:66,75). SE's ordering IS oldest-first by its own comments (:137-138,182). Relying on em-main to correct a mechanically-age-ranked proposal is precisely the "easy default that silently re-admits the banned criterion" the broadcast names (:44). And §3 shows the advisory backstop has real holes (spine + paraphrase) → the FUNCTION can silently fail, so "function is met" is not safely true. COMPLIANT overstates it.
|
||||
|
||||
### Direction B — "I'm OVER-fixing; this should be RED-FLAG / MUST / rip out FIFO"
|
||||
|
||||
**The case for RED-FLAG/MUST:** (iii) is a mandatory function-floor ("sàn chức-năng bắt-buộc", ab6c387e:46) and calls oldest-first-to-cool a **"cờ đỏ"** (:66). The code + comments + resolve-string all carry that signature verbatim. So compliance is mandatory and the base FIFO must be redesigned now.
|
||||
|
||||
**Why it fails (rebuttal):** RED-FLAG/MUST would overstate a **DRY-RUN planner that never cuts**, that **already has a value axis** (partial compliance, not naked FIFO), and whose final decision is a value-aware human. The broadcast itself scopes this as **proactive-prevention, ONE occurrence already fixed, not a spreading incident** (honest-note-1, :90) and type=UPDATE "re-verify CHỈ phần bộ chọn." There is no live harm to block. Ripping out position-ordering entirely is also wrong: after value-filtering, you still need *some* order among equally-low-value candidates, and position-as-tiebreak among already-low-value entries is defensible ("date used only to determine PRESENCE = OK", :68). So the honest necessity is SHOULD, and the fix is a minimal value-primary reorder, not a rewrite.
|
||||
|
||||
### Settle
|
||||
|
||||
Between A and B: the base ordering **is** a real (iii) hit (not COMPLIANT), but DRY-RUN + existing value_protect + em-main form a functional (if leaky) backstop with no automatic harm (not RED-FLAG/MUST). → **NEEDS-FIX, necessity SHOULD**, minimal value-primary reorder that keeps DRY-RUN/keep_floor/em-main-final intact.
|
||||
|
||||
---
|
||||
|
||||
## Concrete change (minimal — value becomes the PRIMARY drain filter)
|
||||
|
||||
**File:** `scripts/memory-archive-gate.ps1`, PASS-1 drain block (currently :141-176). Keep DRY-RUN, `keep_floor`, `strike` gating, and the printed-proposal contract; change only WHICH entries enter the move-set: **hard-skip `value_protect`-matching entries**, so position only orders the LOW-VALUE remainder.
|
||||
|
||||
Pseudocode (lead may reshape — FORM self-determined, honest-note-3):
|
||||
|
||||
```powershell
|
||||
# NEW (before the drain loop): flag each entry span as value-protected
|
||||
# entry k spans lines [ markers[k] .. markers[k+1]-1 ] (last entry -> end of file)
|
||||
$protected = @{} # k -> $true if the entry body carries any $valPatterns token
|
||||
for ($k = 0; $k -lt $entryCount; $k++) {
|
||||
$start = $markers[$k]
|
||||
$end = if ($k -lt $entryCount-1) { $markers[$k+1]-1 } else { $lines.Count-1 }
|
||||
for ($li = $start; $li -le $end; $li++) {
|
||||
foreach ($vp in $valPatterns) { if ($lines[$li] -like "*$vp*") { $protected[$k] = $true; break } }
|
||||
if ($protected[$k]) { break }
|
||||
}
|
||||
}
|
||||
|
||||
# CHANGED drain loop: walk oldest-position-first BUT SKIP protected entries.
|
||||
# Only LOW-VALUE entries (and never the newest keep_floor) can enter the move-set.
|
||||
# Accumulate moved bytes over the SKIP-filtered candidates until est < lowMark.
|
||||
# If the low-value candidates are exhausted and still >= lowMark -> $warnValueFloor = $true
|
||||
# ("over-cap but every drainable entry is value-protected -> SPLIT/condense by hand")
|
||||
# -- mirrors the existing keep_floor WARN at :180; do NOT propose a protected entry.
|
||||
```
|
||||
|
||||
Resolution-string change (:182): drop the word "oldest" → e.g. **"move $moveCount lowest-value (oldest-first tiebreak) -> curate by hand"**, so the human-facing plan no longer reads as an age-queue. Add a `WARN value-floor hit` branch alongside the existing `WARN keep-floor hit` (:180).
|
||||
|
||||
**Net effect:** the PROPOSAL itself is value-gated (a recurring-bug/anti-pattern/root-cause entry is never *proposed* for cool-down regardless of position); position survives only as a tiebreak among equally-low-value entries. This converts "FIFO-queue-with-value-overlay" → "value-gate with position tiebreak," removing the (iii) red-flag signature from the decision step while preserving DRY-RUN/keep_floor/em-main-final. ~10-15 LOC, no new file, no hub-org mirroring.
|
||||
|
||||
**Companion doc-stamps (honest-notes are mandatory, ab6c387e:88-94):**
|
||||
1. budget.json — update `value_protect._note` (:39) + `age_band` caveat (:67) to state the selector is value-PRIMARY (hard-skip), position only a within-low-value tiebreak; and that the grep is a **lower-bound** signal, so em-main must value-scan the whole proposed set, not only flagged lines.
|
||||
2. Keep the 3 broadcast honest-notes once (may be a single cross-lane block synthesized by lead): basis = ONE occurrence proactively fixed / SPECIFIC-APPLICATION not new rule / FUNCTION not FORM.
|
||||
|
||||
**Residual honesty (do NOT overclaim the fix):** the value-signal is still a 9-token substring grep; the hard-skip narrows but does not eliminate the paraphrase leak (§3). The fix makes the DEFAULT value-first (structural), but em-main-value-scan remains the true guarantee (procedural). State this in the note — do not claim "now fully value-gated."
|
||||
|
||||
---
|
||||
|
||||
## Measurable acceptance criteria (fault-injection, not happy-path)
|
||||
|
||||
Use `-RepoRoot <temp-tree>` (the script's own param :29) to build synthetic MEMORY.md fixtures — proves teeth, per `feedback_faultinjection_proves_teeth`.
|
||||
|
||||
1. **High-value-at-top is NOT proposed.** Fixture: over-cap MEMORY.md with a `## ... gotcha #99 root-cause ...` entry at the TOP (oldest position) + several low-value entries below (outside keep_floor). Run gate. **ASSERT:** the top high-value entry is NOT in the proposed move-set; a low-value entry is. (Baseline today: it IS in the move prefix, only advisory-flagged.)
|
||||
2. **Permutation-invariance (broadcast drop-date test).** Two fixtures with the SAME entries but POSITIONS permuted, value-markers preserved. **ASSERT:** both runs propose the same value-CLASS to drain (the low-value entries), not "whatever sits at top." (Baseline: proposal changes with position → age-ranked.)
|
||||
3. **Value-floor WARN fires.** Fixture: over-cap where EVERY non-keep_floor entry carries a value marker. **ASSERT:** output prints a `value-floor hit ... condense by hand` WARN and proposes moving ZERO protected entries (moveCount over protected = 0). (Baseline: proposes draining a protected entry with only an advisory note.)
|
||||
4. **DRY-RUN + keep_floor preserved.** **ASSERT:** no MEMORY.md/archive file mutated by any run; newest-`keep_floor` entries never proposed; over-cap still exits 0 (FLAG not error), A7 exit-2-on-pointer-fail unchanged (:295).
|
||||
5. **A7 pass-2 byte-unchanged behavior.** **ASSERT:** `git diff` touches only the PASS-1 block + resolve-string + budget.json note; PASS-2 integrity gate (:209-295) untouched.
|
||||
|
||||
---
|
||||
|
||||
## One-line finding
|
||||
|
||||
SE `memory-archive-gate.ps1` drains OLDEST-by-position (age-proxy) as the base ordering (:137-138,153-162,182) with `value_protect` only an advisory post-hoc flag (:53-56,188-190) — a FIFO-queue-with-value-overlay carrying the exact (iii) red-flag signature; DRY-RUN + em-main + advisory-flag are a leaky backstop (spine + paraphrase entries drain unflagged), so NEEDS-FIX/SHOULD via a ~10-line hard-skip that makes the PROPOSAL value-primary.
|
||||
@ -0,0 +1,145 @@
|
||||
# sub-reviewer-3 — LENS 3: FLOOR (iii) SELF-CHECK (drop-date/drop-order test)
|
||||
|
||||
**Run:** 2026-07-13-presence-not-age-adopt · **Lane:** reviewer 3 of 5 · **Broadcast:** `ab6c387e` (presence-not-age selector guardrail, type=update) + directed `6c32df89`
|
||||
**FloorPoint owned:** (iii) self-check — apply the broadcast's own drop-date-column test to ALL 3 SE selectors (reinject / archive-gate / mfe age-band).
|
||||
|
||||
---
|
||||
|
||||
## SETTLED VERDICT — floorPoint (iii): **RED-FLAG** · necessity **SHOULD**
|
||||
|
||||
One of the three SE selectors trips the broadcast's own self-check. Enumeration:
|
||||
|
||||
| Selector | Drop date/order input → behavior change? | date/order used for… | Verdict |
|
||||
|---|---|---|---|
|
||||
| REINJECT (`reinject-ledger.md` + CG-1) | SELECTION: no · CG-1 debounce: yes | floor-rot = presence-gap; CG-1 = per-item **last-seen** debounce | **COMPLIANT** |
|
||||
| ARCHIVE-GATE (`memory-archive-gate.ps1`) | **YES** | **age-RANK** (entry-ORDER = oldest-first drain) | **RED-FLAG** |
|
||||
| MFE age-band (`mfe-eval.ps1`) | prints a different count only | advisory **FLAG** print (never cut/select) | **COMPLIANT** |
|
||||
|
||||
The RED-FLAG is driven entirely by the **archive-gate planner**. It is NOT a live incident (DRY-RUN + advisory value-flag + documented design-intent), hence **SHOULD** not MUST — but per the broadcast's own mechanical test it **is** the exact "sorts/filters by oldest-first to decide cool-down" pattern (iii) tells me to surface and redesign. Reporting it (not rationalizing it away) is the anti-Smart-Friend call.
|
||||
|
||||
---
|
||||
|
||||
## SELECTOR 1 — REINJECT — COMPLIANT (with one honest-note)
|
||||
|
||||
**Selection trigger is pure presence.** `reinject-ledger.md:3` — reinject fires for a *"mục-sàn floor-rot (đã-từng-ở-hot-mem nay RỚT)"*; `:12` — *"item ĐÃ-từng-tồn-tại ∩ CÒN-giá-trị ∩ RỚT-khỏi-L1"*. That is verbatim the broadcast (i) condition "should be present but is missing." No age term.
|
||||
|
||||
**Drop-date test on the SELECTION:** the floor-rot classifier tests (a) was-in-L1, (b) still-valuable, (c) dropped-from-L1. None is a date. Drop every date/session column → the *what-to-reinject* decision is unchanged. ✓ presence-based.
|
||||
|
||||
**The one date-touch = CG-1 debounce, and it is the OK kind.** `:9` — *"Mỗi item-id được reinject TỐI-ĐA 1 lần trong N phiên (N = 3)."* Enforcing this needs the `reinjected` session column (`:21`). Drop it → the debounce can't count → behavior changes. BUT this is a **per-item last-acted marker** ("when did I last reinject item X?") used as a loop-breaker — it does NOT rank candidates against each other by age, and it never cuts "the oldest." Per the broadcast carve-out (`ab6c387e:68`: *"date used only to determine PRESENCE (last-seen) … = OK"*) this is compliant. It is a debounce keyed to the last-reinject-event, not an age-rank selector.
|
||||
|
||||
**Extra safety:** ledger is a CONVENTION (`:38` — em-main hand-appends, git-tracked; no OS-hook auto-write) and currently EMPTY (`:34`). No automated age-based selection exists here at all.
|
||||
|
||||
**1-line re-verify note to keep in docs (broadcast §5):** "reinject SELECTS by floor-rot presence-gap; CG-1's N=3 session-distance is a per-item last-seen debounce (loop-breaker), not an age-rank — re-verified, no change."
|
||||
|
||||
---
|
||||
|
||||
## SELECTOR 2 — ARCHIVE-GATE — **RED-FLAG** (the finding)
|
||||
|
||||
### Evidence — the planner selects by entry-ORDER (age-proxy), oldest-first
|
||||
|
||||
`memory-archive-gate.ps1`:
|
||||
- `:137` — *"A4 hysteresis + A5 keep-floor : how many **OLDEST** entries to move?"*
|
||||
- `:138` — *"Move **oldest** entries one-by-one"*
|
||||
- `:153-154` — `for ($move = 1; $move -le ($entryCount - $keepFloor); $move++) { $cutLine = $markers[$move] # first KEPT entry starts here }` → the moved set is always `markers[1..moveCount]` = the entries at the **TOP** of the file.
|
||||
- `:182` — resolution string: *"PROPOSE archive … move `$moveCount` **oldest** -> curate L1->L2 by hand"*
|
||||
- `:164-165` — keep_floor protects the LAST `keepFloor` markers (bottom); the drain targets the top.
|
||||
|
||||
There is **no literal date column** — but **entry-ORDER is the age-proxy** (MEMORY.md convention = append-newest-to-end, `memory-budget.json:24` l2_verbatim *"curated L1 entries APPEND to end only"*; `keep_floor` note *"protect NEWEST-n"* assumes newest=bottom). So "position from top" ≡ "age rank."
|
||||
|
||||
### Drop-order test → FAILS as age-RANK
|
||||
|
||||
Remove the assumption that top=oldest (shuffle entry order / drop the position input): the planner proposes a **different** move-set. The order is consumed as a **rank** (drain from position 1 upward until under low-watermark), not as presence. This is precisely broadcast (iii)'s red flag: *"any place that SORTS/FILTERS by … oldest-first to DECIDE cool-down = RED FLAG."*
|
||||
|
||||
### Worse for SE specifically: "oldest = top" is INVERTED here
|
||||
|
||||
SE MEMORY.md files are **structured** (evergreen header sections first, chronological tail last), not flat chronological logs. Verified against `.claude/agent-memory/reviewer/MEMORY.md` via the gate's own marker regex `^(#{2,3}\s|---\s*$)`:
|
||||
|
||||
- marker 1 = `:7` `## 📁 Area memory` · marker 3 = `:25` `## 🎯 Role baseline` · marker 5 = `:31` `## 🚨 Recurring bug patterns` · marker 7 = `:44` `## 📋 6-category checklist` · `:55` `## ⚠️ Anti-patterns + Smart Friend guard` · `:63` `## 🧠 review essentials`
|
||||
- the chronological `## 📅 Recent activity` only starts at `:75`.
|
||||
|
||||
So "move the oldest (top) entries first" would propose archiving **Area memory → Role baseline → Recurring bug patterns** *first* — the highest-value evergreen content — while `keep_floor` protects the recent chronological tail at the bottom. The age-proxy is not merely age-based, it is **anti-correlated with value** for SE's file shape.
|
||||
|
||||
(Aside, secondary: `---` separators are counted as markers too, so `entryCount` double-counts each section = the moveCount arithmetic is loose. Not my floor; noted for the code-quality lane.)
|
||||
|
||||
### Why it is RED-FLAG and not COMPLIANT — the value-gate exists but is advisory-only
|
||||
|
||||
`value_protect` (`:167-191`) scans the moved prefix for high-value markers (`memory-budget.json:40` patterns: gotcha# / anti-pattern / recurring / lost-update / race / lesson / guard / root-cause / silent-fail) and prints *"KEEP in L1 regardless of age (do NOT age-archive)"* (`:190`). This is the value-axis the broadcast (ii) wants — BUT:
|
||||
|
||||
- it is **ADVISORY ONLY**: `:55-56` *"ADVISORY FLAG ONLY - em-main decides (no auto-exclude); keep_floor stays the recency axis"*; `memory-budget.json:39` *"the patterns below are an advisory grep-hint … NOT an enforced auto-exclude."*
|
||||
- the move-set is **still computed oldest-first**; value_protect **annotates** it, it does not **re-select**. The automated selector's ranking axis remains age-proxy; value only enters as a human-read flag.
|
||||
|
||||
So the *function* "cut low-value not FIFO-by-date" is carried by **em-main (human) + a print-flag**, not by the selector. The selector itself still ranks by age. That is what trips (iii).
|
||||
|
||||
### Concrete fix (redesign the spot to value-gated presence) — pseudocode
|
||||
|
||||
Promote value_protect from *post-hoc annotation* to *pre-selection exclusion*, so age-order only ever tie-breaks the LOW-VALUE pool:
|
||||
|
||||
```
|
||||
# BEFORE draining: partition entries into protected vs movable
|
||||
protectedIdx = { k : entry[k] matches any value_protect pattern } # value axis (age-blind)
|
||||
movablePool = entries NOT in protectedIdx AND NOT in keep_floor(newest N)
|
||||
|
||||
# size-drain ONLY the movable (low-value) pool:
|
||||
for entry in movablePool (order = size-management tie-break, not an age claim):
|
||||
move entry; if bytes-after < lowMark: break
|
||||
|
||||
if bytes still > cap AND movablePool exhausted:
|
||||
emit "cannot auto-drain by size: remaining over-cap is value-protected/keep-floor
|
||||
-> condense high-value entries BY HAND (do NOT age-archive)"
|
||||
```
|
||||
|
||||
Key change: a value-protected entry is **never in the move-set**, regardless of its file position. Then dropping/shuffling entry-order changes only *which low-value entries drain for byte-size* — it can never touch protected content → the drop-order self-check PASSES.
|
||||
|
||||
### Measurable acceptance criteria
|
||||
|
||||
1. **Selection-exclusion, not annotation:** re-run `memory-archive-gate.ps1` on an over-cap sub where the over-cap is caused by value-marked content → planner outputs "cannot auto-drain by size (movable pool exhausted) → condense by hand", and the proposed move-set contains **zero** value_protect-matched lines (today it lists them as "oldest" then flags them).
|
||||
2. **Order-invariance of KEEP set:** in a test MEMORY.md, take one high-value entry and (a) prepend it vs (b) append it. Both runs must **keep** it. (Today: prepended → lands in moved-prefix, only FLAGGED; appended → lands in keep_floor, protected. Order-dependent treatment of the SAME entry = the bug the fix removes.)
|
||||
3. **No regression on pure-size drain:** a sub over-cap purely from many LOW-value episodic entries still drains to below low-watermark, never below keep_floor.
|
||||
|
||||
---
|
||||
|
||||
## SELECTOR 3 — MFE AGE-BAND — COMPLIANT (model of a correct FLAG)
|
||||
|
||||
`mfe-eval.ps1:183-189`:
|
||||
- `:185` — `foreach ($d in $markDates) { if (($Today - $d).TotalDays -gt 30) { $oldN++ } }`
|
||||
- `:186-187` — *"age-band (FLAG only - age=false-proxy) … >30d old but still Active=`$oldN` -> KEPT (status-driven, age-blind)"*
|
||||
|
||||
**Drop-date test:** empty `$markDates` → the printed count changes (goes to 0 / "date=0"). But the load-bearing outputs are untouched:
|
||||
- denominator `:164` `$denomCount = marks + AS + guards + recurring` — the marks in it are gated by STATUS (`:95` `if ($status -match 'Active-High' -or $status -match 'Active\b')`), date-independent;
|
||||
- Coverage-FIT `:169-178`, Goodhart anchor `:191-200` — no date input;
|
||||
- there is **no cut/sort/select** anywhere downstream of `$oldN`. It is a `Write-Host` count and nothing reads it.
|
||||
|
||||
Date is used **purely as an advisory FLAG**, never to rank or cut. Matches `memory-budget.json:67` honest_caveats.age_band *"age is a FLAG, never a cut … An item drops only on status-change … NOT by age."* This is the correct presence/status-driven pattern the broadcast endorses.
|
||||
|
||||
**1-line re-verify note:** "MFE age-band is FLAG-only; dropping the date changes one diagnostic count, not the denominator/FIT/Goodhart or any selection — re-verified, no change."
|
||||
|
||||
---
|
||||
|
||||
## Other date/sort sites reviewed and CLEARED (out of the refine-step selector floor)
|
||||
|
||||
- `memory-selfimprove-audit.ps1:292` `Sort-Object LastWriteTime -Descending` → picks `$runDirs[0]` = latest run-trace to spot-check **HCV harvest-completeness** (`:279-285`, sub-md + synthesis present?). Not a memory reinject/archive selector; and "pick the most-recent run to verify it got harvested" is a **last-seen/presence** use, not an age-rank-then-cut. Cleared.
|
||||
- `memory-archive-gate.ps1:106` / `mfe-eval.ps1:216` `Sort-Object Name` → alphabetical sub ordering, not date. Cleared.
|
||||
- `backup-sql.ps1:49` (SQL .bak retention) and `applied-eval-nokey.ps1` ("NO AGE DIMENSION YET" scaffold note) → unrelated to the memory refine-step. Cleared.
|
||||
|
||||
No literal date-sort exists in any of the three refine-step selectors; the ONLY age-mechanism that reaches a keep-vs-cut decision is the archive-gate's entry-ORDER.
|
||||
|
||||
---
|
||||
|
||||
## SELF-REFUTATION (strongest case AGAINST my RED-FLAG)
|
||||
|
||||
The broadcast is **type=update**, **proactive-prevention** (ONE occurrence elsewhere, already fixed — `ab6c387e:80-90` + §5 note 1), and explicitly **"floor = FUNCTION not FORM"** (§5 note 3) with **"already-presence-based → quick re-verify only"** (§5 note 2). Under that lens the archive-gate arguably ALREADY meets the FUNCTION-floor:
|
||||
1. it is **DRY-RUN** (`:6-8`, `:293-295`) — it NEVER actually cuts anything by age; the binding archival act is em-main (human);
|
||||
2. the human is **explicitly handed the value flags** (`:190`), so the real keep-vs-cut decision *is* value-gated;
|
||||
3. `memory-budget.json:39` already documents the governing principle as *"archival cuts LOW-VALUE, NOT FIFO-by-date … mark RC-…10-29-11 applied to the memory layer"* — i.e., design-intent is already presence/value;
|
||||
4. the byte-drain's job is **size** management (get under cap); ordering the drain oldest-first *while flagging* high-value is a defensible size heuristic, not a claim "old=archive."
|
||||
|
||||
So one could settle archive-gate as **COMPLIANT-with-a-note**, and call my RED-FLAG an over-escalation of a documented, human-gated, dry-run advisory.
|
||||
|
||||
**Rebuttal (why RED-FLAG stands, but only at SHOULD):** The broadcast's (iii) test is deliberately **mechanical**: *"any place that SORTS/FILTERS by … oldest-first to DECIDE cool-down = RED FLAG → redesign to presence."* The planner **literally** sorts oldest-first (entry-order) to produce the archive proposal — that is the flagged pattern **by definition**, independent of dry-run. "DRY-RUN + advisory + human override" mitigates **impact**, but does not make the **selector** presence-based; the selector still ranks by age-proxy, and for SE's structured files that proxy is even anti-correlated with value. Anti-Smart-Friend honesty requires me to **report that it trips** rather than rationalize it as fine — and the broadcast itself prescribes the resolution I gave ("redesign that spot to presence/value"). Hence: **RED-FLAG** (it trips — surface it) at **SHOULD** necessity (impact contained by dry-run/human/value-flag, and this is a proactive update — so harden, don't hard-block). I do not lower the bar to "PASS with a shrug," nor over-raise to a MUST-block on a dry-run advisory.
|
||||
|
||||
---
|
||||
|
||||
## Docs to keep (broadcast §5 three honest-notes — for the synthesized spec)
|
||||
1. Basis = **ONE** occurrence already fixed elsewhere (proactive-prevention, NOT a spreading SE incident).
|
||||
2. **SPECIFIC-APPLICATION**, not a new rule — SE reinject + age-band were already presence/flag-based (quick re-verify, no change); only the archive-gate needs a hardening.
|
||||
3. Floor = **FUNCTION not FORM** — the fix is "value-gate the selection," implementable in SE's own script shape; no obligation to copy hub structure/filenames.
|
||||
@ -0,0 +1,173 @@
|
||||
# sub-reviewer-4 — LENS 4 (PART B): engine-whitelist fail-soft → STOP-HARD + alias-sync
|
||||
|
||||
**Floor point:** `PartB engine-whitelist`
|
||||
**Verdict:** NEEDS-FIX (necessity SHOULD — directed-recommended proactive guardrail, low-risk + strictly-better change; not a live SE incident)
|
||||
**Source directive:** DIRECTED `6c32df89` §2.3 (REC-3) + broadcast `ab6c387e` (presence-not-age, separate lens). REC-3: "chỗ fail-soft-chỉ-WARN của bạn nên nâng lên cùng chuẩn (dừng hẳn + hỏi chủ-sở-hữu)". Hub fixed hub-side today with a 5-case sim test; fail-soft = "the silent-break that caused a real error hub-side".
|
||||
|
||||
---
|
||||
|
||||
## 1. One-line finding
|
||||
|
||||
`hmw.js` still **fail-softs** an unknown non-empty role to a generic default subagent (`:106`, `:107`, `:145`, `:152`) — this contradicts DIRECTED REC-3. Fix = an **up-front STOP-HARD throw** that halts before any fan-out, while **preserving the legitimate role-less (`null`/`''`) inherit-lead path** (H6.2).
|
||||
|
||||
---
|
||||
|
||||
## 2. Evidence (file:line + quote)
|
||||
|
||||
The fail-soft lives at four points in `.claude/workflows/hmw.js`:
|
||||
|
||||
- **`:106`** — `const role = VALID_ROLES.includes(raw) ? raw : undefined` `// S4c whitelist; invalid → default subagent (fail-soft)`
|
||||
- **`:107`** — `if (raw && !role) log(\`⚠️ hmw: agentType "${raw}" ∉ VALID_ROLES → default subagent cho task #${i}\`)` — **WARN only, no halt**.
|
||||
- **`:145`** — `agentType: role || undefined,` — invalid role collapses to `undefined` → generic subagent.
|
||||
- **`:152`** — comment `// ... invalid-role vẫn chạy default subagent, KHÔNG skip`.
|
||||
- **`:46-48`** (`resolveModel`) — `// Invalid-role (typo ∉ VALID_ROLES ...) → fail-UP inherit`; `if (!role && rawRole) return undefined`.
|
||||
|
||||
Docs that **describe this CURRENT behavior** (must sync):
|
||||
- `.claude/commands/ultra-on.md:21` — "role lạ → default subagent + cảnh báo".
|
||||
- `.claude/commands/ultra-on.md:26` — "Role lạ ∉ list → `hmw.js` degrade về default subagent + WARN (fail-soft, KHÔNG crash)".
|
||||
- `.claude/agents/README.md:208` — "Role lạ → default subagent + WARN (fail-soft, S4c)."
|
||||
- `docs/governance/harness-11-engine.md` §K.E `:345` — 🧊 note "limitation cũ ... fail-soft default-subagent" (historical framing, but the current-behavior implication is still fail-soft).
|
||||
- `docs/governance/fable-real-runbook.md:520` (P5-mới) — "2 monitor × lệnh-B → limitation K.E (fail-soft default-subagent)"; also `:74`, `:191`.
|
||||
|
||||
**Reachability today:** `VALID_ROLES` = **12** (full roster, S110 — `:22-30`). So all legitimate roster roles are valid; the fail-soft branch is now reachable **only by a genuine typo** (e.g. `reviewr`) or a rename-drift (a role renamed in the roster but a caller still passing the old name). Current effect of that typo: **silent** loss of the role's persona + memory-pack + skill identity → runs a generic subagent. This is exactly the silent-degrade class REC-3 targets.
|
||||
|
||||
---
|
||||
|
||||
## 3. KEY TECHNICAL FINDING — the naive "just change `:106` to throw" is a TRAP
|
||||
|
||||
The fan-out is `await parallel(A.taskList.map((t,i) => () => { ... agent(...) }))` (`:104-150`), and the tail is:
|
||||
|
||||
- **`:152`** — comment `// trả mảng kết quả (lọc null nếu agent lỗi/null ...)`
|
||||
- **`:154`** — `return results.filter(Boolean)`
|
||||
|
||||
`.filter(Boolean)` + the "lọc null nếu agent lỗi/null" comment mean **individual lane failures may be swallowed into `null` and silently dropped** by the runtime. Therefore a per-lane `throw` inside the map callback (the "minimal" fix) risks being caught → `null` → filtered at `:154` = **STOP-SILENT, which is WORSE than fail-soft** (silent skip vs. at-least-a-default-subagent). This is the very anti-pattern REC-3 warns against.
|
||||
|
||||
**Conclusion:** the throw MUST be an **up-front validation pass BEFORE `parallel(...)`**, so halt semantics do not depend on `parallel()`'s per-lane error handling. This also fails fast before spending tokens on any lane, and mirrors the existing up-front `checkpointApproved` tripwire (`:83-85`).
|
||||
|
||||
---
|
||||
|
||||
## 4. EXACT change (unified-diff style, against current `hmw.js`)
|
||||
|
||||
### 4a. Add up-front STOP-HARD block — insert after `:89` (the `taskList > 16` notice), before `const memoryPack` (`:91`)
|
||||
|
||||
```diff
|
||||
if (A.taskList.length > 16) {
|
||||
log(`hmw: taskList=${A.taskList.length} (>16) → harness queue theo slot (thoải mái, không cap cứng).`)
|
||||
}
|
||||
+
|
||||
+// ─── STOP-HARD role-whitelist (DIRECTED 6c32df89 REC-3, 2026-07-13) ──────────
|
||||
+// Vai ∉ VALID_ROLES (typo / rename-drift) = DỪNG HẲN + báo owner — KHÔNG fail-soft
|
||||
+// về default subagent (fail-soft = "đứt âm-thầm" đã gây lỗi thật hub-side; sim 5-ca).
|
||||
+// PHÂN-BIỆT (backward-compat): role null/omitted/'' = HỢP-LỆ role-less (H6.2
|
||||
+// governed-ultracode → inherit lead) → KHÔNG throw. CHỈ non-empty-string ∉ whitelist
|
||||
+// mới throw. Validate UP-FRONT (trước parallel) — halt KHÔNG phụ-thuộc semantics của
|
||||
+// parallel()/.filter(Boolean) (per-lane throw có thể bị nuốt thành null = STOP-SILENT, tệ hơn).
|
||||
+const badRoles = A.taskList
|
||||
+ .map((t, i) => ({ i, raw: t && t.role }))
|
||||
+ .filter(x => x.raw != null && x.raw !== '' && !VALID_ROLES.includes(x.raw))
|
||||
+if (badRoles.length > 0) {
|
||||
+ const detail = badRoles.map(x => `#${x.i}="${x.raw}"`).join(', ')
|
||||
+ throw new Error(
|
||||
+ `hmw: vai ∉ VALID_ROLES (${detail}) — DỪNG HẲN (STOP-HARD). ` +
|
||||
+ `Sửa typo HOẶC báo owner thêm vai vào VALID_ROLES [${VALID_ROLES.join(', ')}]. ` +
|
||||
+ `KHÔNG fail-soft về default subagent (DIRECTED 6c32df89 REC-3 2026-07-13).`)
|
||||
+}
|
||||
|
||||
const memoryPack = A.memoryPack || {}
|
||||
```
|
||||
|
||||
### 4b. Simplify the now-guaranteed inner map — `:105-107`
|
||||
|
||||
```diff
|
||||
const raw = t && t.role
|
||||
- const role = VALID_ROLES.includes(raw) ? raw : undefined // S4c whitelist; invalid → default subagent (fail-soft)
|
||||
- if (raw && !role) log(`⚠️ hmw: agentType "${raw}" ∉ VALID_ROLES → default subagent cho task #${i}`)
|
||||
+ // STOP-HARD validate up-front (DIRECTED REC-3): raw ở đây CHỈ có thể ∈ VALID_ROLES HOẶC null/'' (role-less H6.2).
|
||||
+ const role = VALID_ROLES.includes(raw) ? raw : undefined // role-less (null/'') → undefined → inherit lead
|
||||
```
|
||||
|
||||
### 4c. Comment-only stale-fix (no behavior change, avoids doc-drift) — `:46-48` and `:152`
|
||||
|
||||
- `:46-48` `resolveModel`: the `if (!role && rawRole) return undefined` branch is now **dead** (invalid non-empty role already threw). Keep as defensive no-op but update the comment from "Invalid-role → fail-UP inherit" to "role-less (null) → inherit lead; invalid-role đã throw up-front".
|
||||
- `:152`: change "invalid-role vẫn chạy default subagent, KHÔNG skip" → "invalid-role đã STOP-HARD up-front; `.filter(Boolean)` chỉ lọc null-lane do agent lỗi runtime".
|
||||
|
||||
> The essential MUST is **4a** (the up-front throw). 4b/4c are consistency cleanups so the code doesn't keep describing the retired fail-soft.
|
||||
|
||||
---
|
||||
|
||||
## 5. Backward-compat / does throwing break a legitimate flow? — NO
|
||||
|
||||
| taskList role | Before | After | Legit? |
|
||||
|---|---|---|---|
|
||||
| `'reviewer'` (∈12) | fan-out | fan-out (no throw) | ✓ unchanged |
|
||||
| `null` / omitted (role-less H6.2) | inherit lead, default subagent | **inherit lead, no throw** (filter excludes `raw==null`) | ✓ unchanged — **critical boundary preserved** |
|
||||
| `''` empty string | role-less (raw falsy) | role-less, no throw (filter excludes `raw===''`) | ✓ unchanged (matches old `if (raw && !role)`) |
|
||||
| `'reviewr'` typo (non-empty ∉12) | default subagent + WARN | **THROW (STOP-HARD)** | ✓ intended change — only a *buggy* flow "breaks" |
|
||||
|
||||
- The args schema (`:18`) explicitly allows `role:<VALID_ROLES|null>` — `null` is a documented legitimate value; my filter's `x.raw != null` clause preserves it. This is the single most important compat guard.
|
||||
- No documented SE flow intentionally passes an invalid non-empty role expecting default-subagent. The historical "fail-soft" uses (database-agent 3× S56; 2 monitors S110) are all **now in VALID_ROLES=12** — moot.
|
||||
- **Scope check:** `hmw.js` is shared by regular HMW-mode AND the engine-đắt `/fable-clone` ensemble. Applying STOP-HARD at this single chokepoint is *correct*, not over-broad: a typo'd role should not silently degrade in either path, and mechanizing at the code chokepoint is more robust than the command-layer convention alone (which stays as first-line: `/fable-*` "thiếu/sai vai → hỏi anh"). Defense-in-depth.
|
||||
- **Regression witness:** the current run under review (`/fable-clone reviewer` — 5 lanes, `role='reviewer'`) still fans out with zero throw, because `reviewer ∈ VALID_ROLES`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Doc-sync list (Harness-20 §L.B 4-group classification)
|
||||
|
||||
**LIVE behavior docs — MUST update to STOP-HARD:**
|
||||
1. `.claude/workflows/hmw.js` — the code (§4).
|
||||
2. `.claude/commands/ultra-on.md:21` — "role lạ → default subagent + cảnh báo" → "role lạ ∉ VALID_ROLES → hmw.js THROW (STOP-HARD) + báo owner; role-less null → inherit lead".
|
||||
3. `.claude/commands/ultra-on.md:26` — "degrade về default subagent + WARN (fail-soft, KHÔNG crash)" → "THROW (STOP-HARD) + báo owner (REC-3 2026-07-13); role-less null → inherit lead".
|
||||
4. `.claude/agents/README.md:208` — "Role lạ → default subagent + WARN (fail-soft, S4c)." → "Role lạ ∉ VALID_ROLES → THROW (STOP-HARD) + báo owner; role-less null → inherit lead".
|
||||
5. `docs/governance/harness-11-engine.md` §K.E `:345` — update current-behavior clause to STOP-HARD (keep the 🧊 saga note as history); optional: add role-whitelist throw to `§D.4:67` as a 2nd mechanized tripwire alongside `hmw.js:76` checkpoint.
|
||||
6. `docs/governance/fable-real-runbook.md:520` (P5-mới "fail-soft default-subagent") + `:74` (caveat "KHÔNG code-enforced" — now the typo-guard IS code-enforced at hmw.js layer; command-layer ask-owner stays convention) + `:191`.
|
||||
|
||||
**FROZEN — DO NOT edit (snapshot-by-date / adap saga; H20 §L.B "KHÔNG rewrite LỊCH-SỬ"):**
|
||||
- `docs/governance/adap-reports/2026-06-03-*.md`, `2026-06-15-Agent-harness-6-*.md`, `2026-07-03-Agent-harness-19-*.md`.
|
||||
- `broadcasts/outbox/ai_infra/2026-06-10-*.md`, `2026-06-15-*.md` (sent mail).
|
||||
- `docs/changelog/sessions/*.md`, agent-memory diaries.
|
||||
|
||||
**Correction to the task premise:** `.claude/commands/session-start.md` does **NOT** document a fail-soft rule (grep-verified: no "default subagent"/"fail-soft"/"Role lạ" in it). BƯỚC 0.5b `:32` already says "thiếu/sai vai → em main hỏi anh, KHÔNG tự chọn" — already STOP-HARD-spirit for the engine-đắt path. **session-start needs no change** (contra the lens's "session-start §BƯỚC 0.5/HMW" assumption).
|
||||
|
||||
---
|
||||
|
||||
## 7. Alias-shortcut to remove? — LARGELY N/A for SE
|
||||
|
||||
DIRECTED §2.3: hub "gộp lệnh hội-thoại vào lệnh ensemble (một cổng duy nhất) + bỏ các alias gán-tắt — nếu hệ của bạn còn alias tương tự, cân nhắc đồng bộ."
|
||||
|
||||
- SE's two engine-đắt commands `/fable-real <vai>` (lệnh-A single top-model) and `/fable-clone <vai>` (lệnh-B ensemble) are **two distinct engines differentiated by DEPTH** (single-deep vs N-lane ensemble) — a deliberate H21 depth-toggle, **not** a conversational-vs-ensemble alias pair. Neither is "conversational". There is **no** SE command that aliases a chat-style invocation onto the ensemble command. → the hub's specific merge has **no SE counterpart**.
|
||||
- `/ultra-on` `/ultra-off` sit on a different axis (bulk-fan-out consent gate), not aliases of `/fable-*`.
|
||||
- The H19 marker `.claude/fable-real-mode.on` alias/toggle was already **retired S110** (`.gitignore:92` keeps the pattern only as a precaution).
|
||||
- **Only vestigial alias:** the `args.wave` → `args.run` **data-arg** back-compat alias in `hmw.js:19` + `:98` (`(A.wave && A.wave.dir) ? A.wave : null`). This is a data alias (RUN-TRACE arg), **not a command alias**, and is a harmless defensive fallback. Optional cleanup only (remove `[legacy alias: args.wave]` + the `A.wave` branch once no caller uses `wave:`); **N/A-severity**, not required by REC-3.
|
||||
|
||||
**Verdict on alias:** N/A (no conversational-vs-ensemble alias exists); optional minor = drop the `args.wave` data-alias.
|
||||
|
||||
---
|
||||
|
||||
## 8. Measurable acceptance criteria
|
||||
|
||||
1. **Syntax invariance:** `node --check .claude/workflows/hmw.js` exit code is **identical before and after** the change. The added block is pure synchronous JS (`.map`/`.filter`/`throw`/template-literal) — introduces no new top-level `await`/`return`/`import`, so cannot change the syntax-validity class (whatever the baseline result is under the Workflow-runtime wrapper).
|
||||
2. **Behavior assertion (deterministic stub harness — the teeth):** stub the validation logic with `VALID_ROLES` and assert:
|
||||
- `[{role:'reviewr',prompt:'x'}]` → **throws**, and the message matches `/STOP-HARD/` and contains `"reviewr"` and `VALID_ROLES`.
|
||||
- `[{role:null,prompt:'x'}]` → **does NOT throw** (role-less path preserved).
|
||||
- `[{role:'',prompt:'x'}]` → **does NOT throw**.
|
||||
- `[{role:'reviewer',prompt:'x'}]` → **does NOT throw**.
|
||||
```js
|
||||
const VALID_ROLES=['investigator-codebase','investigator-api','implementer-backend','implementer-frontend','test-specialist','reviewer','cicd-monitor','frontend-designer','database-agent','office-document','tooling-auditor','harvest-curator']
|
||||
const val=tl=>{const b=tl.map((t,i)=>({i,raw:t&&t.role})).filter(x=>x.raw!=null&&x.raw!==''&&!VALID_ROLES.includes(x.raw));if(b.length)throw new Error('STOP-HARD '+b.map(x=>`"${x.raw}"`).join(','))}
|
||||
let e=0;try{val([{role:'reviewr'}])}catch(x){e=/STOP-HARD/.test(x.message)&&/reviewr/.test(x.message)}
|
||||
let a=1;try{val([{role:null}]);val([{role:''}]);val([{role:'reviewer'}])}catch(x){a=0}
|
||||
console.log(e&&a?'PASS':'FAIL')
|
||||
```
|
||||
3. **Fail-fast proof:** with a typo role, an `agent()` stub call-counter stays **0** (throw happens before `parallel(...)`) — proves no token spend AND not a per-lane silent-drop.
|
||||
4. **Doc-sync grep:** `rg -i "fail-soft.*default subagent|degrade về default subagent|default subagent.*(fail-soft|WARN)"` over `.claude/commands/*.md` + `.claude/agents/README.md` + `docs/governance/harness-11-engine.md` + `docs/governance/fable-real-runbook.md` returns **0** current-behavior hits (frozen adap-reports/sessions/broadcasts excluded).
|
||||
5. **Regression:** the `/fable-clone reviewer` run under review still fans out (no throw).
|
||||
|
||||
---
|
||||
|
||||
## 9. Self-refutation (strongest case AGAINST NEEDS-FIX)
|
||||
|
||||
- **"It's a no-op — VALID_ROLES=12 = full roster, fail-soft is unreachable, so this is paperwork."** Rebuttal: reachable by **typo / rename-drift**; the current effect is a *silent* loss of persona+memory+skill (generic subagent), the exact silent-degrade REC-3 names. STOP-HARD converts a silent quality-break into a loud, owner-actionable halt, and future-proofs role renames. Non-trivial.
|
||||
- **"The directive scoped STOP-HARD to engine-đắt commands; hmw.js also serves regular HMW-mode — over-broad."** Rebuttal: a typo'd role should not silently degrade in *either* path; role-less (`null`) — the only legitimate "no specific role" case — is explicitly preserved. Chokepoint mechanization > command-layer convention alone.
|
||||
- **"Just throw at `:106` — minimal diff."** Rebuttal: `:154 .filter(Boolean)` + "lọc null nếu agent lỗi/null" mean a per-lane throw may be swallowed to `null` = STOP-SILENT (worse). Up-front validation is required for real halt semantics — this is the decisive technical reason, not a style choice.
|
||||
- **"`/fable-real` vs `/fable-clone` IS the conversational-vs-ensemble alias to collapse."** Rebuttal: both are engine-đắt deep commands differentiated by depth (single vs ensemble), a deliberate H21 toggle; neither is conversational. No SE conversational alias exists → alias-sync genuinely N/A.
|
||||
- **Net:** the refutations soften *necessity* (SHOULD, not MUST — proactive, no live SE incident) but do **not** overturn the verdict: SE code still fail-softs where the fleet directive says STOP-HARD → **NEEDS-FIX**, apply this adoption cycle.
|
||||
@ -0,0 +1,98 @@
|
||||
# sub-reviewer-5 — LENS 5: SCOPE-DISCIPLINE / anti-over-engineering / honest-notes
|
||||
|
||||
**VERDICT: COMPLIANT** (memory-selector already meets the 3 function-floors → quick re-verify only, NO rewrite). The ONLY real code change in this run is Part B (hmw.js fail-soft→stop-hard), which is a legitimately-directed in-scope MUST. My lane's value = guarding the other 4 lanes against over-reach: the archive-gate `oldest-first` ordering is a **RED-FLAG-SHAPED** construct but is **functionally gated** (value_protect + keep_floor + DRY-RUN + em-main), so it is at most a bounded SHOULD, never a MUST/rewrite.
|
||||
|
||||
Run: `2026-07-13-presence-not-age-adopt` · lane 5 of 5 · floorPoint = scope-discipline (meta).
|
||||
|
||||
---
|
||||
|
||||
## 0. What the broadcast actually asks (the scope contract I am enforcing)
|
||||
|
||||
Broadcast `ab6c387e` (disk `AI_INFRA/broadcasts/outbox/all/2026-07-13-...reinject.md`) is **type: update**, self-labelled repeatedly as a SMALL delta:
|
||||
- §1 L20 "**bản cập-nhật (type: update), KHÔNG phải một quy-tắc mới**"; L26 "**re-verify đúng phần bộ chọn — KHÔNG cần adopt lại toàn-bộ vòng bộ-nhớ từ đầu**".
|
||||
- §5 three honest-notes (L90/L92/L94): (1) basis = **ONE occurrence already fixed** = proactive-prevention, NOT a spreading incident; (2) **SPECIFIC-APPLICATION not new rule** → already-presence-based = quick re-verify only; (3) **floor = FUNCTION not FORM** → filenames/structure self-determined, **do NOT copy hub org**.
|
||||
- §6 L102 "**Nếu bộ chọn đã đúng → ghi một dòng xác-nhận... 'đã re-verify, không có gì đổi'**".
|
||||
|
||||
So the *default expected outcome* is "already-compliant + one-line re-verify". Any lane proposing a script rewrite bears a heavy burden of proof.
|
||||
|
||||
---
|
||||
|
||||
## 1. Evidence — SE is ALREADY presence-based on all 3 floors
|
||||
|
||||
### Floor (i) reinject-by-absence — COMPLIANT (0 change)
|
||||
`.claude/governance/reinject-ledger.md` L3 + L12: trigger is **floor-rot** = "item **ĐÃ-từng-tồn-tại ∩ CÒN-giá-trị ∩ RỚT-khỏi-L1**" (B3 test). That is a pure **coverage-gap / presence** predicate — age is not a term in it. L12 explicitly routes the two non-presence cases *away* from reinject: "chưa-từng-dựng = build-gap", "hết-giá-trị = cold-archive". Drop-date-column test: the B3 predicate never reads a date, so behavior is invariant → presence-clean.
|
||||
|
||||
### Floor (ii) archive value-gate — COMPLIANT-BY-FUNCTION (value-gate EXISTS)
|
||||
`memory-budget.json` `archive_gate.value_protect.patterns` (L38-41) + the mark it cites, **`RC-pqhuy1987-20-06-2026-10-29-11`** (verified present, `ACTIVE-MARKS.md` L17, Active-High, anh-confirm S79: "time/age/recency-decay = false-proxy … kiến-trúc KHÔNG dựa cũ … archive-gate"). The value-axis gate is already implemented and already anchored to the exact mark the broadcast descends from. `keep_floor_entries=5` (L36) is a **recency PROTECT-floor** (protects newest-5 from being drained) — it is a *floor that prevents cutting*, NOT a selector that cuts by age; the budget `_note` L39 states this orthogonality explicitly ("keep_floor protects NEWEST-n … value_protect protects HIGH-VALUE regardless of age … archival cuts LOW-VALUE, NOT FIFO-by-date").
|
||||
|
||||
### Floor (iii) self-check for age-rank — one real red-flag-SHAPE, but downstream-gated
|
||||
`scripts/memory-archive-gate.ps1` PASS-1 PLANNER L137-165 literally computes "**Move oldest entries one-by-one**" (`for ($move = 1; $move -le ($entryCount - $keepFloor); $move++)`, cutting at `markers[$move]`). This is an **age-RANK ordering** by the broadcast's own drop-date test (remove entry order → cannot compute `moveCount` → behavior changes). BUT: (a) whole script is **DRY-RUN**, header L7 "FLAG-ONLY … Does NOT move/edit"; (b) `value_protect` scans the moved prefix and FLAGS high-value entries "KEEP in L1 regardless of age" (L167-191); (c) the actual archive DECISION is em-main (human), L56/L190 "ADVISORY FLAG ONLY - em-main decides (no auto-exclude)". So the *decision layer* is value-gated; oldest-first is a proposal-ordering heuristic inside a dry-run, not the cool-down decision.
|
||||
|
||||
`scripts/mfe-eval.ps1` age-band L183-188: "**age-band : FLAG old-but-still-required, NEVER cut (mark RC-...10-29-11)**", "**>30d old but still Active … -> KEPT (status-driven, age-blind)**". This is presence-clean already (age = surfacing flag, drop only on status-change).
|
||||
|
||||
---
|
||||
|
||||
## 2. Ranked proposed-change table (my core deliverable)
|
||||
|
||||
| # | Proposed change | Necessity | Rationale (scope-discipline) |
|
||||
|---|---|---|---|
|
||||
| A | Write the presence-not-age **re-verify note** (1 paragraph) into docs (ledger honest-nac §6.5 or reinject-ledger footer) confirming floors i/ii/iii met + citing keep_floor/value_protect/DRY-RUN + mark …29-11 | **MUST** | Broadcast §6 L102/L105 *requires* a written confirmation + the 3 honest-notes. Doc-only, cheap, zero code risk. |
|
||||
| B | Keep all **3 honest-notes** verbatim-in-substance in the spec (ONE-occurrence proactive-prevention · specific-application-not-rule · function-not-form/no-copy-hub) | **MUST** | Broadcast §5 L104 "Giữ đủ ba ghi-chú trung-thực". Free, and it is the anti-over-reach anchor itself. |
|
||||
| C | **Part B** — hmw.js invalid-role fail-soft-WARN → **STOP-HARD + ask owner**, mirrored to `commands/ultra-on.md` doc | **MUST** | Directed `6c32df89` §2.3 REC-3 (in-scope, real silent-break class fixed hub-side today w/ 5-case sim). See §4 for minimal form. |
|
||||
| D | In `memory-archive-gate.ps1`, compute the value-protect / keep-floor exclusion **BEFORE** the oldest-first ordering, so the candidate set = (entries − floor − value-protected) and the PLAN is value-gated-then-ordered rather than ordered-then-flagged | **SHOULD** (optional) | Tightens self-check (iii) at the FORM level using pieces that already exist (~10 LOC reorder). NOT a MUST: DRY-RUN + human + existing advisory flag already deliver the function. **Guard: must NOT balloon into a value-SCORING engine** — that violates function-not-form. |
|
||||
| E | Alias-removal (directed §2.3 "check if SE has a similar alias to remove") | **SKIP / verify-N-A** | SE has no conversational-vs-ensemble alias split. `fable-real.md` + `fable-clone.md` are **2 distinct engines by design** (fable-clone.md L34 "2 engine GIỮ từ H19"), not aliases. Correct outcome = "verified none". Lane 4 owns the definitive call. |
|
||||
| F | Rewrite reinject-ledger / build a coverage-scanner / rewrite archive-gate to delete oldest-first | **SKIP (over-eng)** | Violates §5 note-2 (quick re-verify) + note-3 (function-not-form). Floor (i) already presence-by-construction. |
|
||||
| G | Remove/weaken `keep_floor_entries` or the mfe age-band as "age-based selectors" | **SKIP — ANTI-REGRESSION GUARD** | keep_floor is a recency **PROTECT** floor (never cuts by age); mfe age-band is already FLAG-never-cut. Removing either would HARM (drain-below-floor / lose surfacing) and would MISREAD the mark. Explicitly flag if any lane proposes this. |
|
||||
|
||||
---
|
||||
|
||||
## 3. The crux tension (self-refutation of my COMPLIANT verdict)
|
||||
|
||||
**Strongest case AGAINST "COMPLIANT / no MUST on the memory side":** The broadcast self-check (iii) is literal — "any place that SORTS/FILTERS by … oldest-first to DECIDE … cool-down = RED FLAG → redesign to presence." `memory-archive-gate.ps1` L138/L153-162 DOES sort oldest-first to produce the archive proposal, and `value_protect` is **advisory-only, no auto-exclude** (L55-56, L190). So the *computed* plan (`move $moveCount oldest`, L182) is FIFO-shaped and the value-gate is a flag appended AFTER — not a gate that alters the move-set. Under the drop-date test the planner's behavior changes → age is used for RANK → by the broadcast's own words this is a red flag that "cần sửa".
|
||||
|
||||
**Why I still settle COMPLIANT (not NEEDS-FIX):** The broadcast's target is the **cool-down DECISION**, not every heuristic that touches order inside a dry-run. In SE the decision-maker is em-main (human), the value-gate (value_protect + mark …29-11) is *in that human's loop*, and the script **never cuts** (DRY-RUN, exit-0-unless-pointer-broken L293-295). Function-floor (ii) — "archive passes through a value-gate" — is satisfied at the decision layer. The oldest-first is a proposal-ordering, and the honest fix is not a rewrite but the **bounded reorder in row D** (make the value-gate precede the ordering so the PLAN is value-gated-first). That is a SHOULD, and even it is optional. Calling this a MUST/rewrite would itself violate the scope-discipline the broadcast demands (note-2 quick-re-verify, note-3 function-not-form). Net: the red-flag SHAPE is real and worth one honest sentence in the spec + optional row-D tightening — it is NOT a spreading defect and NOT a rewrite trigger.
|
||||
|
||||
---
|
||||
|
||||
## 4. Part B in-scope confirmation + minimal form (guard against ballooning)
|
||||
|
||||
Part B is **NOT** part of broadcast `ab6c387e`; it comes from directed reply `6c32df89` §2.3 REC-3. Bundling two same-day AI_INFRA items into one run is reasonable batching, **not harmful scope-creep** — but the spec MUST keep Part B in a clearly-separated section so the "quick, no-rewrite" presence-not-age re-verify is not conflated with an actual code change.
|
||||
|
||||
Current state (2 fail-soft spots, only #2 is the target):
|
||||
- `hmw.js` L106 `const role = VALID_ROLES.includes(raw) ? raw : undefined` + L107 WARN + L145 `agentType: role || undefined` → **invalid role silently runs the DEFAULT subagent** (the silent-break class the hub hit).
|
||||
- `hmw.js` L46-49 resolveModel is about MODEL inherit for invalid role — leave the role-LESS legitimate path (`!role && !rawRole`, L49 governed-ultracode default) UNTOUCHED.
|
||||
|
||||
**Minimal in-scope form** (Lane 4 owns the exact diff; I only bound it): add a **pre-flight validation BEFORE `parallel()`** (after the `checkpointApproved` throw ~L85), mirroring the existing throw pattern L83-85:
|
||||
```js
|
||||
// Part B (directed 6c32df89 REC-3): role specified-but-∉-whitelist = STOP-HARD, ask owner.
|
||||
// (role-LESS by design stays legal — governed-ultracode default, L49.)
|
||||
const badRoles = A.taskList
|
||||
.map(t => t && t.role)
|
||||
.filter(r => r != null && !VALID_ROLES.includes(r))
|
||||
if (badRoles.length > 0) {
|
||||
throw new Error(`hmw: role(s) ∉ VALID_ROLES: ${[...new Set(badRoles)].join(', ')} — `
|
||||
+ `STOP-HARD (directed REC-3): báo anh bổ sung vai vào VALID_ROLES hoặc sửa typo. `
|
||||
+ `KHÔNG rơi-lặng về default subagent.`)
|
||||
}
|
||||
```
|
||||
Precision guard (scope-discipline): the predicate is `r != null && !includes(r)` — it must fire ONLY on a *specified-but-invalid* role, never on the legitimate `null` role-less path. Doc mirror: `commands/ultra-on.md` L21 + L26 currently document "role lạ → default subagent + WARN (fail-soft)"; these two lines MUST be updated to "STOP-HARD + hỏi anh" or the doc will lie about the engine. **Do NOT** expand Part B into a rework of resolveModel, the model-tier system, or VALID_ROLES membership.
|
||||
|
||||
---
|
||||
|
||||
## 5. Measurable acceptance criteria
|
||||
|
||||
1. **Spec contains all 3 honest-notes** (grep the spec for: "one/ONE occurrence" + "proactive-prevention"; "specific-application"/"not a new rule"; "function not form"/"do NOT copy hub"). 3/3 present = PASS.
|
||||
2. **Presence re-verify note written** to a git-tracked doc citing floors i/ii/iii + `keep_floor` + `value_protect` + mark `RC-pqhuy1987-20-06-2026-10-29-11`. Exists + cites the mark = PASS.
|
||||
3. **Zero deletion** of `keep_floor_entries`, `value_protect`, or the mfe age-band in the final diff (`git diff` shows these lines intact). Any removal = FAIL (row G regression).
|
||||
4. **Part B**: `hmw.js` throws on a specified-but-invalid role (fault-inject: taskList `[{role:'not-a-role'}]` → hard error, run aborts BEFORE any spawn) AND a role-LESS task (`{role:null}`) STILL runs (no throw). Both = PASS. `commands/ultra-on.md` L21/L26 no longer say "fail-soft/default subagent".
|
||||
5. **No script rewrite**: `memory-archive-gate.ps1` diff is either empty, or (row D) a bounded reorder < ~15 LOC that introduces NO value-scoring/ranking numeric. Larger diff = scope-fail.
|
||||
6. **Alias**: spec records "verified N/A — fable-real/fable-clone are distinct engines, no alias" OR a concrete alias found by Lane 4. Fabricated alias-removal = FAIL.
|
||||
|
||||
---
|
||||
|
||||
## 6. Bottom line for the synthesizer
|
||||
|
||||
- Memory-selector (floors i/ii/iii): **already functionally compliant** → MUST = write the re-verify note + keep 3 honest-notes (docs only). SHOULD = optional row-D reorder. SKIP = any script rewrite / ledger rebuild / keep_floor-or-age-band removal.
|
||||
- Part B (hmw.js): **in-scope MUST**, minimal pre-flight throw + doc mirror, precision-guarded to spare the role-less path.
|
||||
- Alias: **verify-N/A**, do not fabricate.
|
||||
- The single biggest risk in this run is a lane over-reading self-check (iii) into a rewrite of `memory-archive-gate.ps1`. Hold that at SHOULD-optional; the DRY-RUN + human + value_protect + mark already satisfy the FUNCTION.
|
||||
Reference in New Issue
Block a user