wkappbot-code 0.3.32 → 0.3.33
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/bin/wkappbot-code.js +3 -2
- package/lib/holders.js +39 -10
- package/lib/rmtree.js +3 -2
- package/package.json +1 -1
package/bin/wkappbot-code.js
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
import { homedir, tmpdir } from "node:os";
|
|
42
42
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
43
43
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
44
|
-
import { quietCheck, runUpdate, updateRow } from "../lib/update.js"; import { rmTreeHard, waitForArchive, holderReport } from "../lib/rmtree.js"; import { waitForHolders } from "../lib/holders.js"; import { plainFileUrl } from "../lib/fileurl.js"; import { removeStaleWkskill } from "../lib/stale-wkskill.js"; import { checkPath, installBothLinks, removeCreatedLinks } from "../lib/doctor-path.js";
|
|
44
|
+
import { quietCheck, runUpdate, updateRow } from "../lib/update.js"; import { rmTreeHard, waitForArchive, holderReport } from "../lib/rmtree.js"; import { waitForHolders, stopHolders } from "../lib/holders.js"; import { plainFileUrl } from "../lib/fileurl.js"; import { removeStaleWkskill } from "../lib/stale-wkskill.js"; import { checkPath, installBothLinks, removeCreatedLinks } from "../lib/doctor-path.js";
|
|
45
45
|
import { applySets, askEach, buildConfig, configRow, detect, lineDiff, printDetected } from "../lib/config.js";
|
|
46
46
|
import { aiCliRows, installAiClis, removeAiClis } from "../lib/ai-clis.js";
|
|
47
47
|
import { providerCli, providerRow } from "../lib/providers.js";
|
|
@@ -437,6 +437,7 @@ function install(o) {
|
|
|
437
437
|
// 0.3.13 (owner 2026-11-09: Git Bash is a required tool too): bash, sh, curl, tar and git first; on Windows a Git that
|
|
438
438
|
// is not on PATH is used from its folder, and a missing one is installed with winget (lib/prereq.js)
|
|
439
439
|
// 0.3.32 (2026-11-12, kih ROOT 3): the uninstall also waits for every process whose command line names the harness dir (lib/holders.js) and prints the holders as one line the SDK e2e cause filter keeps.
|
|
440
|
+
// 0.3.33 (2026-11-12, kih ROOT 3): the holder wait matches the Git Bash spelling of the harness dir (/d/a/...), and after 8 s the shell-class children still naming it (bash, sh, flock, setsid, nohup, python; never wkappbot, wkchat, node) are ended before the removal (lib/holders.js stopHolders).
|
|
440
441
|
// 0.3.31 (2026-11-12, spokesman order): provider add on a clean machine creates the OpenCode config folder (lib/providers.js mkdirSync) instead of dying with ENOENT, and the opencode.json backup of provider add and remove goes under <config dir>/wkappbot-code/backups, which uninstall removes with the rest of its state, so no opencode.json.bak-<stamp> stays in the user config folder.
|
|
441
442
|
// 0.3.30 (2026-11-12, spokesman order after gate runs 43246725941 and 43247534046): 0.3.29 left the EPERM on .claude/hooks/wkharness-run.sh in one of two win-x64-nopy runs. The detached shadow run (wkjs_shadow) and the skill-draft child of the hook script were still started inside the harness copy with the script fd open; both now start from cwd / with the fd closed (wk_detach_prep), like the archive child. A failing tree removal on Windows now prints lib/rmtree.js holderReport: every Win32_Process row naming the harness dir plus every shell, node and python process with pid, ppid and command line, so a run that still hits EPERM names the real holder.
|
|
442
443
|
// 0.3.29 (2026-11-12, spokesman order after SDK gate run 43227049219): uninstall waits (bounded 15 s) for a RECORDED archive pid before it removes the harness copy (lib/rmtree.js waitForArchive; the detached archive child starts from cwd / with the script fd closed), so no EPERM on .claude/hooks/wkharness-run.sh; install records st.opencodeBackups and uninstall leaves no opencode.json.bak-<stamp> of its own.
|
|
@@ -609,7 +610,7 @@ function uninstall(o) {
|
|
|
609
610
|
};
|
|
610
611
|
// (B) the detached per-turn archive child may still hold .claude/hooks/wkharness-run.sh (Windows EPERM): wait for its RECORDED pid first.
|
|
611
612
|
if (existsSync(HARNESS_DIR())) { const w = waitForArchive(HARNESS_DIR()); if (w.waited) log("harness: waited " + w.waited + " ms for the archive child" + (w.alive.length ? " (pid " + w.alive.join(",") + " still running)" : "")); }
|
|
612
|
-
if (st.harness && st.harness.mode === "web" && pr.harnessDirExisted !== true && existsSync(HARNESS_DIR())) { const h = waitForHolders(HARNESS_DIR()); if (h.waited) log("harness: waited " + h.waited + " ms for process(es) naming the harness dir" + (h.alive.length ? " (pid " + h.alive.join(",") + " still running)" : "")); }
|
|
613
|
+
if (st.harness && st.harness.mode === "web" && pr.harnessDirExisted !== true && existsSync(HARNESS_DIR())) { const h = waitForHolders(HARNESS_DIR()); if (h.waited) log("harness: waited " + h.waited + " ms for process(es) naming the harness dir" + (h.alive.length ? " (pid " + h.alive.join(",") + " still running)" : "")); if (h.alive.length) { const s = stopHolders(HARNESS_DIR()); log("harness: stopped " + s.stopped.length + " shell process(es) still naming the harness dir: " + s.stopped.map((r) => "pid=" + r.pid + " " + r.name + " " + String(r.cmd).slice(0, 200)).join(" ;; ") + (s.alive.length ? " (pid " + s.alive.join(",") + " still running)" : "")); } }
|
|
613
614
|
if (st.harness && st.harness.mode === "web" && pr.harnessDirExisted !== true && existsSync(HARNESS_DIR())) rmTree(HARNESS_DIR(), "harness");
|
|
614
615
|
if (pr.hooksDirExisted !== true && existsSync(HOOKS_DIR())) rmTree(HOOKS_DIR(), "push-gate");
|
|
615
616
|
const cfgFile = join(configDir(o), "opencode.json");
|
package/lib/holders.js
CHANGED
|
@@ -1,32 +1,61 @@
|
|
|
1
|
-
// holders.js -- wait for the processes that still hold the harness copy before an uninstall removes it (kih ROOT 3 2026-11-12
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// holders.js -- wait for, then stop, the processes that still hold the harness copy before an uninstall removes it (kih ROOT 3 2026-11-12).
|
|
2
|
+
// 0.3.32: the SDK win-x64-nopy gate (push run 43284028291, job 131412874028) still failed step 20 with EPERM on .claude/hooks/wkharness-run.sh while a 20 s holder wait ran,
|
|
3
|
+
// and the runner cleanup then killed 8 orphan bash processes. Two defects, both fixed in 0.3.33:
|
|
4
|
+
// 1. The command line of a Git Bash child names the harness dir in MSYS form (/d/a/_temp/home/.wk-harness/tools/wk-hippo-archive.sh), while the uninstall holds the Windows form
|
|
5
|
+
// (D:\a\_temp\home\.wk-harness). Matching only the Windows form never saw them (the live report listed bash.exe as a plain "shell" row, never HOLDS-DIR). dirForms() matches every form.
|
|
6
|
+
// 2. A detached archive child can wait up to 300 s on its flock, longer than any sane wait. After the bounded wait, stopHolders() ends the shell-class children of the harness
|
|
7
|
+
// (bash, sh, flock, setsid, nohup, python) whose command line names the dir; it never touches wkappbot, wkchat or wka11y processes, the uninstall's own pid or parent pid, or node.
|
|
8
|
+
// exec, sleep and kill are injectable so the selftest never queries or kills a real process.
|
|
5
9
|
import { spawnSync } from "node:child_process";
|
|
6
10
|
|
|
7
11
|
const sleepMs = (ms) => { try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch { /* no wait available */ } };
|
|
8
12
|
const procExec = (cmd, args) => { const r = spawnSync(cmd, args, { encoding: "utf8", timeout: 20000 }); return String(r.stdout || r.stderr || ""); };
|
|
13
|
+
const STOPPABLE = /^(?:bash|sh|flock|setsid|nohup|python3?)(?:\.exe)?$/i;
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
// Every spelling of one directory in a process command line: Windows (c:/x), Git Bash (/c/x), Cygwin (/cygdrive/c/x), WSL (/mnt/c/x). All lower case, slashes forward.
|
|
16
|
+
export function dirForms(dir) {
|
|
17
|
+
const w = String(dir).replace(/\\/g, "/").toLowerCase().replace(/\/+$/, "");
|
|
18
|
+
const forms = [w], m = /^([a-z]):\/(.*)$/.exec(w);
|
|
19
|
+
if (m) for (const pre of ["/", "/cygdrive/", "/mnt/"]) forms.push(pre + m[1] + "/" + m[2]);
|
|
20
|
+
return forms;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Rows { pid, name, cmd } of every process whose command line names the dir in any form, never this process or its parent.
|
|
24
|
+
export function listHolderRows(dir, opts = {}) {
|
|
11
25
|
const exec = opts.exec || procExec, isWin = opts.win ?? (process.platform === "win32");
|
|
12
|
-
const
|
|
26
|
+
const forms = dirForms(dir), own = new Set([process.pid, process.ppid]);
|
|
13
27
|
let table = "";
|
|
14
28
|
try {
|
|
15
29
|
table = isWin ? exec("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { \"$($_.ProcessId)`t$($_.ParentProcessId)`t$($_.Name)`t$([string]$_.CommandLine)\" }"])
|
|
16
30
|
: exec("ps", ["-eo", "pid=,ppid=,comm=,args="]).split(/\r?\n/).map((l) => l.trim().replace(/\s+/g, "\t")).join("\n");
|
|
17
31
|
} catch { return []; }
|
|
18
|
-
const
|
|
32
|
+
const rows = [];
|
|
19
33
|
for (const line of String(table).split(/\r?\n/)) {
|
|
20
34
|
const f = line.split("\t"), pid = parseInt(f[0], 10);
|
|
21
35
|
if (f.length < 4 || !(pid > 0) || own.has(pid)) continue;
|
|
22
|
-
|
|
36
|
+
const cmd = f.slice(3).join(" "), hay = cmd.replace(/\\/g, "/").toLowerCase();
|
|
37
|
+
if (forms.some((x) => hay.includes(x))) rows.push({ pid, name: String(f[2]).trim(), cmd });
|
|
23
38
|
}
|
|
24
|
-
return
|
|
39
|
+
return rows;
|
|
25
40
|
}
|
|
26
41
|
|
|
42
|
+
export function listHolders(dir, opts = {}) { return listHolderRows(dir, opts).map((r) => r.pid); }
|
|
43
|
+
|
|
27
44
|
export function waitForHolders(dir, opts = {}) {
|
|
28
|
-
const maxMs = opts.maxMs ??
|
|
45
|
+
const maxMs = opts.maxMs ?? 8000, gap = opts.gap || 500, nap = opts.sleep || sleepMs;
|
|
29
46
|
let alive = listHolders(dir, opts), waited = 0;
|
|
30
47
|
while (alive.length && waited < maxMs) { nap(gap); waited += gap; alive = listHolders(dir, opts); }
|
|
31
48
|
return { waited, alive };
|
|
32
49
|
}
|
|
50
|
+
|
|
51
|
+
// End the shell-class holders still alive, then report who remains. { stopped: [{pid,name,cmd}], alive: [pid...] }
|
|
52
|
+
export function stopHolders(dir, opts = {}) {
|
|
53
|
+
const kill = opts.kill || ((pid) => process.kill(pid, "SIGKILL")), nap = opts.sleep || sleepMs;
|
|
54
|
+
const stopped = [];
|
|
55
|
+
for (const r of listHolderRows(dir, opts)) {
|
|
56
|
+
if (!STOPPABLE.test(r.name)) continue;
|
|
57
|
+
try { kill(r.pid); stopped.push(r); } catch { /* already gone, or not ours to end */ }
|
|
58
|
+
}
|
|
59
|
+
if (stopped.length) nap(opts.settle ?? 1500);
|
|
60
|
+
return { stopped, alive: listHolders(dir, opts) };
|
|
61
|
+
}
|
package/lib/rmtree.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { existsSync, lstatSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { dirForms } from "./holders.js";
|
|
6
7
|
|
|
7
8
|
const sleepMs = (ms) => { try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch { /* no wait available */ } };
|
|
8
9
|
|
|
@@ -16,7 +17,7 @@ const sleepMs = (ms) => { try { Atomics.wait(new Int32Array(new SharedArrayBuffe
|
|
|
16
17
|
export function holderReport(dir, opts = {}) {
|
|
17
18
|
const exec = opts.exec || ((cmd, args) => { const r = spawnSync(cmd, args, { encoding: "utf8", timeout: 20000 }); return String(r.stdout || r.stderr || ""); });
|
|
18
19
|
const isWin = opts.win ?? (process.platform === "win32");
|
|
19
|
-
const needle =
|
|
20
|
+
const needle = dirForms(dir);
|
|
20
21
|
try {
|
|
21
22
|
if (isWin) {
|
|
22
23
|
const ps = "Get-CimInstance Win32_Process | ForEach-Object { $c = [string]$_.CommandLine; $p = [string]$_.ExecutablePath; \"$($_.ProcessId)`t$($_.ParentProcessId)`t$($_.Name)`t$c`t$p\" }";
|
|
@@ -32,7 +33,7 @@ function filterHolderRows(out, needle) {
|
|
|
32
33
|
const f = line.split("\t");
|
|
33
34
|
if (f.length < 3) continue;
|
|
34
35
|
const hay = f.slice(3).join(" ").replace(/\\/g, "/").toLowerCase();
|
|
35
|
-
const named = hay.includes(
|
|
36
|
+
const named = needle.some((x) => hay.includes(x)), shellish = HOLDER_NAMES.test(String(f[2]).trim());
|
|
36
37
|
if (named || shellish) rows.push((named ? "HOLDS-DIR " : "shell ") + "pid=" + f[0] + " ppid=" + f[1] + " " + f[2] + " " + f.slice(3).join(" ").slice(0, 300));
|
|
37
38
|
}
|
|
38
39
|
return rows.length ? rows.slice(0, 60).join("\n") : "no process names the harness dir and no shell, node or python process is running";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wkappbot-code",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.33",
|
|
4
4
|
"description": "wkappbot-code -- Wisdom Kernel: the pinned OpenCode bridge plus the reflex harness plugin, the wkappbot core, the web harness and the latest public skills in one install",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|