@kienntpixon/dieu-phoi 0.7.1 → 0.7.2
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/README.md +3 -0
- package/agents/dp-exec.md +2 -0
- package/agents/dp-helper.md +2 -0
- package/agents/dp-scout.md +2 -0
- package/agents/dp-verify.md +2 -0
- package/bin/dieu-phoi.cjs +14 -2
- package/hook.cjs +576 -44
- package/lib/aggregate.cjs +5 -4
- package/lib/args.cjs +1 -1
- package/lib/commands.cjs +168 -35
- package/lib/knowledge.cjs +3 -4
- package/lib/procs.cjs +223 -0
- package/lib/profile-scan.cjs +85 -0
- package/lib/profile-seed.cjs +147 -0
- package/lib/profile.cjs +141 -1
- package/lib/prune.cjs +56 -8
- package/lib/setup.cjs +31 -7
- package/lib/worktree.cjs +186 -0
- package/package.json +3 -1
- package/profile/lessons.md +83 -0
- package/profile/unity-lanes.md +316 -0
package/lib/aggregate.cjs
CHANGED
|
@@ -125,6 +125,7 @@ function buildAgents(events) {
|
|
|
125
125
|
const userVerdicts = [];
|
|
126
126
|
const autoVerdicts = [];
|
|
127
127
|
const overrides = [];
|
|
128
|
+
const implicitOverrides = []; // core 0.7.2 (K4): deviations the hook logged; not the user's overrides
|
|
128
129
|
const steers = [];
|
|
129
130
|
const get = (id) => {
|
|
130
131
|
if (!map.has(id)) map.set(id, { agent_id: id, dispatch: null, completes: [], verdicts: [], steers: [] });
|
|
@@ -135,7 +136,7 @@ function buildAgents(events) {
|
|
|
135
136
|
if (!ev || typeof ev !== 'object') return;
|
|
136
137
|
if (isInternalComplete(ev, dispatched)) return;
|
|
137
138
|
const e = Object.assign({ _idx: idx }, ev);
|
|
138
|
-
if (ev.kind === 'override') { overrides.push(e); return; }
|
|
139
|
+
if (ev.kind === 'override') { (ev.source === 'implicit' ? implicitOverrides : overrides).push(e); return; }
|
|
139
140
|
if (ev.kind === 'verdict') {
|
|
140
141
|
const v = Object.assign(e, normalizeVerdict(ev));
|
|
141
142
|
if (v.by === 'user') { userVerdicts.push(v); return; }
|
|
@@ -202,7 +203,7 @@ function buildAgents(events) {
|
|
|
202
203
|
last_complete_ts: a.completes.length ? Math.max(...a.completes.map(tsOf)) : 0,
|
|
203
204
|
});
|
|
204
205
|
}
|
|
205
|
-
return { agents, userVerdicts, autoVerdicts, overrides, steers };
|
|
206
|
+
return { agents, userVerdicts, autoVerdicts, overrides, implicitOverrides, steers };
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
/** Group agents into tasks (sample unit = task_id; agents without task_id are their own task). */
|
|
@@ -563,11 +564,11 @@ function orchestrationSummary(events) {
|
|
|
563
564
|
}
|
|
564
565
|
|
|
565
566
|
function analyze(events, opts) {
|
|
566
|
-
const { agents, userVerdicts, autoVerdicts, overrides, steers } = buildAgents(withoutDiscarded(events));
|
|
567
|
+
const { agents, userVerdicts, autoVerdicts, overrides, implicitOverrides, steers } = buildAgents(withoutDiscarded(events));
|
|
567
568
|
const tasks = buildTasks(agents, userVerdicts, autoVerdicts);
|
|
568
569
|
const cells = buildCells(tasks, opts);
|
|
569
570
|
return {
|
|
570
|
-
agents, tasks, cells, overrides, userVerdicts, autoVerdicts, steerEvents: steers, steers: buildSteers(tasks, steers, agents),
|
|
571
|
+
agents, tasks, cells, overrides, implicitOverrides, userVerdicts, autoVerdicts, steerEvents: steers, steers: buildSteers(tasks, steers, agents),
|
|
571
572
|
orchestration: orchestrationSummary(events),
|
|
572
573
|
batch_verify: batchVerifyCells(agents),
|
|
573
574
|
};
|
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']);
|
|
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']);
|
|
8
8
|
|
|
9
9
|
/** Minimal argv parser: positionals, --k v, --k=v, boolean flags. */
|
|
10
10
|
function parseArgs(argv) {
|
package/lib/commands.cjs
CHANGED
|
@@ -9,6 +9,8 @@ const agg = require('./aggregate.cjs');
|
|
|
9
9
|
const chooser = require('./choose.cjs');
|
|
10
10
|
const setupLib = require('./setup.cjs');
|
|
11
11
|
const cbm = require('./cbm.cjs');
|
|
12
|
+
const procs = require('./procs.cjs');
|
|
13
|
+
const wt = require('./worktree.cjs');
|
|
12
14
|
const { claudeDirOf, ctxOf, yesNo, repoOf } = require('./args.cjs');
|
|
13
15
|
|
|
14
16
|
const AGENT_OUTCOMES = ['pass', 'fail', 'timeout', 'aborted'];
|
|
@@ -49,14 +51,24 @@ function table(rows) {
|
|
|
49
51
|
// ------------------------------------------------------------------ setup / uninstall
|
|
50
52
|
|
|
51
53
|
function cmdSetup(opts) {
|
|
52
|
-
if (opts['with-codebase-memory']) return cmdSetupWithCbm(opts);
|
|
54
|
+
if (opts['with-codebase-memory'] && !opts['dry-run']) return cmdSetupWithCbm(opts);
|
|
53
55
|
const res = setupLib.setup({
|
|
54
56
|
claudeDir: claudeDirOf(opts),
|
|
55
57
|
dept: typeof opts.dept === 'string' ? opts.dept : undefined,
|
|
56
58
|
sinkUrl: typeof opts['sink-url'] === 'string' ? opts['sink-url'] : undefined,
|
|
57
59
|
sinkToken: typeof opts['sink-token'] === 'string' ? opts['sink-token'] : undefined,
|
|
58
60
|
skillDir: typeof opts['skill-dir'] === 'string' ? opts['skill-dir'] : undefined,
|
|
61
|
+
dryRun: opts['dry-run'] === true,
|
|
59
62
|
});
|
|
63
|
+
const seed = require('./profile-seed.cjs').seedSummary(res.profile);
|
|
64
|
+
if (res.dry_run) {
|
|
65
|
+
if (opts.json) return { code: 0, out: JSON.stringify(res, null, 2) };
|
|
66
|
+
return { code: 0, out: [
|
|
67
|
+
'setup --dry-run: không ghi gì (hook, settings.json, agent dp-*, config đều giữ nguyên)',
|
|
68
|
+
seed,
|
|
69
|
+
`Skill: ${res.skill_dir || '(không thấy; dùng --skill-dir)'}`,
|
|
70
|
+
].join('\n') };
|
|
71
|
+
}
|
|
60
72
|
if (opts.json) {
|
|
61
73
|
const safe = Object.assign({}, res, { config: Object.assign({}, res.config) });
|
|
62
74
|
if (safe.config.sink_token) safe.config.sink_token = '***';
|
|
@@ -72,6 +84,7 @@ function cmdSetup(opts) {
|
|
|
72
84
|
`Phòng ban: ${res.config.dept || '(chưa đặt, dùng --dept)'}`,
|
|
73
85
|
`Sink: ${res.config.sink_url ? res.config.sink_url : '(chưa có — log chỉ lưu cục bộ)'}`,
|
|
74
86
|
`Skill: ${res.config.skill_dir || '(không thấy; dùng --skill-dir)'}${res.config.skill_version ? ` v${res.config.skill_version}` : ''}`,
|
|
87
|
+
seed,
|
|
75
88
|
'Khởi động lại Claude Code (hoặc mở /hooks) để hook có hiệu lực.',
|
|
76
89
|
];
|
|
77
90
|
return { code: 0, out: lines.join('\n') };
|
|
@@ -201,6 +214,7 @@ async function cmdDoctor(opts) {
|
|
|
201
214
|
if (orch.ctx_tokens !== null) {
|
|
202
215
|
add(!orch.over, 'orch-ctx', `session ${orch.session_id}: ${orch.ctx_tokens} token, tăng ${orch.ctx_growth}/${orch.soft_cap}, ≈ ${fmtUsd(orch.cum_usd)} — ${orch.advice}`, 'CẢNH BÁO');
|
|
203
216
|
}
|
|
217
|
+
add(...orphanCheck(ctx));
|
|
204
218
|
if (ctx.config.sink_url && ctx.config.sink_token) {
|
|
205
219
|
const cached = opts.net ? null : core.sinkStatusCached(ctx);
|
|
206
220
|
if (cached && (cached.state === 'ok' || cached.state === 'fail')) {
|
|
@@ -222,6 +236,48 @@ async function cmdDoctor(opts) {
|
|
|
222
236
|
return { code: failed ? 1 : 0, out };
|
|
223
237
|
}
|
|
224
238
|
|
|
239
|
+
// s16 (core 0.7.2): Claude Code / code-graph processes whose parent is gone and that are older than this
|
|
240
|
+
const ORPHAN_MIN_AGE_MS = 6 * 3600 * 1000;
|
|
241
|
+
const ORPHAN_CACHE_MS = 10 * 60 * 1000;
|
|
242
|
+
|
|
243
|
+
/** Orphaned claude.exe / codebase-memory-mcp older than 6 h from a process list: [{pid,name,age_h,ws_mb}], or null when the list is unreadable. */
|
|
244
|
+
function orphanScan(list, now) {
|
|
245
|
+
return Array.isArray(list) ? procs.staleOrphans(list, { minAgeMs: ORPHAN_MIN_AGE_MS, now: now === undefined ? Date.now() : now }) : null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** doctor item `orphan-processes`: warns with pid + age + working set and a Stop-Process hint; never kills. Returns the args of add(). */
|
|
249
|
+
function orphanCheck(ctx, deps) {
|
|
250
|
+
const d = deps || {};
|
|
251
|
+
const now = d.now === undefined ? Date.now() : d.now;
|
|
252
|
+
const found = orphanScan((d.listProcesses || procs.listProcesses)({ timeoutMs: 4000 }), now);
|
|
253
|
+
if (found === null) return [true, 'orphan-processes', 'không đọc được danh sách process (bỏ qua)'];
|
|
254
|
+
try {
|
|
255
|
+
core.writeFileAtomic(path.join(ctx.paths.data, 'orphan-procs.json'), `${JSON.stringify({ ts: new Date(now).toISOString(), n: found.length })}\n`);
|
|
256
|
+
} catch (_) { /* the cache is optional */ }
|
|
257
|
+
if (!found.length) return [true, 'orphan-processes', 'không có claude.exe / codebase-memory-mcp mồ côi quá 6 giờ'];
|
|
258
|
+
const mb = found.reduce((n, p) => n + (p.ws_mb || 0), 0);
|
|
259
|
+
const shown = found.slice(0, 8);
|
|
260
|
+
const items = shown.map((p) => `${p.name} pid ${p.pid} (${p.age_h} giờ${p.ws_mb ? `, ${p.ws_mb} MB` : ''})`).join('; ');
|
|
261
|
+
return [false, 'orphan-processes',
|
|
262
|
+
`${found.length} tiến trình mồ côi (cha đã thoát) quá 6 giờ, ${mb} MB: ${items}${found.length > shown.length ? '; …' : ''}`
|
|
263
|
+
+ ` — kiểm tra rồi dọn bằng \`Stop-Process -Id ${shown.map((p) => p.pid).join(',')}\` (doctor không tự kill)`, 'CẢNH BÁO'];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** `state` line: orphan count from a 10-minute cache (data/orphan-procs.json), else one live scan. null = unknown (skip the line). */
|
|
267
|
+
function orphanCount(ctx, deps) {
|
|
268
|
+
const d = deps || {};
|
|
269
|
+
const now = d.now === undefined ? Date.now() : d.now;
|
|
270
|
+
const file = path.join(ctx.paths.data, 'orphan-procs.json');
|
|
271
|
+
const c = core.readJsonFile(file, null);
|
|
272
|
+
const at = c && typeof c.ts === 'string' ? Date.parse(c.ts) : NaN;
|
|
273
|
+
if (c && Number.isFinite(at) && now - at >= 0 && now - at < ORPHAN_CACHE_MS) return typeof c.n === 'number' ? c.n : null;
|
|
274
|
+
const found = orphanScan((d.listProcesses || procs.listProcesses)({ timeoutMs: 4000 }), now);
|
|
275
|
+
try {
|
|
276
|
+
core.writeFileAtomic(file, `${JSON.stringify({ ts: new Date(now).toISOString(), n: found ? found.length : null })}\n`);
|
|
277
|
+
} catch (_) { /* the cache is optional */ }
|
|
278
|
+
return found ? found.length : null;
|
|
279
|
+
}
|
|
280
|
+
|
|
225
281
|
const TMP_RE = /\.tmp-\d+-[0-9a-f]+$/;
|
|
226
282
|
const TMP_STALE_MS = 3600 * 1000;
|
|
227
283
|
|
|
@@ -582,28 +638,79 @@ function rootOfTask(ctx, taskId) {
|
|
|
582
638
|
}
|
|
583
639
|
|
|
584
640
|
/**
|
|
585
|
-
*
|
|
586
|
-
* (
|
|
587
|
-
*
|
|
641
|
+
* Run dirs that hold knowledge deltas of `tasks` (retro 11-05 K1). The delta of a task lives where its agent was told to
|
|
642
|
+
* write (the dispatch `run`, 0.7.2), which is not always runs/<root>: a loose task or one outside the approved plan has
|
|
643
|
+
* root = itself yet its brief points at the session's run. Order: runs/<root> (also takes untagged deltas when
|
|
644
|
+
* `untagged`), the dispatch `run` of each task, then any other run dir with a file tagged with one of the tasks.
|
|
645
|
+
* [{ id, dir, files, untagged }]; `scanned` = how many run dirs the fallback looked at; `rootDirExists`.
|
|
588
646
|
*/
|
|
589
|
-
function
|
|
590
|
-
if (opts['no-knowledge']) return null;
|
|
647
|
+
function deltaRunDirs(root, tasks, events, untagged) {
|
|
591
648
|
const kn = require('./knowledge.cjs');
|
|
592
|
-
const
|
|
593
|
-
const
|
|
649
|
+
const runsRoot = core.runsDir();
|
|
650
|
+
const want = new Set(tasks);
|
|
651
|
+
const seen = new Set();
|
|
652
|
+
const found = [];
|
|
653
|
+
let scanned = 0;
|
|
654
|
+
const consider = (id, withUntagged) => {
|
|
655
|
+
if (typeof id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(id) || seen.has(id)) return false;
|
|
656
|
+
seen.add(id);
|
|
657
|
+
const dir = path.join(runsRoot, id);
|
|
658
|
+
try {
|
|
659
|
+
if (!fs.statSync(dir).isDirectory()) return false;
|
|
660
|
+
const files = kn.deltaFiles(dir, { tasks: want, untagged: withUntagged });
|
|
661
|
+
if (files.length) found.push({ id, dir, files, untagged: withUntagged });
|
|
662
|
+
return true;
|
|
663
|
+
} catch (_) { return false; }
|
|
664
|
+
};
|
|
665
|
+
const rootDirExists = consider(root, untagged);
|
|
666
|
+
for (const e of events || []) if (e && e.kind === 'dispatch' && want.has(e.task_id)) consider(e.run, false);
|
|
594
667
|
try {
|
|
595
|
-
|
|
596
|
-
} catch (_) {
|
|
668
|
+
for (const id of fs.readdirSync(runsRoot)) { scanned++; consider(id, false); }
|
|
669
|
+
} catch (_) { /* no runs dir */ }
|
|
670
|
+
return { found, scanned, rootDirExists };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Gate 3 (core 0.6.0): a user accept merges the knowledge deltas of the accepted tasks (knowledge-delta-<task_id>
|
|
675
|
+
* [-r<n>|-h<n>].md; on --root also the root's own and untagged ones) wherever they are (see deltaRunDirs). Never fails
|
|
676
|
+
* the verdict: conflicts/rejections are reported with the command to retry. --no-knowledge skips it.
|
|
677
|
+
* 0.7.2: never silent. It appends a local `knowledge_merge {code, files, runs}` event on every call (also with 0 files),
|
|
678
|
+
* and says so when no delta was found although the root's run dir is missing. Returns { code, out, files }.
|
|
679
|
+
*/
|
|
680
|
+
function knowledgeOnAccept(opts, ctx, root, tasks, untagged, events) {
|
|
681
|
+
if (opts['no-knowledge']) return null;
|
|
682
|
+
const kn = require('./knowledge.cjs');
|
|
683
|
+
const { found, scanned, rootDirExists } = deltaRunDirs(root, tasks, events, untagged);
|
|
684
|
+
const nFiles = found.reduce((n, f) => n + f.files.length, 0);
|
|
597
685
|
const taskArg = tasks.join(',');
|
|
598
|
-
const
|
|
599
|
-
let
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
686
|
+
const outs = [];
|
|
687
|
+
let code = 0;
|
|
688
|
+
for (const f of found) {
|
|
689
|
+
const retry = `dieu-phoi knowledge merge --run ${f.id} --task ${taskArg}${f.untagged ? ' --untagged' : ''}`;
|
|
690
|
+
let r;
|
|
691
|
+
try {
|
|
692
|
+
r = kn.cmdMerge({ _: [], run: f.dir, task: taskArg, untagged: f.untagged, repo: opts.repo });
|
|
693
|
+
} catch (e) {
|
|
694
|
+
code = Math.max(code, 1);
|
|
695
|
+
outs.push(`knowledge: gộp lỗi (${e.message}) — chạy lại: ${retry}`);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
code = Math.max(code, r.code);
|
|
699
|
+
const tail = r.code ? `\n→ có mục chưa gộp: get lại mục đó, chép dòng base: mới vào delta (hoặc bỏ mục), rồi ${retry}` : '';
|
|
700
|
+
outs.push(`knowledge (tự gộp khi accept, run ${f.id}):\n${r.out}${tail}`);
|
|
604
701
|
}
|
|
605
|
-
|
|
606
|
-
|
|
702
|
+
let note = '';
|
|
703
|
+
if (!nFiles) {
|
|
704
|
+
// nothing to merge is normal for most tasks; say it only when the run dir the root names is missing (the s1 silent case)
|
|
705
|
+
if (!rootDirExists) note = `knowledge: không có delta của ${taskArg} (run ${root} không tồn tại; đã quét ${scanned} run dir theo tên knowledge-delta-<task>*).`;
|
|
706
|
+
}
|
|
707
|
+
try {
|
|
708
|
+
core.appendEvent(ctx, Object.assign({
|
|
709
|
+
schema: core.SCHEMA, kind: 'knowledge_merge', ts: core.nowIso(), agent_id: `km:${tasks[0]}`, task_id: tasks[0],
|
|
710
|
+
code, files: nFiles, runs: found.map((f) => f.id), tasks: tasks.slice(0, 20),
|
|
711
|
+
}, core.commonFields(ctx)));
|
|
712
|
+
} catch (_) { /* the log is best effort here: the merge result is still printed */ }
|
|
713
|
+
return { code, files: nFiles, out: outs.concat(note ? [note] : []).join('\n') };
|
|
607
714
|
}
|
|
608
715
|
|
|
609
716
|
function cmdVerdict(opts) {
|
|
@@ -613,27 +720,33 @@ function cmdVerdict(opts) {
|
|
|
613
720
|
const list = verdictRoot(opts, ctx, events);
|
|
614
721
|
for (const x of list) core.appendEvent(ctx, x.ev);
|
|
615
722
|
const root = String(opts.root);
|
|
616
|
-
const kn = knowledgeOnAccept(opts, ctx, root, [root].concat(list.map((x) => x.ev.task_id)), true);
|
|
723
|
+
const kn = knowledgeOnAccept(opts, ctx, root, [root].concat(list.map((x) => x.ev.task_id)), true, events);
|
|
724
|
+
// 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.
|
|
725
|
+
const wtLines = list.reduce((acc, x) => acc.concat(wt.cleanupTask(ctx, events, x.ev.task_id, opts.repo)), []);
|
|
617
726
|
if (!list.length) {
|
|
618
727
|
// Every step was already judged (e.g. accepted one by one with --task): still merge the root + untagged deltas.
|
|
619
728
|
// 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.
|
|
620
|
-
if (!kn) throw new Error(list.emptyMsg);
|
|
729
|
+
if (!kn || !kn.files) throw new Error(`${list.emptyMsg}${kn && kn.out ? `\n${kn.out}` : ''}`);
|
|
621
730
|
const msg = `Không còn bước nào của ${root} chờ nghiệm thu; chỉ gộp delta của root và delta không thẻ.`;
|
|
622
731
|
if (opts.json) { process.stderr.write(`${msg}\n${kn.out}\n`); return { code: kn.code, out: '[]' }; }
|
|
623
732
|
return { code: kn.code, out: `${msg}\n${kn.out}` };
|
|
624
733
|
}
|
|
625
734
|
if (opts.json) {
|
|
626
|
-
if (kn) process.stderr.write(`${kn.out}\n`);
|
|
735
|
+
if (kn && kn.out) process.stderr.write(`${kn.out}\n`);
|
|
736
|
+
if (wtLines.length) process.stderr.write(`${wtLines.join('\n')}\n`);
|
|
627
737
|
return { code: 0, out: JSON.stringify(list.map((x) => x.ev)) };
|
|
628
738
|
}
|
|
629
739
|
const msg = `Đã ghi người dùng accept cho ${list.length} bước của ${opts.root}: ${list.map((x) => `${x.step}=${x.ev.task_id}`).join(', ')}.`;
|
|
630
|
-
return { code: 0, out: kn ?
|
|
740
|
+
return { code: 0, out: [msg].concat(wtLines, kn && kn.out ? [kn.out] : []).join('\n') };
|
|
631
741
|
}
|
|
632
742
|
const ev = typeof opts.task === 'string' ? verdictUser(opts, ctx, events) : verdictAgent(opts, ctx, events);
|
|
633
743
|
core.appendEvent(ctx, ev);
|
|
634
|
-
const kn = ev.by === 'user' && ev.outcome === 'accept' ? knowledgeOnAccept(opts, ctx, rootOfTask(ctx, ev.task_id), [ev.task_id], false) : null;
|
|
744
|
+
const kn = ev.by === 'user' && ev.outcome === 'accept' ? knowledgeOnAccept(opts, ctx, rootOfTask(ctx, ev.task_id), [ev.task_id], false, events) : null;
|
|
745
|
+
// s17: người dùng accept → gỡ worktree có dấu của task (sạch); reject giữ nguyên để làm lại.
|
|
746
|
+
const wtLines = ev.by === 'user' && ev.outcome === 'accept' ? wt.cleanupTask(ctx, events, ev.task_id, opts.repo) : [];
|
|
635
747
|
if (opts.json) {
|
|
636
|
-
if (kn) process.stderr.write(`${kn.out}\n`);
|
|
748
|
+
if (kn && kn.out) process.stderr.write(`${kn.out}\n`);
|
|
749
|
+
if (wtLines.length) process.stderr.write(`${wtLines.join('\n')}\n`);
|
|
637
750
|
return { code: 0, out: JSON.stringify(ev) };
|
|
638
751
|
}
|
|
639
752
|
if (ev.by === 'auto') {
|
|
@@ -641,7 +754,7 @@ function cmdVerdict(opts) {
|
|
|
641
754
|
}
|
|
642
755
|
if (ev.by === 'user') {
|
|
643
756
|
const msg = `Đã ghi người dùng ${ev.outcome}${ev.cause ? ` (${ev.cause})` : ''} cho task ${ev.task_id}, vòng ${ev.round}.`;
|
|
644
|
-
return { code: 0, out: kn ?
|
|
757
|
+
return { code: 0, out: [msg].concat(wtLines, kn && kn.out ? [kn.out] : []).join('\n') };
|
|
645
758
|
}
|
|
646
759
|
return { code: 0, out: `Đã ghi ${ev.outcome} cho ${ev.agent_id}${ev.task_id ? ` (task ${ev.task_id})` : ''}.` };
|
|
647
760
|
}
|
|
@@ -665,8 +778,14 @@ function cmdDiscard(opts) {
|
|
|
665
778
|
}, core.commonFields(ctx));
|
|
666
779
|
core.appendEvent(ctx, ev);
|
|
667
780
|
const n = events.filter((e) => e.task_id === taskId).length;
|
|
668
|
-
|
|
669
|
-
|
|
781
|
+
// s17: task bị loại → gỡ worktree có dấu của task (sạch); còn thay đổi thì giữ và báo.
|
|
782
|
+
const wtLines = wt.cleanupTask(ctx, events, taskId, opts.repo);
|
|
783
|
+
if (opts.json) {
|
|
784
|
+
if (wtLines.length) process.stderr.write(`${wtLines.join('\n')}\n`);
|
|
785
|
+
return { code: 0, out: JSON.stringify(ev) };
|
|
786
|
+
}
|
|
787
|
+
const msg = `Đã loại task ${taskId} (${n} bản ghi) khỏi thống kê, choose và state. Log không bị sửa; sink nhận bản ghi discard ở lần sync kế tiếp.`;
|
|
788
|
+
return { code: 0, out: [msg].concat(wtLines).join('\n') };
|
|
670
789
|
}
|
|
671
790
|
|
|
672
791
|
// ------------------------------------------------------------------ steer
|
|
@@ -748,7 +867,10 @@ function measureOrch(opts, ctx, prof) {
|
|
|
748
867
|
}
|
|
749
868
|
|
|
750
869
|
const CTX_WARN_KEEP_MS = 14 * 24 * 3600 * 1000;
|
|
751
|
-
|
|
870
|
+
// core 0.7.2 (s11 9y3nh4cb, log 62 phiên / 4.749 lượt điều phối, số liệu trong dp-out): 400k -> 250k. Chi phí điều
|
|
871
|
+
// phối mỗi lượt đứng ~0,27–0,29 $ tới 250k rồi nhảy 0,45 $ ở 250–400k; median ctx trước bước nén 211k (150–250k). Bắt buộc
|
|
872
|
+
// < SESSION_CAP_TOKENS (800k) và < SESSION_CAP_COMPACTIONS (10) — test ctx-compact-count bảo vệ bất biến này.
|
|
873
|
+
const ORCH_NEW_SESSION_TOKENS = 250000;
|
|
752
874
|
const ORCH_NEW_SESSION_COMPACTIONS = 5;
|
|
753
875
|
|
|
754
876
|
/** 0 = within the cap; 1 = over the cap, 2 = over 2×, 3 = over 4×, … */
|
|
@@ -820,17 +942,21 @@ function cmdState(opts) {
|
|
|
820
942
|
const sid = typeof opts.session === 'string' && opts.session ? opts.session
|
|
821
943
|
: (process.env.CLAUDE_CODE_SESSION_ID || null);
|
|
822
944
|
// retro C6: knowledge deltas older than 24 h that no store has applied (file reads only, never throws)
|
|
823
|
-
const deltas = core.
|
|
945
|
+
const deltas = core.scanUnmergedDeltasFor(ctx);
|
|
824
946
|
const deltaLines = core.unmergedDeltaLines(deltas);
|
|
947
|
+
const hasDeltas = core.hasUnmergedDeltas(deltas);
|
|
825
948
|
if (!sid) {
|
|
826
949
|
if (opts.json) {
|
|
827
|
-
return { code: 0, out: JSON.stringify(Object.assign({ session_id: null, generated_at: core.nowIso(), runs_dir: null, plans: [], tasks: [] },
|
|
950
|
+
return { code: 0, out: JSON.stringify(Object.assign({ session_id: null, generated_at: core.nowIso(), runs_dir: null, plans: [], tasks: [] }, hasDeltas ? { unmerged_deltas: deltas } : {})) };
|
|
828
951
|
}
|
|
829
952
|
return { code: 0, out: ['Không có session (thiếu CLAUDE_CODE_SESSION_ID, dùng --session <id>).'].concat(deltaLines).join('\n') };
|
|
830
953
|
}
|
|
831
954
|
const state = core.buildSessionState(core.readEvents(ctx), sid, { plansDir: ctx.paths.plans });
|
|
832
|
-
|
|
833
|
-
|
|
955
|
+
const orphans = orphanCount(ctx); // s16: null (unknown) leaves the line out
|
|
956
|
+
if (opts.json) {
|
|
957
|
+
return { code: 0, out: JSON.stringify(Object.assign({}, state, hasDeltas ? { unmerged_deltas: deltas } : {}, orphans === null ? {} : { orphan_procs: orphans }), null, 2) };
|
|
958
|
+
}
|
|
959
|
+
return { code: 0, out: [core.formatSessionState(state, core.STATE_TEXT_CAP)].concat(deltaLines, orphans === null ? [] : [`orphan_procs: ${orphans}`]).join('\n') };
|
|
834
960
|
}
|
|
835
961
|
|
|
836
962
|
// ------------------------------------------------------------------ stats / retro
|
|
@@ -1273,7 +1399,8 @@ function planTypeNotes(plan) {
|
|
|
1273
1399
|
* `choose --override` already logged adds nothing.
|
|
1274
1400
|
*/
|
|
1275
1401
|
function planOverrideEvents(ctx, plan, events) {
|
|
1276
|
-
|
|
1402
|
+
// core 0.7.2 (K4): an implicit override must not hide the user's override of the same tier from the learning
|
|
1403
|
+
const seen = new Set(events.filter((e) => e.kind === 'override' && e.source !== 'implicit').map((e) => `${e.task_id}|${e.chosen_tier}`));
|
|
1277
1404
|
const out = [];
|
|
1278
1405
|
for (const r of plan.steps) {
|
|
1279
1406
|
if (!r.overridden || !r.task_id || !r.tier || !r.suggested_tier) continue;
|
|
@@ -1366,6 +1493,7 @@ function cmdPlanTable(opts) {
|
|
|
1366
1493
|
const base = chooseInput(Object.assign({}, opts, { type: 'x', score: 0, override: undefined }), ctx, core.readEvents(ctx));
|
|
1367
1494
|
delete base.override;
|
|
1368
1495
|
const rootEv = typeof opts.root === 'string' && core.isValidTaskId(opts.root) ? core.rootModeFromEvents(base.events, opts.root) : null;
|
|
1496
|
+
const modeChosen = Boolean(base.mode || rootEv || fileMode);
|
|
1369
1497
|
base.mode = base.mode || (rootEv ? rootEv.mode : null) || fileMode || core.DEFAULT_MODE;
|
|
1370
1498
|
const plan = buildPlanTable(raw, base);
|
|
1371
1499
|
const problems = planTaskIdProblems(plan, opts.approve ? String(opts.root) : null);
|
|
@@ -1402,7 +1530,12 @@ function cmdPlanTable(opts) {
|
|
|
1402
1530
|
}
|
|
1403
1531
|
lines.push('');
|
|
1404
1532
|
lines.push(`**Tổng chi phí ước tính:** ~${fmtUsd(plan.total_cost_usd)} · **Thời gian (đường găng, chạy song song khi không phụ thuộc):** ~${fmtNum(plan.critical_path_min)} phút (cộng dồn ${fmtNum(plan.sum_time_min)} phút)`);
|
|
1405
|
-
|
|
1533
|
+
// Mode được hỏi ở cổng 1 chỉ khi root chưa có mode và kế hoạch có ≥4 bước s≤6 (0.7.2); còn lại không hỏi
|
|
1534
|
+
const shortSteps = plan.steps.filter((r) => r.action !== 'refused' && Number.isFinite(r.score) && r.score <= 6).length;
|
|
1535
|
+
const modeText = modeChosen ? plan.mode : (shortSteps >= 4
|
|
1536
|
+
? `HỎI người dùng chọn mode (${shortSteps} bước s≤6): fast | normal | strict (mặc định normal); đặt bằng \`dieu-phoi mode --root ${typeof opts.root === 'string' ? opts.root : '<root_task_id>'} <mode>\``
|
|
1537
|
+
: 'normal (không hỏi)');
|
|
1538
|
+
lines.push(`**Mode:** ${modeText}${plan.self_batch_steps ? ` · Kiểm (fast): bỏ ${plan.self_batch_steps} verifier riêng, 1 verify gộp (cả suite một lần) trước khi trình người dùng` : ''}`);
|
|
1406
1539
|
if (plan.rough) lines.push('', '_(thô) = ô chưa đủ 5 task, ước lượng theo giả định token của từng tier._');
|
|
1407
1540
|
if (plan.refused.length) {
|
|
1408
1541
|
lines.push('', `**Cảnh báo:** ${plan.refused.length} bước bị từ chối (${plan.refused.map((x) => x.step).join(', ')}), không tính vào tổng — sửa rồi chạy lại.`);
|
|
@@ -1415,6 +1548,6 @@ function cmdPlanTable(opts) {
|
|
|
1415
1548
|
}
|
|
1416
1549
|
|
|
1417
1550
|
module.exports = {
|
|
1418
|
-
skillDirOf, cmdSetup, cmdUninstall, cmdDoctor, cmdStart, checkSinkLive, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdDiscard, ctxWarnOnce, ctxWarnLevel, sweepStaleTmp, planOverrideEvents, recordPlanSuggestions, cmdStats, cmdRetro, cmdPrune: (o) => require('./prune.cjs').cmdPrune(o), cmdChoose, cmdMode, modeEvent, cmdCtx, cmdState, measureOrch,
|
|
1551
|
+
ORCH_NEW_SESSION_TOKENS, ORCH_NEW_SESSION_COMPACTIONS, skillDirOf, cmdSetup, cmdUninstall, cmdDoctor, cmdStart, checkSinkLive, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdDiscard, ctxWarnOnce, ctxWarnLevel, sweepStaleTmp, planOverrideEvents, recordPlanSuggestions, cmdStats, cmdRetro, cmdPrune: (o) => require('./prune.cjs').cmdPrune(o), cmdChoose, cmdMode, modeEvent, cmdCtx, cmdState, orphanCheck, orphanCount, measureOrch,
|
|
1419
1552
|
cmdPlanTable, cmdNewId, requireTaskId, buildPlanTable, recordApprovedPlan, planTaskIdProblems, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
|
|
1420
1553
|
};
|
package/lib/knowledge.cjs
CHANGED
|
@@ -574,10 +574,9 @@ function runDirOf(v) {
|
|
|
574
574
|
return isDir(p) ? { dir: p } : { error: `Không thấy run ${s} (${slash(p)}).` };
|
|
575
575
|
}
|
|
576
576
|
|
|
577
|
-
/** Task id a delta file belongs to: knowledge-delta-<task_id>[-r<n>].md (8 chars a-z0-9); null = untagged. */
|
|
577
|
+
/** Task id a delta file belongs to: knowledge-delta-<task_id>[-r<n>|-h<n>].md (8 chars a-z0-9; -h<n> = handoff); null = untagged. */
|
|
578
578
|
function deltaTaskOf(file) {
|
|
579
|
-
|
|
580
|
-
return m ? m[1].toLowerCase() : null;
|
|
579
|
+
return core.deltaTagOf(file);
|
|
581
580
|
}
|
|
582
581
|
|
|
583
582
|
/**
|
|
@@ -585,7 +584,7 @@ function deltaTaskOf(file) {
|
|
|
585
584
|
* tagged with one of them (plus untagged ones when untagged is true).
|
|
586
585
|
*/
|
|
587
586
|
function deltaFiles(runDir, filter) {
|
|
588
|
-
const num = (f) => { const m = /-
|
|
587
|
+
const num = (f) => { const m = /-[rh](\d+)\.md$/i.exec(f); return m ? Number(m[1]) : 0; };
|
|
589
588
|
return fs.readdirSync(runDir)
|
|
590
589
|
.filter((f) => /^knowledge-delta.*\.md$/i.test(f))
|
|
591
590
|
.filter((f) => {
|
package/lib/procs.cjs
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// s16 (core 0.7.2): read-only look at the process table. Everything here that decides something is a pure function over a
|
|
3
|
+
// plain list [{pid, ppid, name, created (epoch ms | null), ws (bytes | null), cmd}], so tests feed fixture lists and never
|
|
4
|
+
// touch the system. Only listProcesses() and killTree() reach the OS; listProcesses never throws (null on any failure),
|
|
5
|
+
// killTree is called by the hook only when config kill_leftovers is true.
|
|
6
|
+
|
|
7
|
+
const cp = require('child_process');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 4000;
|
|
10
|
+
const ORPHAN_NAMES = ['claude.exe', 'codebase-memory-mcp.exe', 'node.exe', 'bash.exe'];
|
|
11
|
+
// never reported as a leftover of one agent: the Claude Code processes themselves and the code-graph server (shared)
|
|
12
|
+
const LEFTOVER_SKIP_NAMES = ['claude.exe', 'claude', 'codebase-memory-mcp.exe', 'codebase-memory-mcp', 'conhost.exe'];
|
|
13
|
+
|
|
14
|
+
const PS_WIN = [
|
|
15
|
+
'Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,',
|
|
16
|
+
"@{n='Created';e={if($_.CreationDate){[int64]($_.CreationDate.ToUniversalTime()-[datetime]'1970-01-01').TotalMilliseconds}else{$null}}},",
|
|
17
|
+
'WorkingSetSize,',
|
|
18
|
+
"@{n='Cmd';e={if($_.CommandLine){$_.CommandLine.Substring(0,[Math]::Min(200,$_.CommandLine.Length))}else{$null}}}",
|
|
19
|
+
' | ConvertTo-Json -Compress',
|
|
20
|
+
].join('');
|
|
21
|
+
|
|
22
|
+
const lower = (s) => String(s || '').toLowerCase();
|
|
23
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : (typeof v === 'string' && /^\d+$/.test(v) ? Number(v) : null));
|
|
24
|
+
|
|
25
|
+
/** Windows JSON from PS_WIN (also a single object when only one process) -> list, or null when it is not that shape. */
|
|
26
|
+
function parseWindowsJson(text) {
|
|
27
|
+
let j;
|
|
28
|
+
try { j = JSON.parse(String(text).replace(/^/, '')); } catch (_) { return null; }
|
|
29
|
+
const arr = Array.isArray(j) ? j : (j && typeof j === 'object' ? [j] : null);
|
|
30
|
+
if (!arr) return null;
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const r of arr) {
|
|
33
|
+
if (!r || typeof r !== 'object') continue;
|
|
34
|
+
const pid = num(r.ProcessId);
|
|
35
|
+
if (pid === null) continue;
|
|
36
|
+
out.push({
|
|
37
|
+
pid,
|
|
38
|
+
ppid: num(r.ParentProcessId) === null ? 0 : num(r.ParentProcessId),
|
|
39
|
+
name: typeof r.Name === 'string' ? r.Name : '',
|
|
40
|
+
created: num(r.Created),
|
|
41
|
+
ws: num(r.WorkingSetSize),
|
|
42
|
+
cmd: typeof r.Cmd === 'string' ? r.Cmd : null,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return out.length ? out : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const MONTHS = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 };
|
|
49
|
+
|
|
50
|
+
/** `ps -eo pid,ppid,lstart,comm,args` (lstart = "Wed Nov 5 21:06:01 2026", local time) -> list, or null. */
|
|
51
|
+
function parsePs(text) {
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const line of String(text).split(/\r?\n/)) {
|
|
54
|
+
const m = /^\s*(\d+)\s+(\d+)\s+\w{3}\s+(\w{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})\s+(\S+)\s*(.*)$/.exec(line);
|
|
55
|
+
if (!m) continue;
|
|
56
|
+
const t = new Date(Number(m[8]), MONTHS[m[3]], Number(m[4]), Number(m[5]), Number(m[6]), Number(m[7])).getTime();
|
|
57
|
+
out.push({ pid: Number(m[1]), ppid: Number(m[2]), name: m[9].replace(/^.*\//, ''), created: Number.isFinite(t) ? t : null, ws: null, cmd: m[10] ? m[10].slice(0, 200) : null });
|
|
58
|
+
}
|
|
59
|
+
return out.length ? out : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Process list of this machine, or null (any error, timeout, empty or unreadable output). Never throws.
|
|
64
|
+
* `exec(file, args, {timeout})` -> stdout string is injectable for tests.
|
|
65
|
+
*/
|
|
66
|
+
function listProcesses(opts) {
|
|
67
|
+
const o = opts || {};
|
|
68
|
+
const timeout = typeof o.timeoutMs === 'number' && o.timeoutMs > 0 ? o.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
69
|
+
const platform = o.platform || process.platform;
|
|
70
|
+
const exec = typeof o.exec === 'function' ? o.exec : defaultExec;
|
|
71
|
+
if (process.env.DIEU_PHOI_NO_PROCS === '1' && typeof o.exec !== 'function') return null; // tests: never look at the real machine
|
|
72
|
+
try {
|
|
73
|
+
if (platform === 'win32') {
|
|
74
|
+
const out = exec('powershell', ['-NoProfile', '-NonInteractive', '-Command', PS_WIN], { timeout });
|
|
75
|
+
return typeof out === 'string' ? parseWindowsJson(out) : null;
|
|
76
|
+
}
|
|
77
|
+
const out = exec('ps', ['-eo', 'pid,ppid,lstart,comm,args'], { timeout });
|
|
78
|
+
return typeof out === 'string' ? parsePs(out) : null;
|
|
79
|
+
} catch (_) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function defaultExec(file, args, o) {
|
|
85
|
+
const r = cp.spawnSync(file, args, { encoding: 'utf8', timeout: o.timeout, windowsHide: true, maxBuffer: 32 * 1024 * 1024 });
|
|
86
|
+
if (r.error || r.status !== 0 || r.signal) return null;
|
|
87
|
+
return r.stdout || '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function byPid(list) {
|
|
91
|
+
const m = new Map();
|
|
92
|
+
for (const p of list || []) m.set(p.pid, p);
|
|
93
|
+
return m;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** All descendants of rootPid (children, grandchildren, ...; not rootPid itself). Cycle-safe (pid reuse). */
|
|
97
|
+
function descendants(list, rootPid) {
|
|
98
|
+
const kids = new Map();
|
|
99
|
+
for (const p of list || []) {
|
|
100
|
+
if (p.pid === p.ppid) continue;
|
|
101
|
+
if (!kids.has(p.ppid)) kids.set(p.ppid, []);
|
|
102
|
+
kids.get(p.ppid).push(p);
|
|
103
|
+
}
|
|
104
|
+
const out = [];
|
|
105
|
+
const seen = new Set([rootPid]);
|
|
106
|
+
const queue = [rootPid];
|
|
107
|
+
while (queue.length) {
|
|
108
|
+
const cur = queue.shift();
|
|
109
|
+
for (const c of kids.get(cur) || []) {
|
|
110
|
+
if (seen.has(c.pid)) continue;
|
|
111
|
+
seen.add(c.pid);
|
|
112
|
+
out.push(c);
|
|
113
|
+
queue.push(c.pid);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Ancestors of pid, nearest first (stops at a missing parent, a cycle or 64 hops). */
|
|
120
|
+
function ancestors(list, pid) {
|
|
121
|
+
const idx = byPid(list);
|
|
122
|
+
const out = [];
|
|
123
|
+
const seen = new Set([pid]);
|
|
124
|
+
let cur = idx.get(pid);
|
|
125
|
+
for (let i = 0; cur && i < 64; i++) {
|
|
126
|
+
const par = idx.get(cur.ppid);
|
|
127
|
+
if (!par || seen.has(par.pid)) break;
|
|
128
|
+
seen.add(par.pid);
|
|
129
|
+
out.push(par);
|
|
130
|
+
cur = par;
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Nearest ancestor of pid named claude(.exe), or null. */
|
|
136
|
+
function claudeAncestor(list, pid) {
|
|
137
|
+
return ancestors(list, pid).find((p) => /^claude(\.exe)?$/.test(lower(p.name))) || null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A parent that is gone, or whose pid was reused by a process created after the child. */
|
|
141
|
+
function isOrphan(p, idx) {
|
|
142
|
+
if (!p.ppid) return false; // pid 0: the system's own roots
|
|
143
|
+
const par = idx.get(p.ppid);
|
|
144
|
+
if (!par) return true;
|
|
145
|
+
return Boolean(par.created && p.created && par.created > p.created);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Processes whose parent is no longer in the list. `names` (case-insensitive; default claude.exe, codebase-memory-mcp.exe,
|
|
150
|
+
* node.exe, bash.exe), `minAgeMs` + `now` to keep only old ones, `skip(p)` to drop known-legitimate ones.
|
|
151
|
+
*/
|
|
152
|
+
function orphans(list, o) {
|
|
153
|
+
const opt = o || {};
|
|
154
|
+
const names = new Set((opt.names || ORPHAN_NAMES).map(lower));
|
|
155
|
+
const now = typeof opt.now === 'number' ? opt.now : Date.now();
|
|
156
|
+
const idx = byPid(list);
|
|
157
|
+
const out = [];
|
|
158
|
+
for (const p of list || []) {
|
|
159
|
+
if (!names.has(lower(p.name))) continue;
|
|
160
|
+
if (!isOrphan(p, idx)) continue;
|
|
161
|
+
if (opt.minAgeMs && !(p.created && now - p.created >= opt.minAgeMs)) continue;
|
|
162
|
+
if (typeof opt.skip === 'function' && opt.skip(p)) continue;
|
|
163
|
+
out.push(p);
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The code-graph daemon is meant to outlive any session (shared by all of them): never an orphan to clean. */
|
|
169
|
+
function isCbmDaemon(p) {
|
|
170
|
+
return /codebase-memory-mcp/i.test(p.name) && /daemon/i.test(p.cmd || '');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Orphaned Claude Code / code-graph processes older than minAgeMs (doctor and state): [{pid, name, age_h, ws_mb}] oldest first.
|
|
175
|
+
*/
|
|
176
|
+
function staleOrphans(list, o) {
|
|
177
|
+
const opt = o || {};
|
|
178
|
+
const now = typeof opt.now === 'number' ? opt.now : Date.now();
|
|
179
|
+
return orphans(list, { names: ['claude.exe', 'claude', 'codebase-memory-mcp.exe', 'codebase-memory-mcp'], minAgeMs: opt.minAgeMs, now, skip: isCbmDaemon })
|
|
180
|
+
.sort((a, b) => (a.created || 0) - (b.created || 0))
|
|
181
|
+
.map((p) => ({ pid: p.pid, name: p.name, age_h: Math.round(((now - (p.created || now)) / 3600000) * 10) / 10, ws_mb: p.ws ? Math.round(p.ws / 1048576) : null }));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Processes an agent left behind: descendants of the Claude Code process (rootPid) created at or after startMs, still in the
|
|
186
|
+
* list, not on the hook's own chain (selfPid and its ancestors), not claude/code-graph/conhost. Pure. Returns the list items;
|
|
187
|
+
* `roots` = those whose parent is not itself a leftover (what a kill must target; taskkill /T takes their children).
|
|
188
|
+
*/
|
|
189
|
+
function leftoverProcesses(list, o) {
|
|
190
|
+
const opt = o || {};
|
|
191
|
+
if (!Array.isArray(list) || !opt.rootPid || !opt.startMs) return { all: [], roots: [] };
|
|
192
|
+
const own = new Set([opt.selfPid, ...ancestors(list, opt.selfPid).map((p) => p.pid)].filter(Boolean));
|
|
193
|
+
const skip = new Set((opt.skipNames || LEFTOVER_SKIP_NAMES).map(lower));
|
|
194
|
+
const all = descendants(list, opt.rootPid).filter((p) => !own.has(p.pid) && p.created && p.created >= opt.startMs && !skip.has(lower(p.name)));
|
|
195
|
+
const ids = new Set(all.map((p) => p.pid));
|
|
196
|
+
return { all, roots: all.filter((p) => !ids.has(p.ppid)) };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Kill process trees. Windows `taskkill /PID n /T /F`, else SIGTERM. `deadline` (epoch ms) stops early. Returns the pids stopped; never throws. */
|
|
200
|
+
function killTree(pids, o) {
|
|
201
|
+
const opt = o || {};
|
|
202
|
+
const done = [];
|
|
203
|
+
for (const pid of pids || []) {
|
|
204
|
+
const left = opt.deadline ? opt.deadline - Date.now() : 3000;
|
|
205
|
+
if (left <= 0) break; // out of time budget: the rest stay reported, not killed
|
|
206
|
+
try {
|
|
207
|
+
if ((opt.platform || process.platform) === 'win32') {
|
|
208
|
+
const r = cp.spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { timeout: Math.min(3000, left), windowsHide: true });
|
|
209
|
+
if (!r.error && r.status === 0) done.push(pid);
|
|
210
|
+
} else {
|
|
211
|
+
process.kill(pid, 'SIGTERM');
|
|
212
|
+
done.push(pid);
|
|
213
|
+
}
|
|
214
|
+
} catch (_) { /* gone already or not ours */ }
|
|
215
|
+
}
|
|
216
|
+
return done;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
PS_WIN, ORPHAN_NAMES, LEFTOVER_SKIP_NAMES,
|
|
221
|
+
parseWindowsJson, parsePs, listProcesses,
|
|
222
|
+
descendants, ancestors, claudeAncestor, orphans, staleOrphans, isCbmDaemon, leftoverProcesses, killTree,
|
|
223
|
+
};
|