@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/CHANGELOG.md +72 -0
- package/bin/dieu-phoi.cjs +20 -7
- package/hook.cjs +625 -15
- package/lib/aggregate.cjs +181 -16
- package/lib/args.cjs +1 -1
- package/lib/choose.cjs +101 -27
- package/lib/commands.cjs +243 -22
- package/lib/knowledge.cjs +74 -18
- package/lib/setup.cjs +16 -8
- package/package.json +1 -1
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
|
-
|
|
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 (
|
|
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)
|
|
729
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
};
|