@kienntpixon/dieu-phoi 0.6.0 → 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
@@ -8,13 +8,14 @@ const core = require('../hook.cjs');
8
8
  const agg = require('./aggregate.cjs');
9
9
  const chooser = require('./choose.cjs');
10
10
  const setupLib = require('./setup.cjs');
11
+ const cbm = require('./cbm.cjs');
11
12
  const { claudeDirOf, ctxOf, yesNo, repoOf } = require('./args.cjs');
12
13
 
13
14
  const AGENT_OUTCOMES = ['pass', 'fail', 'timeout', 'aborted'];
14
15
  const USER_OUTCOMES = ['accept', 'reject'];
15
16
  const CAUSES = ['quality', 'scope'];
16
17
  const STEER_KINDS = ['minor', 'plan', 'redirect'];
17
- const VERIFY = ['tests', 'lint', 'review', 'judgment', 'none'];
18
+ const VERIFY = ['tests', 'lint', 'review', 'judgment', 'self', 'none'];
18
19
  const INVALID_ID_WINDOW_DAYS = 7;
19
20
  const NEW_ID_MAX = 1000;
20
21
 
@@ -48,6 +49,7 @@ function table(rows) {
48
49
  // ------------------------------------------------------------------ setup / uninstall
49
50
 
50
51
  function cmdSetup(opts) {
52
+ if (opts['with-codebase-memory']) return cmdSetupWithCbm(opts);
51
53
  const res = setupLib.setup({
52
54
  claudeDir: claudeDirOf(opts),
53
55
  dept: typeof opts.dept === 'string' ? opts.dept : undefined,
@@ -75,6 +77,30 @@ function cmdSetup(opts) {
75
77
  return { code: 0, out: lines.join('\n') };
76
78
  }
77
79
 
80
+ /** `setup --with-codebase-memory [--force] [--cbm-dir D]`: normal setup, then install + register codebase-memory-mcp. */
81
+ async function cmdSetupWithCbm(opts) {
82
+ const base = cmdSetup(Object.assign({}, opts, { 'with-codebase-memory': false, json: false }));
83
+ let res;
84
+ let err = null;
85
+ try {
86
+ res = await cbm.install({ claudeDir: claudeDirOf(opts), force: !!opts.force, dir: typeof opts['cbm-dir'] === 'string' ? opts['cbm-dir'] : undefined, allowCustomSource: !!opts['allow-custom-source'] });
87
+ } catch (e) {
88
+ err = e.message;
89
+ }
90
+ if (opts.json) return { code: err ? 1 : 0, out: JSON.stringify({ setup: 'ok', codebase_memory: err ? { ok: false, error: err } : res }, null, 2) };
91
+ const lines = [base.out];
92
+ if (err) {
93
+ lines.push(`codebase-memory-mcp: LỖI — ${err} (setup dieu-phoi vẫn xong; không có gì bị ghi đè)`);
94
+ } else {
95
+ const act = { kept: 'đã có, giữ nguyên (--force để cài lại)', installed: 'đã cài', replaced: 'đã thay' }[res.action];
96
+ lines.push(`codebase-memory-mcp: ${act} v${res.version || '?'} tại ${res.binary.replace(/\\/g, '/')}${res.backup ? ` (bản cũ: ${path.basename(res.backup)})` : ''}`);
97
+ lines.push(`MCP (user scope): ${{ kept: 'đã đăng ký, giữ nguyên', 'kept-other': 'đã đăng ký (binary khác), giữ nguyên', added: 'đã đăng ký bằng `claude mcp add`', replaced: 'đã đăng ký lại', restored: 'đăng ký mới lỗi — đã khôi phục đăng ký cũ', manual: 'chưa đăng ký (xem ghi chú)' }[res.mcp]}`);
98
+ for (const n of res.notes) lines.push(` - ${n}`);
99
+ if (res.action !== 'kept' || res.mcp === 'added' || res.mcp === 'replaced') lines.push('Khởi động lại các phiên Claude Code để nạp MCP; kiểm bằng `dieu-phoi doctor`.');
100
+ }
101
+ return { code: err ? 1 : 0, out: lines.join('\n') };
102
+ }
103
+
78
104
  function cmdUninstall(opts) {
79
105
  const res = setupLib.uninstall({ claudeDir: claudeDirOf(opts), purge: !!opts.purge });
80
106
  if (opts.json) return { code: 0, out: JSON.stringify(res, null, 2) };
@@ -116,6 +142,14 @@ async function cmdDoctor(opts) {
116
142
  const sd = ctx.config.skill_dir;
117
143
  add(!!sd && fs.existsSync(path.join(sd, 'profile', 'profile.json')), 'profile',
118
144
  sd ? path.join(sd, 'profile', 'profile.json') : 'không có skill_dir — choose dùng priors mặc định', 'CẢNH BÁO');
145
+ // codebase-memory-mcp (optional): started here so its `daemon status` (~2.7 s, only with --cbm/--net; else a 10-minute
146
+ // cache) overlaps the hook-run check below
147
+ let cbmP = Promise.resolve([]);
148
+ try {
149
+ cbmP = cbm.doctorChecks({ claudeDir, repo: repoOf(opts), net: !!opts.net, deep: !!opts.cbm, cacheFile: path.join(ctx.paths.data, 'cbm-daemon.json') }).catch((e) => [{ ok: true, level: 'OK', name: 'codebase-memory', detail: `bỏ qua kiểm (${e.message})` }]);
150
+ } catch (e) {
151
+ cbmP = Promise.resolve([{ ok: true, level: 'OK', name: 'codebase-memory', detail: `bỏ qua kiểm (${e.message})` }]);
152
+ }
119
153
  if (reg.commands.length) {
120
154
  const cmd = reg.commands[0];
121
155
  const t0 = Date.now();
@@ -168,13 +202,17 @@ async function cmdDoctor(opts) {
168
202
  const cached = opts.net ? null : core.sinkStatusCached(ctx);
169
203
  if (cached && (cached.state === 'ok' || cached.state === 'fail')) {
170
204
  add(cached.state === 'ok', 'sink', `${ctx.config.sink_url} → ${cached.state === 'ok' ? 'ok' : `lỗi: ${cached.detail}`} (bộ nhớ đệm: ${cached.source} lúc ${cached.ts}; \`doctor --net\` để kiểm trực tiếp)`);
205
+ } else if (!opts.net && recentSinkTimeout(ctx)) {
206
+ const t = recentSinkTimeout(ctx);
207
+ add(false, 'sink', `${ctx.config.sink_url} → chậm/không xác định: lần kiểm lúc ${t.ts} không phản hồi kịp (chưa coi là lỗi); \`doctor --net\` để thử lại`, 'CẢNH BÁO');
171
208
  } else {
172
209
  const live = await checkSinkLive(ctx);
173
- add(live.ok, 'sink', `${ctx.config.sink_url} → ${live.detail}`);
210
+ add(live.ok, 'sink', `${ctx.config.sink_url} → ${live.detail}`, live.slow ? 'CẢNH BÁO' : undefined);
174
211
  }
175
212
  } else {
176
213
  add(true, 'sink', 'chưa cấu hình (chỉ lưu cục bộ)');
177
214
  }
215
+ for (const c of await cbmP) checks.push(c);
178
216
  const failed = checks.some((c) => c.level === 'LỖI');
179
217
  if (opts.json) return { code: failed ? 1 : 0, out: JSON.stringify({ ok: !failed, runs_dir: reg.runs_dir, checks }, null, 2) };
180
218
  const out = checks.map((c) => `[${c.level}] ${c.name}: ${c.detail}`).join('\n');
@@ -208,26 +246,45 @@ function sweepStaleTmp(ctx) {
208
246
  return { removed, left };
209
247
  }
210
248
 
211
- /** Live sink check (view=stats); result cached in data/sink-check.json for 24 h. */
249
+ /** The doctor's own timed-out sink check for the current sink, if less than 1 h old (else null): plain `doctor` does not retry a 20 s wait each time. */
250
+ function recentSinkTimeout(ctx) {
251
+ const chk = core.readJsonFile(ctx.paths.sinkCheck, null);
252
+ if (!chk || !chk.timeout || chk.url !== core.sinkUrlKey(ctx.config)) return null;
253
+ const t = Date.parse(chk.ts);
254
+ return t && Date.now() - t < 3600 * 1000 && t <= Date.now() + 60000 ? { ts: chk.ts } : null;
255
+ }
256
+
257
+ /**
258
+ * Live sink check (view=stats); result cached in data/sink-check.json for 24 h. Apps Script often needs 8-15 s, so the
259
+ * timeout is 20 s (DIEU_PHOI_SINK_CHECK_TIMEOUT_MS overrides, for tests). A timeout is not a hard failure: it is
260
+ * reported as slow/unknown (CẢNH BÁO) and cached with `timeout: true`, which the cache readers ignore.
261
+ */
212
262
  async function checkSinkLive(ctx) {
213
263
  let ok = false;
264
+ let slow = false;
214
265
  let detail;
266
+ const ms = Number(process.env.DIEU_PHOI_SINK_CHECK_TIMEOUT_MS) > 0 ? Number(process.env.DIEU_PHOI_SINK_CHECK_TIMEOUT_MS) : 20000;
215
267
  try {
216
268
  const u = new URL(ctx.config.sink_url);
217
269
  u.searchParams.set('token', ctx.config.sink_token);
218
270
  u.searchParams.set('view', 'stats');
219
- const res = await fetch(u, { redirect: 'follow', signal: AbortSignal.timeout(8000) });
271
+ const res = await fetch(u, { redirect: 'follow', signal: AbortSignal.timeout(ms) });
220
272
  const txt = await res.text();
221
273
  ok = res.ok;
222
274
  try { const b = JSON.parse(txt); if (b && b.ok === false) ok = false; } catch (_) { ok = false; }
223
275
  detail = `HTTP ${res.status}`;
224
276
  } catch (e) {
225
- detail = e.message;
277
+ if (e && (e.name === 'TimeoutError' || e.name === 'AbortError')) {
278
+ slow = true;
279
+ detail = `chậm/không xác định: không phản hồi trong ${Math.round(ms / 1000)} s (Apps Script thường mất 8-15 s; chạy lại \`doctor --net\`, hoặc xem kết quả sync nền)`;
280
+ } else {
281
+ detail = e.message;
282
+ }
226
283
  }
227
284
  try {
228
- core.writeFileAtomic(ctx.paths.sinkCheck, `${JSON.stringify({ ts: core.nowIso(), ok, detail, url: core.sinkUrlKey(ctx.config) })}\n`);
285
+ core.writeFileAtomic(ctx.paths.sinkCheck, `${JSON.stringify({ ts: core.nowIso(), ok, detail, url: core.sinkUrlKey(ctx.config), ...(slow ? { timeout: true } : {}) })}\n`);
229
286
  } catch (_) { /* cache is best effort */ }
230
- return { ok, detail };
287
+ return { ok, detail, slow };
231
288
  }
232
289
 
233
290
  // ------------------------------------------------------------------ start
@@ -238,8 +295,13 @@ async function checkSinkLive(ctx) {
238
295
  */
239
296
  function cmdStart(opts) {
240
297
  const ctx = ctxOf(opts);
298
+ const wantMode = modeOpt(opts); // validate before the run dir is created
241
299
  const reg = setupLib.registration(claudeDirOf(opts));
242
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
+ }
243
305
  const h = info.health;
244
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`);
245
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');
@@ -257,12 +319,58 @@ function cmdStart(opts) {
257
319
  `step_ids: ${info.step_ids.join(' ')}`,
258
320
  `health: ${core.healthLine(h)}`,
259
321
  `scout_brief: ${info.scout_brief || '(không thấy — dùng references/brief-template.md mục E)'}`,
322
+ core.modeLine(info),
260
323
  ...core.orchLessonLines(info),
261
324
  ...core.knowledgeLines(info),
262
325
  ];
263
326
  return { code: 0, out: out.join('\n') };
264
327
  }
265
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
+
266
374
  // ------------------------------------------------------------------ hook / sync
267
375
 
268
376
  function cmdHook(opts) {
@@ -369,6 +477,7 @@ function verdictUser(opts, ctx, events) {
369
477
  if (typeof taskId !== 'string' || !taskId) throw new Error('--task cần task_id');
370
478
  requireTaskId(taskId);
371
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');
372
481
  let cause = typeof opts.cause === 'string' ? opts.cause : null;
373
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)`);
374
483
  if (outcome === 'accept') cause = null;
@@ -376,6 +485,7 @@ function verdictUser(opts, ctx, events) {
376
485
  if (!known) throw new Error(`Không thấy task "${taskId}" trong log.`);
377
486
  const rounds = events.filter((e) => e.kind === 'verdict' && e.task_id === taskId
378
487
  && agg.normalizeVerdict(e).by === 'user').map((e) => Number(e.round) || 1);
488
+ if (opts.auto) return verdictAuto(opts, ctx, events, taskId, rounds);
379
489
  let round = (rounds.length ? Math.max(...rounds) : 0) + 1;
380
490
  if (opts.round !== undefined) {
381
491
  round = Number(opts.round);
@@ -396,6 +506,33 @@ function verdictUser(opts, ctx, events) {
396
506
  }, core.commonFields(ctx));
397
507
  }
398
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
+
399
536
  /**
400
537
  * `verdict --root <root> accept [--steps a,b]` (core 0.4.0): the user accepts the steps of the latest approved plan
401
538
  * in one call. Covers every step whose task has an exec dispatch and no user verdict yet (a step already rejected
@@ -403,6 +540,7 @@ function verdictUser(opts, ctx, events) {
403
540
  */
404
541
  function verdictRoot(opts, ctx, events) {
405
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');
406
544
  if (opts._[0] !== 'accept') {
407
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');
408
546
  }
@@ -495,6 +633,9 @@ function cmdVerdict(opts) {
495
633
  if (kn) process.stderr.write(`${kn.out}\n`);
496
634
  return { code: 0, out: JSON.stringify(ev) };
497
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
+ }
498
639
  if (ev.by === 'user') {
499
640
  const msg = `Đã ghi người dùng ${ev.outcome}${ev.cause ? ` (${ev.cause})` : ''} cho task ${ev.task_id}, vòng ${ev.round}.`;
500
641
  return { code: 0, out: kn ? `${msg}\n${kn.out}` : msg };
@@ -690,12 +831,14 @@ function pct(v) {
690
831
  function cmdStats(opts) {
691
832
  const ctx = ctxOf(opts);
692
833
  const { lambda } = lambdaOf(opts, ctx);
693
- 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);
694
837
  const type = typeof opts.type === 'string' ? opts.type : null;
695
838
  const rows = type ? cells.filter((c) => c.task_type === type) : cells;
696
839
  const steerRows = type ? steers.filter((s) => s.task_type === type) : steers;
697
840
  if (opts.json) {
698
- 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) };
699
842
  }
700
843
  if (!rows.length) {
701
844
  return { code: 0, out: orchestration.sessions ? `Chưa có dữ liệu theo ô.\n${orchLine(orchestration)}` : 'Chưa có dữ liệu.' };
@@ -722,13 +865,40 @@ function cmdStats(opts) {
722
865
  }
723
866
  return {
724
867
  code: 0,
725
- 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`
726
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). '
727
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. '
728
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.`,
729
872
  };
730
873
  }
731
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
+
732
902
  const HIGH_STEER_RATE = 0.3;
733
903
  const HIGH_STEER_MIN_TASKS = 3;
734
904
 
@@ -737,6 +907,7 @@ function cmdRetro(opts) {
737
907
  const { prof, lambda } = lambdaOf(opts, ctx);
738
908
  const events = core.readEvents(ctx);
739
909
  const { agents, tasks, cells, steers, userVerdicts, overrides, orchestration } = agg.analyze(events, { lambda });
910
+ const modes = agg.modeSummary(events, tasks);
740
911
  const reasons = {};
741
912
  const outcomes = {};
742
913
  const userOutcomes = {};
@@ -776,6 +947,10 @@ function cmdRetro(opts) {
776
947
  multi_attempt_tasks: escalations,
777
948
  discarded_tasks: agg.discardedTaskIds(events).size,
778
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,
779
954
  cost_usd: agents.reduce((s, a) => s + (a.cost_usd || 0), 0),
780
955
  },
781
956
  lambda_usd_per_min: prof.lambda,
@@ -788,6 +963,7 @@ function cmdRetro(opts) {
788
963
  high_steer_task_types: highSteer,
789
964
  overrides: overrides.map((o) => ({ ts: o.ts, task_type: o.task_type, suggested_tier: o.suggested_tier, chosen_tier: o.chosen_tier })),
790
965
  orchestration,
966
+ modes,
791
967
  cells,
792
968
  };
793
969
  if (opts.json) return { code: 0, out: JSON.stringify(summary, null, 2) };
@@ -795,10 +971,12 @@ function cmdRetro(opts) {
795
971
  `Retro ${summary.dept || ''} — ${summary.period ? `${summary.period.from.slice(0, 10)} → ${summary.period.to.slice(0, 10)}` : 'chưa có dữ liệu'}`,
796
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.`,
797
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}`,
798
975
  `Lý do hay gặp: ${summary.reason_codes.slice(0, 8).map((r) => `${r.code}×${r.count}`).join(', ') || '-'}`,
799
976
  `Loại task: ${summary.task_types.map((t) => `${t.task_type}(${t.n})`).join(', ') || '-'}`,
800
977
  `Steer nhiều (brainstorm hỏi quá ít): ${highSteer.map((s) => `${s.task_type} ${pct(s.steer_rate)}`).join(', ') || '-'}`,
801
978
  orchLine(orchestration),
979
+ modeRetroLine(modes),
802
980
  'Chi tiết theo ô: `dieu-phoi stats`; đưa cho LLM: `dieu-phoi retro --json`.',
803
981
  ];
804
982
  return { code: 0, out: lines.join('\n') };
@@ -806,6 +984,13 @@ function cmdRetro(opts) {
806
984
 
807
985
  // ------------------------------------------------------------------ choose
808
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
+
809
994
  function chooseInput(opts, ctx, events) {
810
995
  return {
811
996
  type: opts.type,
@@ -813,6 +998,9 @@ function chooseInput(opts, ctx, events) {
813
998
  urgency: typeof opts.urgency === 'string' ? opts.urgency : 'normal',
814
999
  reversible: yesNo(opts.reversible, true),
815
1000
  verifiable: yesNo(opts.verifiable, true),
1001
+ publish: yesNo(opts.publish, false),
1002
+ dependents: yesNo(opts.dependents, false),
1003
+ mode: modeOpt(opts),
816
1004
  role: typeof opts.role === 'string' ? opts.role : 'exec',
817
1005
  seed: opts.seed,
818
1006
  taskId: typeof opts.task === 'string' ? opts.task : null,
@@ -837,6 +1025,18 @@ function overrideEvent(ctx, taskId, taskType, suggested, chosen) {
837
1025
  }, core.commonFields(ctx));
838
1026
  }
839
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
+
840
1040
  function cmdChoose(opts) {
841
1041
  const ctx = ctxOf(opts);
842
1042
  if (opts.override !== undefined && typeof opts.task !== 'string') {
@@ -844,7 +1044,20 @@ function cmdChoose(opts) {
844
1044
  }
845
1045
  if (opts.task !== undefined) requireTaskId(typeof opts.task === 'string' ? opts.task : '');
846
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
+ }
847
1058
  const res = chooser.choose(input);
1059
+ res.mode_source = modeSource;
1060
+ if (res.verify_plan) Object.assign(res, chooseModeSuggest(ctx, input, res, mi));
848
1061
  const orch = measureOrch(opts, ctx, input.profile);
849
1062
  res.orch_ctx_tokens = orch.ctx_tokens;
850
1063
  res.orch_ctx_growth = orch.ctx_growth;
@@ -863,14 +1076,15 @@ function cmdChoose(opts) {
863
1076
  }
864
1077
  if (opts.json) return { code: 0, out: JSON.stringify(res) };
865
1078
  const typeWarn = res.type_warning && !String(res.reason).includes(res.type_warning) ? `\nCẢNH BÁO: ${res.type_warning}` : '';
866
- 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}`;
867
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ô'})`;
868
1082
  if (res.action === 'ask_user') {
869
1083
  return { code: 0, out: `HỎI NGƯỜI DÙNG — ${res.reason}${warn}` };
870
1084
  }
871
1085
  return {
872
1086
  code: 0,
873
- 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}`,
874
1088
  };
875
1089
  }
876
1090
 
@@ -917,6 +1131,8 @@ function buildPlanTable(rawSteps, base) {
917
1131
  score: s.score,
918
1132
  verifiable: s.verifiable === undefined ? true : yesNo(s.verifiable, true),
919
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,
920
1136
  urgency: s.urgency,
921
1137
  deps: deps.map(String),
922
1138
  };
@@ -933,6 +1149,9 @@ function buildPlanTable(rawSteps, base) {
933
1149
  verifiable: s.verifiable,
934
1150
  reversible: s.reversible,
935
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,
936
1155
  taskId: s.task || base.taskId,
937
1156
  override: s.override || base.override,
938
1157
  role: s.role || base.role,
@@ -942,6 +1161,7 @@ function buildPlanTable(rawSteps, base) {
942
1161
  const head = {
943
1162
  step: s.step, title: s.title, type: s.type, task_id: s.task || null, score: Number(s.score),
944
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,
945
1165
  };
946
1166
  try {
947
1167
  return Object.assign(head, call());
@@ -958,7 +1178,10 @@ function buildPlanTable(rawSteps, base) {
958
1178
  const ok = rows.filter((r) => r.action !== 'refused');
959
1179
  const costs = ok.map((r) => r.est_cost_usd);
960
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;
961
1182
  return {
1183
+ mode: base.mode || core.DEFAULT_MODE,
1184
+ self_batch_steps: selfBatch,
962
1185
  steps: rows,
963
1186
  refused: rows.filter((r) => r.action === 'refused').map((r) => ({ step: r.step, error: r.error })),
964
1187
  total_cost_usd: totalCost === null ? null : Math.round(totalCost * 1000) / 1000,
@@ -1054,10 +1277,10 @@ function recordApprovedPlan(ctx, root, stepsFile, plan) {
1054
1277
  steps_file: stepsFile.split(path.sep).join('/'),
1055
1278
  approved_at: core.nowIso(),
1056
1279
  approval: prevApproval(prev) + 1,
1057
- steps: plan.steps.map((r) => ({
1280
+ steps: plan.steps.map((r) => Object.assign({
1058
1281
  id: r.step, task_id: r.task_id || null, title: r.title === undefined ? null : r.title, type: r.task_type || r.type || null,
1059
1282
  tier: r.action === 'refused' ? null : (r.tier || null), depends_on: r.depends_on,
1060
- })),
1283
+ }, r.mode_override ? { mode: r.mode_override } : {})),
1061
1284
  history,
1062
1285
  };
1063
1286
  core.writeFileAtomic(file, `${JSON.stringify(rec, null, 2)}\n`);
@@ -1094,8 +1317,19 @@ function cmdPlanTable(opts) {
1094
1317
  throw new Error(`plan-table: không đọc được ${opts.file}: ${e.message}`);
1095
1318
  }
1096
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
+ }
1097
1329
  const base = chooseInput(Object.assign({}, opts, { type: 'x', score: 0, override: undefined }), ctx, core.readEvents(ctx));
1098
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;
1099
1333
  const plan = buildPlanTable(raw, base);
1100
1334
  const problems = planTaskIdProblems(plan, opts.approve ? String(opts.root) : null);
1101
1335
  if (opts.approve && problems.length) {
@@ -1105,26 +1339,33 @@ function cmdPlanTable(opts) {
1105
1339
  const typeNotes = planTypeNotes(plan);
1106
1340
  if (typeNotes.length) process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${typeNotes.join('; ')}\n`);
1107
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
+ }
1108
1347
  const ovs = rec ? planOverrideEvents(ctx, plan, base.events) : [];
1109
1348
  for (const ev of ovs) core.appendEvent(ctx, ev);
1110
1349
  if (rec) recordPlanSuggestions(ctx, plan);
1111
1350
  if (opts.json) {
1112
1351
  const out = rec ? Object.assign({}, plan, {
1113
- 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 },
1114
1353
  }) : plan;
1115
1354
  return { code: 0, out: JSON.stringify(out, null, 2) };
1116
1355
  }
1117
- 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 |', '|---|---|---|---|---|---|'];
1118
1357
  for (const r of plan.steps) {
1119
1358
  const tierCell = r.action === 'refused' ? '—' : (r.action === 'ask_user' ? `${r.tier} (hỏi người dùng)` : r.tier);
1120
1359
  const yn = (v) => (v ? 'y' : 'n');
1121
1360
  const rubric = `${Number.isFinite(r.score) ? r.score : '?'} · ${yn(r.verifiable)} · ${yn(r.reversible)}`;
1122
1361
  const why = `${r.type}${r.depends_on.length ? `, sau ${r.depends_on.join(', ')}` : ''} — ${r.reason.replace(/; profile: .*$/, '')}`;
1123
1362
  const est = r.action === 'refused' ? '-' : `~${fmtUsd(r.est_cost_usd)} · ~${fmtNum(r.est_time_min)} phút${r.est_source === 'rough' ? ' (thô)' : ''}`;
1124
- 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)} |`);
1125
1365
  }
1126
1366
  lines.push('');
1127
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` : ''}`);
1128
1369
  if (plan.rough) lines.push('', '_(thô) = ô chưa đủ 5 task, ước lượng theo giả định token của từng tier._');
1129
1370
  if (plan.refused.length) {
1130
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.`);
@@ -1132,11 +1373,11 @@ function cmdPlanTable(opts) {
1132
1373
  `);
1133
1374
  }
1134
1375
  if (typeNotes.length) lines.push('', `**Loại task:** ${typeNotes.join('; ')}`);
1135
- 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}` : ''}`);
1136
1377
  return { code: 0, out: lines.join('\n') };
1137
1378
  }
1138
1379
 
1139
1380
  module.exports = {
1140
- 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,
1141
1382
  cmdPlanTable, cmdNewId, requireTaskId, buildPlanTable, recordApprovedPlan, planTaskIdProblems, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
1142
1383
  };
package/lib/knowledge.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
- // `dieu-phoi knowledge ...` — per-project knowledge store (core 0.6.0, unreleased).
2
+ // `dieu-phoi knowledge ...` — per-project knowledge store (core 0.6.0).
3
3
  // s1: project identity (path, rekey). s2: the store itself — index.json + sections/<id>.md + history/, read with
4
4
  // get/list, written only by `merge --run <id>` (lock, tmp+rename, history, 40-line cap, sha/rev conflicts), and
5
5
  // cross-repo links (related: child|parent) for a parent repo whose sessions work in its child repos.
package/lib/profile.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
- // `dieu-phoi profile base|apply` (core 0.6.0, unreleased): apply an approved retro to <skill>/profile/lessons.md and
2
+ // `dieu-phoi profile base|apply` (core 0.6.0): apply an approved retro to <skill>/profile/lessons.md and
3
3
  // profile.json without overwriting what other sessions wrote meanwhile.
4
4
  //
5
5
  // profile base --save <dir> snapshot both files into <dir>/profile-base/ (+ base.json with their sha256) before the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kienntpixon/dieu-phoi",
3
- "version": "0.6.0",
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>",