// hmw.js — HMW P2 (Execute) workflow cho SOLUTION_ERP. CHẠY bởi Workflow runtime (body wrap async →
// top-level await/return hợp lệ); KHÔNG node-runnable trực tiếp (`node hmw.js` sẽ lỗi await).
// Em main lo P0/P1/P3/P4 NGOÀI workflow; script này CHỈ lo P2 fan-out.
// Invoke bằng {scriptPath} (no hot-reload — restart/re-invoke sau khi sửa). Scope = repo SOLUTION_ERP ONLY (S1).
// ⚠️ Script chạy JS-sandbox KHÔNG filesystem → KHÔNG tự tạo folder/ghi file. Scaffold run-folder runs/<run-id>/ (TRACKED) = EM MAIN @P1 (Harness-10, supersedes Harness 2 B3 wave-folder).

export const meta = {
  name: 'hmw',
  description: 'HMW P2 execute (SOLUTION_ERP) — fan-out roster ĐẦY-ĐỦ (số vai canonical → docs/STATUS.md §Sub-agents; KHÔNG hardcode số ở đây — lesson S108 comment stale làm worker đọc sai runtime) có MEMORY-PACK slice (qua args vì script không đọc file) + return findings + checklistEvidence + memoryDelta (spawn-record 4-field). 2 MODE (Harness 2, 06-07): (A) DEFAULT return-delta-only — fan-out NHỎ (<3 task VÀ ≤5 phút), sub KHÔNG ghi file, git-diff verify. (B) RUN-TRACE mode (args.run, Harness-10) — 🔴 BẮT BUỘC khi run ≥3 task HOẶC ước >5 phút (sàn H22 S111; script no-fs KHÔNG tự ép — enforcement = em-main scaffold @P1 + set args.run) — workflow DÀI, em main scaffold .claude/workflows/runs/<run-id>/ TRACKED FLAT (run.md + sub-<role>-<i>.md + <stage>-synthesis.md phẳng cùng cấp) @P1, sub ghi full-detail vào CHỈ sub-<role>-<i>.md mình (B4/B6), harvest per-turn primary (C4) + H2 gom @session-end = backstop verify-idempotent. taskList thoải mái (queue theo slot, không cap cứng). memoryDelta KHÔNG tự ghi — em main VERIFY + APPEND-only @P3 (no-overwrite-unverified, B3). Model worker-tier-pin (owner-decision S124): role-có-frontmatter = opus+effort:max (worker-tier, KHÔNG inherit) · role-less inherit lead · per-task tier:\'fable\'|\'opus\' escape-hatch (fable qua /fable-real; H23-precedence ĐO S126 2026-07-16: spawn-param THẮNG frontmatter hard-pin ⇒ /fable-real SỐNG). Scope = repo SOLUTION_ERP ONLY (S1 — KHÔNG fan-out repo/corpus khác).',
  phases: [{ title: 'Execute', detail: 'fan-out memory-pack-injected agents, structured return' }],
}

// ─── args (em main bơm @P2 sau khi P1 chốt) ──────────────────────────────────
// args = {
//   memoryPack: { 'investigator-codebase':'<slice>', 'implementer-backend':'...', reviewer:'...', ... },  // SLICE liên quan, KHÔNG full
//   spec:       '<acceptance-criteria / context chung>',
//   checkpointApproved: true,   // em main set SAU khi BÁO {số agent·vai·task} @inform (S2)
//   taskList:   [ { role:<VALID_ROLES|null>, label:'..', prompt:'..', tier:'fable'|'opus'? }, ... ]  // tier = per-task model override (H4.5)
//   run:        { name:'<run-id>', dir:'.claude/workflows/runs/<run-id>' }  // 🔴 BẮT BUỘC khi run ≥3 task HOẶC ước >5 phút (sàn H22 S111 — chết giữa run còn vật-liệu đĩa cho /tiep; dưới ngưỡng = tùy chọn) — bật RUN-TRACE mode (B, Harness-10 TRACKED). Folder + run.md em main ĐÃ scaffold @P1 (script no-fs). [legacy alias: args.wave]
// }

