@kienntpixon/dieu-phoi 0.7.4 → 0.7.5
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 +10 -0
- package/bin/dieu-phoi.cjs +5 -1
- package/hook.cjs +1 -1
- package/lib/args.cjs +1 -1
- package/lib/commands.cjs +3 -0
- package/lib/knowledge.cjs +19 -6
- package/lib/worktree.cjs +166 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog — @kienntpixon/dieu-phoi
|
|
2
2
|
|
|
3
|
+
## 0.7.5 (2026-11-06)
|
|
4
|
+
|
|
5
|
+
Đi cùng skill 1.6.5. `CORE_VERSION` 0.7.5. Không đổi sink (`sheet/Code.gs`, `fixtures/` giữ nguyên).
|
|
6
|
+
**Sau khi cài:** `npm i -g @kienntpixon/dieu-phoi` + `dieu-phoi setup` + `dieu-phoi doctor`.
|
|
7
|
+
|
|
8
|
+
- **`wt clean --merged [--repo R] [--yes]`:** gỡ worktree có dấu và nhánh local khi nhánh đã vào HEAD checkout chính. Điều kiện: tuổi ≥ 1 giờ (worktree mới chưa commit luôn "đã merge"), verdict gần nhất của task không thuộc reject/fail/timeout/aborted, sạch, không agent sống. Dry-run mặc định; `git branch -d` (không force).
|
|
9
|
+
- **Nhắc sau nghiệm thu:** `verdict --root … accept` / `verdict --task … accept` của người dùng in một dòng số worktree đã merge của repo; `--clean-merged` gỡ luôn (như `wt clean --merged --yes`).
|
|
10
|
+
- **`wt add --base <ref>`:** tạo nhánh từ commit chọn được, sửa known issue 0.7.4 (trước đây luôn từ HEAD checkout chính). Ở mức cảnh báo 75% hoặc khi bị chặn trần, `wt add` gợi ý worktree của bước đã xong có thể tái dùng (sạch, không agent sống, verdict pass/accept); không tự dùng lại.
|
|
11
|
+
- **Knowledge delta, header `repo:`:** nhận cả dạng hiển thị `repo: <tên> (<tag>) — <dir>` (chép từ `knowledge get`) lẫn đường dẫn thô, LF/CRLF. Lỗi nay nói đúng nguyên nhân: tên ngắn không khớp kho nào / thư mục trong header không còn (worktree đã gỡ hoặc header sai) / đường dẫn không tồn tại.
|
|
12
|
+
|
|
3
13
|
## 0.7.4 (2026-11-06)
|
|
4
14
|
|
|
5
15
|
Đi cùng skill 1.6.4. `CORE_VERSION` 0.7.4. Không đổi sink (`sheet/Code.gs` giữ nguyên).
|
package/bin/dieu-phoi.cjs
CHANGED
|
@@ -35,10 +35,11 @@ Cách dùng: dieu-phoi <lệnh> [tuỳ chọn]
|
|
|
35
35
|
verdict --root <root_task_id> accept [--steps a,b]
|
|
36
36
|
người dùng nhận mọi bước đã giao exec của kế hoạch đã duyệt chưa có verdict người dùng
|
|
37
37
|
accept (--task/--root) tự gộp knowledge-delta-<task_id>*.md của run vào kho (--no-knowledge: bỏ)
|
|
38
|
+
accept của người dùng nhắc số worktree đã merge của repo (một dòng); --clean-merged: gỡ luôn (wt clean --merged --yes)
|
|
38
39
|
discard --task <task_id> [--reason R]
|
|
39
40
|
loại một task nhầm khỏi thống kê/choose/state (ghi sự kiện discard, không sửa log);
|
|
40
41
|
gỡ worktree có dấu của task nếu sạch (còn thay đổi thì giữ và báo)
|
|
41
|
-
wt add --task <task_id> [--repo R] [--path D] [--branch B] [--force] [--deps auto|link|install|none] [--deps-path D]
|
|
42
|
+
wt add --task <task_id> [--repo R] [--path D] [--branch B] [--base REF] [--force] [--deps auto|link|install|none] [--deps-path D]
|
|
42
43
|
tạo worktree cho một bước: <cha repo>/<tên repo>-wt-<task_id> trên nhánh dp/<task_id>,
|
|
43
44
|
có dấu dieu-phoi (điều phối viên dùng lệnh này, không git worktree add tay).
|
|
44
45
|
Repo đủ max_worktrees_repo (8; đếm cả checkout chính) thì từ chối kèm ứng viên dọn; --force vượt trần.
|
|
@@ -48,6 +49,9 @@ Cách dùng: dieu-phoi <lệnh> [tuỳ chọn]
|
|
|
48
49
|
wt clean [--repo R] [--stale-days N] [--yes]
|
|
49
50
|
liệt kê worktree có dấu mà task đã nghiệm thu / bị loại, thư mục đã mất, hoặc sạch + nhánh đã merge
|
|
50
51
|
+ cũ ≥ N ngày (mặc định wt_stale_days 7); mặc định dry-run, --yes mới gỡ (còn thay đổi luôn giữ; nhánh giữ nguyên)
|
|
52
|
+
wt clean --merged [--repo R] [--yes]
|
|
53
|
+
gỡ worktree có dấu mà nhánh đã vào HEAD checkout chính, có verdict (không reject/fail), tuổi ≥ 1 giờ,
|
|
54
|
+
không agent sống, sạch; rồi git branch -d nhánh. Dry-run mặc định; không đụng checkout chính / worktree không dấu
|
|
51
55
|
wt list [--repo R] [--all] [--json]
|
|
52
56
|
bảng worktree có dấu: task, tuổi, sạch/bẩn, node_modules (link|dir|none), deps của dấu; --all thêm worktree không dấu (chỉ báo)
|
|
53
57
|
steer <agent_id> --kind minor|plan|redirect [--reason R]
|
package/hook.cjs
CHANGED
|
@@ -28,7 +28,7 @@ const path = require('path');
|
|
|
28
28
|
const os = require('os');
|
|
29
29
|
const crypto = require('crypto');
|
|
30
30
|
|
|
31
|
-
const CORE_VERSION = '0.7.
|
|
31
|
+
const CORE_VERSION = '0.7.5';
|
|
32
32
|
const SCHEMA = 1;
|
|
33
33
|
const TASK_TYPES = [
|
|
34
34
|
'search', 'extract', 'mechanical-edit', 'feature-impl', 'debug', 'refactor',
|
package/lib/args.cjs
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const core = require('../hook.cjs');
|
|
6
6
|
|
|
7
|
-
const BOOLEAN_FLAGS = new Set(['json', 'help', 'h', 'purge', 'version', 'v', 'dry-run', 'no-sync', 'approve', 'yes', 'force', 'follow', 'remove', 'dirty', 'no-cache', 'untagged', 'no-knowledge', 'net', 'with-codebase-memory', 'cbm', 'allow-custom-source', 'auto', 'include-local', 'orphans']);
|
|
7
|
+
const BOOLEAN_FLAGS = new Set(['json', 'help', 'h', 'purge', 'version', 'v', 'dry-run', 'no-sync', 'approve', 'yes', 'force', 'follow', 'remove', 'dirty', 'no-cache', 'untagged', 'no-knowledge', 'net', 'with-codebase-memory', 'cbm', 'allow-custom-source', 'auto', 'include-local', 'orphans', 'merged', 'clean-merged']);
|
|
8
8
|
|
|
9
9
|
/** Minimal argv parser: positionals, --k v, --k=v, boolean flags. */
|
|
10
10
|
function parseArgs(argv) {
|
package/lib/commands.cjs
CHANGED
|
@@ -754,6 +754,8 @@ function cmdVerdict(opts) {
|
|
|
754
754
|
const kn = knowledgeOnAccept(opts, ctx, root, [root].concat(list.map((x) => x.ev.task_id)), true, events);
|
|
755
755
|
// s17: accept của người dùng gỡ worktree có dấu của từng bước (sạch); còn thay đổi thì giữ và báo.
|
|
756
756
|
const wtLines = list.reduce((acc, x) => acc.concat(wt.cleanupTask(ctx, events, x.ev.task_id, opts.repo)), []);
|
|
757
|
+
// s4 (0.7.5): nhắc số worktree đã merge của repo; --clean-merged thì gỡ luôn (wt clean --merged --yes).
|
|
758
|
+
wtLines.push(...wt.mergedAfterAccept(ctx, list.map((x) => x.ev.task_id), opts.repo, opts['clean-merged'] === true));
|
|
757
759
|
if (!list.length) {
|
|
758
760
|
// Every step was already judged (e.g. accepted one by one with --task): still merge the root + untagged deltas.
|
|
759
761
|
// Exit code follows the merge (0 merged or nothing new, 2 conflict or reject, 1 error); no delta at all keeps the old "nothing to do" error.
|
|
@@ -775,6 +777,7 @@ function cmdVerdict(opts) {
|
|
|
775
777
|
const kn = ev.by === 'user' && ev.outcome === 'accept' ? knowledgeOnAccept(opts, ctx, rootOfTask(ctx, ev.task_id), [ev.task_id], false, events) : null;
|
|
776
778
|
// s17: người dùng accept → gỡ worktree có dấu của task (sạch); reject giữ nguyên để làm lại.
|
|
777
779
|
const wtLines = ev.by === 'user' && ev.outcome === 'accept' ? wt.cleanupTask(ctx, events, ev.task_id, opts.repo) : [];
|
|
780
|
+
if (ev.by === 'user' && ev.outcome === 'accept') wtLines.push(...wt.mergedAfterAccept(ctx, [ev.task_id], opts.repo, opts['clean-merged'] === true));
|
|
778
781
|
if (opts.json) {
|
|
779
782
|
if (kn && kn.out) process.stderr.write(`${kn.out}\n`);
|
|
780
783
|
if (wtLines.length) process.stderr.write(`${wtLines.join('\n')}\n`);
|
package/lib/knowledge.cjs
CHANGED
|
@@ -521,7 +521,7 @@ function splitList(v, lower) {
|
|
|
521
521
|
/** Parse a delta file: { repo, head, sections: [{ add, rawId, id, title?, tags?, paths?, base?, body, lines }] }. */
|
|
522
522
|
function parseDelta(text) {
|
|
523
523
|
const lines = String(text).replace(/^/, '').split(LINE_BREAK);
|
|
524
|
-
const out = { repo: null, head: null, sections: [] };
|
|
524
|
+
const out = { repo: null, repoDir: null, head: null, sections: [] };
|
|
525
525
|
let cur = null;
|
|
526
526
|
let fence = false;
|
|
527
527
|
for (const line of lines) {
|
|
@@ -534,7 +534,16 @@ function parseDelta(text) {
|
|
|
534
534
|
}
|
|
535
535
|
if (cur) { cur.raw.push(line); continue; }
|
|
536
536
|
const m = /^\s*(repo|head)\s*:\s*(.+?)\s*$/i.exec(line);
|
|
537
|
-
if (m && !out[m[1].toLowerCase()])
|
|
537
|
+
if (m && !out[m[1].toLowerCase()]) {
|
|
538
|
+
const val = unquote(m[2].replace(/[.;,]+$/, '')).replace(/[.;,]+$/, '');
|
|
539
|
+
const k = m[1].toLowerCase();
|
|
540
|
+
// display form written by repoLine(): `name (tag) — dir` -> name + dir
|
|
541
|
+
const dm = k === 'repo' ? /^(.*?)\s+—\s+(.+)$/.exec(val) : null;
|
|
542
|
+
if (dm) {
|
|
543
|
+
out.repo = dm[1].replace(/\s*\([^)]*\)\s*$/, '').trim() || dm[2].trim();
|
|
544
|
+
out.repoDir = dm[2].trim();
|
|
545
|
+
} else out[k] = val;
|
|
546
|
+
}
|
|
538
547
|
}
|
|
539
548
|
for (const s of out.sections) {
|
|
540
549
|
s.id = s.rawId.toLowerCase();
|
|
@@ -821,11 +830,15 @@ function targetOf(delta, opts, rep) {
|
|
|
821
830
|
}
|
|
822
831
|
}
|
|
823
832
|
if (delta.repo) {
|
|
824
|
-
const
|
|
833
|
+
const shown = delta.repoDir ? `${delta.repo} — ${delta.repoDir}` : delta.repo;
|
|
834
|
+
const p = path.resolve(core.nativePath(delta.repoDir || delta.repo));
|
|
825
835
|
if (isDir(p)) return finish(p);
|
|
826
|
-
if (opts.repo !== undefined) { rep.note = `repo: ${
|
|
827
|
-
const bare = core.isBareRepoName(delta.repo);
|
|
828
|
-
|
|
836
|
+
if (opts.repo !== undefined) { rep.note = `repo: ${shown} không còn — dùng --repo`; return finish(repoOf(opts)); }
|
|
837
|
+
const bare = !delta.repoDir && core.isBareRepoName(delta.repo);
|
|
838
|
+
const why = bare ? 'tên ngắn không khớp kho nào đã biết (chưa có kho tri thức?) và không phải thư mục'
|
|
839
|
+
: delta.repoDir ? `thư mục ${slash(delta.repoDir)} không tồn tại (worktree đã xoá, hoặc đường dẫn trong header sai?)`
|
|
840
|
+
: 'không tồn tại (worktree đã xoá, hoặc header repo: sai?)';
|
|
841
|
+
return err(`repo: ${shown} ${why} — chạy lại với --repo <đường dẫn repo>`);
|
|
829
842
|
}
|
|
830
843
|
return err('không xác định được repo');
|
|
831
844
|
function finish(target) {
|
package/lib/worktree.cjs
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
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.5 (s4):
|
|
11
|
+
// - `wt clean --merged`: gỡ worktree có dấu mà nhánh đã vào HEAD checkout chính, có verdict (không reject/fail),
|
|
12
|
+
// tuổi ≥ 1 giờ, không agent sống, sạch; rồi `git branch -d`. verdict accept của người dùng chỉ nhắc (--clean-merged: gỡ).
|
|
10
13
|
// core 0.7.3 (s2):
|
|
11
14
|
// - 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
15
|
// (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.
|
|
@@ -31,6 +34,11 @@ const BRANCH_PREFIX = 'dp/';
|
|
|
31
34
|
const DAY_MS = 86400000;
|
|
32
35
|
const DEFAULT_MAX_WORKTREES = 8;
|
|
33
36
|
const DEFAULT_STALE_DAYS = 7;
|
|
37
|
+
// s3 (core 0.7.5): verdict cuối không thuộc nhóm này thì bước chưa xong (nhánh không có commit mới vẫn "đã merge").
|
|
38
|
+
const NOT_DONE = new Set(['reject', 'fail', 'timeout', 'aborted']);
|
|
39
|
+
// s4 (0.7.5): `wt clean --merged` chỉ gỡ worktree tuổi ≥ 1 giờ có verdict (pass/accept). Worktree vừa tạo có 0 commit
|
|
40
|
+
// nên "đã merge" tầm thường; không có ngưỡng này thì bước đang chờ agent đầu tiên có thể bị gỡ.
|
|
41
|
+
const MERGED_MIN_AGE_MS = 60 * 60 * 1000;
|
|
34
42
|
|
|
35
43
|
function git(cwd, args) {
|
|
36
44
|
const r = cp.spawnSync('git', args, { cwd, encoding: 'utf8', windowsHide: true, timeout: 60000 });
|
|
@@ -247,6 +255,35 @@ function candidateLines(common) {
|
|
|
247
255
|
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
256
|
}
|
|
249
257
|
|
|
258
|
+
/**
|
|
259
|
+
* s3 (core 0.7.5): worktree có dấu của bước đã xong, có thể tái dùng. Điều kiện: còn thư mục, sạch, không có agent
|
|
260
|
+
* sống, task không bị loại, và (verdict cuối của task là pass/accept, hoặc nhánh đã vào HEAD mà task đã có verdict).
|
|
261
|
+
* Nhánh "đã merge" một mình không đủ: worktree vừa tạo, chưa commit, cũng có 0 commit ngoài HEAD.
|
|
262
|
+
* Chỉ để báo, không bao giờ tự dùng lại. Không có ứng viên thì trả chuỗi rỗng.
|
|
263
|
+
*/
|
|
264
|
+
function reusableLines(ctx, main, common) {
|
|
265
|
+
const events = core.readEvents(ctx);
|
|
266
|
+
const gone = agg.discardedTaskIds(events);
|
|
267
|
+
const last = new Map();
|
|
268
|
+
for (const e of events) {
|
|
269
|
+
if (e && e.kind === 'verdict' && typeof e.task_id === 'string') last.set(e.task_id, agg.normalizeVerdict(e).outcome);
|
|
270
|
+
}
|
|
271
|
+
const rows = [];
|
|
272
|
+
for (const w of markedWorktrees(common)) {
|
|
273
|
+
if (!w.exists || gone.has(w.task_id)) continue;
|
|
274
|
+
const v = last.get(w.task_id);
|
|
275
|
+
let why = null;
|
|
276
|
+
if (v === 'pass' || v === 'accept') why = `verdict ${v}`;
|
|
277
|
+
else if (v !== undefined && !NOT_DONE.has(v) && branchMerged(main, w.branch)) why = 'nhánh đã vào HEAD';
|
|
278
|
+
if (!why) continue;
|
|
279
|
+
if (liveAgentInWorktree(ctx, w.path)) continue;
|
|
280
|
+
if (dirtyOf(w.path) !== false) continue;
|
|
281
|
+
rows.push(` - ${w.path} task=${w.task_id} nhánh=${w.branch || '?'} (${why})`);
|
|
282
|
+
}
|
|
283
|
+
if (!rows.length) return '';
|
|
284
|
+
return `Worktree bước đã xong có thể tái dùng (chỉ báo, không tự dùng lại; gỡ bằng dieu-phoi wt clean sau khi xem dry-run):\n${rows.join('\n')}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
250
287
|
/** Trạng thái node_modules của một worktree: link (symlink/junction), dir (thư mục thật), none. */
|
|
251
288
|
function nodeModulesOf(p) {
|
|
252
289
|
try {
|
|
@@ -256,8 +293,22 @@ function nodeModulesOf(p) {
|
|
|
256
293
|
} catch (_) { return 'none'; }
|
|
257
294
|
}
|
|
258
295
|
|
|
296
|
+
/**
|
|
297
|
+
* s3 (core 0.7.5): --base <ref> cho `wt add`. Không có --base → nhánh mới từ HEAD của checkout chính (như cũ).
|
|
298
|
+
* Ref phải là commit hợp lệ trong repo chính; không bắt đầu bằng `-` (tránh bị git hiểu là tuỳ chọn).
|
|
299
|
+
*/
|
|
300
|
+
function baseArg(opts, main) {
|
|
301
|
+
if (opts.base === undefined) return null;
|
|
302
|
+
if (typeof opts.base !== 'string' || !opts.base || opts.base.startsWith('-')) {
|
|
303
|
+
throw new Error('wt add: --base cần tên ref (nhánh, tag hoặc commit), không bắt đầu bằng "-"');
|
|
304
|
+
}
|
|
305
|
+
const v = git(main, ['rev-parse', '--verify', '--quiet', `${opts.base}^{commit}`]);
|
|
306
|
+
if (!v.ok || !v.out) throw new Error(`wt add: --base "${opts.base}" không phải commit/nhánh hợp lệ trong ${main}`);
|
|
307
|
+
return opts.base;
|
|
308
|
+
}
|
|
309
|
+
|
|
259
310
|
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]');
|
|
311
|
+
if (typeof opts.task !== 'string') throw new Error('Cú pháp: dieu-phoi wt add --task <task_id> [--repo r] [--path dir] [--branch b] [--base ref] [--force] [--deps auto|link|install|none] [--deps-path dir]');
|
|
261
312
|
const taskId = requireTaskId(opts.task);
|
|
262
313
|
const ctx = ctxOf(opts);
|
|
263
314
|
const depsMode = deps.depsModeOf(opts, ctx);
|
|
@@ -265,22 +316,30 @@ function wtAdd(opts) {
|
|
|
265
316
|
const { main, common } = mainCheckoutOf(repoOf(opts));
|
|
266
317
|
const dup = markedWorktrees(common).filter((w) => w.task_id === taskId && w.exists);
|
|
267
318
|
if (dup.length) throw new Error(`task ${taskId} đã có worktree có dấu: ${dup.map((w) => w.path).join(', ')}`);
|
|
319
|
+
const base = baseArg(opts, main);
|
|
268
320
|
const max = maxWorktreesOf(ctx);
|
|
269
321
|
const total = registeredOf(main).length;
|
|
270
322
|
const capMsg = max > 0 && total >= max ? `repo đã có ${total}/${max} worktree (trần max_worktrees_repo)` : null;
|
|
323
|
+
const near = max > 0 && total + 1 >= warnAtOf(max);
|
|
271
324
|
if (capMsg && opts.force !== true) {
|
|
325
|
+
const hint = reusableLines(ctx, main, common);
|
|
272
326
|
throw new Error(`wt add: ${capMsg}; không tạo thêm cho task ${taskId}.\n${candidateLines(common)}\n`
|
|
327
|
+
+ (hint ? `${hint}\n` : '')
|
|
273
328
|
+ '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
329
|
}
|
|
275
330
|
const notes = [];
|
|
276
331
|
if (capMsg) notes.push(`CẢNH BÁO: vượt trần (--force): ${capMsg}`);
|
|
277
|
-
else if (
|
|
332
|
+
else if (near) notes.push(`CẢNH BÁO: repo sẽ có ${total + 1}/${max} worktree (đạt 75% trần)`);
|
|
333
|
+
if (capMsg || near) {
|
|
334
|
+
const hint = reusableLines(ctx, main, common);
|
|
335
|
+
if (hint) notes.push(hint);
|
|
336
|
+
}
|
|
278
337
|
const wtPath = typeof opts.path === 'string' && opts.path
|
|
279
338
|
? path.resolve(opts.path)
|
|
280
339
|
: path.join(path.dirname(main), `${path.basename(main)}-wt-${taskId}`);
|
|
281
340
|
if (fs.existsSync(wtPath)) throw new Error(`wt add: ${wtPath} đã tồn tại`);
|
|
282
341
|
const branch = typeof opts.branch === 'string' && opts.branch ? opts.branch : `${BRANCH_PREFIX}${taskId}`;
|
|
283
|
-
const r = git(main, ['worktree', 'add', '-b', branch, wtPath]);
|
|
342
|
+
const r = git(main, ['worktree', 'add', '-b', branch, wtPath, ...(base ? [base] : [])]);
|
|
284
343
|
if (!r.ok) throw new Error(`git worktree add lỗi: ${r.err || 'không rõ'}`);
|
|
285
344
|
const adminDir = git(wtPath, ['rev-parse', '--path-format=absolute', '--git-dir']).out;
|
|
286
345
|
if (!adminDir) throw new Error(`worktree ${wtPath} tạo xong nhưng không đọc được git-dir để ghi dấu`);
|
|
@@ -299,7 +358,7 @@ function wtAdd(opts) {
|
|
|
299
358
|
const text = [msg, d.note, warning].filter(Boolean).join('\n');
|
|
300
359
|
return {
|
|
301
360
|
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,
|
|
361
|
+
out: opts.json ? JSON.stringify({ task_id: taskId, path: wtPath, branch, base: base || null, total: total + 1, max, warning, deps: mark.deps, deps_path: depsPath, deps_note: d.note }) : text,
|
|
303
362
|
};
|
|
304
363
|
}
|
|
305
364
|
|
|
@@ -404,14 +463,114 @@ function wtList(opts) {
|
|
|
404
463
|
}
|
|
405
464
|
|
|
406
465
|
/** `dieu-phoi wt add|clean|list` (core 0.7.2 s17; 0.7.3 s2: trần, --stale-days, list). */
|
|
466
|
+
// ---- s4 (0.7.5): wt clean --merged và nhắc sau verdict accept ----
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Worktree có dấu mà nhánh đã vào HEAD của checkout chính (không gồm checkout chính). go = gỡ được: có verdict không
|
|
470
|
+
* phải reject/fail/timeout/aborted, tuổi ≥ MERGED_MIN_AGE_MS, không agent sống, sạch. kept = [{ w, why }] còn lại kèm
|
|
471
|
+
* lý do. Chỉ đọc; cũ nhất trước.
|
|
472
|
+
*/
|
|
473
|
+
function mergedPlan(ctx, main, common, events) {
|
|
474
|
+
const last = new Map();
|
|
475
|
+
for (const e of events) {
|
|
476
|
+
if (e && e.kind === 'verdict' && typeof e.task_id === 'string') last.set(e.task_id, agg.normalizeVerdict(e).outcome);
|
|
477
|
+
}
|
|
478
|
+
const tsOf = (w) => (w.created_ts && Number.isFinite(Date.parse(w.created_ts)) ? Date.parse(w.created_ts) : Infinity);
|
|
479
|
+
const go = [];
|
|
480
|
+
const kept = [];
|
|
481
|
+
for (const w of markedWorktrees(common).sort((a, b) => tsOf(a) - tsOf(b))) {
|
|
482
|
+
if (!w.exists || !w.branch || pathKey(w.path) === pathKey(main)) continue;
|
|
483
|
+
if (!branchMerged(main, w.branch)) continue;
|
|
484
|
+
const v = last.get(w.task_id);
|
|
485
|
+
let why = null;
|
|
486
|
+
if (ageMsOf(w) < MERGED_MIN_AGE_MS) why = 'mới tạo dưới 1 giờ';
|
|
487
|
+
else if (v === undefined) why = 'chưa có verdict';
|
|
488
|
+
else if (NOT_DONE.has(v)) why = `verdict ${v}`;
|
|
489
|
+
else {
|
|
490
|
+
const busy = liveAgentInWorktree(ctx, w.path);
|
|
491
|
+
if (busy) why = `đang là cwd của agent sống (${busy})`;
|
|
492
|
+
else if (dirtyOf(w.path) !== false) why = 'còn thay đổi chưa commit hoặc không đọc được trạng thái git';
|
|
493
|
+
}
|
|
494
|
+
if (why) kept.push({ w, why });
|
|
495
|
+
else go.push(w);
|
|
496
|
+
}
|
|
497
|
+
return { go, kept };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Thực hiện (apply) hoặc liệt kê (dry-run) mergedPlan. Gỡ worktree bằng removeMarked (không --force), rồi
|
|
502
|
+
* `git branch -d` nhánh (từ chối nếu chưa merge vào HEAD: an toàn). Trả { n, removed, lines }. Không ném.
|
|
503
|
+
*/
|
|
504
|
+
function mergedRun(ctx, main, common, apply) {
|
|
505
|
+
const { go, kept } = mergedPlan(ctx, main, common, core.readEvents(ctx));
|
|
506
|
+
const lines = kept.map(({ w, why }) => `giữ ${w.path}: ${why} — task=${w.task_id} nhánh=${w.branch}`);
|
|
507
|
+
let removed = 0;
|
|
508
|
+
for (const w of go) {
|
|
509
|
+
const label = `task=${w.task_id} nhánh=${w.branch}`;
|
|
510
|
+
if (!apply) {
|
|
511
|
+
lines.push(`sẽ gỡ ${w.path} và xoá nhánh (git branch -d): ${label} (đã merge, có verdict, sạch, không agent sống)`);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const r = removeMarked(w, main, ctx);
|
|
515
|
+
if (!r.removed) { lines.push(`${r.msg}: ${label}`); continue; }
|
|
516
|
+
removed++;
|
|
517
|
+
recordWt(ctx, w.task_id, { action: 'remove', path: w.path, repo: main, reason: 'merged' });
|
|
518
|
+
const b = git(main, ['branch', '-d', w.branch]);
|
|
519
|
+
lines.push(`đã gỡ worktree ${w.path}; ${b.ok ? `đã xoá nhánh ${w.branch} (git branch -d)` : `giữ nhánh ${w.branch}: git branch -d lỗi (${b.err || 'không rõ'})`}: ${label}`);
|
|
520
|
+
}
|
|
521
|
+
return { n: go.length, removed, lines };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** `dieu-phoi wt clean --merged [--repo R] [--yes]`: dry-run mặc định; không đụng --stale-days hay worktree không dấu. */
|
|
525
|
+
function wtCleanMerged(opts) {
|
|
526
|
+
const ctx = ctxOf(opts);
|
|
527
|
+
const { main, common } = mainCheckoutOf(repoOf(opts));
|
|
528
|
+
const apply = opts.yes === true;
|
|
529
|
+
const r = mergedRun(ctx, main, common, apply);
|
|
530
|
+
if (!r.lines.length) return { code: 0, out: 'wt clean --merged: không có worktree có dấu nào đã merge vào HEAD.' };
|
|
531
|
+
const tail = apply ? '' : '\nDry-run: chưa xoá gì. Chạy lại với --yes để thực hiện.';
|
|
532
|
+
const head = `wt clean --merged (${apply ? 'đã thực hiện' : 'dry-run'}): ${r.n} worktree đã merge${apply ? `, đã gỡ ${r.removed}` : ''}`;
|
|
533
|
+
return { code: 0, out: `${head}\n${r.lines.join('\n')}${tail}` };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Sau verdict accept của NGƯỜI DÙNG (--task hoặc --root): một dòng nhắc số worktree đã merge của từng repo liên quan
|
|
538
|
+
* (repo lấy từ sự kiện worktree của các task, cộng --repo nếu có). clean (--clean-merged): gỡ luôn như
|
|
539
|
+
* `wt clean --merged --yes`. Không ném: lỗi thành một dòng báo.
|
|
540
|
+
*/
|
|
541
|
+
function mergedAfterAccept(ctx, taskIds, repoDir, clean) {
|
|
542
|
+
const lines = [];
|
|
543
|
+
try {
|
|
544
|
+
const events = core.readEvents(ctx);
|
|
545
|
+
const repos = new Set();
|
|
546
|
+
for (const id of taskIds) for (const r of reposOfTask(events, id, repoDir)) repos.add(r);
|
|
547
|
+
for (const repo of repos) {
|
|
548
|
+
let info;
|
|
549
|
+
try { info = mainCheckoutOf(repo); } catch (e) { lines.push(`wt --merged: bỏ qua ${repo} (${e.message})`); continue; }
|
|
550
|
+
if (clean) {
|
|
551
|
+
const r = mergedRun(ctx, info.main, info.common, true);
|
|
552
|
+
if (!r.lines.length) lines.push(`wt --clean-merged: không có worktree đã merge nào của ${info.main}`);
|
|
553
|
+
for (const x of r.lines) lines.push(`wt --clean-merged: ${x}`);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const n = mergedPlan(ctx, info.main, info.common, events).go.length;
|
|
557
|
+
if (n) lines.push(`wt: ${n} worktree đã merge của ${info.main}; dọn: dieu-phoi wt clean --merged --repo ${info.main} --yes (hoặc thêm --clean-merged vào lệnh verdict)`);
|
|
558
|
+
}
|
|
559
|
+
} catch (e) {
|
|
560
|
+
lines.push(`wt --merged: bỏ qua (${e.message})`);
|
|
561
|
+
}
|
|
562
|
+
return lines;
|
|
563
|
+
}
|
|
564
|
+
|
|
407
565
|
function cmdWt(opts) {
|
|
408
566
|
const sub = opts._[0];
|
|
409
567
|
if (sub === 'add') return wtAdd(opts);
|
|
410
|
-
if (sub === 'clean') return wtClean(opts);
|
|
568
|
+
if (sub === 'clean') return opts.merged === true ? wtCleanMerged(opts) : wtClean(opts);
|
|
411
569
|
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'
|
|
570
|
+
throw new Error('Cú pháp: dieu-phoi wt add --task <task_id> [--repo r] [--path dir] [--branch b] [--base ref] [--force] [--deps auto|link|install|none] [--deps-path dir]\n'
|
|
413
571
|
+ ' dieu-phoi wt clean [--repo r] [--stale-days N] [--yes]\n'
|
|
572
|
+
+ ' dieu-phoi wt clean --merged [--repo r] [--yes]\n'
|
|
414
573
|
+ ' dieu-phoi wt list [--repo r] [--all] [--json]');
|
|
415
574
|
}
|
|
416
575
|
|
|
417
|
-
module.exports = { cmdWt, cleanupTask, mainCheckoutOf, markedWorktrees, registeredOf, liveAgentInWorktree, MARK };
|
|
576
|
+
module.exports = { cmdWt, cleanupTask, mergedAfterAccept, 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.5",
|
|
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>",
|