@kienntpixon/dieu-phoi 0.7.0 → 0.7.2
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 +57 -0
- package/README.md +3 -0
- package/agents/dp-exec.md +2 -0
- package/agents/dp-helper.md +2 -0
- package/agents/dp-scout.md +2 -0
- package/agents/dp-verify.md +2 -0
- package/bin/dieu-phoi.cjs +14 -2
- package/hook.cjs +788 -34
- package/lib/aggregate.cjs +49 -6
- package/lib/args.cjs +1 -1
- package/lib/choose.cjs +41 -25
- package/lib/commands.cjs +212 -42
- package/lib/knowledge.cjs +77 -22
- package/lib/procs.cjs +223 -0
- package/lib/profile-scan.cjs +85 -0
- package/lib/profile-seed.cjs +147 -0
- package/lib/profile.cjs +141 -1
- package/lib/prune.cjs +56 -8
- package/lib/setup.cjs +47 -15
- package/lib/worktree.cjs +186 -0
- package/package.json +3 -1
- package/profile/lessons.md +83 -0
- package/profile/unity-lanes.md +316 -0
package/lib/commands.cjs
CHANGED
|
@@ -9,6 +9,8 @@ const agg = require('./aggregate.cjs');
|
|
|
9
9
|
const chooser = require('./choose.cjs');
|
|
10
10
|
const setupLib = require('./setup.cjs');
|
|
11
11
|
const cbm = require('./cbm.cjs');
|
|
12
|
+
const procs = require('./procs.cjs');
|
|
13
|
+
const wt = require('./worktree.cjs');
|
|
12
14
|
const { claudeDirOf, ctxOf, yesNo, repoOf } = require('./args.cjs');
|
|
13
15
|
|
|
14
16
|
const AGENT_OUTCOMES = ['pass', 'fail', 'timeout', 'aborted'];
|
|
@@ -49,14 +51,24 @@ function table(rows) {
|
|
|
49
51
|
// ------------------------------------------------------------------ setup / uninstall
|
|
50
52
|
|
|
51
53
|
function cmdSetup(opts) {
|
|
52
|
-
if (opts['with-codebase-memory']) return cmdSetupWithCbm(opts);
|
|
54
|
+
if (opts['with-codebase-memory'] && !opts['dry-run']) return cmdSetupWithCbm(opts);
|
|
53
55
|
const res = setupLib.setup({
|
|
54
56
|
claudeDir: claudeDirOf(opts),
|
|
55
57
|
dept: typeof opts.dept === 'string' ? opts.dept : undefined,
|
|
56
58
|
sinkUrl: typeof opts['sink-url'] === 'string' ? opts['sink-url'] : undefined,
|
|
57
59
|
sinkToken: typeof opts['sink-token'] === 'string' ? opts['sink-token'] : undefined,
|
|
58
60
|
skillDir: typeof opts['skill-dir'] === 'string' ? opts['skill-dir'] : undefined,
|
|
61
|
+
dryRun: opts['dry-run'] === true,
|
|
59
62
|
});
|
|
63
|
+
const seed = require('./profile-seed.cjs').seedSummary(res.profile);
|
|
64
|
+
if (res.dry_run) {
|
|
65
|
+
if (opts.json) return { code: 0, out: JSON.stringify(res, null, 2) };
|
|
66
|
+
return { code: 0, out: [
|
|
67
|
+
'setup --dry-run: không ghi gì (hook, settings.json, agent dp-*, config đều giữ nguyên)',
|
|
68
|
+
seed,
|
|
69
|
+
`Skill: ${res.skill_dir || '(không thấy; dùng --skill-dir)'}`,
|
|
70
|
+
].join('\n') };
|
|
71
|
+
}
|
|
60
72
|
if (opts.json) {
|
|
61
73
|
const safe = Object.assign({}, res, { config: Object.assign({}, res.config) });
|
|
62
74
|
if (safe.config.sink_token) safe.config.sink_token = '***';
|
|
@@ -65,13 +77,14 @@ function cmdSetup(opts) {
|
|
|
65
77
|
const lines = [
|
|
66
78
|
`Đã cài hook: ${res.hook}`,
|
|
67
79
|
res.settings_changed
|
|
68
|
-
? `Đã đăng ký PostToolUse(Agent) + SubagentStop + SessionStart(compact) + UserPromptSubmit trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
|
|
80
|
+
? `Đã đăng ký PostToolUse(Agent) + SubagentStop + SessionStart(compact) + UserPromptSubmit + PreToolUse(Agent) trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
|
|
69
81
|
: `settings.json đã có hook, không đổi gì (${res.settings})`,
|
|
70
82
|
`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
83
|
`Thư mục báo cáo subagent: ${res.runs_dir} (quyền: ${res.permissions.allow.join(', ')} + additionalDirectories)`,
|
|
72
84
|
`Phòng ban: ${res.config.dept || '(chưa đặt, dùng --dept)'}`,
|
|
73
85
|
`Sink: ${res.config.sink_url ? res.config.sink_url : '(chưa có — log chỉ lưu cục bộ)'}`,
|
|
74
86
|
`Skill: ${res.config.skill_dir || '(không thấy; dùng --skill-dir)'}${res.config.skill_version ? ` v${res.config.skill_version}` : ''}`,
|
|
87
|
+
seed,
|
|
75
88
|
'Khởi động lại Claude Code (hoặc mở /hooks) để hook có hiệu lực.',
|
|
76
89
|
];
|
|
77
90
|
return { code: 0, out: lines.join('\n') };
|
|
@@ -128,6 +141,9 @@ async function cmdDoctor(opts) {
|
|
|
128
141
|
add(reg.UserPromptSubmit, 'prompt-hook', reg.UserPromptSubmit
|
|
129
142
|
? 'UserPromptSubmit có — prompt "điều phối" được tạo sẵn task_id + run dir, lượt đầu là scout'
|
|
130
143
|
: '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');
|
|
144
|
+
add(reg.PreToolUse, 'chain-gate', reg.PreToolUse
|
|
145
|
+
? 'PreToolUse(Agent) có — chặn exec tiếp nối từ handoff thứ 3 (trừ thẻ chain_ok)'
|
|
146
|
+
: '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
147
|
const permOk = reg.permissions && reg.runs_dir_exists && !reg.legacy_permissions;
|
|
132
148
|
add(permOk, 'run-permissions', permOk
|
|
133
149
|
? `${reg.permission_entries.allow.join(', ')} + additionalDirectories ${reg.permission_entries.dir}`
|
|
@@ -198,6 +214,7 @@ async function cmdDoctor(opts) {
|
|
|
198
214
|
if (orch.ctx_tokens !== null) {
|
|
199
215
|
add(!orch.over, 'orch-ctx', `session ${orch.session_id}: ${orch.ctx_tokens} token, tăng ${orch.ctx_growth}/${orch.soft_cap}, ≈ ${fmtUsd(orch.cum_usd)} — ${orch.advice}`, 'CẢNH BÁO');
|
|
200
216
|
}
|
|
217
|
+
add(...orphanCheck(ctx));
|
|
201
218
|
if (ctx.config.sink_url && ctx.config.sink_token) {
|
|
202
219
|
const cached = opts.net ? null : core.sinkStatusCached(ctx);
|
|
203
220
|
if (cached && (cached.state === 'ok' || cached.state === 'fail')) {
|
|
@@ -219,6 +236,48 @@ async function cmdDoctor(opts) {
|
|
|
219
236
|
return { code: failed ? 1 : 0, out };
|
|
220
237
|
}
|
|
221
238
|
|
|
239
|
+
// s16 (core 0.7.2): Claude Code / code-graph processes whose parent is gone and that are older than this
|
|
240
|
+
const ORPHAN_MIN_AGE_MS = 6 * 3600 * 1000;
|
|
241
|
+
const ORPHAN_CACHE_MS = 10 * 60 * 1000;
|
|
242
|
+
|
|
243
|
+
/** Orphaned claude.exe / codebase-memory-mcp older than 6 h from a process list: [{pid,name,age_h,ws_mb}], or null when the list is unreadable. */
|
|
244
|
+
function orphanScan(list, now) {
|
|
245
|
+
return Array.isArray(list) ? procs.staleOrphans(list, { minAgeMs: ORPHAN_MIN_AGE_MS, now: now === undefined ? Date.now() : now }) : null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** doctor item `orphan-processes`: warns with pid + age + working set and a Stop-Process hint; never kills. Returns the args of add(). */
|
|
249
|
+
function orphanCheck(ctx, deps) {
|
|
250
|
+
const d = deps || {};
|
|
251
|
+
const now = d.now === undefined ? Date.now() : d.now;
|
|
252
|
+
const found = orphanScan((d.listProcesses || procs.listProcesses)({ timeoutMs: 4000 }), now);
|
|
253
|
+
if (found === null) return [true, 'orphan-processes', 'không đọc được danh sách process (bỏ qua)'];
|
|
254
|
+
try {
|
|
255
|
+
core.writeFileAtomic(path.join(ctx.paths.data, 'orphan-procs.json'), `${JSON.stringify({ ts: new Date(now).toISOString(), n: found.length })}\n`);
|
|
256
|
+
} catch (_) { /* the cache is optional */ }
|
|
257
|
+
if (!found.length) return [true, 'orphan-processes', 'không có claude.exe / codebase-memory-mcp mồ côi quá 6 giờ'];
|
|
258
|
+
const mb = found.reduce((n, p) => n + (p.ws_mb || 0), 0);
|
|
259
|
+
const shown = found.slice(0, 8);
|
|
260
|
+
const items = shown.map((p) => `${p.name} pid ${p.pid} (${p.age_h} giờ${p.ws_mb ? `, ${p.ws_mb} MB` : ''})`).join('; ');
|
|
261
|
+
return [false, 'orphan-processes',
|
|
262
|
+
`${found.length} tiến trình mồ côi (cha đã thoát) quá 6 giờ, ${mb} MB: ${items}${found.length > shown.length ? '; …' : ''}`
|
|
263
|
+
+ ` — kiểm tra rồi dọn bằng \`Stop-Process -Id ${shown.map((p) => p.pid).join(',')}\` (doctor không tự kill)`, 'CẢNH BÁO'];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** `state` line: orphan count from a 10-minute cache (data/orphan-procs.json), else one live scan. null = unknown (skip the line). */
|
|
267
|
+
function orphanCount(ctx, deps) {
|
|
268
|
+
const d = deps || {};
|
|
269
|
+
const now = d.now === undefined ? Date.now() : d.now;
|
|
270
|
+
const file = path.join(ctx.paths.data, 'orphan-procs.json');
|
|
271
|
+
const c = core.readJsonFile(file, null);
|
|
272
|
+
const at = c && typeof c.ts === 'string' ? Date.parse(c.ts) : NaN;
|
|
273
|
+
if (c && Number.isFinite(at) && now - at >= 0 && now - at < ORPHAN_CACHE_MS) return typeof c.n === 'number' ? c.n : null;
|
|
274
|
+
const found = orphanScan((d.listProcesses || procs.listProcesses)({ timeoutMs: 4000 }), now);
|
|
275
|
+
try {
|
|
276
|
+
core.writeFileAtomic(file, `${JSON.stringify({ ts: new Date(now).toISOString(), n: found ? found.length : null })}\n`);
|
|
277
|
+
} catch (_) { /* the cache is optional */ }
|
|
278
|
+
return found ? found.length : null;
|
|
279
|
+
}
|
|
280
|
+
|
|
222
281
|
const TMP_RE = /\.tmp-\d+-[0-9a-f]+$/;
|
|
223
282
|
const TMP_STALE_MS = 3600 * 1000;
|
|
224
283
|
|
|
@@ -579,28 +638,79 @@ function rootOfTask(ctx, taskId) {
|
|
|
579
638
|
}
|
|
580
639
|
|
|
581
640
|
/**
|
|
582
|
-
*
|
|
583
|
-
* (
|
|
584
|
-
*
|
|
641
|
+
* Run dirs that hold knowledge deltas of `tasks` (retro 11-05 K1). The delta of a task lives where its agent was told to
|
|
642
|
+
* write (the dispatch `run`, 0.7.2), which is not always runs/<root>: a loose task or one outside the approved plan has
|
|
643
|
+
* root = itself yet its brief points at the session's run. Order: runs/<root> (also takes untagged deltas when
|
|
644
|
+
* `untagged`), the dispatch `run` of each task, then any other run dir with a file tagged with one of the tasks.
|
|
645
|
+
* [{ id, dir, files, untagged }]; `scanned` = how many run dirs the fallback looked at; `rootDirExists`.
|
|
585
646
|
*/
|
|
586
|
-
function
|
|
587
|
-
if (opts['no-knowledge']) return null;
|
|
647
|
+
function deltaRunDirs(root, tasks, events, untagged) {
|
|
588
648
|
const kn = require('./knowledge.cjs');
|
|
589
|
-
const
|
|
590
|
-
const
|
|
649
|
+
const runsRoot = core.runsDir();
|
|
650
|
+
const want = new Set(tasks);
|
|
651
|
+
const seen = new Set();
|
|
652
|
+
const found = [];
|
|
653
|
+
let scanned = 0;
|
|
654
|
+
const consider = (id, withUntagged) => {
|
|
655
|
+
if (typeof id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(id) || seen.has(id)) return false;
|
|
656
|
+
seen.add(id);
|
|
657
|
+
const dir = path.join(runsRoot, id);
|
|
658
|
+
try {
|
|
659
|
+
if (!fs.statSync(dir).isDirectory()) return false;
|
|
660
|
+
const files = kn.deltaFiles(dir, { tasks: want, untagged: withUntagged });
|
|
661
|
+
if (files.length) found.push({ id, dir, files, untagged: withUntagged });
|
|
662
|
+
return true;
|
|
663
|
+
} catch (_) { return false; }
|
|
664
|
+
};
|
|
665
|
+
const rootDirExists = consider(root, untagged);
|
|
666
|
+
for (const e of events || []) if (e && e.kind === 'dispatch' && want.has(e.task_id)) consider(e.run, false);
|
|
591
667
|
try {
|
|
592
|
-
|
|
593
|
-
} catch (_) {
|
|
668
|
+
for (const id of fs.readdirSync(runsRoot)) { scanned++; consider(id, false); }
|
|
669
|
+
} catch (_) { /* no runs dir */ }
|
|
670
|
+
return { found, scanned, rootDirExists };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Gate 3 (core 0.6.0): a user accept merges the knowledge deltas of the accepted tasks (knowledge-delta-<task_id>
|
|
675
|
+
* [-r<n>|-h<n>].md; on --root also the root's own and untagged ones) wherever they are (see deltaRunDirs). Never fails
|
|
676
|
+
* the verdict: conflicts/rejections are reported with the command to retry. --no-knowledge skips it.
|
|
677
|
+
* 0.7.2: never silent. It appends a local `knowledge_merge {code, files, runs}` event on every call (also with 0 files),
|
|
678
|
+
* and says so when no delta was found although the root's run dir is missing. Returns { code, out, files }.
|
|
679
|
+
*/
|
|
680
|
+
function knowledgeOnAccept(opts, ctx, root, tasks, untagged, events) {
|
|
681
|
+
if (opts['no-knowledge']) return null;
|
|
682
|
+
const kn = require('./knowledge.cjs');
|
|
683
|
+
const { found, scanned, rootDirExists } = deltaRunDirs(root, tasks, events, untagged);
|
|
684
|
+
const nFiles = found.reduce((n, f) => n + f.files.length, 0);
|
|
594
685
|
const taskArg = tasks.join(',');
|
|
595
|
-
const
|
|
596
|
-
let
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
686
|
+
const outs = [];
|
|
687
|
+
let code = 0;
|
|
688
|
+
for (const f of found) {
|
|
689
|
+
const retry = `dieu-phoi knowledge merge --run ${f.id} --task ${taskArg}${f.untagged ? ' --untagged' : ''}`;
|
|
690
|
+
let r;
|
|
691
|
+
try {
|
|
692
|
+
r = kn.cmdMerge({ _: [], run: f.dir, task: taskArg, untagged: f.untagged, repo: opts.repo });
|
|
693
|
+
} catch (e) {
|
|
694
|
+
code = Math.max(code, 1);
|
|
695
|
+
outs.push(`knowledge: gộp lỗi (${e.message}) — chạy lại: ${retry}`);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
code = Math.max(code, r.code);
|
|
699
|
+
const tail = r.code ? `\n→ có mục chưa gộp: get lại mục đó, chép dòng base: mới vào delta (hoặc bỏ mục), rồi ${retry}` : '';
|
|
700
|
+
outs.push(`knowledge (tự gộp khi accept, run ${f.id}):\n${r.out}${tail}`);
|
|
601
701
|
}
|
|
602
|
-
|
|
603
|
-
|
|
702
|
+
let note = '';
|
|
703
|
+
if (!nFiles) {
|
|
704
|
+
// nothing to merge is normal for most tasks; say it only when the run dir the root names is missing (the s1 silent case)
|
|
705
|
+
if (!rootDirExists) note = `knowledge: không có delta của ${taskArg} (run ${root} không tồn tại; đã quét ${scanned} run dir theo tên knowledge-delta-<task>*).`;
|
|
706
|
+
}
|
|
707
|
+
try {
|
|
708
|
+
core.appendEvent(ctx, Object.assign({
|
|
709
|
+
schema: core.SCHEMA, kind: 'knowledge_merge', ts: core.nowIso(), agent_id: `km:${tasks[0]}`, task_id: tasks[0],
|
|
710
|
+
code, files: nFiles, runs: found.map((f) => f.id), tasks: tasks.slice(0, 20),
|
|
711
|
+
}, core.commonFields(ctx)));
|
|
712
|
+
} catch (_) { /* the log is best effort here: the merge result is still printed */ }
|
|
713
|
+
return { code, files: nFiles, out: outs.concat(note ? [note] : []).join('\n') };
|
|
604
714
|
}
|
|
605
715
|
|
|
606
716
|
function cmdVerdict(opts) {
|
|
@@ -610,27 +720,33 @@ function cmdVerdict(opts) {
|
|
|
610
720
|
const list = verdictRoot(opts, ctx, events);
|
|
611
721
|
for (const x of list) core.appendEvent(ctx, x.ev);
|
|
612
722
|
const root = String(opts.root);
|
|
613
|
-
const kn = knowledgeOnAccept(opts, ctx, root, [root].concat(list.map((x) => x.ev.task_id)), true);
|
|
723
|
+
const kn = knowledgeOnAccept(opts, ctx, root, [root].concat(list.map((x) => x.ev.task_id)), true, events);
|
|
724
|
+
// s17: accept của người dùng gỡ worktree có dấu của từng bước (sạch); còn thay đổi thì giữ và báo.
|
|
725
|
+
const wtLines = list.reduce((acc, x) => acc.concat(wt.cleanupTask(ctx, events, x.ev.task_id, opts.repo)), []);
|
|
614
726
|
if (!list.length) {
|
|
615
727
|
// Every step was already judged (e.g. accepted one by one with --task): still merge the root + untagged deltas.
|
|
616
728
|
// Exit code follows the merge (0 merged or nothing new, 2 conflict or reject, 1 error); no delta at all keeps the old "nothing to do" error.
|
|
617
|
-
if (!kn) throw new Error(list.emptyMsg);
|
|
729
|
+
if (!kn || !kn.files) throw new Error(`${list.emptyMsg}${kn && kn.out ? `\n${kn.out}` : ''}`);
|
|
618
730
|
const msg = `Không còn bước nào của ${root} chờ nghiệm thu; chỉ gộp delta của root và delta không thẻ.`;
|
|
619
731
|
if (opts.json) { process.stderr.write(`${msg}\n${kn.out}\n`); return { code: kn.code, out: '[]' }; }
|
|
620
732
|
return { code: kn.code, out: `${msg}\n${kn.out}` };
|
|
621
733
|
}
|
|
622
734
|
if (opts.json) {
|
|
623
|
-
if (kn) process.stderr.write(`${kn.out}\n`);
|
|
735
|
+
if (kn && kn.out) process.stderr.write(`${kn.out}\n`);
|
|
736
|
+
if (wtLines.length) process.stderr.write(`${wtLines.join('\n')}\n`);
|
|
624
737
|
return { code: 0, out: JSON.stringify(list.map((x) => x.ev)) };
|
|
625
738
|
}
|
|
626
739
|
const msg = `Đã ghi người dùng accept cho ${list.length} bước của ${opts.root}: ${list.map((x) => `${x.step}=${x.ev.task_id}`).join(', ')}.`;
|
|
627
|
-
return { code: 0, out: kn ?
|
|
740
|
+
return { code: 0, out: [msg].concat(wtLines, kn && kn.out ? [kn.out] : []).join('\n') };
|
|
628
741
|
}
|
|
629
742
|
const ev = typeof opts.task === 'string' ? verdictUser(opts, ctx, events) : verdictAgent(opts, ctx, events);
|
|
630
743
|
core.appendEvent(ctx, ev);
|
|
631
|
-
const kn = ev.by === 'user' && ev.outcome === 'accept' ? knowledgeOnAccept(opts, ctx, rootOfTask(ctx, ev.task_id), [ev.task_id], false) : null;
|
|
744
|
+
const kn = ev.by === 'user' && ev.outcome === 'accept' ? knowledgeOnAccept(opts, ctx, rootOfTask(ctx, ev.task_id), [ev.task_id], false, events) : null;
|
|
745
|
+
// s17: người dùng accept → gỡ worktree có dấu của task (sạch); reject giữ nguyên để làm lại.
|
|
746
|
+
const wtLines = ev.by === 'user' && ev.outcome === 'accept' ? wt.cleanupTask(ctx, events, ev.task_id, opts.repo) : [];
|
|
632
747
|
if (opts.json) {
|
|
633
|
-
if (kn) process.stderr.write(`${kn.out}\n`);
|
|
748
|
+
if (kn && kn.out) process.stderr.write(`${kn.out}\n`);
|
|
749
|
+
if (wtLines.length) process.stderr.write(`${wtLines.join('\n')}\n`);
|
|
634
750
|
return { code: 0, out: JSON.stringify(ev) };
|
|
635
751
|
}
|
|
636
752
|
if (ev.by === 'auto') {
|
|
@@ -638,7 +754,7 @@ function cmdVerdict(opts) {
|
|
|
638
754
|
}
|
|
639
755
|
if (ev.by === 'user') {
|
|
640
756
|
const msg = `Đã ghi người dùng ${ev.outcome}${ev.cause ? ` (${ev.cause})` : ''} cho task ${ev.task_id}, vòng ${ev.round}.`;
|
|
641
|
-
return { code: 0, out: kn ?
|
|
757
|
+
return { code: 0, out: [msg].concat(wtLines, kn && kn.out ? [kn.out] : []).join('\n') };
|
|
642
758
|
}
|
|
643
759
|
return { code: 0, out: `Đã ghi ${ev.outcome} cho ${ev.agent_id}${ev.task_id ? ` (task ${ev.task_id})` : ''}.` };
|
|
644
760
|
}
|
|
@@ -662,8 +778,14 @@ function cmdDiscard(opts) {
|
|
|
662
778
|
}, core.commonFields(ctx));
|
|
663
779
|
core.appendEvent(ctx, ev);
|
|
664
780
|
const n = events.filter((e) => e.task_id === taskId).length;
|
|
665
|
-
|
|
666
|
-
|
|
781
|
+
// s17: task bị loại → gỡ worktree có dấu của task (sạch); còn thay đổi thì giữ và báo.
|
|
782
|
+
const wtLines = wt.cleanupTask(ctx, events, taskId, opts.repo);
|
|
783
|
+
if (opts.json) {
|
|
784
|
+
if (wtLines.length) process.stderr.write(`${wtLines.join('\n')}\n`);
|
|
785
|
+
return { code: 0, out: JSON.stringify(ev) };
|
|
786
|
+
}
|
|
787
|
+
const msg = `Đã loại task ${taskId} (${n} bản ghi) khỏi thống kê, choose và state. Log không bị sửa; sink nhận bản ghi discard ở lần sync kế tiếp.`;
|
|
788
|
+
return { code: 0, out: [msg].concat(wtLines).join('\n') };
|
|
667
789
|
}
|
|
668
790
|
|
|
669
791
|
// ------------------------------------------------------------------ steer
|
|
@@ -726,19 +848,30 @@ function measureOrch(opts, ctx, prof) {
|
|
|
726
848
|
u = core.orchestratorUsage(file, core.loadPricing(ctx), { cacheFile: core.orchCacheFile(ctx.paths, sid, file) });
|
|
727
849
|
}
|
|
728
850
|
} catch (_) { /* measurement never fails the caller */ }
|
|
729
|
-
|
|
851
|
+
// core 0.7.1 (retro 2026-10-26, C1): past ORCH_NEW_SESSION_TOKENS or ORCH_NEW_SESSION_COMPACTIONS a /compact no longer
|
|
852
|
+
// helps (session 36cee993: 946k, 20 compactions, 0 user verdicts) -> advise a new session instead.
|
|
853
|
+
const newSession = (typeof u.ctx_tokens === 'number' && u.ctx_tokens > ORCH_NEW_SESSION_TOKENS)
|
|
854
|
+
|| (typeof u.compactions === 'number' && u.compactions >= ORCH_NEW_SESSION_COMPACTIONS);
|
|
855
|
+
const over = (typeof u.ctx_growth === 'number' && u.ctx_growth > softCap) || newSession;
|
|
730
856
|
let advice;
|
|
731
857
|
if (!sid) advice = 'Không đo được: không có session (thiếu CLAUDE_CODE_SESSION_ID, dùng --session <id>).';
|
|
732
858
|
else if (u.ctx_tokens === null) advice = 'Không đo được: không thấy transcript của session này.';
|
|
733
|
-
else if (
|
|
859
|
+
else if (newSession) {
|
|
860
|
+
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\`.`;
|
|
861
|
+
} 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.`;
|
|
734
862
|
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.`;
|
|
735
863
|
return {
|
|
736
864
|
session_id: sid, ctx_tokens: u.ctx_tokens, ctx_baseline: u.ctx_baseline, ctx_growth: u.ctx_growth,
|
|
737
|
-
cum_usd: u.cum_usd, compactions: u.compactions, soft_cap: softCap, over, advice,
|
|
865
|
+
cum_usd: u.cum_usd, compactions: u.compactions, soft_cap: softCap, over, new_session: newSession, advice,
|
|
738
866
|
};
|
|
739
867
|
}
|
|
740
868
|
|
|
741
869
|
const CTX_WARN_KEEP_MS = 14 * 24 * 3600 * 1000;
|
|
870
|
+
// core 0.7.2 (s11 9y3nh4cb, log 62 phiên / 4.749 lượt điều phối, số liệu trong dp-out): 400k -> 250k. Chi phí điều
|
|
871
|
+
// phối mỗi lượt đứng ~0,27–0,29 $ tới 250k rồi nhảy 0,45 $ ở 250–400k; median ctx trước bước nén 211k (150–250k). Bắt buộc
|
|
872
|
+
// < SESSION_CAP_TOKENS (800k) và < SESSION_CAP_COMPACTIONS (10) — test ctx-compact-count bảo vệ bất biến này.
|
|
873
|
+
const ORCH_NEW_SESSION_TOKENS = 250000;
|
|
874
|
+
const ORCH_NEW_SESSION_COMPACTIONS = 5;
|
|
742
875
|
|
|
743
876
|
/** 0 = within the cap; 1 = over the cap, 2 = over 2×, 3 = over 4×, … */
|
|
744
877
|
function ctxWarnLevel(growth, cap) {
|
|
@@ -808,13 +941,22 @@ function cmdState(opts) {
|
|
|
808
941
|
const ctx = ctxOf(opts);
|
|
809
942
|
const sid = typeof opts.session === 'string' && opts.session ? opts.session
|
|
810
943
|
: (process.env.CLAUDE_CODE_SESSION_ID || null);
|
|
944
|
+
// retro C6: knowledge deltas older than 24 h that no store has applied (file reads only, never throws)
|
|
945
|
+
const deltas = core.scanUnmergedDeltasFor(ctx);
|
|
946
|
+
const deltaLines = core.unmergedDeltaLines(deltas);
|
|
947
|
+
const hasDeltas = core.hasUnmergedDeltas(deltas);
|
|
811
948
|
if (!sid) {
|
|
812
|
-
if (opts.json)
|
|
813
|
-
|
|
949
|
+
if (opts.json) {
|
|
950
|
+
return { code: 0, out: JSON.stringify(Object.assign({ session_id: null, generated_at: core.nowIso(), runs_dir: null, plans: [], tasks: [] }, hasDeltas ? { unmerged_deltas: deltas } : {})) };
|
|
951
|
+
}
|
|
952
|
+
return { code: 0, out: ['Không có session (thiếu CLAUDE_CODE_SESSION_ID, dùng --session <id>).'].concat(deltaLines).join('\n') };
|
|
814
953
|
}
|
|
815
954
|
const state = core.buildSessionState(core.readEvents(ctx), sid, { plansDir: ctx.paths.plans });
|
|
816
|
-
|
|
817
|
-
|
|
955
|
+
const orphans = orphanCount(ctx); // s16: null (unknown) leaves the line out
|
|
956
|
+
if (opts.json) {
|
|
957
|
+
return { code: 0, out: JSON.stringify(Object.assign({}, state, hasDeltas ? { unmerged_deltas: deltas } : {}, orphans === null ? {} : { orphan_procs: orphans }), null, 2) };
|
|
958
|
+
}
|
|
959
|
+
return { code: 0, out: [core.formatSessionState(state, core.STATE_TEXT_CAP)].concat(deltaLines, orphans === null ? [] : [`orphan_procs: ${orphans}`]).join('\n') };
|
|
818
960
|
}
|
|
819
961
|
|
|
820
962
|
// ------------------------------------------------------------------ stats / retro
|
|
@@ -832,13 +974,13 @@ function cmdStats(opts) {
|
|
|
832
974
|
const ctx = ctxOf(opts);
|
|
833
975
|
const { lambda } = lambdaOf(opts, ctx);
|
|
834
976
|
const events = core.readEvents(ctx);
|
|
835
|
-
const { cells, steers, orchestration, tasks } = agg.analyze(events, { lambda });
|
|
977
|
+
const { cells, steers, orchestration, tasks, batch_verify: batchVerify } = agg.analyze(events, { lambda });
|
|
836
978
|
const modes = agg.modeSummary(events, tasks);
|
|
837
979
|
const type = typeof opts.type === 'string' ? opts.type : null;
|
|
838
980
|
const rows = type ? cells.filter((c) => c.task_type === type) : cells;
|
|
839
981
|
const steerRows = type ? steers.filter((s) => s.task_type === type) : steers;
|
|
840
982
|
if (opts.json) {
|
|
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) };
|
|
983
|
+
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) };
|
|
842
984
|
}
|
|
843
985
|
if (!rows.length) {
|
|
844
986
|
return { code: 0, out: orchestration.sessions ? `Chưa có dữ liệu theo ô.\n${orchLine(orchestration)}` : 'Chưa có dữ liệu.' };
|
|
@@ -865,7 +1007,7 @@ function cmdStats(opts) {
|
|
|
865
1007
|
}
|
|
866
1008
|
return {
|
|
867
1009
|
code: 0,
|
|
868
|
-
out: `${table(t)}\n\n${table(s)}\n\n${modeTable(modes)}${orchLine(orchestration)}\n\n`
|
|
1010
|
+
out: `${table(t)}\n\n${table(s)}\n\n${modeTable(modes)}${batchVerifyTable(batchVerify)}${orchLine(orchestration)}\n\n`
|
|
869
1011
|
+ 'n = task tính tỉ lệ (/số đã giao, (-k) = bị loại: reject-scope hoặc lượt exec đầu bị steer plan/redirect). '
|
|
870
1012
|
+ '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. '
|
|
871
1013
|
+ `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.`,
|
|
@@ -890,6 +1032,20 @@ function modeTable(modes) {
|
|
|
890
1032
|
return `${table(t)}\n\n`;
|
|
891
1033
|
}
|
|
892
1034
|
|
|
1035
|
+
/** core 0.7.1 (C5): the batched verifiers' own cell (model/effort), kept out of the task cells; '' when none. */
|
|
1036
|
+
function batchVerifyTable(rows) {
|
|
1037
|
+
if (!rows || !rows.length) return '';
|
|
1038
|
+
const t = [['verifier gộp', 'lần', 'task', 'Σ cost', 'E[cost]', 'p50 cost', 'p50 time']];
|
|
1039
|
+
for (const r of rows) {
|
|
1040
|
+
t.push([
|
|
1041
|
+
`${chooser.shortModel(r.model) || r.model || '?'}/${r.effort || '?'}`, r.n, r.tasks,
|
|
1042
|
+
fmtUsd(r.cost_total_usd), fmtUsd(r.cost_mean_usd), fmtUsd(r.cost_p50_usd),
|
|
1043
|
+
r.time_p50_min === null ? '-' : `${fmtNum(r.time_p50_min)}m`,
|
|
1044
|
+
]);
|
|
1045
|
+
}
|
|
1046
|
+
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`;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
893
1049
|
/** One retro line: per mode with tasks. */
|
|
894
1050
|
function modeRetroLine(modes) {
|
|
895
1051
|
const parts = agg.MODE_KEYS.filter((m) => modes[m].tasks).map((m) => {
|
|
@@ -906,7 +1062,7 @@ function cmdRetro(opts) {
|
|
|
906
1062
|
const ctx = ctxOf(opts);
|
|
907
1063
|
const { prof, lambda } = lambdaOf(opts, ctx);
|
|
908
1064
|
const events = core.readEvents(ctx);
|
|
909
|
-
const { agents, tasks, cells, steers, userVerdicts, overrides, orchestration } = agg.analyze(events, { lambda });
|
|
1065
|
+
const { agents, tasks, cells, steers, userVerdicts, overrides, orchestration, batch_verify: batchVerify } = agg.analyze(events, { lambda });
|
|
910
1066
|
const modes = agg.modeSummary(events, tasks);
|
|
911
1067
|
const reasons = {};
|
|
912
1068
|
const outcomes = {};
|
|
@@ -964,6 +1120,7 @@ function cmdRetro(opts) {
|
|
|
964
1120
|
overrides: overrides.map((o) => ({ ts: o.ts, task_type: o.task_type, suggested_tier: o.suggested_tier, chosen_tier: o.chosen_tier })),
|
|
965
1121
|
orchestration,
|
|
966
1122
|
modes,
|
|
1123
|
+
batch_verify: batchVerify,
|
|
967
1124
|
cells,
|
|
968
1125
|
};
|
|
969
1126
|
if (opts.json) return { code: 0, out: JSON.stringify(summary, null, 2) };
|
|
@@ -977,6 +1134,7 @@ function cmdRetro(opts) {
|
|
|
977
1134
|
`Steer nhiều (brainstorm hỏi quá ít): ${highSteer.map((s) => `${s.task_type} ${pct(s.steer_rate)}`).join(', ') || '-'}`,
|
|
978
1135
|
orchLine(orchestration),
|
|
979
1136
|
modeRetroLine(modes),
|
|
1137
|
+
...(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(' · ')}`] : []),
|
|
980
1138
|
'Chi tiết theo ô: `dieu-phoi stats`; đưa cho LLM: `dieu-phoi retro --json`.',
|
|
981
1139
|
];
|
|
982
1140
|
return { code: 0, out: lines.join('\n') };
|
|
@@ -1062,6 +1220,7 @@ function cmdChoose(opts) {
|
|
|
1062
1220
|
res.orch_ctx_tokens = orch.ctx_tokens;
|
|
1063
1221
|
res.orch_ctx_growth = orch.ctx_growth;
|
|
1064
1222
|
res.orch_ctx_over = orch.over;
|
|
1223
|
+
res.orch_new_session = orch.new_session === true;
|
|
1065
1224
|
res.orch_ctx_warn = ctxWarnOnce(ctx, orch); // over AND a new threshold crossed since the last warning
|
|
1066
1225
|
if (res.overridden) {
|
|
1067
1226
|
const tid = input.taskId;
|
|
@@ -1072,6 +1231,8 @@ function cmdChoose(opts) {
|
|
|
1072
1231
|
core.recordSuggestion(ctx, {
|
|
1073
1232
|
task_id: input.taskId, role: res.role || input.role, suggested_tier: res.suggested_tier || res.tier, choose_tier: res.tier,
|
|
1074
1233
|
source: 'choose', keep_suggested: input.approved !== null && !res.overridden,
|
|
1234
|
+
choose_task_type: res.task_type, choose_score: res.score, explored: res.explored, reason_code: res.reason_code,
|
|
1235
|
+
thompson_score: res.thompson_score === null ? undefined : res.thompson_score,
|
|
1075
1236
|
});
|
|
1076
1237
|
}
|
|
1077
1238
|
if (opts.json) return { code: 0, out: JSON.stringify(res) };
|
|
@@ -1238,7 +1399,8 @@ function planTypeNotes(plan) {
|
|
|
1238
1399
|
* `choose --override` already logged adds nothing.
|
|
1239
1400
|
*/
|
|
1240
1401
|
function planOverrideEvents(ctx, plan, events) {
|
|
1241
|
-
|
|
1402
|
+
// core 0.7.2 (K4): an implicit override must not hide the user's override of the same tier from the learning
|
|
1403
|
+
const seen = new Set(events.filter((e) => e.kind === 'override' && e.source !== 'implicit').map((e) => `${e.task_id}|${e.chosen_tier}`));
|
|
1242
1404
|
const out = [];
|
|
1243
1405
|
for (const r of plan.steps) {
|
|
1244
1406
|
if (!r.overridden || !r.task_id || !r.tier || !r.suggested_tier) continue;
|
|
@@ -1257,6 +1419,8 @@ function recordPlanSuggestions(ctx, plan) {
|
|
|
1257
1419
|
if (!r.task_id || !r.tier || r.action !== 'dispatch') continue;
|
|
1258
1420
|
const ok = core.recordSuggestion(ctx, {
|
|
1259
1421
|
task_id: r.task_id, role: r.role || 'exec', suggested_tier: r.suggested_tier || r.tier, choose_tier: r.tier, source: 'plan',
|
|
1422
|
+
choose_task_type: r.task_type, choose_score: r.score, explored: r.explored, reason_code: r.reason_code,
|
|
1423
|
+
thompson_score: r.thompson_score === null ? undefined : r.thompson_score,
|
|
1260
1424
|
});
|
|
1261
1425
|
if (ok) n++;
|
|
1262
1426
|
}
|
|
@@ -1329,6 +1493,7 @@ function cmdPlanTable(opts) {
|
|
|
1329
1493
|
const base = chooseInput(Object.assign({}, opts, { type: 'x', score: 0, override: undefined }), ctx, core.readEvents(ctx));
|
|
1330
1494
|
delete base.override;
|
|
1331
1495
|
const rootEv = typeof opts.root === 'string' && core.isValidTaskId(opts.root) ? core.rootModeFromEvents(base.events, opts.root) : null;
|
|
1496
|
+
const modeChosen = Boolean(base.mode || rootEv || fileMode);
|
|
1332
1497
|
base.mode = base.mode || (rootEv ? rootEv.mode : null) || fileMode || core.DEFAULT_MODE;
|
|
1333
1498
|
const plan = buildPlanTable(raw, base);
|
|
1334
1499
|
const problems = planTaskIdProblems(plan, opts.approve ? String(opts.root) : null);
|
|
@@ -1365,7 +1530,12 @@ function cmdPlanTable(opts) {
|
|
|
1365
1530
|
}
|
|
1366
1531
|
lines.push('');
|
|
1367
1532
|
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
|
-
|
|
1533
|
+
// Mode được hỏi ở cổng 1 chỉ khi root chưa có mode và kế hoạch có ≥4 bước s≤6 (0.7.2); còn lại không hỏi
|
|
1534
|
+
const shortSteps = plan.steps.filter((r) => r.action !== 'refused' && Number.isFinite(r.score) && r.score <= 6).length;
|
|
1535
|
+
const modeText = modeChosen ? plan.mode : (shortSteps >= 4
|
|
1536
|
+
? `HỎI người dùng chọn mode (${shortSteps} bước s≤6): fast | normal | strict (mặc định normal); đặt bằng \`dieu-phoi mode --root ${typeof opts.root === 'string' ? opts.root : '<root_task_id>'} <mode>\``
|
|
1537
|
+
: 'normal (không hỏi)');
|
|
1538
|
+
lines.push(`**Mode:** ${modeText}${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` : ''}`);
|
|
1369
1539
|
if (plan.rough) lines.push('', '_(thô) = ô chưa đủ 5 task, ước lượng theo giả định token của từng tier._');
|
|
1370
1540
|
if (plan.refused.length) {
|
|
1371
1541
|
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.`);
|
|
@@ -1378,6 +1548,6 @@ function cmdPlanTable(opts) {
|
|
|
1378
1548
|
}
|
|
1379
1549
|
|
|
1380
1550
|
module.exports = {
|
|
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,
|
|
1551
|
+
ORCH_NEW_SESSION_TOKENS, ORCH_NEW_SESSION_COMPACTIONS, 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, orphanCheck, orphanCount, measureOrch,
|
|
1382
1552
|
cmdPlanTable, cmdNewId, requireTaskId, buildPlanTable, recordApprovedPlan, planTaskIdProblems, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
|
|
1383
1553
|
};
|