@kienntpixon/dieu-phoi 0.7.2 → 0.7.4
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 +33 -0
- package/agents/dp-exec.md +6 -1
- package/agents/dp-helper.md +6 -0
- package/agents/dp-scout.md +7 -1
- package/agents/dp-verify.md +6 -0
- package/bin/dieu-phoi.cjs +29 -13
- package/hook.cjs +854 -154
- package/lib/aggregate.cjs +20 -2
- package/lib/args.cjs +1 -1
- package/lib/cbm.cjs +8 -2
- package/lib/choose.cjs +27 -22
- package/lib/commands.cjs +448 -39
- package/lib/deps.cjs +162 -0
- package/lib/knowledge.cjs +47 -4
- package/lib/load.cjs +332 -0
- package/lib/procs.cjs +200 -1
- package/lib/prune.cjs +91 -8
- package/lib/setup.cjs +28 -5
- package/lib/worktree.cjs +250 -19
- package/package.json +1 -1
- package/profile/lessons.md +2 -1
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.4",
|
|
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
|
@@ -20,7 +20,6 @@ Nhiều máy: file trong skill đã publish là bản đã duyệt (một nguồ
|
|
|
20
20
|
|
|
21
21
|
Câu hỏi bắt buộc hỏi ở cổng 1 cho từng loại task (retro thêm khi tỉ lệ steer cao). Định dạng: `- <task_type>: <câu hỏi>`
|
|
22
22
|
|
|
23
|
-
- *: (chỉ khi kế hoạch có ≥ 4 bước s≤6) Chọn mode? fast (bước s≤6 không verifier riêng, test chọn lọc, một verifier gộp chạy cả suite trước khi trình, tự nghiệm thu rồi bạn duyệt cuối) | normal (mặc định) | strict (luôn cả suite + kiểm ngẫu nhiên 1/3). Kế hoạch nhỏ hơn: dùng normal, không hỏi (retro 11-05: 46/46 lần chọn normal, fast 0 lần qua 2 retro). Sàn an toàn giữ ở mọi mode; đổi được giữa chừng.
|
|
24
23
|
- extract: Cần đúng những trường nào, đầu ra định dạng gì (bảng/JSON/cột Sheet), và khi các nguồn lệch nhau thì nguồn nào là chuẩn? (retro 11-05: steer_rate 0,3, n10)
|
|
25
24
|
|
|
26
25
|
## Bài học
|
|
@@ -81,3 +80,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
80
|
- [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
81
|
- [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
82
|
- [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
|
|
83
|
+
|
|
84
|
+
- [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).
|