@kienntpixon/dieu-phoi 0.1.1 → 0.2.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 +24 -0
- package/README.md +14 -9
- package/bin/dieu-phoi.cjs +11 -2
- package/hook.cjs +606 -4
- package/lib/aggregate.cjs +68 -2
- package/lib/args.cjs +1 -1
- package/lib/choose.cjs +13 -1
- package/lib/commands.cjs +186 -22
- package/lib/setup.cjs +111 -7
- package/package.json +10 -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
|
@@ -55,8 +55,9 @@ function cmdSetup(opts) {
|
|
|
55
55
|
const lines = [
|
|
56
56
|
`Đã cài hook: ${res.hook}`,
|
|
57
57
|
res.settings_changed
|
|
58
|
-
? `Đã đăng ký PostToolUse(Agent) + SubagentStop trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
|
|
58
|
+
? `Đã đăng ký PostToolUse(Agent) + SubagentStop + SessionStart(compact) trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
|
|
59
59
|
: `settings.json đã có hook, không đổi gì (${res.settings})`,
|
|
60
|
+
`Thư mục báo cáo subagent: ${res.runs_dir} (quyền: ${res.permissions.allow.join(', ')} + additionalDirectories)`,
|
|
60
61
|
`Phòng ban: ${res.config.dept || '(chưa đặt, dùng --dept)'}`,
|
|
61
62
|
`Sink: ${res.config.sink_url ? res.config.sink_url : '(chưa có — log chỉ lưu cục bộ)'}`,
|
|
62
63
|
`Skill: ${res.config.skill_dir || '(không thấy; dùng --skill-dir)'}${res.config.skill_version ? ` v${res.config.skill_version}` : ''}`,
|
|
@@ -87,8 +88,12 @@ async function cmdDoctor(opts) {
|
|
|
87
88
|
add(major >= 18, 'node', `v${process.versions.node}${major >= 18 ? '' : ' (cần ≥ 18)'}`);
|
|
88
89
|
add(fs.existsSync(ctx.paths.hook), 'hook', ctx.paths.hook);
|
|
89
90
|
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})`);
|
|
91
|
+
add(reg.PostToolUse && reg.SubagentStop && reg.SessionStart, 'settings',
|
|
92
|
+
`PostToolUse(Agent)=${reg.PostToolUse ? 'có' : 'thiếu'}, SubagentStop=${reg.SubagentStop ? 'có' : 'thiếu'}, SessionStart(compact)=${reg.SessionStart ? 'có' : 'thiếu'} (${reg.file})`);
|
|
93
|
+
const permOk = reg.permissions && reg.runs_dir_exists && !reg.legacy_permissions;
|
|
94
|
+
add(permOk, 'run-permissions', permOk
|
|
95
|
+
? `${reg.permission_entries.allow.join(', ')} + additionalDirectories ${reg.permission_entries.dir}`
|
|
96
|
+
: `${!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
97
|
const hasCfg = fs.existsSync(ctx.paths.config);
|
|
93
98
|
add(hasCfg, 'config', hasCfg ? `${ctx.paths.config} dept=${ctx.config.dept || '-'}` : 'chưa có — chạy `dieu-phoi setup`');
|
|
94
99
|
const sd = ctx.config.skill_dir;
|
|
@@ -124,6 +129,10 @@ async function cmdDoctor(opts) {
|
|
|
124
129
|
add(true, 'internal-subagents', skipped && skipped.count
|
|
125
130
|
? `bỏ qua ${skipped.count} SubagentStop của subagent nội bộ Claude Code (lần cuối ${skipped.last_ts})`
|
|
126
131
|
: 'chưa bỏ qua SubagentStop nội bộ nào');
|
|
132
|
+
const orch = measureOrch(opts, ctx, profileOrNull(opts, ctx));
|
|
133
|
+
if (orch.ctx_tokens !== null) {
|
|
134
|
+
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');
|
|
135
|
+
}
|
|
127
136
|
if (ctx.config.sink_url && ctx.config.sink_token) {
|
|
128
137
|
try {
|
|
129
138
|
const u = new URL(ctx.config.sink_url);
|
|
@@ -141,7 +150,7 @@ async function cmdDoctor(opts) {
|
|
|
141
150
|
add(true, 'sink', 'chưa cấu hình (chỉ lưu cục bộ)');
|
|
142
151
|
}
|
|
143
152
|
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) };
|
|
153
|
+
if (opts.json) return { code: failed ? 1 : 0, out: JSON.stringify({ ok: !failed, runs_dir: reg.runs_dir, checks }, null, 2) };
|
|
145
154
|
const out = checks.map((c) => `[${c.level}] ${c.name}: ${c.detail}`).join('\n');
|
|
146
155
|
return { code: failed ? 1 : 0, out };
|
|
147
156
|
}
|
|
@@ -150,8 +159,9 @@ async function cmdDoctor(opts) {
|
|
|
150
159
|
|
|
151
160
|
function cmdHook(opts) {
|
|
152
161
|
const home = typeof opts['claude-dir'] === 'string' ? path.join(path.resolve(opts['claude-dir']), 'dieu-phoi') : undefined;
|
|
153
|
-
core.runHook({ home });
|
|
154
|
-
|
|
162
|
+
const r = core.runHook({ home });
|
|
163
|
+
// SessionStart(compact) returns the JSON to print; every other hook event prints nothing.
|
|
164
|
+
return { code: 0, out: r && typeof r.stdout === 'string' ? r.stdout : null };
|
|
155
165
|
}
|
|
156
166
|
|
|
157
167
|
async function cmdSync(opts) {
|
|
@@ -326,6 +336,76 @@ function cmdSteer(opts) {
|
|
|
326
336
|
return { code: 0, out: `Đã ghi steer ${kind} (${steerId}) cho ${target}.` };
|
|
327
337
|
}
|
|
328
338
|
|
|
339
|
+
// ------------------------------------------------------------------ ctx (orchestrator context)
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Measure the orchestrating session: --session, else env CLAUDE_CODE_SESSION_ID; transcript found under
|
|
343
|
+
* <claude dir>/projects/<project>/<id>.jsonl. Never throws; ctx_tokens null when it cannot measure.
|
|
344
|
+
*/
|
|
345
|
+
function measureOrch(opts, ctx, prof) {
|
|
346
|
+
const sid = typeof opts.session === 'string' && opts.session ? opts.session
|
|
347
|
+
: (process.env.CLAUDE_CODE_SESSION_ID || null);
|
|
348
|
+
const softCap = (prof && prof.orch_ctx_soft_cap) || chooser.DEFAULT_ORCH_CTX_SOFT_CAP;
|
|
349
|
+
let u = { ctx_tokens: null, ctx_baseline: null, ctx_growth: null, cum_usd: null, compactions: null };
|
|
350
|
+
try {
|
|
351
|
+
const file = sid ? core.findSessionTranscript(claudeDirOf(opts), sid) : null;
|
|
352
|
+
if (file) {
|
|
353
|
+
u = core.orchestratorUsage(file, core.loadPricing(ctx), { cacheFile: core.orchCacheFile(ctx.paths, sid, file) });
|
|
354
|
+
}
|
|
355
|
+
} catch (_) { /* measurement never fails the caller */ }
|
|
356
|
+
const over = typeof u.ctx_growth === 'number' && u.ctx_growth > softCap;
|
|
357
|
+
let advice;
|
|
358
|
+
if (!sid) advice = 'Không đo được: không có session (thiếu CLAUDE_CODE_SESSION_ID, dùng --session <id>).';
|
|
359
|
+
else if (u.ctx_tokens === null) advice = 'Không đo được: không thấy transcript của session này.';
|
|
360
|
+
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.`;
|
|
361
|
+
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.`;
|
|
362
|
+
return {
|
|
363
|
+
session_id: sid, ctx_tokens: u.ctx_tokens, ctx_baseline: u.ctx_baseline, ctx_growth: u.ctx_growth,
|
|
364
|
+
cum_usd: u.cum_usd, compactions: u.compactions, soft_cap: softCap, over, advice,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function profileOrNull(opts, ctx) {
|
|
369
|
+
try { return chooser.loadProfile(skillDirOf(opts, ctx)); } catch (_) { return null; }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function cmdCtx(opts) {
|
|
373
|
+
const ctx = ctxOf(opts);
|
|
374
|
+
const m = measureOrch(opts, ctx, profileOrNull(opts, ctx));
|
|
375
|
+
if (opts.json) return { code: 0, out: JSON.stringify(m) };
|
|
376
|
+
if (m.ctx_tokens === null) return { code: 0, out: m.advice };
|
|
377
|
+
return {
|
|
378
|
+
code: 0,
|
|
379
|
+
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' : ''}, `
|
|
380
|
+
+ `chi phí điều phối ≈ ${fmtUsd(m.cum_usd)}, đã nén ${m.compactions === null ? '?' : m.compactions} lần\n${m.advice}`,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function orchLine(o) {
|
|
385
|
+
if (!o || !o.sessions) return 'Điều phối: chưa có dispatch nào ghi session_id (cần core ≥ 0.2.0).';
|
|
386
|
+
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}), `
|
|
387
|
+
+ `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).`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ------------------------------------------------------------------ state (re-injected after /compact)
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Orchestration state of one session rebuilt purely from events.jsonl (same builder the SessionStart(compact)
|
|
394
|
+
* hook uses). Session = --session, else env CLAUDE_CODE_SESSION_ID. Run dirs are pointed to, never read.
|
|
395
|
+
*/
|
|
396
|
+
function cmdState(opts) {
|
|
397
|
+
const ctx = ctxOf(opts);
|
|
398
|
+
const sid = typeof opts.session === 'string' && opts.session ? opts.session
|
|
399
|
+
: (process.env.CLAUDE_CODE_SESSION_ID || null);
|
|
400
|
+
if (!sid) {
|
|
401
|
+
if (opts.json) return { code: 0, out: JSON.stringify({ session_id: null, generated_at: core.nowIso(), runs_dir: null, plans: [], tasks: [] }) };
|
|
402
|
+
return { code: 0, out: 'Không có session (thiếu CLAUDE_CODE_SESSION_ID, dùng --session <id>).' };
|
|
403
|
+
}
|
|
404
|
+
const state = core.buildSessionState(core.readEvents(ctx), sid, { plansDir: ctx.paths.plans });
|
|
405
|
+
if (opts.json) return { code: 0, out: JSON.stringify(state, null, 2) };
|
|
406
|
+
return { code: 0, out: core.formatSessionState(state, core.STATE_TEXT_CAP) };
|
|
407
|
+
}
|
|
408
|
+
|
|
329
409
|
// ------------------------------------------------------------------ stats / retro
|
|
330
410
|
|
|
331
411
|
function lambdaOf(opts, ctx) {
|
|
@@ -340,14 +420,16 @@ function pct(v) {
|
|
|
340
420
|
function cmdStats(opts) {
|
|
341
421
|
const ctx = ctxOf(opts);
|
|
342
422
|
const { lambda } = lambdaOf(opts, ctx);
|
|
343
|
-
const { cells, steers } = agg.analyze(core.readEvents(ctx), { lambda });
|
|
423
|
+
const { cells, steers, orchestration } = agg.analyze(core.readEvents(ctx), { lambda });
|
|
344
424
|
const type = typeof opts.type === 'string' ? opts.type : null;
|
|
345
425
|
const rows = type ? cells.filter((c) => c.task_type === type) : cells;
|
|
346
426
|
const steerRows = type ? steers.filter((s) => s.task_type === type) : steers;
|
|
347
427
|
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) };
|
|
428
|
+
return { code: 0, out: JSON.stringify({ lambda_usd_per_min: lambda, min_n: agg.MIN_N, cells: rows, steers: steerRows, orchestration }, null, 2) };
|
|
429
|
+
}
|
|
430
|
+
if (!rows.length) {
|
|
431
|
+
return { code: 0, out: orchestration.sessions ? `Chưa có dữ liệu theo ô.\n${orchLine(orchestration)}` : 'Chưa có dữ liệu.' };
|
|
349
432
|
}
|
|
350
|
-
if (!rows.length) return { code: 0, out: 'Chưa có dữ liệu.' };
|
|
351
433
|
const t = [['task_type', 'model/effort', 'n', 'accept%', 'agent_pass%', 'E[cost]', 'E[time]', 'p50', 'p75', 'score', '']];
|
|
352
434
|
for (const c of rows) {
|
|
353
435
|
t.push([
|
|
@@ -370,7 +452,7 @@ function cmdStats(opts) {
|
|
|
370
452
|
}
|
|
371
453
|
return {
|
|
372
454
|
code: 0,
|
|
373
|
-
out: `${table(t)}\n\n${table(s)}\n\n`
|
|
455
|
+
out: `${table(t)}\n\n${table(s)}\n\n${orchLine(orchestration)}\n\n`
|
|
374
456
|
+ '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
457
|
+ '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
458
|
+ `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 +466,7 @@ function cmdRetro(opts) {
|
|
|
384
466
|
const ctx = ctxOf(opts);
|
|
385
467
|
const { prof, lambda } = lambdaOf(opts, ctx);
|
|
386
468
|
const events = core.readEvents(ctx);
|
|
387
|
-
const { agents, tasks, cells, steers, userVerdicts, overrides } = agg.analyze(events, { lambda });
|
|
469
|
+
const { agents, tasks, cells, steers, userVerdicts, overrides, orchestration } = agg.analyze(events, { lambda });
|
|
388
470
|
const reasons = {};
|
|
389
471
|
const outcomes = {};
|
|
390
472
|
const userOutcomes = {};
|
|
@@ -433,6 +515,7 @@ function cmdRetro(opts) {
|
|
|
433
515
|
steers,
|
|
434
516
|
high_steer_task_types: highSteer,
|
|
435
517
|
overrides: overrides.map((o) => ({ ts: o.ts, task_type: o.task_type, suggested_tier: o.suggested_tier, chosen_tier: o.chosen_tier })),
|
|
518
|
+
orchestration,
|
|
436
519
|
cells,
|
|
437
520
|
};
|
|
438
521
|
if (opts.json) return { code: 0, out: JSON.stringify(summary, null, 2) };
|
|
@@ -443,6 +526,7 @@ function cmdRetro(opts) {
|
|
|
443
526
|
`Lý do hay gặp: ${summary.reason_codes.slice(0, 8).map((r) => `${r.code}×${r.count}`).join(', ') || '-'}`,
|
|
444
527
|
`Loại task: ${summary.task_types.map((t) => `${t.task_type}(${t.n})`).join(', ') || '-'}`,
|
|
445
528
|
`Steer nhiều (brainstorm hỏi quá ít): ${highSteer.map((s) => `${s.task_type} ${pct(s.steer_rate)}`).join(', ') || '-'}`,
|
|
529
|
+
orchLine(orchestration),
|
|
446
530
|
'Chi tiết theo ô: `dieu-phoi stats`; đưa cho LLM: `dieu-phoi retro --json`.',
|
|
447
531
|
];
|
|
448
532
|
return { code: 0, out: lines.join('\n') };
|
|
@@ -475,6 +559,10 @@ function cmdChoose(opts) {
|
|
|
475
559
|
}
|
|
476
560
|
const input = chooseInput(opts, ctx, core.readEvents(ctx));
|
|
477
561
|
const res = chooser.choose(input);
|
|
562
|
+
const orch = measureOrch(opts, ctx, input.profile);
|
|
563
|
+
res.orch_ctx_tokens = orch.ctx_tokens;
|
|
564
|
+
res.orch_ctx_growth = orch.ctx_growth;
|
|
565
|
+
res.orch_ctx_over = orch.over;
|
|
478
566
|
if (res.overridden) {
|
|
479
567
|
const tid = input.taskId;
|
|
480
568
|
core.appendEvent(ctx, Object.assign({
|
|
@@ -489,13 +577,14 @@ function cmdChoose(opts) {
|
|
|
489
577
|
}, core.commonFields(ctx)));
|
|
490
578
|
}
|
|
491
579
|
if (opts.json) return { code: 0, out: JSON.stringify(res) };
|
|
492
|
-
const
|
|
580
|
+
const warn = orch.over ? `\nCẢNH BÁO: ${orch.advice}` : '';
|
|
581
|
+
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
582
|
if (res.action === 'ask_user') {
|
|
494
|
-
return { code: 0, out: `HỎI NGƯỜI DÙNG — ${res.reason}` };
|
|
583
|
+
return { code: 0, out: `HỎI NGƯỜI DÙNG — ${res.reason}${warn}` };
|
|
495
584
|
}
|
|
496
585
|
return {
|
|
497
586
|
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}`,
|
|
587
|
+
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
588
|
};
|
|
500
589
|
}
|
|
501
590
|
|
|
@@ -563,7 +652,11 @@ function buildPlanTable(rawSteps, base) {
|
|
|
563
652
|
role: s.role || base.role,
|
|
564
653
|
seed: base.seed === undefined || base.seed === null ? `plan:${s.step}` : `${base.seed}:${s.step}`,
|
|
565
654
|
}));
|
|
566
|
-
|
|
655
|
+
// Rubric inputs that drive the safety floors: shown to the user at gate 1 so mis-scoring is caught.
|
|
656
|
+
const head = {
|
|
657
|
+
step: s.step, title: s.title, type: s.type, task_id: s.task || null, score: Number(s.score),
|
|
658
|
+
verifiable: s.verifiable, reversible: s.reversible, depends_on: s.deps,
|
|
659
|
+
};
|
|
567
660
|
try {
|
|
568
661
|
return Object.assign(head, call());
|
|
569
662
|
} catch (e) {
|
|
@@ -589,12 +682,71 @@ function buildPlanTable(rawSteps, base) {
|
|
|
589
682
|
};
|
|
590
683
|
}
|
|
591
684
|
|
|
685
|
+
const ROOT_RE = /^[A-Za-z0-9_-]{1,32}$/;
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Record the plan the user approved at gate 1: <data>/plans/<root>.json (local only — not an event, never
|
|
689
|
+
* synced). Each re-approval increments `approval` (number of approvals for this root) and overwrites the
|
|
690
|
+
* record; earlier approvals move to `history`. Not the user-verdict `round`.
|
|
691
|
+
* tier = exactly what the approved table shows (override applied); refused steps -> null.
|
|
692
|
+
*/
|
|
693
|
+
function prevApproval(prev) {
|
|
694
|
+
if (!prev || typeof prev !== 'object') return 0;
|
|
695
|
+
const n = Number.isInteger(prev.approval) ? prev.approval : prev.round; // `round`: 0.2.0 dev records
|
|
696
|
+
return Number.isInteger(n) && n > 0 ? n : 0;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Steps that cannot be matched to dispatches once approved: missing task_id, a task_id used twice, or a
|
|
701
|
+
* task_id equal to the root. Returns Vietnamese problem strings (empty = ok).
|
|
702
|
+
*/
|
|
703
|
+
function planTaskIdProblems(plan, root) {
|
|
704
|
+
const problems = [];
|
|
705
|
+
const missing = plan.steps.filter((r) => !r.task_id).map((r) => r.step);
|
|
706
|
+
if (missing.length) problems.push(`thiếu task_id ở bước ${missing.join(', ')}`);
|
|
707
|
+
const by = new Map();
|
|
708
|
+
for (const r of plan.steps) if (r.task_id) by.set(r.task_id, (by.get(r.task_id) || []).concat(r.step));
|
|
709
|
+
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(', ')}`);
|
|
710
|
+
if (root) {
|
|
711
|
+
const same = plan.steps.filter((r) => r.task_id === root).map((r) => r.step);
|
|
712
|
+
if (same.length) problems.push(`task_id của bước ${same.join(', ')} trùng --root "${root}" (root dành cho scout/planner)`);
|
|
713
|
+
}
|
|
714
|
+
return problems;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function recordApprovedPlan(ctx, root, stepsFile, plan) {
|
|
718
|
+
const file = path.join(ctx.paths.plans, `${root}.json`);
|
|
719
|
+
const prev = core.readJsonFile(file, null);
|
|
720
|
+
let history = [];
|
|
721
|
+
if (prev && typeof prev === 'object' && !Array.isArray(prev)) {
|
|
722
|
+
const old = Object.assign({}, prev);
|
|
723
|
+
delete old.history;
|
|
724
|
+
history = (Array.isArray(prev.history) ? prev.history : []).concat([old]);
|
|
725
|
+
}
|
|
726
|
+
const rec = {
|
|
727
|
+
root_task_id: root,
|
|
728
|
+
steps_file: stepsFile.split(path.sep).join('/'),
|
|
729
|
+
approved_at: core.nowIso(),
|
|
730
|
+
approval: prevApproval(prev) + 1,
|
|
731
|
+
steps: plan.steps.map((r) => ({
|
|
732
|
+
id: r.step, task_id: r.task_id || null, title: r.title === undefined ? null : r.title, type: r.type || null,
|
|
733
|
+
tier: r.action === 'refused' ? null : (r.tier || null), depends_on: r.depends_on,
|
|
734
|
+
})),
|
|
735
|
+
history,
|
|
736
|
+
};
|
|
737
|
+
core.writeFileAtomic(file, `${JSON.stringify(rec, null, 2)}\n`);
|
|
738
|
+
return Object.assign({ file }, rec);
|
|
739
|
+
}
|
|
740
|
+
|
|
592
741
|
function mdCell(s) {
|
|
593
742
|
return String(s).replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
|
|
594
743
|
}
|
|
595
744
|
|
|
596
745
|
function cmdPlanTable(opts) {
|
|
597
|
-
if (typeof opts.file !== 'string') throw new Error('Cú pháp: dieu-phoi plan-table --file <steps.json> [--json]');
|
|
746
|
+
if (typeof opts.file !== 'string') throw new Error('Cú pháp: dieu-phoi plan-table --file <steps.json> [--json] [--approve --root <root_task_id>]');
|
|
747
|
+
if (opts.approve && (typeof opts.root !== 'string' || !ROOT_RE.test(opts.root))) {
|
|
748
|
+
throw new Error('--approve cần --root <root_task_id> (chữ, số, - _; tối đa 32 ký tự)');
|
|
749
|
+
}
|
|
598
750
|
let raw;
|
|
599
751
|
try {
|
|
600
752
|
raw = JSON.parse(fs.readFileSync(path.resolve(opts.file), 'utf8').replace(/^/, ''));
|
|
@@ -605,13 +757,24 @@ function cmdPlanTable(opts) {
|
|
|
605
757
|
const base = chooseInput(Object.assign({}, opts, { type: 'x', score: 0, override: undefined }), ctx, core.readEvents(ctx));
|
|
606
758
|
delete base.override;
|
|
607
759
|
const plan = buildPlanTable(raw, base);
|
|
608
|
-
|
|
609
|
-
|
|
760
|
+
const problems = planTaskIdProblems(plan, opts.approve ? String(opts.root) : null);
|
|
761
|
+
if (opts.approve && problems.length) {
|
|
762
|
+
throw new Error(`plan-table --approve: không ghi kế hoạch — ${problems.join('; ')}. Mỗi bước cần task_id riêng, khác --root.`);
|
|
763
|
+
}
|
|
764
|
+
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`);
|
|
765
|
+
const rec = opts.approve ? recordApprovedPlan(ctx, String(opts.root), path.resolve(opts.file), plan) : null;
|
|
766
|
+
if (opts.json) {
|
|
767
|
+
const out = rec ? Object.assign({}, plan, { approved: { root_task_id: rec.root_task_id, approval: rec.approval, record: rec.file } }) : plan;
|
|
768
|
+
return { code: 0, out: JSON.stringify(out, null, 2) };
|
|
769
|
+
}
|
|
770
|
+
const lines = ['| Bước | Model/effort | Rubric (điểm · kiểm chứng · đảo ngược) | Lý do | Ước tính |', '|---|---|---|---|---|'];
|
|
610
771
|
for (const r of plan.steps) {
|
|
611
772
|
const tierCell = r.action === 'refused' ? '—' : (r.action === 'ask_user' ? `${r.tier} (hỏi người dùng)` : r.tier);
|
|
612
|
-
const
|
|
773
|
+
const yn = (v) => (v ? 'y' : 'n');
|
|
774
|
+
const rubric = `${Number.isFinite(r.score) ? r.score : '?'} · ${yn(r.verifiable)} · ${yn(r.reversible)}`;
|
|
775
|
+
const why = `${r.type}${r.depends_on.length ? `, sau ${r.depends_on.join(', ')}` : ''} — ${r.reason.replace(/; profile: .*$/, '')}`;
|
|
613
776
|
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)} |`);
|
|
777
|
+
lines.push(`| ${mdCell(r.title ? `${r.step}. ${r.title}` : r.step)} | ${mdCell(tierCell)} | ${mdCell(rubric)} | ${mdCell(why)} | ${mdCell(est)} |`);
|
|
615
778
|
}
|
|
616
779
|
lines.push('');
|
|
617
780
|
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 +784,11 @@ function cmdPlanTable(opts) {
|
|
|
621
784
|
process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${plan.refused.length} bước bị từ chối
|
|
622
785
|
`);
|
|
623
786
|
}
|
|
787
|
+
if (rec) lines.push('', `Đã ghi kế hoạch đã duyệt (root ${rec.root_task_id}, lần duyệt ${rec.approval})`);
|
|
624
788
|
return { code: 0, out: lines.join('\n') };
|
|
625
789
|
}
|
|
626
790
|
|
|
627
791
|
module.exports = {
|
|
628
|
-
cmdSetup, cmdUninstall, cmdDoctor, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdStats, cmdRetro, cmdChoose,
|
|
629
|
-
cmdPlanTable, buildPlanTable, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
|
|
792
|
+
cmdSetup, cmdUninstall, cmdDoctor, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdStats, cmdRetro, cmdChoose, cmdCtx, cmdState, measureOrch,
|
|
793
|
+
cmdPlanTable, buildPlanTable, recordApprovedPlan, planTaskIdProblems, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
|
|
630
794
|
};
|
package/lib/setup.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const core = require('../hook.cjs');
|
|
6
7
|
|
|
@@ -70,10 +71,11 @@ function alreadyRegistered(settings, cmd) {
|
|
|
70
71
|
if (h.command !== cmd) continue;
|
|
71
72
|
if (event === 'PostToolUse' && g.matcher === 'Agent') good++;
|
|
72
73
|
else if (event === 'SubagentStop') good++;
|
|
74
|
+
else if (event === 'SessionStart' && g.matcher === 'compact') good++;
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
}
|
|
76
|
-
return ours ===
|
|
78
|
+
return ours === 3 && good === 3;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
function addOurHooks(settings, hookPath) {
|
|
@@ -84,6 +86,84 @@ function addOurHooks(settings, hookPath) {
|
|
|
84
86
|
h.PostToolUse.push({ matcher: 'Agent', hooks: [{ type: 'command', command: cmd, timeout: 10 }] });
|
|
85
87
|
if (!Array.isArray(h.SubagentStop)) h.SubagentStop = [];
|
|
86
88
|
h.SubagentStop.push({ hooks: [{ type: 'command', command: cmd, timeout: 10 }] });
|
|
89
|
+
// after /compact or auto-compact: re-inject the orchestration state rebuilt from the log
|
|
90
|
+
if (!Array.isArray(h.SessionStart)) h.SessionStart = [];
|
|
91
|
+
h.SessionStart.push({ matcher: 'compact', hooks: [{ type: 'command', command: cmd, timeout: 10 }] });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ------------------------------------------------------------------ run-dir permissions
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Run directory for subagent reports: ~/.dieu-phoi/runs (override: DIEU_PHOI_RUNS_DIR, absolute).
|
|
98
|
+
* Deliberately OUTSIDE ~/.claude: `.claude` is a Claude Code protected path, where allow rules never
|
|
99
|
+
* pre-approve writes, so background agents could not write their reports there.
|
|
100
|
+
*/
|
|
101
|
+
function runsDir() {
|
|
102
|
+
return core.runsDir();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Run dir used by 0.2.0 dev builds (<claudeDir>/dieu-phoi/runs); its rules are removed by setup/uninstall. */
|
|
106
|
+
function legacyRunsDirOf(claudeDir) {
|
|
107
|
+
return path.join(path.resolve(claudeDir), 'dieu-phoi', 'runs');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Permission entries that let background subagents (default permission mode) read and write reports in
|
|
112
|
+
* `runs`: Read(<p>/**) + Edit(<p>/**) allow rules (Edit rules cover every file-writing tool; Claude
|
|
113
|
+
* Code ignores path rules on Write and warns about them, so no Write(...) rule) and the dir itself in
|
|
114
|
+
* permissions.additionalDirectories. <p> = ~/<rel> under the home dir, else //<abs> (Windows: //c/...).
|
|
115
|
+
*/
|
|
116
|
+
function permissionEntries(runs, homeDir) {
|
|
117
|
+
const home = path.resolve(homeDir || os.homedir());
|
|
118
|
+
const rel = path.relative(home, runs);
|
|
119
|
+
let pat;
|
|
120
|
+
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) {
|
|
121
|
+
pat = `~/${toSlash(rel)}/**`;
|
|
122
|
+
} else {
|
|
123
|
+
const abs = toSlash(runs);
|
|
124
|
+
const m = /^([A-Za-z]):\/(.*)$/.exec(abs);
|
|
125
|
+
pat = m ? `//${m[1].toLowerCase()}/${m[2]}/**` : `/${abs}/**`;
|
|
126
|
+
}
|
|
127
|
+
return { allow: [`Read(${pat})`, `Edit(${pat})`], dir: toSlash(runs) };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Add our permission entries (idempotent). Returns true when settings changed. */
|
|
131
|
+
function addOurPermissions(settings, entries) {
|
|
132
|
+
let changed = false;
|
|
133
|
+
if (!settings.permissions || typeof settings.permissions !== 'object' || Array.isArray(settings.permissions)) {
|
|
134
|
+
settings.permissions = {};
|
|
135
|
+
changed = true;
|
|
136
|
+
}
|
|
137
|
+
const p = settings.permissions;
|
|
138
|
+
if (!Array.isArray(p.allow)) { p.allow = []; changed = true; }
|
|
139
|
+
for (const r of entries.allow) if (!p.allow.includes(r)) { p.allow.push(r); changed = true; }
|
|
140
|
+
if (!Array.isArray(p.additionalDirectories)) { p.additionalDirectories = []; changed = true; }
|
|
141
|
+
if (!p.additionalDirectories.includes(entries.dir)) { p.additionalDirectories.push(entries.dir); changed = true; }
|
|
142
|
+
return changed;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Remove exactly our permission entries; a list we emptied is deleted. Returns number removed. */
|
|
146
|
+
function stripOurPermissions(settings, entries) {
|
|
147
|
+
const p = settings.permissions;
|
|
148
|
+
if (!p || typeof p !== 'object' || Array.isArray(p)) return 0;
|
|
149
|
+
let removed = 0;
|
|
150
|
+
const strip = (key, items) => {
|
|
151
|
+
if (!Array.isArray(p[key])) return;
|
|
152
|
+
const kept = p[key].filter((x) => !items.includes(x));
|
|
153
|
+
if (kept.length === p[key].length) return;
|
|
154
|
+
removed += p[key].length - kept.length;
|
|
155
|
+
if (kept.length) p[key] = kept; else delete p[key];
|
|
156
|
+
};
|
|
157
|
+
strip('allow', entries.allow);
|
|
158
|
+
strip('additionalDirectories', [entries.dir]);
|
|
159
|
+
return removed;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function hasOurPermissions(settings, entries) {
|
|
163
|
+
const p = settings && settings.permissions;
|
|
164
|
+
if (!p || typeof p !== 'object') return false;
|
|
165
|
+
return entries.allow.every((r) => Array.isArray(p.allow) && p.allow.includes(r))
|
|
166
|
+
&& Array.isArray(p.additionalDirectories) && p.additionalDirectories.includes(entries.dir);
|
|
87
167
|
}
|
|
88
168
|
|
|
89
169
|
function readSettings(file) {
|
|
@@ -204,11 +284,20 @@ function setup(opts) {
|
|
|
204
284
|
const cmd = hookCommand(paths.hook);
|
|
205
285
|
let removed = 0;
|
|
206
286
|
let res = { changed: false, backup: null };
|
|
287
|
+
let dirty = false;
|
|
207
288
|
if (!(before.exists && alreadyRegistered(obj, cmd))) {
|
|
208
289
|
removed = stripOurHooks(obj);
|
|
209
290
|
addOurHooks(obj, paths.hook);
|
|
210
|
-
|
|
291
|
+
dirty = true;
|
|
211
292
|
}
|
|
293
|
+
// 4. run dir + permissions so background subagents can write their reports there
|
|
294
|
+
const perms = permissionEntries(runsDir());
|
|
295
|
+
fs.mkdirSync(perms.dir, { recursive: true });
|
|
296
|
+
// rules a 0.2.0 dev build added for <claudeDir>/dieu-phoi/runs (protected path, never worked) go away
|
|
297
|
+
const legacyRemoved = stripOurPermissions(obj, permissionEntries(legacyRunsDirOf(claudeDir)));
|
|
298
|
+
if (legacyRemoved) dirty = true;
|
|
299
|
+
if (addOurPermissions(obj, perms)) dirty = true;
|
|
300
|
+
if (dirty) res = writeSettings(settingsFile, before, obj);
|
|
212
301
|
|
|
213
302
|
return {
|
|
214
303
|
home,
|
|
@@ -218,6 +307,9 @@ function setup(opts) {
|
|
|
218
307
|
settings_changed: res.changed,
|
|
219
308
|
backup: res.backup,
|
|
220
309
|
replaced_entries: removed,
|
|
310
|
+
runs_dir: perms.dir,
|
|
311
|
+
permissions: perms,
|
|
312
|
+
legacy_permissions_removed: legacyRemoved,
|
|
221
313
|
config: cfg,
|
|
222
314
|
};
|
|
223
315
|
}
|
|
@@ -229,29 +321,41 @@ function uninstall(opts) {
|
|
|
229
321
|
const before = readSettings(settingsFile);
|
|
230
322
|
const obj = JSON.parse(JSON.stringify(before.obj));
|
|
231
323
|
const removed = stripOurHooks(obj);
|
|
324
|
+
const permsRemoved = stripOurPermissions(obj, permissionEntries(runsDir()))
|
|
325
|
+
+ stripOurPermissions(obj, permissionEntries(legacyRunsDirOf(claudeDir)));
|
|
232
326
|
let res = { changed: false, backup: null };
|
|
233
|
-
if (removed) res = writeSettings(settingsFile, before, obj);
|
|
327
|
+
if (removed || permsRemoved) res = writeSettings(settingsFile, before, obj);
|
|
234
328
|
const hookFile = core.pathsFor(home).hook;
|
|
235
329
|
let hookRemoved = false;
|
|
236
330
|
if (opts.purge && fs.existsSync(hookFile)) {
|
|
237
331
|
fs.unlinkSync(hookFile);
|
|
238
332
|
hookRemoved = true;
|
|
239
333
|
}
|
|
240
|
-
return { removed, settings: settingsFile, backup: res.backup, hook_removed: hookRemoved, data_kept: core.pathsFor(home).data };
|
|
334
|
+
return { removed, permissions_removed: permsRemoved, settings: settingsFile, backup: res.backup, hook_removed: hookRemoved, data_kept: core.pathsFor(home).data };
|
|
241
335
|
}
|
|
242
336
|
|
|
243
337
|
/** Inspect settings for our registration. */
|
|
244
338
|
function registration(claudeDir) {
|
|
245
339
|
const file = path.join(path.resolve(claudeDir), 'settings.json');
|
|
246
|
-
const
|
|
340
|
+
const perms = permissionEntries(runsDir());
|
|
341
|
+
const legacy = permissionEntries(legacyRunsDirOf(claudeDir));
|
|
342
|
+
const out = {
|
|
343
|
+
file, PostToolUse: false, SubagentStop: false, SessionStart: false, commands: [], permissions: false, permission_entries: perms,
|
|
344
|
+
runs_dir: perms.dir, runs_dir_exists: fs.existsSync(perms.dir), legacy_permissions: false,
|
|
345
|
+
};
|
|
247
346
|
let obj;
|
|
248
347
|
try { obj = readSettings(file).obj; } catch (_) { return out; }
|
|
348
|
+
out.permissions = hasOurPermissions(obj, perms);
|
|
349
|
+
const lp = obj.permissions || {};
|
|
350
|
+
out.legacy_permissions = legacy.allow.some((r) => Array.isArray(lp.allow) && lp.allow.includes(r))
|
|
351
|
+
|| (Array.isArray(lp.additionalDirectories) && lp.additionalDirectories.includes(legacy.dir));
|
|
249
352
|
const hooks = obj.hooks || {};
|
|
250
|
-
for (const ev of ['PostToolUse', 'SubagentStop']) {
|
|
353
|
+
for (const ev of ['PostToolUse', 'SubagentStop', 'SessionStart']) {
|
|
251
354
|
for (const g of Array.isArray(hooks[ev]) ? hooks[ev] : []) {
|
|
252
355
|
for (const h of (g && Array.isArray(g.hooks) ? g.hooks : [])) {
|
|
253
356
|
if (!isOurs(h)) continue;
|
|
254
357
|
if (ev === 'PostToolUse' && g.matcher && !/\bAgent\b/.test(g.matcher)) continue;
|
|
358
|
+
if (ev === 'SessionStart' && g.matcher !== 'compact') continue;
|
|
255
359
|
out[ev] = true;
|
|
256
360
|
out.commands.push(h.command);
|
|
257
361
|
}
|
|
@@ -262,5 +366,5 @@ function registration(claudeDir) {
|
|
|
262
366
|
|
|
263
367
|
module.exports = {
|
|
264
368
|
MARKER, setup, uninstall, registration, stripOurHooks, addOurHooks, alreadyRegistered, hookCommand,
|
|
265
|
-
skillVersionOf, findSkillDir, tsStamp,
|
|
369
|
+
skillVersionOf, findSkillDir, tsStamp, runsDir, legacyRunsDirOf, permissionEntries, addOurPermissions, stripOurPermissions, hasOurPermissions,
|
|
266
370
|
};
|