@kienntpixon/dieu-phoi 0.7.3 → 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 +25 -0
- package/agents/dp-exec.md +6 -1
- package/agents/dp-helper.md +6 -0
- package/agents/dp-scout.md +7 -1
- package/agents/dp-verify.md +6 -0
- package/bin/dieu-phoi.cjs +13 -9
- package/hook.cjs +196 -129
- package/lib/aggregate.cjs +20 -2
- package/lib/args.cjs +1 -1
- package/lib/choose.cjs +27 -22
- package/lib/commands.cjs +198 -34
- package/lib/knowledge.cjs +66 -10
- package/lib/prune.cjs +81 -7
- package/lib/worktree.cjs +166 -7
- package/package.json +1 -1
- package/profile/lessons.md +0 -1
package/lib/knowledge.cjs
CHANGED
|
@@ -40,7 +40,9 @@ const KNOWLEDGE_HELP = `Cách dùng: dieu-phoi knowledge <lệnh> [--repo <đư
|
|
|
40
40
|
thư mục). Không --from thì tự lấy kho của khoá git-common-dir nếu có
|
|
41
41
|
list [--follow] [--dirty] [--json]
|
|
42
42
|
các mục của kho (id, tiêu đề, số dòng, sha#rev, tags, paths) + repo liên quan (related),
|
|
43
|
-
mỗi mục gắn ok / STALE / ? (độ tươi so với HEAD của worktree đang đứng
|
|
43
|
+
mỗi mục gắn ok / STALE / drift / ? (độ tươi so với HEAD của worktree đang đứng; drift = chỉ file
|
|
44
|
+
file đổi nhưng không làm mục stale: glob, bản version, CHANGELOG, dòng không nêu tên mục);
|
|
45
|
+
cuối dòng tổng: số mục không stale
|
|
44
46
|
status [--follow] [--dirty] [--no-cache] [--json]
|
|
45
47
|
chỉ độ tươi: mục nào stale (file nào đổi sau sha của mục), mục nào không xác định (sha không
|
|
46
48
|
có trong repo này…). Một tiến trình git (diff-tree --no-renames, pathspec = paths của các mục),
|
|
@@ -60,6 +62,8 @@ const KNOWLEDGE_HELP = `Cách dùng: dieu-phoi knowledge <lệnh> [--repo <đư
|
|
|
60
62
|
"## +id" thêm mục (bắt buộc paths:), "## id" thay mục (bắt buộc base: <sha>#<rev>); base cũ
|
|
61
63
|
→ xung đột, không ghi đè; mục > 40 dòng bị từ chối. Có xung đột/từ chối → exit 2.
|
|
62
64
|
Idempotent: file delta đã gộp (hash trong meta.json) và mục trùng một rev cũ trong history/ → "không đổi"
|
|
65
|
+
review <id> [--repo] đánh dấu mục đã xem xét: reviewed_sha = HEAD hiện tại (ghi dưới khoá kho); độ tươi chỉ tính thay đổi sau sha đó.
|
|
66
|
+
Mục bị thay bằng merge sẽ mất dấu này. Không đổi nội dung mục.
|
|
63
67
|
bootstrap [--children <mẫu tên>[,…]] [--json]
|
|
64
68
|
chỉ đọc: liệt kê nguồn để seed kho (CLAUDE.md/README/AGENTS.md của repo, context-r/contract/
|
|
65
69
|
handoff cũ trong runs có nhắc repo), mục đã có, mục nên có và mẫu delta. --children: repo anh em
|
|
@@ -272,7 +276,10 @@ function sectionOut(repo, sec, body, fr) {
|
|
|
272
276
|
const f = fr && fr.sections && fr.sections[sec.id];
|
|
273
277
|
if (f) {
|
|
274
278
|
out.fresh = f.state;
|
|
275
|
-
if (f.files) {
|
|
279
|
+
if (f.files) {
|
|
280
|
+
const k = f.state === 'drift' ? 'drift' : 'stale';
|
|
281
|
+
out[k] = f.files; out[`${k}_count`] = f.n_files || f.files.length;
|
|
282
|
+
}
|
|
276
283
|
if (f.why) out.fresh_why = f.why;
|
|
277
284
|
if (f.dirty) out.dirty = f.dirty;
|
|
278
285
|
}
|
|
@@ -299,6 +306,7 @@ function freshnessOf(storeDir, idx, start, opts) {
|
|
|
299
306
|
|
|
300
307
|
function freshLine(s) {
|
|
301
308
|
if (s.fresh === 'stale') return `STALE — ${s.stale_count} file đổi sau sha của mục: ${core.shortFileList(s.stale, 5)} (kiểm lại trước khi dùng)`;
|
|
309
|
+
if (s.fresh === 'drift') return `drift — ${s.drift_count} file đổi nhưng không tính stale: ${core.shortFileList(s.drift, 5)}`;
|
|
302
310
|
if (s.fresh === 'unknown') return `độ tươi: không xác định — ${s.fresh_why}`;
|
|
303
311
|
if (s.fresh === 'fresh') return 'độ tươi: tươi';
|
|
304
312
|
return null;
|
|
@@ -415,7 +423,7 @@ function cmdGet(opts) {
|
|
|
415
423
|
return { code, out: parts.join('\n\n') };
|
|
416
424
|
}
|
|
417
425
|
|
|
418
|
-
const FRESH_MARK = { fresh: 'ok ', stale: 'STALE', unknown: '? ' };
|
|
426
|
+
const FRESH_MARK = { fresh: 'ok ', stale: 'STALE', drift: 'drift', unknown: '? ' };
|
|
419
427
|
|
|
420
428
|
function listLines(idx, fr, dir) {
|
|
421
429
|
return idx.sections.map((s) => {
|
|
@@ -429,7 +437,8 @@ function listLines(idx, fr, dir) {
|
|
|
429
437
|
function freshHeader(fr, n) {
|
|
430
438
|
if (!fr) return '';
|
|
431
439
|
const head = fr.head ? `HEAD ${fr.head.slice(0, 7)}${fr.branch ? ` (${fr.branch})` : ''}` : 'HEAD ?';
|
|
432
|
-
|
|
440
|
+
const notStale = n - ((fr.counts && fr.counts.stale) || 0);
|
|
441
|
+
return `${head} · ${core.knowledgeSummary(fr, n)} · ${notStale} không stale · ${fr.cached ? 'cache' : `tính mới${fr.ms_git ? ` (git ${fr.ms_git} ms)` : ''}`}${fr.error ? ` · git lỗi: ${fr.error}` : ''}`;
|
|
433
442
|
}
|
|
434
443
|
|
|
435
444
|
/** Details under list/status: stale files, unknown reasons, uncommitted files (--dirty). */
|
|
@@ -440,6 +449,7 @@ function freshDetails(idx, fr) {
|
|
|
440
449
|
const f = fr.sections[s.id];
|
|
441
450
|
if (!f) continue;
|
|
442
451
|
if (f.state === 'stale') out.push(` ${s.id}: ${f.n_files || f.files.length} file đổi — ${core.shortFileList(f.files, 5)}`);
|
|
452
|
+
else if (f.state === 'drift') out.push(` ${s.id}: drift (không tính stale) — ${f.n_files || f.files.length} file đổi — ${core.shortFileList(f.files, 5)}`);
|
|
443
453
|
else if (f.state === 'unknown') out.push(` ${s.id}: không xác định — ${f.why}`);
|
|
444
454
|
if (f.dirty) out.push(` ${s.id}: chưa commit — ${core.shortFileList(f.dirty, 5)}`);
|
|
445
455
|
}
|
|
@@ -511,7 +521,7 @@ function splitList(v, lower) {
|
|
|
511
521
|
/** Parse a delta file: { repo, head, sections: [{ add, rawId, id, title?, tags?, paths?, base?, body, lines }] }. */
|
|
512
522
|
function parseDelta(text) {
|
|
513
523
|
const lines = String(text).replace(/^/, '').split(LINE_BREAK);
|
|
514
|
-
const out = { repo: null, head: null, sections: [] };
|
|
524
|
+
const out = { repo: null, repoDir: null, head: null, sections: [] };
|
|
515
525
|
let cur = null;
|
|
516
526
|
let fence = false;
|
|
517
527
|
for (const line of lines) {
|
|
@@ -524,7 +534,16 @@ function parseDelta(text) {
|
|
|
524
534
|
}
|
|
525
535
|
if (cur) { cur.raw.push(line); continue; }
|
|
526
536
|
const m = /^\s*(repo|head)\s*:\s*(.+?)\s*$/i.exec(line);
|
|
527
|
-
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
|
+
}
|
|
528
547
|
}
|
|
529
548
|
for (const s of out.sections) {
|
|
530
549
|
s.id = s.rawId.toLowerCase();
|
|
@@ -811,11 +830,15 @@ function targetOf(delta, opts, rep) {
|
|
|
811
830
|
}
|
|
812
831
|
}
|
|
813
832
|
if (delta.repo) {
|
|
814
|
-
const
|
|
833
|
+
const shown = delta.repoDir ? `${delta.repo} — ${delta.repoDir}` : delta.repo;
|
|
834
|
+
const p = path.resolve(core.nativePath(delta.repoDir || delta.repo));
|
|
815
835
|
if (isDir(p)) return finish(p);
|
|
816
|
-
if (opts.repo !== undefined) { rep.note = `repo: ${
|
|
817
|
-
const bare = core.isBareRepoName(delta.repo);
|
|
818
|
-
|
|
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>`);
|
|
819
842
|
}
|
|
820
843
|
return err('không xác định được repo');
|
|
821
844
|
function finish(target) {
|
|
@@ -1035,6 +1058,38 @@ function cmdLink(opts) {
|
|
|
1035
1058
|
return { code: 0, out: `${verb}: ${other.name} là ${opts.rel === 'child' ? 'con' : 'cha'} của ${info.name}${a.changed || b.changed ? '' : ' (không đổi)'}.` };
|
|
1036
1059
|
}
|
|
1037
1060
|
|
|
1061
|
+
/**
|
|
1062
|
+
* Review mark (s6): records reviewed_sha = HEAD of the checkout around --repo on one section, under the store lock.
|
|
1063
|
+
* freshness then counts only the changes after that sha. Body, paths and base are untouched; a merge that replaces the
|
|
1064
|
+
* section writes a new entry without the mark, so new content is never shown as reviewed.
|
|
1065
|
+
*/
|
|
1066
|
+
function cmdReview(opts) {
|
|
1067
|
+
const { start, info } = infoOf(opts, false);
|
|
1068
|
+
if (!info) return notRepo(start);
|
|
1069
|
+
const ids = opts._.map(String).filter((t) => t.trim());
|
|
1070
|
+
if (ids.length !== 1 || !ID_RE.test(ids[0])) return { code: 1, out: `Cần đúng một <id> mục.\n\n${KNOWLEDGE_HELP.trimEnd()}` };
|
|
1071
|
+
const id = ids[0];
|
|
1072
|
+
const g = core.findGit(start);
|
|
1073
|
+
const h = g ? core.readHead(g) : null;
|
|
1074
|
+
const sha = h && h.sha ? h.sha : (h && h.unsupported ? headShaOf(start) : null);
|
|
1075
|
+
if (!sha || !/^[0-9a-f]{40,64}$/i.test(sha)) return { code: 1, out: 'Không đọc được HEAD đầy đủ của repo (chưa có commit?).' };
|
|
1076
|
+
const ri = readIndex(info.dir);
|
|
1077
|
+
if (ri.error) return { code: 1, out: ri.error };
|
|
1078
|
+
if (!ri.exists) return { code: 1, out: `Kho chưa có mục nào: ${info.dir}.` };
|
|
1079
|
+
const lk = withLock(path.join(info.dir, 'knowledge.lock'), () => {
|
|
1080
|
+
const cur = readIndex(info.dir);
|
|
1081
|
+
if (cur.error) return { code: 1, out: cur.error };
|
|
1082
|
+
const at = cur.idx.sections.findIndex((s) => s.id === id);
|
|
1083
|
+
if (at < 0) return { code: 1, out: `Chưa có mục ${id} trong kho ${info.name}.` };
|
|
1084
|
+
const prev = cur.idx.sections[at].reviewed_sha || null;
|
|
1085
|
+
cur.idx.sections[at] = Object.assign({}, cur.idx.sections[at], { reviewed_sha: sha.toLowerCase(), reviewed_at: new Date().toISOString() });
|
|
1086
|
+
core.writeFileAtomic(path.join(info.dir, 'index.json'), `${JSON.stringify(cur.idx, null, 2)}\n`);
|
|
1087
|
+
return { code: 0, out: `review ${id}: reviewed_sha ${shortSha(sha)}${prev ? ` (trước: ${shortSha(prev)})` : ''}` };
|
|
1088
|
+
}, LOCK_TRIES);
|
|
1089
|
+
if (lk.locked) return { code: 1, out: 'Kho đang bị khoá (knowledge.lock), thử lại.' };
|
|
1090
|
+
return lk.value;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1038
1093
|
function cmdKnowledge(opts) {
|
|
1039
1094
|
const sub = opts._.shift();
|
|
1040
1095
|
if (!sub || opts.help || opts.h || sub === 'help') {
|
|
@@ -1045,6 +1100,7 @@ function cmdKnowledge(opts) {
|
|
|
1045
1100
|
if (sub === 'get') return cmdGet(opts);
|
|
1046
1101
|
if (sub === 'list') return cmdList(opts);
|
|
1047
1102
|
if (sub === 'status') return cmdStatus(opts);
|
|
1103
|
+
if (sub === 'review') return cmdReview(opts);
|
|
1048
1104
|
if (sub === 'merge') return cmdMerge(opts);
|
|
1049
1105
|
if (sub === 'link') return cmdLink(opts);
|
|
1050
1106
|
if (sub === 'bootstrap') return cmdBootstrap(opts);
|
package/lib/prune.cjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
// `dieu-phoi prune --after-retro <file> [--yes]` (core 0.5.2): post-retro cleanup of closed run dirs,
|
|
3
|
-
// files and events.jsonl (moved to a gz archive, never deleted). Dry-run unless --yes.
|
|
2
|
+
// `dieu-phoi prune --after-retro <file> [--yes] [--delta-days N]` (core 0.5.2): post-retro cleanup of closed run dirs,
|
|
3
|
+
// bad plan files and events.jsonl (moved to a gz archive, never deleted). Dry-run unless --yes. 0.7.4 (s6): with
|
|
4
|
+
// --delta-days N (default 30) rejected unmerged deltas of runs idle > N days are deleted; accepted ones never.
|
|
5
|
+
// s9: `--delta-days N` alone (no --after-retro) runs only that delta step (cmdPruneDeltas).
|
|
4
6
|
// Only touches: <runs dir>/<id>/, <data>/plans, <data>/events.jsonl, <data>/archive, <data>/sync-offset.json.
|
|
5
7
|
|
|
6
8
|
const fs = require('fs');
|
|
@@ -58,6 +60,8 @@ function human(n) {
|
|
|
58
60
|
// contract/steps/plan/handoff files of a run dir: the record of what was planned and handed over (never pruned)
|
|
59
61
|
const KEEP_RE = /^(?:contract-r.*\.md|steps-r.*\.json|plan.*\.md|handoff-.*\.md)$/;
|
|
60
62
|
const DELTA_RE = /^knowledge-delta.*\.md$/i;
|
|
63
|
+
const DAY_MS = 86400000;
|
|
64
|
+
const DELTA_DAYS_DEFAULT = 30;
|
|
61
65
|
|
|
62
66
|
/**
|
|
63
67
|
* Files to keep in a run dir: retro-*.md + per task/role the highest-try result AND the newest-by-mtime result
|
|
@@ -123,6 +127,7 @@ function taskIdsOfDir(id, names, plansDir) {
|
|
|
123
127
|
function planRuns(ctx, events, cutoffMs, protect, popts) {
|
|
124
128
|
const po = popts || {};
|
|
125
129
|
const nowMs = typeof po.now === 'number' ? po.now : Date.now();
|
|
130
|
+
const deltaDays = typeof po.deltaDays === 'number' ? po.deltaDays : 0;
|
|
126
131
|
const cls = core.deltaClassifier(events, ctx.paths.plans);
|
|
127
132
|
const runs = core.runsDir();
|
|
128
133
|
const byTask = new Map();
|
|
@@ -149,6 +154,19 @@ function planRuns(ctx, events, cutoffMs, protect, popts) {
|
|
|
149
154
|
if (!inside(runs, dir)) continue;
|
|
150
155
|
const names = fs.readdirSync(dir);
|
|
151
156
|
const tasks = Array.from(taskIdsOfDir(id, names, ctx.paths.plans));
|
|
157
|
+
if (po.deltaOnly) { // s9 `prune --delta-days N` alone: only the rejected unmerged deltas of an idle run, nothing else
|
|
158
|
+
let runMs = 0;
|
|
159
|
+
for (const n of names) { try { runMs = Math.max(runMs, fs.lstatSync(path.join(dir, n)).mtimeMs); } catch (_) { /* gone */ } }
|
|
160
|
+
if (nowMs - runMs <= deltaDays * DAY_MS) continue;
|
|
161
|
+
const drops = names.filter((n) => {
|
|
162
|
+
if (!DELTA_RE.test(n) || cls.deltaState(id, core.deltaTagOf(n)) !== 'reject') return false;
|
|
163
|
+
let text = null;
|
|
164
|
+
try { text = fs.readFileSync(path.join(dir, n), 'utf8'); } catch (_) { return false; }
|
|
165
|
+
return !appliedSet().has(core.deltaHash(text));
|
|
166
|
+
});
|
|
167
|
+
if (drops.length) out.prune.push({ id, dir, tasks, keep: [], keep_deltas: 0, archive: [], drop_deltas: drops, del: [], delete_bytes: 0, keep_bytes: 0 });
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
152
170
|
let anyVerdict = false; let reason = null; let latest = 0;
|
|
153
171
|
for (const t of tasks) {
|
|
154
172
|
const evs = byTask.get(t) || [];
|
|
@@ -189,6 +207,21 @@ function planRuns(ctx, events, cutoffMs, protect, popts) {
|
|
|
189
207
|
if (nowMs - m > core.DELTA_WINDOW_MS && cls.deltaState(id, core.deltaTagOf(n)) === null) archive.push(n);
|
|
190
208
|
}
|
|
191
209
|
for (const n of archive) keep.delete(n);
|
|
210
|
+
// s6 --delta-days N: a delta the user REJECTED, unmerged, in a run idle for more than N days, is no work any more:
|
|
211
|
+
// deleted with --yes. Accepted-but-unmerged (K7) and no-verdict deltas (K1: archived above, never deleted) stay.
|
|
212
|
+
const dropDeltas = [];
|
|
213
|
+
if (deltaDays > 0) {
|
|
214
|
+
let runMs = 0;
|
|
215
|
+
for (const n of names) { try { runMs = Math.max(runMs, fs.lstatSync(path.join(dir, n)).mtimeMs); } catch (_) { /* gone */ } }
|
|
216
|
+
if (nowMs - runMs > deltaDays * DAY_MS) {
|
|
217
|
+
for (const n of names) {
|
|
218
|
+
if (!DELTA_RE.test(n) || !keep.has(n) || archive.includes(n)) continue;
|
|
219
|
+
if (cls.deltaState(id, core.deltaTagOf(n)) !== 'reject' || !unmerged(n)) continue;
|
|
220
|
+
keep.delete(n);
|
|
221
|
+
dropDeltas.push(n);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
192
225
|
const keepDeltas = names.filter((n) => DELTA_RE.test(n) && keep.has(n)).length;
|
|
193
226
|
const del = [];
|
|
194
227
|
let delBytes = 0; let keepBytes = 0;
|
|
@@ -200,7 +233,7 @@ function planRuns(ctx, events, cutoffMs, protect, popts) {
|
|
|
200
233
|
del.push({ name: n, bytes: b });
|
|
201
234
|
delBytes += b;
|
|
202
235
|
}
|
|
203
|
-
out.prune.push({ id, dir, tasks, keep: Array.from(keep).sort(), keep_deltas: keepDeltas, archive, del, delete_bytes: delBytes, keep_bytes: keepBytes });
|
|
236
|
+
out.prune.push({ id, dir, tasks, keep: Array.from(keep).sort(), keep_deltas: keepDeltas, archive, drop_deltas: dropDeltas, del, delete_bytes: delBytes, keep_bytes: keepBytes });
|
|
204
237
|
}
|
|
205
238
|
return out;
|
|
206
239
|
}
|
|
@@ -275,10 +308,46 @@ function otherSessionActive(ctx, opts, events) {
|
|
|
275
308
|
return count ? { count, sessions: Array.from(sessions), last: new Date(last).toISOString(), ownKnown: !!own } : null;
|
|
276
309
|
}
|
|
277
310
|
|
|
311
|
+
/**
|
|
312
|
+
* s9 `prune --delta-days N` without --after-retro: deletes (with --yes) only the rejected, unmerged deltas of runs idle
|
|
313
|
+
* for more than N days. No run dir is archived, no event moves, no bad plan goes. Dry-run otherwise.
|
|
314
|
+
*/
|
|
315
|
+
function cmdPruneDeltas(opts, ctx) {
|
|
316
|
+
const dd = opts['delta-days'];
|
|
317
|
+
const deltaDays = dd === true ? DELTA_DAYS_DEFAULT : Number(dd);
|
|
318
|
+
if (!Number.isFinite(deltaDays) || deltaDays < 0) return { code: 2, out: `prune: --delta-days phải là số ngày ≥ 0 (nhận: ${dd})` };
|
|
319
|
+
const runs = core.runsDir();
|
|
320
|
+
if (!path.isAbsolute(runs) || runs === path.parse(runs).root) return { code: 2, out: `prune: từ chối đường dẫn runs không an toàn: ${runs}` };
|
|
321
|
+
const yes = opts.yes === true || opts.yes === 'y' || opts.yes === 'yes';
|
|
322
|
+
const rp = planRuns(ctx, core.readEvents(ctx), 0, null, { now: typeof opts.now === 'number' ? opts.now : undefined, deltaDays, deltaOnly: true });
|
|
323
|
+
let removed = 0;
|
|
324
|
+
if (yes) {
|
|
325
|
+
for (const r of rp.prune) {
|
|
326
|
+
for (const n of r.drop_deltas) {
|
|
327
|
+
const p = path.join(r.dir, n);
|
|
328
|
+
if (!inside(r.dir, p) || !inside(runs, p)) continue;
|
|
329
|
+
fs.rmSync(p, { force: true });
|
|
330
|
+
removed++;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const total = rp.prune.reduce((n, r) => n + r.drop_deltas.length, 0);
|
|
335
|
+
if (opts.json) {
|
|
336
|
+
return { code: 0, out: JSON.stringify({ mode: yes ? 'apply' : 'dry-run', delta_only: true, delta_days: deltaDays,
|
|
337
|
+
runs: rp.prune.map((r) => ({ id: r.id, drop_deltas: r.drop_deltas })), totals: { drop_deltas: total, removed } }, null, 2) };
|
|
338
|
+
}
|
|
339
|
+
const L = [];
|
|
340
|
+
L.push(`prune --delta-days ${deltaDays} ${yes ? '(ĐÃ THỰC HIỆN)' : '(dry-run — thêm --yes để thực hiện)'}: chỉ delta đã từ chối, chưa gộp, của run rảnh > ${deltaDays} ngày`);
|
|
341
|
+
for (const r of rp.prune) L.push(` ${r.id}: ${yes ? 'xoá' : 'sẽ xoá'} ${r.drop_deltas.length} delta (${r.drop_deltas.join(', ')})`);
|
|
342
|
+
L.push(`Tổng: ${total} delta ${yes ? `đã xoá ${removed}` : 'sẽ xoá'}. Không đụng run dir khác, events.jsonl, plan hay archive.`);
|
|
343
|
+
return { code: 0, out: L.join('\n') };
|
|
344
|
+
}
|
|
345
|
+
|
|
278
346
|
function cmdPrune(opts) {
|
|
279
347
|
const ctx = ctxOf(opts);
|
|
280
348
|
const retro = typeof opts['after-retro'] === 'string' ? path.resolve(opts['after-retro']) : null;
|
|
281
|
-
if (!retro
|
|
349
|
+
if (!retro && opts['delta-days'] !== undefined) return cmdPruneDeltas(opts, ctx);
|
|
350
|
+
if (!retro) return { code: 2, out: 'prune: thiếu --after-retro <file retro> (hoặc --delta-days N để chỉ dọn delta đã từ chối)' };
|
|
282
351
|
let st; try { st = fs.statSync(retro); } catch (_) { st = null; }
|
|
283
352
|
if (!st || !st.isFile()) return { code: 2, out: `prune: không đọc được file retro: ${retro}` };
|
|
284
353
|
const runs = core.runsDir();
|
|
@@ -287,10 +356,14 @@ function cmdPrune(opts) {
|
|
|
287
356
|
if (!path.isAbsolute(p) || p === path.parse(p).root) return { code: 2, out: `prune: từ chối đường dẫn ${name} không an toàn: ${p}` };
|
|
288
357
|
}
|
|
289
358
|
const yes = opts.yes === true || opts.yes === 'y' || opts.yes === 'yes';
|
|
359
|
+
// --delta-days N (default 30; 0 = off): rejected unmerged deltas of runs idle > N days are deleted with --yes
|
|
360
|
+
const dd = opts['delta-days'];
|
|
361
|
+
const deltaDays = dd === undefined ? DELTA_DAYS_DEFAULT : (dd === true ? DELTA_DAYS_DEFAULT : Number(dd));
|
|
362
|
+
if (!Number.isFinite(deltaDays) || deltaDays < 0) return { code: 2, out: `prune: --delta-days phải là số ngày ≥ 0 (nhận: ${dd})` };
|
|
290
363
|
const cut = cutoffOf(retro);
|
|
291
364
|
const { lines } = fs.existsSync(ctx.paths.events) ? eventLines(ctx) : { lines: [] };
|
|
292
365
|
const allEvents = core.readEvents(ctx);
|
|
293
|
-
const rp = planRuns(ctx, allEvents, cut.ms, retro, { now: typeof opts.now === 'number' ? opts.now : undefined });
|
|
366
|
+
const rp = planRuns(ctx, allEvents, cut.ms, retro, { now: typeof opts.now === 'number' ? opts.now : undefined, deltaDays });
|
|
294
367
|
const prunedTasks = new Set();
|
|
295
368
|
for (const r of rp.prune) for (const t of r.tasks) prunedTasks.add(t);
|
|
296
369
|
const plans = badPlans(ctx);
|
|
@@ -308,7 +381,7 @@ function cmdPrune(opts) {
|
|
|
308
381
|
|
|
309
382
|
const report = {
|
|
310
383
|
mode: yes ? 'apply' : 'dry-run', retro, cutoff: new Date(cut.ms).toISOString(), cutoff_source: cut.source,
|
|
311
|
-
runs: rp.prune.map((r) => ({ id: r.id, delete_files: r.del.length, delete_bytes: r.delete_bytes, keep: r.keep, keep_deltas: r.keep_deltas, archive_deltas: r.archive.length, keep_bytes: r.keep_bytes })),
|
|
384
|
+
runs: rp.prune.map((r) => ({ id: r.id, delete_files: r.del.length, delete_bytes: r.delete_bytes, keep: r.keep, keep_deltas: r.keep_deltas, archive_deltas: r.archive.length, drop_deltas: r.drop_deltas.length, keep_bytes: r.keep_bytes })),
|
|
312
385
|
runs_skipped: rp.skipped.length,
|
|
313
386
|
skipped_reasons: rp.skipped.reduce((a, s) => { const k = s.reason.replace(/task \S+/, 'task *').replace(/\(task \S+\)/, ''); a[k] = (a[k] || 0) + 1; return a; }, {}),
|
|
314
387
|
bad_plans: plans.map((p) => ({ name: p.name, bytes: p.bytes })),
|
|
@@ -317,6 +390,7 @@ function cmdPrune(opts) {
|
|
|
317
390
|
report.totals = {
|
|
318
391
|
run_dirs: rp.prune.length,
|
|
319
392
|
archive_deltas: rp.prune.reduce((n, r) => n + r.archive.length, 0),
|
|
393
|
+
drop_deltas: rp.prune.reduce((n, r) => n + r.drop_deltas.length, 0),
|
|
320
394
|
files: rp.prune.reduce((n, r) => n + r.del.length, 0),
|
|
321
395
|
bytes: rp.prune.reduce((n, r) => n + r.delete_bytes, 0) + plans.reduce((n, p) => n + p.bytes, 0),
|
|
322
396
|
};
|
|
@@ -402,7 +476,7 @@ function cmdPrune(opts) {
|
|
|
402
476
|
const L = [];
|
|
403
477
|
L.push(`prune ${yes ? '(ĐÃ THỰC HIỆN)' : '(dry-run — thêm --yes để thực hiện)'}: mốc cắt ${report.cutoff} (${cut.source}) từ ${retro}`);
|
|
404
478
|
L.push(`Run dir: ${rp.prune.length} sẽ dọn, ${rp.skipped.length} giữ nguyên (còn mở / sau mốc)`);
|
|
405
|
-
for (const r of rp.prune) L.push(` ${r.id}: xoá ${r.del.length} mục (${human(r.delete_bytes)}), giữ ${r.keep.length} (${human(r.keep_bytes)}), trong đó delta chưa gộp ${r.keep_deltas}${r.archive.length ? `; chuyển ${r.archive.length} delta >30 ngày chưa có verdict sang _archive/deltas/` : ''}`);
|
|
479
|
+
for (const r of rp.prune) L.push(` ${r.id}: xoá ${r.del.length} mục (${human(r.delete_bytes)}), giữ ${r.keep.length} (${human(r.keep_bytes)}), trong đó delta chưa gộp ${r.keep_deltas}${r.archive.length ? `; chuyển ${r.archive.length} delta >30 ngày chưa có verdict sang _archive/deltas/` : ''}${r.drop_deltas.length ? `; xoá ${r.drop_deltas.length} delta đã từ chối (run nằm yên >${deltaDays} ngày)` : ''}`);
|
|
406
480
|
L.push(`Plan hỏng: ${plans.length} (${human(plans.reduce((n, p) => n + p.bytes, 0))})${plans.length ? ` — ${plans.map((p) => p.name).join(', ')}` : ''}`);
|
|
407
481
|
const evTxt = yes
|
|
408
482
|
? `đã chuyển ${removedLines}/${lines.length} dòng vào archive và xoá khỏi events.jsonl (${freshCount} dòng ghi mới${freshCount ? ` vào archive/events-${archiveToday()}.jsonl.gz` : ''}; ${human(evBytes)})`
|
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>",
|
package/profile/lessons.md
CHANGED
|
@@ -20,7 +20,6 @@ Nhiều máy: file trong skill đã publish là bản đã duyệt (một nguồ
|
|
|
20
20
|
|
|
21
21
|
Câu hỏi bắt buộc hỏi ở cổng 1 cho từng loại task (retro thêm khi tỉ lệ steer cao). Định dạng: `- <task_type>: <câu hỏi>`
|
|
22
22
|
|
|
23
|
-
- *: (chỉ khi kế hoạch có ≥ 4 bước s≤6) Chọn mode? fast (bước s≤6 không verifier riêng, test chọn lọc, một verifier gộp chạy cả suite trước khi trình, tự nghiệm thu rồi bạn duyệt cuối) | normal (mặc định) | strict (luôn cả suite + kiểm ngẫu nhiên 1/3). Kế hoạch nhỏ hơn: dùng normal, không hỏi (retro 11-05: 46/46 lần chọn normal, fast 0 lần qua 2 retro). Sàn an toàn giữ ở mọi mode; đổi được giữa chừng.
|
|
24
23
|
- extract: Cần đúng những trường nào, đầu ra định dạng gì (bảng/JSON/cột Sheet), và khi các nguồn lệch nhau thì nguồn nào là chuẩn? (retro 11-05: steer_rate 0,3, n10)
|
|
25
24
|
|
|
26
25
|
## Bài học
|