@kienntpixon/dieu-phoi 0.3.0 → 0.5.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/CHANGELOG.md +33 -0
- package/README.md +10 -3
- package/agents/dp-exec.md +38 -0
- package/agents/dp-helper.md +23 -0
- package/agents/dp-scout.md +25 -0
- package/agents/dp-verify.md +25 -0
- package/bin/dieu-phoi.cjs +8 -3
- package/hook.cjs +600 -23
- package/lib/aggregate.cjs +98 -19
- package/lib/choose.cjs +79 -19
- package/lib/commands.cjs +206 -23
- package/lib/setup.cjs +71 -2
- package/package.json +2 -1
package/lib/commands.cjs
CHANGED
|
@@ -10,7 +10,7 @@ const chooser = require('./choose.cjs');
|
|
|
10
10
|
const setupLib = require('./setup.cjs');
|
|
11
11
|
const { claudeDirOf, ctxOf, yesNo } = require('./args.cjs');
|
|
12
12
|
|
|
13
|
-
const AGENT_OUTCOMES = ['pass', 'fail', 'timeout'];
|
|
13
|
+
const AGENT_OUTCOMES = ['pass', 'fail', 'timeout', 'aborted'];
|
|
14
14
|
const USER_OUTCOMES = ['accept', 'reject'];
|
|
15
15
|
const CAUSES = ['quality', 'scope'];
|
|
16
16
|
const STEER_KINDS = ['minor', 'plan', 'redirect'];
|
|
@@ -65,6 +65,7 @@ function cmdSetup(opts) {
|
|
|
65
65
|
res.settings_changed
|
|
66
66
|
? `Đã đăng ký PostToolUse(Agent) + SubagentStop + SessionStart(compact) + UserPromptSubmit trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
|
|
67
67
|
: `settings.json đã có hook, không đổi gì (${res.settings})`,
|
|
68
|
+
`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)' : ''}`,
|
|
68
69
|
`Thư mục báo cáo subagent: ${res.runs_dir} (quyền: ${res.permissions.allow.join(', ')} + additionalDirectories)`,
|
|
69
70
|
`Phòng ban: ${res.config.dept || '(chưa đặt, dùng --dept)'}`,
|
|
70
71
|
`Sink: ${res.config.sink_url ? res.config.sink_url : '(chưa có — log chỉ lưu cục bộ)'}`,
|
|
@@ -80,7 +81,7 @@ function cmdUninstall(opts) {
|
|
|
80
81
|
return {
|
|
81
82
|
code: 0,
|
|
82
83
|
out: res.removed
|
|
83
|
-
? `Đã gỡ ${res.removed} hook khỏi ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}. Dữ liệu giữ ở ${res.data_kept}.`
|
|
84
|
+
? `Đã gỡ ${res.removed} hook khỏi ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}${res.agents_removed.length ? `, agent ${res.agents_removed.join(', ')}` : ''}. Dữ liệu giữ ở ${res.data_kept}.`
|
|
84
85
|
: `Không thấy hook dieu-phoi trong ${res.settings}.`,
|
|
85
86
|
};
|
|
86
87
|
}
|
|
@@ -105,6 +106,11 @@ async function cmdDoctor(opts) {
|
|
|
105
106
|
add(permOk, 'run-permissions', permOk
|
|
106
107
|
? `${reg.permission_entries.allow.join(', ')} + additionalDirectories ${reg.permission_entries.dir}`
|
|
107
108
|
: `${!reg.permissions ? `thiếu quyền cho thư mục báo cáo ${reg.permission_entries.dir} (subagent nền sẽ không ghi được)` : (!reg.runs_dir_exists ? `chưa có thư mục ${reg.runs_dir}` : 'còn quyền cũ cho ~/.claude/dieu-phoi/runs (đường dẫn được bảo vệ, không dùng được)')} — chạy dieu-phoi setup`, 'CẢNH BÁO');
|
|
109
|
+
// compare with the node binary setup wrote into settings.json (and the agent files), not this process's node: a doctor run
|
|
110
|
+
// under another node (nvm, upgrade) must not report a false "outdated"
|
|
111
|
+
const agentSt = setupLib.agentStatus(claudeDir, ctx.paths.hook, setupLib.nodePathOf(reg.commands[0]));
|
|
112
|
+
const agentsOk = agentSt.every((a) => a.state === 'ok');
|
|
113
|
+
add(agentsOk, 'agents', `${agentSt.map((a) => `${a.name}=${a.state}`).join(', ')}${agentsOk ? '' : ' — chạy `dieu-phoi setup` (foreign = file trùng tên không phải của dieu-phoi, không bị ghi đè)'}`, 'CẢNH BÁO');
|
|
108
114
|
const hasCfg = fs.existsSync(ctx.paths.config);
|
|
109
115
|
add(hasCfg, 'config', hasCfg ? `${ctx.paths.config} dept=${ctx.config.dept || '-'}` : 'chưa có — chạy `dieu-phoi setup`');
|
|
110
116
|
const sd = ctx.config.skill_dir;
|
|
@@ -145,6 +151,15 @@ async function cmdDoctor(opts) {
|
|
|
145
151
|
add(!badIds.length, 'task-ids', badIds.length
|
|
146
152
|
? `${badIds.length} dispatch trong ${INVALID_ID_WINDOW_DAYS} ngày gần đây có task_id sai định dạng (${Array.from(new Set(badIds.map((e) => e.task_id))).slice(0, 3).map((x) => `"${x}"`).join(', ')}) — task_id phải đúng 8 ký tự a-z0-9, lấy bằng \`dieu-phoi new-id\``
|
|
147
153
|
: `mọi dispatch trong ${INVALID_ID_WINDOW_DAYS} ngày gần đây có task_id đúng định dạng`, 'CẢNH BÁO');
|
|
154
|
+
const tmp = sweepStaleTmp(ctx);
|
|
155
|
+
add(!tmp.left.length, 'tmp-files', tmp.left.length
|
|
156
|
+
? `${tmp.left.length} file tạm còn mới (đang ghi?): ${tmp.left.slice(0, 3).join(', ')}`
|
|
157
|
+
: `không còn file *.tmp-* bỏ dở${tmp.removed ? ` (đã xoá ${tmp.removed} file cũ hơn 1 giờ)` : ''}`, 'CẢNH BÁO');
|
|
158
|
+
let badPlans = [];
|
|
159
|
+
try { badPlans = fs.readdirSync(ctx.paths.plans).filter((n) => n.endsWith('.json') && !core.isValidTaskId(n.slice(0, -5))); } catch (_) { /* none */ }
|
|
160
|
+
add(!badPlans.length, 'plans', badPlans.length
|
|
161
|
+
? `${badPlans.length} kế hoạch có root id sai định dạng (${badPlans.slice(0, 3).join(', ')}) — \`state\` vẫn đọc; loại task bằng \`dieu-phoi discard --task <id>\` hoặc xoá file trong ${ctx.paths.plans}`
|
|
162
|
+
: 'mọi kế hoạch đã duyệt có root id đúng định dạng', 'CẢNH BÁO');
|
|
148
163
|
const orch = measureOrch(opts, ctx, profileOrNull(opts, ctx));
|
|
149
164
|
if (orch.ctx_tokens !== null) {
|
|
150
165
|
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');
|
|
@@ -166,6 +181,33 @@ async function cmdDoctor(opts) {
|
|
|
166
181
|
return { code: failed ? 1 : 0, out };
|
|
167
182
|
}
|
|
168
183
|
|
|
184
|
+
const TMP_RE = /\.tmp-\d+-[0-9a-f]+$/;
|
|
185
|
+
const TMP_STALE_MS = 3600 * 1000;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Leftovers of writeFileAtomic (tmp written, rename never happened: process killed, or rename lost a race on
|
|
189
|
+
* Windows) under data/, data/orch, data/segments, data/plans. Older than 1 h → removed; newer → reported.
|
|
190
|
+
*/
|
|
191
|
+
function sweepStaleTmp(ctx) {
|
|
192
|
+
let removed = 0;
|
|
193
|
+
const left = [];
|
|
194
|
+
const now = Date.now();
|
|
195
|
+
for (const dir of [ctx.paths.data, path.join(ctx.paths.data, 'orch'), ctx.paths.segments, ctx.paths.plans]) {
|
|
196
|
+
let names = [];
|
|
197
|
+
try { names = fs.readdirSync(dir); } catch (_) { continue; }
|
|
198
|
+
for (const n of names) {
|
|
199
|
+
if (!TMP_RE.test(n)) continue;
|
|
200
|
+
const f = path.join(dir, n);
|
|
201
|
+
try {
|
|
202
|
+
const st = fs.statSync(f);
|
|
203
|
+
if (!st.isFile()) continue; // a directory (or anything else) with a tmp-like name: not ours, leave it alone
|
|
204
|
+
if (now - st.mtimeMs > TMP_STALE_MS) { fs.unlinkSync(f); removed++; } else left.push(n);
|
|
205
|
+
} catch (_) { left.push(n); }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { removed, left };
|
|
209
|
+
}
|
|
210
|
+
|
|
169
211
|
/** Live sink check (view=stats); result cached in data/sink-check.json for 24 h. */
|
|
170
212
|
async function checkSinkLive(ctx) {
|
|
171
213
|
let ok = false;
|
|
@@ -211,6 +253,7 @@ function cmdStart(opts) {
|
|
|
211
253
|
`step_ids: ${info.step_ids.join(' ')}`,
|
|
212
254
|
`health: ${core.healthLine(h)}`,
|
|
213
255
|
`scout_brief: ${info.scout_brief || '(không thấy — dùng references/brief-template.md mục E)'}`,
|
|
256
|
+
...core.orchLessonLines(info),
|
|
214
257
|
];
|
|
215
258
|
return { code: 0, out: out.join('\n') };
|
|
216
259
|
}
|
|
@@ -248,7 +291,10 @@ function resolveAgentId(events, ref, now) {
|
|
|
248
291
|
const verdicted = new Set();
|
|
249
292
|
events.forEach((e, i) => {
|
|
250
293
|
if (!e.agent_id) return;
|
|
251
|
-
if (e.kind === 'verdict'
|
|
294
|
+
if (e.kind === 'verdict') {
|
|
295
|
+
const nv = agg.normalizeVerdict(e);
|
|
296
|
+
if (nv.by === 'agent' && nv.outcome !== agg.ABORTED) verdicted.add(e.agent_id); // aborted still needs a verdict
|
|
297
|
+
}
|
|
252
298
|
if (e.kind !== 'complete') return;
|
|
253
299
|
const t = Date.parse(e.ts) || 0;
|
|
254
300
|
const cur = latest.get(e.agent_id);
|
|
@@ -285,8 +331,9 @@ function kebab(s) {
|
|
|
285
331
|
function verdictAgent(opts, ctx, events) {
|
|
286
332
|
const [ref, outcome] = opts._;
|
|
287
333
|
if (!ref || !outcome) {
|
|
288
|
-
throw new Error('Cú pháp: dieu-phoi verdict <agent_id|last> <pass|fail|timeout> [--verify V] [--reason R]\n'
|
|
289
|
-
+ ' dieu-phoi verdict --task <task_id> <accept|reject> [--cause quality|scope] [--round n]'
|
|
334
|
+
throw new Error('Cú pháp: dieu-phoi verdict <agent_id|last> <pass|fail|timeout|aborted> [--verify V] [--reason R]\n'
|
|
335
|
+
+ ' dieu-phoi verdict --task <task_id> <accept|reject> [--cause quality|scope] [--round n]\n'
|
|
336
|
+
+ ' dieu-phoi verdict --root <root_task_id> accept [--steps a,b]');
|
|
290
337
|
}
|
|
291
338
|
if (outcome === 'rework') {
|
|
292
339
|
throw new Error('"rework" đã bỏ. Người dùng không nhận kết quả → dieu-phoi verdict --task <task_id> reject --cause quality');
|
|
@@ -344,9 +391,46 @@ function verdictUser(opts, ctx, events) {
|
|
|
344
391
|
}, core.commonFields(ctx));
|
|
345
392
|
}
|
|
346
393
|
|
|
394
|
+
/**
|
|
395
|
+
* `verdict --root <root> accept [--steps a,b]` (core 0.4.0): the user accepts the steps of the latest approved plan
|
|
396
|
+
* in one call. Covers every step whose task has an exec dispatch and no user verdict yet (a step already rejected
|
|
397
|
+
* or accepted is left alone: reject the bad ones per task first, then accept the rest by root).
|
|
398
|
+
*/
|
|
399
|
+
function verdictRoot(opts, ctx, events) {
|
|
400
|
+
const root = requireTaskId(String(opts.root));
|
|
401
|
+
if (opts._[0] !== 'accept') {
|
|
402
|
+
throw new Error('verdict --root chỉ nhận accept; bước không đạt → dieu-phoi verdict --task <task_id> reject --cause quality|scope');
|
|
403
|
+
}
|
|
404
|
+
const plan = core.readJsonFile(path.join(ctx.paths.plans, `${root}.json`), null);
|
|
405
|
+
if (!plan || !Array.isArray(plan.steps)) throw new Error(`Không thấy kế hoạch đã duyệt của root ${root} (plan-table --approve --root ${root}).`);
|
|
406
|
+
let steps = plan.steps.filter((x) => x && x.task_id);
|
|
407
|
+
if (typeof opts.steps === 'string') {
|
|
408
|
+
const want = new Set(opts.steps.split(',').map((x) => x.trim()).filter(Boolean));
|
|
409
|
+
const ids = new Set(steps.map((x) => String(x.id)));
|
|
410
|
+
const missing = Array.from(want).filter((x) => !ids.has(x));
|
|
411
|
+
if (missing.length) throw new Error(`--steps: không có bước ${missing.join(', ')} trong kế hoạch ${root}`);
|
|
412
|
+
steps = steps.filter((x) => want.has(String(x.id)));
|
|
413
|
+
}
|
|
414
|
+
const execTasks = new Set(events.filter((e) => e.kind === 'dispatch' && (e.role || 'exec') === 'exec').map((e) => e.task_id));
|
|
415
|
+
const judged = new Set(events.filter((e) => e.kind === 'verdict' && agg.normalizeVerdict(e).by === 'user').map((e) => e.task_id));
|
|
416
|
+
const gone = agg.discardedTaskIds(events);
|
|
417
|
+
const todo = steps.filter((x) => execTasks.has(x.task_id) && !judged.has(x.task_id) && !gone.has(x.task_id));
|
|
418
|
+
if (!todo.length) throw new Error(`Không có bước nào của ${root} cần nghiệm thu (chưa giao exec, hoặc đã có verdict người dùng).`);
|
|
419
|
+
return todo.map((x) => ({
|
|
420
|
+
step: String(x.id),
|
|
421
|
+
ev: verdictUser(Object.assign({}, opts, { task: x.task_id, _: ['accept'], round: undefined, cause: undefined }), ctx, events),
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
|
|
347
425
|
function cmdVerdict(opts) {
|
|
348
426
|
const ctx = ctxOf(opts);
|
|
349
427
|
const events = core.readEvents(ctx);
|
|
428
|
+
if (typeof opts.root === 'string' && typeof opts.task !== 'string') {
|
|
429
|
+
const list = verdictRoot(opts, ctx, events);
|
|
430
|
+
for (const x of list) core.appendEvent(ctx, x.ev);
|
|
431
|
+
if (opts.json) return { code: 0, out: JSON.stringify(list.map((x) => x.ev)) };
|
|
432
|
+
return { code: 0, out: `Đã 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(', ')}.` };
|
|
433
|
+
}
|
|
350
434
|
const ev = typeof opts.task === 'string' ? verdictUser(opts, ctx, events) : verdictAgent(opts, ctx, events);
|
|
351
435
|
core.appendEvent(ctx, ev);
|
|
352
436
|
if (opts.json) return { code: 0, out: JSON.stringify(ev) };
|
|
@@ -356,6 +440,29 @@ function cmdVerdict(opts) {
|
|
|
356
440
|
return { code: 0, out: `Đã ghi ${ev.outcome} cho ${ev.agent_id}${ev.task_id ? ` (task ${ev.task_id})` : ''}.` };
|
|
357
441
|
}
|
|
358
442
|
|
|
443
|
+
// ------------------------------------------------------------------ discard
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* `dieu-phoi discard --task <task_id> [--reason R]` (core 0.4.0): withdraw a mistaken task. Appends one
|
|
447
|
+
* `discard` event (synced like any other); stats, choose, state and the sink then leave out every event of the
|
|
448
|
+
* task and of its agents. events.jsonl is never edited — a hand edit once lost a dispatch that the sink kept.
|
|
449
|
+
*/
|
|
450
|
+
function cmdDiscard(opts) {
|
|
451
|
+
if (typeof opts.task !== 'string') throw new Error('Cú pháp: dieu-phoi discard --task <task_id> [--reason R]');
|
|
452
|
+
const taskId = requireTaskId(opts.task);
|
|
453
|
+
const ctx = ctxOf(opts);
|
|
454
|
+
const events = core.readEvents(ctx);
|
|
455
|
+
if (!events.some((e) => e.task_id === taskId)) throw new Error(`Không thấy task "${taskId}" trong log.`);
|
|
456
|
+
if (agg.discardedTaskIds(events).has(taskId)) return { code: 0, out: opts.json ? JSON.stringify({ already: true, task_id: taskId }) : `Task ${taskId} đã bị loại trước đó.` };
|
|
457
|
+
const ev = Object.assign({
|
|
458
|
+
schema: core.SCHEMA, kind: 'discard', ts: core.nowIso(), agent_id: `task:${taskId}`, task_id: taskId, reason_code: kebab(opts.reason),
|
|
459
|
+
}, core.commonFields(ctx));
|
|
460
|
+
core.appendEvent(ctx, ev);
|
|
461
|
+
const n = events.filter((e) => e.task_id === taskId).length;
|
|
462
|
+
if (opts.json) return { code: 0, out: JSON.stringify(ev) };
|
|
463
|
+
return { code: 0, out: `Đã 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.` };
|
|
464
|
+
}
|
|
465
|
+
|
|
359
466
|
// ------------------------------------------------------------------ steer
|
|
360
467
|
|
|
361
468
|
/**
|
|
@@ -383,7 +490,8 @@ function cmdSteer(opts) {
|
|
|
383
490
|
const taskId = String(opts.task);
|
|
384
491
|
const mine = agents.filter((a) => a.task_id === taskId);
|
|
385
492
|
if (!mine.length && !events.some((e) => e.task_id === taskId)) throw new Error(`Không thấy task ${taskId} trong log`);
|
|
386
|
-
|
|
493
|
+
// core 0.4.0: verifiers are never marked (a steer is about the work, not its check)
|
|
494
|
+
const inFlight = mine.filter((a) => a.dispatched && a.role !== 'verify' && !(a.completed && a.outcome));
|
|
387
495
|
const type = (mine.find((a) => a.role === 'exec') || mine[0] || {}).task_type || null;
|
|
388
496
|
out = inFlight.length ? inFlight.map((a) => mk(a.agent_id, taskId, a.task_type)) : [mk(`task:${taskId}`, taskId, type)];
|
|
389
497
|
} else {
|
|
@@ -427,6 +535,40 @@ function measureOrch(opts, ctx, prof) {
|
|
|
427
535
|
};
|
|
428
536
|
}
|
|
429
537
|
|
|
538
|
+
const CTX_WARN_KEEP_MS = 14 * 24 * 3600 * 1000;
|
|
539
|
+
|
|
540
|
+
/** 0 = within the cap; 1 = over the cap, 2 = over 2×, 3 = over 4×, … */
|
|
541
|
+
function ctxWarnLevel(growth, cap) {
|
|
542
|
+
if (!(typeof growth === 'number' && cap > 0 && growth > cap)) return 0;
|
|
543
|
+
return 1 + Math.floor(Math.log2(growth / cap));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Core 0.4.0: the over-cap advice fires once per threshold crossing (cap, 2×, 4×, …) per session, and again after
|
|
548
|
+
* a /compact that left the session over. State: data/ctx-warn.json {session_id: {level, compactions, t}}.
|
|
549
|
+
* Never throws (no state → warn).
|
|
550
|
+
*/
|
|
551
|
+
function ctxWarnOnce(ctx, m) {
|
|
552
|
+
if (!m || !m.over || !m.session_id) return false;
|
|
553
|
+
const file = path.join(ctx.paths.data, 'ctx-warn.json');
|
|
554
|
+
const level = ctxWarnLevel(m.ctx_growth, m.soft_cap);
|
|
555
|
+
try {
|
|
556
|
+
const st = core.readJsonFile(file, {}) || {};
|
|
557
|
+
const cur = st[m.session_id];
|
|
558
|
+
const comp = typeof m.compactions === 'number' ? m.compactions : null;
|
|
559
|
+
const warn = !cur || cur.compactions !== comp || level > (Number(cur.level) || 0);
|
|
560
|
+
if (warn) {
|
|
561
|
+
const now = Date.now();
|
|
562
|
+
st[m.session_id] = { level, compactions: comp, t: now };
|
|
563
|
+
for (const k of Object.keys(st)) if (!(now - Number(st[k] && st[k].t) < CTX_WARN_KEEP_MS)) delete st[k];
|
|
564
|
+
core.writeFileAtomic(file, `${JSON.stringify(st)}\n`);
|
|
565
|
+
}
|
|
566
|
+
return warn;
|
|
567
|
+
} catch (_) {
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
430
572
|
function profileOrNull(opts, ctx) {
|
|
431
573
|
try { return chooser.loadProfile(skillDirOf(opts, ctx)); } catch (_) { return null; }
|
|
432
574
|
}
|
|
@@ -508,9 +650,9 @@ function cmdStats(opts) {
|
|
|
508
650
|
c.note,
|
|
509
651
|
]);
|
|
510
652
|
}
|
|
511
|
-
const s = [['task_type', 'task', 'task có steer', 'lần steer', 'minor', 'plan', 'redirect', 'tỉ lệ steer']];
|
|
653
|
+
const s = [['task_type', 'task', 'task có steer', 'lần steer', 'minor', 'plan', 'redirect', 'tỉ lệ steer', 'ngoài exec (không tính)']];
|
|
512
654
|
for (const r of steerRows) {
|
|
513
|
-
s.push([r.task_type, r.tasks, r.steered_tasks, r.actions, r.minor, r.plan, r.redirect, pct(r.steer_rate)]);
|
|
655
|
+
s.push([r.task_type, r.tasks, r.steered_tasks, r.actions, r.minor, r.plan, r.redirect, pct(r.steer_rate), r.plan_phase_actions || 0]);
|
|
514
656
|
}
|
|
515
657
|
return {
|
|
516
658
|
code: 0,
|
|
@@ -534,6 +676,7 @@ function cmdRetro(opts) {
|
|
|
534
676
|
const userOutcomes = {};
|
|
535
677
|
for (const a of agents) {
|
|
536
678
|
if (a.outcome) outcomes[a.outcome] = (outcomes[a.outcome] || 0) + 1;
|
|
679
|
+
if (a.aborted) outcomes.aborted = (outcomes.aborted || 0) + 1;
|
|
537
680
|
if (a.reason_code) reasons[a.reason_code] = (reasons[a.reason_code] || 0) + 1;
|
|
538
681
|
}
|
|
539
682
|
for (const v of userVerdicts) {
|
|
@@ -565,6 +708,7 @@ function cmdRetro(opts) {
|
|
|
565
708
|
no_verdict_agents: agents.filter((a) => a.completed && !a.outcome && a.role !== 'verify').length,
|
|
566
709
|
no_user_verdict_tasks: tasks.filter((t) => !t.user_verdicts.length).length,
|
|
567
710
|
multi_attempt_tasks: escalations,
|
|
711
|
+
discarded_tasks: agg.discardedTaskIds(events).size,
|
|
568
712
|
overrides: overrides.length,
|
|
569
713
|
cost_usd: agents.reduce((s, a) => s + (a.cost_usd || 0), 0),
|
|
570
714
|
},
|
|
@@ -614,6 +758,19 @@ function chooseInput(opts, ctx, events) {
|
|
|
614
758
|
};
|
|
615
759
|
}
|
|
616
760
|
|
|
761
|
+
function overrideEvent(ctx, taskId, taskType, suggested, chosen) {
|
|
762
|
+
return Object.assign({
|
|
763
|
+
schema: core.SCHEMA,
|
|
764
|
+
kind: 'override',
|
|
765
|
+
ts: core.nowIso(),
|
|
766
|
+
agent_id: taskId ? `task:${taskId}` : `override-${crypto.randomBytes(6).toString('hex')}`,
|
|
767
|
+
task_id: taskId || null,
|
|
768
|
+
task_type: taskType,
|
|
769
|
+
suggested_tier: suggested,
|
|
770
|
+
chosen_tier: chosen,
|
|
771
|
+
}, core.commonFields(ctx));
|
|
772
|
+
}
|
|
773
|
+
|
|
617
774
|
function cmdChoose(opts) {
|
|
618
775
|
const ctx = ctxOf(opts);
|
|
619
776
|
if (opts.override !== undefined && typeof opts.task !== 'string') {
|
|
@@ -626,21 +783,14 @@ function cmdChoose(opts) {
|
|
|
626
783
|
res.orch_ctx_tokens = orch.ctx_tokens;
|
|
627
784
|
res.orch_ctx_growth = orch.ctx_growth;
|
|
628
785
|
res.orch_ctx_over = orch.over;
|
|
786
|
+
res.orch_ctx_warn = ctxWarnOnce(ctx, orch); // over AND a new threshold crossed since the last warning
|
|
629
787
|
if (res.overridden) {
|
|
630
788
|
const tid = input.taskId;
|
|
631
|
-
core.appendEvent(ctx,
|
|
632
|
-
schema: core.SCHEMA,
|
|
633
|
-
kind: 'override',
|
|
634
|
-
ts: core.nowIso(),
|
|
635
|
-
agent_id: tid ? `task:${tid}` : `override-${require('crypto').randomBytes(6).toString('hex')}`,
|
|
636
|
-
task_id: tid,
|
|
637
|
-
task_type: String(input.type).toLowerCase(),
|
|
638
|
-
suggested_tier: res.suggested_tier,
|
|
639
|
-
chosen_tier: res.tier,
|
|
640
|
-
}, core.commonFields(ctx)));
|
|
789
|
+
core.appendEvent(ctx, overrideEvent(ctx, tid, res.task_type || String(input.type).toLowerCase(), res.suggested_tier, res.tier));
|
|
641
790
|
}
|
|
642
791
|
if (opts.json) return { code: 0, out: JSON.stringify(res) };
|
|
643
|
-
const
|
|
792
|
+
const typeWarn = res.type_warning && !String(res.reason).includes(res.type_warning) ? `\nCẢNH BÁO: ${res.type_warning}` : '';
|
|
793
|
+
const warn = `${typeWarn}${res.orch_ctx_warn ? `\nCẢNH BÁO: ${orch.advice}` : ''}`;
|
|
644
794
|
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ô'})`;
|
|
645
795
|
if (res.action === 'ask_user') {
|
|
646
796
|
return { code: 0, out: `HỎI NGƯỜI DÙNG — ${res.reason}${warn}` };
|
|
@@ -775,9 +925,35 @@ function planTaskIdProblems(plan, root) {
|
|
|
775
925
|
const same = plan.steps.filter((r) => r.task_id === root).map((r) => r.step);
|
|
776
926
|
if (same.length) problems.push(`task_id của bước ${same.join(', ')} trùng --root "${root}" (root dành cho scout/planner)`);
|
|
777
927
|
}
|
|
928
|
+
// core 0.4.0: a label outside the fixed list (and without an alias) would never pool its stats
|
|
929
|
+
for (const r of plan.steps) if (r.unknown_type) problems.push(`bước ${r.step}: ${r.type_warning}`);
|
|
778
930
|
return problems;
|
|
779
931
|
}
|
|
780
932
|
|
|
933
|
+
/** Mapped ad-hoc labels (bug-fix → debug, …): not an error, but the tag must carry the mapped type. */
|
|
934
|
+
function planTypeNotes(plan) {
|
|
935
|
+
return plan.steps.filter((r) => r.mapped_from).map((r) => `bước ${r.step}: ${r.type_warning}`);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Overrides chosen in the steps file (step `override`) become override events when the plan is approved —
|
|
940
|
+
* also on a re-approval that changes a step's tier — so the override learning sees them (core 0.4.0; before,
|
|
941
|
+
* only `choose --override` logged one). One event per (task_id, chosen tier): re-approving the same tier or a
|
|
942
|
+
* `choose --override` already logged adds nothing.
|
|
943
|
+
*/
|
|
944
|
+
function planOverrideEvents(ctx, plan, events) {
|
|
945
|
+
const seen = new Set(events.filter((e) => e.kind === 'override').map((e) => `${e.task_id}|${e.chosen_tier}`));
|
|
946
|
+
const out = [];
|
|
947
|
+
for (const r of plan.steps) {
|
|
948
|
+
if (!r.overridden || !r.task_id || !r.tier || !r.suggested_tier) continue;
|
|
949
|
+
const k = `${r.task_id}|${r.tier}`;
|
|
950
|
+
if (seen.has(k)) continue;
|
|
951
|
+
seen.add(k);
|
|
952
|
+
out.push(overrideEvent(ctx, r.task_id, r.task_type || r.type, r.suggested_tier, r.tier));
|
|
953
|
+
}
|
|
954
|
+
return out;
|
|
955
|
+
}
|
|
956
|
+
|
|
781
957
|
function recordApprovedPlan(ctx, root, stepsFile, plan) {
|
|
782
958
|
const file = path.join(ctx.paths.plans, `${root}.json`);
|
|
783
959
|
const prev = core.readJsonFile(file, null);
|
|
@@ -793,7 +969,7 @@ function recordApprovedPlan(ctx, root, stepsFile, plan) {
|
|
|
793
969
|
approved_at: core.nowIso(),
|
|
794
970
|
approval: prevApproval(prev) + 1,
|
|
795
971
|
steps: plan.steps.map((r) => ({
|
|
796
|
-
id: r.step, task_id: r.task_id || null, title: r.title === undefined ? null : r.title, type: r.type || null,
|
|
972
|
+
id: r.step, task_id: r.task_id || null, title: r.title === undefined ? null : r.title, type: r.task_type || r.type || null,
|
|
797
973
|
tier: r.action === 'refused' ? null : (r.tier || null), depends_on: r.depends_on,
|
|
798
974
|
})),
|
|
799
975
|
history,
|
|
@@ -840,9 +1016,15 @@ function cmdPlanTable(opts) {
|
|
|
840
1016
|
throw new Error(`plan-table --approve: không ghi kế hoạch — ${problems.join('; ')}. Mỗi bước cần task_id riêng (8 ký tự a-z0-9, lấy bằng \`dieu-phoi new-id\`), khác --root.`);
|
|
841
1017
|
}
|
|
842
1018
|
if (problems.length) process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${problems.join('; ')} (cần sửa trước khi --approve)\n`);
|
|
1019
|
+
const typeNotes = planTypeNotes(plan);
|
|
1020
|
+
if (typeNotes.length) process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${typeNotes.join('; ')}\n`);
|
|
843
1021
|
const rec = opts.approve ? recordApprovedPlan(ctx, String(opts.root), path.resolve(opts.file), plan) : null;
|
|
1022
|
+
const ovs = rec ? planOverrideEvents(ctx, plan, base.events) : [];
|
|
1023
|
+
for (const ev of ovs) core.appendEvent(ctx, ev);
|
|
844
1024
|
if (opts.json) {
|
|
845
|
-
const out = rec ? Object.assign({}, plan, {
|
|
1025
|
+
const out = rec ? Object.assign({}, plan, {
|
|
1026
|
+
approved: { root_task_id: rec.root_task_id, approval: rec.approval, record: rec.file, overrides_logged: ovs.length },
|
|
1027
|
+
}) : plan;
|
|
846
1028
|
return { code: 0, out: JSON.stringify(out, null, 2) };
|
|
847
1029
|
}
|
|
848
1030
|
const lines = ['| Bước | Model/effort | Rubric (điểm · kiểm chứng · đảo ngược) | Lý do | Ước tính |', '|---|---|---|---|---|'];
|
|
@@ -862,11 +1044,12 @@ function cmdPlanTable(opts) {
|
|
|
862
1044
|
process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${plan.refused.length} bước bị từ chối
|
|
863
1045
|
`);
|
|
864
1046
|
}
|
|
865
|
-
if (
|
|
1047
|
+
if (typeNotes.length) lines.push('', `**Loại task:** ${typeNotes.join('; ')}`);
|
|
1048
|
+
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` : ''}`);
|
|
866
1049
|
return { code: 0, out: lines.join('\n') };
|
|
867
1050
|
}
|
|
868
1051
|
|
|
869
1052
|
module.exports = {
|
|
870
|
-
cmdSetup, cmdUninstall, cmdDoctor, cmdStart, checkSinkLive, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdStats, cmdRetro, cmdChoose, cmdCtx, cmdState, measureOrch,
|
|
1053
|
+
cmdSetup, cmdUninstall, cmdDoctor, cmdStart, checkSinkLive, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdDiscard, ctxWarnOnce, ctxWarnLevel, sweepStaleTmp, planOverrideEvents, cmdStats, cmdRetro, cmdChoose, cmdCtx, cmdState, measureOrch,
|
|
871
1054
|
cmdPlanTable, cmdNewId, requireTaskId, buildPlanTable, recordApprovedPlan, planTaskIdProblems, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
|
|
872
1055
|
};
|
package/lib/setup.cjs
CHANGED
|
@@ -245,6 +245,71 @@ function skillVersionOf(dir) {
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
// ------------------------------------------------------------------ dp-* agent files (core 0.5.0)
|
|
249
|
+
|
|
250
|
+
const AGENT_NAMES = ['dp-exec', 'dp-verify', 'dp-scout', 'dp-helper'];
|
|
251
|
+
const AGENT_MARKER = '# dieu-phoi-managed:';
|
|
252
|
+
const AGENT_HOOK_PLACEHOLDER = '__DP_HOOK__';
|
|
253
|
+
|
|
254
|
+
function agentsDirOf(claudeDir) {
|
|
255
|
+
return path.join(path.resolve(claudeDir), 'agents');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Agent file content for this install: template with the absolute hook command (YAML single-quoted). */
|
|
259
|
+
function renderAgent(name, hookPath, nodePath) {
|
|
260
|
+
const tpl = fs.readFileSync(path.join(PKG_ROOT, 'agents', `${name}.md`), 'utf8').replace(/\r\n/g, '\n');
|
|
261
|
+
const cmd = hookCommand(hookPath, nodePath).replace(/'/g, "''");
|
|
262
|
+
return tpl.split(AGENT_HOOK_PLACEHOLDER).join(cmd);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Node binary of a registered hook command (`"<node>" "<hook>" ...`), or undefined. */
|
|
266
|
+
function nodePathOf(cmd) {
|
|
267
|
+
const m = typeof cmd === 'string' ? /^\s*"([^"]+)"\s+"[^"]+"/.exec(cmd) : null;
|
|
268
|
+
return m ? m[1] : undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* State of each dp-* agent file: ok (ours, identical), outdated (ours, differs), missing, foreign (a file with
|
|
273
|
+
* that name we did not write — never touched).
|
|
274
|
+
*/
|
|
275
|
+
function agentStatus(claudeDir, hookPath, nodePath) {
|
|
276
|
+
const dir = agentsDirOf(claudeDir);
|
|
277
|
+
return AGENT_NAMES.map((name) => {
|
|
278
|
+
const file = path.join(dir, `${name}.md`);
|
|
279
|
+
let cur = null;
|
|
280
|
+
try { cur = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n'); } catch (_) { /* missing */ }
|
|
281
|
+
let state = 'missing';
|
|
282
|
+
if (cur !== null) {
|
|
283
|
+
if (!cur.includes(AGENT_MARKER)) state = 'foreign';
|
|
284
|
+
else state = cur === renderAgent(name, hookPath, nodePath) ? 'ok' : 'outdated';
|
|
285
|
+
}
|
|
286
|
+
return { name, file, state };
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Write missing/outdated dp-* agent files (LF). Foreign files are reported, never overwritten. */
|
|
291
|
+
function installAgents(claudeDir, hookPath, nodePath) {
|
|
292
|
+
const dir = agentsDirOf(claudeDir);
|
|
293
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
294
|
+
return agentStatus(claudeDir, hookPath, nodePath).map((a) => {
|
|
295
|
+
if (a.state === 'ok' || a.state === 'foreign') return Object.assign({ action: a.state === 'ok' ? 'unchanged' : 'skipped-foreign' }, a);
|
|
296
|
+
core.writeFileAtomic(a.file, renderAgent(a.name, hookPath, nodePath));
|
|
297
|
+
return Object.assign({}, a, { action: a.state === 'missing' ? 'installed' : 'updated', state: 'ok' });
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Remove only our dp-* agent files (marker present). */
|
|
302
|
+
function removeAgents(claudeDir) {
|
|
303
|
+
const removed = [];
|
|
304
|
+
for (const name of AGENT_NAMES) {
|
|
305
|
+
const file = path.join(agentsDirOf(claudeDir), `${name}.md`);
|
|
306
|
+
try {
|
|
307
|
+
if (fs.readFileSync(file, 'utf8').includes(AGENT_MARKER)) { fs.unlinkSync(file); removed.push(name); }
|
|
308
|
+
} catch (_) { /* missing */ }
|
|
309
|
+
}
|
|
310
|
+
return removed;
|
|
311
|
+
}
|
|
312
|
+
|
|
248
313
|
/**
|
|
249
314
|
* Install: copy hook + pricing, register hooks, write config.
|
|
250
315
|
* opts: { claudeDir, dept, sinkUrl, sinkToken, skillDir }
|
|
@@ -302,8 +367,11 @@ function setup(opts) {
|
|
|
302
367
|
if (legacyRemoved) dirty = true;
|
|
303
368
|
if (addOurPermissions(obj, perms)) dirty = true;
|
|
304
369
|
if (dirty) res = writeSettings(settingsFile, before, obj);
|
|
370
|
+
// 5. dp-* agent types (~/.claude/agents): only used when the orchestrator dispatches with subagent_type dp-*
|
|
371
|
+
const agents = installAgents(claudeDir, paths.hook, opts.nodePath);
|
|
305
372
|
|
|
306
373
|
return {
|
|
374
|
+
agents,
|
|
307
375
|
home,
|
|
308
376
|
hook: paths.hook,
|
|
309
377
|
command: cmd,
|
|
@@ -329,13 +397,14 @@ function uninstall(opts) {
|
|
|
329
397
|
+ stripOurPermissions(obj, permissionEntries(legacyRunsDirOf(claudeDir)));
|
|
330
398
|
let res = { changed: false, backup: null };
|
|
331
399
|
if (removed || permsRemoved) res = writeSettings(settingsFile, before, obj);
|
|
400
|
+
const agentsRemoved = removeAgents(claudeDir);
|
|
332
401
|
const hookFile = core.pathsFor(home).hook;
|
|
333
402
|
let hookRemoved = false;
|
|
334
403
|
if (opts.purge && fs.existsSync(hookFile)) {
|
|
335
404
|
fs.unlinkSync(hookFile);
|
|
336
405
|
hookRemoved = true;
|
|
337
406
|
}
|
|
338
|
-
return { removed, permissions_removed: permsRemoved, settings: settingsFile, backup: res.backup, hook_removed: hookRemoved, data_kept: core.pathsFor(home).data };
|
|
407
|
+
return { removed, agents_removed: agentsRemoved, permissions_removed: permsRemoved, settings: settingsFile, backup: res.backup, hook_removed: hookRemoved, data_kept: core.pathsFor(home).data };
|
|
339
408
|
}
|
|
340
409
|
|
|
341
410
|
/** Inspect settings for our registration. */
|
|
@@ -369,6 +438,6 @@ function registration(claudeDir) {
|
|
|
369
438
|
}
|
|
370
439
|
|
|
371
440
|
module.exports = {
|
|
372
|
-
MARKER, setup, uninstall, registration, stripOurHooks, addOurHooks, alreadyRegistered, hookCommand,
|
|
441
|
+
MARKER, AGENT_NAMES, AGENT_MARKER, renderAgent, agentStatus, nodePathOf, installAgents, removeAgents, agentsDirOf, setup, uninstall, registration, stripOurHooks, addOurHooks, alreadyRegistered, hookCommand,
|
|
373
442
|
skillVersionOf, findSkillDir, tsStamp, runsDir, legacyRunsDirOf, permissionEntries, addOurPermissions, stripOurPermissions, hasOurPermissions,
|
|
374
443
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kienntpixon/dieu-phoi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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>",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"main": "hook.cjs",
|
|
21
21
|
"files": [
|
|
22
22
|
"bin/",
|
|
23
|
+
"agents/",
|
|
23
24
|
"lib/",
|
|
24
25
|
"hook.cjs",
|
|
25
26
|
"pricing.json",
|