const VALID_ROLES = [
  '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; read-advisory DB lens (DB1-DB11)
  'office-document', // +S107 — Office document WRITE specialist (OD1-OD10; docx/xlsx/pptx/pdf + form-engine)
  'tooling-auditor', 'harvest-curator', // +S110 anh-directed (H21 roster-parity lệnh-B ensemble; supersede
  //   điểm H6-design "monitor ngoài workflow" — monitor VẪN INFORM-only, lane RETURN-only nên không xung đột)
  'lead-stale-auditor', 'lead-gap-auditor', // +S121 W2 (H24 lead-self-audit) — 2 monitor MỚI, INFORM-only propose-only.
  //   🔴 KHÔNG gộp vào tooling-auditor(H1)/harvest-curator(H2): H24 §2(1) soi LEAD (view-* = lead nhìn sai / gap-* =
  //   lead bỏ sót), khác trục H1 (tooling-freshness) và H2 (harvest-integrity). Class-flag = enum ĐÓNG ở
  //   memory-budget.json → lead_self_audit.flag_classes — vai KHÔNG tự chế class. 🔴 KHÔNG chép SỐ lớp vào đây (B1):
  //   đếm bằng chính khoá đó, đừng tin comment. Câu cũ chép cứng một con số lớp và SAI CẢ TỔNG LẪN THÀNH PHẦN —
  //   lớp `view-claim-broader-than-sample` nằm CUỐI mảng, ngoài cụm `view-*` liền kề, nên đếm lướt bị hụt.
  //   Từ-vựng "view"/"omission" chọn CÓ CHỦ-ĐÍCH tránh va-chạm: governance-detectors.ps1:15 đã sở-hữu
  //   "derived-staleness" ⇒ cấm tên *-stale-role; "GAP" quá-tải (session-end build-gap · memory-selfimprove CRITERIA+GAP).
  'harness-eval', 'harness-refine', 'harness-audit', // +S141 bộ-ba đồng-kiểm memory-system (adopt AI_INFRA 2026-07-18 khuôn 5-câu) read-only INFORM-only propose-only;
  //   trục = CHÍNH HỆ MEMORY (độ-giữ·hành-động·đúng-spec) — KHÁC H1 tooling / H2 harvest / H24 lead / reviewer diff;
  //   TUẦN TỰ eval→refine→audit (CẤM song song — audit đọc bản cũ = verdict vô-nghĩa), lead scribe return từng con vào run-folder.
  'ring1-audit', // +S145 C4/C4b (adap dot-11, owner-decision [2] TÁCH) — vai-KIỂM ĐỘC-LẬP vòng tooling/harvest: đọc OUTPUT H1(tooling-auditor)+H2(harvest-curator), no-self-exempt ≠ H1/H2. AUTO fire-after-measure @session-end §L.b(g-bis) (KHÁC trio/H24 consent-gated). Vai #1/3 round-KIỂM (ring2-audit + ring4-audit land dần incremental).
  'ring2-audit', 'ring4-audit', // +S145 C4/C4b vai #2/3 + #3/3 round-KIỂM (owner-decision [2] TÁCH mỗi-vòng-1-con). ring2-audit = KIỂM vòng soi-lead (đọc OUTPUT lead-stale+lead-gap, no-self-exempt ≠ chúng, KHÔNG soi lead trực-tiếp) · ring4-audit = KIỂM vòng nén-ngủ (đọc /sleep-recovery gist+Fidelity, tự đo byte). AUTO fire-after-measure, propose-only, NO-OP nếu vòng không đo.
  'ring5-audit', // +S185 slot(41) YC-021/W-2 anh chốt — KIỂM vòng-6 nghi-thức cửa phiên (đọc OUTPUT tien-kiem-cua + bảng-nhãn ritual-labels 4-nhãn, quyền ĐỀ-NGHỊ-XOÁ-ÁN khi nhãn nhầm lớp). AUTO @bookend-close sau bảng-nhãn, NO-OP nếu 0-nhãn-có-khai.
  'ctx-audit', // +S153 ctx soft-memory 0df10df4 — vai-3 vòng Ctx: SOI-CHUỖI 2 đầu phiên, INFORM-only (🔄 OWNER-VETO @S153: (38)(a) 1-vai → hub-form 3-vai, anh chốt trực tiếp)
  'ctx-curator', 'ctx-verifier', // +S153 OWNER-VETO hub-form 3-vai — vai-1 KIỂM nháp @mỗi-pause (carve-out pause §1) · vai-2 ĐỐI-CHIẾU+NẠP @mỗi-tiep; cả 2 INFORM-only trả-văn-bản
  'score-counter', 'score-count-auditor', // +S181 W4b (chủ dự án phán P4) — tầng-2 + tầng-3 của luật chấm điểm
  //   đáp-ứng-yêu-cầu (canonical `harness-11-engine.md` PHẦN P). 🔴 RANH CỨNG giữa 2 con, đừng gộp:
  //   tầng-2 `score-counter` ĐẾM ĐỘC LẬP và **CẤM xem điểm tầng-1** trước khi đếm xong;
  //   tầng-3 `score-count-auditor` kiểm PHÉP ĐẾM và **CẤM đề xuất một điểm khác** (đề xuất điểm = thành
  //   tầng-2 thứ hai ⇒ không còn ai kiểm phép đếm). Gộp 2 con = mất đúng tính độc lập mà 4 tầng dựng ra.
  //   Nợ P4 đã trả cùng lượt: seed `measured{}` cho CẢ HAI (số 0 = CHƯA ĐO, không phải đã-đo-bằng-0).
  'front-end-reviewer-style', 'back-end-reviewer-style', 'database-reviewer-style', // +S176 ĐỘI STYLE (anh chốt trực-tiếp).
  //   🔴 Trục = ĐỒNG-NHẤT style, ⟂ `reviewer` (ĐÚNG-SAI logic) ⇒ chạy SONG SONG được, không giẫm nhau.
  //   Luật lõi anh giao: "tính năng đã deploy production = CHUẨN, trừ khi tao điều chỉnh lại lần nữa" —
  //   lý do sinh đội: "mày cứ làm đi làm lại hoài" (ca thật S176 vòng-4b: thùng rác KHKK thiếu banner+dấu xoá
  //   mà màn Duyệt NCC ĐÃ chạy prod có đủ ⇒ anh phải chụp màn gửi làm mẫu — một lượt làm lại đáng lẽ không cần).
  //   Địa-phận RỜI theo glob diff: fe-*/src/** · src/Backend/** trừ Persistence/** · Persistence/**.
  //   🧊 Anh nêu ban đầu 4 con (có clean-code-reviewer-style) rồi tự chốt lại 3 ("3 con tao nhầm" + "chốt theo ý mày"):
  //   clean-code phủ chồng cả 3 nên KHÔNG viết nổi câu scope-rời mà roster này bắt buộc ⇒ gộp trục đó vào từng con.
]

