@kienntpixon/dieu-phoi 0.6.1 → 0.7.1

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/knowledge.cjs CHANGED
@@ -14,6 +14,7 @@
14
14
  //
15
15
  // Delta (written by agents into a run dir as knowledge-delta*.md, merged at gate 3):
16
16
  // repo: <path of the repo/worktree the delta was checked against> (header, before the first `## `)
17
+ // or a short name (`pff-tailor`) that matches exactly one known store (its name / dir name / a recorded root)
17
18
  // head: <sha> (optional; else HEAD read from repo:)
18
19
  // ## +<id> new section ## <id> replace an existing one
19
20
  // title: / tags: a, b / paths: glob, glob / base: <sha>#<rev> (meta lines right under the heading)
@@ -27,7 +28,6 @@
27
28
  const fs = require('fs');
28
29
  const path = require('path');
29
30
  const cp = require('child_process');
30
- const crypto = require('crypto');
31
31
  const core = require('../hook.cjs');
32
32
  const { repoOf } = require('./args.cjs');
33
33
 
@@ -55,7 +55,8 @@ const KNOWLEDGE_HELP = `Cách dùng: dieu-phoi knowledge <lệnh> [--repo <đư
55
55
  merge --run <id|thư mục run> [--task <id>[,<id>] [--untagged]] [--dry-run] [--json]
56
56
  gộp mọi knowledge-delta*.md của run (--task: chỉ knowledge-delta-<task_id>[-r<n>].md của các
57
57
  task đó, --untagged: thêm file không mang task_id; verdict accept tự gọi bước này) vào kho của repo ghi ở header "repo:" (không có thì
58
- --repo/cwd): khoá knowledge.lock, ghi tmp+rename, bản cũ vào history/, sha = HEAD của repo đó.
58
+ --repo/cwd; "repo:" là tên ngắn như pff-tailor thì khớp kho đã biết nếu duy nhất — HEAD lấy từ repo đã ghi
59
+ nhớ (meta.json roots), --repo hoặc dòng head:): khoá knowledge.lock, ghi tmp+rename, bản cũ vào history/, sha = HEAD của repo đó.
59
60
  "## +id" thêm mục (bắt buộc paths:), "## id" thay mục (bắt buộc base: <sha>#<rev>); base cũ
60
61
  → xung đột, không ghi đè; mục > 40 dòng bị từ chối. Có xung đột/từ chối → exit 2.
61
62
  Idempotent: file delta đã gộp (hash trong meta.json) và mục trùng một rev cũ trong history/ → "không đổi"
@@ -609,8 +610,25 @@ function sameContent(cur, body, next) {
609
610
  const APPLIED_MAX = 300;
610
611
 
611
612
  /** sha256 of the delta text, BOM and CRLF normalised so a CRLF checkout hashes like the LF original. */
612
- function deltaHash(text) {
613
- return crypto.createHash('sha256').update(String(text).replace(/^/, '').replace(/\r\n?/g, '\n')).digest('hex');
613
+ const deltaHash = core.deltaHash; // shared with the unmerged-delta scan in hook.cjs
614
+
615
+ /** Main checkout of the repo `info` describes (a linked worktree is deleted later; the checkout stays). */
616
+ function mainRootOf(info) {
617
+ const common = info && info.common_dir ? String(info.common_dir) : '';
618
+ if (common && path.basename(common) === '.git') return slash(path.dirname(common));
619
+ return info && info.root ? slash(info.root) : null;
620
+ }
621
+
622
+ /** Remember where a store's repo lives (meta.json `roots`, newest first, <= 5) so a bare `repo: <name>` can find HEAD. */
623
+ function noteRoot(dir, info) {
624
+ const root = mainRootOf(info);
625
+ if (!root || !isDir(dir)) return;
626
+ const file = path.join(dir, 'meta.json');
627
+ const meta = core.readJsonFile(file, {}) || {};
628
+ const roots = Array.isArray(meta.roots) ? meta.roots : [];
629
+ if (roots[0] === root) return;
630
+ const next = [root].concat(roots.filter((r) => r !== root)).slice(0, 5);
631
+ core.writeFileAtomic(file, `${JSON.stringify(Object.assign({}, meta, { roots: next }), null, 2)}\n`);
614
632
  }
615
633
 
616
634
  function appliedOf(dir) {
@@ -741,9 +759,11 @@ function applyDelta(delta, info, sha, dry, src) {
741
759
  core.writeFileAtomic(path.join(dir, 'index.json'), `${JSON.stringify(idx, null, 2)}\n`);
742
760
  ensureMeta(dir, info);
743
761
  }
744
- if (changed && !dry && src && !results.some((r) => r.op === 'conflict' || r.op === 'reject')) {
762
+ if (!dry && src && isDir(dir) && !results.some((r) => r.op === 'conflict' || r.op === 'reject')) {
763
+ // also when every section was already merged ("same"): the file counts as handled, not as an unmerged delta
745
764
  recordApplied(dir, info, { sha256: src.hash, file: src.file, run: src.run, at: now.toISOString() });
746
765
  }
766
+ if (!dry && changed && info.common_dir) noteRoot(dir, info);
747
767
  return { results };
748
768
  }
749
769
 
@@ -766,6 +786,45 @@ function describe(r) {
766
786
  return `${OP_MARK[r.op] || '?'} ${r.id} BỊ TỪ CHỐI: ${r.why}`;
767
787
  }
768
788
 
789
+ /**
790
+ * Where a delta goes: { target, info } — target = repo dir to read HEAD from (null when only the store is known),
791
+ * info = project identity (resolveProject, or the bare store entry for a short `repo:` name). Order: a short name that
792
+ * matches exactly one known store, then the path, then --repo as the fallback for a repo that no longer exists.
793
+ */
794
+ function targetOf(delta, opts, rep) {
795
+ const err = (error) => ({ error });
796
+ if (!delta.repo) return finish(repoOf(opts));
797
+ if (core.isBareRepoName(delta.repo)) {
798
+ const hit = core.storeByName(delta.repo);
799
+ if (hit.matches.length > 1) {
800
+ return err(`repo: ${delta.repo} khớp ${hit.matches.length} kho (${hit.matches.map((m) => m.dir_name).join(', ')}) — ghi đường dẫn đầy đủ hoặc chạy với --repo`);
801
+ }
802
+ if (hit.store) {
803
+ const st = hit.store;
804
+ rep.note = `repo: ${delta.repo} → kho ${st.dir_name}`;
805
+ const cands = st.roots.map((r) => core.nativePath(r)).concat(opts.repo !== undefined ? [repoOf(opts)] : []);
806
+ for (const c of cands) {
807
+ if (!isDir(c)) continue;
808
+ const pi = core.resolveProject(c);
809
+ if (pi && pi.dir === st.dir) return { target: path.resolve(c), info: pi };
810
+ }
811
+ return { target: null, info: { dir: st.dir, dir_name: st.dir_name, key: st.key, name: st.name } };
812
+ }
813
+ }
814
+ if (delta.repo) {
815
+ const p = path.resolve(core.nativePath(delta.repo));
816
+ if (isDir(p)) return finish(p);
817
+ if (opts.repo !== undefined) { rep.note = `repo: ${delta.repo} không còn — dùng --repo`; return finish(repoOf(opts)); }
818
+ const bare = core.isBareRepoName(delta.repo);
819
+ return err(`repo: ${delta.repo} ${bare ? 'không khớp kho nào đã biết và không phải thư mục' : 'không tồn tại (worktree đã xoá?)'} — chạy lại với --repo <đường dẫn repo>`);
820
+ }
821
+ return err('không xác định được repo');
822
+ function finish(target) {
823
+ const info = core.resolveProject(target);
824
+ return info ? { target, info } : err(`không phải repo git: ${slash(target)}`);
825
+ }
826
+ }
827
+
769
828
  function cmdMerge(opts) {
770
829
  const rd = runDirOf(opts.run);
771
830
  if (rd.error) return { code: 1, out: rd.error };
@@ -786,23 +845,20 @@ function cmdMerge(opts) {
786
845
  src = { hash: deltaHash(text), file: path.basename(file), run: path.basename(rd.dir) };
787
846
  } catch (e) { rep.error = `không đọc được: ${e.message}`; totals.error++; continue; }
788
847
  if (!delta.sections.length) { rep.error = 'không có mục "## <id>" nào'; totals.error++; continue; }
789
- let target = null;
790
- if (delta.repo) {
791
- const p = path.resolve(core.nativePath(delta.repo));
792
- if (isDir(p)) target = p;
793
- else if (opts.repo !== undefined) { target = repoOf(opts); rep.note = `repo: ${delta.repo} không còn — dùng --repo`; } else {
794
- rep.error = `repo: ${delta.repo} không tồn tại (worktree đã xoá?) — chạy lại với --repo <đường dẫn repo>`; totals.error++; continue;
795
- }
796
- } else target = repoOf(opts);
797
- const info = core.resolveProject(target);
798
- if (!info) { rep.error = `không phải repo git: ${slash(target)}`; totals.error++; continue; }
848
+ const t = targetOf(delta, opts, rep);
849
+ if (t.error) { rep.error = t.error; totals.error++; continue; }
850
+ const { target, info } = t;
799
851
  let sha = null;
800
852
  if (delta.head) {
801
853
  if (!/^[0-9a-f]{7,64}$/i.test(delta.head)) { rep.error = `head: không phải sha: ${delta.head}`; totals.error++; continue; }
802
854
  sha = delta.head.toLowerCase();
803
- } else sha = headShaOf(target);
804
- if (!sha) { rep.error = `không xác định được HEAD của ${slash(target)} (repo chưa có commit?) — thêm dòng "head: <sha>" vào delta`; totals.error++; continue; }
805
- Object.assign(rep, { repo: slash(target), store: info.dir, key: info.key, sha });
855
+ } else if (target) sha = headShaOf(target);
856
+ if (!sha) {
857
+ rep.error = target ? `không xác định được HEAD của ${slash(target)} (repo chưa có commit?) — thêm dòng "head: <sha>" vào delta`
858
+ : `repo: ${delta.repo} khớp kho ${info.dir_name} nhưng chưa biết repo nằm đâu để đọc HEAD — thêm dòng "head: <sha>" vào delta hoặc chạy với --repo <đường dẫn repo>`;
859
+ totals.error++; continue;
860
+ }
861
+ Object.assign(rep, { repo: target ? slash(target) : null, store: info.dir, key: info.key, sha });
806
862
  let out;
807
863
  if (dry) out = applyDelta(delta, info, sha, true, src);
808
864
  else {
package/lib/setup.cjs CHANGED
@@ -57,7 +57,11 @@ function stripOurHooks(settings) {
57
57
  return removed;
58
58
  }
59
59
 
60
- /** True when settings already hold exactly our four entries with this command. */
60
+ // core 0.7.1: the subagent tool is `Agent` in current Claude Code and `Task` in older builds — match both
61
+ // (an old `Agent`-only registration is not "already registered", so `setup` rewrites it).
62
+ const AGENT_MATCHER = 'Agent|Task';
63
+
64
+ /** True when settings already hold exactly our five entries with this command. */
61
65
  function alreadyRegistered(settings, cmd) {
62
66
  const hooks = settings && settings.hooks;
63
67
  if (!hooks || typeof hooks !== 'object') return false;
@@ -69,14 +73,15 @@ function alreadyRegistered(settings, cmd) {
69
73
  if (!isOurs(h)) continue;
70
74
  ours++;
71
75
  if (h.command !== cmd) continue;
72
- if (event === 'PostToolUse' && g.matcher === 'Agent') good++;
76
+ if (event === 'PostToolUse' && g.matcher === AGENT_MATCHER) good++;
73
77
  else if (event === 'SubagentStop') good++;
74
78
  else if (event === 'SessionStart' && g.matcher === 'compact') good++;
75
79
  else if (event === 'UserPromptSubmit' && !g.matcher) good++;
80
+ else if (event === 'PreToolUse' && g.matcher === AGENT_MATCHER) good++;
76
81
  }
77
82
  }
78
83
  }
79
- return ours === 4 && good === 4;
84
+ return ours === 5 && good === 5;
80
85
  }
81
86
 
82
87
  function addOurHooks(settings, hookPath) {
@@ -84,7 +89,7 @@ function addOurHooks(settings, hookPath) {
84
89
  if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
85
90
  const h = settings.hooks;
86
91
  if (!Array.isArray(h.PostToolUse)) h.PostToolUse = [];
87
- h.PostToolUse.push({ matcher: 'Agent', hooks: [{ type: 'command', command: cmd, timeout: 10 }] });
92
+ h.PostToolUse.push({ matcher: AGENT_MATCHER, hooks: [{ type: 'command', command: cmd, timeout: 10 }] });
88
93
  if (!Array.isArray(h.SubagentStop)) h.SubagentStop = [];
89
94
  h.SubagentStop.push({ hooks: [{ type: 'command', command: cmd, timeout: 10 }] });
90
95
  // after /compact or auto-compact: re-inject the orchestration state rebuilt from the log
@@ -93,6 +98,9 @@ function addOurHooks(settings, hookPath) {
93
98
  // on a dieu-phoi prompt: pre-create the run dir + id so the first tool call can be the scout (no shell)
94
99
  if (!Array.isArray(h.UserPromptSubmit)) h.UserPromptSubmit = [];
95
100
  h.UserPromptSubmit.push({ hooks: [{ type: 'command', command: cmd, timeout: 5 }] });
101
+ // core 0.7.1 (C1): deny another exec continuation of a task that already handed off 3 times (unless chain_ok)
102
+ if (!Array.isArray(h.PreToolUse)) h.PreToolUse = [];
103
+ h.PreToolUse.push({ matcher: AGENT_MATCHER, hooks: [{ type: 'command', command: cmd, timeout: 5 }] });
96
104
  }
97
105
 
98
106
  // ------------------------------------------------------------------ run-dir permissions
@@ -413,7 +421,7 @@ function registration(claudeDir) {
413
421
  const perms = permissionEntries(runsDir());
414
422
  const legacy = permissionEntries(legacyRunsDirOf(claudeDir));
415
423
  const out = {
416
- file, PostToolUse: false, SubagentStop: false, SessionStart: false, UserPromptSubmit: false, commands: [], permissions: false, permission_entries: perms,
424
+ file, PostToolUse: false, SubagentStop: false, SessionStart: false, UserPromptSubmit: false, PreToolUse: false, commands: [], permissions: false, permission_entries: perms,
417
425
  runs_dir: perms.dir, runs_dir_exists: fs.existsSync(perms.dir), legacy_permissions: false,
418
426
  };
419
427
  let obj;
@@ -423,11 +431,11 @@ function registration(claudeDir) {
423
431
  out.legacy_permissions = legacy.allow.some((r) => Array.isArray(lp.allow) && lp.allow.includes(r))
424
432
  || (Array.isArray(lp.additionalDirectories) && lp.additionalDirectories.includes(legacy.dir));
425
433
  const hooks = obj.hooks || {};
426
- for (const ev of ['PostToolUse', 'SubagentStop', 'SessionStart', 'UserPromptSubmit']) {
434
+ for (const ev of ['PostToolUse', 'SubagentStop', 'SessionStart', 'UserPromptSubmit', 'PreToolUse']) {
427
435
  for (const g of Array.isArray(hooks[ev]) ? hooks[ev] : []) {
428
436
  for (const h of (g && Array.isArray(g.hooks) ? g.hooks : [])) {
429
437
  if (!isOurs(h)) continue;
430
- if (ev === 'PostToolUse' && g.matcher && !/\bAgent\b/.test(g.matcher)) continue;
438
+ if ((ev === 'PostToolUse' || ev === 'PreToolUse') && g.matcher && !/\b(Agent|Task)\b/.test(g.matcher)) continue;
431
439
  if (ev === 'SessionStart' && g.matcher !== 'compact') continue;
432
440
  out[ev] = true;
433
441
  out.commands.push(h.command);
@@ -438,6 +446,6 @@ function registration(claudeDir) {
438
446
  }
439
447
 
440
448
  module.exports = {
441
- MARKER, AGENT_NAMES, AGENT_MARKER, renderAgent, agentStatus, nodePathOf, installAgents, removeAgents, agentsDirOf, setup, uninstall, registration, stripOurHooks, addOurHooks, alreadyRegistered, hookCommand,
449
+ MARKER, AGENT_NAMES, AGENT_MARKER, renderAgent, agentStatus, nodePathOf, installAgents, removeAgents, agentsDirOf, setup, uninstall, registration, stripOurHooks, addOurHooks, alreadyRegistered, hookCommand, AGENT_MATCHER,
442
450
  skillVersionOf, findSkillDir, tsStamp, runsDir, legacyRunsDirOf, permissionEntries, addOurPermissions, stripOurPermissions, hasOurPermissions,
443
451
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kienntpixon/dieu-phoi",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
4
4
  "description": "Hook + CLI ghi log subagent của Claude Code và chọn model/effort theo chi phí + thời gian tới lúc nghiệm thu.",
5
5
  "license": "MIT",
6
6
  "author": "kiennt <kiennt@pixon.games>",