@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/CHANGELOG.md +72 -0
- package/bin/dieu-phoi.cjs +20 -7
- package/hook.cjs +625 -15
- package/lib/aggregate.cjs +181 -16
- package/lib/args.cjs +1 -1
- package/lib/choose.cjs +101 -27
- package/lib/commands.cjs +243 -22
- package/lib/knowledge.cjs +74 -18
- package/lib/setup.cjs +16 -8
- package/package.json +1 -1
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
|
|
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
|
-
|
|
613
|
-
|
|
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 (
|
|
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
|
-
|
|
790
|
-
if (
|
|
791
|
-
|
|
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) {
|
|
805
|
-
|
|
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
|
-
|
|
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 ===
|
|
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 ===
|
|
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:
|
|
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 && !/\
|
|
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.
|
|
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>",
|