// ─── PA-2a — hằng-số kỳ-vọng model tier-2 (H23 model-at-spawn, owner-chốt O1 @S121 W2) ──
// 🔴 ĐÂY LÀ HẰNG-SỐ KHAI-BÁO, KHÔNG PHẢI CƠ-CHẾ NEO. Nó KHÔNG ép model lúc spawn.
// Vì sao chỉ là hằng-số: neo-model-tại-spawn BẤT-KHẢ-THI ở SE — param `model:` chỉ nhận ALIAS
//   ('opus'/'fable'/...), không nhận full-id; SE nay pin ALIAS `opus` (KHÔNG full-id) ở TOÀN-BỘ frontmatter
//   roster (KHÔNG chép số — lesson S108, tái @S151: "17/17" viết cứng đã stale khi roster lên 20).
//   ⇒ owner chốt PA-2 = PA-2a (hằng-số này, để audit có thứ ĐỐI-CHIẾU) + PA-2b (scripts/spawn-model-audit.ps1
//   so resolved-vs-expected). Đọc bởi: scripts/spawn-model-audit.ps1 (thiếu hằng-số ⇒ script FAIL-LOUD).
// 🔸 H23 precedence — ĐÃ ĐO S126 (2026-07-16, phiên đầu sau restart; run wf_f960dae2-fa0, đọc bằng
//   spawn-model-audit.ps1, KHÔNG tin lane tự khai): khi CẢ HAI cùng hiện-diện ⇒ SPAWN-PARAM THẮNG hard-pin.
//   Lane reviewer (frontmatter `opus`) + tier:'fable' → resolved `claude-fable-5` ⇒ /fable-real·/fable-clone
//   SỐNG, KHÔNG NO-OP, không cần owner re-pin. Control: lane investigator-codebase KHÔNG tier → resolved
//   `claude-opus-4-8` DÙ lead=Fable ⇒ pin frontmatter ăn runtime (loại giả-thuyết inherit-lead).
//   Caveat: retrospective (không hứa spawn TƯƠNG-LAI — đúng PA-2b) · đo trên ALIAS-pin, KHÔNG generalize
//   sang full-id-pin (hub form). Evidence: runs/2026-07-16-S126-adap-spec-execute/runtime-verify-synthesis.md.
// 🔸 CHƯA THỬ (fix#8a): param `model:` có TỪ-CHỐI full-id không — doc nói enum-alias; chưa ai thử thật.
// 🔄 VALUE-update @S149 2026-07-25 (broadcast `model-default-opus-5-max` sha published 9c909007 — re-stamp G-024a 07-25, pre 59ce1d0f, hub-owner "làm gấp"
//   + SE-owner cùng ngày "Giao cho đám Opus 5 MAX apply"): 'claude-opus-4-8' → 'claude-opus-5'.
//   Khoản 1.2: SE pin ALIAS `opus` ⇒ CODE spawn-site GIỮ NGUYÊN, alias family tự resolve Opus 5.
//   Khoản 1.4: 'claude-opus-4-8*' = bucket LEGACY giai đoạn chuyển tiếp (record cũ KHÔNG cờ drift).
//   2-key: đổi ĐỒNG THỜI với $OWNER_RATIFIED_TIER2_FULL_ID trong scripts/spawn-model-audit.ps1.
const TIER2_EXPECTED_FULL_ID = 'claude-opus-5'

