@kienntpixon/dieu-phoi 0.7.1 → 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.
@@ -0,0 +1,417 @@
1
+ 'use strict';
2
+ // s17 (core 0.7.2): worktree của một bước điều phối, có dấu dieu-phoi.
3
+ // - `wt add` tạo `<cha của repo>/<tên repo>-wt-<task_id>` trên nhánh dp/<task_id> và ghi dấu
4
+ // `<git-dir của worktree>/dieu-phoi.json` {task_id, root, created_ts, branch}. Dấu nằm trong git-dir nên không làm
5
+ // worktree dirty.
6
+ // - verdict --task accept (người dùng) và discard chỉ gỡ worktree CÓ DẤU của đúng task: sạch thì
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
+ // Worktree không dấu không bị đụng.
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.
20
+ // Sự kiện `worktree` (chỉ ở máy, không đồng bộ sink) ghi repo chính để verdict/discard tìm đúng repo.
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const cp = require('child_process');
24
+ const core = require('../hook.cjs');
25
+ const agg = require('./aggregate.cjs');
26
+ const { ctxOf, repoOf } = require('./args.cjs');
27
+ const deps = require('./deps.cjs');
28
+
29
+ const MARK = 'dieu-phoi.json';
30
+ const BRANCH_PREFIX = 'dp/';
31
+ const DAY_MS = 86400000;
32
+ const DEFAULT_MAX_WORKTREES = 8;
33
+ const DEFAULT_STALE_DAYS = 7;
34
+
35
+ function git(cwd, args) {
36
+ const r = cp.spawnSync('git', args, { cwd, encoding: 'utf8', windowsHide: true, timeout: 60000 });
37
+ const err = r.stderr || (r.error ? r.error.message : '') || '';
38
+ return { ok: r.status === 0, out: (r.stdout || '').trim(), err: err.trim() };
39
+ }
40
+
41
+ function requireTaskId(id) {
42
+ if (!core.isValidTaskId(id)) throw new Error(core.taskIdError(id));
43
+ return id;
44
+ }
45
+
46
+ /** Checkout chính của repo (thư mục có .git) từ một đường dẫn bất kỳ trong repo hoặc worktree của nó. */
47
+ function mainCheckoutOf(dir) {
48
+ const r = git(dir, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
49
+ if (!r.ok) throw new Error(`không phải repo git: ${dir}${r.err ? ` (${r.err})` : ''}`);
50
+ if (path.basename(r.out) !== '.git') throw new Error(`repo không có thư mục .git (${r.out}); wt chỉ làm việc với checkout thường`);
51
+ return { main: path.dirname(r.out), common: r.out };
52
+ }
53
+
54
+ /** Worktree có dấu của repo (đọc <common>/worktrees/<name>/dieu-phoi.json). Không ném. */
55
+ function markedWorktrees(common) {
56
+ const dir = path.join(common, 'worktrees');
57
+ let names = [];
58
+ try { names = fs.readdirSync(dir); } catch (_) { return []; }
59
+ const out = [];
60
+ for (const name of names) {
61
+ const admin = path.join(dir, name);
62
+ const mark = core.readJsonFile(path.join(admin, MARK), null);
63
+ if (!mark || typeof mark.task_id !== 'string') continue;
64
+ let wtPath = null;
65
+ try { wtPath = path.dirname(fs.readFileSync(path.join(admin, 'gitdir'), 'utf8').trim()); } catch (_) { /* no gitdir: stale admin */ }
66
+ out.push({
67
+ admin, name, task_id: mark.task_id, root: mark.root || null, branch: mark.branch || null,
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 : '.',
70
+ });
71
+ }
72
+ return out;
73
+ }
74
+
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
+ */
79
+ function dirtyOf(wtPath) {
80
+ const r = git(wtPath, ['status', '--porcelain']);
81
+ if (!r.ok) return null;
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;
115
+ }
116
+
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})` };
125
+ const dirty = dirtyOf(w.path);
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
+ }
131
+ const r = git(mainDir, ['worktree', 'remove', w.path]);
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})` };
142
+ }
143
+
144
+ function recordWt(ctx, taskId, fields) {
145
+ try {
146
+ core.appendEvent(ctx, Object.assign({
147
+ schema: core.SCHEMA, kind: 'worktree', ts: core.nowIso(), agent_id: `wt:${taskId}`, task_id: taskId,
148
+ }, fields, core.commonFields(ctx)));
149
+ } catch (_) { /* best effort: the worktree itself is the truth */ }
150
+ }
151
+
152
+ /** Các repo chính mà task đã có worktree (sự kiện `worktree` action=add), kèm repo người dùng chỉ định nếu có. */
153
+ function reposOfTask(events, taskId, repoDir) {
154
+ const set = new Set();
155
+ for (const e of events) {
156
+ if (e && e.kind === 'worktree' && e.task_id === taskId && e.action === 'add' && typeof e.repo === 'string') set.add(e.repo);
157
+ }
158
+ if (typeof repoDir === 'string' && repoDir) set.add(repoDir);
159
+ return Array.from(set);
160
+ }
161
+
162
+ /**
163
+ * verdict --task accept (người dùng) / discard: gỡ worktree có dấu của đúng task. Trả các dòng báo, không ném.
164
+ * repoDir: --repo của lệnh (nếu có) — ngoài repo đã ghi trong sự kiện worktree.
165
+ */
166
+ function cleanupTask(ctx, events, taskId, repoDir) {
167
+ const lines = [];
168
+ for (const repo of reposOfTask(events, taskId, repoDir)) {
169
+ let info;
170
+ try { info = mainCheckoutOf(repo); } catch (e) { lines.push(`wt: bỏ qua ${repo} (${e.message})`); continue; }
171
+ const mine = markedWorktrees(info.common).filter((w) => w.task_id === taskId);
172
+ for (const w of mine) {
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; }
174
+ const r = removeMarked(w, info.main, ctx);
175
+ if (r.removed) recordWt(ctx, taskId, { action: 'remove', path: w.path, repo: info.main, reason: 'verdict' });
176
+ lines.push(`wt: ${r.msg}`);
177
+ }
178
+ }
179
+ return lines;
180
+ }
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
+
259
+ function wtAdd(opts) {
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]');
261
+ const taskId = requireTaskId(opts.task);
262
+ const ctx = ctxOf(opts);
263
+ const depsMode = deps.depsModeOf(opts, ctx);
264
+ const depsPath = deps.normDepsPath(opts['deps-path']);
265
+ const { main, common } = mainCheckoutOf(repoOf(opts));
266
+ const dup = markedWorktrees(common).filter((w) => w.task_id === taskId && w.exists);
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)`);
278
+ const wtPath = typeof opts.path === 'string' && opts.path
279
+ ? path.resolve(opts.path)
280
+ : path.join(path.dirname(main), `${path.basename(main)}-wt-${taskId}`);
281
+ if (fs.existsSync(wtPath)) throw new Error(`wt add: ${wtPath} đã tồn tại`);
282
+ const branch = typeof opts.branch === 'string' && opts.branch ? opts.branch : `${BRANCH_PREFIX}${taskId}`;
283
+ const r = git(main, ['worktree', 'add', '-b', branch, wtPath]);
284
+ if (!r.ok) throw new Error(`git worktree add lỗi: ${r.err || 'không rõ'}`);
285
+ const adminDir = git(wtPath, ['rev-parse', '--path-format=absolute', '--git-dir']).out;
286
+ if (!adminDir) throw new Error(`worktree ${wtPath} tạo xong nhưng không đọc được git-dir để ghi dấu`);
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;
313
+ }
314
+
315
+ function wtClean(opts) {
316
+ const ctx = ctxOf(opts);
317
+ const { main, common } = mainCheckoutOf(repoOf(opts));
318
+ const staleDays = staleDaysArg(opts, ctx);
319
+ const events = core.readEvents(ctx);
320
+ const accepted = new Set(events.filter((e) => e && e.kind === 'verdict' && agg.normalizeVerdict(e).by === 'user' && e.outcome === 'accept')
321
+ .map((e) => e.task_id));
322
+ const gone = agg.discardedTaskIds(events);
323
+ const rows = [];
324
+ for (const w of markedWorktrees(common)) {
325
+ let why = null;
326
+ if (!w.exists) why = 'thư mục đã mất';
327
+ else if (accepted.has(w.task_id)) why = 'đã nghiệm thu';
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
+ }
332
+ if (why) rows.push(Object.assign({}, w, { why }));
333
+ }
334
+ if (!rows.length) return { code: 0, out: 'wt clean: không có worktree có dấu nào cần dọn.' };
335
+ const apply = opts.yes === true;
336
+ const lines = [];
337
+ for (const w of rows) {
338
+ const label = `task=${w.task_id} nhánh=${w.branch || '?'} (${w.why})`;
339
+ if (!w.exists) {
340
+ // Thư mục đã mất: chỉ bỏ thư mục đăng ký của chính worktree có dấu này (git-dir), không đụng thư mục khác.
341
+ if (apply && path.dirname(w.admin) === path.join(common, 'worktrees')) {
342
+ fs.rmSync(w.admin, { recursive: true, force: true, maxRetries: 3 });
343
+ recordWt(ctx, w.task_id, { action: 'remove', path: w.path, repo: main, reason: 'missing' });
344
+ lines.push(`đã bỏ đăng ký ${w.path || w.name}: ${label}`);
345
+ } else lines.push(`sẽ bỏ đăng ký ${w.path || w.name}: ${label}`);
346
+ continue;
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; }
350
+ const dirty = dirtyOf(w.path);
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; }
352
+ if (!apply) { lines.push(`sẽ gỡ ${w.path}: ${label}`); continue; }
353
+ const r = removeMarked(w, main, ctx);
354
+ if (r.removed) recordWt(ctx, w.task_id, { action: 'remove', path: w.path, repo: main, reason: w.why });
355
+ lines.push(`${r.msg}: ${label}`);
356
+ }
357
+ const tail = apply ? '' : '\nDry-run: chưa xoá gì. Chạy lại với --yes để thực hiện.';
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') };
404
+ }
405
+
406
+ /** `dieu-phoi wt add|clean|list` (core 0.7.2 s17; 0.7.3 s2: trần, --stale-days, list). */
407
+ function cmdWt(opts) {
408
+ const sub = opts._[0];
409
+ if (sub === 'add') return wtAdd(opts);
410
+ if (sub === 'clean') return wtClean(opts);
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]');
415
+ }
416
+
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.1",
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>",
@@ -22,6 +22,7 @@
22
22
  "bin/",
23
23
  "agents/",
24
24
  "lib/",
25
+ "profile/",
25
26
  "hook.cjs",
26
27
  "pricing.json",
27
28
  "README.md",
@@ -32,6 +33,7 @@
32
33
  "node": ">=18"
33
34
  },
34
35
  "scripts": {
36
+ "prepack": "node scripts/pack-profile.cjs",
35
37
  "test": "node --test"
36
38
  },
37
39
  "keywords": [
@@ -0,0 +1,85 @@
1
+ # Bài học điều phối — phòng ai-lab
2
+
3
+ Một dòng mỗi bài học, ghi sau mỗi lần fail hoặc người dùng phải sửa lại. Brief cho lần sau chèn các dòng khớp `task_type` / tag. Retro gộp dòng trùng và xoá dòng lỗi thời.
4
+
5
+ Định dạng:
6
+
7
+ ```
8
+ - [YYYY-MM-DD] <task_type> | <model>/<effort> | <tag tự do, vd. repo/miền> — <điều đã sai> → <cách tránh trong brief/kiểm tra>
9
+ ```
10
+
11
+ Ví dụ: `- [2026-10-09] feature-impl | sonnet/high | isle — quên chạy migration → thêm "chạy migration + test trên DB test" vào tiêu chí nghiệm thu`
12
+
13
+ Không ghi nội dung prompt, dữ liệu khách hàng, secrets, đường dẫn tuyệt đối.
14
+
15
+ Bài học viết cho **điều phối viên** đặt `[orch]` ở chỗ `<task_type>` (vd. `- [2026-10-12] [orch] | * | verdict — …`): điều phối viên không đọc file này, CLI (≥ 0.4.0) hiện 5 dòng `[orch]` cuối trong khối khởi động / `dieu-phoi start`. Các dòng khác do agent tự lọc theo `task_type`.
16
+
17
+ Nhiều máy: file trong skill đã publish là bản đã duyệt (một nguồn duy nhất, sửa ở repo nguồn). Dòng ghi `>>` trên một máy chỉ ở máy đó; khi cài hoặc cập nhật skill, `profile/.lessons.published.md` giữ nguyên bản đã publish mà máy đang dùng, để `tools/lessons_sync.cjs` (`harvest`) biết dòng nào là của riêng máy (so sánh văn bản đã chuẩn hoá) và đẩy vào `lessons-inbox/<máy>.md` trong repo nguồn (ngoài `skill/`, không lên Hub). Retro gộp inbox vào file này; dòng đã gộp tự biến khỏi tập dòng riêng của máy.
18
+
19
+ ## Câu hỏi cổng 1
20
+
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
+
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
+ - 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
+
26
+ ## Bài học
27
+
28
+ - [2026-10-12] plan-design,search | * | steps.json — 23 agent dùng task_type tự đặt (bug-fix, config, shell, docs, design, plan, deploy, research, art-fix) nên mất prior, rơi về opus/medium → chỉ dùng 12 loại cố định + art-gen; bug-fix→debug, config/shell/deploy→mechanical-edit, docs→content-write, design/plan→plan-design, research→research-synth, art-fix→art-gen
29
+ - [2026-10-12] feature-impl,content-write,debug,art-gen | * | quota — dừng vì hết usage limit bị ghi thành timeout (5/18 lượt không pass) → báo "STATUS: partial — quota-pause", không bao giờ báo timeout
30
+ - [2026-10-11] [orch] | * | state/log — lấy tier --approved từ `dieu-phoi state` (lần duyệt mới nhất), xem lại sau mỗi lần duyệt lại, không theo trí nhớ; yêu cầu nhầm thì `dieu-phoi discard --task <id>`, không sửa tay events.jsonl
31
+ - [2026-10-12] [orch] | * | verdict — 4 verdict ghi "điều phối viên chạy lại test" → verdict chỉ dựa trên file dp-out verify của verifier; còn nghi thì giao verifier mới, không tự chạy
32
+ - [2026-10-12] [orch] | * | cổng 3 — chỉ 16% task có verdict người dùng nên retro học trên verdict agent → ở cổng 3 hỏi nghiệm thu từng bước; bước chưa đạt reject theo task, phần còn lại `verdict --root <root> accept`
33
+ - [2026-10-16] * | * | report — Claude Code chặn cứng Write trong subagent khi tên file khớp `/^(REPORT|SUMMARY|FINDINGS|ANALYSIS).*.md$/i` ("Subagents should return findings as text"; không cấu hình được, chạy trước permissions) → mọi file kết quả của exec/verify/scout đặt tên `dp-out-<task>-<role>-try<n>.md` (handoff-*.md không bị chặn); ghi hỏng thì Bash heredoc, vẫn hỏng thì trả inline (hook tự lưu `.inline.md`); áp dụng cả file sản phẩm trong repo (REPORT.md bị chặn 2026-10-17) → đặt tên khác, vd. RESULTS.md / docs/<topic>.md
34
+ - [2026-10-13] feature-impl | * | web/brand — QA headless bỏ sót lỗi cache cũ → trước khi deploy yêu cầu cache-busting `?v=` cho css/js và layout vẫn đúng khi không có CSS/JS (inline SVG)
35
+ - [2026-10-11] art-gen | * | qis — agent in hub key từ qis-information.md ra output công cụ → brief ghi rõ: đọc file trong script, không cat/grep/in nội dung
36
+ - [2026-10-11] feature-impl,test-write | * | lanes — DoD của bước lane phải chạy mọi smoke mode chạm màn hình bước đổi; smoke mode mới chờ theo điều kiện, không đếm tap cố định (PB)
37
+ - [2026-10-13] art-gen,feature-impl | * | brand/logo — logo nhiều lớp: mọi xử lý tương phản (viền/glow) áp dụng giống nhau cho mọi lớp của logo, không chỉ một lớp
38
+ - [2026-10-13] feature-impl,debug,test-write | * | unity-lanes — nhiều editor Unity cùng máy dẫm nhau (khớp process lỏng, đóng nhầm editor, play không focus đứng hình, C: đầy) → brief bước Unity đọc <skill>/profile/unity-lanes.md mục 4-11
39
+ - [2026-10-13] plan-design | * | unity-lanes — kế hoạch Unity dài chạy tuần tự → planner đọc unity-lanes.md mục 1-3, ghi unity_lane/unity_lane_2 + bước integrator
40
+ - [2026-10-13] plan-design,feature-impl | * | unity-lanes — nhiều máy cùng repo game → planner gán thêm `machine` cho bước Unity; claim bước bằng commit+push, push thường (không force); đọc unity-lanes.md mục "Nhiều máy"
41
+ - [2026-10-13] [orch] | * | unity-lanes — bước Unity song song/nhiều máy → planner gán unity_lane (+machine), mỗi lane một agent, sau mỗi bước lane có bước integrator trên trunk; editor/khoá/claim: <skill>/profile/unity-lanes.md
42
+ - [2026-10-13] feature-impl,debug,test-write | * | stuck — git/Unity "treo" thường là hộp thoại modal ẩn (UnityYAMLMerge, Safe Mode) → soi cây process con + tên cửa sổ; git qua lanes.py có timeout; chỉ kill PID mình; unity-lanes.md mục 14
43
+ - [2026-10-13] test-write,feature-impl | * | device-qa — QA MuMu chơi save thật của người dùng, adb hay chết → sao lưu save app trước, khôi phục sau; adb offline: kill-server, MuMuManager restart, đọc lại cổng
44
+ - [2026-10-13] [orch] | integrate | trunk — phiên khác commit/push trunk trong lúc integrator đang test → mọi commit/push trên trunk phải tôn trọng MERGE_LOCK; lượt tap đầu của smoke chờ theo điều kiện ≤ 30 s khi nhiều editor cùng chạy
45
+ - [2026-10-13] [orch] | * | stuck — agent im > min(15 phút, ½ budget_s) hoặc lặp một trạng thái → SendMessage hỏi đang chờ gì; 10 phút không tiến → steer theo unity-lanes.md mục 14; không ngồi chờ, "Waiting." lặp thì không trả lời
46
+ - [2026-10-16] [orch] | feature-impl | brief — brief trỏ 4+ báo cáo/hợp đồng làm exec chạm ngưỡng handoff (200k) trước khi viết code → chỉ trỏ bước + báo cáo đầu vào trực tiếp; agent tiếp nối chỉ đọc handoff
47
+ - [2026-10-18] feature-impl | * | demo-match — game làm sát demo: test xanh không chứng minh khớp hình ảnh → exec đo tỉ lệ (px/ô, nhân vật, khung UI) từ ảnh demo tham chiếu, dựng sprite theo demo; verifier so ảnh chụp cạnh demo
48
+ - [2026-10-18] feature-impl | * | process — `taskkill /F /IM node.exe` giết tiến trình node của agent song song/người dùng → chỉ dừng PID mình khởi chạy
49
+ - [2026-10-18] feature-impl | * | web-audio — Chromium desktop xanh không đủ cho game web có âm thanh → nghiệm thu gồm Safari iOS/Chrome Android thật hoặc WebKit e2e (unlock/resume AudioContext, định dạng)
50
+ - [2026-10-18] [orch] | * | ports — exec song song trên cùng repo chạy e2e/preview tranh cổng 4173 và dist → brief ghi cổng riêng (--port) hoặc gộp e2e vào 1 verifier sau khi các bước song song xong
51
+ - [2026-10-18] feature-impl | sonnet/high | handoff — era 200k: 3/9 chuỗi handoff sonnet không đạt (0/12 opus) → bước feature-impl lớn (dự đoán > 200k) giao opus/medium, hoặc tách bước ngay ở kế hoạch; không leo tier giữa chuỗi
52
+ - [2026-10-22] [orch] | feature-impl | handoff — 10-22: 15/63 task handoff ≥ 2 lần (median $16 so với $2,3), cq7yn1ws 10 handoff $70 rồi fail; 10-26: phiên 36cee993 vẫn 25/66 task ≥ 2 handoff, 11 task tới handoff thứ 3 (phiên khác 2/87) → sau handoff thứ 2 của một task: dừng tiếp nối, giao planner chia phần còn lại thành bước mới (task_id mới); nhận handoff còn > 1 hạng mục thì giao thành các bước riêng; ở cổng 1, bước feature-impl đụng > 1 màn hình/module lớn thì tách sẵn
53
+ - [2026-10-18] [orch] | * | handoff — 2 chuỗi đóng ở `partial — handoff` không có agent tiếp nối mà vẫn pass → STATUS handoff luôn phải có agent tiếp nối hoặc verifier kiểm phần còn lại trước verdict
54
+ - [2026-10-18] review,verify | haiku/* | dp-out — 10/24 file .inline.md ở era B là của verifier → brief verify: Write dp-out trước khi trả VERDICT
55
+ - [2026-10-18] feature-impl,content-write | * | edit — s32b xoá nhầm 41 dòng design.md bằng script regex/sed → sửa docs/markdown bằng Edit với chuỗi khớp duy nhất, không dùng script thay khối; backup file trước khi sửa hàng loạt
56
+ - [2026-10-18] [orch] | * | verify — giao verifier sớm thì nó đọc dp-out rỗng và trả fail giả → chỉ giao verifier SAU khi nhận task-notification completed của chính exec đó
57
+ - [2026-10-22] [orch] | * | tag — 76 dp-exec ($407) mất task_id vì `[dp:<id>|…]`; 10-26 còn 10 nhãn lạ; 11-05 hook đã tự đổi 15 bí danh, còn 10 bad-type (test-run, test-verify, device-qa, git-ops, release-build, read-scan, untagged), và sau compact từng viết root=/step= → tag luôn đủ `[dp:<task_type>|s<n>|<urgency>|try<n>|task=<id>|role=…]`, task_type lấy đúng chuỗi `choose --json` trả về; quét/đọc → search, architecture → plan-design, shell/git-ops/release-build → mechanical-edit, test-run/device-qa → test-write
58
+ - [2026-10-22] [orch] | * | agent — 9 agent general-purpose chạy tới 311–367k (không có watchdog handoff) → mọi việc điều phối dùng dp-scout/dp-exec/dp-verify; general-purpose chỉ khi `doctor` báo chưa cài
59
+ - [2026-10-26] [orch] | * | session — 10-26: phiên 36cee993 (168 lượt, 946k, 20 nén, 73% chi phí); 11-05: phiên d8fe5944 vẫn chạy 10-25..11-04, 286 lượt, ngữ cảnh 956k, 27 nén, chi phí điều phối $673 (66% chi phí điều phối của kỳ), 0 verdict người dùng, 31 task ≥ 2 handoff, 273 lượt exec không chạy `choose` → khi `choose`/`ctx` báo `new_session` (ngữ cảnh > 400k hoặc ≥ 5 nén): ghi trạng thái (plan.md + backlog), trình cổng 3 các bước đã xong, mở phiên mới; không giao bước mới trong phiên cũ
60
+ - [2026-10-26] [orch] | * | knowledge — kho chỉ gộp delta khi người dùng `accept`; 11-05: 73 delta đọng > 24h, đa số thuộc task chỉ có verdict agent, nhưng 16 task ĐÃ accept vẫn chưa gộp (7 task không có trong plan file nên accept không tìm thấy run dir; 3 xung đột build-test) → ở cổng 3 dùng `verdict --root <root> accept` và ĐỌC dòng "knowledge (tự gộp khi accept)": không có dòng đó mà run có delta của task → `knowledge merge --run <root> --task <id> --dry-run` rồi chạy thật; bước thêm ngoài kế hoạch cũng phải vào plan file; header `repo:` là đường dẫn tuyệt đối
61
+ - [2026-10-26] [orch] | * | input-recall — sau khi nén ngữ cảnh, điều phối viên báo "nội dung X còn chờ người khác" dù người dùng đã giao đủ nguồn trong run dir → trước khi báo việc gì "chờ người dùng/bên ngoài": grep plan.md + mở thư mục nguồn đã giao; việc đã giao mà chưa xếp lane ghi backlog "chưa làm", không ghi "chờ"
62
+ - [2026-10-26] [orch] | * | e2e-base — exec s7 chạy e2e mcp/install/integrity quên đặt BASE nên gọi prod skillhub.pixon.cc (chỉ đọc, token sai) → brief chạy e2e luôn ghi rõ BASE=localhost và bắt agent kiểm biến trước khi chạy
63
+ - [2026-10-26] [orch] | feature-impl,debug,test-write | unity-lanes — agent nào cũng tự mở/đóng Unity, worktree mới mỗi bước → trần 3 editor/máy, 3 slot worktree cố định (Library ấm), editor mở sẵn + cầu nối job queue (`unity_slot.py`); brief bước Unity ghi slot + "không tự chạy Unity.exe"; xoá worktree hết dùng sau khi kiểm sạch; đọc unity-lanes.md mục 2.1
64
+ - [2026-10-27] [orch] | debug,test-write,feature-impl | slow-fix-escalate — bug khó tái hiện (flaky E2E/race) ở sonnet/medium→sonnet/high: 2 lượt ~45 phút đổi test qua lại không ra gốc, người dùng phải tự nâng lên opus/medium thì 1 lượt ~16 phút chốt nguyên nhân bằng log timestamp và sửa ở code app → khi bước debug/flaky không hội tụ sau 1 lượt (tỉ lệ xanh không tăng, chưa có nguyên nhân gốc) hoặc vượt ~2× est_time, đề xuất leo thẳng opus/medium thay vì bậc kế tiếp cùng họ model; retro nên đưa ngưỡng thời gian vào luật leo thang
65
+ - [2026-11-05] [orch] | * | python-stdin — máy Windows này: `python3 -`/heredoc/pipe vào python qua shim pyenv-win mất stdin → REPL lặp vô hạn (log 63 GB đầy C:); agent đã vi phạm 2 lần dù có luật → brief ghi nguyên văn: "chỉ chạy Python bằng `python file.py` (ghi script ra file), không bao giờ qua stdin"; giới hạn output lệnh nền
66
+ - [orch] Parallel dp-exec in one worktree: an agent staging a shared file swept another's WIP and left HEAD uncompilable; give each parallel agent its own worktree, or require py_compile at HEAD + clean git status before reporting.
67
+ - [2026-11-02] [orch] | feature-impl | ctx-ceiling — prototype game Three.js+WS ở sonnet/medium: 3 bước liên tiếp handoff ở ~210k token (server+client+smoke trong 1 bước) → chia bước theo lớp: (1) shared+server+test, (2) client+smoke+push; dặn commit phần xanh trước handoff
68
+ - [2026-11-03] [orch] | feature-impl | netcode-smooth — prototype multiplayer qua verifier (smoke/test xanh) nhưng người dùng thấy di chuyển giật + camera không ra TPS hành động: tiêu chí máy không bắt cảm giác chơi → brief game client phải ghi rõ nội suy/snapshot buffer + client prediction và tham chiếu camera của game TPS cụ thể; verifier chụp/quay vài khung để đánh giá
69
+ - [2026-11-03] [orch] | art-gen,feature-impl | asset-tier — người dùng chỉ định: việc model nhân vật (mọi dự án; sonnet/medium ra bản diorama "cute" chưa đủ) và MỌI việc dựng asset của Anti Sleep City (nhân vật, NPC, monster, cosmetic, props, VFX, UI art) giao opus/medium bằng `--override`; brief trỏ asset guideline (docs/asset-guideline + PAS) khi đã có
70
+ - [2026-11-03] [orch] | deploy,smoke | prod-cleanup — smoke trên prod: script dọn dẹp revoke cả token thật (pixi) của người dùng → brief smoke prod phải buộc chỉ thu hồi/xoá đúng id token/user do chính lượt đó tạo (ghi id lúc tạo), cấm revoke theo tên/pattern/toàn bộ.
71
+
72
+ - [2026-11-03] review,feature-impl | * | docker-cleanup — verifier kill "tiến trình nghe cổng pg/redis của mình" (2kaq9gvq M1) nhưng đó là backend Docker Desktop, mọi container trên máy dừng → dọn bằng `docker compose -p <project> down -v`, chỉ kill PID node/vite do chính mình spawn (ghi PID lúc spawn); KHÔNG kill theo cổng, KHÔNG kill com.docker.*/wsl*/vmmem
73
+ - [orch] home-school: không cho agent `docker exec`/psql/pg_isready vào DB prod (Postgres PID 1 không init → tiến trình mồ côi exit 2 gây crash recovery); hạn chế chạy song song test/DB container khi ổ D HDD nghẽn (docker vhdx trên D:).
74
+ - [orch] feature-impl port lớn (Rust parity): agent opus/medium hay chạm 200k ngữ cảnh trước khi commit — brief phải ép 'build+commit từng phần ngay', đọc file lớn theo đoạn, và tách bước >8 người-ngày thành bước con trước khi giao
75
+ - [2026-11-04] [orch] | debug,security | argv-secret — brief điều tra tiến trình có secret trong argv phải cấm ps -ef/ps aux/cat /proc/*/cmdline (chỉ grep -c), agent haiku đã in argv làm lộ token GitLab trong phiên
76
+ - [2026-11-04] [orch] | debug,security | secret-file-read — brief sửa webapp/deploy phải cấm sed/cat/grep không -c lên /etc/comfy-studio/* và mọi file secret (chỉ stat/ls/grep -c), agent sonnet đã in admin-initial-password ra tool output
77
+ - [orch] Agent kiểm prod viết lệnh git grep sai (pattern/ref đảo) nên kết luận ngược; brief kiểm code phải ghi sẵn lệnh git grep đúng cú pháp hoặc yêu cầu trích dòng code làm bằng chứng.
78
+ - [2026-11-05] [orch] | * | orphan — 143/2.423 agent (5,9%) kết thúc không có dòng STATUS (đã chết giữa chừng), 117 không được giao lại, 42 không có verdict nào; giao lại sau p50 31 phút, p75 71, tối đa 12 giờ; sau nén ngữ cảnh tỉ lệ này cao gấp 3 lần (10% so với 3%); từng báo s16 "đang chạy" nhiều giờ khi không còn agent → sau mỗi restart/compact/handoff/stop: lấy danh sách agent đang chạy (TaskStop lỗi liệt kê) đối chiếu với các bước báo "đang chạy"; bước không có agent hoặc dp-out chỉ có `.inline.md` dở dang/không STATUS → giao lại ngay (try+1), không chờ thông báo sẽ không tới
79
+ - [orch] Trước khi giao một bước, xem tên/đặc tả trong steps có phải deploy không. Nếu đúng: choose --publish y, hỏi người dùng go-ahead rồi trích nguyên văn vào brief, kẻo exec dừng blocked (bwkuqdna s8, 05/11).
80
+ - [orch] Trước khi giao bước, kiểm depends_on của nó đã merge vào nhánh tích hợp chưa (g4 giao trước khi g1 merge → blocked).
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
+ - [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
+ - [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).