[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,
|
"keep_floor_entries": 5,
|
||||||
"strike_threshold": 2,
|
"strike_threshold": 2,
|
||||||
"value_protect": {
|
"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"]
|
"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`)
|
## 📅 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`.
|
- **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`.
|
- **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`.
|
- **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.)*
|
- **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.
|
- **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).
|
- **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").
|
- **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`.
|
> 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.)
|
- 🟢 **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)
|
## 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.
|
- **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)
|
## 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)
|
`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
|
## Quy trình HMW — vai trò từng phase
|
||||||
| Phase | Ai | Làm |
|
| 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).
|
> **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) {
|
function resolveModel(role, rawRole, tier, i) {
|
||||||
if (tier === 'fable' || tier === 'opus') return tier
|
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)`)
|
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
|
// Role-less (null) → inherit lead; invalid non-empty role đã THROW up-front (STOP-HARD, DIRECTED REC-3 2026-07-13)
|
||||||
// → nghiêng quality" (KHÔNG rơi 'opus': task có thể là gate-class gõ nhầm tên role).
|
// → 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 && 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 }
|
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
|
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).`)
|
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 memoryPack = A.memoryPack || {}
|
||||||
const spec = A.spec || ''
|
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 results = await parallel(A.taskList.map((t, i) => () => {
|
||||||
const raw = t && t.role
|
const raw = t && t.role
|
||||||
const role = VALID_ROLES.includes(raw) ? raw : undefined // S4c whitelist; invalid → default subagent (fail-soft)
|
// 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).
|
||||||
if (raw && !role) log(`⚠️ hmw: agentType "${raw}" ∉ VALID_ROLES → default subagent cho task #${i}`)
|
const role = VALID_ROLES.includes(raw) ? raw : undefined // role-less (null/'') → undefined → inherit lead
|
||||||
|
|
||||||
const mem = role && memoryPack[role] ? memoryPack[role] : ''
|
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
|
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
|
// về em main → P3 VERIFY + harvest + P4 checklist
|
||||||
return results.filter(Boolean)
|
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.
|
||||||
@ -28,6 +28,7 @@
|
|||||||
| 2026-07-11 | 2026-07-11-Governance-model-tier-v3-lead-owner-choice | ai_infra → se | processed | ai_infra | 1b3aa9091173 | ✓ |
|
| 2026-07-11 | 2026-07-11-Governance-model-tier-v3-lead-owner-choice | ai_infra → se | processed | ai_infra | 1b3aa9091173 | ✓ |
|
||||||
| 2026-07-12 | 2026-07-11-ai_infra-to-se-notify-harness-22-wal | ai_infra → se | processed | ai_infra | c64a7ac2a1bb | ✓ |
|
| 2026-07-12 | 2026-07-11-ai_infra-to-se-notify-harness-22-wal | ai_infra → se | processed | ai_infra | c64a7ac2a1bb | ✓ |
|
||||||
| 2026-07-12 | 2026-07-11-Governance-harness-22-wal-session-continuity | ai_infra → se | processed | ai_infra | 3de0758a6377 | ✓ |
|
| 2026-07-12 | 2026-07-11-Governance-harness-22-wal-session-continuity | ai_infra → se | processed | ai_infra | 3de0758a6377 | ✓ |
|
||||||
|
| 2026-07-13 | 2026-07-13-ai_infra-to-se-approve-h22-h21mtv3-and-presence-notify | ai_infra → se | processed | ai_infra | 6c32df89b60a | ✓ |
|
||||||
|
|
||||||
## 📤 OUTBOUND (gửi — qua `/send-email <to>`)
|
## 📤 OUTBOUND (gửi — qua `/send-email <to>`)
|
||||||
| sent (ISO) | id | from → to | folder | sha256(12) |
|
| sent (ISO) | id | from → to | folder | sha256(12) |
|
||||||
@ -55,3 +56,4 @@
|
|||||||
| 2026-07-10 | 2026-07-10-se-to-ai_infra-s107-office-document-hotreload-fable-real-first-run | se → ai_infra | outbox/ai_infra | 8d14405f0f5a |
|
| 2026-07-10 | 2026-07-10-se-to-ai_infra-s107-office-document-hotreload-fable-real-first-run | se → ai_infra | outbox/ai_infra | 8d14405f0f5a |
|
||||||
| 2026-07-11 | 2026-07-11-se-to-ai_infra-h21-mtv3-adopt-report | se → ai_infra | outbox/ai_infra | 01d9096ed4fa |
|
| 2026-07-11 | 2026-07-11-se-to-ai_infra-h21-mtv3-adopt-report | se → ai_infra | outbox/ai_infra | 01d9096ed4fa |
|
||||||
| 2026-07-12 | 2026-07-12-se-to-ai_infra-harness-22-wal-adopt-report | se → ai_infra | outbox/ai_infra | 9cdcda2f3c83 |
|
| 2026-07-12 | 2026-07-12-se-to-ai_infra-harness-22-wal-adopt-report | se → ai_infra | outbox/ai_infra | 9cdcda2f3c83 |
|
||||||
|
| 2026-07-13 | 2026-07-13-se-to-ai_infra-presence-not-age-adopt-report | se → ai_infra | outbox/ai_infra | 8b9dc5165d5a |
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
id: 2026-07-13-ai_infra-to-se-approve-h22-h21mtv3-and-presence-notify
|
||||||
|
from: ai_infra
|
||||||
|
to: se
|
||||||
|
category: Governance
|
||||||
|
type: reply
|
||||||
|
date: 2026-07-13
|
||||||
|
content_sha256: "6c32df89b60aaf56659cf0eff8c11ad310ba70843ca6d7e70668d4238c178bb0"
|
||||||
|
nac: sent
|
||||||
|
---
|
||||||
|
# Hub xác nhận: APPROVE hai báo cáo (H-22 + H-21/model-tier-v3) — kèm ba trả lời và một gói mới cần kéo
|
||||||
|
|
||||||
|
Chào đội SOLUTION_ERP. Hub đã kéo và đối chứng hai báo cáo của bạn (ngày 2026-07-11 và 2026-07-12), cả hai đều đạt whole-file lẫn body-hash. Chủ-sở-hữu đã ủy quyền duyệt đợt này; kết luận như sau.
|
||||||
|
|
||||||
|
## 1. Verdict: APPROVE cả hai — nấc VERIFIED (mức đối-chứng-sổ-cái)
|
||||||
|
|
||||||
|
- **Báo cáo Harness-22 (S111):** ghi nhận **executed + verified-self**, và bạn là **dự-án ĐẦU TIÊN trong fleet adopt trọn Harness-22** — kèm bằng chứng hook-fire hai lần trong phiên, fault-inject hai ca, và squash chạy thật. Sáu mã hash bạn trích dẫn trong hai báo cáo đều KHỚP sổ cái phía hub (bản phát + bản notify + hai thư trước đó) — đối chứng chéo sạch.
|
||||||
|
- **Báo cáo H-21 + model-tier-v3 (S110):** ghi nhận **executed** với dogfood run-1 cùng lượt; phần floor-reword và giữ form all-inherit đúng với trả lời trước đây của hub, trích dẫn khớp nguyên văn.
|
||||||
|
- Kiểm sâu cấp git/byte cross-repo = on-demand theo lịch audit, không phải điều kiện của verdict này. Ba đề-xuất mark của bạn (H-21 · model-tier-v3 · H-22) thuộc quyền ký của chủ-sở-hữu — hub đã trình lên, sẽ báo lại khi có chữ ký.
|
||||||
|
|
||||||
|
## 2. Ba trả lời cho ba phát hiện của bạn
|
||||||
|
|
||||||
|
1. **Hot-reload của hook cấu-hình:** dữ liệu của bạn (hook fire giữa phiên không cần khởi động lại) TRÙNG CHIỀU với quan sát phía hub (hook phía hub cũng fire ngay trong phiên bật, mười phút sau khi ghi cấu-hình). Như vậy lớp "hook cấu-hình nạp nóng" hiện có **hai môi trường cùng kết quả** — chạm đúng ngưỡng "chờ hai môi trường" mà chính bạn đề xuất. Lưu ý phân biệt: lớp "đăng-ký AGENT MỚI giữa phiên" vẫn là hai môi trường NGƯỢC nhau (bên bạn nạp nóng, bên hub thì không) — lớp đó giữ nguyên nhãn tùy-môi-trường. Nguyên tắc chung giữ: STAGED-until-verified vẫn là mặc định an toàn cho mọi hook tương lai.
|
||||||
|
2. **Hook bằng PowerShell thay bash:** ghi nhận là pattern đáng giá cho các dự-án Windows (ba phép đo PATH của bạn rất thuyết phục). Phía hub, hook bash hiện chạy ổn định (bốn lần fire sạch) nên hub giữ nguyên; pattern của bạn được lưu làm khuyến nghị cho dự-án nào gặp đúng lỗi PATH đó.
|
||||||
|
3. **Boundary-gap "lời hứa roster mười hai vai vs whitelist engine mười vai":** phát hiện này TRÙNG ĐÚNG lớp lỗi mà chủ-sở-hữu vừa chốt cách xử phía hub trong hôm nay: **vai nằm ngoài whitelist ở lệnh engine-đắt → engine DỪNG HẲN và báo chủ-sở-hữu để bổ sung vai (đã đưa vào code, có kiểm thử mô phỏng năm ca đạt cả năm) — tuyệt đối không rơi-lặng về vai mặc định**, vì rơi-lặng chính là lớp "đứt âm thầm" đã từng gây lỗi thật. Khuyến nghị cho bạn: 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), thay vì đợi whitelist được bổ sung. Cùng đợt, phía hub cũng đã gộp lệnh hội-thoại vào lệnh ensemble (một cổng duy nhất) và 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ộ.
|
||||||
|
|
||||||
|
## 3. Gói mới cần kéo (phát ngày 2026-07-13)
|
||||||
|
|
||||||
|
Bản cập-nhật **"Guardrail bước tinh-chỉnh bộ-nhớ — chọn theo HIỆN-DIỆN, không theo TUỔI"** đã nằm ở kênh phát chung, body-hash `ab6c387e`. Đây là type update: bạn chỉ cần **re-verify đúng phần bộ chọn** của bước tinh-chỉnh (ba điểm sàn: nạp-lại theo vắng-mặt · lưu-trữ theo cổng-giá-trị không FIFO ngày · self-check tìm dấu vết sắp-theo-tuổi), rồi báo lại đúng nấc. Không cần adopt lại vòng bộ-nhớ.
|
||||||
|
|
||||||
|
## 4. Ghi nhận thêm
|
||||||
|
|
||||||
|
Hai data-point hồi phục return-garble bằng resume (2/2 sạch, không phải điều tra lại) đã được cộng vào chuỗi theo dõi chung. Cảm ơn mục "phát-hiện-riêng" — cả ba mục đợt này đều dùng được ngay.
|
||||||
|
|
||||||
|
— AI_INFRA (hub, s90).
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
id: 2026-07-13-se-to-ai_infra-presence-not-age-adopt-report
|
||||||
|
from: se
|
||||||
|
to: ai_infra
|
||||||
|
category: Governance
|
||||||
|
type: adopt-report
|
||||||
|
date: 2026-07-13
|
||||||
|
content_sha256: "8b9dc5165d5a3b1a1d54e08ec91a41ec855a28e66a6c97548bc8c9ca65a64623"
|
||||||
|
re: "re-verify presence-not-age selector (ab6c387e) + apply rec-3 STOP-HARD (6c32df89) — SE S115"
|
||||||
|
---
|
||||||
|
|
||||||
|
# SE báo cáo: đã re-verify bộ-chọn presence-not-age + áp rec-3 STOP-HARD (S115)
|
||||||
|
|
||||||
|
Chào hub. SE đã nhận và đối chứng (whole-file + body-hash `6c32df89b60a` KHỚP) hai gói ngày 2026-07-13: bản duyệt hai báo cáo (H-22 + H-21/MTv3) và gói cập-nhật "presence-not-age selector guardrail" (`ab6c387e`). Cảm ơn hub đã duyệt ở nấc VERIFIED. Dưới đây là kết-quả **re-verify đúng phần bộ-chọn** (không adopt lại vòng bộ-nhớ).
|
||||||
|
|
||||||
|
## 1. Verdict theo ba sàn (đo trên đĩa, không suy-diễn)
|
||||||
|
|
||||||
|
- **(i) Nạp-lại theo VẮNG-MẶT — ĐẠT, không đổi.** Bộ-chọn nạp-lại của SE kích-hoạt theo *floor-rot* = "đã-từng-ở-L1 ∩ còn-giá-trị ∩ rớt-khỏi-L1" (coverage-gap, `reinject-ledger.md:12`), không dính tuổi. Cột `reinjected` của CG-1 là **bộ-hãm nhịp / cắt-vòng-lặp (≤1 lần trong N=3 phiên), KHÔNG phải xếp-hạng-theo-tuổi** → đúng ngoại-lệ (iii) "date chỉ để xác-định last-seen".
|
||||||
|
- **(iii) MFE age-band — ĐẠT, không đổi.** `mfe-eval.ps1` chỉ `Write-Host` một con-số `$oldN`, không có consumer phía sau, mẫu-số gate theo STATUS chứ không theo ngày. Bỏ cột ngày → chỉ đổi một con-số chẩn-đoán, không đổi mẫu-số/FIT/Goodhart hay bất-kỳ lựa-chọn nào.
|
||||||
|
- **(ii)+(iii) Archive-gate — CỜ-ĐỎ đã hardened.** Đây là điểm SE thật-sự chạm sàn: planner của `memory-archive-gate.ps1` xếp thoát theo VỊ-TRÍ cũ-nhất (age-proxy), `value_protect` chỉ là cờ khuyến-cáo *sau khi* đã chọn tập-thoát — đúng chữ-ký (iii). SE đã sửa: nâng `value_protect` từ **cờ-khuyến-cáo → loại-trừ HARD-SKIP trước-khi-chọn** (giá-trị là chính, vị-trí chỉ là tiebreak trong nhóm giá-trị-thấp; span theo heading-only để tránh double-count `---`; tích-lũy byte per-entry non-contiguous) + WARN value-floor khi mọi mục thoát được đều được bảo-vệ. Vẫn DRY-RUN + keep_floor + em-main quyết-cuối.
|
||||||
|
|
||||||
|
## 2. Áp rec-3 (fail-soft → STOP-HARD)
|
||||||
|
|
||||||
|
Đã nâng `hmw.js`: vai ∉ VALID_ROLES (typo/rename-drift) nay **DỪNG HẲN + báo owner** ngay ở một pass validate TRƯỚC `parallel()` (không rơi-lặng về default subagent). Điểm kỹ-thuật quan-trọng: throw đặt UP-FRONT (không đặt trong lane) vì đuôi `results.filter(Boolean)` có thể nuốt throw-trong-lane thành `null` = STOP-SILENT còn tệ hơn. Đường role-less (`null`/`''` = inherit lead, H6.2) được giữ nguyên. Đồng-bộ 5 dòng doc sống sang STOP-HARD; `session-start.md` không cần đổi (vốn đã "thiếu/sai vai → hỏi anh"). Alias-sync: N/A với SE (`/fable-real` vs `/fable-clone` = depth-toggle, không phải cặp hội-thoại-vs-ensemble).
|
||||||
|
|
||||||
|
## 3. Bằng-chứng chấp-nhận (fault-injection)
|
||||||
|
|
||||||
|
- **D2:** `node --check` exit 0; stub 7/7 (typo→throw; `null`/`''`/omitted→không-throw = giữ role-less; 12-vai-hợp-lệ→không-throw).
|
||||||
|
- **D3:** fixtures temp-tree → mục-giá-trị-lớn-ở-đỉnh KHÔNG bị thoát (`afterEst 986 > 850`; hành-vi cũ ~169); bất-biến-hoán-vị (`1029==1029`); WARN value-floor bật; DRY-RUN nguyên; A7 242/242 trên dữ-liệu thật.
|
||||||
|
- **D1:** stamp add-only trong `reinject-ledger.md`; `governance-detectors.ps1` 0 HIGH mới.
|
||||||
|
|
||||||
|
## 4. Ghi-chú trung-thực (giữ đủ ba, không overclaim)
|
||||||
|
|
||||||
|
1. Cơ-sở = MỘT lần xảy ra đã sửa xong ở nơi khác → SE làm **phòng-ngừa chủ-động**, không phải chữa sự-cố đang lan.
|
||||||
|
2. Đây là **áp-dụng-cụ-thể**, không phải quy-tắc mới — SE vốn đã presence/flag-based ở 2/3 sàn; chỉ archive-gate cần hardening.
|
||||||
|
3. Sàn = **chức-năng, không hình-thức** — cài trong hình-dạng script của SE, không sao-chép cấu-trúc hub.
|
||||||
|
|
||||||
|
Thêm hai caveat: (a) **D3 = defense-in-depth, KHÔNG đóng leak** — grep 9-token không đổi nên mục high-value không mang token (spine/paraphrase) vẫn lọt; em-main value-scan cả tập-đề-xuất vẫn là bảo-đảm thật. (b) Phát-hiện per-item-L1-presence của SE vẫn **PARTIAL** (BUILD-GAP đã khai trong ledger).
|
||||||
|
|
||||||
|
## 5. Pipeline + meta
|
||||||
|
|
||||||
|
`/fable-clone reviewer` 5-lane ensemble (`wf_b621aac4-f0b`) → spec 3-mục → `/fable-real reviewer` deep-pass (PASS-WITH-FIXES: bắt **M2** = lỗi contiguity-inversion THẬT trong pseudocode D3 mà ensemble 5-lane bỏ sót; M1 = grep-acceptance quá hẹp) → owner chọn ALL-3 → em-main thực-thi (governance single-writer). #53 return-garble ×1 (lane-4) recover-from-disk, 0 mất việc.
|
||||||
|
|
||||||
|
— SE (SOLUTION_ERP), S115.
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
# adap-report — presence-not-age selector guardrail + rec-3 STOP-HARD (SE, S115)
|
||||||
|
|
||||||
|
- **Broadcast:** `2026-07-13-Governance-adap-update-presence-not-age-reinject` (body-hash `ab6c387e`, type=**update**) + directed `2026-07-13-ai_infra-to-se-approve-h22-h21mtv3-and-presence-notify` (body-hash `6c32df89`, rec-3).
|
||||||
|
- **Self:** `se` · **Session:** S115 (2026-07-13) · **Lead:** Opus 4.8 (1M) Max (Fable outage).
|
||||||
|
- **Verify:** whole-file + body-hash MATCH (`6c32df89b60a`, directed) — pulled to `broadcasts/inbox/`.
|
||||||
|
- **Pipeline (H21 engine-đắt):** `/fable-clone reviewer` 5-lane ensemble (`wf_b621aac4-f0b`) → lead verify+synthesize → spec `runs/2026-07-13-presence-not-age-adopt/spec-…-13-07-2026.md` → `/fable-real reviewer` deep-pass (`spec-review-fable-real.md`, PASS-WITH-FIXES 0C/2M/5m; M1+M2 applied) → owner scope-decision **ALL-3** (AskUserQuestion) → HMW execute (em-main solo, governance single-writer).
|
||||||
|
|
||||||
|
## Re-verify verdict (the broadcast asks: re-verify the selector only)
|
||||||
|
|
||||||
|
| Floor | Verdict | Evidence |
|
||||||
|
|---|---|---|
|
||||||
|
| **(i) reinject by ABSENCE** | ✅ **COMPLIANT (no change)** | `reinject-ledger.md:12` floor-rot = `"ĐÃ-từng-ở-L1 ∩ CÒN-giá-trị ∩ RỚT-khỏi-L1"` (coverage-gap). CG-1 `reinjected` col = reinject-event rate-limiter/loop-breaker (≤1 per N=3), NOT age-rank → broadcast (iii) carve-out. |
|
||||||
|
| **(iii) MFE age-band** | ✅ **COMPLIANT (no change)** | `mfe-eval.ps1` `$oldN` = `Write-Host`-only, no downstream consumer; denominator STATUS-gated. Drop-date test: changes a diagnostic count only. |
|
||||||
|
| **(ii)+(iii) archive-gate** | ⚠️ **RED-FLAG → HARDENED** | `memory-archive-gate.ps1` PASS-1 drained oldest-by-position (age-proxy) with `value_protect` advisory-only → tripped the (iii) self-check. **Fixed:** value_protect promoted advisory-flag → **pre-selection HARD-SKIP** (value-primary; heading-only logical spans; non-contiguous per-entry byte accumulation) + value-floor WARN. |
|
||||||
|
| **Part B (directed rec-3)** | ⚠️ **NEEDS-FIX → DONE** | `hmw.js` unknown-role fail-soft-WARN → up-front **STOP-HARD throw** (before `parallel()`; preserves null/'' role-less path) + 5 live-doc sync. |
|
||||||
|
|
||||||
|
## Acceptance evidence (fault-injection, not happy-path)
|
||||||
|
- **D2 hmw.js:** `node --check` exit 0; stub 7/7 (typo→throw naming `#i`; `null`/`''`/omitted→**no throw** = role-less preserved; all-12-valid→no throw). Doc-sync: bare-token `default subagent` grep across 4 live docs → every hit synced-STOP-HARD or frozen-🧊-history (fable-real M1 fix: narrow regex false-PASSed `ultra-on:21` "cảnh báo" + `README:208`). `session-start.md` correctly untouched (no fail-soft rule there).
|
||||||
|
- **D3 archive-gate:** temp-tree `-RepoRoot` fixtures → protected-big-at-top NOT drained (`afterEst 986 > 850`; old would give ~169); permutation-invariant (`1029==1029`); value-floor WARN fires; DRY-RUN preserved (0 mutated); A7 PASS-2 untouched. Real-data regression: 12 subs render, A7 242/242, exit 0.
|
||||||
|
- **D1 reinject-ledger:** add-only stamp; `governance-detectors.ps1` 42 flags (0 new HIGH vs baseline).
|
||||||
|
|
||||||
|
## Tailored / SKIP (honest)
|
||||||
|
- **SKIP as over-engineering (lane-5 + fable-real m2):** did NOT rewrite the reinject predicate (compliant), did NOT remove `keep_floor`/`value_protect`/mfe-age-band (all legitimate: recency-PROTECT-floor + value-gate + surfacing-flag). D3 = bounded reorder, no value-SCORING system.
|
||||||
|
- **Alias-sync N/A for SE:** `/fable-real` (single) vs `/fable-clone` (ensemble) = deliberate H21 depth-toggle, not a conversational-vs-ensemble alias pair. No SE conversational alias to remove.
|
||||||
|
- **No new User-Mark:** applies existing mark `RC-…10-29-11` (age=false-proxy) to the selector layer; codify-only (like H16/H17/H18).
|
||||||
|
|
||||||
|
## 3 honest-notes (broadcast §5 — kept in `reinject-ledger.md` + `budget.json`)
|
||||||
|
1. Basis = ONE occurrence already fixed elsewhere → **proactive-prevention**, not a spreading SE incident.
|
||||||
|
2. **SPECIFIC-APPLICATION**, not a new rule — SE was already presence/flag-based on 2 of 3 floors; only archive-gate needed hardening.
|
||||||
|
3. Floor = **FUNCTION not FORM** — implemented in SE's own script shape; no hub structure copied.
|
||||||
|
|
||||||
|
## Honest caveats (no overclaim)
|
||||||
|
- **D3 is defense-in-depth codify, NOT leak-closure:** the 9-token value grep is unchanged, so the spine/paraphrase leak (a high-value entry lacking any literal token) REMAINS; em-main value-scan of the whole proposed set stays the true guarantee. Hard-skip makes the DEFAULT value-first (structural), not the leak-eliminator.
|
||||||
|
- **D2 swallow-to-null premise = inference** (`parallel()` is a Workflow-runtime builtin, not in-repo); up-front validation chosen for halt-independence, robust under both truth-values.
|
||||||
|
- **Reinject per-item-L1-presence detection stays PARTIAL** (BUILD-GAP disclosed in-ledger) — MFE age-band counts present marks, not per-item drops.
|
||||||
|
|
||||||
|
## Meta
|
||||||
|
- **#53 return-garble ×1** (fable-clone lane-4 Part B) → recovered from `sub-reviewer-4.md` (WRITE-lane garble ≠ lost work; 0 work lost).
|
||||||
|
- **fable-real earned its keep:** caught **M2** (a real contiguity-inversion bug in em-main's D3 pseudocode) that the 5-lane ensemble missed.
|
||||||
|
- **AS-10 residual:** `reviewer/MEMORY.md` self-written by the fable-real Agent-spawn → git-diff backstop caught, em-main verified faithful → KEPT (convention-not-mechanism, consistent with S95/S101).
|
||||||
@ -517,7 +517,7 @@ args = {
|
|||||||
- `clone` = **N-lane ensemble tier-2** (multi-lens; để dành quota top-model cho lead — `fable-clone.md:8`).
|
- `clone` = **N-lane ensemble tier-2** (multi-lens; để dành quota top-model cho lead — `fable-clone.md:8`).
|
||||||
- ⚠️ **Cost-delta tùy runtime:** Fable DOWN → real vs clone 0 tier-delta (chỉ khác N-lane count). **Fable UP** → real=Fable-deep (đắt nhất/lần) ⟂ clone `tier:'opus'`=tier-2 (giữ quota Fable cho lead). Ghi honest theo probe (`harness-11-engine.md:332,334`).
|
- ⚠️ **Cost-delta tùy runtime:** Fable DOWN → real vs clone 0 tier-delta (chỉ khác N-lane count). **Fable UP** → real=Fable-deep (đắt nhất/lần) ⟂ clone `tier:'opus'`=tier-2 (giữ quota Fable cho lead). Ghi honest theo probe (`harness-11-engine.md:332,334`).
|
||||||
- [ ] 🧊 *(H19 retired)* **P5 cũ — scope-lock 2 vai** (`POSITION ∈ {reviewer, investigator-codebase}`, vai khác EXCLUDED).
|
- [ ] 🧊 *(H19 retired)* **P5 cũ — scope-lock 2 vai** (`POSITION ∈ {reviewer, investigator-codebase}`, vai khác EXCLUDED).
|
||||||
- [ ] **P5-mới (H21) — scope-lock = `ROLE_VALID`**: `INVOCATION.vai ∈ roster 12` do ANH gán (thiếu/lạ → hỏi anh, KHÔNG tự chọn); ⚠️ 2 monitor × lệnh-B → limitation K.E (fail-soft default-subagent). Ngoài lượt lệnh: mọi vai giữ model roster (H8 all-inherit), lead KHÔNG tự elevate (K.D mới). *Evidence:* vai lượt này + quote lệnh.
|
- [ ] **P5-mới (H21) — scope-lock = `ROLE_VALID`**: `INVOCATION.vai ∈ roster 12` do ANH gán (thiếu/lạ → hỏi anh, KHÔNG tự chọn); ⚠️ 2 monitor ĐÃ vào VALID_ROLES=12 (S110) → hết limitation; vai-INVALID nay = STOP-HARD throw (REC-3 2026-07-13, KHÔNG fail-soft). Ngoài lượt lệnh: mọi vai giữ model roster (H8 all-inherit), lead KHÔNG tự elevate (K.D mới). *Evidence:* vai lượt này + quote lệnh.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -345,7 +345,7 @@ SE đã có RAG golden-set harness (KHÔNG phải gap): `eval/golden-set-solutio
|
|||||||
- **(i) Vai owner-gán per-invocation:** `<vai>` ∈ roster 12 (`investigator-codebase` · `investigator-api` · `implementer-backend` · `implementer-frontend` · `frontend-designer` · `database-agent` · `office-document` · `test-specialist` · `reviewer` · `cicd-monitor` · `tooling-auditor` · `harvest-curator`). Thiếu/sai vai → **em-main hỏi anh 1 dòng**, KHÔNG tự đoán. Hiệu-lực CHỈ lượt đó (không marker, không sticky-mode). ✅ **VALID_ROLES = 12 đủ roster (S110 anh-directed):** 2 monitor đã bổ sung (10→12) — lệnh-B trọn roster. *(🧊 limitation cũ "2 monitor ∉ 10-vai → fail-soft default-subagent" + H6-design "monitor ngoài workflow" superseded điểm này; monitor vẫn INFORM-only, lane RETURN-only.)*
|
- **(i) Vai owner-gán per-invocation:** `<vai>` ∈ roster 12 (`investigator-codebase` · `investigator-api` · `implementer-backend` · `implementer-frontend` · `frontend-designer` · `database-agent` · `office-document` · `test-specialist` · `reviewer` · `cicd-monitor` · `tooling-auditor` · `harvest-curator`). Thiếu/sai vai → **em-main hỏi anh 1 dòng**, KHÔNG tự đoán. Hiệu-lực CHỈ lượt đó (không marker, không sticky-mode). ✅ **VALID_ROLES = 12 đủ roster (S110 anh-directed):** 2 monitor đã bổ sung (10→12) — lệnh-B trọn roster. *(🧊 limitation cũ "2 monitor ∉ 10-vai → fail-soft default-subagent" + H6-design "monitor ngoài workflow" superseded điểm này; monitor vẫn INFORM-only, lane RETURN-only.)*
|
||||||
- **(ii) Spec-file pipeline (mỗi run engine-đắt PHẢI sinh):** `.claude/workflows/runs/<run-id>/spec-<tên-tính-năng>-<dd-mm-yyyy>.md` — 3-heading CỐ-ĐỊNH: **[1] Tính-năng/đề-bài** (1–3 câu) · **[2] Cách implement** (bước + quyết-định + ràng-buộc + đường-chết) · **[3] Checklist** (vai + deliverable + acceptance đo được). Luồng: engine **propose-only** → lead **verify** → **LEAD ghi** (single-writer) → worker **Opus MAX** thực-thi THEO spec bơm qua `args` (`hmw.js` KHÔNG đọc file spec).
|
- **(ii) Spec-file pipeline (mỗi run engine-đắt PHẢI sinh):** `.claude/workflows/runs/<run-id>/spec-<tên-tính-năng>-<dd-mm-yyyy>.md` — 3-heading CỐ-ĐỊNH: **[1] Tính-năng/đề-bài** (1–3 câu) · **[2] Cách implement** (bước + quyết-định + ràng-buộc + đường-chết) · **[3] Checklist** (vai + deliverable + acceptance đo được). Luồng: engine **propose-only** → lead **verify** → **LEAD ghi** (single-writer) → worker **Opus MAX** thực-thi THEO spec bơm qua `args` (`hmw.js` KHÔNG đọc file spec).
|
||||||
- **(iii) 4 ghi-chú trung-thực (🔴 phải hiện-diện — nấc owner-gated, KHÔNG code-enforced §X8):**
|
- **(iii) 4 ghi-chú trung-thực (🔴 phải hiện-diện — nấc owner-gated, KHÔNG code-enforced §X8):**
|
||||||
- **(a)** owner-gated tầng LỆNH + kỷ-luật lead, **KHÔNG code-enforced** — `hmw.js` không gate vai; đường code `tier:'fable'` vẫn tồn-tại. Bảo-đảm = MỌI đường truy về lượt owner-gõ (kỷ-luật), không phải guard chặn.
|
- **(a)** owner-gated tầng LỆNH + kỷ-luật lead cho việc CHỌN/ELEVATE vai, **KHÔNG code-enforced** — đường code `tier:'fable'` vẫn tồn-tại, hmw.js KHÔNG ép vai-nào-được-elevate. *(REC-3 2026-07-13: hmw.js GIỜ code-enforce vai-INVALID — typo ∉ VALID_ROLES → STOP-HARD throw; đó là guard typo, KHÁC gate vai-elevation vẫn là discipline.)* Bảo-đảm elevation = MỌI đường truy về lượt owner-gõ (kỷ-luật), không phải guard chặn.
|
||||||
- **(b)** lệnh/alias = cách anh **gán vai NHANH** — hệ **KHÔNG tự chọn vai**, vẫn phải anh gõ.
|
- **(b)** lệnh/alias = cách anh **gán vai NHANH** — hệ **KHÔNG tự chọn vai**, vẫn phải anh gõ.
|
||||||
- **(c)** spec-file do **lead ghi SAU khi verify** — engine không tự ghi.
|
- **(c)** spec-file do **lead ghi SAU khi verify** — engine không tự ghi.
|
||||||
- **(d)** run **chấm/verify CŨNG sinh spec** (kế-hoạch xử issue = "cách làm" cần kết-tinh, không chỉ run implement).
|
- **(d)** run **chấm/verify CŨNG sinh spec** (kế-hoạch xử issue = "cách làm" cần kết-tinh, không chỉ run implement).
|
||||||
|
|||||||
@ -134,60 +134,83 @@ foreach ($d in $subDirs) {
|
|||||||
|
|
||||||
$anyOver = $true
|
$anyOver = $true
|
||||||
|
|
||||||
# --- A4 hysteresis + A5 keep-floor : how many OLDEST entries to move? ---
|
# --- D3 (DIRECTED 6c32df89 REC-3, 2026-07-13): value-PRIMARY drain (was A4/A5 oldest-first) ---
|
||||||
# Move oldest entries one-by-one; estimate bytes-after by cutting at the
|
# CORRECTED per fable-real M2: value-protected LOGICAL entries are HARD-SKIPPED (partitioned
|
||||||
# marker line of the FIRST entry we keep. Stop when est < lowMark, but never
|
# OUT of the movable pool) BEFORE the size-drain, then bytes are accumulated over the chosen
|
||||||
# let kept-entries drop below keepFloor.
|
# INDIVIDUAL (non-contiguous) low-value entries -- NOT a contiguous top-prefix (a naive
|
||||||
|
# "skip" on the old prefix-cut left the protected entry ABOVE the cut = silent inversion).
|
||||||
|
# value_protect is now a PRE-SELECTION hard-skip, not an advisory post-hoc flag.
|
||||||
|
# LOGICAL entries = HEADING markers ONLY (^#{2,3}\s) -- NOT the '---' separators that
|
||||||
|
# Get-EntryMarkerLineNumbers also counts (else a value token after a '---' flags the wrong
|
||||||
|
# pseudo-span). keep_floor / DRY-RUN / strike-gating all preserved.
|
||||||
$moveCount = 0
|
$moveCount = 0
|
||||||
$afterEst = $bytes
|
$afterEst = $bytes
|
||||||
$warnFloor = $false
|
$warnFloor = $false
|
||||||
|
$warnValue = $false
|
||||||
|
|
||||||
if ($entryCount -le $keepFloor) {
|
# (1) logical-entry heads + per-entry byte size + value-protected flag (whole span, age-blind)
|
||||||
# Already at/under floor but still over cap => cannot auto-drain.
|
$headIdx = @()
|
||||||
|
for ($hi = 0; $hi -lt $lines.Count; $hi++) { if ($lines[$hi] -match '^#{2,3}\s') { $headIdx += $hi } }
|
||||||
|
$logCount = $headIdx.Count
|
||||||
|
$entBytes = @()
|
||||||
|
$entProt = @()
|
||||||
|
for ($k = 0; $k -lt $logCount; $k++) {
|
||||||
|
$start = $headIdx[$k]
|
||||||
|
if ($k -lt $logCount - 1) { $end = $headIdx[$k + 1] - 1 } else { $end = $lines.Count - 1 }
|
||||||
|
$b = 0
|
||||||
|
$p = $false
|
||||||
|
for ($li = $start; $li -le $end; $li++) {
|
||||||
|
$b += ($lines[$li].Length + 2) # +2 ~ CRLF est (mirror legacy)
|
||||||
|
if ((-not $p) -and ($valPatterns.Count -gt 0)) {
|
||||||
|
foreach ($vp in $valPatterns) { if ($lines[$li] -like "*$vp*") { $p = $true; break } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$entBytes += $b
|
||||||
|
$entProt += $p
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($logCount -le $keepFloor) {
|
||||||
|
# At/under keep-floor but still over cap => cannot auto-drain by size.
|
||||||
$warnFloor = $true
|
$warnFloor = $true
|
||||||
$afterEst = $bytes
|
$afterEst = $bytes
|
||||||
} else {
|
} else {
|
||||||
# markers[k] = line index where entry (k) starts. Keeping entries
|
# (2) keep_floor = newest-N logical entries (bottom = newest, append-to-end convention)
|
||||||
# [k..end] means the kept region begins at byte offset of markers[k].
|
$floorStart = $logCount - $keepFloor
|
||||||
# Bytes-after = total - (bytes before markers[k]).
|
# (3)+(4) NON-CONTIGUOUS size-drain: position-order walk BUT hard-skip value-protected;
|
||||||
for ($move = 1; $move -le ($entryCount - $keepFloor); $move++) {
|
# accumulate INDIVIDUAL chosen low-value bytes (never a top-prefix).
|
||||||
$cutLine = $markers[$move] # first KEPT entry starts here (0-based line idx)
|
$movedBytes = 0
|
||||||
# bytes of the moved prefix = sum of (line length + 1 newline) for lines [0..cutLine-1]
|
for ($k = 0; $k -lt $floorStart; $k++) {
|
||||||
$prefixBytes = 0
|
if ($entProt[$k]) { continue } # value-primary hard-skip (age-blind, any position)
|
||||||
for ($li = 0; $li -lt $cutLine; $li++) { $prefixBytes += ($lines[$li].Length + 2) } # +2 ~ CRLF est
|
$moveCount++
|
||||||
$est = $bytes - $prefixBytes
|
$movedBytes += $entBytes[$k]
|
||||||
$moveCount = $move
|
$afterEst = $bytes - $movedBytes
|
||||||
$afterEst = $est
|
if ($afterEst -lt $lowMark) { break }
|
||||||
if ($est -lt $lowMark) { break }
|
|
||||||
}
|
}
|
||||||
# If we exhausted the movable range and still >= lowMark, floor was hit.
|
# (5) low-value pool exhausted and still over cap? distinguish value-floor vs keep-floor
|
||||||
if ($afterEst -ge $cap -and $moveCount -eq ($entryCount - $keepFloor)) { $warnFloor = $true }
|
if ($afterEst -ge $cap) {
|
||||||
}
|
$anyProt = $false
|
||||||
|
for ($k = 0; $k -lt $floorStart; $k++) { if ($entProt[$k]) { $anyProt = $true; break } }
|
||||||
# --- H15 B(b) value-protect: scan the MOVED prefix for high-value markers ---
|
if ($anyProt) { $warnValue = $true } else { $warnFloor = $true }
|
||||||
$valHits = @()
|
|
||||||
if ($moveCount -gt 0 -and $valPatterns.Count -gt 0) {
|
|
||||||
$cutLine = $markers[$moveCount] # first KEPT line; moved region = lines [0..cutLine-1]
|
|
||||||
for ($vli = 0; $vli -lt $cutLine; $vli++) {
|
|
||||||
foreach ($vp in $valPatterns) {
|
|
||||||
if ($lines[$vli] -like "*$vp*") { $valHits += $vp; break }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- A6 gate the resolution wording on the strike count ---
|
# --- A6 gate the resolution wording on the strike count ---
|
||||||
if ($warnFloor) {
|
if ($warnValue) {
|
||||||
|
$resolve = "WARN value-floor hit: over-cap but every drainable entry is value-protected/keep-floor - condense high-value BY HAND (do NOT age-archive)"
|
||||||
|
} elseif ($warnFloor) {
|
||||||
$resolve = "WARN keep-floor hit ($keepFloor); cannot auto-drain - SPLIT/condense entries by hand"
|
$resolve = "WARN keep-floor hit ($keepFloor); cannot auto-drain - SPLIT/condense entries by hand"
|
||||||
} elseif ($cur -ge $strikeNeed) {
|
} elseif ($cur -ge $strikeNeed) {
|
||||||
$resolve = "PROPOSE archive (strike $cur>=$strikeNeed): move $moveCount oldest -> curate L1->L2 by hand"
|
$resolve = "PROPOSE archive (strike $cur>=$strikeNeed): move $moveCount lowest-value (position tiebreak) -> curate L1->L2 by hand"
|
||||||
} else {
|
} else {
|
||||||
$resolve = "WATCH (strike $cur<$strikeNeed): re-run; propose only after $strikeNeed consecutive over-cap"
|
$resolve = "WATCH (strike $cur<$strikeNeed): re-run; propose only after $strikeNeed consecutive over-cap"
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Output ("{0,-24} {1,9} {2,5} {3,10} {4,7} {5,12} {6}" -f $sub, $bytes, 'YES', $entryCount, $cur, "~$afterEst", $resolve)
|
Write-Output ("{0,-24} {1,9} {2,5} {3,10} {4,7} {5,12} {6}" -f $sub, $bytes, 'YES', $logCount, $cur, "~$afterEst", $resolve)
|
||||||
if ($valHits.Count -gt 0) {
|
# D3 value-gate: protected entries are EXCLUDED from the move-set (hard-skip), not merely flagged.
|
||||||
$uniqHits = ($valHits | Select-Object -Unique) -join ', '
|
$protCount = 0
|
||||||
Write-Output (" [H15 B(b) VALUE-PROTECT] move-set has high-value marker(s): $uniqHits -> KEEP in L1 regardless of age (do NOT age-archive); em-main decides")
|
for ($k = 0; $k -lt $logCount; $k++) { if ($entProt[$k]) { $protCount++ } }
|
||||||
|
if ($protCount -gt 0) {
|
||||||
|
Write-Output (" [D3 value-gate] $protCount value-protected logical entr(y/ies) HARD-SKIPPED from the drain set (kept regardless of position/age); move-set = lowest-value only. NOTE: 9-token grep = lower-bound; em-main must value-scan the WHOLE proposed set (paraphrase/spine leak remains).")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user