// ─── Worker-tier-pin opus (owner-decision S124 — thay thế H8 all-inherit; adopt erratum-h8 floor) ──
// MỌI sub-agent (TOÀN roster — số canonical docs/STATUS.md §Sub-agents, KHÔNG chép số ở comment)
//   → frontmatter `model: opus` + `effort: max` = worker-tier gán-cứng (KHÔNG còn
//   inherit lead). Lead = DEFAULT Opus 5 MAX (thư 9c909007 @S149, re-stamp 07-25 pre 59ce1d0f; BƯỚC 0.6 probe runtime -
//   idiom cũ "owner-choice per-session" THÔI DÙNG @S149, sweep @S151); Fable/cao-hơn = owner gõ /fable-real
//   HOẶC /model tay (owner "còn Fable tao sẽ chỉ định qua Fable-real hoặc tự chọn").
//   KHÔNG hardcode model hiện-hành ở comment — lesson S108: comment stale làm 2/4 worker đọc sai runtime.
//   (🔴 restart-CLI để frontmatter mới ăn runtime — EDIT-agent-tồn-tại no-hot-reload.)
// hmw KHÔNG set model cho role-có-frontmatter → return undefined, frontmatter (opus) tự lo (= worker-tier).
// Role-less (H6.2 governed-ultracode): default = inherit lead-model (top-tier) — KHÔNG pin rẻ làm default.
// H8.2 "workflow nhanh nhất" = song song tối đa + xuất nhanh, KHÔNG hạ model làm rẻ.
// Escape-hatch per-task tier:'opus' GIỮ cho sweep/cost (ngoại lệ có chủ đích); tier:'fable' CHỈ hợp lệ
//   trong lượt lệnh engine-đắt anh gõ kèm vai (/fable-real|/fable-clone <vai> <đề-bài> — H21 S110);
//   lead KHÔNG tự ý elevate theo phán-đoán riêng (MTv3 §1.3: đường DUY NHẤT nâng 1 vai lên hạng-nhất = lệnh owner).
// D-3 (W5/S181) — MỘT NGUỒN cho ý nghĩa "CỐ Ý không override model".
// Trước đây 3 nhánh của resolveModel đều `return undefined`, nên đọc mã KHÔNG phân biệt được
// *cố ý để mặc định* với *quên set* — đúng lớp "undefined-im-lặng" mà D-3 gọi tên.
// Sentinel này KHÔNG đổi hành vi runtime: chỗ-gọi dịch nó về `undefined` trước khi truyền đi.
const MODEL_INHERIT = 'inherit'

function resolveModel(role, rawRole, tier, i) {
  if (tier === 'fable' || tier === 'opus') return tier
  if (tier) log(`⚠️ hmw: tier "${tier}" lạ (task #${i}) → bỏ qua, dùng mặc định (frontmatter worker-tier opus)`)
  // Role-less (null) → inherit lead; invalid non-empty role đã THROW up-front (STOP-HARD, DIRECTED REC-3 2026-07-13)
  //   → nhánh này giờ CHỈ đạt bởi role-less thật (rawRole null/''); giữ làm defensive no-op.
  if (!role && rawRole) return MODEL_INHERIT
  if (!role) { log(`hmw: task #${i} role-less → inherit lead-model (H6.2 governed-ultracode; per-task tier:'opus' = escape-hatch sweep/cost)`); return MODEL_INHERIT }
  return MODEL_INHERIT // role có frontmatter: tất cả `opus`+`effort:max` (worker-tier-pin S124) — KHÔNG override
}

const SCHEMA = {
  type: 'object',
  required: ['findings', 'memoryDelta'],
  properties: {
    findings: { type: 'string', description: 'Kết quả chính. MỌI claim kèm evidence file:line. KHÔNG narrative suông.' },
    checklistEvidence: { type: 'string', description: 'Bằng chứng cho acceptance-checklist P1 (số đo / PASS-FAIL / verdict).' },
    subMdPath: { type: 'string', description: 'RUN-TRACE mode FLAT: đường-dẫn sub-<role>-<i>.md agent đã ghi (em main/H2 đọc on-demand). DEFAULT-mode: bỏ trống.' },
    memoryDelta: {
      type: 'object',
      description: 'Spawn-record 4-field — RETURN-only để EM MAIN harvest @P3. Agent KHÔNG tự ghi ký ức (KHÔNG file MEMORY.md, KHÔNG store_memory/RAG). Em main VERIFY + APPEND-only (KHÔNG overwrite entry cũ nếu chưa kiểm tra — B3).',
      required: ['task', 'verdict', 'learned', 'surprise'],
      properties: {
        task: { type: 'string', description: 'việc gì (1 dòng)' },
        verdict: { type: 'string', description: 'kết luận / PASS-FAIL / root-cause' },
        learned: { type: 'string', description: 'pattern / anti-pattern rút ra' },
        surprise: { type: 'string', description: 'edge-case / bất ngờ (chống mất discovery)' },
      },
    },
  },
}

// S4b — args có thể tới OBJECT hoặc JSON-STRING (harness đôi khi stringify object args) → normalize defensive
// 🔴 LUẬT (per 6c2eb2b1 — B6 non-JSON-args): args LUÔN là object JSON thật — dòng normalize NGAY DƯỚI là
//   LƯỚI-CUỐI cho ca tầng-máy tự stringify, KHÔNG phải giấy phép để em-main tự stringify args khi gọi.
const A = (typeof args === 'string') ? JSON.parse(args) : (args || {})

