@kienntpixon/dieu-phoi 0.5.3 → 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 +62 -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 +673 -7
- package/lib/args.cjs +19 -2
- package/lib/commands.cjs +86 -20
- 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'];
|
|
@@ -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
|
}
|
|
@@ -1071,6 +1137,6 @@ function cmdPlanTable(opts) {
|
|
|
1071
1137
|
}
|
|
1072
1138
|
|
|
1073
1139
|
module.exports = {
|
|
1074
|
-
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,
|
|
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,
|
|
1075
1141
|
cmdPlanTable, cmdNewId, requireTaskId, buildPlanTable, recordApprovedPlan, planTaskIdProblems, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
|
|
1076
1142
|
};
|