@kienntpixon/dieu-phoi 0.7.2 → 0.7.3
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 +18 -0
- package/bin/dieu-phoi.cjs +21 -5
- package/hook.cjs +659 -26
- package/lib/args.cjs +1 -1
- package/lib/cbm.cjs +8 -2
- package/lib/commands.cjs +255 -7
- package/lib/deps.cjs +162 -0
- package/lib/load.cjs +332 -0
- package/lib/procs.cjs +200 -1
- package/lib/prune.cjs +10 -1
- package/lib/setup.cjs +28 -5
- package/lib/worktree.cjs +250 -19
- package/package.json +1 -1
- package/profile/lessons.md +2 -0
package/lib/setup.cjs
CHANGED
|
@@ -67,7 +67,7 @@ function stripOurHooks(settings) {
|
|
|
67
67
|
// (an old `Agent`-only registration is not "already registered", so `setup` rewrites it).
|
|
68
68
|
const AGENT_MATCHER = 'Agent|Task';
|
|
69
69
|
|
|
70
|
-
/** True when settings already hold exactly our
|
|
70
|
+
/** True when settings already hold exactly our six entries with this command. */
|
|
71
71
|
function alreadyRegistered(settings, cmd) {
|
|
72
72
|
const hooks = settings && settings.hooks;
|
|
73
73
|
if (!hooks || typeof hooks !== 'object') return false;
|
|
@@ -81,13 +81,14 @@ function alreadyRegistered(settings, cmd) {
|
|
|
81
81
|
if (h.command !== cmd) continue;
|
|
82
82
|
if (event === 'PostToolUse' && g.matcher === AGENT_MATCHER) good++;
|
|
83
83
|
else if (event === 'SubagentStop') good++;
|
|
84
|
+
else if (event === 'SubagentStart' && !g.matcher) good++;
|
|
84
85
|
else if (event === 'SessionStart' && g.matcher === 'compact') good++;
|
|
85
86
|
else if (event === 'UserPromptSubmit' && !g.matcher) good++;
|
|
86
87
|
else if (event === 'PreToolUse' && g.matcher === AGENT_MATCHER) good++;
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
|
-
return ours ===
|
|
91
|
+
return ours === 6 && good === 6;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
function addOurHooks(settings, hookPath) {
|
|
@@ -98,6 +99,9 @@ function addOurHooks(settings, hookPath) {
|
|
|
98
99
|
h.PostToolUse.push({ matcher: AGENT_MATCHER, hooks: [{ type: 'command', command: cmd, timeout: 10 }] });
|
|
99
100
|
if (!Array.isArray(h.SubagentStop)) h.SubagentStop = [];
|
|
100
101
|
h.SubagentStop.push({ hooks: [{ type: 'command', command: cmd, timeout: 10 }] });
|
|
102
|
+
// core 0.7.3 (s1): live-agent register (data/live/<agent_id>.json) for the parallel cap; no matcher = every subagent
|
|
103
|
+
if (!Array.isArray(h.SubagentStart)) h.SubagentStart = [];
|
|
104
|
+
h.SubagentStart.push({ hooks: [{ type: 'command', command: cmd, timeout: 5 }] });
|
|
101
105
|
// after /compact or auto-compact: re-inject the orchestration state rebuilt from the log
|
|
102
106
|
if (!Array.isArray(h.SessionStart)) h.SessionStart = [];
|
|
103
107
|
h.SessionStart.push({ matcher: 'compact', hooks: [{ type: 'command', command: cmd, timeout: 10 }] });
|
|
@@ -282,6 +286,22 @@ function nodePathOf(cmd) {
|
|
|
282
286
|
return m ? m[1] : undefined;
|
|
283
287
|
}
|
|
284
288
|
|
|
289
|
+
function realOf(p) {
|
|
290
|
+
try { return fs.realpathSync.native(p); } catch (_) { return path.resolve(p); }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Rewrite hook commands inside an installed agent file whose node/hook paths resolve (symlink/junction, e.g. an
|
|
295
|
+
* Orca profile linking ~/.claude) to the same real files as ours, so the comparison ignores path spelling only.
|
|
296
|
+
*/
|
|
297
|
+
function normalizeHookPaths(cur, hookPath, nodePath) {
|
|
298
|
+
const myNode = realOf(nodePath || process.execPath);
|
|
299
|
+
const myHook = realOf(hookPath);
|
|
300
|
+
const mine = hookCommand(hookPath, nodePath).replace(/'/g, "''");
|
|
301
|
+
return cur.replace(/'"([^"']+)" "([^"']+)"( --[A-Za-z-]+)'/g, (m, n, h, flag) =>
|
|
302
|
+
(realOf(n) === myNode && realOf(h) === myHook ? `'${mine}${flag}'` : m));
|
|
303
|
+
}
|
|
304
|
+
|
|
285
305
|
/**
|
|
286
306
|
* State of each dp-* agent file: ok (ours, identical), outdated (ours, differs), missing, foreign (a file with
|
|
287
307
|
* that name we did not write — never touched).
|
|
@@ -295,7 +315,10 @@ function agentStatus(claudeDir, hookPath, nodePath) {
|
|
|
295
315
|
let state = 'missing';
|
|
296
316
|
if (cur !== null) {
|
|
297
317
|
if (!cur.includes(AGENT_MARKER)) state = 'foreign';
|
|
298
|
-
else
|
|
318
|
+
else {
|
|
319
|
+
const want = renderAgent(name, hookPath, nodePath);
|
|
320
|
+
state = (cur === want || normalizeHookPaths(cur, hookPath, nodePath) === want) ? 'ok' : 'outdated';
|
|
321
|
+
}
|
|
299
322
|
}
|
|
300
323
|
return { name, file, state };
|
|
301
324
|
});
|
|
@@ -445,7 +468,7 @@ function registration(claudeDir) {
|
|
|
445
468
|
const perms = permissionEntries(runsDir());
|
|
446
469
|
const legacy = permissionEntries(legacyRunsDirOf(claudeDir));
|
|
447
470
|
const out = {
|
|
448
|
-
file, PostToolUse: false, SubagentStop: false, SessionStart: false, UserPromptSubmit: false, PreToolUse: false, commands: [], permissions: false, permission_entries: perms,
|
|
471
|
+
file, PostToolUse: false, SubagentStop: false, SubagentStart: false, SessionStart: false, UserPromptSubmit: false, PreToolUse: false, commands: [], permissions: false, permission_entries: perms,
|
|
449
472
|
runs_dir: perms.dir, runs_dir_exists: fs.existsSync(perms.dir), legacy_permissions: false,
|
|
450
473
|
};
|
|
451
474
|
let obj;
|
|
@@ -455,7 +478,7 @@ function registration(claudeDir) {
|
|
|
455
478
|
out.legacy_permissions = legacy.allow.some((r) => Array.isArray(lp.allow) && lp.allow.includes(r))
|
|
456
479
|
|| (Array.isArray(lp.additionalDirectories) && lp.additionalDirectories.includes(legacy.dir));
|
|
457
480
|
const hooks = obj.hooks || {};
|
|
458
|
-
for (const ev of ['PostToolUse', 'SubagentStop', 'SessionStart', 'UserPromptSubmit', 'PreToolUse']) {
|
|
481
|
+
for (const ev of ['PostToolUse', 'SubagentStop', 'SubagentStart', 'SessionStart', 'UserPromptSubmit', 'PreToolUse']) {
|
|
459
482
|
for (const g of Array.isArray(hooks[ev]) ? hooks[ev] : []) {
|
|
460
483
|
for (const h of (g && Array.isArray(g.hooks) ? g.hooks : [])) {
|
|
461
484
|
if (!isOurs(h)) continue;
|
package/lib/worktree.cjs
CHANGED
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
// `git worktree remove` (không --force), nhánh giữ nguyên; còn thay đổi chưa commit thì giữ và báo.
|
|
8
8
|
// Worktree không dấu không bị đụng.
|
|
9
9
|
// - `wt clean` (dry-run mặc định) liệt kê worktree có dấu mà task đã nghiệm thu / bị loại, hoặc thư mục đã mất.
|
|
10
|
+
// core 0.7.3 (s2):
|
|
11
|
+
// - Trần worktree/repo (max_worktrees_repo, mặc định 8, 0 = tắt): `wt add` đếm mọi mục của `git worktree list`
|
|
12
|
+
// (gồm checkout chính, có dấu hay không). Đủ trần → từ chối kèm ứng viên dọn (sạch, có dấu, cũ nhất); --force bỏ qua.
|
|
13
|
+
// Đạt 75% trần → cảnh báo.
|
|
14
|
+
// - `wt clean --stale-days N` (mặc định wt_stale_days = 7): thêm diện sạch + có dấu + nhánh không có commit chưa merge
|
|
15
|
+
// vào HEAD chính + cũ ≥ N ngày. Vẫn dry-run; --yes mới gỡ.
|
|
16
|
+
// - `wt list [--all]`: bảng path/task/tuổi/sạch/node_modules(link|dir|none)/có dấu. Worktree không dấu: chỉ báo.
|
|
17
|
+
// core 0.7.3 (s3): `wt add --deps auto|link|install|none` (OPT-IN, mặc định none; env DIEU_PHOI_WT_DEPS → config wt_deps)
|
|
18
|
+
// và `--deps-path <thư mục con>`; logic ở lib/deps.cjs. Dấu ghi `deps: link|pnpm|none` + `deps_path`. Gỡ worktree
|
|
19
|
+
// (removeMarked: verdict/discard/wt clean) unlink junction node_modules TRƯỚC `git worktree remove`, rồi xoá vỏ còn sót.
|
|
10
20
|
// Sự kiện `worktree` (chỉ ở máy, không đồng bộ sink) ghi repo chính để verdict/discard tìm đúng repo.
|
|
11
21
|
const fs = require('fs');
|
|
12
22
|
const path = require('path');
|
|
@@ -14,9 +24,13 @@ const cp = require('child_process');
|
|
|
14
24
|
const core = require('../hook.cjs');
|
|
15
25
|
const agg = require('./aggregate.cjs');
|
|
16
26
|
const { ctxOf, repoOf } = require('./args.cjs');
|
|
27
|
+
const deps = require('./deps.cjs');
|
|
17
28
|
|
|
18
29
|
const MARK = 'dieu-phoi.json';
|
|
19
30
|
const BRANCH_PREFIX = 'dp/';
|
|
31
|
+
const DAY_MS = 86400000;
|
|
32
|
+
const DEFAULT_MAX_WORKTREES = 8;
|
|
33
|
+
const DEFAULT_STALE_DAYS = 7;
|
|
20
34
|
|
|
21
35
|
function git(cwd, args) {
|
|
22
36
|
const r = cp.spawnSync('git', args, { cwd, encoding: 'utf8', windowsHide: true, timeout: 60000 });
|
|
@@ -52,25 +66,79 @@ function markedWorktrees(common) {
|
|
|
52
66
|
out.push({
|
|
53
67
|
admin, name, task_id: mark.task_id, root: mark.root || null, branch: mark.branch || null,
|
|
54
68
|
created_ts: mark.created_ts || null, path: wtPath, exists: !!wtPath && fs.existsSync(wtPath),
|
|
69
|
+
deps: typeof mark.deps === 'string' ? mark.deps : null, deps_path: typeof mark.deps_path === 'string' ? mark.deps_path : '.',
|
|
55
70
|
});
|
|
56
71
|
}
|
|
57
72
|
return out;
|
|
58
73
|
}
|
|
59
74
|
|
|
60
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* true = còn thay đổi (kể cả file chưa theo dõi), false = sạch, null = không đọc được.
|
|
77
|
+
* s3: dòng `?? …node_modules/` là link (junction dùng chung do --deps link tạo) không tính là thay đổi.
|
|
78
|
+
*/
|
|
61
79
|
function dirtyOf(wtPath) {
|
|
62
80
|
const r = git(wtPath, ['status', '--porcelain']);
|
|
63
81
|
if (!r.ok) return null;
|
|
64
|
-
|
|
82
|
+
const lines = r.out.split(/\r?\n/).filter(Boolean).filter((l) => {
|
|
83
|
+
const m = /^\?\? "?(.*?node_modules)\/?"?$/.exec(l);
|
|
84
|
+
return !(m && path.basename(m[1]) === 'node_modules' && deps.isLink(path.join(wtPath, m[1])));
|
|
85
|
+
});
|
|
86
|
+
return lines.length > 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Khoá so sánh đường dẫn: resolve symlink/junction nếu còn tồn tại, đổi \ thành /, bỏ / cuối, chữ thường trên Windows. */
|
|
90
|
+
function realKey(p) {
|
|
91
|
+
if (!p || typeof p !== 'string') return '';
|
|
92
|
+
let r = path.resolve(p);
|
|
93
|
+
try { r = fs.realpathSync.native(r); } catch (_) { /* thư mục đã mất: dùng đường dẫn đã resolve */ }
|
|
94
|
+
r = r.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
95
|
+
return process.platform === 'win32' ? r.toLowerCase() : r;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* s8: kiểm "worktree đang là cwd (hoặc thư mục con) của một agent sống" trước khi gỡ. Đọc sổ agent sống data/live
|
|
100
|
+
* (core.readLive: bỏ file quá TTL; kèm ý định giao chưa nhận SubagentStart, mang worktree của brief). Trả mô tả agent
|
|
101
|
+
* ("agent ab12 task xyz") để removeMarked GIỮ worktree, hoặc null. Không ném: sổ lỗi thì coi như không có agent.
|
|
102
|
+
*/
|
|
103
|
+
function liveAgentInWorktree(ctx, wtPath) {
|
|
104
|
+
const wt = realKey(wtPath);
|
|
105
|
+
if (!wt) return null;
|
|
106
|
+
let live;
|
|
107
|
+
try { live = core.readLive(ctx); } catch (_) { return null; }
|
|
108
|
+
for (const r of live.agents.concat(live.pending)) {
|
|
109
|
+
const at = realKey(r.cwd || r.worktree);
|
|
110
|
+
if (!at || !(at === wt || at.startsWith(`${wt}/`))) continue;
|
|
111
|
+
const who = r.agent_id ? `agent ${String(r.agent_id).slice(0, 8)}` : 'agent đang chờ nhận';
|
|
112
|
+
return `${who}${r.task_id ? ` task ${r.task_id}` : ''}`;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
65
115
|
}
|
|
66
116
|
|
|
67
|
-
/**
|
|
68
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Gỡ một worktree có dấu đang còn thư mục. Không --force: còn thay đổi thì giữ.
|
|
119
|
+
* s3: gỡ junction node_modules (không đi xuyên) TRƯỚC `git worktree remove`; git lỗi thì khôi phục junction.
|
|
120
|
+
* Gỡ xong mà còn vỏ thư mục (git để lại) thì xoá vỏ bằng removeShell (link chỉ unlink, không vào đích).
|
|
121
|
+
*/
|
|
122
|
+
function removeMarked(w, mainDir, ctx) {
|
|
123
|
+
const busy = liveAgentInWorktree(ctx, w.path);
|
|
124
|
+
if (busy) return { removed: false, msg: `giữ ${w.path}: đang là cwd của agent sống (${busy})` };
|
|
69
125
|
const dirty = dirtyOf(w.path);
|
|
70
126
|
if (dirty !== false) return { removed: false, msg: `giữ ${w.path}: ${dirty ? 'còn thay đổi chưa commit' : 'không đọc được trạng thái git'}` };
|
|
127
|
+
let links;
|
|
128
|
+
try { links = deps.unlinkDepLinks(w.path, [w.deps_path || '.']); } catch (e) {
|
|
129
|
+
return { removed: false, msg: `giữ ${w.path}: không gỡ được junction node_modules (${e.message}); KHÔNG gỡ worktree để tránh xoá xuyên junction` };
|
|
130
|
+
}
|
|
71
131
|
const r = git(mainDir, ['worktree', 'remove', w.path]);
|
|
72
|
-
if (!r.ok)
|
|
73
|
-
|
|
132
|
+
if (!r.ok) {
|
|
133
|
+
deps.relink(links);
|
|
134
|
+
return { removed: false, msg: `giữ ${w.path}: git worktree remove lỗi (${r.err || 'không rõ'})` };
|
|
135
|
+
}
|
|
136
|
+
let shell = '';
|
|
137
|
+
if (fs.existsSync(w.path) || deps.isLink(w.path)) {
|
|
138
|
+
try { deps.removeShell(w.path); } catch (e) { shell = `; còn vỏ thư mục chưa xoá được (${e.message})`; }
|
|
139
|
+
}
|
|
140
|
+
const unl = links.length ? `; đã gỡ junction node_modules trước` : '';
|
|
141
|
+
return { removed: true, msg: `đã gỡ worktree ${w.path} (nhánh ${w.branch || '?'} giữ nguyên${unl}${shell})` };
|
|
74
142
|
}
|
|
75
143
|
|
|
76
144
|
function recordWt(ctx, taskId, fields) {
|
|
@@ -103,7 +171,7 @@ function cleanupTask(ctx, events, taskId, repoDir) {
|
|
|
103
171
|
const mine = markedWorktrees(info.common).filter((w) => w.task_id === taskId);
|
|
104
172
|
for (const w of mine) {
|
|
105
173
|
if (!w.exists) { lines.push(`wt: ${w.path || w.name} đã mất thư mục — dọn bằng dieu-phoi wt clean --yes`); continue; }
|
|
106
|
-
const r = removeMarked(w, info.main);
|
|
174
|
+
const r = removeMarked(w, info.main, ctx);
|
|
107
175
|
if (r.removed) recordWt(ctx, taskId, { action: 'remove', path: w.path, repo: info.main, reason: 'verdict' });
|
|
108
176
|
lines.push(`wt: ${r.msg}`);
|
|
109
177
|
}
|
|
@@ -111,13 +179,102 @@ function cleanupTask(ctx, events, taskId, repoDir) {
|
|
|
111
179
|
return lines;
|
|
112
180
|
}
|
|
113
181
|
|
|
182
|
+
// ---- core 0.7.3 (s2): trần, ngưỡng, dọn theo tuổi, bảng ----
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Số nguyên không âm: env DIEU_PHOI_<KEY> → config.json → mặc định. Mỗi nguồn hỏng (không phải số nguyên ≥ 0)
|
|
186
|
+
* rơi xuống nguồn kế tiếp, không nhảy thẳng về mặc định (env "abc" vẫn đọc config.json).
|
|
187
|
+
*/
|
|
188
|
+
function numSetting(ctx, envName, cfgKey, def) {
|
|
189
|
+
const cfg = ctx && ctx.config ? ctx.config : {};
|
|
190
|
+
for (const v of [process.env[envName], cfg[cfgKey]]) {
|
|
191
|
+
if (v === undefined || v === null || v === '') continue;
|
|
192
|
+
const n = Number(v);
|
|
193
|
+
if (Number.isInteger(n) && n >= 0) return n;
|
|
194
|
+
}
|
|
195
|
+
return def;
|
|
196
|
+
}
|
|
197
|
+
const maxWorktreesOf = (ctx) => numSetting(ctx, 'DIEU_PHOI_MAX_WORKTREES_REPO', 'max_worktrees_repo', DEFAULT_MAX_WORKTREES);
|
|
198
|
+
const staleDaysOf = (ctx) => numSetting(ctx, 'DIEU_PHOI_WT_STALE_DAYS', 'wt_stale_days', DEFAULT_STALE_DAYS);
|
|
199
|
+
/** Ngưỡng cảnh báo = 75% trần, làm tròn lên (8 → 6). */
|
|
200
|
+
const warnAtOf = (max) => Math.ceil(max * 0.75);
|
|
201
|
+
|
|
202
|
+
/** Khoá so sánh đường dẫn: chuẩn hoá; Windows không phân biệt hoa thường. */
|
|
203
|
+
function pathKey(p) {
|
|
204
|
+
const r = path.resolve(p);
|
|
205
|
+
return process.platform === 'win32' ? r.toLowerCase() : r;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Mọi mục của `git worktree list --porcelain` (checkout chính đứng đầu). Ném nếu git lỗi. */
|
|
209
|
+
function registeredOf(main) {
|
|
210
|
+
const r = git(main, ['worktree', 'list', '--porcelain']);
|
|
211
|
+
if (!r.ok) throw new Error(`git worktree list lỗi: ${r.err || 'không rõ'}`);
|
|
212
|
+
const out = [];
|
|
213
|
+
let cur = null;
|
|
214
|
+
for (const line of r.out.split(/\r?\n/)) {
|
|
215
|
+
if (line.startsWith('worktree ')) { cur = { path: line.slice(9), branch: null, detached: false }; out.push(cur); }
|
|
216
|
+
else if (cur && line.startsWith('branch ')) cur.branch = line.slice(7).replace(/^refs\/heads\//, '');
|
|
217
|
+
else if (cur && line === 'detached') cur.detached = true;
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Tuổi (ms) của worktree có dấu: từ created_ts, không có thì mtime thư mục git-dir. */
|
|
223
|
+
function ageMsOf(w) {
|
|
224
|
+
const t = w.created_ts ? Date.parse(w.created_ts) : NaN;
|
|
225
|
+
if (Number.isFinite(t)) return Date.now() - t;
|
|
226
|
+
try { return Date.now() - fs.statSync(w.admin).mtimeMs; } catch (_) { return 0; }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Nhánh không có commit nào chưa vào HEAD của checkout chính. Không đọc được nhánh → false (giữ lại). */
|
|
230
|
+
function branchMerged(main, branch) {
|
|
231
|
+
if (!branch) return false;
|
|
232
|
+
const r = git(main, ['rev-list', '--count', branch, '--not', 'HEAD']);
|
|
233
|
+
return r.ok && r.out === '0';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Ứng viên dọn: có dấu, thư mục còn, sạch; cũ nhất trước. */
|
|
237
|
+
function cleanCandidates(common) {
|
|
238
|
+
const tsOf = (w) => (w.created_ts && Number.isFinite(Date.parse(w.created_ts)) ? Date.parse(w.created_ts) : Infinity);
|
|
239
|
+
return markedWorktrees(common)
|
|
240
|
+
.filter((w) => w.exists && dirtyOf(w.path) === false)
|
|
241
|
+
.sort((a, b) => tsOf(a) - tsOf(b));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function candidateLines(common) {
|
|
245
|
+
const c = cleanCandidates(common);
|
|
246
|
+
if (!c.length) return 'Không có ứng viên dọn (không có worktree có dấu nào sạch).';
|
|
247
|
+
return `Ứng viên dọn (sạch, có dấu, cũ nhất trước):\n${c.map((w) => ` - ${w.path} task=${w.task_id} tạo ${w.created_ts || '?'}`).join('\n')}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Trạng thái node_modules của một worktree: link (symlink/junction), dir (thư mục thật), none. */
|
|
251
|
+
function nodeModulesOf(p) {
|
|
252
|
+
try {
|
|
253
|
+
const st = fs.lstatSync(path.join(p, 'node_modules'));
|
|
254
|
+
if (st.isSymbolicLink()) return 'link';
|
|
255
|
+
return st.isDirectory() ? 'dir' : 'none';
|
|
256
|
+
} catch (_) { return 'none'; }
|
|
257
|
+
}
|
|
258
|
+
|
|
114
259
|
function wtAdd(opts) {
|
|
115
|
-
if (typeof opts.task !== 'string') throw new Error('Cú pháp: dieu-phoi wt add --task <task_id> [--repo r] [--path dir] [--branch b]');
|
|
260
|
+
if (typeof opts.task !== 'string') throw new Error('Cú pháp: dieu-phoi wt add --task <task_id> [--repo r] [--path dir] [--branch b] [--force] [--deps auto|link|install|none] [--deps-path dir]');
|
|
116
261
|
const taskId = requireTaskId(opts.task);
|
|
117
262
|
const ctx = ctxOf(opts);
|
|
263
|
+
const depsMode = deps.depsModeOf(opts, ctx);
|
|
264
|
+
const depsPath = deps.normDepsPath(opts['deps-path']);
|
|
118
265
|
const { main, common } = mainCheckoutOf(repoOf(opts));
|
|
119
266
|
const dup = markedWorktrees(common).filter((w) => w.task_id === taskId && w.exists);
|
|
120
267
|
if (dup.length) throw new Error(`task ${taskId} đã có worktree có dấu: ${dup.map((w) => w.path).join(', ')}`);
|
|
268
|
+
const max = maxWorktreesOf(ctx);
|
|
269
|
+
const total = registeredOf(main).length;
|
|
270
|
+
const capMsg = max > 0 && total >= max ? `repo đã có ${total}/${max} worktree (trần max_worktrees_repo)` : null;
|
|
271
|
+
if (capMsg && opts.force !== true) {
|
|
272
|
+
throw new Error(`wt add: ${capMsg}; không tạo thêm cho task ${taskId}.\n${candidateLines(common)}\n`
|
|
273
|
+
+ 'Dọn: dieu-phoi wt clean --yes (sau khi xem dry-run) hoặc dieu-phoi wt list --all; tạo vượt trần: thêm --force.');
|
|
274
|
+
}
|
|
275
|
+
const notes = [];
|
|
276
|
+
if (capMsg) notes.push(`CẢNH BÁO: vượt trần (--force): ${capMsg}`);
|
|
277
|
+
else if (max > 0 && total + 1 >= warnAtOf(max)) notes.push(`CẢNH BÁO: repo sẽ có ${total + 1}/${max} worktree (đạt 75% trần)`);
|
|
121
278
|
const wtPath = typeof opts.path === 'string' && opts.path
|
|
122
279
|
? path.resolve(opts.path)
|
|
123
280
|
: path.join(path.dirname(main), `${path.basename(main)}-wt-${taskId}`);
|
|
@@ -127,16 +284,38 @@ function wtAdd(opts) {
|
|
|
127
284
|
if (!r.ok) throw new Error(`git worktree add lỗi: ${r.err || 'không rõ'}`);
|
|
128
285
|
const adminDir = git(wtPath, ['rev-parse', '--path-format=absolute', '--git-dir']).out;
|
|
129
286
|
if (!adminDir) throw new Error(`worktree ${wtPath} tạo xong nhưng không đọc được git-dir để ghi dấu`);
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
287
|
+
const markPath = path.join(adminDir, MARK);
|
|
288
|
+
const mark = { task_id: taskId, root: main, created_ts: core.nowIso(), branch, deps: 'none', deps_path: depsPath };
|
|
289
|
+
// Dấu ghi trước khi chuẩn bị deps: nếu chết giữa chừng, worktree vẫn có dấu để wt clean nhận ra.
|
|
290
|
+
fs.writeFileSync(markPath, `${JSON.stringify(mark, null, 2)}\n`);
|
|
291
|
+
const d = deps.setupDeps({ main, wtPath, mode: depsMode, depsPath });
|
|
292
|
+
if (d.deps !== 'none') {
|
|
293
|
+
mark.deps = d.deps;
|
|
294
|
+
fs.writeFileSync(markPath, `${JSON.stringify(mark, null, 2)}\n`);
|
|
295
|
+
}
|
|
296
|
+
recordWt(ctx, taskId, { action: 'add', path: wtPath, repo: main, branch, deps: mark.deps });
|
|
297
|
+
const msg = `Đã tạo worktree ${wtPath} trên nhánh ${branch} (dấu dieu-phoi: task ${taskId}; deps ${mark.deps}). Brief dùng dòng "worktree: ${wtPath}".`;
|
|
298
|
+
const warning = notes.length ? notes.join('\n') : null;
|
|
299
|
+
const text = [msg, d.note, warning].filter(Boolean).join('\n');
|
|
300
|
+
return {
|
|
301
|
+
code: 0,
|
|
302
|
+
out: opts.json ? JSON.stringify({ task_id: taskId, path: wtPath, branch, total: total + 1, max, warning, deps: mark.deps, deps_path: depsPath, deps_note: d.note }) : text,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** --stale-days N (số nguyên ≥ 0) hoặc mặc định cấu hình. */
|
|
307
|
+
function staleDaysArg(opts, ctx) {
|
|
308
|
+
if (opts['stale-days'] === undefined) return staleDaysOf(ctx);
|
|
309
|
+
const v = opts['stale-days'];
|
|
310
|
+
const n = typeof v === 'string' && /^\d+$/.test(v) ? Number(v) : NaN;
|
|
311
|
+
if (!Number.isInteger(n)) throw new Error('--stale-days cần số nguyên ≥ 0 (số ngày)');
|
|
312
|
+
return n;
|
|
135
313
|
}
|
|
136
314
|
|
|
137
315
|
function wtClean(opts) {
|
|
138
316
|
const ctx = ctxOf(opts);
|
|
139
317
|
const { main, common } = mainCheckoutOf(repoOf(opts));
|
|
318
|
+
const staleDays = staleDaysArg(opts, ctx);
|
|
140
319
|
const events = core.readEvents(ctx);
|
|
141
320
|
const accepted = new Set(events.filter((e) => e && e.kind === 'verdict' && agg.normalizeVerdict(e).by === 'user' && e.outcome === 'accept')
|
|
142
321
|
.map((e) => e.task_id));
|
|
@@ -147,6 +326,9 @@ function wtClean(opts) {
|
|
|
147
326
|
if (!w.exists) why = 'thư mục đã mất';
|
|
148
327
|
else if (accepted.has(w.task_id)) why = 'đã nghiệm thu';
|
|
149
328
|
else if (gone.has(w.task_id)) why = 'đã loại (discard)';
|
|
329
|
+
else if (ageMsOf(w) >= staleDays * DAY_MS && branchMerged(main, w.branch) && dirtyOf(w.path) === false) {
|
|
330
|
+
why = `cũ ≥ ${staleDays} ngày, nhánh đã merge vào HEAD`;
|
|
331
|
+
}
|
|
150
332
|
if (why) rows.push(Object.assign({}, w, { why }));
|
|
151
333
|
}
|
|
152
334
|
if (!rows.length) return { code: 0, out: 'wt clean: không có worktree có dấu nào cần dọn.' };
|
|
@@ -163,24 +345,73 @@ function wtClean(opts) {
|
|
|
163
345
|
} else lines.push(`sẽ bỏ đăng ký ${w.path || w.name}: ${label}`);
|
|
164
346
|
continue;
|
|
165
347
|
}
|
|
348
|
+
const busy = liveAgentInWorktree(ctx, w.path);
|
|
349
|
+
if (busy) { lines.push(`giữ ${w.path}: đang là cwd của agent sống (${busy}) — ${label}`); continue; }
|
|
166
350
|
const dirty = dirtyOf(w.path);
|
|
167
351
|
if (dirty !== false) { lines.push(`giữ ${w.path}: ${dirty ? 'còn thay đổi chưa commit' : 'không đọc được trạng thái git'} — ${label}`); continue; }
|
|
168
352
|
if (!apply) { lines.push(`sẽ gỡ ${w.path}: ${label}`); continue; }
|
|
169
|
-
const r = removeMarked(w, main);
|
|
353
|
+
const r = removeMarked(w, main, ctx);
|
|
170
354
|
if (r.removed) recordWt(ctx, w.task_id, { action: 'remove', path: w.path, repo: main, reason: w.why });
|
|
171
355
|
lines.push(`${r.msg}: ${label}`);
|
|
172
356
|
}
|
|
173
357
|
const tail = apply ? '' : '\nDry-run: chưa xoá gì. Chạy lại với --yes để thực hiện.';
|
|
174
|
-
return { code: 0, out: `wt clean (${apply ? 'đã thực hiện' : 'dry-run'}):\n${lines.join('\n')}${tail}` };
|
|
358
|
+
return { code: 0, out: `wt clean (${apply ? 'đã thực hiện' : 'dry-run'}, ngưỡng cũ ${staleDays} ngày):\n${lines.join('\n')}${tail}` };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** `dieu-phoi wt list [--all]`: trạng thái worktree của repo. Worktree không dấu chỉ báo, không gỡ. */
|
|
362
|
+
function wtList(opts) {
|
|
363
|
+
const ctx = ctxOf(opts);
|
|
364
|
+
const { main, common } = mainCheckoutOf(repoOf(opts));
|
|
365
|
+
const max = maxWorktreesOf(ctx);
|
|
366
|
+
const reg = registeredOf(main);
|
|
367
|
+
const marks = new Map();
|
|
368
|
+
for (const w of markedWorktrees(common)) if (w.path) marks.set(pathKey(w.path), w);
|
|
369
|
+
const rows = reg.map((r) => {
|
|
370
|
+
const isMain = pathKey(r.path) === pathKey(main);
|
|
371
|
+
const mk = isMain ? null : marks.get(pathKey(r.path)) || null;
|
|
372
|
+
const exists = fs.existsSync(r.path);
|
|
373
|
+
const dirty = exists ? dirtyOf(r.path) : null;
|
|
374
|
+
const state = !exists ? 'mất' : dirty === null ? 'không rõ' : dirty ? 'bẩn' : 'sạch';
|
|
375
|
+
const ageDays = mk && Number.isFinite(ageMsOf(mk)) && mk.created_ts ? Math.floor(ageMsOf(mk) / DAY_MS) : null;
|
|
376
|
+
return {
|
|
377
|
+
path: r.path, branch: r.branch, main: isMain, marked: !!mk, task_id: mk ? mk.task_id : null,
|
|
378
|
+
age_days: ageDays, state, node_modules: exists ? nodeModulesOf(path.join(r.path, (mk && mk.deps_path) || '.')) : 'none',
|
|
379
|
+
deps: mk ? mk.deps || '-' : '-',
|
|
380
|
+
};
|
|
381
|
+
});
|
|
382
|
+
const unmarked = rows.filter((r) => !r.main && !r.marked).length;
|
|
383
|
+
const shown = opts.all === true ? rows : rows.filter((r) => r.marked);
|
|
384
|
+
const warnAt = max > 0 ? warnAtOf(max) : Infinity;
|
|
385
|
+
const head = `repo: ${rows.length}/${max > 0 ? max : '∞'} worktree`
|
|
386
|
+
+ (max > 0 && rows.length >= warnAt ? ` (đạt 75% trần — CẢNH BÁO)` : '')
|
|
387
|
+
+ (opts.all === true ? '' : `; ${unmarked} worktree không dấu (chỉ báo; dùng --all để xem)`);
|
|
388
|
+
if (opts.json) return { code: 0, out: JSON.stringify({ count: rows.length, max, rows: shown }) };
|
|
389
|
+
if (!shown.length) return { code: 0, out: `${head}\nwt list: không có worktree có dấu nào.` };
|
|
390
|
+
const table = shown.map((r) => [
|
|
391
|
+
r.main ? 'chính' : r.marked ? 'có' : 'không',
|
|
392
|
+
r.task_id || '-',
|
|
393
|
+
r.age_days === null ? '-' : `${r.age_days}d`,
|
|
394
|
+
r.state,
|
|
395
|
+
r.node_modules,
|
|
396
|
+
r.deps,
|
|
397
|
+
r.branch || '(detached)',
|
|
398
|
+
r.path,
|
|
399
|
+
]);
|
|
400
|
+
const heads = ['dấu', 'task', 'tuổi', 'sạch', 'node_modules', 'deps', 'nhánh', 'đường dẫn'];
|
|
401
|
+
const widths = heads.map((h, i) => Math.max(h.length, ...table.map((row) => String(row[i]).length)));
|
|
402
|
+
const fmt = (row) => row.map((c, i) => String(c).padEnd(widths[i])).join(' ').trimEnd();
|
|
403
|
+
return { code: 0, out: [head, fmt(heads), ...table.map(fmt)].join('\n') };
|
|
175
404
|
}
|
|
176
405
|
|
|
177
|
-
/** `dieu-phoi wt add|clean` (core 0.7.2 s17). */
|
|
406
|
+
/** `dieu-phoi wt add|clean|list` (core 0.7.2 s17; 0.7.3 s2: trần, --stale-days, list). */
|
|
178
407
|
function cmdWt(opts) {
|
|
179
408
|
const sub = opts._[0];
|
|
180
409
|
if (sub === 'add') return wtAdd(opts);
|
|
181
410
|
if (sub === 'clean') return wtClean(opts);
|
|
182
|
-
|
|
183
|
-
|
|
411
|
+
if (sub === 'list') return wtList(opts);
|
|
412
|
+
throw new Error('Cú pháp: dieu-phoi wt add --task <task_id> [--repo r] [--path dir] [--branch b] [--force] [--deps auto|link|install|none] [--deps-path dir]\n'
|
|
413
|
+
+ ' dieu-phoi wt clean [--repo r] [--stale-days N] [--yes]\n'
|
|
414
|
+
+ ' dieu-phoi wt list [--repo r] [--all] [--json]');
|
|
184
415
|
}
|
|
185
416
|
|
|
186
|
-
module.exports = { cmdWt, cleanupTask, mainCheckoutOf, markedWorktrees, MARK };
|
|
417
|
+
module.exports = { cmdWt, cleanupTask, mainCheckoutOf, markedWorktrees, registeredOf, liveAgentInWorktree, MARK };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kienntpixon/dieu-phoi",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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>",
|
package/profile/lessons.md
CHANGED
|
@@ -81,3 +81,5 @@ Câu hỏi bắt buộc hỏi ở cổng 1 cho từng loại task (retro thêm k
|
|
|
81
81
|
- [2026-11-05] [orch] | * | verify-task-id — 41/756 lượt verify (5,4%) mang task_id không có exec nào (19 không có cả trong plan), 24 verdict agent rơi vào id sai nên bước thật thiếu verdict → thẻ verify chép đúng `task=<id>` từ lượt exec của bước đó (không tự đặt id mới); verifier gộp dùng thẻ `|batch`
|
|
82
82
|
- [2026-11-05] [orch] | * | choose — 425/1.540 lượt exec (28%) giao không chạy `choose` (273 trong phiên d8fe5944: debug chạy opus 61 lượt, $p50 5,5 so với sonnet/medium 1,1) và 138 lượt khác tier của `choose` không ghi override → mỗi lượt exec chạy `choose --json`; giao tier khác thì `--override` để retro học
|
|
83
83
|
- [2026-11-05] plan-design | * | tier-table — kế hoạch xếp opus/medium cho mechanical-edit ≥ 35 lần (n105, $p50 1,50 trong khi haiku/medium $0,03 accept 0,91, sonnet/medium $0,58 accept 0,91) và haiku/low 13 lần (accept 0,73, n15) → cột tier lấy từ `dieu-phoi choose --json` cho từng bước; opus cho mechanical-edit/debug phải ghi lý do một dòng; không xếp haiku/low
|
|
84
|
+
|
|
85
|
+
- [orch] Exec sửa nhỏ (s≤4): brief chỉ yêu cầu chạy test của module liên quan (node --test <file>); chạy full suite đúng MỘT lần ở verifier/cổng phát hành — không bắt cả exec lẫn verifier chạy full (tốn ~10+ phút mỗi lần trên máy tải nặng).
|