if (!A || !Array.isArray(A.taskList) || A.taskList.length === 0) {
  throw new Error('hmw: cần args.taskList (mảng {role,label,prompt}) — em main bơm @P1. (đã thử JSON.parse nếu args bị stringify.)')
}
// S2 mechanize: checkpoint TRƯỚC P2 = gate CỨNG, không chỉ prompt. Em main BÁO {số agent·vai·task} @inform
// (KHÔNG chờ confirm) → set args.checkpointApproved=true → fan-out. Flag GIỮ = anti-accidental-fire
// (call quên-set-flag → throw, chặn 515K-class accidental).
if (A.checkpointApproved !== true) {
  throw new Error('hmw: checkpointApproved chưa set — em-main BÁO {số agent·vai·task} @inform rồi set args.checkpointApproved=true (guard = anti-accidental-fire).')
}
// ─── C8b LOUD-skip trần WIDTH (S144, adap đợt-11 · 🔴 CHƯA RATIFY — xem ĐÍNH-CHÍNH dưới) ────
// 🔴 ĐÍNH-CHÍNH ATTRIBUTION @S149 (2026-07-24) — bản trước ghi "owner chốt O-7 2026-07-22" ×2.
//   ĐÓ LÀ SAI. Đo 4 nguồn: `docs/HANDOFF.md` mục (19) ghi "chưa ai duyệt" + "số = owner-SỐ";
//   `docs/STATUS.md` ghi O-7 "câm 5 nhãn"; grep `O-7.{0,60}(anh chốt|owner chốt|đã duyệt|ratif)`
//   toàn `docs/` + `.claude/governance/` = **0 dòng**; `ring2-audit` @S149 chấm FLAG "bảng owner-gated
//   O-2/O-3/O-7 câm" = ĐẠT/HELD (falsify F2+F3 đều HELD).
//   ⇒ Nguồn thật của cơ-chế = **ADOPT-theo-floor** của spec-R2 + C14 3.4 theo `depth-axis:43`,
//   KHÔNG phải owner ký. Code tự nâng adopt-theo-floor thành owner-decision = cùng họ "lời-hứa-máy"
//   (`goi-chot:52`) và họ "SHA bịa" — chữ ký của owner KHÔNG được suy ra từ việc đã implement.
//   🔸 Chưa loại trừ owner chốt MIỆNG @S144 không ghi sổ ⇒ đang HỎI owner @S149; nếu đúng thì
//   đây chỉ là ghi-vết-lại, và dòng này đổi thành "owner ratified <date>".
// LINEAGE (giữ vết, KHÔNG xoá — khuôn `auto-toan-vong:35` "đảo có chủ đích, chủ-ký, ngày, lý do"):
//   BẢN CŨ: `if (len > 16) log(...)` + comment "số task THOẢI MÁI … KHÔNG cap cứng (soft notice only)".
//   VÌ SAO ĐẢO: broadcast `depth-axis:40` đòi "clamp trần cứng + assert tổng-lượt ≤ trần";
//   soft-notice = chạm trần vẫn chạy hết ⇒ không phải clamp. Cơ-chế LOUD-skip = adopt theo floor.
//   VÌ SAO KHÔNG THROW: `depth-axis:43` nguyên văn "BỎ đào + báo RÕ (KHÔNG văng lỗi, không
//   lặng-thầm bỏ)"; và :124-125 dưới đã tự khai throw per-lane có thể bị nuốt thành null =
//   STOP-SILENT, tệ hơn. Reviewer R2 bắt bản spec v1 đòi THROW = tự mâu thuẫn → đã sửa.
// 🔴 CANONICAL của số này = `.claude/agent-memory/memory-budget.json` → `hmw_width.cap` (B1).
//   Dòng dưới là **MIRROR HARDCODE**, KHÔNG đọc runtime — sandbox workflow no-fs (xem :5).
//   Lệch giữa 2 nơi ⇒ drift-check `governance-detectors.ps1` phải FLAG (K8, chưa land — grep
//   `width` toàn `scripts/` @S149 = 0 hit).
// 🔸 16 → 20 @S149 2026-07-24 (owner-DELEGATED → lead-decided; xem `hmw_width._ratified_by`).
//   VÌ SAO: trần này chặn ĐỐT TOKEN NHẦM, không chặn concurrency (engine đã cap min(16,cpu-2),
//   >16 chỉ xếp hàng). Sweep TOÀN-ROSTER là use-case đã chạy thật (S125/S126 = 14/14 thời roster
//   14); roster nay **20** ⇒ cap 16 cắt 4 lane, ép chia 2 đợt vô cớ. Đánh đổi khai thẳng:
//   8 lane @S149 ≈ 1,06M token ⇒ 20 lane ước ~2,5M.
const WIDTH_CAP = 30
if (A.taskList.length > WIDTH_CAP) {
  const dropped = A.taskList.slice(WIDTH_CAP)
  const keptN = WIDTH_CAP
  log(`hmw: 🔴 VƯỢT TRẦN WIDTH — taskList=${A.taskList.length} > trần=${WIDTH_CAP}.`)
  log(`hmw: 🔴 CHẠY ${keptN} lane ĐẦU, BỎ ${dropped.length} lane CUỐI (LOUD-skip, KHÔNG throw — depth-axis:43).`)
  log(`hmw: 🔴 LANE BỊ BỎ: ${dropped.map((t, k) => `[${WIDTH_CAP + k}]${(t && t.label) || (t && t.role) || '(no-label)'}`).join(' · ')}`)
  log(`hmw: 🔴 Đây KHÔNG phải "đã chạy hết" — em-main PHẢI khai số bỏ trong report @P4.`)
  A.taskList = A.taskList.slice(0, WIDTH_CAP)
}

