@kienntpixon/dieu-phoi 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +101 -2
- package/bin/dieu-phoi.cjs +82 -0
- package/hook.cjs +834 -0
- package/lib/aggregate.cjs +337 -0
- package/lib/args.cjs +55 -0
- package/lib/choose.cjs +466 -0
- package/lib/commands.cjs +626 -0
- package/lib/setup.cjs +266 -0
- package/package.json +34 -4
- package/pricing.json +25 -0
package/lib/commands.cjs
ADDED
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const cp = require('child_process');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const core = require('../hook.cjs');
|
|
8
|
+
const agg = require('./aggregate.cjs');
|
|
9
|
+
const chooser = require('./choose.cjs');
|
|
10
|
+
const setupLib = require('./setup.cjs');
|
|
11
|
+
const { claudeDirOf, ctxOf, yesNo } = require('./args.cjs');
|
|
12
|
+
|
|
13
|
+
const AGENT_OUTCOMES = ['pass', 'fail', 'timeout'];
|
|
14
|
+
const USER_OUTCOMES = ['accept', 'reject'];
|
|
15
|
+
const CAUSES = ['quality', 'scope'];
|
|
16
|
+
const STEER_KINDS = ['minor', 'plan', 'redirect'];
|
|
17
|
+
const VERIFY = ['tests', 'lint', 'review', 'judgment', 'none'];
|
|
18
|
+
|
|
19
|
+
function skillDirOf(opts, ctx) {
|
|
20
|
+
if (typeof opts['skill-dir'] === 'string') return path.resolve(opts['skill-dir']);
|
|
21
|
+
if (ctx.config.skill_dir && fs.existsSync(path.join(ctx.config.skill_dir, 'profile', 'profile.json'))) {
|
|
22
|
+
return ctx.config.skill_dir;
|
|
23
|
+
}
|
|
24
|
+
return setupLib.findSkillDir(claudeDirOf(opts)) || ctx.config.skill_dir || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fmtUsd(v) {
|
|
28
|
+
return v === null || v === undefined ? '-' : `$${v.toFixed(v < 1 ? 3 : 2)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fmtNum(v, d) {
|
|
32
|
+
return v === null || v === undefined ? '-' : v.toFixed(d === undefined ? 1 : d);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function table(rows) {
|
|
36
|
+
const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => String(r[i]).length)));
|
|
37
|
+
return rows.map((r) => r.map((c, i) => String(c).padEnd(widths[i])).join(' ').trimEnd()).join('\n');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ------------------------------------------------------------------ setup / uninstall
|
|
41
|
+
|
|
42
|
+
function cmdSetup(opts) {
|
|
43
|
+
const res = setupLib.setup({
|
|
44
|
+
claudeDir: claudeDirOf(opts),
|
|
45
|
+
dept: typeof opts.dept === 'string' ? opts.dept : undefined,
|
|
46
|
+
sinkUrl: typeof opts['sink-url'] === 'string' ? opts['sink-url'] : undefined,
|
|
47
|
+
sinkToken: typeof opts['sink-token'] === 'string' ? opts['sink-token'] : undefined,
|
|
48
|
+
skillDir: typeof opts['skill-dir'] === 'string' ? opts['skill-dir'] : undefined,
|
|
49
|
+
});
|
|
50
|
+
if (opts.json) {
|
|
51
|
+
const safe = Object.assign({}, res, { config: Object.assign({}, res.config) });
|
|
52
|
+
if (safe.config.sink_token) safe.config.sink_token = '***';
|
|
53
|
+
return { code: 0, out: JSON.stringify(safe, null, 2) };
|
|
54
|
+
}
|
|
55
|
+
const lines = [
|
|
56
|
+
`Đã cài hook: ${res.hook}`,
|
|
57
|
+
res.settings_changed
|
|
58
|
+
? `Đã đăng ký PostToolUse(Agent) + SubagentStop trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
|
|
59
|
+
: `settings.json đã có hook, không đổi gì (${res.settings})`,
|
|
60
|
+
`Phòng ban: ${res.config.dept || '(chưa đặt, dùng --dept)'}`,
|
|
61
|
+
`Sink: ${res.config.sink_url ? res.config.sink_url : '(chưa có — log chỉ lưu cục bộ)'}`,
|
|
62
|
+
`Skill: ${res.config.skill_dir || '(không thấy; dùng --skill-dir)'}${res.config.skill_version ? ` v${res.config.skill_version}` : ''}`,
|
|
63
|
+
'Khởi động lại Claude Code (hoặc mở /hooks) để hook có hiệu lực.',
|
|
64
|
+
];
|
|
65
|
+
return { code: 0, out: lines.join('\n') };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cmdUninstall(opts) {
|
|
69
|
+
const res = setupLib.uninstall({ claudeDir: claudeDirOf(opts), purge: !!opts.purge });
|
|
70
|
+
if (opts.json) return { code: 0, out: JSON.stringify(res, null, 2) };
|
|
71
|
+
return {
|
|
72
|
+
code: 0,
|
|
73
|
+
out: res.removed
|
|
74
|
+
? `Đã gỡ ${res.removed} hook khỏi ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}. Dữ liệu giữ ở ${res.data_kept}.`
|
|
75
|
+
: `Không thấy hook dieu-phoi trong ${res.settings}.`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ------------------------------------------------------------------ doctor
|
|
80
|
+
|
|
81
|
+
async function cmdDoctor(opts) {
|
|
82
|
+
const ctx = ctxOf(opts);
|
|
83
|
+
const claudeDir = claudeDirOf(opts);
|
|
84
|
+
const checks = [];
|
|
85
|
+
const add = (ok, name, detail, level) => checks.push({ ok, level: ok ? 'OK' : (level || 'LỖI'), name, detail });
|
|
86
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
87
|
+
add(major >= 18, 'node', `v${process.versions.node}${major >= 18 ? '' : ' (cần ≥ 18)'}`);
|
|
88
|
+
add(fs.existsSync(ctx.paths.hook), 'hook', ctx.paths.hook);
|
|
89
|
+
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})`);
|
|
92
|
+
const hasCfg = fs.existsSync(ctx.paths.config);
|
|
93
|
+
add(hasCfg, 'config', hasCfg ? `${ctx.paths.config} dept=${ctx.config.dept || '-'}` : 'chưa có — chạy `dieu-phoi setup`');
|
|
94
|
+
const sd = ctx.config.skill_dir;
|
|
95
|
+
add(!!sd && fs.existsSync(path.join(sd, 'profile', 'profile.json')), 'profile',
|
|
96
|
+
sd ? path.join(sd, 'profile', 'profile.json') : 'không có skill_dir — choose dùng priors mặc định', 'CẢNH BÁO');
|
|
97
|
+
if (reg.commands.length) {
|
|
98
|
+
const cmd = reg.commands[0];
|
|
99
|
+
const t0 = Date.now();
|
|
100
|
+
const r = cp.spawnSync(cmd, {
|
|
101
|
+
shell: true, input: '{}', encoding: 'utf8', timeout: 10000, windowsHide: true,
|
|
102
|
+
});
|
|
103
|
+
const ms = Date.now() - t0;
|
|
104
|
+
const ok = r.status === 0 && !r.error && !(r.stdout || '').trim();
|
|
105
|
+
add(ok, 'hook-run', ok ? `chạy lệnh đã đăng ký với {} → exit 0, không in stdout (${ms} ms)`
|
|
106
|
+
: `chạy \`${cmd}\` lỗi: exit=${r.status}${r.error ? ` ${r.error.message}` : ''}${(r.stdout || '').trim() ? ' (có stdout)' : ''} ${(r.stderr || '').trim().slice(0, 200)}`);
|
|
107
|
+
}
|
|
108
|
+
let rejectedLines = [];
|
|
109
|
+
try {
|
|
110
|
+
rejectedLines = fs.readFileSync(ctx.paths.syncRejected, 'utf8').split(/\r?\n/).filter((l) => l.trim());
|
|
111
|
+
} catch (_) { /* none */ }
|
|
112
|
+
const rejectedCount = rejectedLines.reduce((n, l) => {
|
|
113
|
+
try { const o = JSON.parse(l); return n + (o.unidentified || 1); } catch (_) { return n + 1; }
|
|
114
|
+
}, 0);
|
|
115
|
+
add(rejectedCount === 0, 'sync-rejected', rejectedCount
|
|
116
|
+
? `sink từ chối ${rejectedCount} bản ghi — xem ${ctx.paths.syncRejected}` : 'không có bản ghi bị sink từ chối', 'CẢNH BÁO');
|
|
117
|
+
const events = core.readEvents(ctx);
|
|
118
|
+
const un = core.unsyncedInfo(ctx);
|
|
119
|
+
add(true, 'events', `${events.length} bản ghi, ${un.lines} chưa đẩy lên sink (${un.bytes} byte sau offset) (${ctx.paths.events})`);
|
|
120
|
+
let errSize = 0;
|
|
121
|
+
try { errSize = fs.statSync(ctx.paths.errlog).size; } catch (_) { /* none */ }
|
|
122
|
+
add(errSize === 0, 'hook-errors', errSize ? `${ctx.paths.errlog} có ${errSize} byte lỗi` : 'không có lỗi', 'CẢNH BÁO');
|
|
123
|
+
if (ctx.config.sink_url && ctx.config.sink_token) {
|
|
124
|
+
try {
|
|
125
|
+
const u = new URL(ctx.config.sink_url);
|
|
126
|
+
u.searchParams.set('token', ctx.config.sink_token);
|
|
127
|
+
u.searchParams.set('view', 'stats');
|
|
128
|
+
const res = await fetch(u, { redirect: 'follow', signal: AbortSignal.timeout(8000) });
|
|
129
|
+
const txt = await res.text();
|
|
130
|
+
let ok = res.ok;
|
|
131
|
+
try { const b = JSON.parse(txt); if (b && b.ok === false) ok = false; } catch (_) { ok = false; }
|
|
132
|
+
add(ok, 'sink', `${ctx.config.sink_url} → HTTP ${res.status}`);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
add(false, 'sink', `${ctx.config.sink_url} → ${e.message}`);
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
add(true, 'sink', 'chưa cấu hình (chỉ lưu cục bộ)');
|
|
138
|
+
}
|
|
139
|
+
const failed = checks.some((c) => c.level === 'LỖI');
|
|
140
|
+
if (opts.json) return { code: failed ? 1 : 0, out: JSON.stringify({ ok: !failed, checks }, null, 2) };
|
|
141
|
+
const out = checks.map((c) => `[${c.level}] ${c.name}: ${c.detail}`).join('\n');
|
|
142
|
+
return { code: failed ? 1 : 0, out };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ------------------------------------------------------------------ hook / sync
|
|
146
|
+
|
|
147
|
+
function cmdHook(opts) {
|
|
148
|
+
const home = typeof opts['claude-dir'] === 'string' ? path.join(path.resolve(opts['claude-dir']), 'dieu-phoi') : undefined;
|
|
149
|
+
core.runHook({ home });
|
|
150
|
+
return { code: 0, out: null };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function cmdSync(opts) {
|
|
154
|
+
const ctx = ctxOf(opts);
|
|
155
|
+
const res = await core.runSync(ctx);
|
|
156
|
+
if (opts.json) return { code: res.ok ? 0 : 1, out: JSON.stringify(res) };
|
|
157
|
+
if (res.skipped === 'no-sink') return { code: 0, out: 'Chưa cấu hình sink — không đẩy gì.' };
|
|
158
|
+
if (res.skipped === 'locked') return { code: 0, out: 'Đang có tiến trình sync khác — bỏ qua.' };
|
|
159
|
+
const msg = `Đã đẩy ${res.sent} bản ghi trong ${res.batches} lô (nhận ${res.accepted}, trùng ${res.duplicates}).`;
|
|
160
|
+
return { code: res.ok ? 0 : 1, out: res.ok ? msg : `${msg} Lỗi: ${res.error}` };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ------------------------------------------------------------------ verdict
|
|
164
|
+
|
|
165
|
+
const LAST_WINDOW_MS = 30 * 60 * 1000;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* `last`: the most recent completed agent. Refuses when more than one agent completed in
|
|
169
|
+
* the last 30 min without a verdict (ambiguous: give the agent_id explicitly).
|
|
170
|
+
*/
|
|
171
|
+
function resolveAgentId(events, ref, now) {
|
|
172
|
+
if (ref === 'last') {
|
|
173
|
+
const latest = new Map();
|
|
174
|
+
const verdicted = new Set();
|
|
175
|
+
events.forEach((e, i) => {
|
|
176
|
+
if (!e.agent_id) return;
|
|
177
|
+
if (e.kind === 'verdict' && agg.normalizeVerdict(e).by === 'agent') verdicted.add(e.agent_id);
|
|
178
|
+
if (e.kind !== 'complete') return;
|
|
179
|
+
const t = Date.parse(e.ts) || 0;
|
|
180
|
+
const cur = latest.get(e.agent_id);
|
|
181
|
+
if (!cur || t > cur.t || (t === cur.t && i > cur.i)) latest.set(e.agent_id, { id: e.agent_id, t, i, ev: e });
|
|
182
|
+
});
|
|
183
|
+
if (!latest.size) throw new Error('Chưa có agent nào hoàn thành trong log.');
|
|
184
|
+
const nowMs = now || Date.now();
|
|
185
|
+
const pending = Array.from(latest.values())
|
|
186
|
+
.filter((x) => !verdicted.has(x.id) && nowMs - x.t <= LAST_WINDOW_MS)
|
|
187
|
+
.sort((a, b) => b.t - a.t || b.i - a.i);
|
|
188
|
+
if (pending.length > 1) {
|
|
189
|
+
const list = pending.slice(0, 10).map((x) => ` ${x.id} ${x.ev.task_type || '?'}${x.ev.task_id ? ` task=${x.ev.task_id}` : ''}${x.ev.try ? ` try${x.ev.try}` : ''} ${new Date(x.t).toISOString()}`);
|
|
190
|
+
throw new Error(`"last" không rõ: ${pending.length} agent hoàn thành trong 30 phút qua chưa có verdict. Ghi rõ agent_id:\n${list.join('\n')}`);
|
|
191
|
+
}
|
|
192
|
+
if (pending.length === 1) return pending[0].id;
|
|
193
|
+
let best = null;
|
|
194
|
+
for (const x of latest.values()) if (!best || x.t > best.t || (x.t === best.t && x.i > best.i)) best = x;
|
|
195
|
+
return best.id;
|
|
196
|
+
}
|
|
197
|
+
const ids = new Set(events.filter((e) => e.kind === 'dispatch' || e.kind === 'complete')
|
|
198
|
+
.map((e) => e.agent_id).filter(Boolean));
|
|
199
|
+
if (ids.has(ref)) return ref;
|
|
200
|
+
const pref = Array.from(ids).filter((id) => ref.length >= 4 && id.startsWith(ref));
|
|
201
|
+
if (pref.length === 1) return pref[0];
|
|
202
|
+
if (pref.length > 1) throw new Error(`agent_id "${ref}" khớp nhiều agent: ${pref.slice(0, 5).join(', ')}`);
|
|
203
|
+
throw new Error(`Không thấy agent "${ref}" trong log.`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function kebab(s) {
|
|
207
|
+
if (typeof s !== 'string') return null;
|
|
208
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function verdictAgent(opts, ctx, events) {
|
|
212
|
+
const [ref, outcome] = opts._;
|
|
213
|
+
if (!ref || !outcome) {
|
|
214
|
+
throw new Error('Cú pháp: dieu-phoi verdict <agent_id|last> <pass|fail|timeout> [--verify V] [--reason R]\n'
|
|
215
|
+
+ ' dieu-phoi verdict --task <task_id> <accept|reject> [--cause quality|scope] [--round n]');
|
|
216
|
+
}
|
|
217
|
+
if (outcome === 'rework') {
|
|
218
|
+
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');
|
|
219
|
+
}
|
|
220
|
+
if (!AGENT_OUTCOMES.includes(outcome)) throw new Error(`outcome tầng agent phải là ${AGENT_OUTCOMES.join('|')} (accept/reject dùng --task)`);
|
|
221
|
+
const verify = typeof opts.verify === 'string' ? opts.verify : 'none';
|
|
222
|
+
if (!VERIFY.includes(verify)) throw new Error(`--verify phải là ${VERIFY.join('|')}`);
|
|
223
|
+
const agentId = resolveAgentId(events, ref);
|
|
224
|
+
const agent = agg.buildAgents(events).agents.find((a) => a.agent_id === agentId);
|
|
225
|
+
return Object.assign({
|
|
226
|
+
schema: core.SCHEMA,
|
|
227
|
+
kind: 'verdict',
|
|
228
|
+
ts: core.nowIso(),
|
|
229
|
+
agent_id: agentId,
|
|
230
|
+
task_id: (agent && agent.task_id) || null,
|
|
231
|
+
by: 'agent',
|
|
232
|
+
outcome,
|
|
233
|
+
verify,
|
|
234
|
+
reason_code: kebab(opts.reason),
|
|
235
|
+
cause: null,
|
|
236
|
+
round: null,
|
|
237
|
+
}, core.commonFields(ctx));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function verdictUser(opts, ctx, events) {
|
|
241
|
+
const taskId = opts.task;
|
|
242
|
+
const outcome = opts._[0];
|
|
243
|
+
if (typeof taskId !== 'string' || !taskId) throw new Error('--task cần task_id');
|
|
244
|
+
if (!USER_OUTCOMES.includes(outcome)) throw new Error(`verdict --task: outcome phải là ${USER_OUTCOMES.join('|')}`);
|
|
245
|
+
let cause = typeof opts.cause === 'string' ? opts.cause : null;
|
|
246
|
+
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)`);
|
|
247
|
+
if (outcome === 'accept') cause = null;
|
|
248
|
+
const known = events.some((e) => e.task_id === taskId);
|
|
249
|
+
if (!known) throw new Error(`Không thấy task "${taskId}" trong log.`);
|
|
250
|
+
const rounds = events.filter((e) => e.kind === 'verdict' && e.task_id === taskId
|
|
251
|
+
&& agg.normalizeVerdict(e).by === 'user').map((e) => Number(e.round) || 1);
|
|
252
|
+
let round = (rounds.length ? Math.max(...rounds) : 0) + 1;
|
|
253
|
+
if (opts.round !== undefined) {
|
|
254
|
+
round = Number(opts.round);
|
|
255
|
+
if (!Number.isInteger(round) || round < 1) throw new Error('--round phải là số nguyên ≥ 1');
|
|
256
|
+
}
|
|
257
|
+
return Object.assign({
|
|
258
|
+
schema: core.SCHEMA,
|
|
259
|
+
kind: 'verdict',
|
|
260
|
+
ts: core.nowIso(),
|
|
261
|
+
agent_id: `task:${taskId}`,
|
|
262
|
+
task_id: taskId,
|
|
263
|
+
by: 'user',
|
|
264
|
+
outcome,
|
|
265
|
+
cause,
|
|
266
|
+
round,
|
|
267
|
+
verify: 'judgment',
|
|
268
|
+
reason_code: kebab(opts.reason),
|
|
269
|
+
}, core.commonFields(ctx));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function cmdVerdict(opts) {
|
|
273
|
+
const ctx = ctxOf(opts);
|
|
274
|
+
const events = core.readEvents(ctx);
|
|
275
|
+
const ev = typeof opts.task === 'string' ? verdictUser(opts, ctx, events) : verdictAgent(opts, ctx, events);
|
|
276
|
+
core.appendEvent(ctx, ev);
|
|
277
|
+
if (opts.json) return { code: 0, out: JSON.stringify(ev) };
|
|
278
|
+
if (ev.by === 'user') {
|
|
279
|
+
return { code: 0, out: `Đã ghi người dùng ${ev.outcome}${ev.cause ? ` (${ev.cause})` : ''} cho task ${ev.task_id}, vòng ${ev.round}.` };
|
|
280
|
+
}
|
|
281
|
+
return { code: 0, out: `Đã ghi ${ev.outcome} cho ${ev.agent_id}${ev.task_id ? ` (task ${ev.task_id})` : ''}.` };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ------------------------------------------------------------------ steer
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* One `steer` invocation = one user action with one steer_id shared by its events.
|
|
288
|
+
* --task: marks only the task's in-flight agents (dispatched, not yet complete+verdict);
|
|
289
|
+
* none in flight -> one task-level event (agent_id task:<id>) that affects no rate.
|
|
290
|
+
*/
|
|
291
|
+
function cmdSteer(opts) {
|
|
292
|
+
const ref = opts._[0];
|
|
293
|
+
const kind = opts.kind;
|
|
294
|
+
if (!ref && !opts.task) throw new Error('Cú pháp: dieu-phoi steer <agent_id> | --task <task_id> --kind minor|plan|redirect [--reason R]');
|
|
295
|
+
if (!STEER_KINDS.includes(kind)) throw new Error(`--kind phải là ${STEER_KINDS.join('|')}`);
|
|
296
|
+
const ctx = ctxOf(opts);
|
|
297
|
+
const events = core.readEvents(ctx);
|
|
298
|
+
const steerId = crypto.randomBytes(4).toString('hex');
|
|
299
|
+
const ts = core.nowIso();
|
|
300
|
+
const { agents } = agg.buildAgents(events);
|
|
301
|
+
const mk = (agentId, taskId, taskType) => Object.assign({
|
|
302
|
+
schema: core.SCHEMA, kind: 'steer', ts, agent_id: agentId, steer_id: steerId,
|
|
303
|
+
task_id: taskId || null, task_type: taskType || null, steer_kind: kind, reason_code: kebab(opts.reason),
|
|
304
|
+
}, core.commonFields(ctx));
|
|
305
|
+
let out;
|
|
306
|
+
if (!ref) {
|
|
307
|
+
const taskId = String(opts.task);
|
|
308
|
+
const mine = agents.filter((a) => a.task_id === taskId);
|
|
309
|
+
if (!mine.length && !events.some((e) => e.task_id === taskId)) throw new Error(`Không thấy task ${taskId} trong log`);
|
|
310
|
+
const inFlight = mine.filter((a) => a.dispatched && !(a.completed && a.outcome));
|
|
311
|
+
const type = (mine.find((a) => a.role === 'exec') || mine[0] || {}).task_type || null;
|
|
312
|
+
out = inFlight.length ? inFlight.map((a) => mk(a.agent_id, taskId, a.task_type)) : [mk(`task:${taskId}`, taskId, type)];
|
|
313
|
+
} else {
|
|
314
|
+
const agentId = resolveAgentId(events, ref);
|
|
315
|
+
const agent = agents.find((a) => a.agent_id === agentId);
|
|
316
|
+
out = [mk(agentId, agent && agent.task_id, agent && agent.task_type)];
|
|
317
|
+
}
|
|
318
|
+
for (const ev of out) core.appendEvent(ctx, ev);
|
|
319
|
+
if (opts.json) return { code: 0, out: JSON.stringify(ref ? out[0] : out) };
|
|
320
|
+
const target = out[0].agent_id.startsWith('task:') ? `task ${out[0].task_id} (không có agent đang chạy)`
|
|
321
|
+
: `${out.length} agent${out[0].task_id ? ` của task ${out[0].task_id}` : ''}`;
|
|
322
|
+
return { code: 0, out: `Đã ghi steer ${kind} (${steerId}) cho ${target}.` };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ------------------------------------------------------------------ stats / retro
|
|
326
|
+
|
|
327
|
+
function lambdaOf(opts, ctx) {
|
|
328
|
+
const prof = chooser.loadProfile(skillDirOf(opts, ctx));
|
|
329
|
+
return { prof, lambda: prof.lambda.normal };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function pct(v) {
|
|
333
|
+
return v === null || v === undefined ? '-' : `${Math.round(v * 100)}%`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function cmdStats(opts) {
|
|
337
|
+
const ctx = ctxOf(opts);
|
|
338
|
+
const { lambda } = lambdaOf(opts, ctx);
|
|
339
|
+
const { cells, steers } = agg.analyze(core.readEvents(ctx), { lambda });
|
|
340
|
+
const type = typeof opts.type === 'string' ? opts.type : null;
|
|
341
|
+
const rows = type ? cells.filter((c) => c.task_type === type) : cells;
|
|
342
|
+
const steerRows = type ? steers.filter((s) => s.task_type === type) : steers;
|
|
343
|
+
if (opts.json) {
|
|
344
|
+
return { code: 0, out: JSON.stringify({ lambda_usd_per_min: lambda, min_n: agg.MIN_N, cells: rows, steers: steerRows }, null, 2) };
|
|
345
|
+
}
|
|
346
|
+
if (!rows.length) return { code: 0, out: 'Chưa có dữ liệu.' };
|
|
347
|
+
const t = [['task_type', 'model/effort', 'n', 'accept%', 'agent_pass%', 'E[cost]', 'E[time]', 'p50', 'p75', 'score', '']];
|
|
348
|
+
for (const c of rows) {
|
|
349
|
+
t.push([
|
|
350
|
+
c.task_type,
|
|
351
|
+
`${chooser.shortModel(c.model) || c.model || '?'}/${c.effort || '?'}`,
|
|
352
|
+
`${c.n}${c.dispatched > c.n ? `/${c.dispatched}` : ''}${c.n_excluded ? ` (-${c.n_excluded})` : ''}`,
|
|
353
|
+
c.n ? `${pct(c.accept_rate)}${c.n_user ? ` (${c.n_user} ng.dùng)` : ''}` : '-',
|
|
354
|
+
c.n_agent ? `${pct(c.agent_pass_rate)} (${c.n_agent})` : '-',
|
|
355
|
+
fmtUsd(c.cost_mean_usd),
|
|
356
|
+
c.time_mean_min === null ? '-' : `${fmtNum(c.time_mean_min)}m`,
|
|
357
|
+
c.time_p50_min === null ? '-' : `${fmtNum(c.time_p50_min)}m`,
|
|
358
|
+
c.time_p75_min === null ? '-' : `${fmtNum(c.time_p75_min)}m`,
|
|
359
|
+
c.score === null ? '-' : fmtNum(c.score, 3),
|
|
360
|
+
c.note,
|
|
361
|
+
]);
|
|
362
|
+
}
|
|
363
|
+
const s = [['task_type', 'task', 'task có steer', 'lần steer', 'minor', 'plan', 'redirect', 'tỉ lệ steer']];
|
|
364
|
+
for (const r of steerRows) {
|
|
365
|
+
s.push([r.task_type, r.tasks, r.steered_tasks, r.actions, r.minor, r.plan, r.redirect, pct(r.steer_rate)]);
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
code: 0,
|
|
369
|
+
out: `${table(t)}\n\n${table(s)}\n\n`
|
|
370
|
+
+ 'n = task tính tỉ lệ (/số đã giao, (-k) = bị loại: reject-scope hoặc lượt exec đầu bị steer plan/redirect). '
|
|
371
|
+
+ '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. '
|
|
372
|
+
+ `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.`,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const HIGH_STEER_RATE = 0.3;
|
|
377
|
+
const HIGH_STEER_MIN_TASKS = 3;
|
|
378
|
+
|
|
379
|
+
function cmdRetro(opts) {
|
|
380
|
+
const ctx = ctxOf(opts);
|
|
381
|
+
const { prof, lambda } = lambdaOf(opts, ctx);
|
|
382
|
+
const events = core.readEvents(ctx);
|
|
383
|
+
const { agents, tasks, cells, steers, userVerdicts, overrides } = agg.analyze(events, { lambda });
|
|
384
|
+
const reasons = {};
|
|
385
|
+
const outcomes = {};
|
|
386
|
+
const userOutcomes = {};
|
|
387
|
+
for (const a of agents) {
|
|
388
|
+
if (a.outcome) outcomes[a.outcome] = (outcomes[a.outcome] || 0) + 1;
|
|
389
|
+
if (a.reason_code) reasons[a.reason_code] = (reasons[a.reason_code] || 0) + 1;
|
|
390
|
+
}
|
|
391
|
+
for (const v of userVerdicts) {
|
|
392
|
+
const k = v.outcome === 'reject' ? `reject_${v.cause}` : v.outcome;
|
|
393
|
+
userOutcomes[k] = (userOutcomes[k] || 0) + 1;
|
|
394
|
+
if (v.reason_code) reasons[v.reason_code] = (reasons[v.reason_code] || 0) + 1;
|
|
395
|
+
}
|
|
396
|
+
const types = {};
|
|
397
|
+
for (const t of tasks) {
|
|
398
|
+
if (!types[t.task_type]) types[t.task_type] = { task_type: t.task_type, n: 0, verdicted: 0, accepted: 0 };
|
|
399
|
+
types[t.task_type].n++;
|
|
400
|
+
if (t.success !== null) types[t.task_type].verdicted++;
|
|
401
|
+
if (t.accepted) types[t.task_type].accepted++;
|
|
402
|
+
}
|
|
403
|
+
const ts = events.map((e) => Date.parse(e.ts)).filter((x) => !isNaN(x));
|
|
404
|
+
const escalations = tasks.filter((t) => t.attempts.length > 1).length;
|
|
405
|
+
const highSteer = steers.filter((s) => s.tasks >= HIGH_STEER_MIN_TASKS && s.steer_rate >= HIGH_STEER_RATE)
|
|
406
|
+
.map((s) => Object.assign({}, s, { signal: 'brainstorm hỏi người dùng quá ít trước khi giao' }));
|
|
407
|
+
const summary = {
|
|
408
|
+
generated_at: core.nowIso(),
|
|
409
|
+
core_version: core.CORE_VERSION,
|
|
410
|
+
dept: ctx.config.dept || prof.dept || null,
|
|
411
|
+
period: ts.length ? { from: new Date(Math.min(...ts)).toISOString(), to: new Date(Math.max(...ts)).toISOString() } : null,
|
|
412
|
+
totals: {
|
|
413
|
+
events: events.length,
|
|
414
|
+
agents: agents.length,
|
|
415
|
+
tasks: tasks.length,
|
|
416
|
+
untagged_agents: agents.filter((a) => a.task_type === 'untagged').length,
|
|
417
|
+
no_verdict_agents: agents.filter((a) => a.completed && !a.outcome && a.role !== 'verify').length,
|
|
418
|
+
no_user_verdict_tasks: tasks.filter((t) => !t.user_verdicts.length).length,
|
|
419
|
+
multi_attempt_tasks: escalations,
|
|
420
|
+
overrides: overrides.length,
|
|
421
|
+
cost_usd: agents.reduce((s, a) => s + (a.cost_usd || 0), 0),
|
|
422
|
+
},
|
|
423
|
+
lambda_usd_per_min: prof.lambda,
|
|
424
|
+
min_n: agg.MIN_N,
|
|
425
|
+
task_types: Object.values(types).sort((a, b) => b.n - a.n),
|
|
426
|
+
outcomes,
|
|
427
|
+
user_outcomes: userOutcomes,
|
|
428
|
+
reason_codes: Object.entries(reasons).sort((a, b) => b[1] - a[1]).map(([code, count]) => ({ code, count })),
|
|
429
|
+
steers,
|
|
430
|
+
high_steer_task_types: highSteer,
|
|
431
|
+
overrides: overrides.map((o) => ({ ts: o.ts, task_type: o.task_type, suggested_tier: o.suggested_tier, chosen_tier: o.chosen_tier })),
|
|
432
|
+
cells,
|
|
433
|
+
};
|
|
434
|
+
if (opts.json) return { code: 0, out: JSON.stringify(summary, null, 2) };
|
|
435
|
+
const lines = [
|
|
436
|
+
`Retro ${summary.dept || ''} — ${summary.period ? `${summary.period.from.slice(0, 10)} → ${summary.period.to.slice(0, 10)}` : 'chưa có dữ liệu'}`,
|
|
437
|
+
`${summary.totals.tasks} task, ${summary.totals.agents} agent, chi phí ≈ ${fmtUsd(summary.totals.cost_usd)}, ${summary.totals.untagged_agents} agent không thẻ, ${summary.totals.no_user_verdict_tasks} task chưa có verdict người dùng, ${escalations} task nhiều lượt, ${overrides.length} override.`,
|
|
438
|
+
`Người dùng: ${Object.entries(userOutcomes).map(([k, v]) => `${k}=${v}`).join(', ') || '-'} · Agent: ${Object.entries(outcomes).map(([k, v]) => `${k}=${v}`).join(', ') || '-'}`,
|
|
439
|
+
`Lý do hay gặp: ${summary.reason_codes.slice(0, 8).map((r) => `${r.code}×${r.count}`).join(', ') || '-'}`,
|
|
440
|
+
`Loại task: ${summary.task_types.map((t) => `${t.task_type}(${t.n})`).join(', ') || '-'}`,
|
|
441
|
+
`Steer nhiều (brainstorm hỏi quá ít): ${highSteer.map((s) => `${s.task_type} ${pct(s.steer_rate)}`).join(', ') || '-'}`,
|
|
442
|
+
'Chi tiết theo ô: `dieu-phoi stats`; đưa cho LLM: `dieu-phoi retro --json`.',
|
|
443
|
+
];
|
|
444
|
+
return { code: 0, out: lines.join('\n') };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ------------------------------------------------------------------ choose
|
|
448
|
+
|
|
449
|
+
function chooseInput(opts, ctx, events) {
|
|
450
|
+
return {
|
|
451
|
+
type: opts.type,
|
|
452
|
+
score: opts.score,
|
|
453
|
+
urgency: typeof opts.urgency === 'string' ? opts.urgency : 'normal',
|
|
454
|
+
reversible: yesNo(opts.reversible, true),
|
|
455
|
+
verifiable: yesNo(opts.verifiable, true),
|
|
456
|
+
role: typeof opts.role === 'string' ? opts.role : 'exec',
|
|
457
|
+
seed: opts.seed,
|
|
458
|
+
taskId: typeof opts.task === 'string' ? opts.task : null,
|
|
459
|
+
override: typeof opts.override === 'string' ? opts.override : null,
|
|
460
|
+
approved: typeof opts.approved === 'string' ? opts.approved : null,
|
|
461
|
+
events,
|
|
462
|
+
profile: chooser.loadProfile(skillDirOf(opts, ctx)),
|
|
463
|
+
pricing: core.loadPricing(ctx),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function cmdChoose(opts) {
|
|
468
|
+
const ctx = ctxOf(opts);
|
|
469
|
+
if (opts.override !== undefined && typeof opts.task !== 'string') {
|
|
470
|
+
throw new Error('--override cần --task <task_id> (để học override theo từng task)');
|
|
471
|
+
}
|
|
472
|
+
const input = chooseInput(opts, ctx, core.readEvents(ctx));
|
|
473
|
+
const res = chooser.choose(input);
|
|
474
|
+
if (res.overridden) {
|
|
475
|
+
const tid = input.taskId;
|
|
476
|
+
core.appendEvent(ctx, Object.assign({
|
|
477
|
+
schema: core.SCHEMA,
|
|
478
|
+
kind: 'override',
|
|
479
|
+
ts: core.nowIso(),
|
|
480
|
+
agent_id: tid ? `task:${tid}` : `override-${require('crypto').randomBytes(6).toString('hex')}`,
|
|
481
|
+
task_id: tid,
|
|
482
|
+
task_type: String(input.type).toLowerCase(),
|
|
483
|
+
suggested_tier: res.suggested_tier,
|
|
484
|
+
chosen_tier: res.tier,
|
|
485
|
+
}, core.commonFields(ctx)));
|
|
486
|
+
}
|
|
487
|
+
if (opts.json) return { code: 0, out: JSON.stringify(res) };
|
|
488
|
+
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ô'})`;
|
|
489
|
+
if (res.action === 'ask_user') {
|
|
490
|
+
return { code: 0, out: `HỎI NGƯỜI DÙNG — ${res.reason}` };
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
code: 0,
|
|
494
|
+
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}`,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ------------------------------------------------------------------ plan-table
|
|
499
|
+
|
|
500
|
+
/** Longest path through the step DAG; steps without depends_on run in parallel. */
|
|
501
|
+
function criticalPath(steps, timeOf) {
|
|
502
|
+
const byName = new Map(steps.map((s) => [s.step, s]));
|
|
503
|
+
const finish = new Map();
|
|
504
|
+
const visiting = new Set();
|
|
505
|
+
const visit = (name) => {
|
|
506
|
+
if (finish.has(name)) return finish.get(name);
|
|
507
|
+
if (visiting.has(name)) throw new Error(`plan-table: phụ thuộc vòng tại bước "${name}"`);
|
|
508
|
+
const s = byName.get(name);
|
|
509
|
+
if (!s) throw new Error(`plan-table: không có bước "${name}" (trong depends_on)`);
|
|
510
|
+
visiting.add(name);
|
|
511
|
+
let start = 0;
|
|
512
|
+
for (const d of s.deps) start = Math.max(start, visit(d));
|
|
513
|
+
visiting.delete(name);
|
|
514
|
+
const f = start + (timeOf(name) || 0);
|
|
515
|
+
finish.set(name, f);
|
|
516
|
+
return f;
|
|
517
|
+
};
|
|
518
|
+
let total = 0;
|
|
519
|
+
for (const s of steps) total = Math.max(total, visit(s.step));
|
|
520
|
+
return total;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function buildPlanTable(rawSteps, base) {
|
|
524
|
+
if (!Array.isArray(rawSteps) || !rawSteps.length) throw new Error('plan-table: file phải là mảng JSON các bước');
|
|
525
|
+
const steps = rawSteps.map((s, i) => {
|
|
526
|
+
if (!s || typeof s !== 'object') throw new Error(`plan-table: bước #${i + 1} không phải object`);
|
|
527
|
+
const rawName = s.step !== undefined ? s.step : s.id;
|
|
528
|
+
const name = String(rawName !== undefined ? rawName : i + 1);
|
|
529
|
+
let deps = s.depends_on === undefined || s.depends_on === null ? [] : s.depends_on;
|
|
530
|
+
if (!Array.isArray(deps)) deps = [deps];
|
|
531
|
+
return {
|
|
532
|
+
step: name,
|
|
533
|
+
title: s.title,
|
|
534
|
+
type: s.type !== undefined ? s.type : s.task_type,
|
|
535
|
+
task: s.task_id,
|
|
536
|
+
override: s.override,
|
|
537
|
+
role: s.role,
|
|
538
|
+
score: s.score,
|
|
539
|
+
verifiable: s.verifiable === undefined ? true : yesNo(s.verifiable, true),
|
|
540
|
+
reversible: s.reversible === undefined ? true : yesNo(s.reversible, true),
|
|
541
|
+
urgency: s.urgency,
|
|
542
|
+
deps: deps.map(String),
|
|
543
|
+
};
|
|
544
|
+
});
|
|
545
|
+
const names = new Set();
|
|
546
|
+
for (const s of steps) {
|
|
547
|
+
if (names.has(s.step)) throw new Error(`plan-table: trùng tên bước "${s.step}"`);
|
|
548
|
+
names.add(s.step);
|
|
549
|
+
}
|
|
550
|
+
const rows = steps.map((s) => {
|
|
551
|
+
const call = () => chooser.choose(Object.assign({}, base, {
|
|
552
|
+
type: s.type,
|
|
553
|
+
score: s.score,
|
|
554
|
+
verifiable: s.verifiable,
|
|
555
|
+
reversible: s.reversible,
|
|
556
|
+
urgency: s.urgency || base.urgency,
|
|
557
|
+
taskId: s.task || base.taskId,
|
|
558
|
+
override: s.override || base.override,
|
|
559
|
+
role: s.role || base.role,
|
|
560
|
+
seed: base.seed === undefined || base.seed === null ? `plan:${s.step}` : `${base.seed}:${s.step}`,
|
|
561
|
+
}));
|
|
562
|
+
const head = { step: s.step, title: s.title, type: s.type, score: Number(s.score), depends_on: s.deps };
|
|
563
|
+
try {
|
|
564
|
+
return Object.assign(head, call());
|
|
565
|
+
} catch (e) {
|
|
566
|
+
// One bad step (e.g. override below a floor) must not hide the rest of the plan.
|
|
567
|
+
return Object.assign(head, {
|
|
568
|
+
action: 'refused', tier: null, reason: `từ chối: ${e.message}`, error: e.message,
|
|
569
|
+
est_cost_usd: null, est_time_min: null, est_source: null,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
const timeOf = new Map(rows.map((r) => [r.step, r.est_time_min]));
|
|
574
|
+
const critical = criticalPath(steps, (n) => timeOf.get(n));
|
|
575
|
+
const ok = rows.filter((r) => r.action !== 'refused');
|
|
576
|
+
const costs = ok.map((r) => r.est_cost_usd);
|
|
577
|
+
const totalCost = costs.every((c) => typeof c === 'number') ? costs.reduce((a, b) => a + b, 0) : null;
|
|
578
|
+
return {
|
|
579
|
+
steps: rows,
|
|
580
|
+
refused: rows.filter((r) => r.action === 'refused').map((r) => ({ step: r.step, error: r.error })),
|
|
581
|
+
total_cost_usd: totalCost === null ? null : Math.round(totalCost * 1000) / 1000,
|
|
582
|
+
critical_path_min: Math.round(critical * 10) / 10,
|
|
583
|
+
sum_time_min: Math.round(rows.reduce((a, r) => a + (r.est_time_min || 0), 0) * 10) / 10,
|
|
584
|
+
rough: rows.some((r) => r.est_source === 'rough'),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function mdCell(s) {
|
|
589
|
+
return String(s).replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function cmdPlanTable(opts) {
|
|
593
|
+
if (typeof opts.file !== 'string') throw new Error('Cú pháp: dieu-phoi plan-table --file <steps.json> [--json]');
|
|
594
|
+
let raw;
|
|
595
|
+
try {
|
|
596
|
+
raw = JSON.parse(fs.readFileSync(path.resolve(opts.file), 'utf8').replace(/^/, ''));
|
|
597
|
+
} catch (e) {
|
|
598
|
+
throw new Error(`plan-table: không đọc được ${opts.file}: ${e.message}`);
|
|
599
|
+
}
|
|
600
|
+
const ctx = ctxOf(opts);
|
|
601
|
+
const base = chooseInput(Object.assign({}, opts, { type: 'x', score: 0, override: undefined }), ctx, core.readEvents(ctx));
|
|
602
|
+
delete base.override;
|
|
603
|
+
const plan = buildPlanTable(raw, base);
|
|
604
|
+
if (opts.json) return { code: 0, out: JSON.stringify(plan, null, 2) };
|
|
605
|
+
const lines = ['| Bước | Model/effort | Lý do | Ước tính |', '|---|---|---|---|'];
|
|
606
|
+
for (const r of plan.steps) {
|
|
607
|
+
const tierCell = r.action === 'refused' ? '—' : (r.action === 'ask_user' ? `${r.tier} (hỏi người dùng)` : r.tier);
|
|
608
|
+
const why = `${r.type}, điểm ${r.score}${r.depends_on.length ? `, sau ${r.depends_on.join(', ')}` : ''} — ${r.reason.replace(/; profile: .*$/, '')}`;
|
|
609
|
+
const est = r.action === 'refused' ? '-' : `~${fmtUsd(r.est_cost_usd)} · ~${fmtNum(r.est_time_min)} phút${r.est_source === 'rough' ? ' (thô)' : ''}`;
|
|
610
|
+
lines.push(`| ${mdCell(r.title ? `${r.step}. ${r.title}` : r.step)} | ${mdCell(tierCell)} | ${mdCell(why)} | ${mdCell(est)} |`);
|
|
611
|
+
}
|
|
612
|
+
lines.push('');
|
|
613
|
+
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)`);
|
|
614
|
+
if (plan.rough) lines.push('', '_(thô) = ô chưa đủ 5 task, ước lượng theo giả định token của từng tier._');
|
|
615
|
+
if (plan.refused.length) {
|
|
616
|
+
lines.push('', `**Cảnh báo:** ${plan.refused.length} bước bị từ chối (${plan.refused.map((x) => x.step).join(', ')}), không tính vào tổng — sửa rồi chạy lại.`);
|
|
617
|
+
process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${plan.refused.length} bước bị từ chối
|
|
618
|
+
`);
|
|
619
|
+
}
|
|
620
|
+
return { code: 0, out: lines.join('\n') };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
module.exports = {
|
|
624
|
+
cmdSetup, cmdUninstall, cmdDoctor, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdStats, cmdRetro, cmdChoose,
|
|
625
|
+
cmdPlanTable, buildPlanTable, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
|
|
626
|
+
};
|