@kienntpixon/dieu-phoi 0.7.4 → 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 +21 -0
- package/bin/dieu-phoi.cjs +14 -1
- package/hook.cjs +218 -9
- package/lib/args.cjs +1 -1
- package/lib/commands.cjs +10 -5
- package/lib/knowledge.cjs +19 -6
- package/lib/repos.cjs +387 -0
- package/lib/setup.cjs +22 -0
- package/lib/worktree.cjs +672 -38
- package/lib/wtfast.cjs +156 -0
- package/lib/wtgc.cjs +756 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
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
|
+
|
|
14
|
+
## 0.7.5 (2026-11-06)
|
|
15
|
+
|
|
16
|
+
Đi cùng skill 1.6.5. `CORE_VERSION` 0.7.5. Không đổi sink (`sheet/Code.gs`, `fixtures/` giữ nguyên).
|
|
17
|
+
**Sau khi cài:** `npm i -g @kienntpixon/dieu-phoi` + `dieu-phoi setup` + `dieu-phoi doctor`.
|
|
18
|
+
|
|
19
|
+
- **`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).
|
|
20
|
+
- **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`).
|
|
21
|
+
- **`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.
|
|
22
|
+
- **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.
|
|
23
|
+
|
|
3
24
|
## 0.7.4 (2026-11-06)
|
|
4
25
|
|
|
5
26
|
Đ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,8 +49,20 @@ 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)
|
|
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
|
|
53
66
|
steer <agent_id> --kind minor|plan|redirect [--reason R]
|
|
54
67
|
steer --task <task_id> --kind minor|plan|redirect [--reason R]
|
|
55
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']);
|
|
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
|
@@ -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.gcAfterAccept(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.gcAfterAccept(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`);
|
|
@@ -810,7 +813,7 @@ function cmdDiscard(opts) {
|
|
|
810
813
|
core.appendEvent(ctx, ev);
|
|
811
814
|
const n = events.filter((e) => e.task_id === taskId).length;
|
|
812
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.
|
|
813
|
-
const wtLines = wt.cleanupTask(ctx, events, taskId, opts.repo);
|
|
816
|
+
const wtLines = wt.cleanupTask(ctx, events, taskId, opts.repo, { discard: true });
|
|
814
817
|
if (opts.json) {
|
|
815
818
|
if (wtLines.length) process.stderr.write(`${wtLines.join('\n')}\n`);
|
|
816
819
|
return { code: 0, out: JSON.stringify(ev) };
|
|
@@ -1011,11 +1014,13 @@ function cmdState(opts) {
|
|
|
1011
1014
|
const deltas = core.scanUnmergedDeltasFor(ctx);
|
|
1012
1015
|
const deltaLines = core.unmergedDeltaLines(deltas);
|
|
1013
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));
|
|
1014
1019
|
if (!sid) {
|
|
1015
1020
|
if (opts.json) {
|
|
1016
|
-
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 } : {})) };
|
|
1017
1022
|
}
|
|
1018
|
-
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') };
|
|
1019
1024
|
}
|
|
1020
1025
|
const state = core.buildSessionState(core.readEvents(ctx), sid, { plansDir: ctx.paths.plans });
|
|
1021
1026
|
const orphans = orphanCount(ctx); // s16: null (unknown) leaves the line out
|
|
@@ -1027,13 +1032,13 @@ function cmdState(opts) {
|
|
|
1027
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,
|
|
1028
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 })),
|
|
1029
1034
|
};
|
|
1030
|
-
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) };
|
|
1031
1036
|
}
|
|
1032
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`);
|
|
1033
1038
|
const stuckLines = stuck.length
|
|
1034
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\``]
|
|
1035
1040
|
: [];
|
|
1036
|
-
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') };
|
|
1037
1042
|
}
|
|
1038
1043
|
|
|
1039
1044
|
// ------------------------------------------------------------------ reap (core 0.7.3 s6)
|
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) {
|