@kienntpixon/dieu-phoi 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/commands.cjs CHANGED
@@ -15,7 +15,7 @@ const AGENT_OUTCOMES = ['pass', 'fail', 'timeout', 'aborted'];
15
15
  const USER_OUTCOMES = ['accept', 'reject'];
16
16
  const CAUSES = ['quality', 'scope'];
17
17
  const STEER_KINDS = ['minor', 'plan', 'redirect'];
18
- const VERIFY = ['tests', 'lint', 'review', 'judgment', 'none'];
18
+ const VERIFY = ['tests', 'lint', 'review', 'judgment', 'self', 'none'];
19
19
  const INVALID_ID_WINDOW_DAYS = 7;
20
20
  const NEW_ID_MAX = 1000;
21
21
 
@@ -65,7 +65,7 @@ function cmdSetup(opts) {
65
65
  const lines = [
66
66
  `Đã cài hook: ${res.hook}`,
67
67
  res.settings_changed
68
- ? `Đã đăng ký PostToolUse(Agent) + SubagentStop + SessionStart(compact) + UserPromptSubmit trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
68
+ ? `Đã đăng ký PostToolUse(Agent) + SubagentStop + SessionStart(compact) + UserPromptSubmit + PreToolUse(Agent) trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
69
69
  : `settings.json đã có hook, không đổi gì (${res.settings})`,
70
70
  `Agent dp-*: ${res.agents.map((a) => `${a.name}=${a.action}`).join(', ')}${res.agents.some((a) => a.action === 'skipped-foreign') ? ' (file trùng tên không phải của dieu-phoi: giữ nguyên, đổi tên rồi chạy lại setup)' : ''}`,
71
71
  `Thư mục báo cáo subagent: ${res.runs_dir} (quyền: ${res.permissions.allow.join(', ')} + additionalDirectories)`,
@@ -128,6 +128,9 @@ async function cmdDoctor(opts) {
128
128
  add(reg.UserPromptSubmit, 'prompt-hook', reg.UserPromptSubmit
129
129
  ? 'UserPromptSubmit có — prompt "điều phối" được tạo sẵn task_id + run dir, lượt đầu là scout'
130
130
  : 'thiếu hook UserPromptSubmit — mỗi lần điều phối phải chạy `dieu-phoi start` trước scout; chạy lại `dieu-phoi setup`', 'CẢNH BÁO');
131
+ add(reg.PreToolUse, 'chain-gate', reg.PreToolUse
132
+ ? 'PreToolUse(Agent) có — chặn exec tiếp nối từ handoff thứ 3 (trừ thẻ chain_ok)'
133
+ : 'thiếu hook PreToolUse(Agent) (core 0.7.1) — chuỗi handoff dài không bị chặn; chạy lại `dieu-phoi setup`', 'CẢNH BÁO');
131
134
  const permOk = reg.permissions && reg.runs_dir_exists && !reg.legacy_permissions;
132
135
  add(permOk, 'run-permissions', permOk
133
136
  ? `${reg.permission_entries.allow.join(', ')} + additionalDirectories ${reg.permission_entries.dir}`
@@ -295,8 +298,13 @@ async function checkSinkLive(ctx) {
295
298
  */
296
299
  function cmdStart(opts) {
297
300
  const ctx = ctxOf(opts);
301
+ const wantMode = modeOpt(opts); // validate before the run dir is created
298
302
  const reg = setupLib.registration(claudeDirOf(opts));
299
303
  const info = core.startInfo(ctx, repoOf(opts));
304
+ if (wantMode) {
305
+ core.appendEvent(ctx, modeEvent(ctx, info.id, wantMode, 'start', opts.reason));
306
+ info.mode = wantMode;
307
+ }
300
308
  const h = info.health;
301
309
  if (!fs.existsSync(ctx.paths.hook)) h.problems.unshift(`chưa cài hook (${ctx.paths.hook}) — chạy \`dieu-phoi setup --dept <phòng>\`, log không được ghi`);
302
310
  else if (!(reg.PostToolUse && reg.SubagentStop && reg.SessionStart)) h.problems.unshift('hook chưa đăng ký đủ trong settings.json — chạy lại `dieu-phoi setup`, log/trạng thái không được ghi');
@@ -314,12 +322,58 @@ function cmdStart(opts) {
314
322
  `step_ids: ${info.step_ids.join(' ')}`,
315
323
  `health: ${core.healthLine(h)}`,
316
324
  `scout_brief: ${info.scout_brief || '(không thấy — dùng references/brief-template.md mục E)'}`,
325
+ core.modeLine(info),
317
326
  ...core.orchLessonLines(info),
318
327
  ...core.knowledgeLines(info),
319
328
  ];
320
329
  return { code: 0, out: out.join('\n') };
321
330
  }
322
331
 
332
+ // ------------------------------------------------------------------ mode
333
+
334
+ /** One local-only mode event for a request (root_task_id). source: start | cmd | plan. */
335
+ function modeEvent(ctx, root, mode, source, reason, session) {
336
+ const sid = typeof session === 'string' && session ? session : (process.env.CLAUDE_CODE_SESSION_ID || null);
337
+ return Object.assign({
338
+ schema: core.SCHEMA,
339
+ kind: 'mode',
340
+ ts: core.nowIso(),
341
+ agent_id: `root:${root}`,
342
+ task_id: null,
343
+ root_task_id: root,
344
+ mode,
345
+ source,
346
+ reason_code: kebab(reason),
347
+ session_id: sid,
348
+ }, core.commonFields(ctx));
349
+ }
350
+
351
+ /**
352
+ * `dieu-phoi mode --root <root_task_id> [fast|normal|strict] [--reason R] [--session S]`: show or change the mode of a
353
+ * request. One event per change; the same mode again writes nothing. Steps not yet dispatched follow the new mode,
354
+ * dispatched steps keep the mode they were given. Never changes a mode by itself.
355
+ */
356
+ function cmdMode(opts) {
357
+ const usage = 'Cú pháp: dieu-phoi mode --root <root_task_id> [fast|normal|strict] [--reason R] [--session S] [--json]';
358
+ if (typeof opts.root !== 'string') throw new Error(usage);
359
+ const root = requireTaskId(opts.root);
360
+ const ctx = ctxOf(opts);
361
+ const cur = core.rootModeFromEvents(core.readEvents(ctx), root);
362
+ const was = cur ? cur.mode : core.DEFAULT_MODE;
363
+ const want = opts._[0];
364
+ const show = (extra) => (opts.json ? JSON.stringify(Object.assign({ root_task_id: root, mode: was, previous: was, changed: false }, extra)) : null);
365
+ if (want === undefined) {
366
+ return { code: 0, out: show({}) || `root ${root}: mode=${was}${cur ? ` (từ ${cur.ts}, nguồn ${cur.source || '?'})` : ' (mặc định, chưa đặt)'}` };
367
+ }
368
+ if (!core.isMode(want)) throw new Error(`mode phải là ${core.MODES.join('|')} (nhận "${want}"). ${usage}`);
369
+ if (cur && was === want) {
370
+ return { code: 0, out: show({}) || `root ${root}: vẫn ${want} (không đổi, không ghi sự kiện)` };
371
+ }
372
+ core.appendEvent(ctx, modeEvent(ctx, root, want, 'cmd', opts.reason, opts.session));
373
+ if (opts.json) return { code: 0, out: JSON.stringify({ root_task_id: root, mode: want, previous: was, changed: true }) };
374
+ return { code: 0, out: `root ${root}: ${was} → ${want} (áp cho bước chưa giao; bước đã giao giữ mode lúc giao)` };
375
+ }
376
+
323
377
  // ------------------------------------------------------------------ hook / sync
324
378
 
325
379
  function cmdHook(opts) {
@@ -426,6 +480,7 @@ function verdictUser(opts, ctx, events) {
426
480
  if (typeof taskId !== 'string' || !taskId) throw new Error('--task cần task_id');
427
481
  requireTaskId(taskId);
428
482
  if (!USER_OUTCOMES.includes(outcome)) throw new Error(`verdict --task: outcome phải là ${USER_OUTCOMES.join('|')}`);
483
+ if (opts.auto && outcome !== 'accept') throw new Error('--auto chỉ dùng với accept (tự nghiệm thu bước fast); reject luôn là của người dùng: verdict --task <task_id> reject --cause quality|scope');
429
484
  let cause = typeof opts.cause === 'string' ? opts.cause : null;
430
485
  if (outcome === 'reject' && !CAUSES.includes(cause)) throw new Error(`reject cần --cause ${CAUSES.join('|')} (scope = yêu cầu đổi/thiếu, không phải lỗi model)`);
431
486
  if (outcome === 'accept') cause = null;
@@ -433,6 +488,7 @@ function verdictUser(opts, ctx, events) {
433
488
  if (!known) throw new Error(`Không thấy task "${taskId}" trong log.`);
434
489
  const rounds = events.filter((e) => e.kind === 'verdict' && e.task_id === taskId
435
490
  && agg.normalizeVerdict(e).by === 'user').map((e) => Number(e.round) || 1);
491
+ if (opts.auto) return verdictAuto(opts, ctx, events, taskId, rounds);
436
492
  let round = (rounds.length ? Math.max(...rounds) : 0) + 1;
437
493
  if (opts.round !== undefined) {
438
494
  round = Number(opts.round);
@@ -453,6 +509,33 @@ function verdictUser(opts, ctx, events) {
453
509
  }, core.commonFields(ctx));
454
510
  }
455
511
 
512
+ /**
513
+ * `verdict --task <task_id> accept --auto` (core 0.7.0): a fast-mode step that passed the batch verifier is accepted
514
+ * automatically (`by=auto`). Not a user verdict: never counts in accept_rate/user_outcomes/rounds, never merges
515
+ * knowledge; the user's final `verdict --root … accept` (by=user) or a later reject (wins) still decides.
516
+ */
517
+ function verdictAuto(opts, ctx, events, taskId, userRounds) {
518
+ if (opts.round !== undefined || opts.cause !== undefined) throw new Error('--auto không nhận --round/--cause');
519
+ if (userRounds.length) throw new Error(`Task ${taskId} đã có verdict người dùng; --auto chỉ dùng trước khi người dùng nghiệm thu.`);
520
+ const mode = core.taskModeInfo(ctx, taskId, events).mode;
521
+ if (mode !== 'fast') throw new Error(`--auto chỉ cho bước mode fast (task ${taskId} đang ${mode}); bước khác chờ người dùng nghiệm thu.`);
522
+ const exec = events.find((e) => e.kind === 'dispatch' && e.task_id === taskId && (e.role || 'exec') === 'exec');
523
+ if (exec && typeof exec.score === 'number' && exec.score > 6) throw new Error(`--auto chỉ cho bước score ≤ 6 (task ${taskId} score ${exec.score}).`);
524
+ return Object.assign({
525
+ schema: core.SCHEMA,
526
+ kind: 'verdict',
527
+ ts: core.nowIso(),
528
+ agent_id: `task:${taskId}`,
529
+ task_id: taskId,
530
+ by: 'auto',
531
+ outcome: 'accept',
532
+ cause: null,
533
+ round: null,
534
+ verify: 'tests',
535
+ reason_code: kebab(opts.reason),
536
+ }, core.commonFields(ctx));
537
+ }
538
+
456
539
  /**
457
540
  * `verdict --root <root> accept [--steps a,b]` (core 0.4.0): the user accepts the steps of the latest approved plan
458
541
  * in one call. Covers every step whose task has an exec dispatch and no user verdict yet (a step already rejected
@@ -460,6 +543,7 @@ function verdictUser(opts, ctx, events) {
460
543
  */
461
544
  function verdictRoot(opts, ctx, events) {
462
545
  const root = requireTaskId(String(opts.root));
546
+ if (opts.auto) throw new Error('verdict --root là người dùng duyệt cuối (by=user), không nhận --auto; tự nghiệm thu từng bước: verdict --task <task_id> accept --auto');
463
547
  if (opts._[0] !== 'accept') {
464
548
  throw new Error('verdict --root chỉ nhận accept; bước không đạt → dieu-phoi verdict --task <task_id> reject --cause quality|scope');
465
549
  }
@@ -552,6 +636,9 @@ function cmdVerdict(opts) {
552
636
  if (kn) process.stderr.write(`${kn.out}\n`);
553
637
  return { code: 0, out: JSON.stringify(ev) };
554
638
  }
639
+ if (ev.by === 'auto') {
640
+ return { code: 0, out: `Đã ghi tự nghiệm thu (by=auto) cho task ${ev.task_id}: không tính vào accept_rate; người dùng vẫn duyệt cuối (verdict --root <root> accept) hoặc reject (thắng auto).` };
641
+ }
555
642
  if (ev.by === 'user') {
556
643
  const msg = `Đã ghi người dùng ${ev.outcome}${ev.cause ? ` (${ev.cause})` : ''} cho task ${ev.task_id}, vòng ${ev.round}.`;
557
644
  return { code: 0, out: kn ? `${msg}\n${kn.out}` : msg };
@@ -642,19 +729,27 @@ function measureOrch(opts, ctx, prof) {
642
729
  u = core.orchestratorUsage(file, core.loadPricing(ctx), { cacheFile: core.orchCacheFile(ctx.paths, sid, file) });
643
730
  }
644
731
  } catch (_) { /* measurement never fails the caller */ }
645
- const over = typeof u.ctx_growth === 'number' && u.ctx_growth > softCap;
732
+ // core 0.7.1 (retro 2026-10-26, C1): past ORCH_NEW_SESSION_TOKENS or ORCH_NEW_SESSION_COMPACTIONS a /compact no longer
733
+ // helps (session 36cee993: 946k, 20 compactions, 0 user verdicts) -> advise a new session instead.
734
+ const newSession = (typeof u.ctx_tokens === 'number' && u.ctx_tokens > ORCH_NEW_SESSION_TOKENS)
735
+ || (typeof u.compactions === 'number' && u.compactions >= ORCH_NEW_SESSION_COMPACTIONS);
736
+ const over = (typeof u.ctx_growth === 'number' && u.ctx_growth > softCap) || newSession;
646
737
  let advice;
647
738
  if (!sid) advice = 'Không đo được: không có session (thiếu CLAUDE_CODE_SESSION_ID, dùng --session <id>).';
648
739
  else if (u.ctx_tokens === null) advice = 'Không đo được: không thấy transcript của session này.';
649
- else if (over) advice = `Context điều phối đã tăng ${u.ctx_growth} token (nền ${u.ctx_baseline}, nay ${u.ctx_tokens}) vượt trần mềm ${softCap}: gõ /compact để nén phiên — trạng thái điều phối sẽ được nạp lại tự động.`;
740
+ else if (newSession) {
741
+ advice = `Phiên điều phối đã quá lớn (context ${u.ctx_tokens} token, nén ${u.compactions === null ? '?' : u.compactions} lần; ngưỡng ${ORCH_NEW_SESSION_TOKENS} token hoặc ${ORCH_NEW_SESSION_COMPACTIONS} lần nén): đừng /compact thêm — ghi trạng thái (\`dieu-phoi state --session ${sid}\` + file trạng thái trong run dir), trình cổng 3 phần đã xong, rồi MỞ PHIÊN MỚI và nạp lại từ \`state\`.`;
742
+ } else if (over) advice = `Context điều phối đã tăng ${u.ctx_growth} token (nền ${u.ctx_baseline}, nay ${u.ctx_tokens}) vượt trần mềm ${softCap}: gõ /compact để nén phiên — trạng thái điều phối sẽ được nạp lại tự động.`;
650
743
  else advice = `Context điều phối tăng ${u.ctx_growth}/${softCap} token so với nền ${u.ctx_baseline}, còn trong trần mềm.`;
651
744
  return {
652
745
  session_id: sid, ctx_tokens: u.ctx_tokens, ctx_baseline: u.ctx_baseline, ctx_growth: u.ctx_growth,
653
- cum_usd: u.cum_usd, compactions: u.compactions, soft_cap: softCap, over, advice,
746
+ cum_usd: u.cum_usd, compactions: u.compactions, soft_cap: softCap, over, new_session: newSession, advice,
654
747
  };
655
748
  }
656
749
 
657
750
  const CTX_WARN_KEEP_MS = 14 * 24 * 3600 * 1000;
751
+ const ORCH_NEW_SESSION_TOKENS = 400000;
752
+ const ORCH_NEW_SESSION_COMPACTIONS = 5;
658
753
 
659
754
  /** 0 = within the cap; 1 = over the cap, 2 = over 2×, 3 = over 4×, … */
660
755
  function ctxWarnLevel(growth, cap) {
@@ -724,13 +819,18 @@ function cmdState(opts) {
724
819
  const ctx = ctxOf(opts);
725
820
  const sid = typeof opts.session === 'string' && opts.session ? opts.session
726
821
  : (process.env.CLAUDE_CODE_SESSION_ID || null);
822
+ // retro C6: knowledge deltas older than 24 h that no store has applied (file reads only, never throws)
823
+ const deltas = core.scanUnmergedDeltas();
824
+ const deltaLines = core.unmergedDeltaLines(deltas);
727
825
  if (!sid) {
728
- if (opts.json) return { code: 0, out: JSON.stringify({ session_id: null, generated_at: core.nowIso(), runs_dir: null, plans: [], tasks: [] }) };
729
- return { code: 0, out: 'Không có session (thiếu CLAUDE_CODE_SESSION_ID, dùng --session <id>).' };
826
+ if (opts.json) {
827
+ return { code: 0, out: JSON.stringify(Object.assign({ session_id: null, generated_at: core.nowIso(), runs_dir: null, plans: [], tasks: [] }, deltas.count ? { unmerged_deltas: deltas } : {})) };
828
+ }
829
+ return { code: 0, out: ['Không có session (thiếu CLAUDE_CODE_SESSION_ID, dùng --session <id>).'].concat(deltaLines).join('\n') };
730
830
  }
731
831
  const state = core.buildSessionState(core.readEvents(ctx), sid, { plansDir: ctx.paths.plans });
732
- if (opts.json) return { code: 0, out: JSON.stringify(state, null, 2) };
733
- return { code: 0, out: core.formatSessionState(state, core.STATE_TEXT_CAP) };
832
+ if (opts.json) return { code: 0, out: JSON.stringify(deltas.count ? Object.assign({}, state, { unmerged_deltas: deltas }) : state, null, 2) };
833
+ return { code: 0, out: [core.formatSessionState(state, core.STATE_TEXT_CAP)].concat(deltaLines).join('\n') };
734
834
  }
735
835
 
736
836
  // ------------------------------------------------------------------ stats / retro
@@ -747,12 +847,14 @@ function pct(v) {
747
847
  function cmdStats(opts) {
748
848
  const ctx = ctxOf(opts);
749
849
  const { lambda } = lambdaOf(opts, ctx);
750
- const { cells, steers, orchestration } = agg.analyze(core.readEvents(ctx), { lambda });
850
+ const events = core.readEvents(ctx);
851
+ const { cells, steers, orchestration, tasks, batch_verify: batchVerify } = agg.analyze(events, { lambda });
852
+ const modes = agg.modeSummary(events, tasks);
751
853
  const type = typeof opts.type === 'string' ? opts.type : null;
752
854
  const rows = type ? cells.filter((c) => c.task_type === type) : cells;
753
855
  const steerRows = type ? steers.filter((s) => s.task_type === type) : steers;
754
856
  if (opts.json) {
755
- return { code: 0, out: JSON.stringify({ lambda_usd_per_min: lambda, min_n: agg.MIN_N, cells: rows, steers: steerRows, orchestration }, null, 2) };
857
+ return { code: 0, out: JSON.stringify({ lambda_usd_per_min: lambda, min_n: agg.MIN_N, cells: rows, steers: steerRows, orchestration, modes, batch_verify: batchVerify }, null, 2) };
756
858
  }
757
859
  if (!rows.length) {
758
860
  return { code: 0, out: orchestration.sessions ? `Chưa có dữ liệu theo ô.\n${orchLine(orchestration)}` : 'Chưa có dữ liệu.' };
@@ -779,13 +881,54 @@ function cmdStats(opts) {
779
881
  }
780
882
  return {
781
883
  code: 0,
782
- out: `${table(t)}\n\n${table(s)}\n\n${orchLine(orchestration)}\n\n`
884
+ out: `${table(t)}\n\n${table(s)}\n\n${modeTable(modes)}${batchVerifyTable(batchVerify)}${orchLine(orchestration)}\n\n`
783
885
  + 'n = task tính tỉ lệ (/số đã giao, (-k) = bị loại: reject-scope hoặc lượt exec đầu bị steer plan/redirect). '
784
886
  + 'accept% = thành công theo verdict người dùng đầu tiên, chưa có thì verdict agent; agent_pass% = chỉ tầng agent. '
785
887
  + `E[cost], E[time] tính trọn mọi lượt + review của task. score = E[cost] + λ·p50[time] (trung vị, không dùng TB: thời gian chờ thổi phồng TB), λ=${lambda} USD/phút.`,
786
888
  };
787
889
  }
788
890
 
891
+ /** Stats by mode (core 0.7.0): one row per mode with tasks, then a blank line; '' when only normal has data. */
892
+ function modeTable(modes) {
893
+ const keys = agg.MODE_KEYS.filter((m) => modes[m] && modes[m].tasks);
894
+ if (!keys.length || (keys.length === 1 && keys[0] === 'normal')) return '';
895
+ const t = [['mode', 'task', 'có verifier', 'catch%', 'reject%', 'phút verify', 'test_s', 'test_s/task', 'lần cả suite', 'verdict self', 'fail verify gộp']];
896
+ for (const m of keys) {
897
+ const r = modes[m];
898
+ t.push([
899
+ m, r.tasks, r.verified_tasks,
900
+ r.checked_verdicts ? `${pct(r.catch_rate)} (${r.checked_verdicts})` : '-',
901
+ r.user_verdict_tasks ? `${pct(r.reject_rate)} (${r.user_verdict_tasks})` : '-',
902
+ fmtNum(r.verify_min), fmtNum(r.test_s), r.test_s_per_task === null ? '-' : fmtNum(r.test_s_per_task),
903
+ r.full_suite_runs, r.self_verdicts, r.batch_verify_fails,
904
+ ]);
905
+ }
906
+ return `${table(t)}\n\n`;
907
+ }
908
+
909
+ /** core 0.7.1 (C5): the batched verifiers' own cell (model/effort), kept out of the task cells; '' when none. */
910
+ function batchVerifyTable(rows) {
911
+ if (!rows || !rows.length) return '';
912
+ const t = [['verifier gộp', 'lần', 'task', 'Σ cost', 'E[cost]', 'p50 cost', 'p50 time']];
913
+ for (const r of rows) {
914
+ t.push([
915
+ `${chooser.shortModel(r.model) || r.model || '?'}/${r.effort || '?'}`, r.n, r.tasks,
916
+ fmtUsd(r.cost_total_usd), fmtUsd(r.cost_mean_usd), fmtUsd(r.cost_p50_usd),
917
+ r.time_p50_min === null ? '-' : `${fmtNum(r.time_p50_min)}m`,
918
+ ]);
919
+ }
920
+ return `${table(t)}\n(chi phí verifier gộp là ô riêng, không cộng vào ô task_type × tier của bước fast mang task_id của nó)\n\n`;
921
+ }
922
+
923
+ /** One retro line: per mode with tasks. */
924
+ function modeRetroLine(modes) {
925
+ const parts = agg.MODE_KEYS.filter((m) => modes[m].tasks).map((m) => {
926
+ const r = modes[m];
927
+ return `${m} ${r.tasks} task, catch ${pct(r.catch_rate)}, reject ${pct(r.reject_rate)}, verify ${fmtNum(r.verify_min)}m, test ${fmtNum(r.test_s)}s, cả suite ×${r.full_suite_runs}`;
928
+ });
929
+ return `Theo mode: ${parts.join(' · ') || '-'}`;
930
+ }
931
+
789
932
  const HIGH_STEER_RATE = 0.3;
790
933
  const HIGH_STEER_MIN_TASKS = 3;
791
934
 
@@ -793,7 +936,8 @@ function cmdRetro(opts) {
793
936
  const ctx = ctxOf(opts);
794
937
  const { prof, lambda } = lambdaOf(opts, ctx);
795
938
  const events = core.readEvents(ctx);
796
- const { agents, tasks, cells, steers, userVerdicts, overrides, orchestration } = agg.analyze(events, { lambda });
939
+ const { agents, tasks, cells, steers, userVerdicts, overrides, orchestration, batch_verify: batchVerify } = agg.analyze(events, { lambda });
940
+ const modes = agg.modeSummary(events, tasks);
797
941
  const reasons = {};
798
942
  const outcomes = {};
799
943
  const userOutcomes = {};
@@ -833,6 +977,10 @@ function cmdRetro(opts) {
833
977
  multi_attempt_tasks: escalations,
834
978
  discarded_tasks: agg.discardedTaskIds(events).size,
835
979
  overrides: overrides.length,
980
+ // core 0.7.0: fast-mode evidence kept apart from the learning data (CONTRACT "Mode theo yêu cầu")
981
+ self_verdicts: agents.reduce((s, a) => s + (a.self_verdicts || 0), 0),
982
+ auto_accept: tasks.filter((t) => t.auto_accept).length,
983
+ auto_overturned: tasks.filter((t) => t.auto_overturned).length,
836
984
  cost_usd: agents.reduce((s, a) => s + (a.cost_usd || 0), 0),
837
985
  },
838
986
  lambda_usd_per_min: prof.lambda,
@@ -845,6 +993,8 @@ function cmdRetro(opts) {
845
993
  high_steer_task_types: highSteer,
846
994
  overrides: overrides.map((o) => ({ ts: o.ts, task_type: o.task_type, suggested_tier: o.suggested_tier, chosen_tier: o.chosen_tier })),
847
995
  orchestration,
996
+ modes,
997
+ batch_verify: batchVerify,
848
998
  cells,
849
999
  };
850
1000
  if (opts.json) return { code: 0, out: JSON.stringify(summary, null, 2) };
@@ -852,10 +1002,13 @@ function cmdRetro(opts) {
852
1002
  `Retro ${summary.dept || ''} — ${summary.period ? `${summary.period.from.slice(0, 10)} → ${summary.period.to.slice(0, 10)}` : 'chưa có dữ liệu'}`,
853
1003
  `${summary.totals.tasks} task, ${summary.totals.agents} agent, chi phí ≈ ${fmtUsd(summary.totals.cost_usd)}, ${summary.totals.untagged_agents} agent không thẻ, ${summary.totals.no_user_verdict_tasks} task chưa có verdict người dùng, ${escalations} task nhiều lượt, ${overrides.length} override.`,
854
1004
  `Người dùng: ${Object.entries(userOutcomes).map(([k, v]) => `${k}=${v}`).join(', ') || '-'} · Agent: ${Object.entries(outcomes).map(([k, v]) => `${k}=${v}`).join(', ') || '-'}`,
1005
+ `Fast: verdict self=${summary.totals.self_verdicts} (không vào agent_pass_rate), tự nghiệm thu auto_accept=${summary.totals.auto_accept}, người dùng lật auto_overturned=${summary.totals.auto_overturned}`,
855
1006
  `Lý do hay gặp: ${summary.reason_codes.slice(0, 8).map((r) => `${r.code}×${r.count}`).join(', ') || '-'}`,
856
1007
  `Loại task: ${summary.task_types.map((t) => `${t.task_type}(${t.n})`).join(', ') || '-'}`,
857
1008
  `Steer nhiều (brainstorm hỏi quá ít): ${highSteer.map((s) => `${s.task_type} ${pct(s.steer_rate)}`).join(', ') || '-'}`,
858
1009
  orchLine(orchestration),
1010
+ modeRetroLine(modes),
1011
+ ...(batchVerify.length ? [`Verifier gộp (ô riêng): ${batchVerify.map((r) => `${chooser.shortModel(r.model) || r.model || '?'}/${r.effort || '?'} ×${r.n}, Σ ${fmtUsd(r.cost_total_usd)}, p50 ${r.time_p50_min === null ? '-' : `${fmtNum(r.time_p50_min)}m`}`).join(' · ')}`] : []),
859
1012
  'Chi tiết theo ô: `dieu-phoi stats`; đưa cho LLM: `dieu-phoi retro --json`.',
860
1013
  ];
861
1014
  return { code: 0, out: lines.join('\n') };
@@ -863,6 +1016,13 @@ function cmdRetro(opts) {
863
1016
 
864
1017
  // ------------------------------------------------------------------ choose
865
1018
 
1019
+ /** --mode value (validated) or undefined; a bare --mode is an error. */
1020
+ function modeOpt(opts) {
1021
+ if (opts.mode === undefined) return undefined;
1022
+ if (typeof opts.mode !== 'string' || !core.isMode(opts.mode)) throw new Error(`--mode phải là ${core.MODES.join('|')}`);
1023
+ return opts.mode;
1024
+ }
1025
+
866
1026
  function chooseInput(opts, ctx, events) {
867
1027
  return {
868
1028
  type: opts.type,
@@ -870,6 +1030,9 @@ function chooseInput(opts, ctx, events) {
870
1030
  urgency: typeof opts.urgency === 'string' ? opts.urgency : 'normal',
871
1031
  reversible: yesNo(opts.reversible, true),
872
1032
  verifiable: yesNo(opts.verifiable, true),
1033
+ publish: yesNo(opts.publish, false),
1034
+ dependents: yesNo(opts.dependents, false),
1035
+ mode: modeOpt(opts),
873
1036
  role: typeof opts.role === 'string' ? opts.role : 'exec',
874
1037
  seed: opts.seed,
875
1038
  taskId: typeof opts.task === 'string' ? opts.task : null,
@@ -894,6 +1057,18 @@ function overrideEvent(ctx, taskId, taskType, suggested, chosen) {
894
1057
  }, core.commonFields(ctx));
895
1058
  }
896
1059
 
1060
+ /**
1061
+ * core 0.7.0 (s2): mode_suggest for choose (exec only). The root's CURRENT mode decides (a dispatched step keeps its
1062
+ * fast mode, but once the user moved the root back there is nothing to suggest). Never changes the mode.
1063
+ */
1064
+ function chooseModeSuggest(ctx, input, res, mi) {
1065
+ const root = mi && mi.root ? mi.root : null;
1066
+ const rootEv = root ? core.rootModeFromEvents(input.events, root) : null;
1067
+ const mode = rootEv ? rootEv.mode : res.mode;
1068
+ const sug = core.modeSuggestion(input.events, root, { mode, taskIds: core.rootTaskIds(core.loadPlans(ctx.paths.plans), root) });
1069
+ return { mode_suggest: sug.mode_suggest, mode_suggest_reason: sug.mode_suggest_reason, mode_suggest_root: sug.mode_suggest ? root : null };
1070
+ }
1071
+
897
1072
  function cmdChoose(opts) {
898
1073
  const ctx = ctxOf(opts);
899
1074
  if (opts.override !== undefined && typeof opts.task !== 'string') {
@@ -901,11 +1076,25 @@ function cmdChoose(opts) {
901
1076
  }
902
1077
  if (opts.task !== undefined) requireTaskId(typeof opts.task === 'string' ? opts.task : '');
903
1078
  const input = chooseInput(opts, ctx, core.readEvents(ctx));
1079
+ let modeSource = 'flag';
1080
+ const mi = input.taskId ? core.taskModeInfo(ctx, input.taskId, input.events) : null;
1081
+ if (input.mode === undefined) {
1082
+ if (mi) {
1083
+ input.mode = mi.mode;
1084
+ modeSource = mi.source;
1085
+ } else {
1086
+ input.mode = core.DEFAULT_MODE;
1087
+ modeSource = 'default';
1088
+ }
1089
+ }
904
1090
  const res = chooser.choose(input);
1091
+ res.mode_source = modeSource;
1092
+ if (res.verify_plan) Object.assign(res, chooseModeSuggest(ctx, input, res, mi));
905
1093
  const orch = measureOrch(opts, ctx, input.profile);
906
1094
  res.orch_ctx_tokens = orch.ctx_tokens;
907
1095
  res.orch_ctx_growth = orch.ctx_growth;
908
1096
  res.orch_ctx_over = orch.over;
1097
+ res.orch_new_session = orch.new_session === true;
909
1098
  res.orch_ctx_warn = ctxWarnOnce(ctx, orch); // over AND a new threshold crossed since the last warning
910
1099
  if (res.overridden) {
911
1100
  const tid = input.taskId;
@@ -916,18 +1105,21 @@ function cmdChoose(opts) {
916
1105
  core.recordSuggestion(ctx, {
917
1106
  task_id: input.taskId, role: res.role || input.role, suggested_tier: res.suggested_tier || res.tier, choose_tier: res.tier,
918
1107
  source: 'choose', keep_suggested: input.approved !== null && !res.overridden,
1108
+ choose_task_type: res.task_type, choose_score: res.score, explored: res.explored, reason_code: res.reason_code,
1109
+ thompson_score: res.thompson_score === null ? undefined : res.thompson_score,
919
1110
  });
920
1111
  }
921
1112
  if (opts.json) return { code: 0, out: JSON.stringify(res) };
922
1113
  const typeWarn = res.type_warning && !String(res.reason).includes(res.type_warning) ? `\nCẢNH BÁO: ${res.type_warning}` : '';
923
- const warn = `${typeWarn}${res.orch_ctx_warn ? `\nCẢNH BÁO: ${orch.advice}` : ''}`;
1114
+ const sugLine = res.mode_suggest ? `\n${core.modeSuggestLine(res.mode_suggest_root, 'fast', res.mode_suggest_reason)}` : '';
1115
+ const warn = `${typeWarn}${res.orch_ctx_warn ? `\nCẢNH BÁO: ${orch.advice}` : ''}${sugLine}`;
924
1116
  const est =`~${fmtUsd(res.est_cost_usd)} · ~${fmtNum(res.est_time_min)} phút (${res.est_source === 'data' ? 'dữ liệu' : 'ước lượng thô'})`;
925
1117
  if (res.action === 'ask_user') {
926
1118
  return { code: 0, out: `HỎI NGƯỜI DÙNG — ${res.reason}${warn}` };
927
1119
  }
928
1120
  return {
929
1121
  code: 0,
930
- out: `model=${res.model} effort=${res.effort}${res.explored ? ' (thăm dò)' : ''}${res.overridden ? ' (override)' : ''}${res.deviation ? ' (LỆCH khỏi tier đã duyệt)' : ''} ${est} — ${res.reason}${warn}`,
1122
+ out: `model=${res.model} effort=${res.effort}${res.explored ? ' (thăm dò)' : ''}${res.overridden ? ' (override)' : ''}${res.deviation ? ' (LỆCH khỏi tier đã duyệt)' : ''} ${est} — ${res.reason}${res.verify_plan ? ` | mode=${res.mode} kiểm=${res.verify_plan} (${res.test_scope === 'selective' ? 'test chọn lọc' : 'cả suite'})` : ''}${warn}`,
931
1123
  };
932
1124
  }
933
1125
 
@@ -974,6 +1166,8 @@ function buildPlanTable(rawSteps, base) {
974
1166
  score: s.score,
975
1167
  verifiable: s.verifiable === undefined ? true : yesNo(s.verifiable, true),
976
1168
  reversible: s.reversible === undefined ? true : yesNo(s.reversible, true),
1169
+ publish: s.publish === undefined ? false : yesNo(s.publish, false),
1170
+ mode: s.mode === undefined || s.mode === null ? null : s.mode,
977
1171
  urgency: s.urgency,
978
1172
  deps: deps.map(String),
979
1173
  };
@@ -990,6 +1184,9 @@ function buildPlanTable(rawSteps, base) {
990
1184
  verifiable: s.verifiable,
991
1185
  reversible: s.reversible,
992
1186
  urgency: s.urgency || base.urgency,
1187
+ publish: s.publish === true || base.publish === true,
1188
+ dependents: steps.some((o) => o !== s && o.deps.includes(s.step)),
1189
+ mode: s.mode || base.mode || core.DEFAULT_MODE,
993
1190
  taskId: s.task || base.taskId,
994
1191
  override: s.override || base.override,
995
1192
  role: s.role || base.role,
@@ -999,6 +1196,7 @@ function buildPlanTable(rawSteps, base) {
999
1196
  const head = {
1000
1197
  step: s.step, title: s.title, type: s.type, task_id: s.task || null, score: Number(s.score),
1001
1198
  verifiable: s.verifiable, reversible: s.reversible, depends_on: s.deps,
1199
+ mode: s.mode || base.mode || core.DEFAULT_MODE, mode_override: s.mode || null,
1002
1200
  };
1003
1201
  try {
1004
1202
  return Object.assign(head, call());
@@ -1015,7 +1213,10 @@ function buildPlanTable(rawSteps, base) {
1015
1213
  const ok = rows.filter((r) => r.action !== 'refused');
1016
1214
  const costs = ok.map((r) => r.est_cost_usd);
1017
1215
  const totalCost = costs.every((c) => typeof c === 'number') ? costs.reduce((a, b) => a + b, 0) : null;
1216
+ const selfBatch = rows.filter((r) => r.verify_plan === 'self+batch').length;
1018
1217
  return {
1218
+ mode: base.mode || core.DEFAULT_MODE,
1219
+ self_batch_steps: selfBatch,
1019
1220
  steps: rows,
1020
1221
  refused: rows.filter((r) => r.action === 'refused').map((r) => ({ step: r.step, error: r.error })),
1021
1222
  total_cost_usd: totalCost === null ? null : Math.round(totalCost * 1000) / 1000,
@@ -1091,6 +1292,8 @@ function recordPlanSuggestions(ctx, plan) {
1091
1292
  if (!r.task_id || !r.tier || r.action !== 'dispatch') continue;
1092
1293
  const ok = core.recordSuggestion(ctx, {
1093
1294
  task_id: r.task_id, role: r.role || 'exec', suggested_tier: r.suggested_tier || r.tier, choose_tier: r.tier, source: 'plan',
1295
+ choose_task_type: r.task_type, choose_score: r.score, explored: r.explored, reason_code: r.reason_code,
1296
+ thompson_score: r.thompson_score === null ? undefined : r.thompson_score,
1094
1297
  });
1095
1298
  if (ok) n++;
1096
1299
  }
@@ -1111,10 +1314,10 @@ function recordApprovedPlan(ctx, root, stepsFile, plan) {
1111
1314
  steps_file: stepsFile.split(path.sep).join('/'),
1112
1315
  approved_at: core.nowIso(),
1113
1316
  approval: prevApproval(prev) + 1,
1114
- steps: plan.steps.map((r) => ({
1317
+ steps: plan.steps.map((r) => Object.assign({
1115
1318
  id: r.step, task_id: r.task_id || null, title: r.title === undefined ? null : r.title, type: r.task_type || r.type || null,
1116
1319
  tier: r.action === 'refused' ? null : (r.tier || null), depends_on: r.depends_on,
1117
- })),
1320
+ }, r.mode_override ? { mode: r.mode_override } : {})),
1118
1321
  history,
1119
1322
  };
1120
1323
  core.writeFileAtomic(file, `${JSON.stringify(rec, null, 2)}\n`);
@@ -1151,8 +1354,19 @@ function cmdPlanTable(opts) {
1151
1354
  throw new Error(`plan-table: không đọc được ${opts.file}: ${e.message}`);
1152
1355
  }
1153
1356
  const ctx = ctxOf(opts);
1357
+ // steps.json: array (old) or { mode?, steps: [...] }; root mode = --mode > current mode event of --root > file.mode > normal
1358
+ let fileMode;
1359
+ if (raw && !Array.isArray(raw) && typeof raw === 'object' && Array.isArray(raw.steps)) {
1360
+ if (raw.mode !== undefined && raw.mode !== null) {
1361
+ if (!core.isMode(raw.mode)) throw new Error(`plan-table: "mode" trong file phải là ${core.MODES.join('|')} (nhận "${raw.mode}")`);
1362
+ fileMode = raw.mode;
1363
+ }
1364
+ raw = raw.steps;
1365
+ }
1154
1366
  const base = chooseInput(Object.assign({}, opts, { type: 'x', score: 0, override: undefined }), ctx, core.readEvents(ctx));
1155
1367
  delete base.override;
1368
+ const rootEv = typeof opts.root === 'string' && core.isValidTaskId(opts.root) ? core.rootModeFromEvents(base.events, opts.root) : null;
1369
+ base.mode = base.mode || (rootEv ? rootEv.mode : null) || fileMode || core.DEFAULT_MODE;
1156
1370
  const plan = buildPlanTable(raw, base);
1157
1371
  const problems = planTaskIdProblems(plan, opts.approve ? String(opts.root) : null);
1158
1372
  if (opts.approve && problems.length) {
@@ -1162,26 +1376,33 @@ function cmdPlanTable(opts) {
1162
1376
  const typeNotes = planTypeNotes(plan);
1163
1377
  if (typeNotes.length) process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${typeNotes.join('; ')}\n`);
1164
1378
  const rec = opts.approve ? recordApprovedPlan(ctx, String(opts.root), path.resolve(opts.file), plan) : null;
1379
+ let modeLogged = false;
1380
+ if (rec && (rootEv ? rootEv.mode !== plan.mode : plan.mode !== core.DEFAULT_MODE)) {
1381
+ core.appendEvent(ctx, modeEvent(ctx, rec.root_task_id, plan.mode, 'plan', opts.reason));
1382
+ modeLogged = true;
1383
+ }
1165
1384
  const ovs = rec ? planOverrideEvents(ctx, plan, base.events) : [];
1166
1385
  for (const ev of ovs) core.appendEvent(ctx, ev);
1167
1386
  if (rec) recordPlanSuggestions(ctx, plan);
1168
1387
  if (opts.json) {
1169
1388
  const out = rec ? Object.assign({}, plan, {
1170
- approved: { root_task_id: rec.root_task_id, approval: rec.approval, record: rec.file, overrides_logged: ovs.length },
1389
+ approved: { root_task_id: rec.root_task_id, approval: rec.approval, record: rec.file, overrides_logged: ovs.length, mode_logged: modeLogged },
1171
1390
  }) : plan;
1172
1391
  return { code: 0, out: JSON.stringify(out, null, 2) };
1173
1392
  }
1174
- const lines = ['| Bước | Model/effort | Rubric (điểm · kiểm chứng · đảo ngược) | Lý do | Ước tính |', '|---|---|---|---|---|'];
1393
+ const lines = ['| Bước | Model/effort | Rubric (điểm · kiểm chứng · đảo ngược) | Lý do | Ước tính | Kiểm |', '|---|---|---|---|---|---|'];
1175
1394
  for (const r of plan.steps) {
1176
1395
  const tierCell = r.action === 'refused' ? '—' : (r.action === 'ask_user' ? `${r.tier} (hỏi người dùng)` : r.tier);
1177
1396
  const yn = (v) => (v ? 'y' : 'n');
1178
1397
  const rubric = `${Number.isFinite(r.score) ? r.score : '?'} · ${yn(r.verifiable)} · ${yn(r.reversible)}`;
1179
1398
  const why = `${r.type}${r.depends_on.length ? `, sau ${r.depends_on.join(', ')}` : ''} — ${r.reason.replace(/; profile: .*$/, '')}`;
1180
1399
  const est = r.action === 'refused' ? '-' : `~${fmtUsd(r.est_cost_usd)} · ~${fmtNum(r.est_time_min)} phút${r.est_source === 'rough' ? ' (thô)' : ''}`;
1181
- lines.push(`| ${mdCell(r.title ? `${r.step}. ${r.title}` : r.step)} | ${mdCell(tierCell)} | ${mdCell(rubric)} | ${mdCell(why)} | ${mdCell(est)} |`);
1400
+ const kiem = r.action === 'refused' || !r.verify_plan ? '—' : `${r.verify_plan}${r.mode_override ? ` (mode ${r.mode_override})` : ''}`;
1401
+ lines.push(`| ${mdCell(r.title ? `${r.step}. ${r.title}` : r.step)} | ${mdCell(tierCell)} | ${mdCell(rubric)} | ${mdCell(why)} | ${mdCell(est)} | ${mdCell(kiem)} |`);
1182
1402
  }
1183
1403
  lines.push('');
1184
1404
  lines.push(`**Tổng chi phí ước tính:** ~${fmtUsd(plan.total_cost_usd)} · **Thời gian (đường găng, chạy song song khi không phụ thuộc):** ~${fmtNum(plan.critical_path_min)} phút (cộng dồn ${fmtNum(plan.sum_time_min)} phút)`);
1405
+ lines.push(`**Mode:** ${plan.mode}${plan.self_batch_steps ? ` · Kiểm (fast): bỏ ${plan.self_batch_steps} verifier riêng, 1 verify gộp (cả suite một lần) trước khi trình người dùng` : ''}`);
1185
1406
  if (plan.rough) lines.push('', '_(thô) = ô chưa đủ 5 task, ước lượng theo giả định token của từng tier._');
1186
1407
  if (plan.refused.length) {
1187
1408
  lines.push('', `**Cảnh báo:** ${plan.refused.length} bước bị từ chối (${plan.refused.map((x) => x.step).join(', ')}), không tính vào tổng — sửa rồi chạy lại.`);
@@ -1189,11 +1410,11 @@ function cmdPlanTable(opts) {
1189
1410
  `);
1190
1411
  }
1191
1412
  if (typeNotes.length) lines.push('', `**Loại task:** ${typeNotes.join('; ')}`);
1192
- if (rec) lines.push('', `Đã ghi kế hoạch đã duyệt (root ${rec.root_task_id}, lần duyệt ${rec.approval})${ovs.length ? `; ghi ${ovs.length} override` : ''}`);
1413
+ if (rec) lines.push('', `Đã ghi kế hoạch đã duyệt (root ${rec.root_task_id}, lần duyệt ${rec.approval})${ovs.length ? `; ghi ${ovs.length} override` : ''}${modeLogged ? `; ghi mode ${plan.mode}` : ''}`);
1193
1414
  return { code: 0, out: lines.join('\n') };
1194
1415
  }
1195
1416
 
1196
1417
  module.exports = {
1197
- skillDirOf, cmdSetup, cmdUninstall, cmdDoctor, cmdStart, checkSinkLive, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdDiscard, ctxWarnOnce, ctxWarnLevel, sweepStaleTmp, planOverrideEvents, recordPlanSuggestions, cmdStats, cmdRetro, cmdPrune: (o) => require('./prune.cjs').cmdPrune(o), cmdChoose, cmdCtx, cmdState, measureOrch,
1418
+ skillDirOf, cmdSetup, cmdUninstall, cmdDoctor, cmdStart, checkSinkLive, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdDiscard, ctxWarnOnce, ctxWarnLevel, sweepStaleTmp, planOverrideEvents, recordPlanSuggestions, cmdStats, cmdRetro, cmdPrune: (o) => require('./prune.cjs').cmdPrune(o), cmdChoose, cmdMode, modeEvent, cmdCtx, cmdState, measureOrch,
1198
1419
  cmdPlanTable, cmdNewId, requireTaskId, buildPlanTable, recordApprovedPlan, planTaskIdProblems, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
1199
1420
  };