@kienntpixon/dieu-phoi 0.5.2 → 0.6.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 +73 -0
- package/README.md +9 -3
- package/agents/dp-exec.md +2 -0
- package/agents/dp-scout.md +2 -0
- package/agents/dp-verify.md +2 -0
- package/bin/dieu-phoi.cjs +21 -3
- package/hook.cjs +907 -19
- package/lib/args.cjs +19 -2
- package/lib/commands.cjs +109 -22
- package/lib/knowledge.cjs +1002 -0
- package/lib/profile.cjs +204 -0
- package/lib/prune.cjs +57 -20
- package/package.json +1 -1
package/lib/args.cjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
3
4
|
const path = require('path');
|
|
4
5
|
const core = require('../hook.cjs');
|
|
5
6
|
|
|
6
|
-
const BOOLEAN_FLAGS = new Set(['json', 'help', 'h', 'purge', 'version', 'v', 'dry-run', 'no-sync', 'approve', 'yes']);
|
|
7
|
+
const BOOLEAN_FLAGS = new Set(['json', 'help', 'h', 'purge', 'version', 'v', 'dry-run', 'no-sync', 'approve', 'yes', 'force', 'follow', 'remove', 'dirty', 'no-cache', 'untagged', 'no-knowledge']);
|
|
7
8
|
|
|
8
9
|
/** Minimal argv parser: positionals, --k v, --k=v, boolean flags. */
|
|
9
10
|
function parseArgs(argv) {
|
|
@@ -52,4 +53,20 @@ function yesNo(v, dflt) {
|
|
|
52
53
|
throw new Error(`Giá trị y/n không hợp lệ: ${v}`);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Repo a command works on: `--repo <path>` (any path inside the repo or one of its worktrees), else the cwd.
|
|
58
|
+
* Throws a readable error when --repo is not an existing directory. Does not require the path to be a git repo.
|
|
59
|
+
*/
|
|
60
|
+
function repoOf(opts) {
|
|
61
|
+
if (opts && opts.repo !== undefined) {
|
|
62
|
+
if (typeof opts.repo !== 'string' || !opts.repo) throw new Error('--repo cần một đường dẫn');
|
|
63
|
+
const p = path.resolve(core.nativePath(opts.repo));
|
|
64
|
+
let ok = false;
|
|
65
|
+
try { ok = fs.statSync(p).isDirectory(); } catch (_) { /* missing */ }
|
|
66
|
+
if (!ok) throw new Error(`--repo: không phải thư mục: ${p}`);
|
|
67
|
+
return p;
|
|
68
|
+
}
|
|
69
|
+
return process.cwd();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { parseArgs, claudeDirOf, ctxOf, yesNo, repoOf };
|
package/lib/commands.cjs
CHANGED
|
@@ -8,7 +8,7 @@ const core = require('../hook.cjs');
|
|
|
8
8
|
const agg = require('./aggregate.cjs');
|
|
9
9
|
const chooser = require('./choose.cjs');
|
|
10
10
|
const setupLib = require('./setup.cjs');
|
|
11
|
-
const { claudeDirOf, ctxOf, yesNo } = require('./args.cjs');
|
|
11
|
+
const { claudeDirOf, ctxOf, yesNo, repoOf } = require('./args.cjs');
|
|
12
12
|
|
|
13
13
|
const AGENT_OUTCOMES = ['pass', 'fail', 'timeout', 'aborted'];
|
|
14
14
|
const USER_OUTCOMES = ['accept', 'reject'];
|
|
@@ -186,13 +186,13 @@ const TMP_STALE_MS = 3600 * 1000;
|
|
|
186
186
|
|
|
187
187
|
/**
|
|
188
188
|
* Leftovers of writeFileAtomic (tmp written, rename never happened: process killed, or rename lost a race on
|
|
189
|
-
* Windows) under data/, data/orch, data/segments, data/plans. Older than 1 h → removed; newer → reported.
|
|
189
|
+
* Windows) under data/, data/orch, data/segments, data/plans, data/suggest. Older than 1 h → removed; newer → reported.
|
|
190
190
|
*/
|
|
191
191
|
function sweepStaleTmp(ctx) {
|
|
192
192
|
let removed = 0;
|
|
193
193
|
const left = [];
|
|
194
194
|
const now = Date.now();
|
|
195
|
-
for (const dir of [ctx.paths.data, path.join(ctx.paths.data, 'orch'), ctx.paths.segments, ctx.paths.plans]) {
|
|
195
|
+
for (const dir of [ctx.paths.data, path.join(ctx.paths.data, 'orch'), ctx.paths.segments, ctx.paths.plans, ctx.paths.suggest]) {
|
|
196
196
|
let names = [];
|
|
197
197
|
try { names = fs.readdirSync(dir); } catch (_) { continue; }
|
|
198
198
|
for (const n of names) {
|
|
@@ -239,13 +239,17 @@ async function checkSinkLive(ctx) {
|
|
|
239
239
|
function cmdStart(opts) {
|
|
240
240
|
const ctx = ctxOf(opts);
|
|
241
241
|
const reg = setupLib.registration(claudeDirOf(opts));
|
|
242
|
-
const info = core.startInfo(ctx);
|
|
242
|
+
const info = core.startInfo(ctx, repoOf(opts));
|
|
243
243
|
const h = info.health;
|
|
244
244
|
if (!fs.existsSync(ctx.paths.hook)) h.problems.unshift(`chưa cài hook (${ctx.paths.hook}) — chạy \`dieu-phoi setup --dept <phòng>\`, log không được ghi`);
|
|
245
245
|
else if (!(reg.PostToolUse && reg.SubagentStop && reg.SessionStart)) h.problems.unshift('hook chưa đăng ký đủ trong settings.json — chạy lại `dieu-phoi setup`, log/trạng thái không được ghi');
|
|
246
246
|
if (!reg.UserPromptSubmit) h.problems.push('thiếu hook UserPromptSubmit — chạy lại `dieu-phoi setup` để bỏ được lệnh này');
|
|
247
247
|
if (!reg.permissions || reg.legacy_permissions) h.problems.push(`thiếu quyền ghi cho ${reg.permission_entries.dir} — chạy lại \`dieu-phoi setup\` (agent nền có thể không ghi được report)`);
|
|
248
248
|
h.ok = !h.problems.length;
|
|
249
|
+
// core 0.6.0: project of --repo / cwd (file reads only; null outside a git repo)
|
|
250
|
+
let proj = null;
|
|
251
|
+
try { proj = core.resolveProject(repoOf(opts)); } catch (_) { /* no project */ }
|
|
252
|
+
info.project = proj ? { dir_name: proj.dir_name, key: proj.key, kind: proj.kind, dir: proj.dir, exists: proj.exists } : null;
|
|
249
253
|
if (opts.json) return { code: 0, out: JSON.stringify(info, null, 2) };
|
|
250
254
|
const out = [
|
|
251
255
|
`root_task_id: ${info.id}`,
|
|
@@ -254,6 +258,7 @@ function cmdStart(opts) {
|
|
|
254
258
|
`health: ${core.healthLine(h)}`,
|
|
255
259
|
`scout_brief: ${info.scout_brief || '(không thấy — dùng references/brief-template.md mục E)'}`,
|
|
256
260
|
...core.orchLessonLines(info),
|
|
261
|
+
...core.knowledgeLines(info),
|
|
257
262
|
];
|
|
258
263
|
return { code: 0, out: out.join('\n') };
|
|
259
264
|
}
|
|
@@ -415,11 +420,49 @@ function verdictRoot(opts, ctx, events) {
|
|
|
415
420
|
const judged = new Set(events.filter((e) => e.kind === 'verdict' && agg.normalizeVerdict(e).by === 'user').map((e) => e.task_id));
|
|
416
421
|
const gone = agg.discardedTaskIds(events);
|
|
417
422
|
const todo = steps.filter((x) => execTasks.has(x.task_id) && !judged.has(x.task_id) && !gone.has(x.task_id));
|
|
418
|
-
|
|
419
|
-
return todo.map((x) => ({
|
|
423
|
+
const list = todo.map((x) => ({
|
|
420
424
|
step: String(x.id),
|
|
421
425
|
ev: verdictUser(Object.assign({}, opts, { task: x.task_id, _: ['accept'], round: undefined, cause: undefined }), ctx, events),
|
|
422
426
|
}));
|
|
427
|
+
// No step waiting is not an error by itself: the root's own and untagged deltas still need merging (cmdVerdict decides).
|
|
428
|
+
if (!list.length) list.emptyMsg = `Không có bước nào của ${root} cần nghiệm thu (chưa giao exec, hoặc đã có verdict người dùng).`;
|
|
429
|
+
return list;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Root task of a step: the approved plan that lists it, else the task itself (a loose task is its own run). */
|
|
433
|
+
function rootOfTask(ctx, taskId) {
|
|
434
|
+
let files = [];
|
|
435
|
+
try { files = fs.readdirSync(ctx.paths.plans).filter((f) => f.endsWith('.json')); } catch (_) { return taskId; }
|
|
436
|
+
for (const f of files) {
|
|
437
|
+
const p = core.readJsonFile(path.join(ctx.paths.plans, f), null);
|
|
438
|
+
if (p && typeof p.root_task_id === 'string' && Array.isArray(p.steps) && p.steps.some((x) => x && x.task_id === taskId)) return p.root_task_id;
|
|
439
|
+
}
|
|
440
|
+
return taskId;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Gate 3 (core 0.6.0): a user accept merges the run's knowledge deltas of the accepted tasks
|
|
445
|
+
* (knowledge-delta-<task_id>[-r<n>].md; on --root also the root's own and untagged ones). Never fails the verdict:
|
|
446
|
+
* conflicts/rejections are reported with the command to retry. --no-knowledge skips it.
|
|
447
|
+
*/
|
|
448
|
+
function knowledgeOnAccept(opts, ctx, root, tasks, untagged) {
|
|
449
|
+
if (opts['no-knowledge']) return null;
|
|
450
|
+
const kn = require('./knowledge.cjs');
|
|
451
|
+
const runDir = path.join(core.runsDir(), root);
|
|
452
|
+
const filter = { tasks: new Set(tasks), untagged };
|
|
453
|
+
try {
|
|
454
|
+
if (!fs.statSync(runDir).isDirectory() || !kn.deltaFiles(runDir, filter).length) return null;
|
|
455
|
+
} catch (_) { return null; }
|
|
456
|
+
const taskArg = tasks.join(',');
|
|
457
|
+
const retry = `dieu-phoi knowledge merge --run ${root} --task ${taskArg}${untagged ? ' --untagged' : ''}`;
|
|
458
|
+
let r;
|
|
459
|
+
try {
|
|
460
|
+
r = kn.cmdMerge({ _: [], run: runDir, task: taskArg, untagged, repo: opts.repo });
|
|
461
|
+
} catch (e) {
|
|
462
|
+
return { code: 1, out: `knowledge: gộp lỗi (${e.message}) — chạy lại: ${retry}` };
|
|
463
|
+
}
|
|
464
|
+
const tail = r.code ? `\n→ có mục chưa gộp: get lại mục đó, chép dòng base: mới vào delta (hoặc bỏ mục), rồi ${retry}` : '';
|
|
465
|
+
return { code: r.code, out: `knowledge (tự gộp khi accept):\n${r.out}${tail}` };
|
|
423
466
|
}
|
|
424
467
|
|
|
425
468
|
function cmdVerdict(opts) {
|
|
@@ -428,14 +471,33 @@ function cmdVerdict(opts) {
|
|
|
428
471
|
if (typeof opts.root === 'string' && typeof opts.task !== 'string') {
|
|
429
472
|
const list = verdictRoot(opts, ctx, events);
|
|
430
473
|
for (const x of list) core.appendEvent(ctx, x.ev);
|
|
431
|
-
|
|
432
|
-
|
|
474
|
+
const root = String(opts.root);
|
|
475
|
+
const kn = knowledgeOnAccept(opts, ctx, root, [root].concat(list.map((x) => x.ev.task_id)), true);
|
|
476
|
+
if (!list.length) {
|
|
477
|
+
// Every step was already judged (e.g. accepted one by one with --task): still merge the root + untagged deltas.
|
|
478
|
+
// Exit code follows the merge (0 merged or nothing new, 2 conflict or reject, 1 error); no delta at all keeps the old "nothing to do" error.
|
|
479
|
+
if (!kn) throw new Error(list.emptyMsg);
|
|
480
|
+
const msg = `Không còn bước nào của ${root} chờ nghiệm thu; chỉ gộp delta của root và delta không thẻ.`;
|
|
481
|
+
if (opts.json) { process.stderr.write(`${msg}\n${kn.out}\n`); return { code: kn.code, out: '[]' }; }
|
|
482
|
+
return { code: kn.code, out: `${msg}\n${kn.out}` };
|
|
483
|
+
}
|
|
484
|
+
if (opts.json) {
|
|
485
|
+
if (kn) process.stderr.write(`${kn.out}\n`);
|
|
486
|
+
return { code: 0, out: JSON.stringify(list.map((x) => x.ev)) };
|
|
487
|
+
}
|
|
488
|
+
const msg = `Đã ghi người dùng accept cho ${list.length} bước của ${opts.root}: ${list.map((x) => `${x.step}=${x.ev.task_id}`).join(', ')}.`;
|
|
489
|
+
return { code: 0, out: kn ? `${msg}\n${kn.out}` : msg };
|
|
433
490
|
}
|
|
434
491
|
const ev = typeof opts.task === 'string' ? verdictUser(opts, ctx, events) : verdictAgent(opts, ctx, events);
|
|
435
492
|
core.appendEvent(ctx, ev);
|
|
436
|
-
|
|
493
|
+
const kn = ev.by === 'user' && ev.outcome === 'accept' ? knowledgeOnAccept(opts, ctx, rootOfTask(ctx, ev.task_id), [ev.task_id], false) : null;
|
|
494
|
+
if (opts.json) {
|
|
495
|
+
if (kn) process.stderr.write(`${kn.out}\n`);
|
|
496
|
+
return { code: 0, out: JSON.stringify(ev) };
|
|
497
|
+
}
|
|
437
498
|
if (ev.by === 'user') {
|
|
438
|
-
|
|
499
|
+
const msg = `Đã ghi người dùng ${ev.outcome}${ev.cause ? ` (${ev.cause})` : ''} cho task ${ev.task_id}, vòng ${ev.round}.`;
|
|
500
|
+
return { code: 0, out: kn ? `${msg}\n${kn.out}` : msg };
|
|
439
501
|
}
|
|
440
502
|
return { code: 0, out: `Đã ghi ${ev.outcome} cho ${ev.agent_id}${ev.task_id ? ` (task ${ev.task_id})` : ''}.` };
|
|
441
503
|
}
|
|
@@ -553,17 +615,21 @@ function ctxWarnOnce(ctx, m) {
|
|
|
553
615
|
const file = path.join(ctx.paths.data, 'ctx-warn.json');
|
|
554
616
|
const level = ctxWarnLevel(m.ctx_growth, m.soft_cap);
|
|
555
617
|
try {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
618
|
+
// Core 0.5.4: read-modify-write under ctx-warn.lock — two sessions warning at once no longer overwrite each
|
|
619
|
+
// other's entry (a lost entry made the warning repeat). Lock timeout => go on unlocked (state is advisory).
|
|
620
|
+
return core.withFileLock(ctx.paths.ctxWarnLock, { timeoutMs: 2000, staleMs: 15000 }, () => {
|
|
621
|
+
const st = core.readJsonFile(file, {}) || {};
|
|
622
|
+
const cur = st[m.session_id];
|
|
623
|
+
const comp = typeof m.compactions === 'number' ? m.compactions : null;
|
|
624
|
+
const warn = !cur || cur.compactions !== comp || level > (Number(cur.level) || 0);
|
|
625
|
+
if (warn) {
|
|
626
|
+
const now = Date.now();
|
|
627
|
+
st[m.session_id] = { level, compactions: comp, t: now };
|
|
628
|
+
for (const k of Object.keys(st)) if (!(now - Number(st[k] && st[k].t) < CTX_WARN_KEEP_MS)) delete st[k];
|
|
629
|
+
core.writeFileAtomic(file, `${JSON.stringify(st)}\n`);
|
|
630
|
+
}
|
|
631
|
+
return warn;
|
|
632
|
+
}).value;
|
|
567
633
|
} catch (_) {
|
|
568
634
|
return true;
|
|
569
635
|
}
|
|
@@ -788,6 +854,13 @@ function cmdChoose(opts) {
|
|
|
788
854
|
const tid = input.taskId;
|
|
789
855
|
core.appendEvent(ctx, overrideEvent(ctx, tid, res.task_type || String(input.type).toLowerCase(), res.suggested_tier, res.tier));
|
|
790
856
|
}
|
|
857
|
+
// core 0.5.3: the next dispatch of this task + role logs suggested_tier / choose_tier next to the tier it ran on
|
|
858
|
+
if (input.taskId && res.action === 'dispatch' && res.tier) {
|
|
859
|
+
core.recordSuggestion(ctx, {
|
|
860
|
+
task_id: input.taskId, role: res.role || input.role, suggested_tier: res.suggested_tier || res.tier, choose_tier: res.tier,
|
|
861
|
+
source: 'choose', keep_suggested: input.approved !== null && !res.overridden,
|
|
862
|
+
});
|
|
863
|
+
}
|
|
791
864
|
if (opts.json) return { code: 0, out: JSON.stringify(res) };
|
|
792
865
|
const typeWarn = res.type_warning && !String(res.reason).includes(res.type_warning) ? `\nCẢNH BÁO: ${res.type_warning}` : '';
|
|
793
866
|
const warn = `${typeWarn}${res.orch_ctx_warn ? `\nCẢNH BÁO: ${orch.advice}` : ''}`;
|
|
@@ -954,6 +1027,19 @@ function planOverrideEvents(ctx, plan, events) {
|
|
|
954
1027
|
return out;
|
|
955
1028
|
}
|
|
956
1029
|
|
|
1030
|
+
/** Core 0.5.3: each approved step with a task_id leaves its choose tier for the step's dispatch (data/suggest/). */
|
|
1031
|
+
function recordPlanSuggestions(ctx, plan) {
|
|
1032
|
+
let n = 0;
|
|
1033
|
+
for (const r of plan.steps) {
|
|
1034
|
+
if (!r.task_id || !r.tier || r.action !== 'dispatch') continue;
|
|
1035
|
+
const ok = core.recordSuggestion(ctx, {
|
|
1036
|
+
task_id: r.task_id, role: r.role || 'exec', suggested_tier: r.suggested_tier || r.tier, choose_tier: r.tier, source: 'plan',
|
|
1037
|
+
});
|
|
1038
|
+
if (ok) n++;
|
|
1039
|
+
}
|
|
1040
|
+
return n;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
957
1043
|
function recordApprovedPlan(ctx, root, stepsFile, plan) {
|
|
958
1044
|
const file = path.join(ctx.paths.plans, `${root}.json`);
|
|
959
1045
|
const prev = core.readJsonFile(file, null);
|
|
@@ -1021,6 +1107,7 @@ function cmdPlanTable(opts) {
|
|
|
1021
1107
|
const rec = opts.approve ? recordApprovedPlan(ctx, String(opts.root), path.resolve(opts.file), plan) : null;
|
|
1022
1108
|
const ovs = rec ? planOverrideEvents(ctx, plan, base.events) : [];
|
|
1023
1109
|
for (const ev of ovs) core.appendEvent(ctx, ev);
|
|
1110
|
+
if (rec) recordPlanSuggestions(ctx, plan);
|
|
1024
1111
|
if (opts.json) {
|
|
1025
1112
|
const out = rec ? Object.assign({}, plan, {
|
|
1026
1113
|
approved: { root_task_id: rec.root_task_id, approval: rec.approval, record: rec.file, overrides_logged: ovs.length },
|
|
@@ -1050,6 +1137,6 @@ function cmdPlanTable(opts) {
|
|
|
1050
1137
|
}
|
|
1051
1138
|
|
|
1052
1139
|
module.exports = {
|
|
1053
|
-
cmdSetup, cmdUninstall, cmdDoctor, cmdStart, checkSinkLive, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdDiscard, ctxWarnOnce, ctxWarnLevel, sweepStaleTmp, planOverrideEvents, cmdStats, cmdRetro, cmdPrune: (o) => require('./prune.cjs').cmdPrune(o), cmdChoose, cmdCtx, cmdState, measureOrch,
|
|
1140
|
+
skillDirOf, cmdSetup, cmdUninstall, cmdDoctor, cmdStart, checkSinkLive, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdDiscard, ctxWarnOnce, ctxWarnLevel, sweepStaleTmp, planOverrideEvents, recordPlanSuggestions, cmdStats, cmdRetro, cmdPrune: (o) => require('./prune.cjs').cmdPrune(o), cmdChoose, cmdCtx, cmdState, measureOrch,
|
|
1054
1141
|
cmdPlanTable, cmdNewId, requireTaskId, buildPlanTable, recordApprovedPlan, planTaskIdProblems, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
|
|
1055
1142
|
};
|