@kienntpixon/dieu-phoi 0.6.1 → 0.7.0

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
 
@@ -295,8 +295,13 @@ async function checkSinkLive(ctx) {
295
295
  */
296
296
  function cmdStart(opts) {
297
297
  const ctx = ctxOf(opts);
298
+ const wantMode = modeOpt(opts); // validate before the run dir is created
298
299
  const reg = setupLib.registration(claudeDirOf(opts));
299
300
  const info = core.startInfo(ctx, repoOf(opts));
301
+ if (wantMode) {
302
+ core.appendEvent(ctx, modeEvent(ctx, info.id, wantMode, 'start', opts.reason));
303
+ info.mode = wantMode;
304
+ }
300
305
  const h = info.health;
301
306
  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
307
  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 +319,58 @@ function cmdStart(opts) {
314
319
  `step_ids: ${info.step_ids.join(' ')}`,
315
320
  `health: ${core.healthLine(h)}`,
316
321
  `scout_brief: ${info.scout_brief || '(không thấy — dùng references/brief-template.md mục E)'}`,
322
+ core.modeLine(info),
317
323
  ...core.orchLessonLines(info),
318
324
  ...core.knowledgeLines(info),
319
325
  ];
320
326
  return { code: 0, out: out.join('\n') };
321
327
  }
322
328
 
329
+ // ------------------------------------------------------------------ mode
330
+
331
+ /** One local-only mode event for a request (root_task_id). source: start | cmd | plan. */
332
+ function modeEvent(ctx, root, mode, source, reason, session) {
333
+ const sid = typeof session === 'string' && session ? session : (process.env.CLAUDE_CODE_SESSION_ID || null);
334
+ return Object.assign({
335
+ schema: core.SCHEMA,
336
+ kind: 'mode',
337
+ ts: core.nowIso(),
338
+ agent_id: `root:${root}`,
339
+ task_id: null,
340
+ root_task_id: root,
341
+ mode,
342
+ source,
343
+ reason_code: kebab(reason),
344
+ session_id: sid,
345
+ }, core.commonFields(ctx));
346
+ }
347
+
348
+ /**
349
+ * `dieu-phoi mode --root <root_task_id> [fast|normal|strict] [--reason R] [--session S]`: show or change the mode of a
350
+ * request. One event per change; the same mode again writes nothing. Steps not yet dispatched follow the new mode,
351
+ * dispatched steps keep the mode they were given. Never changes a mode by itself.
352
+ */
353
+ function cmdMode(opts) {
354
+ const usage = 'Cú pháp: dieu-phoi mode --root <root_task_id> [fast|normal|strict] [--reason R] [--session S] [--json]';
355
+ if (typeof opts.root !== 'string') throw new Error(usage);
356
+ const root = requireTaskId(opts.root);
357
+ const ctx = ctxOf(opts);
358
+ const cur = core.rootModeFromEvents(core.readEvents(ctx), root);
359
+ const was = cur ? cur.mode : core.DEFAULT_MODE;
360
+ const want = opts._[0];
361
+ const show = (extra) => (opts.json ? JSON.stringify(Object.assign({ root_task_id: root, mode: was, previous: was, changed: false }, extra)) : null);
362
+ if (want === undefined) {
363
+ return { code: 0, out: show({}) || `root ${root}: mode=${was}${cur ? ` (từ ${cur.ts}, nguồn ${cur.source || '?'})` : ' (mặc định, chưa đặt)'}` };
364
+ }
365
+ if (!core.isMode(want)) throw new Error(`mode phải là ${core.MODES.join('|')} (nhận "${want}"). ${usage}`);
366
+ if (cur && was === want) {
367
+ return { code: 0, out: show({}) || `root ${root}: vẫn ${want} (không đổi, không ghi sự kiện)` };
368
+ }
369
+ core.appendEvent(ctx, modeEvent(ctx, root, want, 'cmd', opts.reason, opts.session));
370
+ if (opts.json) return { code: 0, out: JSON.stringify({ root_task_id: root, mode: want, previous: was, changed: true }) };
371
+ return { code: 0, out: `root ${root}: ${was} → ${want} (áp cho bước chưa giao; bước đã giao giữ mode lúc giao)` };
372
+ }
373
+
323
374
  // ------------------------------------------------------------------ hook / sync
324
375
 
325
376
  function cmdHook(opts) {
@@ -426,6 +477,7 @@ function verdictUser(opts, ctx, events) {
426
477
  if (typeof taskId !== 'string' || !taskId) throw new Error('--task cần task_id');
427
478
  requireTaskId(taskId);
428
479
  if (!USER_OUTCOMES.includes(outcome)) throw new Error(`verdict --task: outcome phải là ${USER_OUTCOMES.join('|')}`);
480
+ 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
481
  let cause = typeof opts.cause === 'string' ? opts.cause : null;
430
482
  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
483
  if (outcome === 'accept') cause = null;
@@ -433,6 +485,7 @@ function verdictUser(opts, ctx, events) {
433
485
  if (!known) throw new Error(`Không thấy task "${taskId}" trong log.`);
434
486
  const rounds = events.filter((e) => e.kind === 'verdict' && e.task_id === taskId
435
487
  && agg.normalizeVerdict(e).by === 'user').map((e) => Number(e.round) || 1);
488
+ if (opts.auto) return verdictAuto(opts, ctx, events, taskId, rounds);
436
489
  let round = (rounds.length ? Math.max(...rounds) : 0) + 1;
437
490
  if (opts.round !== undefined) {
438
491
  round = Number(opts.round);
@@ -453,6 +506,33 @@ function verdictUser(opts, ctx, events) {
453
506
  }, core.commonFields(ctx));
454
507
  }
455
508
 
509
+ /**
510
+ * `verdict --task <task_id> accept --auto` (core 0.7.0): a fast-mode step that passed the batch verifier is accepted
511
+ * automatically (`by=auto`). Not a user verdict: never counts in accept_rate/user_outcomes/rounds, never merges
512
+ * knowledge; the user's final `verdict --root … accept` (by=user) or a later reject (wins) still decides.
513
+ */
514
+ function verdictAuto(opts, ctx, events, taskId, userRounds) {
515
+ if (opts.round !== undefined || opts.cause !== undefined) throw new Error('--auto không nhận --round/--cause');
516
+ 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.`);
517
+ const mode = core.taskModeInfo(ctx, taskId, events).mode;
518
+ 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.`);
519
+ const exec = events.find((e) => e.kind === 'dispatch' && e.task_id === taskId && (e.role || 'exec') === 'exec');
520
+ if (exec && typeof exec.score === 'number' && exec.score > 6) throw new Error(`--auto chỉ cho bước score ≤ 6 (task ${taskId} score ${exec.score}).`);
521
+ return Object.assign({
522
+ schema: core.SCHEMA,
523
+ kind: 'verdict',
524
+ ts: core.nowIso(),
525
+ agent_id: `task:${taskId}`,
526
+ task_id: taskId,
527
+ by: 'auto',
528
+ outcome: 'accept',
529
+ cause: null,
530
+ round: null,
531
+ verify: 'tests',
532
+ reason_code: kebab(opts.reason),
533
+ }, core.commonFields(ctx));
534
+ }
535
+
456
536
  /**
457
537
  * `verdict --root <root> accept [--steps a,b]` (core 0.4.0): the user accepts the steps of the latest approved plan
458
538
  * in one call. Covers every step whose task has an exec dispatch and no user verdict yet (a step already rejected
@@ -460,6 +540,7 @@ function verdictUser(opts, ctx, events) {
460
540
  */
461
541
  function verdictRoot(opts, ctx, events) {
462
542
  const root = requireTaskId(String(opts.root));
543
+ 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
544
  if (opts._[0] !== 'accept') {
464
545
  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
546
  }
@@ -552,6 +633,9 @@ function cmdVerdict(opts) {
552
633
  if (kn) process.stderr.write(`${kn.out}\n`);
553
634
  return { code: 0, out: JSON.stringify(ev) };
554
635
  }
636
+ if (ev.by === 'auto') {
637
+ 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).` };
638
+ }
555
639
  if (ev.by === 'user') {
556
640
  const msg = `Đã ghi người dùng ${ev.outcome}${ev.cause ? ` (${ev.cause})` : ''} cho task ${ev.task_id}, vòng ${ev.round}.`;
557
641
  return { code: 0, out: kn ? `${msg}\n${kn.out}` : msg };
@@ -747,12 +831,14 @@ function pct(v) {
747
831
  function cmdStats(opts) {
748
832
  const ctx = ctxOf(opts);
749
833
  const { lambda } = lambdaOf(opts, ctx);
750
- const { cells, steers, orchestration } = agg.analyze(core.readEvents(ctx), { lambda });
834
+ const events = core.readEvents(ctx);
835
+ const { cells, steers, orchestration, tasks } = agg.analyze(events, { lambda });
836
+ const modes = agg.modeSummary(events, tasks);
751
837
  const type = typeof opts.type === 'string' ? opts.type : null;
752
838
  const rows = type ? cells.filter((c) => c.task_type === type) : cells;
753
839
  const steerRows = type ? steers.filter((s) => s.task_type === type) : steers;
754
840
  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) };
841
+ return { code: 0, out: JSON.stringify({ lambda_usd_per_min: lambda, min_n: agg.MIN_N, cells: rows, steers: steerRows, orchestration, modes }, null, 2) };
756
842
  }
757
843
  if (!rows.length) {
758
844
  return { code: 0, out: orchestration.sessions ? `Chưa có dữ liệu theo ô.\n${orchLine(orchestration)}` : 'Chưa có dữ liệu.' };
@@ -779,13 +865,40 @@ function cmdStats(opts) {
779
865
  }
780
866
  return {
781
867
  code: 0,
782
- out: `${table(t)}\n\n${table(s)}\n\n${orchLine(orchestration)}\n\n`
868
+ out: `${table(t)}\n\n${table(s)}\n\n${modeTable(modes)}${orchLine(orchestration)}\n\n`
783
869
  + '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
870
  + '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
871
  + `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
872
  };
787
873
  }
788
874
 
875
+ /** Stats by mode (core 0.7.0): one row per mode with tasks, then a blank line; '' when only normal has data. */
876
+ function modeTable(modes) {
877
+ const keys = agg.MODE_KEYS.filter((m) => modes[m] && modes[m].tasks);
878
+ if (!keys.length || (keys.length === 1 && keys[0] === 'normal')) return '';
879
+ 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']];
880
+ for (const m of keys) {
881
+ const r = modes[m];
882
+ t.push([
883
+ m, r.tasks, r.verified_tasks,
884
+ r.checked_verdicts ? `${pct(r.catch_rate)} (${r.checked_verdicts})` : '-',
885
+ r.user_verdict_tasks ? `${pct(r.reject_rate)} (${r.user_verdict_tasks})` : '-',
886
+ fmtNum(r.verify_min), fmtNum(r.test_s), r.test_s_per_task === null ? '-' : fmtNum(r.test_s_per_task),
887
+ r.full_suite_runs, r.self_verdicts, r.batch_verify_fails,
888
+ ]);
889
+ }
890
+ return `${table(t)}\n\n`;
891
+ }
892
+
893
+ /** One retro line: per mode with tasks. */
894
+ function modeRetroLine(modes) {
895
+ const parts = agg.MODE_KEYS.filter((m) => modes[m].tasks).map((m) => {
896
+ const r = modes[m];
897
+ 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}`;
898
+ });
899
+ return `Theo mode: ${parts.join(' · ') || '-'}`;
900
+ }
901
+
789
902
  const HIGH_STEER_RATE = 0.3;
790
903
  const HIGH_STEER_MIN_TASKS = 3;
791
904
 
@@ -794,6 +907,7 @@ function cmdRetro(opts) {
794
907
  const { prof, lambda } = lambdaOf(opts, ctx);
795
908
  const events = core.readEvents(ctx);
796
909
  const { agents, tasks, cells, steers, userVerdicts, overrides, orchestration } = agg.analyze(events, { lambda });
910
+ const modes = agg.modeSummary(events, tasks);
797
911
  const reasons = {};
798
912
  const outcomes = {};
799
913
  const userOutcomes = {};
@@ -833,6 +947,10 @@ function cmdRetro(opts) {
833
947
  multi_attempt_tasks: escalations,
834
948
  discarded_tasks: agg.discardedTaskIds(events).size,
835
949
  overrides: overrides.length,
950
+ // core 0.7.0: fast-mode evidence kept apart from the learning data (CONTRACT "Mode theo yêu cầu")
951
+ self_verdicts: agents.reduce((s, a) => s + (a.self_verdicts || 0), 0),
952
+ auto_accept: tasks.filter((t) => t.auto_accept).length,
953
+ auto_overturned: tasks.filter((t) => t.auto_overturned).length,
836
954
  cost_usd: agents.reduce((s, a) => s + (a.cost_usd || 0), 0),
837
955
  },
838
956
  lambda_usd_per_min: prof.lambda,
@@ -845,6 +963,7 @@ function cmdRetro(opts) {
845
963
  high_steer_task_types: highSteer,
846
964
  overrides: overrides.map((o) => ({ ts: o.ts, task_type: o.task_type, suggested_tier: o.suggested_tier, chosen_tier: o.chosen_tier })),
847
965
  orchestration,
966
+ modes,
848
967
  cells,
849
968
  };
850
969
  if (opts.json) return { code: 0, out: JSON.stringify(summary, null, 2) };
@@ -852,10 +971,12 @@ function cmdRetro(opts) {
852
971
  `Retro ${summary.dept || ''} — ${summary.period ? `${summary.period.from.slice(0, 10)} → ${summary.period.to.slice(0, 10)}` : 'chưa có dữ liệu'}`,
853
972
  `${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
973
  `Người dùng: ${Object.entries(userOutcomes).map(([k, v]) => `${k}=${v}`).join(', ') || '-'} · Agent: ${Object.entries(outcomes).map(([k, v]) => `${k}=${v}`).join(', ') || '-'}`,
974
+ `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
975
  `Lý do hay gặp: ${summary.reason_codes.slice(0, 8).map((r) => `${r.code}×${r.count}`).join(', ') || '-'}`,
856
976
  `Loại task: ${summary.task_types.map((t) => `${t.task_type}(${t.n})`).join(', ') || '-'}`,
857
977
  `Steer nhiều (brainstorm hỏi quá ít): ${highSteer.map((s) => `${s.task_type} ${pct(s.steer_rate)}`).join(', ') || '-'}`,
858
978
  orchLine(orchestration),
979
+ modeRetroLine(modes),
859
980
  'Chi tiết theo ô: `dieu-phoi stats`; đưa cho LLM: `dieu-phoi retro --json`.',
860
981
  ];
861
982
  return { code: 0, out: lines.join('\n') };
@@ -863,6 +984,13 @@ function cmdRetro(opts) {
863
984
 
864
985
  // ------------------------------------------------------------------ choose
865
986
 
987
+ /** --mode value (validated) or undefined; a bare --mode is an error. */
988
+ function modeOpt(opts) {
989
+ if (opts.mode === undefined) return undefined;
990
+ if (typeof opts.mode !== 'string' || !core.isMode(opts.mode)) throw new Error(`--mode phải là ${core.MODES.join('|')}`);
991
+ return opts.mode;
992
+ }
993
+
866
994
  function chooseInput(opts, ctx, events) {
867
995
  return {
868
996
  type: opts.type,
@@ -870,6 +998,9 @@ function chooseInput(opts, ctx, events) {
870
998
  urgency: typeof opts.urgency === 'string' ? opts.urgency : 'normal',
871
999
  reversible: yesNo(opts.reversible, true),
872
1000
  verifiable: yesNo(opts.verifiable, true),
1001
+ publish: yesNo(opts.publish, false),
1002
+ dependents: yesNo(opts.dependents, false),
1003
+ mode: modeOpt(opts),
873
1004
  role: typeof opts.role === 'string' ? opts.role : 'exec',
874
1005
  seed: opts.seed,
875
1006
  taskId: typeof opts.task === 'string' ? opts.task : null,
@@ -894,6 +1025,18 @@ function overrideEvent(ctx, taskId, taskType, suggested, chosen) {
894
1025
  }, core.commonFields(ctx));
895
1026
  }
896
1027
 
1028
+ /**
1029
+ * core 0.7.0 (s2): mode_suggest for choose (exec only). The root's CURRENT mode decides (a dispatched step keeps its
1030
+ * fast mode, but once the user moved the root back there is nothing to suggest). Never changes the mode.
1031
+ */
1032
+ function chooseModeSuggest(ctx, input, res, mi) {
1033
+ const root = mi && mi.root ? mi.root : null;
1034
+ const rootEv = root ? core.rootModeFromEvents(input.events, root) : null;
1035
+ const mode = rootEv ? rootEv.mode : res.mode;
1036
+ const sug = core.modeSuggestion(input.events, root, { mode, taskIds: core.rootTaskIds(core.loadPlans(ctx.paths.plans), root) });
1037
+ return { mode_suggest: sug.mode_suggest, mode_suggest_reason: sug.mode_suggest_reason, mode_suggest_root: sug.mode_suggest ? root : null };
1038
+ }
1039
+
897
1040
  function cmdChoose(opts) {
898
1041
  const ctx = ctxOf(opts);
899
1042
  if (opts.override !== undefined && typeof opts.task !== 'string') {
@@ -901,7 +1044,20 @@ function cmdChoose(opts) {
901
1044
  }
902
1045
  if (opts.task !== undefined) requireTaskId(typeof opts.task === 'string' ? opts.task : '');
903
1046
  const input = chooseInput(opts, ctx, core.readEvents(ctx));
1047
+ let modeSource = 'flag';
1048
+ const mi = input.taskId ? core.taskModeInfo(ctx, input.taskId, input.events) : null;
1049
+ if (input.mode === undefined) {
1050
+ if (mi) {
1051
+ input.mode = mi.mode;
1052
+ modeSource = mi.source;
1053
+ } else {
1054
+ input.mode = core.DEFAULT_MODE;
1055
+ modeSource = 'default';
1056
+ }
1057
+ }
904
1058
  const res = chooser.choose(input);
1059
+ res.mode_source = modeSource;
1060
+ if (res.verify_plan) Object.assign(res, chooseModeSuggest(ctx, input, res, mi));
905
1061
  const orch = measureOrch(opts, ctx, input.profile);
906
1062
  res.orch_ctx_tokens = orch.ctx_tokens;
907
1063
  res.orch_ctx_growth = orch.ctx_growth;
@@ -920,14 +1076,15 @@ function cmdChoose(opts) {
920
1076
  }
921
1077
  if (opts.json) return { code: 0, out: JSON.stringify(res) };
922
1078
  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}` : ''}`;
1079
+ const sugLine = res.mode_suggest ? `\n${core.modeSuggestLine(res.mode_suggest_root, 'fast', res.mode_suggest_reason)}` : '';
1080
+ const warn = `${typeWarn}${res.orch_ctx_warn ? `\nCẢNH BÁO: ${orch.advice}` : ''}${sugLine}`;
924
1081
  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
1082
  if (res.action === 'ask_user') {
926
1083
  return { code: 0, out: `HỎI NGƯỜI DÙNG — ${res.reason}${warn}` };
927
1084
  }
928
1085
  return {
929
1086
  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}`,
1087
+ 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
1088
  };
932
1089
  }
933
1090
 
@@ -974,6 +1131,8 @@ function buildPlanTable(rawSteps, base) {
974
1131
  score: s.score,
975
1132
  verifiable: s.verifiable === undefined ? true : yesNo(s.verifiable, true),
976
1133
  reversible: s.reversible === undefined ? true : yesNo(s.reversible, true),
1134
+ publish: s.publish === undefined ? false : yesNo(s.publish, false),
1135
+ mode: s.mode === undefined || s.mode === null ? null : s.mode,
977
1136
  urgency: s.urgency,
978
1137
  deps: deps.map(String),
979
1138
  };
@@ -990,6 +1149,9 @@ function buildPlanTable(rawSteps, base) {
990
1149
  verifiable: s.verifiable,
991
1150
  reversible: s.reversible,
992
1151
  urgency: s.urgency || base.urgency,
1152
+ publish: s.publish === true || base.publish === true,
1153
+ dependents: steps.some((o) => o !== s && o.deps.includes(s.step)),
1154
+ mode: s.mode || base.mode || core.DEFAULT_MODE,
993
1155
  taskId: s.task || base.taskId,
994
1156
  override: s.override || base.override,
995
1157
  role: s.role || base.role,
@@ -999,6 +1161,7 @@ function buildPlanTable(rawSteps, base) {
999
1161
  const head = {
1000
1162
  step: s.step, title: s.title, type: s.type, task_id: s.task || null, score: Number(s.score),
1001
1163
  verifiable: s.verifiable, reversible: s.reversible, depends_on: s.deps,
1164
+ mode: s.mode || base.mode || core.DEFAULT_MODE, mode_override: s.mode || null,
1002
1165
  };
1003
1166
  try {
1004
1167
  return Object.assign(head, call());
@@ -1015,7 +1178,10 @@ function buildPlanTable(rawSteps, base) {
1015
1178
  const ok = rows.filter((r) => r.action !== 'refused');
1016
1179
  const costs = ok.map((r) => r.est_cost_usd);
1017
1180
  const totalCost = costs.every((c) => typeof c === 'number') ? costs.reduce((a, b) => a + b, 0) : null;
1181
+ const selfBatch = rows.filter((r) => r.verify_plan === 'self+batch').length;
1018
1182
  return {
1183
+ mode: base.mode || core.DEFAULT_MODE,
1184
+ self_batch_steps: selfBatch,
1019
1185
  steps: rows,
1020
1186
  refused: rows.filter((r) => r.action === 'refused').map((r) => ({ step: r.step, error: r.error })),
1021
1187
  total_cost_usd: totalCost === null ? null : Math.round(totalCost * 1000) / 1000,
@@ -1111,10 +1277,10 @@ function recordApprovedPlan(ctx, root, stepsFile, plan) {
1111
1277
  steps_file: stepsFile.split(path.sep).join('/'),
1112
1278
  approved_at: core.nowIso(),
1113
1279
  approval: prevApproval(prev) + 1,
1114
- steps: plan.steps.map((r) => ({
1280
+ steps: plan.steps.map((r) => Object.assign({
1115
1281
  id: r.step, task_id: r.task_id || null, title: r.title === undefined ? null : r.title, type: r.task_type || r.type || null,
1116
1282
  tier: r.action === 'refused' ? null : (r.tier || null), depends_on: r.depends_on,
1117
- })),
1283
+ }, r.mode_override ? { mode: r.mode_override } : {})),
1118
1284
  history,
1119
1285
  };
1120
1286
  core.writeFileAtomic(file, `${JSON.stringify(rec, null, 2)}\n`);
@@ -1151,8 +1317,19 @@ function cmdPlanTable(opts) {
1151
1317
  throw new Error(`plan-table: không đọc được ${opts.file}: ${e.message}`);
1152
1318
  }
1153
1319
  const ctx = ctxOf(opts);
1320
+ // steps.json: array (old) or { mode?, steps: [...] }; root mode = --mode > current mode event of --root > file.mode > normal
1321
+ let fileMode;
1322
+ if (raw && !Array.isArray(raw) && typeof raw === 'object' && Array.isArray(raw.steps)) {
1323
+ if (raw.mode !== undefined && raw.mode !== null) {
1324
+ if (!core.isMode(raw.mode)) throw new Error(`plan-table: "mode" trong file phải là ${core.MODES.join('|')} (nhận "${raw.mode}")`);
1325
+ fileMode = raw.mode;
1326
+ }
1327
+ raw = raw.steps;
1328
+ }
1154
1329
  const base = chooseInput(Object.assign({}, opts, { type: 'x', score: 0, override: undefined }), ctx, core.readEvents(ctx));
1155
1330
  delete base.override;
1331
+ const rootEv = typeof opts.root === 'string' && core.isValidTaskId(opts.root) ? core.rootModeFromEvents(base.events, opts.root) : null;
1332
+ base.mode = base.mode || (rootEv ? rootEv.mode : null) || fileMode || core.DEFAULT_MODE;
1156
1333
  const plan = buildPlanTable(raw, base);
1157
1334
  const problems = planTaskIdProblems(plan, opts.approve ? String(opts.root) : null);
1158
1335
  if (opts.approve && problems.length) {
@@ -1162,26 +1339,33 @@ function cmdPlanTable(opts) {
1162
1339
  const typeNotes = planTypeNotes(plan);
1163
1340
  if (typeNotes.length) process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${typeNotes.join('; ')}\n`);
1164
1341
  const rec = opts.approve ? recordApprovedPlan(ctx, String(opts.root), path.resolve(opts.file), plan) : null;
1342
+ let modeLogged = false;
1343
+ if (rec && (rootEv ? rootEv.mode !== plan.mode : plan.mode !== core.DEFAULT_MODE)) {
1344
+ core.appendEvent(ctx, modeEvent(ctx, rec.root_task_id, plan.mode, 'plan', opts.reason));
1345
+ modeLogged = true;
1346
+ }
1165
1347
  const ovs = rec ? planOverrideEvents(ctx, plan, base.events) : [];
1166
1348
  for (const ev of ovs) core.appendEvent(ctx, ev);
1167
1349
  if (rec) recordPlanSuggestions(ctx, plan);
1168
1350
  if (opts.json) {
1169
1351
  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 },
1352
+ approved: { root_task_id: rec.root_task_id, approval: rec.approval, record: rec.file, overrides_logged: ovs.length, mode_logged: modeLogged },
1171
1353
  }) : plan;
1172
1354
  return { code: 0, out: JSON.stringify(out, null, 2) };
1173
1355
  }
1174
- const lines = ['| Bước | Model/effort | Rubric (điểm · kiểm chứng · đảo ngược) | Lý do | Ước tính |', '|---|---|---|---|---|'];
1356
+ const lines = ['| Bước | Model/effort | Rubric (điểm · kiểm chứng · đảo ngược) | Lý do | Ước tính | Kiểm |', '|---|---|---|---|---|---|'];
1175
1357
  for (const r of plan.steps) {
1176
1358
  const tierCell = r.action === 'refused' ? '—' : (r.action === 'ask_user' ? `${r.tier} (hỏi người dùng)` : r.tier);
1177
1359
  const yn = (v) => (v ? 'y' : 'n');
1178
1360
  const rubric = `${Number.isFinite(r.score) ? r.score : '?'} · ${yn(r.verifiable)} · ${yn(r.reversible)}`;
1179
1361
  const why = `${r.type}${r.depends_on.length ? `, sau ${r.depends_on.join(', ')}` : ''} — ${r.reason.replace(/; profile: .*$/, '')}`;
1180
1362
  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)} |`);
1363
+ const kiem = r.action === 'refused' || !r.verify_plan ? '—' : `${r.verify_plan}${r.mode_override ? ` (mode ${r.mode_override})` : ''}`;
1364
+ lines.push(`| ${mdCell(r.title ? `${r.step}. ${r.title}` : r.step)} | ${mdCell(tierCell)} | ${mdCell(rubric)} | ${mdCell(why)} | ${mdCell(est)} | ${mdCell(kiem)} |`);
1182
1365
  }
1183
1366
  lines.push('');
1184
1367
  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)`);
1368
+ 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
1369
  if (plan.rough) lines.push('', '_(thô) = ô chưa đủ 5 task, ước lượng theo giả định token của từng tier._');
1186
1370
  if (plan.refused.length) {
1187
1371
  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 +1373,11 @@ function cmdPlanTable(opts) {
1189
1373
  `);
1190
1374
  }
1191
1375
  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` : ''}`);
1376
+ 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
1377
  return { code: 0, out: lines.join('\n') };
1194
1378
  }
1195
1379
 
1196
1380
  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,
1381
+ 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
1382
  cmdPlanTable, cmdNewId, requireTaskId, buildPlanTable, recordApprovedPlan, planTaskIdProblems, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
1199
1383
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kienntpixon/dieu-phoi",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Hook + CLI ghi log subagent của Claude Code và chọn model/effort theo chi phí + thời gian tới lúc nghiệm thu.",
5
5
  "license": "MIT",
6
6
  "author": "kiennt <kiennt@pixon.games>",