@kienntpixon/dieu-phoi 0.7.5 → 0.7.6
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 +11 -0
- package/bin/dieu-phoi.cjs +9 -0
- package/hook.cjs +218 -9
- package/lib/args.cjs +1 -1
- package/lib/commands.cjs +9 -7
- package/lib/repos.cjs +387 -0
- package/lib/setup.cjs +22 -0
- package/lib/worktree.cjs +518 -43
- package/lib/wtfast.cjs +156 -0
- package/lib/wtgc.cjs +756 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog — @kienntpixon/dieu-phoi
|
|
2
2
|
|
|
3
|
+
## 0.7.6 (2026-11-07)
|
|
4
|
+
|
|
5
|
+
Đi cùng skill 1.6.6. `CORE_VERSION` 0.7.6. 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` (bắt buộc: hook cài nay đi kèm `bin/lib/`)** + `dieu-phoi doctor`.
|
|
7
|
+
|
|
8
|
+
- **`wt gc [--repo R | --all-repos] [--yes] [--json] [--no-measure]`:** quét MỌI worktree đăng ký trong git (có dấu hay không) và xếp lớp `safe` / `keep-slot` / `abandoned` / `keep` / `prunable` / `shell` / `temp-branch`, kèm dung lượng từng mục và tổng thu hồi (ước lượng, trần thời gian). Dry-run mặc định; `--yes` chỉ gỡ `safe`, vỏ mồ côi, mục prunable và nhánh tạm đã merge. Nhàn rỗi tính theo mốc mới nhất của commit cuối, file mới nhất trong worktree và agent sống; không biết thì không bao giờ safe. Slot tên tự đặt (`keep-slot`) KHÔNG BAO GIỜ tự gỡ. Lớp `abandoned` (chưa merge, nhàn rỗi ≥ `wt_abandon_days` = 14) chỉ liệt kê.
|
|
9
|
+
- **Sổ repo + `wt repos [--seed <dir>…] [--depth N] [--json]`:** `data/repos.json` ghi repo mà hook (cwd, repo đích của brief), agent sống, `wt add` và sự kiện worktree chạm tới; `--seed` quét thư mục. `wt gc --all-repos` đọc sổ này.
|
|
10
|
+
- **Dọn tự động, chỉ bốn lớp an toàn — safe, vỏ, prunable, nhánh tạm `-d`; không bao giờ bỏ dở/keep/slot (`wt_auto_gc` = `safe`|`off`, mặc định `safe`):** (a) `verdict … accept` của người dùng gc ở repo vừa nghiệm thu (`--clean-merged` còn là bí danh); (b) hook `UserPromptSubmit` spawn nền `hook.cjs --gc-auto` mỗi `wt_gc_interval_h` (6) giờ, có khoá và nhật ký `data/gc-log.jsonl`, không chặn prompt; (c) ổ chứa repo còn dưới `wt_disk_low_gb` (15, 0 = tắt) GB: một dòng cảnh báo kèm số GB thu hồi được ở lớp cần duyệt, và gc safe chạy ngay. Đường đĩa spawn tối đa mỗi 30 phút. MỖI lần gỡ ghi một sự kiện `worktree` riêng (`remove`, `remove-shell`, `prune`, `branch-delete`, kèm `trigger`) + `wt_gc_auto` có `paths` và `branches`; `state` hiện cả hai. Lỗi lặp do thiếu `bin/lib` chỉ ghi `hook-errors.log` một lần mỗi 24 giờ. `wt_keep` thêm mẫu slot giữ lại.
|
|
11
|
+
- **Sửa: hook cài bằng `setup` thiếu `lib/`.** Từ 0.7.6 s2 hook nạp `./lib/repos.cjs` / `./lib/wtgc.cjs`, nhưng `setup` chỉ chép một file, nên hook cài sẽ lỗi nạp (lỗi này có từ commit nền b8c5011). `setup` nay chép toàn bộ `lib/*.cjs` vào `bin/lib/` (`uninstall --purge` xoá). Tính năng phụ (sổ repo, cảnh báo ổ, gc nền) bọc try/catch: thiếu `bin/lib` chỉ ghi `errors.log`, prompt không bao giờ hỏng. Test chạy bản đã cài từ thư mục setup tạm.
|
|
12
|
+
- **Hoãn sang 0.7.7 (s4):** tag `dp-archive/<branch>-<yyyymmdd>` và `wt gc --abandoned --yes` cho lớp bỏ dở. 0.7.6 chưa gắn tag, chưa gỡ lớp này.
|
|
13
|
+
|
|
3
14
|
## 0.7.5 (2026-11-06)
|
|
4
15
|
|
|
5
16
|
Đi cùng skill 1.6.5. `CORE_VERSION` 0.7.5. Không đổi sink (`sheet/Code.gs`, `fixtures/` giữ nguyên).
|
package/bin/dieu-phoi.cjs
CHANGED
|
@@ -54,6 +54,15 @@ Cách dùng: dieu-phoi <lệnh> [tuỳ chọn]
|
|
|
54
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
|
|
55
55
|
wt list [--repo R] [--all] [--json]
|
|
56
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)
|
|
57
|
+
wt repos [--seed <dir>…] [--depth N] [--json]
|
|
58
|
+
sổ repo đã chạm (data/repos.json): --seed quét thư mục tới độ sâu N (mặc định 3), sự kiện worktree và git worktree list của repo đã biết
|
|
59
|
+
wt gc [--repo R | --all-repos] [--yes] [--json] [--no-measure]
|
|
60
|
+
quét MỌI worktree đăng ký trong git (có dấu hay không; --all-repos = mọi repo trong sổ repo) và xếp lớp:
|
|
61
|
+
safe (nhánh đang checkout đã vào một nhánh bền bất kỳ, sạch, không agent sống, không khoá thật; có dấu: tuổi ≥ 1 giờ
|
|
62
|
+
+ verdict pass/accept; không dấu hoặc thiếu verdict: nhàn rỗi > 24 giờ), keep-slot (tên tự đặt, Library/, lane/*,
|
|
63
|
+
.dieu-phoi-keep, wt_keep: không bao giờ tự gỡ), abandoned (chưa merge, nhàn rỗi ≥ wt_abandon_days 14: chỉ liệt kê),
|
|
64
|
+
keep (kèm lý do), prunable, vỏ mồ côi, nhánh tạm đã merge. Báo dung lượng từng mục + tổng thu hồi (ước lượng, trần thời gian).
|
|
65
|
+
Dry-run mặc định; --yes chỉ gỡ safe + vỏ + prune + git branch -d nhánh tạm
|
|
57
66
|
steer <agent_id> --kind minor|plan|redirect [--reason R]
|
|
58
67
|
steer --task <task_id> --kind minor|plan|redirect [--reason R]
|
|
59
68
|
ghi một lần người dùng lái giữa chừng (--task: chỉ các agent đang chạy của task)
|
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.6';
|
|
32
32
|
const SCHEMA = 1;
|
|
33
33
|
const TASK_TYPES = [
|
|
34
34
|
'search', 'extract', 'mechanical-edit', 'feature-impl', 'debug', 'refactor',
|
|
@@ -205,6 +205,27 @@ function logError(home, err, where) {
|
|
|
205
205
|
} catch (_) { /* nothing left to do */ }
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Lỗi lặp lại theo prompt (thiếu bin/lib ở bản cài lẻ): chỉ ghi MỘT lần mỗi LOG_ONCE_MS cho mỗi (nơi, mã lỗi).
|
|
210
|
+
* Mốc ở <home>/hook-errors-once.json; không đọc/ghi được mốc thì cứ ghi (thà ồn còn hơn câm).
|
|
211
|
+
*/
|
|
212
|
+
const LOG_ONCE_MS = 24 * 3600000;
|
|
213
|
+
function logErrorOnce(home, err, where, now) {
|
|
214
|
+
try {
|
|
215
|
+
const p = pathsFor(home || resolveHome());
|
|
216
|
+
const f = path.join(p.home, 'hook-errors-once.json');
|
|
217
|
+
const t = typeof now === 'number' ? now : Date.now();
|
|
218
|
+
const key = where + ':' + String((err && (err.code || err.message)) || err).slice(0, 120);
|
|
219
|
+
let m = {};
|
|
220
|
+
try { m = JSON.parse(fs.readFileSync(f, 'utf8')) || {}; } catch (_) { m = {}; }
|
|
221
|
+
if (t - (Number(m[key]) || 0) < LOG_ONCE_MS) return;
|
|
222
|
+
m[key] = t;
|
|
223
|
+
fs.mkdirSync(p.home, { recursive: true });
|
|
224
|
+
fs.writeFileSync(f, JSON.stringify(m) + '\n');
|
|
225
|
+
} catch (_) { /* không ghi được mốc: vẫn ghi lỗi */ }
|
|
226
|
+
logError(home, err, where);
|
|
227
|
+
}
|
|
228
|
+
|
|
208
229
|
// ------------------------------------------------------------------ identity
|
|
209
230
|
|
|
210
231
|
function computeUserHash() {
|
|
@@ -946,6 +967,10 @@ const EVENTS_LOCK_WAIT_MS = 1500;
|
|
|
946
967
|
|
|
947
968
|
function appendEvent(ctx, ev) {
|
|
948
969
|
fs.mkdirSync(ctx.paths.data, { recursive: true });
|
|
970
|
+
// core 0.7.6 (s2): sự kiện worktree (add/remove/gc) có repo → sổ repo đã chạm (best effort)
|
|
971
|
+
try {
|
|
972
|
+
if (ev && ev.kind === 'worktree' && typeof ev.repo === 'string' && ev.repo) require('./lib/repos.cjs').recordRepo(ctx.paths.data, ev.repo, 'worktree');
|
|
973
|
+
} catch (_) { /* sổ repo không được làm hỏng sự kiện */ }
|
|
949
974
|
// Core 0.5.4: appends take events.lock so `prune --yes` (which rewrites the file under the same lock) can never
|
|
950
975
|
// lose a line that lands between its read and its rename. Lock wait is capped well under the shortest hook timeout
|
|
951
976
|
// (UserPromptSubmit 5 s) so a stuck lock cannot get the hook killed mid-write; on timeout append anyway (never drop telemetry).
|
|
@@ -1708,21 +1733,174 @@ function formatStartBlock(info) {
|
|
|
1708
1733
|
].filter((l) => l !== '').join('\n');
|
|
1709
1734
|
}
|
|
1710
1735
|
|
|
1736
|
+
// ------------------------------------------------------------------ r1 s5 (core 0.7.6): gc nền tự động
|
|
1737
|
+
// Hook chỉ làm việc RẺ: đọc data/gc-auto.json (+ sổ repo, statfs mỗi ổ) rồi spawn tách rời `hook.cjs --gc-auto`; không require wtgc.
|
|
1738
|
+
// Chỉ lớp safe (gc quyết định bằng kiểm tra tươi, xem wtgc.cjs); lớp bỏ dở / slot / keep KHÔNG BAO GIỜ tự gỡ.
|
|
1739
|
+
const GC_AUTO_FILE = 'gc-auto.json'; // { t: lần spawn theo giờ, disk_t: lần spawn vì thiếu đĩa, warn_t: lần cảnh báo }
|
|
1740
|
+
const GC_TOTALS_FILE = 'gc-totals.json'; // do tiến trình gc nền ghi: { ts, abandoned_bytes, keep_slot_bytes, reclaimed_bytes }
|
|
1741
|
+
const GC_LOCK_FILE = 'gc-auto.lock';
|
|
1742
|
+
const GC_LOCK_STALE_MS = 30 * 60 * 1000;
|
|
1743
|
+
const GC_DISK_GAP_MS = 30 * 60 * 1000; // spawn vì thiếu đĩa: cách nhau tối thiểu
|
|
1744
|
+
const GC_WARN_GAP_MS = 30 * 60 * 1000; // cảnh báo thiếu đĩa: cách nhau tối thiểu (mỗi prompt một dòng là ồn)
|
|
1745
|
+
const GC_DEFAULT_INTERVAL_H = 6;
|
|
1746
|
+
const GC_DEFAULT_DISK_LOW_GB = 15;
|
|
1747
|
+
const GC_GB = 1073741824;
|
|
1748
|
+
|
|
1749
|
+
/** wt_auto_gc: env DIEU_PHOI_WT_AUTO_GC -> config.json -> 'safe'. off/false/0/no -> 'off', còn lại 'safe'. */
|
|
1750
|
+
function autoGcModeOf(ctx) {
|
|
1751
|
+
const cfg = ctx && ctx.config ? ctx.config : {};
|
|
1752
|
+
const v = [process.env.DIEU_PHOI_WT_AUTO_GC, cfg.wt_auto_gc].find((x) => x !== undefined && x !== null && x !== '');
|
|
1753
|
+
const s = String(v === undefined ? 'safe' : v).trim().toLowerCase();
|
|
1754
|
+
return ['off', 'false', '0', 'no'].includes(s) ? 'off' : 'safe';
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/** Số >= 0 (có thể thập phân): env DIEU_PHOI_<KEY> -> config.json -> mặc định; nguồn hỏng rơi xuống nguồn kế. */
|
|
1758
|
+
function gcNumberOf(ctx, key, def) {
|
|
1759
|
+
const cfg = ctx && ctx.config ? ctx.config : {};
|
|
1760
|
+
for (const v of [process.env['DIEU_PHOI_' + key.toUpperCase()], cfg[key]]) {
|
|
1761
|
+
if (v === undefined || v === null || v === '') continue;
|
|
1762
|
+
const n = Number(v);
|
|
1763
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
1764
|
+
}
|
|
1765
|
+
return def;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
/** Ổ chứa repo đã đăng ký có dưới lowGb GB trống không -> [{ drive, free_bytes }] (rẻ: một statfs mỗi ổ). */
|
|
1769
|
+
function lowDisksOf(ctx, lowGb, statfs) {
|
|
1770
|
+
let reg;
|
|
1771
|
+
try { reg = require('./lib/repos.cjs').readRegistry(ctx.paths.data); } catch (e) {
|
|
1772
|
+
// thiếu/hỏng lib (cài lẻ): không có sổ repo thì không cảnh báo ổ; nhưng gc theo giờ vẫn chạy. Ghi lỗi để doctor thấy.
|
|
1773
|
+
try { logErrorOnce(ctx.home, e, 'disk-registry'); } catch (_) { /* hết cách */ }
|
|
1774
|
+
return [];
|
|
1775
|
+
}
|
|
1776
|
+
const sf = statfs || ((p) => fs.statfsSync(p));
|
|
1777
|
+
const seen = new Set();
|
|
1778
|
+
const low = [];
|
|
1779
|
+
for (const repo of Object.keys(reg)) {
|
|
1780
|
+
let key;
|
|
1781
|
+
let probe = repo;
|
|
1782
|
+
if (process.platform === 'win32') { probe = path.parse(repo).root; key = probe.toLowerCase(); } else {
|
|
1783
|
+
try { key = String(fs.statSync(repo).dev); } catch (_) { continue; }
|
|
1784
|
+
}
|
|
1785
|
+
if (!key || seen.has(key)) continue;
|
|
1786
|
+
seen.add(key);
|
|
1787
|
+
try {
|
|
1788
|
+
const s = sf(probe);
|
|
1789
|
+
const free = Number(s.bavail) * Number(s.bsize);
|
|
1790
|
+
if (Number.isFinite(free) && free < lowGb * GC_GB) low.push({ drive: probe, free_bytes: free });
|
|
1791
|
+
} catch (_) { /* ổ không đọc được: bỏ qua */ }
|
|
1792
|
+
}
|
|
1793
|
+
return low;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
/** Tạo khoá (wx); khoá cũ hơn GC_LOCK_STALE_MS bị chiếm lại. true = đã giữ khoá. */
|
|
1797
|
+
function takeGcLock(file, now) {
|
|
1798
|
+
for (let i = 0; i < 2; i++) {
|
|
1799
|
+
try {
|
|
1800
|
+
const fd = fs.openSync(file, 'wx');
|
|
1801
|
+
fs.writeSync(fd, JSON.stringify({ t: now, pid: process.pid }) + '\n');
|
|
1802
|
+
fs.closeSync(fd);
|
|
1803
|
+
return true;
|
|
1804
|
+
} catch (e) {
|
|
1805
|
+
if (e.code !== 'EEXIST') return false;
|
|
1806
|
+
try { if (Date.now() - fs.statSync(file).mtimeMs <= GC_LOCK_STALE_MS) return false; fs.unlinkSync(file); } catch (_) { return false; }
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
return false;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
/**
|
|
1813
|
+
* Mỗi prompt (rẻ, không chặn): quá wt_gc_interval_h (6) hoặc ổ chứa repo dưới wt_disk_low_gb (15) -> spawn tách rời gc lớp safe.
|
|
1814
|
+
* Mốc ghi TRƯỚC khi spawn (hai prompt liền nhau chỉ spawn một lần); khoá data/gc-auto.lock chặn chạy chồng.
|
|
1815
|
+
* deps (test): now, statfs, spawn(exe, args, env), cmd:[exe, args]. Trả { spawned, reason, message }.
|
|
1816
|
+
*/
|
|
1817
|
+
function autoGcTick(ctx, deps) {
|
|
1818
|
+
const d = deps || {};
|
|
1819
|
+
const res = { spawned: false, reason: null, message: null };
|
|
1820
|
+
try {
|
|
1821
|
+
if (autoGcModeOf(ctx) === 'off') { res.reason = 'off'; return res; }
|
|
1822
|
+
const injected = !!(d.spawn || d.cmd);
|
|
1823
|
+
if (!injected && (process.env.DIEU_PHOI_NO_SPAWN === '1' || process.env.NODE_TEST_CONTEXT)) { res.reason = 'no-spawn'; return res; }
|
|
1824
|
+
const now = typeof d.now === 'number' ? d.now : Date.now();
|
|
1825
|
+
const file = path.join(ctx.paths.data, GC_AUTO_FILE);
|
|
1826
|
+
const st = readJsonFile(file, {}) || {};
|
|
1827
|
+
const intervalMs = gcNumberOf(ctx, 'wt_gc_interval_h', GC_DEFAULT_INTERVAL_H) * 3600000;
|
|
1828
|
+
const lowGb = gcNumberOf(ctx, 'wt_disk_low_gb', GC_DEFAULT_DISK_LOW_GB);
|
|
1829
|
+
const low = lowGb > 0 ? lowDisksOf(ctx, lowGb, d.statfs) : [];
|
|
1830
|
+
const timed = intervalMs > 0 && now - (Number(st.t) || 0) >= intervalMs;
|
|
1831
|
+
const disk = low.length > 0 && now - (Number(st.disk_t) || 0) >= GC_DISK_GAP_MS;
|
|
1832
|
+
let running = false;
|
|
1833
|
+
if (timed || disk) {
|
|
1834
|
+
const lock = path.join(ctx.paths.data, GC_LOCK_FILE);
|
|
1835
|
+
fs.mkdirSync(ctx.paths.data, { recursive: true });
|
|
1836
|
+
if (!takeGcLock(lock, now)) { running = true; res.reason = 'locked'; } else {
|
|
1837
|
+
const next = Object.assign({}, st, { t: now });
|
|
1838
|
+
if (disk) next.disk_t = now;
|
|
1839
|
+
writeFileAtomic(file, JSON.stringify(next) + '\n'); // mốc trước khi spawn
|
|
1840
|
+
Object.assign(st, next);
|
|
1841
|
+
try {
|
|
1842
|
+
const cmd = d.cmd || [process.execPath, [__filename, '--gc-auto']];
|
|
1843
|
+
const env = Object.assign({}, process.env, { DIEU_PHOI_HOME: ctx.home, DIEU_PHOI_GC_TRIGGER: disk && !timed ? 'disk' : 'hook' });
|
|
1844
|
+
if (d.spawn) d.spawn(cmd[0], cmd[1], env); else {
|
|
1845
|
+
const child = require('child_process').spawn(cmd[0], cmd[1], { detached: true, stdio: 'ignore', windowsHide: true, env });
|
|
1846
|
+
child.on('error', (e) => { logError(ctx.home, e, 'spawn-gc'); try { fs.unlinkSync(lock); } catch (_) { /* đã mất */ } });
|
|
1847
|
+
child.unref();
|
|
1848
|
+
}
|
|
1849
|
+
res.spawned = true;
|
|
1850
|
+
res.reason = disk && !timed ? 'disk' : 'interval';
|
|
1851
|
+
} catch (e) {
|
|
1852
|
+
logError(ctx.home, e, 'spawn-gc');
|
|
1853
|
+
try { fs.unlinkSync(lock); } catch (_) { /* đã mất */ }
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
if (low.length && now - (Number(st.warn_t) || 0) >= GC_WARN_GAP_MS) {
|
|
1858
|
+
const tot = readJsonFile(path.join(ctx.paths.data, GC_TOTALS_FILE), null);
|
|
1859
|
+
const need = tot ? (Number(tot.abandoned_bytes) || 0) + (Number(tot.keep_slot_bytes) || 0) : null;
|
|
1860
|
+
const gb = (n) => (n / GC_GB).toFixed(1);
|
|
1861
|
+
const drives = low.map((x) => x.drive.replace(/[\\/]+$/, '') + ' còn ' + gb(x.free_bytes) + ' GB').join(', ');
|
|
1862
|
+
const figure = need === null ? 'chưa đo lớp cần duyệt' : 'thu hồi được ≥ ' + gb(need) + ' GB ở lớp bỏ dở/slot (cần duyệt)';
|
|
1863
|
+
const doing = res.spawned || running ? 'đang dọn nền lớp safe' : 'lớp safe vừa dọn gần đây';
|
|
1864
|
+
res.message = 'dieu-phoi: ổ ' + drives + ' (< ' + lowGb + ' GB); ' + doing + '; ' + figure + '. Xem lớp bỏ dở/giữ (không tự gỡ): dieu-phoi wt gc --all-repos --measure';
|
|
1865
|
+
writeFileAtomic(file, JSON.stringify(Object.assign({}, st, { warn_t: now })) + '\n');
|
|
1866
|
+
}
|
|
1867
|
+
} catch (e) {
|
|
1868
|
+
try { logErrorOnce(ctx.home, e, 'gc-tick'); } catch (_) { /* hết cách */ }
|
|
1869
|
+
}
|
|
1870
|
+
return res;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
/** Tiến trình nền (hook.cjs --gc-auto): gc lớp safe mọi repo, ghi log + sự kiện; luôn nhả khoá. */
|
|
1874
|
+
function runGcChild() {
|
|
1875
|
+
const ctx = makeCtx();
|
|
1876
|
+
try {
|
|
1877
|
+
require('./lib/wtgc.cjs').autoGc(ctx, { allRepos: true, trigger: process.env.DIEU_PHOI_GC_TRIGGER || 'hook' });
|
|
1878
|
+
} catch (e) {
|
|
1879
|
+
logError(ctx.home, e, 'gc-auto');
|
|
1880
|
+
} finally {
|
|
1881
|
+
try { fs.unlinkSync(path.join(ctx.paths.data, GC_LOCK_FILE)); } catch (_) { /* đã mất */ }
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1711
1885
|
/** UserPromptSubmit: on a dieu-phoi prompt, pre-create the run and inject id/run dir/health/brief. */
|
|
1712
|
-
function handleUserPromptSubmit(p, ctx) {
|
|
1886
|
+
function handleUserPromptSubmit(p, ctx, gcDeps) {
|
|
1713
1887
|
// s6: stuck/orphaned agents of this session → TaskStop + reap reminder, on any prompt (not only dieu-phoi ones)
|
|
1714
1888
|
const stuck = typeof p.session_id === 'string' && p.session_id && !watchAgentId(p) ? stuckReminder(ctx, { sessionId: p.session_id }) : null;
|
|
1889
|
+
// r1 s5: gc nền lớp safe, không chặn prompt. r1 s6: tính năng phụ — mọi lỗi (thiếu lib, statfs, spawn) chỉ ghi log, prompt vẫn đi tiếp.
|
|
1890
|
+
let gcTick = { spawned: false, reason: 'error', message: null };
|
|
1891
|
+
try { gcTick = autoGcTick(ctx, gcDeps || null); } catch (e) { try { logError(ctx.home, e, 'gc-tick'); } catch (_) { /* hết cách */ } }
|
|
1892
|
+
const sys = gcTick.message ? { systemMessage: gcTick.message } : {};
|
|
1715
1893
|
if (!isDieuPhoiPrompt(p.prompt)) {
|
|
1716
|
-
if (!stuck) return null;
|
|
1717
|
-
return { stdout: JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: stuck } }), stuck: true };
|
|
1894
|
+
if (!stuck) return gcTick.message ? { stdout: JSON.stringify(sys), gc: gcTick } : null;
|
|
1895
|
+
return { stdout: JSON.stringify(Object.assign({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: stuck } }, sys)), stuck: true };
|
|
1718
1896
|
}
|
|
1719
1897
|
const info = startInfo(ctx, typeof p.cwd === 'string' ? p.cwd : null);
|
|
1720
1898
|
const text = stuck ? `${formatStartBlock(info)}
|
|
1721
1899
|
${stuck}` : formatStartBlock(info);
|
|
1722
1900
|
return {
|
|
1723
|
-
stdout: JSON.stringify({
|
|
1901
|
+
stdout: JSON.stringify(Object.assign({
|
|
1724
1902
|
hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: text },
|
|
1725
|
-
}),
|
|
1903
|
+
}, sys)),
|
|
1726
1904
|
info,
|
|
1727
1905
|
};
|
|
1728
1906
|
}
|
|
@@ -1875,7 +2053,7 @@ const DEFAULT_MODE = 'normal';
|
|
|
1875
2053
|
/** Removed in 0.7.4: old events/plans/configs may still carry it; every reader treats it as normal. */
|
|
1876
2054
|
const LEGACY_MODE = 'strict';
|
|
1877
2055
|
/** Event kinds that stay on this machine: runSync does not send them (an older sink would answer bad_kind). */
|
|
1878
|
-
const LOCAL_ONLY_KINDS = new Set(['mode', 'knowledge_merge', 'leftover', 'worktree', 'reap', 'reap_orphans']);
|
|
2056
|
+
const LOCAL_ONLY_KINDS = new Set(['mode', 'knowledge_merge', 'leftover', 'worktree', 'reap', 'reap_orphans', 'wt_gc_auto']);
|
|
1879
2057
|
/** A mode the user may choose now (fast | normal). */
|
|
1880
2058
|
function isMode(m) {
|
|
1881
2059
|
return typeof m === 'string' && MODES.includes(m);
|
|
@@ -2897,6 +3075,31 @@ function projectFields(p) {
|
|
|
2897
3075
|
return {};
|
|
2898
3076
|
}
|
|
2899
3077
|
|
|
3078
|
+
/**
|
|
3079
|
+
* core 0.7.6 (s2): ghi repo của cwd payload và repo đích của brief vào data/repos.json (best effort, không spawn git).
|
|
3080
|
+
* Module lười nạp trong try: lỗi nạp không được làm hỏng hook.
|
|
3081
|
+
*/
|
|
3082
|
+
function recordReposFromPayload(p, ctx) {
|
|
3083
|
+
if (!p || typeof p !== 'object' || !ctx || !ctx.paths || !ctx.paths.data) return;
|
|
3084
|
+
const R = require('./lib/repos.cjs');
|
|
3085
|
+
const dataDir = ctx.paths.data;
|
|
3086
|
+
const name = typeof p.hook_event_name === 'string' ? p.hook_event_name : '';
|
|
3087
|
+
const src = /^Subagent/.test(name) ? 'live' : 'hook';
|
|
3088
|
+
const ti = p.tool_input && typeof p.tool_input === 'object' ? p.tool_input : {};
|
|
3089
|
+
const prompt = typeof ti.prompt === 'string' ? ti.prompt : '';
|
|
3090
|
+
const cwd = typeof p.cwd === 'string' && p.cwd ? p.cwd : null;
|
|
3091
|
+
// (a) `git [-C repo] worktree add` trong Bash (Pre/Post): repo resolve bằng git rev-parse --git-common-dir
|
|
3092
|
+
if (p.tool_name === 'Bash' && typeof ti.command === 'string') R.recordWorktreeAddCommand(dataDir, ti.command, cwd);
|
|
3093
|
+
// (b) SubagentStart/Stop: cwd payload và worktree: của brief (Claude Code isolation) → checkout chính của nó
|
|
3094
|
+
const cands = [cwd, prompt ? repoFromBrief(prompt) : null, prompt ? briefWorktree(prompt) : null];
|
|
3095
|
+
const seen = new Set();
|
|
3096
|
+
for (const d of cands) {
|
|
3097
|
+
if (!d || seen.has(d)) continue;
|
|
3098
|
+
seen.add(d);
|
|
3099
|
+
R.recordRepo(dataDir, d, src);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
|
|
2900
3103
|
/** Run dir id the brief points at (retro 11-05 K1: accept finds the delta there whatever the plan/root says), else null. */
|
|
2901
3104
|
function dispatchRun(prompt) {
|
|
2902
3105
|
try {
|
|
@@ -4879,6 +5082,7 @@ function runHook(opts) {
|
|
|
4879
5082
|
if (!raw || !raw.trim()) return null;
|
|
4880
5083
|
const p = JSON.parse(raw.replace(/^/, ''));
|
|
4881
5084
|
const ctx = makeCtx(home);
|
|
5085
|
+
try { recordReposFromPayload(p, ctx); } catch (_) { /* sổ repo (core 0.7.6): best effort, không chặn hook */ }
|
|
4882
5086
|
let ev = null;
|
|
4883
5087
|
const name = p.hook_event_name;
|
|
4884
5088
|
if (o.mode === 'watch') return handleWatch(p, ctx, o); // { stdout } or null
|
|
@@ -4893,6 +5097,8 @@ function runHook(opts) {
|
|
|
4893
5097
|
// s7: keep the live record (worktree, start) for the guarded leftover kill before the file goes
|
|
4894
5098
|
let live = null;
|
|
4895
5099
|
try { live = typeof p.agent_id === 'string' && p.agent_id ? readJsonFile(liveFile(ctx, p.agent_id), null) : null; } catch (_) { live = null; }
|
|
5100
|
+
// (b) core 0.7.6: worktree của agent (live.cwd) → checkout chính vào sổ repo, trước khi live bị xoá
|
|
5101
|
+
try { if (live && typeof live.cwd === 'string' && live.cwd) require('./lib/repos.cjs').recordRepo(ctx.paths.data, live.cwd, 'live'); } catch (_) { /* best effort */ }
|
|
4896
5102
|
try { removeLive(ctx, p.agent_id); } catch (e) { logError(home, e, 'live-remove'); }
|
|
4897
5103
|
const r = handleSubagentStop(p, ctx, { live });
|
|
4898
5104
|
if (r && typeof r.stdout === 'string') return r; // s15: block decision, printed as the hook's JSON answer
|
|
@@ -5047,7 +5253,7 @@ async function runSync(ctx, opts) {
|
|
|
5047
5253
|
// ------------------------------------------------------------------ exports / main
|
|
5048
5254
|
|
|
5049
5255
|
module.exports = {
|
|
5050
|
-
runsDir, loadPlans, isDieuPhoiPrompt, foldPrompt, sinkStatusCached, sinkUrlKey, SINK_CACHE_TTL_MS, scoutBriefPath, quickHealth, startRun, startInfo, startKnowledgeOf, knowledgeLines, orchLessons, orchLessonLines, abortReasonOf, completionStatus, logGate, QUOTA_PAUSE_MARKER, stepIds, START_STEP_IDS, healthLine, formatStartBlock, handleUserPromptSubmit, START_PREFIX, buildSessionState, findOrphanAgents, orphanLines, ORPHAN_MIN_MS, ORPHAN_NO_COMPLETE_MS, formatSessionState, stateCounts, compactSystemMessage, handleSessionStart, dispatchInSession, STATE_TEXT_CAP, COMPACT_PREFIX,
|
|
5256
|
+
runsDir, loadPlans, isDieuPhoiPrompt, foldPrompt, sinkStatusCached, sinkUrlKey, SINK_CACHE_TTL_MS, scoutBriefPath, quickHealth, startRun, startInfo, startKnowledgeOf, knowledgeLines, orchLessons, orchLessonLines, abortReasonOf, completionStatus, logGate, QUOTA_PAUSE_MARKER, stepIds, START_STEP_IDS, healthLine, formatStartBlock, handleUserPromptSubmit, autoGcTick, autoGcModeOf, gcNumberOf, lowDisksOf, runGcChild, START_PREFIX, buildSessionState, findOrphanAgents, orphanLines, ORPHAN_MIN_MS, ORPHAN_NO_COMPLETE_MS, formatSessionState, stateCounts, compactSystemMessage, handleSessionStart, dispatchInSession, STATE_TEXT_CAP, COMPACT_PREFIX,
|
|
5051
5257
|
modeLine, MODES, DEFAULT_MODE, LOCAL_ONLY_KINDS, isMode, normMode, LEGACY_MODE, rootModeFromEvents, rootModeFromText, dispatchModeFromEvents, taskModeInfo, planOfTask, modeFields,
|
|
5052
5258
|
rootTaskIds,
|
|
5053
5259
|
testCommandInfo, testTimeOf, testSFromText,
|
|
@@ -5070,7 +5276,10 @@ module.exports = {
|
|
|
5070
5276
|
if (require.main === module) {
|
|
5071
5277
|
process.on('uncaughtException', (e) => { logError(null, e, 'uncaught'); process.exit(0); });
|
|
5072
5278
|
process.on('unhandledRejection', (e) => { logError(null, e, 'unhandled'); process.exit(0); });
|
|
5073
|
-
if (process.argv.includes('--
|
|
5279
|
+
if (process.argv.includes('--gc-auto')) {
|
|
5280
|
+
runGcChild();
|
|
5281
|
+
process.exit(0);
|
|
5282
|
+
} else if (process.argv.includes('--sync')) {
|
|
5074
5283
|
const ctx = makeCtx();
|
|
5075
5284
|
runSync(ctx)
|
|
5076
5285
|
.then((r) => { if (!r.ok) logError(ctx.home, new Error(r.error), 'sync'); })
|
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', 'merged', 'clean-merged']);
|
|
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', 'all-repos', 'measure', 'no-measure']);
|
|
8
8
|
|
|
9
9
|
/** Minimal argv parser: positionals, --k v, --k=v, boolean flags. */
|
|
10
10
|
function parseArgs(argv) {
|
package/lib/commands.cjs
CHANGED
|
@@ -755,7 +755,7 @@ function cmdVerdict(opts) {
|
|
|
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
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.
|
|
758
|
+
wtLines.push(...wt.gcAfterAccept(ctx, list.map((x) => x.ev.task_id), opts.repo, opts['clean-merged'] === true));
|
|
759
759
|
if (!list.length) {
|
|
760
760
|
// Every step was already judged (e.g. accepted one by one with --task): still merge the root + untagged deltas.
|
|
761
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.
|
|
@@ -777,7 +777,7 @@ function cmdVerdict(opts) {
|
|
|
777
777
|
const kn = ev.by === 'user' && ev.outcome === 'accept' ? knowledgeOnAccept(opts, ctx, rootOfTask(ctx, ev.task_id), [ev.task_id], false, events) : null;
|
|
778
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.
|
|
779
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.
|
|
780
|
+
if (ev.by === 'user' && ev.outcome === 'accept') wtLines.push(...wt.gcAfterAccept(ctx, [ev.task_id], opts.repo, opts['clean-merged'] === true));
|
|
781
781
|
if (opts.json) {
|
|
782
782
|
if (kn && kn.out) process.stderr.write(`${kn.out}\n`);
|
|
783
783
|
if (wtLines.length) process.stderr.write(`${wtLines.join('\n')}\n`);
|
|
@@ -813,7 +813,7 @@ function cmdDiscard(opts) {
|
|
|
813
813
|
core.appendEvent(ctx, ev);
|
|
814
814
|
const n = events.filter((e) => e.task_id === taskId).length;
|
|
815
815
|
// s17: task bị loại → gỡ worktree có dấu của task (sạch); còn thay đổi thì giữ và báo.
|
|
816
|
-
const wtLines = wt.cleanupTask(ctx, events, taskId, opts.repo);
|
|
816
|
+
const wtLines = wt.cleanupTask(ctx, events, taskId, opts.repo, { discard: true });
|
|
817
817
|
if (opts.json) {
|
|
818
818
|
if (wtLines.length) process.stderr.write(`${wtLines.join('\n')}\n`);
|
|
819
819
|
return { code: 0, out: JSON.stringify(ev) };
|
|
@@ -1014,11 +1014,13 @@ function cmdState(opts) {
|
|
|
1014
1014
|
const deltas = core.scanUnmergedDeltasFor(ctx);
|
|
1015
1015
|
const deltaLines = core.unmergedDeltaLines(deltas);
|
|
1016
1016
|
const hasDeltas = core.hasUnmergedDeltas(deltas);
|
|
1017
|
+
const gcState = require('./wtgc.cjs').autoGcState(core.readEvents(ctx)); // r1 s5 (d): gc tự động đã gỡ gì
|
|
1018
|
+
const gcLines = require('./wtgc.cjs').autoGcLines(core.readEvents(ctx));
|
|
1017
1019
|
if (!sid) {
|
|
1018
1020
|
if (opts.json) {
|
|
1019
|
-
return { code: 0, out: JSON.stringify(Object.assign({ session_id: null, generated_at: core.nowIso(), runs_dir: null, plans: [], tasks: [] }, hasDeltas ? { unmerged_deltas: deltas } : {})) };
|
|
1021
|
+
return { code: 0, out: JSON.stringify(Object.assign({ session_id: null, generated_at: core.nowIso(), runs_dir: null, plans: [], tasks: [] }, hasDeltas ? { unmerged_deltas: deltas } : {}, gcState ? { wt_gc_auto: gcState } : {})) };
|
|
1020
1022
|
}
|
|
1021
|
-
return { code: 0, out: ['Không có session (thiếu CLAUDE_CODE_SESSION_ID, dùng --session <id>).'].concat(deltaLines).join('\n') };
|
|
1023
|
+
return { code: 0, out: ['Không có session (thiếu CLAUDE_CODE_SESSION_ID, dùng --session <id>).'].concat(deltaLines, gcLines).join('\n') };
|
|
1022
1024
|
}
|
|
1023
1025
|
const state = core.buildSessionState(core.readEvents(ctx), sid, { plansDir: ctx.paths.plans });
|
|
1024
1026
|
const orphans = orphanCount(ctx); // s16: null (unknown) leaves the line out
|
|
@@ -1030,13 +1032,13 @@ function cmdState(opts) {
|
|
|
1030
1032
|
session: live.session, machine: live.machine, limits: live.limits, free_session: freeSlots(live.limits.session, live.session), free_machine: freeSlots(live.limits.machine, live.machine), agents: live.agents,
|
|
1031
1033
|
stuck: stuck.map((a) => ({ agent_id: a.agent_id, task_id: a.task_id, role: a.role, gen: a.gen, state: a.state, reasons: a.reasons, idle_min: a.idle_min, age_min: a.age_min })),
|
|
1032
1034
|
};
|
|
1033
|
-
return { code: 0, out: JSON.stringify(Object.assign({}, state, hasDeltas ? { unmerged_deltas: deltas } : {}, orphans === null ? {} : { orphan_procs: orphans }, { live: liveJ }, batchPending.length ? { batch_pending: batchPending } : {}), null, 2) };
|
|
1035
|
+
return { code: 0, out: JSON.stringify(Object.assign({}, state, hasDeltas ? { unmerged_deltas: deltas } : {}, gcState ? { wt_gc_auto: gcState } : {}, orphans === null ? {} : { orphan_procs: orphans }, { live: liveJ }, batchPending.length ? { batch_pending: batchPending } : {}), null, 2) };
|
|
1034
1036
|
}
|
|
1035
1037
|
const batchLines = batchPending.map((p) => `CẢNH BÁO: ${p.root_task_id}: ${p.steps.length} bước self+batch (${p.steps.join(', ')}) chưa có verdict verifier gộp (batch-verify) — giao verifier gộp (brief H) trước cổng 3`);
|
|
1036
1038
|
const stuckLines = stuck.length
|
|
1037
1039
|
? [`stuck: ${stuck.slice(0, 6).map((a) => `${a.task_id || a.agent_id}:${a.role} ${a.state === 'orphan' ? 'mồ côi' : 'kẹt'} ${a.idle_min}'`).join(', ')} → TaskStop rồi \`dieu-phoi reap --yes\``]
|
|
1038
1040
|
: [];
|
|
1039
|
-
return { code: 0, out: [core.formatSessionState(state, core.STATE_TEXT_CAP)].concat(deltaLines, orphans === null ? [] : [`orphan_procs: ${orphans}`], [liveLine(live)], stuckLines, batchLines).join('\n') };
|
|
1041
|
+
return { code: 0, out: [core.formatSessionState(state, core.STATE_TEXT_CAP)].concat(deltaLines, orphans === null ? [] : [`orphan_procs: ${orphans}`], [liveLine(live)], stuckLines, batchLines, gcLines).join('\n') };
|
|
1040
1042
|
}
|
|
1041
1043
|
|
|
1042
1044
|
// ------------------------------------------------------------------ reap (core 0.7.3 s6)
|