@kienntpixon/dieu-phoi 0.2.2 → 0.4.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 +29 -0
- package/README.md +10 -5
- package/bin/dieu-phoi.cjs +11 -3
- package/hook.cjs +258 -11
- package/lib/aggregate.cjs +72 -13
- package/lib/choose.cjs +74 -18
- package/lib/commands.cjs +258 -34
- package/lib/setup.cjs +8 -4
- package/package.json +1 -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'];
|
|
@@ -63,7 +63,7 @@ function cmdSetup(opts) {
|
|
|
63
63
|
const lines = [
|
|
64
64
|
`Đã cài hook: ${res.hook}`,
|
|
65
65
|
res.settings_changed
|
|
66
|
-
? `Đã đăng ký PostToolUse(Agent) + SubagentStop + SessionStart(compact) trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
|
|
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
68
|
`Thư mục báo cáo subagent: ${res.runs_dir} (quyền: ${res.permissions.allow.join(', ')} + additionalDirectories)`,
|
|
69
69
|
`Phòng ban: ${res.config.dept || '(chưa đặt, dùng --dept)'}`,
|
|
@@ -98,6 +98,9 @@ async function cmdDoctor(opts) {
|
|
|
98
98
|
const reg = setupLib.registration(claudeDir);
|
|
99
99
|
add(reg.PostToolUse && reg.SubagentStop && reg.SessionStart, 'settings',
|
|
100
100
|
`PostToolUse(Agent)=${reg.PostToolUse ? 'có' : 'thiếu'}, SubagentStop=${reg.SubagentStop ? 'có' : 'thiếu'}, SessionStart(compact)=${reg.SessionStart ? 'có' : 'thiếu'} (${reg.file})`);
|
|
101
|
+
add(reg.UserPromptSubmit, 'prompt-hook', reg.UserPromptSubmit
|
|
102
|
+
? 'UserPromptSubmit có — prompt "điều phối" được tạo sẵn task_id + run dir, lượt đầu là scout'
|
|
103
|
+
: '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');
|
|
101
104
|
const permOk = reg.permissions && reg.runs_dir_exists && !reg.legacy_permissions;
|
|
102
105
|
add(permOk, 'run-permissions', permOk
|
|
103
106
|
? `${reg.permission_entries.allow.join(', ')} + additionalDirectories ${reg.permission_entries.dir}`
|
|
@@ -142,22 +145,26 @@ async function cmdDoctor(opts) {
|
|
|
142
145
|
add(!badIds.length, 'task-ids', badIds.length
|
|
143
146
|
? `${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\``
|
|
144
147
|
: `mọi dispatch trong ${INVALID_ID_WINDOW_DAYS} ngày gần đây có task_id đúng định dạng`, 'CẢNH BÁO');
|
|
148
|
+
const tmp = sweepStaleTmp(ctx);
|
|
149
|
+
add(!tmp.left.length, 'tmp-files', tmp.left.length
|
|
150
|
+
? `${tmp.left.length} file tạm còn mới (đang ghi?): ${tmp.left.slice(0, 3).join(', ')}`
|
|
151
|
+
: `không còn file *.tmp-* bỏ dở${tmp.removed ? ` (đã xoá ${tmp.removed} file cũ hơn 1 giờ)` : ''}`, 'CẢNH BÁO');
|
|
152
|
+
let badPlans = [];
|
|
153
|
+
try { badPlans = fs.readdirSync(ctx.paths.plans).filter((n) => n.endsWith('.json') && !core.isValidTaskId(n.slice(0, -5))); } catch (_) { /* none */ }
|
|
154
|
+
add(!badPlans.length, 'plans', badPlans.length
|
|
155
|
+
? `${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}`
|
|
156
|
+
: 'mọi kế hoạch đã duyệt có root id đúng định dạng', 'CẢNH BÁO');
|
|
145
157
|
const orch = measureOrch(opts, ctx, profileOrNull(opts, ctx));
|
|
146
158
|
if (orch.ctx_tokens !== null) {
|
|
147
159
|
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');
|
|
148
160
|
}
|
|
149
161
|
if (ctx.config.sink_url && ctx.config.sink_token) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
let ok = res.ok;
|
|
157
|
-
try { const b = JSON.parse(txt); if (b && b.ok === false) ok = false; } catch (_) { ok = false; }
|
|
158
|
-
add(ok, 'sink', `${ctx.config.sink_url} → HTTP ${res.status}`);
|
|
159
|
-
} catch (e) {
|
|
160
|
-
add(false, 'sink', `${ctx.config.sink_url} → ${e.message}`);
|
|
162
|
+
const cached = opts.net ? null : core.sinkStatusCached(ctx);
|
|
163
|
+
if (cached && (cached.state === 'ok' || cached.state === 'fail')) {
|
|
164
|
+
add(cached.state === 'ok', 'sink', `${ctx.config.sink_url} → ${cached.state === 'ok' ? 'ok' : `lỗi: ${cached.detail}`} (bộ nhớ đệm: ${cached.source} lúc ${cached.ts}; \`doctor --net\` để kiểm trực tiếp)`);
|
|
165
|
+
} else {
|
|
166
|
+
const live = await checkSinkLive(ctx);
|
|
167
|
+
add(live.ok, 'sink', `${ctx.config.sink_url} → ${live.detail}`);
|
|
161
168
|
}
|
|
162
169
|
} else {
|
|
163
170
|
add(true, 'sink', 'chưa cấu hình (chỉ lưu cục bộ)');
|
|
@@ -168,6 +175,83 @@ async function cmdDoctor(opts) {
|
|
|
168
175
|
return { code: failed ? 1 : 0, out };
|
|
169
176
|
}
|
|
170
177
|
|
|
178
|
+
const TMP_RE = /\.tmp-\d+-[0-9a-f]+$/;
|
|
179
|
+
const TMP_STALE_MS = 3600 * 1000;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Leftovers of writeFileAtomic (tmp written, rename never happened: process killed, or rename lost a race on
|
|
183
|
+
* Windows) under data/, data/orch, data/segments, data/plans. Older than 1 h → removed; newer → reported.
|
|
184
|
+
*/
|
|
185
|
+
function sweepStaleTmp(ctx) {
|
|
186
|
+
let removed = 0;
|
|
187
|
+
const left = [];
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
for (const dir of [ctx.paths.data, path.join(ctx.paths.data, 'orch'), ctx.paths.segments, ctx.paths.plans]) {
|
|
190
|
+
let names = [];
|
|
191
|
+
try { names = fs.readdirSync(dir); } catch (_) { continue; }
|
|
192
|
+
for (const n of names) {
|
|
193
|
+
if (!TMP_RE.test(n)) continue;
|
|
194
|
+
const f = path.join(dir, n);
|
|
195
|
+
try {
|
|
196
|
+
const st = fs.statSync(f);
|
|
197
|
+
if (!st.isFile()) continue; // a directory (or anything else) with a tmp-like name: not ours, leave it alone
|
|
198
|
+
if (now - st.mtimeMs > TMP_STALE_MS) { fs.unlinkSync(f); removed++; } else left.push(n);
|
|
199
|
+
} catch (_) { left.push(n); }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { removed, left };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Live sink check (view=stats); result cached in data/sink-check.json for 24 h. */
|
|
206
|
+
async function checkSinkLive(ctx) {
|
|
207
|
+
let ok = false;
|
|
208
|
+
let detail;
|
|
209
|
+
try {
|
|
210
|
+
const u = new URL(ctx.config.sink_url);
|
|
211
|
+
u.searchParams.set('token', ctx.config.sink_token);
|
|
212
|
+
u.searchParams.set('view', 'stats');
|
|
213
|
+
const res = await fetch(u, { redirect: 'follow', signal: AbortSignal.timeout(8000) });
|
|
214
|
+
const txt = await res.text();
|
|
215
|
+
ok = res.ok;
|
|
216
|
+
try { const b = JSON.parse(txt); if (b && b.ok === false) ok = false; } catch (_) { ok = false; }
|
|
217
|
+
detail = `HTTP ${res.status}`;
|
|
218
|
+
} catch (e) {
|
|
219
|
+
detail = e.message;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
core.writeFileAtomic(ctx.paths.sinkCheck, `${JSON.stringify({ ts: core.nowIso(), ok, detail, url: core.sinkUrlKey(ctx.config) })}\n`);
|
|
223
|
+
} catch (_) { /* cache is best effort */ }
|
|
224
|
+
return { ok, detail };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ------------------------------------------------------------------ start
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* `dieu-phoi start [--json]`: one fast, network-free call for gate 1 when the UserPromptSubmit hook
|
|
231
|
+
* did not run: local health (sink from cache), fresh root task id, run dir created, scout brief path.
|
|
232
|
+
*/
|
|
233
|
+
function cmdStart(opts) {
|
|
234
|
+
const ctx = ctxOf(opts);
|
|
235
|
+
const reg = setupLib.registration(claudeDirOf(opts));
|
|
236
|
+
const info = core.startInfo(ctx);
|
|
237
|
+
const h = info.health;
|
|
238
|
+
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`);
|
|
239
|
+
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');
|
|
240
|
+
if (!reg.UserPromptSubmit) h.problems.push('thiếu hook UserPromptSubmit — chạy lại `dieu-phoi setup` để bỏ được lệnh này');
|
|
241
|
+
if (!reg.permissions || reg.legacy_permissions) h.problems.push(`thiếu quyền ghi cho ${reg.permission_entries.dir} — chạy lại \`dieu-phoi setup\` (agent nền có thể không ghi được report)`);
|
|
242
|
+
h.ok = !h.problems.length;
|
|
243
|
+
if (opts.json) return { code: 0, out: JSON.stringify(info, null, 2) };
|
|
244
|
+
const out = [
|
|
245
|
+
`root_task_id: ${info.id}`,
|
|
246
|
+
`run_dir: ${info.run_dir}/ (đã tạo)`,
|
|
247
|
+
`step_ids: ${info.step_ids.join(' ')}`,
|
|
248
|
+
`health: ${core.healthLine(h)}`,
|
|
249
|
+
`scout_brief: ${info.scout_brief || '(không thấy — dùng references/brief-template.md mục E)'}`,
|
|
250
|
+
...core.orchLessonLines(info),
|
|
251
|
+
];
|
|
252
|
+
return { code: 0, out: out.join('\n') };
|
|
253
|
+
}
|
|
254
|
+
|
|
171
255
|
// ------------------------------------------------------------------ hook / sync
|
|
172
256
|
|
|
173
257
|
function cmdHook(opts) {
|
|
@@ -201,7 +285,10 @@ function resolveAgentId(events, ref, now) {
|
|
|
201
285
|
const verdicted = new Set();
|
|
202
286
|
events.forEach((e, i) => {
|
|
203
287
|
if (!e.agent_id) return;
|
|
204
|
-
if (e.kind === 'verdict'
|
|
288
|
+
if (e.kind === 'verdict') {
|
|
289
|
+
const nv = agg.normalizeVerdict(e);
|
|
290
|
+
if (nv.by === 'agent' && nv.outcome !== agg.ABORTED) verdicted.add(e.agent_id); // aborted still needs a verdict
|
|
291
|
+
}
|
|
205
292
|
if (e.kind !== 'complete') return;
|
|
206
293
|
const t = Date.parse(e.ts) || 0;
|
|
207
294
|
const cur = latest.get(e.agent_id);
|
|
@@ -238,8 +325,9 @@ function kebab(s) {
|
|
|
238
325
|
function verdictAgent(opts, ctx, events) {
|
|
239
326
|
const [ref, outcome] = opts._;
|
|
240
327
|
if (!ref || !outcome) {
|
|
241
|
-
throw new Error('Cú pháp: dieu-phoi verdict <agent_id|last> <pass|fail|timeout> [--verify V] [--reason R]\n'
|
|
242
|
-
+ ' dieu-phoi verdict --task <task_id> <accept|reject> [--cause quality|scope] [--round n]'
|
|
328
|
+
throw new Error('Cú pháp: dieu-phoi verdict <agent_id|last> <pass|fail|timeout|aborted> [--verify V] [--reason R]\n'
|
|
329
|
+
+ ' dieu-phoi verdict --task <task_id> <accept|reject> [--cause quality|scope] [--round n]\n'
|
|
330
|
+
+ ' dieu-phoi verdict --root <root_task_id> accept [--steps a,b]');
|
|
243
331
|
}
|
|
244
332
|
if (outcome === 'rework') {
|
|
245
333
|
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');
|
|
@@ -297,9 +385,46 @@ function verdictUser(opts, ctx, events) {
|
|
|
297
385
|
}, core.commonFields(ctx));
|
|
298
386
|
}
|
|
299
387
|
|
|
388
|
+
/**
|
|
389
|
+
* `verdict --root <root> accept [--steps a,b]` (core 0.4.0): the user accepts the steps of the latest approved plan
|
|
390
|
+
* in one call. Covers every step whose task has an exec dispatch and no user verdict yet (a step already rejected
|
|
391
|
+
* or accepted is left alone: reject the bad ones per task first, then accept the rest by root).
|
|
392
|
+
*/
|
|
393
|
+
function verdictRoot(opts, ctx, events) {
|
|
394
|
+
const root = requireTaskId(String(opts.root));
|
|
395
|
+
if (opts._[0] !== 'accept') {
|
|
396
|
+
throw new Error('verdict --root chỉ nhận accept; bước không đạt → dieu-phoi verdict --task <task_id> reject --cause quality|scope');
|
|
397
|
+
}
|
|
398
|
+
const plan = core.readJsonFile(path.join(ctx.paths.plans, `${root}.json`), null);
|
|
399
|
+
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}).`);
|
|
400
|
+
let steps = plan.steps.filter((x) => x && x.task_id);
|
|
401
|
+
if (typeof opts.steps === 'string') {
|
|
402
|
+
const want = new Set(opts.steps.split(',').map((x) => x.trim()).filter(Boolean));
|
|
403
|
+
const ids = new Set(steps.map((x) => String(x.id)));
|
|
404
|
+
const missing = Array.from(want).filter((x) => !ids.has(x));
|
|
405
|
+
if (missing.length) throw new Error(`--steps: không có bước ${missing.join(', ')} trong kế hoạch ${root}`);
|
|
406
|
+
steps = steps.filter((x) => want.has(String(x.id)));
|
|
407
|
+
}
|
|
408
|
+
const execTasks = new Set(events.filter((e) => e.kind === 'dispatch' && (e.role || 'exec') === 'exec').map((e) => e.task_id));
|
|
409
|
+
const judged = new Set(events.filter((e) => e.kind === 'verdict' && agg.normalizeVerdict(e).by === 'user').map((e) => e.task_id));
|
|
410
|
+
const gone = agg.discardedTaskIds(events);
|
|
411
|
+
const todo = steps.filter((x) => execTasks.has(x.task_id) && !judged.has(x.task_id) && !gone.has(x.task_id));
|
|
412
|
+
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).`);
|
|
413
|
+
return todo.map((x) => ({
|
|
414
|
+
step: String(x.id),
|
|
415
|
+
ev: verdictUser(Object.assign({}, opts, { task: x.task_id, _: ['accept'], round: undefined, cause: undefined }), ctx, events),
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
|
|
300
419
|
function cmdVerdict(opts) {
|
|
301
420
|
const ctx = ctxOf(opts);
|
|
302
421
|
const events = core.readEvents(ctx);
|
|
422
|
+
if (typeof opts.root === 'string' && typeof opts.task !== 'string') {
|
|
423
|
+
const list = verdictRoot(opts, ctx, events);
|
|
424
|
+
for (const x of list) core.appendEvent(ctx, x.ev);
|
|
425
|
+
if (opts.json) return { code: 0, out: JSON.stringify(list.map((x) => x.ev)) };
|
|
426
|
+
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(', ')}.` };
|
|
427
|
+
}
|
|
303
428
|
const ev = typeof opts.task === 'string' ? verdictUser(opts, ctx, events) : verdictAgent(opts, ctx, events);
|
|
304
429
|
core.appendEvent(ctx, ev);
|
|
305
430
|
if (opts.json) return { code: 0, out: JSON.stringify(ev) };
|
|
@@ -309,6 +434,29 @@ function cmdVerdict(opts) {
|
|
|
309
434
|
return { code: 0, out: `Đã ghi ${ev.outcome} cho ${ev.agent_id}${ev.task_id ? ` (task ${ev.task_id})` : ''}.` };
|
|
310
435
|
}
|
|
311
436
|
|
|
437
|
+
// ------------------------------------------------------------------ discard
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* `dieu-phoi discard --task <task_id> [--reason R]` (core 0.4.0): withdraw a mistaken task. Appends one
|
|
441
|
+
* `discard` event (synced like any other); stats, choose, state and the sink then leave out every event of the
|
|
442
|
+
* task and of its agents. events.jsonl is never edited — a hand edit once lost a dispatch that the sink kept.
|
|
443
|
+
*/
|
|
444
|
+
function cmdDiscard(opts) {
|
|
445
|
+
if (typeof opts.task !== 'string') throw new Error('Cú pháp: dieu-phoi discard --task <task_id> [--reason R]');
|
|
446
|
+
const taskId = requireTaskId(opts.task);
|
|
447
|
+
const ctx = ctxOf(opts);
|
|
448
|
+
const events = core.readEvents(ctx);
|
|
449
|
+
if (!events.some((e) => e.task_id === taskId)) throw new Error(`Không thấy task "${taskId}" trong log.`);
|
|
450
|
+
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 đó.` };
|
|
451
|
+
const ev = Object.assign({
|
|
452
|
+
schema: core.SCHEMA, kind: 'discard', ts: core.nowIso(), agent_id: `task:${taskId}`, task_id: taskId, reason_code: kebab(opts.reason),
|
|
453
|
+
}, core.commonFields(ctx));
|
|
454
|
+
core.appendEvent(ctx, ev);
|
|
455
|
+
const n = events.filter((e) => e.task_id === taskId).length;
|
|
456
|
+
if (opts.json) return { code: 0, out: JSON.stringify(ev) };
|
|
457
|
+
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.` };
|
|
458
|
+
}
|
|
459
|
+
|
|
312
460
|
// ------------------------------------------------------------------ steer
|
|
313
461
|
|
|
314
462
|
/**
|
|
@@ -336,7 +484,8 @@ function cmdSteer(opts) {
|
|
|
336
484
|
const taskId = String(opts.task);
|
|
337
485
|
const mine = agents.filter((a) => a.task_id === taskId);
|
|
338
486
|
if (!mine.length && !events.some((e) => e.task_id === taskId)) throw new Error(`Không thấy task ${taskId} trong log`);
|
|
339
|
-
|
|
487
|
+
// core 0.4.0: verifiers are never marked (a steer is about the work, not its check)
|
|
488
|
+
const inFlight = mine.filter((a) => a.dispatched && a.role !== 'verify' && !(a.completed && a.outcome));
|
|
340
489
|
const type = (mine.find((a) => a.role === 'exec') || mine[0] || {}).task_type || null;
|
|
341
490
|
out = inFlight.length ? inFlight.map((a) => mk(a.agent_id, taskId, a.task_type)) : [mk(`task:${taskId}`, taskId, type)];
|
|
342
491
|
} else {
|
|
@@ -380,6 +529,40 @@ function measureOrch(opts, ctx, prof) {
|
|
|
380
529
|
};
|
|
381
530
|
}
|
|
382
531
|
|
|
532
|
+
const CTX_WARN_KEEP_MS = 14 * 24 * 3600 * 1000;
|
|
533
|
+
|
|
534
|
+
/** 0 = within the cap; 1 = over the cap, 2 = over 2×, 3 = over 4×, … */
|
|
535
|
+
function ctxWarnLevel(growth, cap) {
|
|
536
|
+
if (!(typeof growth === 'number' && cap > 0 && growth > cap)) return 0;
|
|
537
|
+
return 1 + Math.floor(Math.log2(growth / cap));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Core 0.4.0: the over-cap advice fires once per threshold crossing (cap, 2×, 4×, …) per session, and again after
|
|
542
|
+
* a /compact that left the session over. State: data/ctx-warn.json {session_id: {level, compactions, t}}.
|
|
543
|
+
* Never throws (no state → warn).
|
|
544
|
+
*/
|
|
545
|
+
function ctxWarnOnce(ctx, m) {
|
|
546
|
+
if (!m || !m.over || !m.session_id) return false;
|
|
547
|
+
const file = path.join(ctx.paths.data, 'ctx-warn.json');
|
|
548
|
+
const level = ctxWarnLevel(m.ctx_growth, m.soft_cap);
|
|
549
|
+
try {
|
|
550
|
+
const st = core.readJsonFile(file, {}) || {};
|
|
551
|
+
const cur = st[m.session_id];
|
|
552
|
+
const comp = typeof m.compactions === 'number' ? m.compactions : null;
|
|
553
|
+
const warn = !cur || cur.compactions !== comp || level > (Number(cur.level) || 0);
|
|
554
|
+
if (warn) {
|
|
555
|
+
const now = Date.now();
|
|
556
|
+
st[m.session_id] = { level, compactions: comp, t: now };
|
|
557
|
+
for (const k of Object.keys(st)) if (!(now - Number(st[k] && st[k].t) < CTX_WARN_KEEP_MS)) delete st[k];
|
|
558
|
+
core.writeFileAtomic(file, `${JSON.stringify(st)}\n`);
|
|
559
|
+
}
|
|
560
|
+
return warn;
|
|
561
|
+
} catch (_) {
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
383
566
|
function profileOrNull(opts, ctx) {
|
|
384
567
|
try { return chooser.loadProfile(skillDirOf(opts, ctx)); } catch (_) { return null; }
|
|
385
568
|
}
|
|
@@ -461,9 +644,9 @@ function cmdStats(opts) {
|
|
|
461
644
|
c.note,
|
|
462
645
|
]);
|
|
463
646
|
}
|
|
464
|
-
const s = [['task_type', 'task', 'task có steer', 'lần steer', 'minor', 'plan', 'redirect', 'tỉ lệ steer']];
|
|
647
|
+
const s = [['task_type', 'task', 'task có steer', 'lần steer', 'minor', 'plan', 'redirect', 'tỉ lệ steer', 'ngoài exec (không tính)']];
|
|
465
648
|
for (const r of steerRows) {
|
|
466
|
-
s.push([r.task_type, r.tasks, r.steered_tasks, r.actions, r.minor, r.plan, r.redirect, pct(r.steer_rate)]);
|
|
649
|
+
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]);
|
|
467
650
|
}
|
|
468
651
|
return {
|
|
469
652
|
code: 0,
|
|
@@ -487,6 +670,7 @@ function cmdRetro(opts) {
|
|
|
487
670
|
const userOutcomes = {};
|
|
488
671
|
for (const a of agents) {
|
|
489
672
|
if (a.outcome) outcomes[a.outcome] = (outcomes[a.outcome] || 0) + 1;
|
|
673
|
+
if (a.aborted) outcomes.aborted = (outcomes.aborted || 0) + 1;
|
|
490
674
|
if (a.reason_code) reasons[a.reason_code] = (reasons[a.reason_code] || 0) + 1;
|
|
491
675
|
}
|
|
492
676
|
for (const v of userVerdicts) {
|
|
@@ -518,6 +702,7 @@ function cmdRetro(opts) {
|
|
|
518
702
|
no_verdict_agents: agents.filter((a) => a.completed && !a.outcome && a.role !== 'verify').length,
|
|
519
703
|
no_user_verdict_tasks: tasks.filter((t) => !t.user_verdicts.length).length,
|
|
520
704
|
multi_attempt_tasks: escalations,
|
|
705
|
+
discarded_tasks: agg.discardedTaskIds(events).size,
|
|
521
706
|
overrides: overrides.length,
|
|
522
707
|
cost_usd: agents.reduce((s, a) => s + (a.cost_usd || 0), 0),
|
|
523
708
|
},
|
|
@@ -567,6 +752,19 @@ function chooseInput(opts, ctx, events) {
|
|
|
567
752
|
};
|
|
568
753
|
}
|
|
569
754
|
|
|
755
|
+
function overrideEvent(ctx, taskId, taskType, suggested, chosen) {
|
|
756
|
+
return Object.assign({
|
|
757
|
+
schema: core.SCHEMA,
|
|
758
|
+
kind: 'override',
|
|
759
|
+
ts: core.nowIso(),
|
|
760
|
+
agent_id: taskId ? `task:${taskId}` : `override-${crypto.randomBytes(6).toString('hex')}`,
|
|
761
|
+
task_id: taskId || null,
|
|
762
|
+
task_type: taskType,
|
|
763
|
+
suggested_tier: suggested,
|
|
764
|
+
chosen_tier: chosen,
|
|
765
|
+
}, core.commonFields(ctx));
|
|
766
|
+
}
|
|
767
|
+
|
|
570
768
|
function cmdChoose(opts) {
|
|
571
769
|
const ctx = ctxOf(opts);
|
|
572
770
|
if (opts.override !== undefined && typeof opts.task !== 'string') {
|
|
@@ -579,21 +777,14 @@ function cmdChoose(opts) {
|
|
|
579
777
|
res.orch_ctx_tokens = orch.ctx_tokens;
|
|
580
778
|
res.orch_ctx_growth = orch.ctx_growth;
|
|
581
779
|
res.orch_ctx_over = orch.over;
|
|
780
|
+
res.orch_ctx_warn = ctxWarnOnce(ctx, orch); // over AND a new threshold crossed since the last warning
|
|
582
781
|
if (res.overridden) {
|
|
583
782
|
const tid = input.taskId;
|
|
584
|
-
core.appendEvent(ctx,
|
|
585
|
-
schema: core.SCHEMA,
|
|
586
|
-
kind: 'override',
|
|
587
|
-
ts: core.nowIso(),
|
|
588
|
-
agent_id: tid ? `task:${tid}` : `override-${require('crypto').randomBytes(6).toString('hex')}`,
|
|
589
|
-
task_id: tid,
|
|
590
|
-
task_type: String(input.type).toLowerCase(),
|
|
591
|
-
suggested_tier: res.suggested_tier,
|
|
592
|
-
chosen_tier: res.tier,
|
|
593
|
-
}, core.commonFields(ctx)));
|
|
783
|
+
core.appendEvent(ctx, overrideEvent(ctx, tid, res.task_type || String(input.type).toLowerCase(), res.suggested_tier, res.tier));
|
|
594
784
|
}
|
|
595
785
|
if (opts.json) return { code: 0, out: JSON.stringify(res) };
|
|
596
|
-
const
|
|
786
|
+
const typeWarn = res.type_warning && !String(res.reason).includes(res.type_warning) ? `\nCẢNH BÁO: ${res.type_warning}` : '';
|
|
787
|
+
const warn = `${typeWarn}${res.orch_ctx_warn ? `\nCẢNH BÁO: ${orch.advice}` : ''}`;
|
|
597
788
|
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ô'})`;
|
|
598
789
|
if (res.action === 'ask_user') {
|
|
599
790
|
return { code: 0, out: `HỎI NGƯỜI DÙNG — ${res.reason}${warn}` };
|
|
@@ -728,9 +919,35 @@ function planTaskIdProblems(plan, root) {
|
|
|
728
919
|
const same = plan.steps.filter((r) => r.task_id === root).map((r) => r.step);
|
|
729
920
|
if (same.length) problems.push(`task_id của bước ${same.join(', ')} trùng --root "${root}" (root dành cho scout/planner)`);
|
|
730
921
|
}
|
|
922
|
+
// core 0.4.0: a label outside the fixed list (and without an alias) would never pool its stats
|
|
923
|
+
for (const r of plan.steps) if (r.unknown_type) problems.push(`bước ${r.step}: ${r.type_warning}`);
|
|
731
924
|
return problems;
|
|
732
925
|
}
|
|
733
926
|
|
|
927
|
+
/** Mapped ad-hoc labels (bug-fix → debug, …): not an error, but the tag must carry the mapped type. */
|
|
928
|
+
function planTypeNotes(plan) {
|
|
929
|
+
return plan.steps.filter((r) => r.mapped_from).map((r) => `bước ${r.step}: ${r.type_warning}`);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Overrides chosen in the steps file (step `override`) become override events when the plan is approved —
|
|
934
|
+
* also on a re-approval that changes a step's tier — so the override learning sees them (core 0.4.0; before,
|
|
935
|
+
* only `choose --override` logged one). One event per (task_id, chosen tier): re-approving the same tier or a
|
|
936
|
+
* `choose --override` already logged adds nothing.
|
|
937
|
+
*/
|
|
938
|
+
function planOverrideEvents(ctx, plan, events) {
|
|
939
|
+
const seen = new Set(events.filter((e) => e.kind === 'override').map((e) => `${e.task_id}|${e.chosen_tier}`));
|
|
940
|
+
const out = [];
|
|
941
|
+
for (const r of plan.steps) {
|
|
942
|
+
if (!r.overridden || !r.task_id || !r.tier || !r.suggested_tier) continue;
|
|
943
|
+
const k = `${r.task_id}|${r.tier}`;
|
|
944
|
+
if (seen.has(k)) continue;
|
|
945
|
+
seen.add(k);
|
|
946
|
+
out.push(overrideEvent(ctx, r.task_id, r.task_type || r.type, r.suggested_tier, r.tier));
|
|
947
|
+
}
|
|
948
|
+
return out;
|
|
949
|
+
}
|
|
950
|
+
|
|
734
951
|
function recordApprovedPlan(ctx, root, stepsFile, plan) {
|
|
735
952
|
const file = path.join(ctx.paths.plans, `${root}.json`);
|
|
736
953
|
const prev = core.readJsonFile(file, null);
|
|
@@ -746,7 +963,7 @@ function recordApprovedPlan(ctx, root, stepsFile, plan) {
|
|
|
746
963
|
approved_at: core.nowIso(),
|
|
747
964
|
approval: prevApproval(prev) + 1,
|
|
748
965
|
steps: plan.steps.map((r) => ({
|
|
749
|
-
id: r.step, task_id: r.task_id || null, title: r.title === undefined ? null : r.title, type: r.type || null,
|
|
966
|
+
id: r.step, task_id: r.task_id || null, title: r.title === undefined ? null : r.title, type: r.task_type || r.type || null,
|
|
750
967
|
tier: r.action === 'refused' ? null : (r.tier || null), depends_on: r.depends_on,
|
|
751
968
|
})),
|
|
752
969
|
history,
|
|
@@ -793,9 +1010,15 @@ function cmdPlanTable(opts) {
|
|
|
793
1010
|
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.`);
|
|
794
1011
|
}
|
|
795
1012
|
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`);
|
|
1013
|
+
const typeNotes = planTypeNotes(plan);
|
|
1014
|
+
if (typeNotes.length) process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${typeNotes.join('; ')}\n`);
|
|
796
1015
|
const rec = opts.approve ? recordApprovedPlan(ctx, String(opts.root), path.resolve(opts.file), plan) : null;
|
|
1016
|
+
const ovs = rec ? planOverrideEvents(ctx, plan, base.events) : [];
|
|
1017
|
+
for (const ev of ovs) core.appendEvent(ctx, ev);
|
|
797
1018
|
if (opts.json) {
|
|
798
|
-
const out = rec ? Object.assign({}, plan, {
|
|
1019
|
+
const out = rec ? Object.assign({}, plan, {
|
|
1020
|
+
approved: { root_task_id: rec.root_task_id, approval: rec.approval, record: rec.file, overrides_logged: ovs.length },
|
|
1021
|
+
}) : plan;
|
|
799
1022
|
return { code: 0, out: JSON.stringify(out, null, 2) };
|
|
800
1023
|
}
|
|
801
1024
|
const lines = ['| Bước | Model/effort | Rubric (điểm · kiểm chứng · đảo ngược) | Lý do | Ước tính |', '|---|---|---|---|---|'];
|
|
@@ -815,11 +1038,12 @@ function cmdPlanTable(opts) {
|
|
|
815
1038
|
process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${plan.refused.length} bước bị từ chối
|
|
816
1039
|
`);
|
|
817
1040
|
}
|
|
818
|
-
if (
|
|
1041
|
+
if (typeNotes.length) lines.push('', `**Loại task:** ${typeNotes.join('; ')}`);
|
|
1042
|
+
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` : ''}`);
|
|
819
1043
|
return { code: 0, out: lines.join('\n') };
|
|
820
1044
|
}
|
|
821
1045
|
|
|
822
1046
|
module.exports = {
|
|
823
|
-
cmdSetup, cmdUninstall, cmdDoctor, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdStats, cmdRetro, cmdChoose, cmdCtx, cmdState, measureOrch,
|
|
1047
|
+
cmdSetup, cmdUninstall, cmdDoctor, cmdStart, checkSinkLive, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdDiscard, ctxWarnOnce, ctxWarnLevel, sweepStaleTmp, planOverrideEvents, cmdStats, cmdRetro, cmdChoose, cmdCtx, cmdState, measureOrch,
|
|
824
1048
|
cmdPlanTable, cmdNewId, requireTaskId, buildPlanTable, recordApprovedPlan, planTaskIdProblems, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
|
|
825
1049
|
};
|
package/lib/setup.cjs
CHANGED
|
@@ -57,7 +57,7 @@ function stripOurHooks(settings) {
|
|
|
57
57
|
return removed;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/** True when settings already hold exactly our
|
|
60
|
+
/** True when settings already hold exactly our four entries with this command. */
|
|
61
61
|
function alreadyRegistered(settings, cmd) {
|
|
62
62
|
const hooks = settings && settings.hooks;
|
|
63
63
|
if (!hooks || typeof hooks !== 'object') return false;
|
|
@@ -72,10 +72,11 @@ function alreadyRegistered(settings, cmd) {
|
|
|
72
72
|
if (event === 'PostToolUse' && g.matcher === 'Agent') good++;
|
|
73
73
|
else if (event === 'SubagentStop') good++;
|
|
74
74
|
else if (event === 'SessionStart' && g.matcher === 'compact') good++;
|
|
75
|
+
else if (event === 'UserPromptSubmit' && !g.matcher) good++;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
|
-
return ours ===
|
|
79
|
+
return ours === 4 && good === 4;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
function addOurHooks(settings, hookPath) {
|
|
@@ -89,6 +90,9 @@ function addOurHooks(settings, hookPath) {
|
|
|
89
90
|
// after /compact or auto-compact: re-inject the orchestration state rebuilt from the log
|
|
90
91
|
if (!Array.isArray(h.SessionStart)) h.SessionStart = [];
|
|
91
92
|
h.SessionStart.push({ matcher: 'compact', hooks: [{ type: 'command', command: cmd, timeout: 10 }] });
|
|
93
|
+
// on a dieu-phoi prompt: pre-create the run dir + id so the first tool call can be the scout (no shell)
|
|
94
|
+
if (!Array.isArray(h.UserPromptSubmit)) h.UserPromptSubmit = [];
|
|
95
|
+
h.UserPromptSubmit.push({ hooks: [{ type: 'command', command: cmd, timeout: 5 }] });
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
// ------------------------------------------------------------------ run-dir permissions
|
|
@@ -340,7 +344,7 @@ function registration(claudeDir) {
|
|
|
340
344
|
const perms = permissionEntries(runsDir());
|
|
341
345
|
const legacy = permissionEntries(legacyRunsDirOf(claudeDir));
|
|
342
346
|
const out = {
|
|
343
|
-
file, PostToolUse: false, SubagentStop: false, SessionStart: false, commands: [], permissions: false, permission_entries: perms,
|
|
347
|
+
file, PostToolUse: false, SubagentStop: false, SessionStart: false, UserPromptSubmit: false, commands: [], permissions: false, permission_entries: perms,
|
|
344
348
|
runs_dir: perms.dir, runs_dir_exists: fs.existsSync(perms.dir), legacy_permissions: false,
|
|
345
349
|
};
|
|
346
350
|
let obj;
|
|
@@ -350,7 +354,7 @@ function registration(claudeDir) {
|
|
|
350
354
|
out.legacy_permissions = legacy.allow.some((r) => Array.isArray(lp.allow) && lp.allow.includes(r))
|
|
351
355
|
|| (Array.isArray(lp.additionalDirectories) && lp.additionalDirectories.includes(legacy.dir));
|
|
352
356
|
const hooks = obj.hooks || {};
|
|
353
|
-
for (const ev of ['PostToolUse', 'SubagentStop', 'SessionStart']) {
|
|
357
|
+
for (const ev of ['PostToolUse', 'SubagentStop', 'SessionStart', 'UserPromptSubmit']) {
|
|
354
358
|
for (const g of Array.isArray(hooks[ev]) ? hooks[ev] : []) {
|
|
355
359
|
for (const h of (g && Array.isArray(g.hooks) ? g.hooks : [])) {
|
|
356
360
|
if (!isOurs(h)) continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kienntpixon/dieu-phoi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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>",
|