@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/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
- module.exports = { parseArgs, claudeDirOf, ctxOf, yesNo };
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
- if (!todo.length) throw new Error(`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).`);
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
- if (opts.json) return { code: 0, out: JSON.stringify(list.map((x) => x.ev)) };
432
- return { code: 0, out: `Đã 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(', ')}.` };
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
- if (opts.json) return { code: 0, out: JSON.stringify(ev) };
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
- return { code: 0, out: `Đã ghi người dùng ${ev.outcome}${ev.cause ? ` (${ev.cause})` : ''} cho task ${ev.task_id}, vòng ${ev.round}.` };
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
- const st = core.readJsonFile(file, {}) || {};
557
- const cur = st[m.session_id];
558
- const comp = typeof m.compactions === 'number' ? m.compactions : null;
559
- const warn = !cur || cur.compactions !== comp || level > (Number(cur.level) || 0);
560
- if (warn) {
561
- const now = Date.now();
562
- st[m.session_id] = { level, compactions: comp, t: now };
563
- for (const k of Object.keys(st)) if (!(now - Number(st[k] && st[k].t) < CTX_WARN_KEEP_MS)) delete st[k];
564
- core.writeFileAtomic(file, `${JSON.stringify(st)}\n`);
565
- }
566
- return warn;
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
  };