@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/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'];
@@ -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
  }
@@ -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
  };