@kienntpixon/dieu-phoi 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +15 -9
- package/bin/dieu-phoi.cjs +14 -2
- package/hook.cjs +651 -5
- package/lib/aggregate.cjs +68 -2
- package/lib/args.cjs +1 -1
- package/lib/choose.cjs +13 -1
- package/lib/commands.cjs +217 -22
- package/lib/setup.cjs +111 -7
- package/package.json +1 -1
package/lib/aggregate.cjs
CHANGED
|
@@ -339,14 +339,80 @@ function buildSteers(tasks, steers, agents) {
|
|
|
339
339
|
}).sort((a, b) => b.steer_rate - a.steer_rate || a.task_type.localeCompare(b.task_type));
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
function finiteNum(v) {
|
|
343
|
+
return typeof v === 'number' && isFinite(v) ? v : null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Orchestration cost (CONTRACT "Thước đo" mục 6). Sessions = distinct non-empty `session_id` over
|
|
348
|
+
* dispatch events. Per session: ctx = max orch_ctx_tokens, usd = max orch_cum_usd, growth = max of
|
|
349
|
+
* max(0, orch_ctx_tokens - orch_ctx_baseline) over dispatches carrying both, comp = max orch_compactions
|
|
350
|
+
* (numbers only). compactions_total = sum of per-session comp, compactions_max = max.
|
|
351
|
+
* Each agent belongs to the session of its FIRST dispatch (file order); sub_usd_total sums the
|
|
352
|
+
* cost_usd of every complete of those agents (internal completes never have a dispatch, so never count).
|
|
353
|
+
*/
|
|
354
|
+
function orchestrationSummary(events) {
|
|
355
|
+
const sessions = new Map();
|
|
356
|
+
const agentSession = new Map();
|
|
357
|
+
for (const ev of events) {
|
|
358
|
+
if (!ev || ev.kind !== 'dispatch' || typeof ev.session_id !== 'string' || !ev.session_id) continue;
|
|
359
|
+
const sid = ev.session_id;
|
|
360
|
+
if (!sessions.has(sid)) sessions.set(sid, { ctx: null, usd: null, growth: null, comp: null });
|
|
361
|
+
const s = sessions.get(sid);
|
|
362
|
+
const c = finiteNum(ev.orch_ctx_tokens);
|
|
363
|
+
const u = finiteNum(ev.orch_cum_usd);
|
|
364
|
+
if (c !== null && (s.ctx === null || c > s.ctx)) s.ctx = c;
|
|
365
|
+
if (u !== null && (s.usd === null || u > s.usd)) s.usd = u;
|
|
366
|
+
const k = finiteNum(ev.orch_compactions);
|
|
367
|
+
if (k !== null && (s.comp === null || k > s.comp)) s.comp = k;
|
|
368
|
+
const b = finiteNum(ev.orch_ctx_baseline);
|
|
369
|
+
if (c !== null && b !== null) {
|
|
370
|
+
const g = Math.max(0, c - b);
|
|
371
|
+
if (s.growth === null || g > s.growth) s.growth = g;
|
|
372
|
+
}
|
|
373
|
+
if (typeof ev.agent_id === 'string' && ev.agent_id && !agentSession.has(ev.agent_id)) agentSession.set(ev.agent_id, sid);
|
|
374
|
+
}
|
|
375
|
+
let sub = 0;
|
|
376
|
+
for (const ev of events) {
|
|
377
|
+
if (!ev || ev.kind !== 'complete' || !agentSession.has(ev.agent_id)) continue;
|
|
378
|
+
const c = finiteNum(ev.cost_usd);
|
|
379
|
+
if (c !== null) sub += c;
|
|
380
|
+
}
|
|
381
|
+
const ctxs = [];
|
|
382
|
+
const growths = [];
|
|
383
|
+
const comps = [];
|
|
384
|
+
let orch = 0;
|
|
385
|
+
for (const s of sessions.values()) {
|
|
386
|
+
if (s.ctx !== null) ctxs.push(s.ctx);
|
|
387
|
+
if (s.growth !== null) growths.push(s.growth);
|
|
388
|
+
if (s.comp !== null) comps.push(s.comp);
|
|
389
|
+
if (s.usd !== null) orch += s.usd;
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
sessions: sessions.size,
|
|
393
|
+
ctx_p50: round6(percentile(ctxs, 0.5)),
|
|
394
|
+
ctx_max: ctxs.length ? Math.max(...ctxs) : null,
|
|
395
|
+
growth_p50: round6(percentile(growths, 0.5)),
|
|
396
|
+
growth_max: growths.length ? Math.max(...growths) : null,
|
|
397
|
+
compactions_total: comps.reduce((x, y) => x + y, 0),
|
|
398
|
+
compactions_max: comps.length ? Math.max(...comps) : null,
|
|
399
|
+
orch_usd_total: round6(orch),
|
|
400
|
+
sub_usd_total: round6(sub),
|
|
401
|
+
orch_share: orch + sub > 0 ? round6(orch / (orch + sub)) : null,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
342
405
|
function analyze(events, opts) {
|
|
343
406
|
const { agents, userVerdicts, overrides, steers } = buildAgents(events);
|
|
344
407
|
const tasks = buildTasks(agents, userVerdicts);
|
|
345
408
|
const cells = buildCells(tasks, opts);
|
|
346
|
-
return {
|
|
409
|
+
return {
|
|
410
|
+
agents, tasks, cells, overrides, userVerdicts, steerEvents: steers, steers: buildSteers(tasks, steers, agents),
|
|
411
|
+
orchestration: orchestrationSummary(events),
|
|
412
|
+
};
|
|
347
413
|
}
|
|
348
414
|
|
|
349
415
|
module.exports = {
|
|
350
416
|
MIN_N, FAIL_OUTCOMES, HEAVY_STEERS, CANONICAL_CELL_FIELDS, normalizeVerdict, isInternalComplete, buildAgents, buildTasks,
|
|
351
|
-
buildCells, buildSteers, canonicalCells, analyze, cellKey, mean, percentile, tsOf, steerIdOf,
|
|
417
|
+
buildCells, buildSteers, canonicalCells, orchestrationSummary, analyze, cellKey, mean, percentile, tsOf, steerIdOf,
|
|
352
418
|
};
|
package/lib/args.cjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const core = require('../hook.cjs');
|
|
5
5
|
|
|
6
|
-
const BOOLEAN_FLAGS = new Set(['json', 'help', 'h', 'purge', 'version', 'v', 'dry-run', 'no-sync']);
|
|
6
|
+
const BOOLEAN_FLAGS = new Set(['json', 'help', 'h', 'purge', 'version', 'v', 'dry-run', 'no-sync', 'approve']);
|
|
7
7
|
|
|
8
8
|
/** Minimal argv parser: positionals, --k v, --k=v, boolean flags. */
|
|
9
9
|
function parseArgs(argv) {
|
package/lib/choose.cjs
CHANGED
|
@@ -50,6 +50,9 @@ const DEFAULT_TIME_CAP_MIN = {
|
|
|
50
50
|
};
|
|
51
51
|
const DEFAULT_LAMBDA = { relaxed: 0.02, normal: 0.1, urgent: 1.0 };
|
|
52
52
|
const DEFAULT_EXPLORE = 0.1;
|
|
53
|
+
// Orchestrator context soft cap = allowed GROWTH in tokens over the session's first request (baseline:
|
|
54
|
+
// system prompt + tools, ~60-70k). Core default = ceiling: a profile may only lower it.
|
|
55
|
+
const DEFAULT_ORCH_CTX_SOFT_CAP = 60000;
|
|
53
56
|
|
|
54
57
|
function tierName(i) {
|
|
55
58
|
return `${LADDER[i].model}/${LADDER[i].effort}`;
|
|
@@ -121,7 +124,15 @@ function loadProfile(skillDir) {
|
|
|
121
124
|
const timeCap = Object.assign({}, DEFAULT_TIME_CAP_MIN,
|
|
122
125
|
r.time_cap_min && typeof r.time_cap_min === 'object' ? r.time_cap_min : {});
|
|
123
126
|
const extra = Array.isArray(r.extra_task_types) ? r.extra_task_types.map(String) : [];
|
|
124
|
-
|
|
127
|
+
let softCap = DEFAULT_ORCH_CTX_SOFT_CAP;
|
|
128
|
+
if (r.orch_ctx_soft_cap !== undefined && r.orch_ctx_soft_cap !== null) {
|
|
129
|
+
const v = Number(r.orch_ctx_soft_cap);
|
|
130
|
+
if (isFinite(v) && v > 0) softCap = Math.min(DEFAULT_ORCH_CTX_SOFT_CAP, Math.floor(v));
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
source, dept: r.dept || null, priors, lambda, explore_rate: explore, time_cap_min: timeCap, extra_task_types: extra,
|
|
134
|
+
orch_ctx_soft_cap: softCap,
|
|
135
|
+
};
|
|
125
136
|
}
|
|
126
137
|
|
|
127
138
|
// ------------------------------------------------------------------ seeded RNG
|
|
@@ -461,6 +472,7 @@ function choose(opts) {
|
|
|
461
472
|
}
|
|
462
473
|
|
|
463
474
|
module.exports = {
|
|
475
|
+
DEFAULT_ORCH_CTX_SOFT_CAP,
|
|
464
476
|
LADDER, DEFAULT_PRIORS, DEFAULT_LAMBDA, DEFAULT_TIME_CAP_MIN, ROUGH, tierIndex, tierName, parsePrior,
|
|
465
477
|
parseTier, loadProfile, makeRng, betaSample, escalation, overridePrior, roughEstimate, choose, shortModel,
|
|
466
478
|
};
|
package/lib/commands.cjs
CHANGED
|
@@ -15,6 +15,14 @@ const USER_OUTCOMES = ['accept', 'reject'];
|
|
|
15
15
|
const CAUSES = ['quality', 'scope'];
|
|
16
16
|
const STEER_KINDS = ['minor', 'plan', 'redirect'];
|
|
17
17
|
const VERIFY = ['tests', 'lint', 'review', 'judgment', 'none'];
|
|
18
|
+
const INVALID_ID_WINDOW_DAYS = 7;
|
|
19
|
+
const NEW_ID_MAX = 1000;
|
|
20
|
+
|
|
21
|
+
/** Throw the Vietnamese task_id error (with a fresh suggestion) unless `id` is a valid task_id. */
|
|
22
|
+
function requireTaskId(id) {
|
|
23
|
+
if (!core.isValidTaskId(id)) throw new Error(core.taskIdError(id));
|
|
24
|
+
return id;
|
|
25
|
+
}
|
|
18
26
|
|
|
19
27
|
function skillDirOf(opts, ctx) {
|
|
20
28
|
if (typeof opts['skill-dir'] === 'string') return path.resolve(opts['skill-dir']);
|
|
@@ -55,8 +63,9 @@ function cmdSetup(opts) {
|
|
|
55
63
|
const lines = [
|
|
56
64
|
`Đã cài hook: ${res.hook}`,
|
|
57
65
|
res.settings_changed
|
|
58
|
-
? `Đã đăng ký PostToolUse(Agent) + SubagentStop trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
|
|
66
|
+
? `Đã đăng ký PostToolUse(Agent) + SubagentStop + SessionStart(compact) trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
|
|
59
67
|
: `settings.json đã có hook, không đổi gì (${res.settings})`,
|
|
68
|
+
`Thư mục báo cáo subagent: ${res.runs_dir} (quyền: ${res.permissions.allow.join(', ')} + additionalDirectories)`,
|
|
60
69
|
`Phòng ban: ${res.config.dept || '(chưa đặt, dùng --dept)'}`,
|
|
61
70
|
`Sink: ${res.config.sink_url ? res.config.sink_url : '(chưa có — log chỉ lưu cục bộ)'}`,
|
|
62
71
|
`Skill: ${res.config.skill_dir || '(không thấy; dùng --skill-dir)'}${res.config.skill_version ? ` v${res.config.skill_version}` : ''}`,
|
|
@@ -87,8 +96,12 @@ async function cmdDoctor(opts) {
|
|
|
87
96
|
add(major >= 18, 'node', `v${process.versions.node}${major >= 18 ? '' : ' (cần ≥ 18)'}`);
|
|
88
97
|
add(fs.existsSync(ctx.paths.hook), 'hook', ctx.paths.hook);
|
|
89
98
|
const reg = setupLib.registration(claudeDir);
|
|
90
|
-
add(reg.PostToolUse && reg.SubagentStop, 'settings',
|
|
91
|
-
`PostToolUse(Agent)=${reg.PostToolUse ? 'có' : 'thiếu'}, SubagentStop=${reg.SubagentStop ? 'có' : 'thiếu'} (${reg.file})`);
|
|
99
|
+
add(reg.PostToolUse && reg.SubagentStop && reg.SessionStart, 'settings',
|
|
100
|
+
`PostToolUse(Agent)=${reg.PostToolUse ? 'có' : 'thiếu'}, SubagentStop=${reg.SubagentStop ? 'có' : 'thiếu'}, SessionStart(compact)=${reg.SessionStart ? 'có' : 'thiếu'} (${reg.file})`);
|
|
101
|
+
const permOk = reg.permissions && reg.runs_dir_exists && !reg.legacy_permissions;
|
|
102
|
+
add(permOk, 'run-permissions', permOk
|
|
103
|
+
? `${reg.permission_entries.allow.join(', ')} + additionalDirectories ${reg.permission_entries.dir}`
|
|
104
|
+
: `${!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');
|
|
92
105
|
const hasCfg = fs.existsSync(ctx.paths.config);
|
|
93
106
|
add(hasCfg, 'config', hasCfg ? `${ctx.paths.config} dept=${ctx.config.dept || '-'}` : 'chưa có — chạy `dieu-phoi setup`');
|
|
94
107
|
const sd = ctx.config.skill_dir;
|
|
@@ -124,6 +137,15 @@ async function cmdDoctor(opts) {
|
|
|
124
137
|
add(true, 'internal-subagents', skipped && skipped.count
|
|
125
138
|
? `bỏ qua ${skipped.count} SubagentStop của subagent nội bộ Claude Code (lần cuối ${skipped.last_ts})`
|
|
126
139
|
: 'chưa bỏ qua SubagentStop nội bộ nào');
|
|
140
|
+
const since = Date.now() - INVALID_ID_WINDOW_DAYS * 86400000;
|
|
141
|
+
const badIds = events.filter((e) => e.kind === 'dispatch' && e.task_id_invalid === true && !(Date.parse(e.ts) < since));
|
|
142
|
+
add(!badIds.length, 'task-ids', badIds.length
|
|
143
|
+
? `${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
|
+
: `mọi dispatch trong ${INVALID_ID_WINDOW_DAYS} ngày gần đây có task_id đúng định dạng`, 'CẢNH BÁO');
|
|
145
|
+
const orch = measureOrch(opts, ctx, profileOrNull(opts, ctx));
|
|
146
|
+
if (orch.ctx_tokens !== null) {
|
|
147
|
+
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
|
+
}
|
|
127
149
|
if (ctx.config.sink_url && ctx.config.sink_token) {
|
|
128
150
|
try {
|
|
129
151
|
const u = new URL(ctx.config.sink_url);
|
|
@@ -141,7 +163,7 @@ async function cmdDoctor(opts) {
|
|
|
141
163
|
add(true, 'sink', 'chưa cấu hình (chỉ lưu cục bộ)');
|
|
142
164
|
}
|
|
143
165
|
const failed = checks.some((c) => c.level === 'LỖI');
|
|
144
|
-
if (opts.json) return { code: failed ? 1 : 0, out: JSON.stringify({ ok: !failed, checks }, null, 2) };
|
|
166
|
+
if (opts.json) return { code: failed ? 1 : 0, out: JSON.stringify({ ok: !failed, runs_dir: reg.runs_dir, checks }, null, 2) };
|
|
145
167
|
const out = checks.map((c) => `[${c.level}] ${c.name}: ${c.detail}`).join('\n');
|
|
146
168
|
return { code: failed ? 1 : 0, out };
|
|
147
169
|
}
|
|
@@ -150,8 +172,9 @@ async function cmdDoctor(opts) {
|
|
|
150
172
|
|
|
151
173
|
function cmdHook(opts) {
|
|
152
174
|
const home = typeof opts['claude-dir'] === 'string' ? path.join(path.resolve(opts['claude-dir']), 'dieu-phoi') : undefined;
|
|
153
|
-
core.runHook({ home });
|
|
154
|
-
|
|
175
|
+
const r = core.runHook({ home });
|
|
176
|
+
// SessionStart(compact) returns the JSON to print; every other hook event prints nothing.
|
|
177
|
+
return { code: 0, out: r && typeof r.stdout === 'string' ? r.stdout : null };
|
|
155
178
|
}
|
|
156
179
|
|
|
157
180
|
async function cmdSync(opts) {
|
|
@@ -245,6 +268,7 @@ function verdictUser(opts, ctx, events) {
|
|
|
245
268
|
const taskId = opts.task;
|
|
246
269
|
const outcome = opts._[0];
|
|
247
270
|
if (typeof taskId !== 'string' || !taskId) throw new Error('--task cần task_id');
|
|
271
|
+
requireTaskId(taskId);
|
|
248
272
|
if (!USER_OUTCOMES.includes(outcome)) throw new Error(`verdict --task: outcome phải là ${USER_OUTCOMES.join('|')}`);
|
|
249
273
|
let cause = typeof opts.cause === 'string' ? opts.cause : null;
|
|
250
274
|
if (outcome === 'reject' && !CAUSES.includes(cause)) throw new Error(`reject cần --cause ${CAUSES.join('|')} (scope = yêu cầu đổi/thiếu, không phải lỗi model)`);
|
|
@@ -297,6 +321,7 @@ function cmdSteer(opts) {
|
|
|
297
321
|
const kind = opts.kind;
|
|
298
322
|
if (!ref && !opts.task) throw new Error('Cú pháp: dieu-phoi steer <agent_id> | --task <task_id> --kind minor|plan|redirect [--reason R]');
|
|
299
323
|
if (!STEER_KINDS.includes(kind)) throw new Error(`--kind phải là ${STEER_KINDS.join('|')}`);
|
|
324
|
+
if (!ref) requireTaskId(String(opts.task));
|
|
300
325
|
const ctx = ctxOf(opts);
|
|
301
326
|
const events = core.readEvents(ctx);
|
|
302
327
|
const steerId = crypto.randomBytes(4).toString('hex');
|
|
@@ -326,6 +351,76 @@ function cmdSteer(opts) {
|
|
|
326
351
|
return { code: 0, out: `Đã ghi steer ${kind} (${steerId}) cho ${target}.` };
|
|
327
352
|
}
|
|
328
353
|
|
|
354
|
+
// ------------------------------------------------------------------ ctx (orchestrator context)
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Measure the orchestrating session: --session, else env CLAUDE_CODE_SESSION_ID; transcript found under
|
|
358
|
+
* <claude dir>/projects/<project>/<id>.jsonl. Never throws; ctx_tokens null when it cannot measure.
|
|
359
|
+
*/
|
|
360
|
+
function measureOrch(opts, ctx, prof) {
|
|
361
|
+
const sid = typeof opts.session === 'string' && opts.session ? opts.session
|
|
362
|
+
: (process.env.CLAUDE_CODE_SESSION_ID || null);
|
|
363
|
+
const softCap = (prof && prof.orch_ctx_soft_cap) || chooser.DEFAULT_ORCH_CTX_SOFT_CAP;
|
|
364
|
+
let u = { ctx_tokens: null, ctx_baseline: null, ctx_growth: null, cum_usd: null, compactions: null };
|
|
365
|
+
try {
|
|
366
|
+
const file = sid ? core.findSessionTranscript(claudeDirOf(opts), sid) : null;
|
|
367
|
+
if (file) {
|
|
368
|
+
u = core.orchestratorUsage(file, core.loadPricing(ctx), { cacheFile: core.orchCacheFile(ctx.paths, sid, file) });
|
|
369
|
+
}
|
|
370
|
+
} catch (_) { /* measurement never fails the caller */ }
|
|
371
|
+
const over = typeof u.ctx_growth === 'number' && u.ctx_growth > softCap;
|
|
372
|
+
let advice;
|
|
373
|
+
if (!sid) advice = 'Không đo được: không có session (thiếu CLAUDE_CODE_SESSION_ID, dùng --session <id>).';
|
|
374
|
+
else if (u.ctx_tokens === null) advice = 'Không đo được: không thấy transcript của session này.';
|
|
375
|
+
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.`;
|
|
376
|
+
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.`;
|
|
377
|
+
return {
|
|
378
|
+
session_id: sid, ctx_tokens: u.ctx_tokens, ctx_baseline: u.ctx_baseline, ctx_growth: u.ctx_growth,
|
|
379
|
+
cum_usd: u.cum_usd, compactions: u.compactions, soft_cap: softCap, over, advice,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function profileOrNull(opts, ctx) {
|
|
384
|
+
try { return chooser.loadProfile(skillDirOf(opts, ctx)); } catch (_) { return null; }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function cmdCtx(opts) {
|
|
388
|
+
const ctx = ctxOf(opts);
|
|
389
|
+
const m = measureOrch(opts, ctx, profileOrNull(opts, ctx));
|
|
390
|
+
if (opts.json) return { code: 0, out: JSON.stringify(m) };
|
|
391
|
+
if (m.ctx_tokens === null) return { code: 0, out: m.advice };
|
|
392
|
+
return {
|
|
393
|
+
code: 0,
|
|
394
|
+
out: `session ${m.session_id}: context ${m.ctx_tokens} token, nền ${m.ctx_baseline}, tăng ${m.ctx_growth} (trần mềm ${m.soft_cap})${m.over ? ' — VƯỢT' : ''}, `
|
|
395
|
+
+ `chi phí điều phối ≈ ${fmtUsd(m.cum_usd)}, đã nén ${m.compactions === null ? '?' : m.compactions} lần\n${m.advice}`,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function orchLine(o) {
|
|
400
|
+
if (!o || !o.sessions) return 'Điều phối: chưa có dispatch nào ghi session_id (cần core ≥ 0.2.0).';
|
|
401
|
+
return `Điều phối: ${o.sessions} phiên, context p50 ${o.ctx_p50 === null ? '-' : Math.round(o.ctx_p50)} / max ${o.ctx_max === null ? '-' : o.ctx_max} token (tăng so với nền p50 ${o.growth_p50 === null ? '-' : Math.round(o.growth_p50)} / max ${o.growth_max === null ? '-' : o.growth_max}), `
|
|
402
|
+
+ `chi phí điều phối ${fmtUsd(o.orch_usd_total)} vs subagent ${fmtUsd(o.sub_usd_total)} (điều phối chiếm ${pct(o.orch_share)}); nén context ${o.compactions_total} lần (tối đa ${o.compactions_max === null ? '-' : o.compactions_max}/phiên).`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ------------------------------------------------------------------ state (re-injected after /compact)
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Orchestration state of one session rebuilt purely from events.jsonl (same builder the SessionStart(compact)
|
|
409
|
+
* hook uses). Session = --session, else env CLAUDE_CODE_SESSION_ID. Run dirs are pointed to, never read.
|
|
410
|
+
*/
|
|
411
|
+
function cmdState(opts) {
|
|
412
|
+
const ctx = ctxOf(opts);
|
|
413
|
+
const sid = typeof opts.session === 'string' && opts.session ? opts.session
|
|
414
|
+
: (process.env.CLAUDE_CODE_SESSION_ID || null);
|
|
415
|
+
if (!sid) {
|
|
416
|
+
if (opts.json) return { code: 0, out: JSON.stringify({ session_id: null, generated_at: core.nowIso(), runs_dir: null, plans: [], tasks: [] }) };
|
|
417
|
+
return { code: 0, out: 'Không có session (thiếu CLAUDE_CODE_SESSION_ID, dùng --session <id>).' };
|
|
418
|
+
}
|
|
419
|
+
const state = core.buildSessionState(core.readEvents(ctx), sid, { plansDir: ctx.paths.plans });
|
|
420
|
+
if (opts.json) return { code: 0, out: JSON.stringify(state, null, 2) };
|
|
421
|
+
return { code: 0, out: core.formatSessionState(state, core.STATE_TEXT_CAP) };
|
|
422
|
+
}
|
|
423
|
+
|
|
329
424
|
// ------------------------------------------------------------------ stats / retro
|
|
330
425
|
|
|
331
426
|
function lambdaOf(opts, ctx) {
|
|
@@ -340,14 +435,16 @@ function pct(v) {
|
|
|
340
435
|
function cmdStats(opts) {
|
|
341
436
|
const ctx = ctxOf(opts);
|
|
342
437
|
const { lambda } = lambdaOf(opts, ctx);
|
|
343
|
-
const { cells, steers } = agg.analyze(core.readEvents(ctx), { lambda });
|
|
438
|
+
const { cells, steers, orchestration } = agg.analyze(core.readEvents(ctx), { lambda });
|
|
344
439
|
const type = typeof opts.type === 'string' ? opts.type : null;
|
|
345
440
|
const rows = type ? cells.filter((c) => c.task_type === type) : cells;
|
|
346
441
|
const steerRows = type ? steers.filter((s) => s.task_type === type) : steers;
|
|
347
442
|
if (opts.json) {
|
|
348
|
-
return { code: 0, out: JSON.stringify({ lambda_usd_per_min: lambda, min_n: agg.MIN_N, cells: rows, steers: steerRows }, null, 2) };
|
|
443
|
+
return { code: 0, out: JSON.stringify({ lambda_usd_per_min: lambda, min_n: agg.MIN_N, cells: rows, steers: steerRows, orchestration }, null, 2) };
|
|
444
|
+
}
|
|
445
|
+
if (!rows.length) {
|
|
446
|
+
return { code: 0, out: orchestration.sessions ? `Chưa có dữ liệu theo ô.\n${orchLine(orchestration)}` : 'Chưa có dữ liệu.' };
|
|
349
447
|
}
|
|
350
|
-
if (!rows.length) return { code: 0, out: 'Chưa có dữ liệu.' };
|
|
351
448
|
const t = [['task_type', 'model/effort', 'n', 'accept%', 'agent_pass%', 'E[cost]', 'E[time]', 'p50', 'p75', 'score', '']];
|
|
352
449
|
for (const c of rows) {
|
|
353
450
|
t.push([
|
|
@@ -370,7 +467,7 @@ function cmdStats(opts) {
|
|
|
370
467
|
}
|
|
371
468
|
return {
|
|
372
469
|
code: 0,
|
|
373
|
-
out: `${table(t)}\n\n${table(s)}\n\n`
|
|
470
|
+
out: `${table(t)}\n\n${table(s)}\n\n${orchLine(orchestration)}\n\n`
|
|
374
471
|
+ 'n = task tính tỉ lệ (/số đã giao, (-k) = bị loại: reject-scope hoặc lượt exec đầu bị steer plan/redirect). '
|
|
375
472
|
+ '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. '
|
|
376
473
|
+ `E[cost], E[time] tính trọn mọi lượt + review của task. score = E[cost] + λ·E[time], λ=${lambda} USD/phút.`,
|
|
@@ -384,7 +481,7 @@ function cmdRetro(opts) {
|
|
|
384
481
|
const ctx = ctxOf(opts);
|
|
385
482
|
const { prof, lambda } = lambdaOf(opts, ctx);
|
|
386
483
|
const events = core.readEvents(ctx);
|
|
387
|
-
const { agents, tasks, cells, steers, userVerdicts, overrides } = agg.analyze(events, { lambda });
|
|
484
|
+
const { agents, tasks, cells, steers, userVerdicts, overrides, orchestration } = agg.analyze(events, { lambda });
|
|
388
485
|
const reasons = {};
|
|
389
486
|
const outcomes = {};
|
|
390
487
|
const userOutcomes = {};
|
|
@@ -433,6 +530,7 @@ function cmdRetro(opts) {
|
|
|
433
530
|
steers,
|
|
434
531
|
high_steer_task_types: highSteer,
|
|
435
532
|
overrides: overrides.map((o) => ({ ts: o.ts, task_type: o.task_type, suggested_tier: o.suggested_tier, chosen_tier: o.chosen_tier })),
|
|
533
|
+
orchestration,
|
|
436
534
|
cells,
|
|
437
535
|
};
|
|
438
536
|
if (opts.json) return { code: 0, out: JSON.stringify(summary, null, 2) };
|
|
@@ -443,6 +541,7 @@ function cmdRetro(opts) {
|
|
|
443
541
|
`Lý do hay gặp: ${summary.reason_codes.slice(0, 8).map((r) => `${r.code}×${r.count}`).join(', ') || '-'}`,
|
|
444
542
|
`Loại task: ${summary.task_types.map((t) => `${t.task_type}(${t.n})`).join(', ') || '-'}`,
|
|
445
543
|
`Steer nhiều (brainstorm hỏi quá ít): ${highSteer.map((s) => `${s.task_type} ${pct(s.steer_rate)}`).join(', ') || '-'}`,
|
|
544
|
+
orchLine(orchestration),
|
|
446
545
|
'Chi tiết theo ô: `dieu-phoi stats`; đưa cho LLM: `dieu-phoi retro --json`.',
|
|
447
546
|
];
|
|
448
547
|
return { code: 0, out: lines.join('\n') };
|
|
@@ -473,8 +572,13 @@ function cmdChoose(opts) {
|
|
|
473
572
|
if (opts.override !== undefined && typeof opts.task !== 'string') {
|
|
474
573
|
throw new Error('--override cần --task <task_id> (để học override theo từng task)');
|
|
475
574
|
}
|
|
575
|
+
if (opts.task !== undefined) requireTaskId(typeof opts.task === 'string' ? opts.task : '');
|
|
476
576
|
const input = chooseInput(opts, ctx, core.readEvents(ctx));
|
|
477
577
|
const res = chooser.choose(input);
|
|
578
|
+
const orch = measureOrch(opts, ctx, input.profile);
|
|
579
|
+
res.orch_ctx_tokens = orch.ctx_tokens;
|
|
580
|
+
res.orch_ctx_growth = orch.ctx_growth;
|
|
581
|
+
res.orch_ctx_over = orch.over;
|
|
478
582
|
if (res.overridden) {
|
|
479
583
|
const tid = input.taskId;
|
|
480
584
|
core.appendEvent(ctx, Object.assign({
|
|
@@ -489,13 +593,14 @@ function cmdChoose(opts) {
|
|
|
489
593
|
}, core.commonFields(ctx)));
|
|
490
594
|
}
|
|
491
595
|
if (opts.json) return { code: 0, out: JSON.stringify(res) };
|
|
492
|
-
const
|
|
596
|
+
const warn = orch.over ? `\nCẢNH BÁO: ${orch.advice}` : '';
|
|
597
|
+
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ô'})`;
|
|
493
598
|
if (res.action === 'ask_user') {
|
|
494
|
-
return { code: 0, out: `HỎI NGƯỜI DÙNG — ${res.reason}` };
|
|
599
|
+
return { code: 0, out: `HỎI NGƯỜI DÙNG — ${res.reason}${warn}` };
|
|
495
600
|
}
|
|
496
601
|
return {
|
|
497
602
|
code: 0,
|
|
498
|
-
out: `model=${res.model} effort=${res.effort}${res.explored ? ' (thăm dò)' : ''}${res.overridden ? ' (override)' : ''}${res.deviation ? ' (LỆCH khỏi tier đã duyệt)' : ''} ${est} — ${res.reason}`,
|
|
603
|
+
out: `model=${res.model} effort=${res.effort}${res.explored ? ' (thăm dò)' : ''}${res.overridden ? ' (override)' : ''}${res.deviation ? ' (LỆCH khỏi tier đã duyệt)' : ''} ${est} — ${res.reason}${warn}`,
|
|
499
604
|
};
|
|
500
605
|
}
|
|
501
606
|
|
|
@@ -563,7 +668,11 @@ function buildPlanTable(rawSteps, base) {
|
|
|
563
668
|
role: s.role || base.role,
|
|
564
669
|
seed: base.seed === undefined || base.seed === null ? `plan:${s.step}` : `${base.seed}:${s.step}`,
|
|
565
670
|
}));
|
|
566
|
-
|
|
671
|
+
// Rubric inputs that drive the safety floors: shown to the user at gate 1 so mis-scoring is caught.
|
|
672
|
+
const head = {
|
|
673
|
+
step: s.step, title: s.title, type: s.type, task_id: s.task || null, score: Number(s.score),
|
|
674
|
+
verifiable: s.verifiable, reversible: s.reversible, depends_on: s.deps,
|
|
675
|
+
};
|
|
567
676
|
try {
|
|
568
677
|
return Object.assign(head, call());
|
|
569
678
|
} catch (e) {
|
|
@@ -589,12 +698,86 @@ function buildPlanTable(rawSteps, base) {
|
|
|
589
698
|
};
|
|
590
699
|
}
|
|
591
700
|
|
|
701
|
+
/**
|
|
702
|
+
* Record the plan the user approved at gate 1: <data>/plans/<root>.json (local only — not an event, never
|
|
703
|
+
* synced). Each re-approval increments `approval` (number of approvals for this root) and overwrites the
|
|
704
|
+
* record; earlier approvals move to `history`. Not the user-verdict `round`.
|
|
705
|
+
* tier = exactly what the approved table shows (override applied); refused steps -> null.
|
|
706
|
+
*/
|
|
707
|
+
function prevApproval(prev) {
|
|
708
|
+
if (!prev || typeof prev !== 'object') return 0;
|
|
709
|
+
const n = Number.isInteger(prev.approval) ? prev.approval : prev.round; // `round`: 0.2.0 dev records
|
|
710
|
+
return Number.isInteger(n) && n > 0 ? n : 0;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Steps that cannot be matched to dispatches once approved: missing task_id, a task_id used twice, or a
|
|
715
|
+
* task_id equal to the root. Returns Vietnamese problem strings (empty = ok).
|
|
716
|
+
*/
|
|
717
|
+
function planTaskIdProblems(plan, root) {
|
|
718
|
+
const problems = [];
|
|
719
|
+
const missing = plan.steps.filter((r) => !r.task_id).map((r) => r.step);
|
|
720
|
+
if (missing.length) problems.push(`thiếu task_id ở bước ${missing.join(', ')}`);
|
|
721
|
+
for (const r of plan.steps) {
|
|
722
|
+
if (r.task_id && !core.isValidTaskId(r.task_id)) problems.push(`bước ${r.step}: ${core.taskIdError(r.task_id)}`);
|
|
723
|
+
}
|
|
724
|
+
const by = new Map();
|
|
725
|
+
for (const r of plan.steps) if (r.task_id) by.set(r.task_id, (by.get(r.task_id) || []).concat(r.step));
|
|
726
|
+
for (const [id, steps] of by) if (steps.length > 1) problems.push(`task_id "${id}" bị dùng lại ở các bước ${steps.join(', ')}`);
|
|
727
|
+
if (root) {
|
|
728
|
+
const same = plan.steps.filter((r) => r.task_id === root).map((r) => r.step);
|
|
729
|
+
if (same.length) problems.push(`task_id của bước ${same.join(', ')} trùng --root "${root}" (root dành cho scout/planner)`);
|
|
730
|
+
}
|
|
731
|
+
return problems;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function recordApprovedPlan(ctx, root, stepsFile, plan) {
|
|
735
|
+
const file = path.join(ctx.paths.plans, `${root}.json`);
|
|
736
|
+
const prev = core.readJsonFile(file, null);
|
|
737
|
+
let history = [];
|
|
738
|
+
if (prev && typeof prev === 'object' && !Array.isArray(prev)) {
|
|
739
|
+
const old = Object.assign({}, prev);
|
|
740
|
+
delete old.history;
|
|
741
|
+
history = (Array.isArray(prev.history) ? prev.history : []).concat([old]);
|
|
742
|
+
}
|
|
743
|
+
const rec = {
|
|
744
|
+
root_task_id: root,
|
|
745
|
+
steps_file: stepsFile.split(path.sep).join('/'),
|
|
746
|
+
approved_at: core.nowIso(),
|
|
747
|
+
approval: prevApproval(prev) + 1,
|
|
748
|
+
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,
|
|
750
|
+
tier: r.action === 'refused' ? null : (r.tier || null), depends_on: r.depends_on,
|
|
751
|
+
})),
|
|
752
|
+
history,
|
|
753
|
+
};
|
|
754
|
+
core.writeFileAtomic(file, `${JSON.stringify(rec, null, 2)}\n`);
|
|
755
|
+
return Object.assign({ file }, rec);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// ------------------------------------------------------------------ new-id
|
|
759
|
+
|
|
760
|
+
/** `dieu-phoi new-id [--count n]`: fresh valid task_ids, one per line (the orchestrator never invents ids). */
|
|
761
|
+
function cmdNewId(opts) {
|
|
762
|
+
const raw = opts.count === undefined ? '1' : String(opts.count);
|
|
763
|
+
const n = /^\d+$/.test(raw) ? Number(raw) : NaN;
|
|
764
|
+
if (!Number.isInteger(n) || n < 1 || n > NEW_ID_MAX) throw new Error(`--count phải là số nguyên 1–${NEW_ID_MAX}`);
|
|
765
|
+
const ids = new Set();
|
|
766
|
+
while (ids.size < n) ids.add(core.newTaskId());
|
|
767
|
+
const list = Array.from(ids);
|
|
768
|
+
return { code: 0, out: opts.json ? JSON.stringify(list) : list.join('\n') };
|
|
769
|
+
}
|
|
770
|
+
|
|
592
771
|
function mdCell(s) {
|
|
593
772
|
return String(s).replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
|
|
594
773
|
}
|
|
595
774
|
|
|
596
775
|
function cmdPlanTable(opts) {
|
|
597
|
-
if (typeof opts.file !== 'string') throw new Error('Cú pháp: dieu-phoi plan-table --file <steps.json> [--json]');
|
|
776
|
+
if (typeof opts.file !== 'string') throw new Error('Cú pháp: dieu-phoi plan-table --file <steps.json> [--json] [--approve --root <root_task_id>]');
|
|
777
|
+
if (opts.approve && typeof opts.root !== 'string') {
|
|
778
|
+
throw new Error(`--approve cần --root <root_task_id> (đúng 8 ký tự a-z0-9, vd. ${core.newTaskId()})`);
|
|
779
|
+
}
|
|
780
|
+
if (opts.approve) requireTaskId(opts.root);
|
|
598
781
|
let raw;
|
|
599
782
|
try {
|
|
600
783
|
raw = JSON.parse(fs.readFileSync(path.resolve(opts.file), 'utf8').replace(/^/, ''));
|
|
@@ -605,13 +788,24 @@ function cmdPlanTable(opts) {
|
|
|
605
788
|
const base = chooseInput(Object.assign({}, opts, { type: 'x', score: 0, override: undefined }), ctx, core.readEvents(ctx));
|
|
606
789
|
delete base.override;
|
|
607
790
|
const plan = buildPlanTable(raw, base);
|
|
608
|
-
|
|
609
|
-
|
|
791
|
+
const problems = planTaskIdProblems(plan, opts.approve ? String(opts.root) : null);
|
|
792
|
+
if (opts.approve && problems.length) {
|
|
793
|
+
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
|
+
}
|
|
795
|
+
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`);
|
|
796
|
+
const rec = opts.approve ? recordApprovedPlan(ctx, String(opts.root), path.resolve(opts.file), plan) : null;
|
|
797
|
+
if (opts.json) {
|
|
798
|
+
const out = rec ? Object.assign({}, plan, { approved: { root_task_id: rec.root_task_id, approval: rec.approval, record: rec.file } }) : plan;
|
|
799
|
+
return { code: 0, out: JSON.stringify(out, null, 2) };
|
|
800
|
+
}
|
|
801
|
+
const lines = ['| Bước | Model/effort | Rubric (điểm · kiểm chứng · đảo ngược) | Lý do | Ước tính |', '|---|---|---|---|---|'];
|
|
610
802
|
for (const r of plan.steps) {
|
|
611
803
|
const tierCell = r.action === 'refused' ? '—' : (r.action === 'ask_user' ? `${r.tier} (hỏi người dùng)` : r.tier);
|
|
612
|
-
const
|
|
804
|
+
const yn = (v) => (v ? 'y' : 'n');
|
|
805
|
+
const rubric = `${Number.isFinite(r.score) ? r.score : '?'} · ${yn(r.verifiable)} · ${yn(r.reversible)}`;
|
|
806
|
+
const why = `${r.type}${r.depends_on.length ? `, sau ${r.depends_on.join(', ')}` : ''} — ${r.reason.replace(/; profile: .*$/, '')}`;
|
|
613
807
|
const est = r.action === 'refused' ? '-' : `~${fmtUsd(r.est_cost_usd)} · ~${fmtNum(r.est_time_min)} phút${r.est_source === 'rough' ? ' (thô)' : ''}`;
|
|
614
|
-
lines.push(`| ${mdCell(r.title ? `${r.step}. ${r.title}` : r.step)} | ${mdCell(tierCell)} | ${mdCell(why)} | ${mdCell(est)} |`);
|
|
808
|
+
lines.push(`| ${mdCell(r.title ? `${r.step}. ${r.title}` : r.step)} | ${mdCell(tierCell)} | ${mdCell(rubric)} | ${mdCell(why)} | ${mdCell(est)} |`);
|
|
615
809
|
}
|
|
616
810
|
lines.push('');
|
|
617
811
|
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)`);
|
|
@@ -621,10 +815,11 @@ function cmdPlanTable(opts) {
|
|
|
621
815
|
process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${plan.refused.length} bước bị từ chối
|
|
622
816
|
`);
|
|
623
817
|
}
|
|
818
|
+
if (rec) lines.push('', `Đã ghi kế hoạch đã duyệt (root ${rec.root_task_id}, lần duyệt ${rec.approval})`);
|
|
624
819
|
return { code: 0, out: lines.join('\n') };
|
|
625
820
|
}
|
|
626
821
|
|
|
627
822
|
module.exports = {
|
|
628
|
-
cmdSetup, cmdUninstall, cmdDoctor, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdStats, cmdRetro, cmdChoose,
|
|
629
|
-
cmdPlanTable, buildPlanTable, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
|
|
823
|
+
cmdSetup, cmdUninstall, cmdDoctor, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdStats, cmdRetro, cmdChoose, cmdCtx, cmdState, measureOrch,
|
|
824
|
+
cmdPlanTable, cmdNewId, requireTaskId, buildPlanTable, recordApprovedPlan, planTaskIdProblems, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
|
|
630
825
|
};
|