// ─── STOP-HARD role-whitelist (DIRECTED 6c32df89 REC-3, 2026-07-13 · S115) ────
// Vai ∉ VALID_ROLES (typo / rename-drift) = DỪNG HẲN + báo owner — KHÔNG fail-soft
// về default subagent (fail-soft = "đứt âm-thầm" đã gây lỗi thật hub-side; sim 5-ca).
// PHÂN-BIỆT (backward-compat H6.2): role null/omitted/'' = HỢP-LỆ role-less (inherit
// lead) → KHÔNG throw. CHỈ non-empty-string ∉ whitelist mới throw. Validate UP-FRONT
// (trước parallel) — halt KHÔNG phụ-thuộc semantics parallel()/.filter(Boolean) (per-lane
// throw có thể bị nuốt thành null = STOP-SILENT, tệ hơn). Mirror checkpointApproved tripwire.
const badRoles = A.taskList
  .map((t, i) => ({ i, raw: t && t.role }))
  .filter(x => x.raw != null && x.raw !== '' && !VALID_ROLES.includes(x.raw))
if (badRoles.length > 0) {
  const detail = badRoles.map(x => `#${x.i}="${x.raw}"`).join(', ')
  throw new Error(
    `hmw: vai ∉ VALID_ROLES (${detail}) — DỪNG HẲN (STOP-HARD). ` +
    `Sửa typo HOẶC báo owner thêm vai vào VALID_ROLES [${VALID_ROLES.join(', ')}]. ` +
    `KHÔNG fail-soft về default subagent (DIRECTED 6c32df89 REC-3 2026-07-13).`)
}

const memoryPack = A.memoryPack || {}
const spec = A.spec || ''

// ─── RUN-TRACE mode (Harness-10, supersedes Harness-2 wave-mode B) ────────────
// run = { name, dir }. Folder runs/<run-id>/ (TRACKED) + run.md em main ĐÃ scaffold @P1 (script no-fs). Bật → sub ghi full-detail
// vào CHỈ sub-<role>-<i>.md phẳng mình + return memoryDelta. Containment: tracked-change NGOÀI run-folder + code-disjoint = vi-phạm
// (run-folder TRACKED → HIỆN trong git-diff = audit trực-tiếp; em main git-diff post-P2 + chunk-count RAG). [legacy alias args.wave]
const wave = (A.run && A.run.dir) ? A.run : ((A.wave && A.wave.dir) ? A.wave : null)
if (wave) log(`hmw: RUN-TRACE mode on → dir=${wave.dir} (TRACKED FLAT; sub ghi sub-<role>-<i>.md phẳng isolated; em main scaffold @P1; harvest per-turn primary C4, H2 gom @session-end = backstop).`)

// ─── SÀN-1 GATE — RUN-TRACE BẮT BUỘC khi ≥3 task (H22 wal-defect-fix, adopt S121 W2) ─────
// TRƯỚC S121 sàn này chỉ là CHỮ trong doc ("script no-fs KHÔNG tự ép") ⇒ phụ-thuộc em-main NHỚ set
// args.run. Đo thật @S119: `git grep args.run .claude/commands/` = 0 hit ⇒ 4/4 wrapper KHÔNG cờ
// ⇒ sàn = danh-nghĩa. Nay CƠ-KHÍ-HOÁ (mirror tripwire checkpointApproved).
// Vì sao ≥3: chết giữa run DÀI mà không có run-folder ⇒ /tiep §4 KHÔNG có vật-liệu trên đĩa để cắt-gọt
//   ⇒ phải chạy lại TỪ ĐẦU (per-turn returns mất theo phiên; chỉ file trên đĩa sống sót).
//   Bằng-chứng: S118 raw-Agent 7-spawn ⇒ 0 run-folder ⇒ mất trace.
// 🔴 THROW chứ KHÔNG fail-soft: fail-soft = "đứt âm-thầm" (cùng lý-lẽ STOP-HARD role-whitelist bên trên).
if (!wave && A.taskList.length >= 3) {
  throw new Error(
    `hmw: SÀN-1 — ${A.taskList.length} task (≥3) mà THIẾU args.run ⇒ DỪNG HẲN. ` +
    `RUN-TRACE là BẮT BUỘC ở ngưỡng này (sàn H22 S111): chết giữa run mà không có runs/<run-id>/ ` +
    `thì /tiep §4 không có vật-liệu đĩa để relaunch cắt-gọt, phải chạy lại từ đầu. ` +
    `SỬA: em-main scaffold .claude/workflows/runs/<run-id>/ (run.md + taskList snapshot) @P1 rồi truyền ` +
    `args.run = { name:'<run-id>', dir:'.claude/workflows/runs/<run-id>' }. ` +
    `(Script chạy JS-sandbox KHÔNG filesystem ⇒ KHÔNG tự tạo folder được — vì thế mới gate ở đây.) ` +
    `Dưới 3 task = MODE A return-delta-only, args.run tùy chọn.`)
}
// 🔸 KHAI THẬT — gate này CHỈ phủ đường `hmw.js`. Ngưỡng "hoặc ước >5 phút" KHÔNG cơ-khí-hoá được
//   (script không đo trước được thời-lượng) ⇒ phần đó vẫn là kỷ-luật người, không phải cổng.
// 🔸 KẼ CÒN-LẠI (F2, KHÔNG gate được từ đây): raw `Workflow` tool và raw `Agent` tool đi THẲNG, không
//   qua file này ⇒ mất luôn writeGuard + run-trace. S72 raw-Workflow ⇒ reviewer ghi MEMORY +2850B over-cap;
//   S118 raw-Agent 7-spawn ⇒ 0 run-folder; và chính run S119 cũng đi raw Workflow. SE KHÔNG có CLI-launcher
//   để whitelist ⇒ 3 nguyên-tắc detector của broadcast là N/A **vì không có launcher**, KHÔNG phải vì "hết kẽ".

