@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/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
- /** true = còn thay đổi (kể cả file chưa theo dõi), false = sạch, null = không đọc được. */
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
- return r.out.length > 0;
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
- /** Gỡ một worktree có dấu đang còn thư mục. Không --force: còn thay đổi thì giữ. */
68
- function removeMarked(w, mainDir) {
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) return { removed: false, msg: `giữ ${w.path}: git worktree remove lỗi (${r.err || 'không rõ'})` };
73
- return { removed: true, msg: `đã gỡ worktree ${w.path} (nhánh ${w.branch || '?'} giữ nguyên)` };
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 mark = { task_id: taskId, root: main, created_ts: core.nowIso(), branch };
131
- fs.writeFileSync(path.join(adminDir, MARK), `${JSON.stringify(mark, null, 2)}\n`);
132
- recordWt(ctx, taskId, { action: 'add', path: wtPath, repo: main, branch });
133
- const msg = `Đã tạo worktree ${wtPath} trên nhánh ${branch} (dấu dieu-phoi: task ${taskId}). Brief dùng dòng "worktree: ${wtPath}".`;
134
- return { code: 0, out: opts.json ? JSON.stringify({ task_id: taskId, path: wtPath, branch }) : msg };
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
- throw new Error('Cú pháp: dieu-phoi wt add --task <task_id> [--repo r] [--path dir] [--branch b]\n'
183
- + ' dieu-phoi wt clean [--repo r] [--yes]');
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.2",
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>",
@@ -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).