phase('Execute')
log(`HMW P2: fan-out ${A.taskList.length} task (${wave ? 'RUN-TRACE' : 'return-delta-only'}, worker-tier-pin opus, memory-pack-injected, scope=SOLUTION_ERP repo only)`)

const results = await parallel(A.taskList.map((t, i) => () => {
  const raw = t && t.role
  // STOP-HARD validate up-front (DIRECTED REC-3 2026-07-13): raw ở đây CHỈ có thể ∈ VALID_ROLES HOẶC null/'' (role-less H6.2).
  const role = VALID_ROLES.includes(raw) ? raw : undefined           // role-less (null/'') → undefined → inherit lead

  const mem = role && memoryPack[role] ? memoryPack[role] : ''
  const subMd = wave ? `${wave.dir}/sub-${role || 'task'}-${i}.md` : null  // Harness-10 FLAT (h10-refine 2026-06-18): sub-<role>-<i>.md phẳng cùng cấp dưới runs/<run-id>/ — KHÔNG sub-md/ subdir

  // Write-guard TOOL-AWARE theo MODE (B6 isolation). SE read-only sub (KHÔNG Write tool): investigator-codebase/api,
  // reviewer, cicd-monitor (+ monitor tooling-auditor/harvest-curator) (+S153 bộ-ba Ctx: ctx-audit, ctx-curator, ctx-verifier — lớp mềm _mind, tools: KHÔNG Write/Edit)
  // (+S176 đội STYLE: front-end/back-end/database-reviewer-style — tools: KHÔNG Write/Edit, propose-only).
  // Write sub: implementer-backend/frontend, test-specialist, frontend-designer.
  const writeGuard = wave
    ? [
        `## ✍️ RUN-TRACE ghi sub-<role>-<i>.md FLAT (Harness-10 h10-refine, supersedes subfolder sub-md/) — TOOL-AWARE (chống mojibake G-009):`,
        `- Full-detail công-việc của mày → ĐÚNG 1 file: \`${subMd}\` (folder đã scaffold sẵn — KHÔNG tạo folder).`,
        `  • NẾU mày CÓ Write/Edit tool (implementer-backend/frontend, test-specialist, frontend-designer): GHI TRỰC TIẾP via Write/Edit. 🔴 KHÔNG Bash-write MD ($-expansion/mojibake).`,
        `  • NẾU mày CHỈ có Bash (read-only sub: investigator-codebase/api, reviewer, cicd-monitor, tooling-auditor, harvest-curator, lead-stale-auditor, lead-gap-auditor, harness-eval, harness-refine, harness-audit, ctx-audit, ctx-curator, ctx-verifier, front-end-reviewer-style, back-end-reviewer-style, database-reviewer-style — KHÔNG Write tool): 🔴 TUYỆT ĐỐI KHÔNG Bash-write MD → để full-detail trong "findings" + đặt subMdPath="${subMd}"; EM MAIN scribe @P3 (single-writer Write-tool, no-corruption).`,
        `- 🔴 ISOLATION (B6→Harness-10, AUDIT): CHỈ ghi \`${subMd}\` (+ code-file-disjoint nếu task giao). TUYỆT ĐỐI KHÔNG ghi/sửa: agent-memory/* (MEMORY.md BẤT KỲ sub) · MD canonical (CLAUDE/README/STATUS/agents) · sub-MD agent khác. Em main git-status/diff audit sau P2 — tracked-file đổi NGOÀI run-folder (runs/<run-id>/) + code-disjoint = vi-phạm (run-folder TRACKED → HIỆN trong diff).`,
        `- LUÔN return: findings (FULL) + checklistEvidence + memoryDelta (4-field) + subMdPath="${subMd}". Harvest per-turn primary (C4); H2 gom @session-end = backstop verify-idempotent → agent-memory/${role || 'sub'}.`,
        `- 🔴 KHÔNG store_memory/RAG-write · KHÔNG Bash curl/HTTP Qdrant (:6333 = git-diff MÙ, chỉ chunk-count bắt) · KHÔNG ghi file NGOÀI repo/run-folder. RAG single-writer=em main; containment = git-diff(in-repo)+chunk-count(RAG) [G-015].`,
      ].join('\n')
    : [
        `## OUTPUT write-guard (DEFAULT return-delta-only):`,
        `- ⚠️ KHÔNG ghi ký ức kênh nào: KHÔNG file MEMORY.md, KHÔNG store_memory/RAG-write (Qdrant). CHỈ return memoryDelta → em main VERIFY + APPEND-only @P3 (B3). KHÔNG overwrite file/chunk của sub khác.`,
        `- ⚠️ Nếu task có WRITE file: CHỈ file-disjoint được giao (em main git-diff verify sau P2). KHÔNG đụng file ngoài phạm vi. Scope = repo SOLUTION_ERP only (S1).`,
      ].join('\n')

  const prompt = [
    mem ? `## MEMORY-PACK (${role}) — SLICE ký ức tích lũy của sub này, ĐỌC trước khi làm (KHÔNG phải full memory):\n${mem}\n` : '',
    spec ? `## SPEC / acceptance-criteria (chung):\n${spec}\n` : '',
    `## TASK:\n${(t && t.prompt) || '(thiếu prompt — lỗi taskList @P0)'}`,
    [
      '## OUTPUT (structured bắt buộc):',
      '- findings: kết quả + evidence file:line (KHÔNG narrative suông).',
      '- checklistEvidence: số đo / verdict cho acceptance-checklist.',
      '- memoryDelta {task·verdict·learned·surprise}: để EM MAIN harvest @P3.',
      wave ? '- subMdPath: đường-dẫn sub-MD mày đã ghi.' : '',
    ].filter(Boolean).join('\n'),
    writeGuard,
  ].filter(Boolean).join('\n')

  return agent(prompt, {
    agentType: role || undefined,
    schema: SCHEMA,
    label: (t && t.label) || `hmw:${role || 'task'}-${i}`,
    // D-3 (W5/S181): dịch sentinel → `undefined` NGAY TẠI ĐÂY, tường minh.
    // `undefined` truyền xuống nghĩa là "không override, để frontmatter worker-tier quyết"
    // (= opus + effort:max, S124). Hành vi runtime y hệt bản trước; cái đổi là ĐỌC RA ĐƯỢC Ý ĐỊNH.
    model: (() => { const m = resolveModel(role, raw, t && t.tier, i); return m === MODEL_INHERIT ? undefined : m })(),
  })
}))

// ─── C1 LANE-CHẾT-PHẢI-KÊU (S144, adap đợt-11 · repro NGAY trong run sinh ra nó) ───────────
// LINEAGE (giữ vết): BẢN CŨ = `return results.filter(Boolean)` + comment "lọc null nếu agent
// lỗi/null … .filter(Boolean) chỉ lọc null-lane do agent lỗi runtime" — tức code TỰ KHAI nó
// nuốt lane chết, mà KHÔNG phát tín hiệu nào ra ngoài.
// VÌ SAO SỬA — bằng chứng ngôi-thứ-nhất `wf_491c05a8-6dc` (2026-07-22): 3/5 lane chết vì cháy
// maxTurns; em-main nhận mảng 2 phần tử KHÔNG tín hiệu "chết", chỉ biết nhờ khối <failures> của
// HARNESS chứ không nhờ file này. Đúng `depth-cost:53` "Thử-rồi-chết ≠ chạy-rồi-sạch … tầng
// tổng-hợp sẽ viết 'tầng-2 không thêm gì' = củng-cố verdict bằng một tầng-2 không tồn tại".
// Falsifier hub `depth-cost:59` hỏi "tầng chết thì tầng sau thấy 'chết' hay 'sạch'?" — trước
// bản vá này SE trả lời: thấy SẠCH.
// 🔴 CỐ Ý KHÔNG throw: :124-125 đã khai per-lane throw bị nuốt thành null = STOP-SILENT.
//    Fix = THÊM TÍN HIỆU, KHÔNG đổi shape trả về (P3 downstream giữ nguyên hợp đồng).
const deadIdx = results.map((r, i) => (r ? -1 : i)).filter(i => i >= 0)
if (deadIdx.length > 0) {
  const nameOf = i => (A.taskList[i] && (A.taskList[i].label || A.taskList[i].role)) || '(no-label)'
  log(`hmw: 🔴 ${deadIdx.length}/${results.length} lane CHẾT (null) — KHÔNG phải "sạch".`)
  log(`hmw: 🔴 LANE CHẾT: ${deadIdx.map(i => `[${i}]${nameOf(i)}`).join(' · ')}`)
  log(`hmw: 🔴 Lane chết = 0 findings, KHÔNG phải "không tìm thấy gì". Em-main @P3 PHẢI khai số chết trong report, và CẤM đọc mảng ngắn thành "đã phủ đủ".`)
  log(`hmw: 🔴 Recovery: đọc ĐĨA trước (runs/<id>/sub-*.md) — lane có thể đã ghi xong mà return hỏng.`)
}
// về em main → P3 VERIFY + harvest + P4 checklist (shape GIỮ NGUYÊN: chỉ lane sống)
return results.filter(Boolean)
