github-router 0.3.276 → 0.3.285

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.
Files changed (107) hide show
  1. package/dist/{attribution-settings-VOPvSn6v.js → attribution-settings-B8M2fvhz.js} +44 -45
  2. package/dist/attribution-settings-B8M2fvhz.js.map +1 -0
  3. package/dist/{auth-CYoRwhC9.js → auth-VUL2Zxvw.js} +3 -3
  4. package/dist/{auth-CYoRwhC9.js.map → auth-VUL2Zxvw.js.map} +1 -1
  5. package/dist/browser-ext/manifest.json +1 -1
  6. package/dist/{check-usage-DDUyg8ZB.js → check-usage-CcLdFPGr.js} +4 -4
  7. package/dist/{check-usage-DDUyg8ZB.js.map → check-usage-CcLdFPGr.js.map} +1 -1
  8. package/dist/{claude-OTHh7iKz.js → claude-e-JIQGhR.js} +55 -38
  9. package/dist/claude-e-JIQGhR.js.map +1 -0
  10. package/dist/{codex-tCxK30Su.js → codex-DSnVZ9oK.js} +5 -5
  11. package/dist/{codex-tCxK30Su.js.map → codex-DSnVZ9oK.js.map} +1 -1
  12. package/dist/{debug-CJgsxoVw.js → debug-CtzYWxpJ.js} +2 -2
  13. package/dist/{debug-CJgsxoVw.js.map → debug-CtzYWxpJ.js.map} +1 -1
  14. package/dist/engine-DMveEa9x.js +2 -0
  15. package/dist/{gate-discovery-nf7PGgnS.js → gate-discovery-BCFwLm0q.js} +5 -5
  16. package/dist/{gate-discovery-nf7PGgnS.js.map → gate-discovery-BCFwLm0q.js.map} +1 -1
  17. package/dist/{get-copilot-usage-D1OAH0EU.js → get-copilot-usage-B-EDAqQb.js} +2 -2
  18. package/dist/{get-copilot-usage-D1OAH0EU.js.map → get-copilot-usage-B-EDAqQb.js.map} +1 -1
  19. package/dist/hooks.mjs +37870 -0
  20. package/dist/hooks.sha256 +1 -0
  21. package/dist/{internal-artifact-open-BCz9lnq0.js → internal-artifact-open-Dj5Nlq0L.js} +2 -2
  22. package/dist/{internal-artifact-open-BCz9lnq0.js.map → internal-artifact-open-Dj5Nlq0L.js.map} +1 -1
  23. package/dist/{internal-first-mate-guard-7v2CtMA6.js → internal-first-mate-guard-B7p4NttK.js} +1 -1
  24. package/dist/{internal-first-mate-guard-La0tIjTU.js → internal-first-mate-guard-DOgFVki5.js} +5 -4
  25. package/dist/{internal-first-mate-guard-La0tIjTU.js.map → internal-first-mate-guard-DOgFVki5.js.map} +1 -1
  26. package/dist/{internal-plan-review-jRMpHu68.js → internal-plan-review-DRFHET_B.js} +11 -4
  27. package/dist/internal-plan-review-DRFHET_B.js.map +1 -0
  28. package/dist/{internal-prompt-submit-CqWRFCuv.js → internal-prompt-submit-f1LR2P2G.js} +4 -4
  29. package/dist/{internal-prompt-submit-CqWRFCuv.js.map → internal-prompt-submit-f1LR2P2G.js.map} +1 -1
  30. package/dist/{internal-session-bind-BiHWDxuC.js → internal-session-bind-DhZhxJ3T.js} +2 -2
  31. package/dist/{internal-session-bind-BiHWDxuC.js.map → internal-session-bind-DhZhxJ3T.js.map} +1 -1
  32. package/dist/{internal-stop-hook-Ds9LwmZA.js → internal-stop-hook-Cf-7w3RH.js} +68 -20
  33. package/dist/internal-stop-hook-Cf-7w3RH.js.map +1 -0
  34. package/dist/{internal-stop-review-BOFj7R3q.js → internal-stop-review-BBsLcbPG.js} +2 -2
  35. package/dist/{internal-stop-review-BOFj7R3q.js.map → internal-stop-review-BBsLcbPG.js.map} +1 -1
  36. package/dist/{internal-worker-guard-Lu8VHj5k.js → internal-worker-guard-CKgYFiFO.js} +2 -2
  37. package/dist/{internal-worker-guard-Lu8VHj5k.js.map → internal-worker-guard-CKgYFiFO.js.map} +1 -1
  38. package/dist/{internal-workspace-header-BRMz0Yql.js → internal-workspace-header-OYgHEnFt.js} +2 -2
  39. package/dist/{internal-workspace-header-BRMz0Yql.js.map → internal-workspace-header-OYgHEnFt.js.map} +1 -1
  40. package/dist/lib/tree-sitter-pool/lifecycle-C7W8D2lx.js +165 -0
  41. package/dist/lib/tree-sitter-pool/lifecycle-Dx0SnP7Z.js +157 -0
  42. package/dist/lib/tree-sitter-pool/worker.js +287 -14
  43. package/dist/{lifecycle-LA4rAuAL.js → lifecycle-B7CHqKlF.js} +2 -2
  44. package/dist/{lifecycle-LA4rAuAL.js.map → lifecycle-B7CHqKlF.js.map} +1 -1
  45. package/dist/lifecycle-CbmHMMSD.js +2 -0
  46. package/dist/{lifecycle-AGXnd-ZY.js → lifecycle-D-rL81tT.js} +2 -2
  47. package/dist/{lifecycle-AGXnd-ZY.js.map → lifecycle-D-rL81tT.js.map} +1 -1
  48. package/dist/lifecycle-DTcZwa7U.js +2 -0
  49. package/dist/lifecycle-K9oVtGde.mjs +16 -0
  50. package/dist/main.js +18 -18
  51. package/dist/{mcp-workspace-header-CGJbNeHb.js → mcp-workspace-header-ucs2SDST.js} +5 -6
  52. package/dist/mcp-workspace-header-ucs2SDST.js.map +1 -0
  53. package/dist/{models-4Q45Q2Dw.js → models-C16mBK2M.js} +3 -3
  54. package/dist/{models-4Q45Q2Dw.js.map → models-C16mBK2M.js.map} +1 -1
  55. package/dist/{orchestration-CpkG8ScN.js → orchestration-CtM6FYNx.js} +2 -2
  56. package/dist/{orchestration-CpkG8ScN.js.map → orchestration-CtM6FYNx.js.map} +1 -1
  57. package/dist/package-root-B-osctCk.js +29 -0
  58. package/dist/package-root-B-osctCk.js.map +1 -0
  59. package/dist/paths-CV9K7Xqm.js +2 -0
  60. package/dist/{paths-j2B7b0DZ.js → paths-wLC0InjX.js} +28 -4
  61. package/dist/paths-wLC0InjX.js.map +1 -0
  62. package/dist/{peer-mcp-personas-DRL_xT4h.js → peer-mcp-personas-V6stFvpq.js} +1158 -297
  63. package/dist/peer-mcp-personas-V6stFvpq.js.map +1 -0
  64. package/dist/{plan-review-hook-Dk5zTNke.js → plan-review-hook-Lf9ISdF6.js} +5 -6
  65. package/dist/plan-review-hook-Lf9ISdF6.js.map +1 -0
  66. package/dist/{prompt-submit-hook-lvTWwaTV.js → prompt-submit-hook-BlijaOn7.js} +6 -11
  67. package/dist/prompt-submit-hook-BlijaOn7.js.map +1 -0
  68. package/dist/{provision-HzZ547dW.js → provision-BpL6gZIt.js} +4 -4
  69. package/dist/{provision-HzZ547dW.js.map → provision-BpL6gZIt.js.map} +1 -1
  70. package/dist/self-invocation-CKMjcA5F.js +380 -0
  71. package/dist/self-invocation-CKMjcA5F.js.map +1 -0
  72. package/dist/{serve-CM3OmF4I.js → serve-CXUf7RtJ.js} +34 -27
  73. package/dist/serve-CXUf7RtJ.js.map +1 -0
  74. package/dist/{server-setup-C9r7jYnz.js → server-setup-Bppjt9xO.js} +81 -207
  75. package/dist/server-setup-Bppjt9xO.js.map +1 -0
  76. package/dist/{start-KAUCsDao.js → start-C1-jrHfU.js} +3 -3
  77. package/dist/{start-KAUCsDao.js.map → start-C1-jrHfU.js.map} +1 -1
  78. package/dist/{stop-gate-hook-CaUrldLh.js → stop-gate-hook-DgJ6sW8N.js} +100 -52
  79. package/dist/stop-gate-hook-DgJ6sW8N.js.map +1 -0
  80. package/dist/{stop-gate-policy-lAabj_pP.js → stop-gate-policy-DG5yYWGn.js} +2 -2
  81. package/dist/{stop-gate-policy-lAabj_pP.js.map → stop-gate-policy-DG5yYWGn.js.map} +1 -1
  82. package/dist/{token-CxPaBEUS.js → token-CnlB0884.js} +2 -2
  83. package/dist/token-CnlB0884.js.map +1 -0
  84. package/dist/{version-_Q1WpsQp.js → version-C8x2hQrZ.js} +14 -2
  85. package/dist/version-C8x2hQrZ.js.map +1 -0
  86. package/dist/{worker-dispatch-Bj1uYyG9.js → worker-dispatch-zW8Zi69V.js} +11 -7
  87. package/dist/worker-dispatch-zW8Zi69V.js.map +1 -0
  88. package/package.json +2 -2
  89. package/dist/attribution-settings-VOPvSn6v.js.map +0 -1
  90. package/dist/claude-OTHh7iKz.js.map +0 -1
  91. package/dist/engine-BKJKMAzh.js +0 -2
  92. package/dist/internal-plan-review-jRMpHu68.js.map +0 -1
  93. package/dist/internal-stop-hook-Ds9LwmZA.js.map +0 -1
  94. package/dist/lifecycle-DCrKbaIU.js +0 -2
  95. package/dist/lifecycle-KKSTKwd6.js +0 -2
  96. package/dist/mcp-workspace-header-CGJbNeHb.js.map +0 -1
  97. package/dist/paths-DN3Nio42.js +0 -2
  98. package/dist/paths-j2B7b0DZ.js.map +0 -1
  99. package/dist/peer-mcp-personas-DRL_xT4h.js.map +0 -1
  100. package/dist/plan-review-hook-Dk5zTNke.js.map +0 -1
  101. package/dist/prompt-submit-hook-lvTWwaTV.js.map +0 -1
  102. package/dist/serve-CM3OmF4I.js.map +0 -1
  103. package/dist/server-setup-C9r7jYnz.js.map +0 -1
  104. package/dist/stop-gate-hook-CaUrldLh.js.map +0 -1
  105. package/dist/token-CxPaBEUS.js.map +0 -1
  106. package/dist/version-_Q1WpsQp.js.map +0 -1
  107. package/dist/worker-dispatch-Bj1uYyG9.js.map +0 -1
@@ -2,17 +2,247 @@ import { createRequire } from "node:module";
2
2
  import { readFileSync, statSync } from "node:fs";
3
3
  import { parentPort } from "node:worker_threads";
4
4
  import { Language, Parser } from "web-tree-sitter";
5
- import * as path from "node:path";
5
+ import * as path$1 from "node:path";
6
+ import path from "node:path";
7
+ import "node:fs/promises";
6
8
  import consola from "consola";
9
+ import { randomBytes } from "node:crypto";
10
+ import os from "node:os";
7
11
  //#region \0rolldown/runtime.js
8
12
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
13
+ //#endregion
14
+ //#region src/lib/paths.ts
15
+ function appDir() {
16
+ return path.join(os.homedir(), ".local", "share", "github-router");
17
+ }
18
+ const PATHS = {
19
+ get APP_DIR() {
20
+ return appDir();
21
+ },
22
+ get GITHUB_TOKEN_PATH() {
23
+ return path.join(appDir(), "github_token");
24
+ },
25
+ /**
26
+ * Second, WRITE-capable GitHub token for the first-mate
27
+ * agent-orchestration surface (`--agents`), stored apart from
28
+ * `github_token` (the Copilot App token). Minted by
29
+ * `setupGitHubAgentToken()` via a device-flow login against the GitHub
30
+ * CLI OAuth client. Mode 0o600.
31
+ */
32
+ get GITHUB_AGENT_TOKEN_PATH() {
33
+ return path.join(appDir(), "github_agent_token");
34
+ },
35
+ /**
36
+ * Root of the durable first-mate fleet registry (`--agents`): the
37
+ * global `missions.json` mission index + per-repo `<owner>__<repo>.json`
38
+ * unit ledgers + the decisions/approvals ledger. Cross-launch and
39
+ * OUTSIDE the per-launch CLAUDE_CONFIG_DIR mirror (which is swept each
40
+ * launch) — the first mate must survive proxy restarts / context
41
+ * clears. Modeled on the worker-agent lifecycle ledger.
42
+ */
43
+ get FIRST_MATE_DIR() {
44
+ return process.env.GH_ROUTER_FIRST_MATE_DIR ?? path.join(appDir(), "first-mate");
45
+ },
46
+ get ERROR_LOG_PATH() {
47
+ return path.join(appDir(), "error.log");
48
+ },
49
+ /**
50
+ * Durable, router-owned directory for worker OVERFLOW artifacts. Two
51
+ * kinds land here, both kept OUT of the MCP result relay so they don't
52
+ * overflow Claude Code's 25k-token cap:
53
+ * - `<pid>-<8hex>.patch` — a `worker_implement`/`worker_test` worktree
54
+ * diff larger than `PREVIEW_CAP` (worker-agent/worktree.ts): the FULL
55
+ * `git diff --binary --full-index` patch, written BEFORE the worktree
56
+ * is removed so the (possibly binary) change stays `git apply`-able.
57
+ * - `<pid>-<8hex>.txt` — any worker result over the relay byte cap
58
+ * (worker-agent/relay-cap.ts `relaySafeText`): the full result text,
59
+ * with a bounded preview + this path returned in its place.
60
+ * `sweepAgedWorkerDiffs()` reaps entries older than 7 days on each write.
61
+ * Cross-launch cache (like TOOLBELT_BIN_DIR) — deliberately OUTSIDE the
62
+ * per-launch CLAUDE_CONFIG_DIR mirror and OUTSIDE the user's repo.
63
+ */
64
+ get WORKER_DIFFS_DIR() {
65
+ return path.join(appDir(), "worker-diffs");
66
+ },
67
+ /**
68
+ * Stable home for the relocated hook launcher (`hooks-<sha256>.mjs`).
69
+ *
70
+ * Claude Code hook commands are persisted into a settings.json that outlives
71
+ * the process that wrote them. Under `bunx` the package tree lives in
72
+ * `$TMPDIR`, which macOS reaps — so a command pointing there dies on its
73
+ * first bare import and every hook in the session fails. This directory is
74
+ * under APP_DIR precisely because APP_DIR is neither a cache nor a temp dir
75
+ * and is therefore never reaped. Entries are content-addressed and immutable,
76
+ * so two proxies on different versions coexist and neither can delete a
77
+ * launcher the other is still using.
78
+ */
79
+ get HOOK_LAUNCHER_DIR() {
80
+ return path.join(appDir(), "hooks");
81
+ },
82
+ /**
83
+ * Durable copy of the tree-sitter WASM runtime and the grammar subset used
84
+ * by code search. The package tree can live under a reaped bunx temp dir;
85
+ * APP_DIR is stable across launches, so structural ranking and outlines do
86
+ * not silently disappear after the original install files are removed.
87
+ */
88
+ get TREE_SITTER_ASSETS_DIR() {
89
+ return path.join(appDir(), "tree-sitter");
90
+ },
91
+ /**
92
+ * Isolated CODEX_HOME for the spawned Codex CLI. Masks any cached
93
+ * ChatGPT subscription login (openai/codex#2733 — cached login can
94
+ * override OPENAI_API_KEY) so the proxy's dummy key is authoritative.
95
+ */
96
+ get CODEX_HOME() {
97
+ return path.join(appDir(), "codex-isolated");
98
+ },
99
+ /**
100
+ * Router-owned install + data dir for CloudCLI (the `serve` browser control
101
+ * plane). Holds the pinned local `npm install` (node_modules/), an isolated
102
+ * `auth.db` (so we never collide with a user's standalone CloudCLI DB), and
103
+ * the persisted `.serve-secret.json` login. See docs/serve-control-plane.md.
104
+ */
105
+ get CLOUDCLI_HOME() {
106
+ return path.join(appDir(), "cloudcli");
107
+ },
108
+ /**
109
+ * Runtime tempfiles for the per-launch peer-MCP wiring (the
110
+ * `--mcp-config` JSON and `--agents` JSON written before spawning
111
+ * Claude Code). Mode 0o700 to match the security review's mandate;
112
+ * cleaned on shutdown via the per-launch `cleanup()`, plus a
113
+ * boot-time sweep of stale files (dead PIDs, >24h old).
114
+ */
115
+ get CLAUDE_RUNTIME_DIR() {
116
+ return path.join(appDir(), "runtime");
117
+ },
118
+ /**
119
+ * Router-owned CLAUDE_CONFIG_DIR. The spawned Claude Code (and any
120
+ * teammates it spawns via the agent-teams primitive) reads its
121
+ * config — including `.credentials.json` — from this dir. We
122
+ * snapshot-copy the user's `~/.claude/` here at startup (excluding
123
+ * `.credentials.json` and volatile state), then write our own
124
+ * synthetic Console OAuth credential. The teammate-spawn allowlist
125
+ * propagates `CLAUDE_CONFIG_DIR` to children, so teammates find the
126
+ * synthetic credential and authenticate instead of falling into the
127
+ * "Not logged in · Run /login" gate that would otherwise leave
128
+ * them mute. See `ensureClaudeConfigMirror` below.
129
+ *
130
+ * Per-launch dir: `<appDir>/claude-config/<pid>-<8 hex>`. Two
131
+ * concurrent `github-router claude` launches each get their own
132
+ * isolated mirror, so per-launch state (synthetic credential,
133
+ * snapshot copy of `~/.claude/`, future per-launch `.claude.json`
134
+ * mutation with the peer-MCP entry) cannot cross-talk. The
135
+ * per-launch suffix is cached on first access (see
136
+ * `claudeConfigDirSuffix()`) so all callers within a single proxy
137
+ * lifetime see the same value. Boot-time `sweepStaleClaudeConfigMirrors`
138
+ * reaps mirrors from crashed prior PIDs.
139
+ */
140
+ get CLAUDE_CONFIG_DIR() {
141
+ return path.join(appDir(), "claude-config", claudeConfigDirSuffix());
142
+ },
143
+ /**
144
+ * Durable router-owned `bin/` for the LLM toolbelt — curated CLI
145
+ * tools (rg/fd/jq/sd/sg/yq) materialized here and prepended to the
146
+ * spawned agent's PATH. Intentionally OUTSIDE the per-launch
147
+ * CLAUDE_CONFIG_DIR mirror (which is swept per launch): the toolbelt
148
+ * is a cross-launch cache, like `last-update-check`.
149
+ */
150
+ get TOOLBELT_BIN_DIR() {
151
+ return path.join(appDir(), "bin");
152
+ },
153
+ /**
154
+ * Root of the github-router-managed ColBERT semantic-search sidecar
155
+ * (the `--semantic-search` opt-in). Everything heavy that the
156
+ * `colgrep` child needs is provisioned, SHA-verified, and stored here
157
+ * by the router — never in the user's repo, never in colgrep's own
158
+ * global `%APPDATA%\colgrep` / `~/.config/colgrep`. Cross-launch cache
159
+ * (like TOOLBELT_BIN_DIR), deliberately OUTSIDE the per-launch
160
+ * CLAUDE_CONFIG_DIR mirror.
161
+ *
162
+ * Layout (see docs/research/colbert-sidecar-design.md §4):
163
+ * colbert/
164
+ * bin/colgrep[.exe] # provisioned binary
165
+ * models/LateOn-Code-edge/<rev>/ # 5 model files
166
+ * onnxruntime/1.23.0/cpu/<libname> # ORT dylib (ORT_DYLIB_PATH)
167
+ * indices/ # === COLGREP_DATA_DIR ===
168
+ * <project>-<xxh3(path|model)>/ # colgrep-owned per-(path,model)
169
+ * .gh-router-meta/<hash>.json # router-owned sidecar metadata
170
+ */
171
+ get COLBERT_DIR() {
172
+ return path.join(appDir(), "colbert");
173
+ },
174
+ get COLBERT_BIN_DIR() {
175
+ return path.join(appDir(), "colbert", "bin");
176
+ },
177
+ get COLBERT_MODELS_DIR() {
178
+ return path.join(appDir(), "colbert", "models");
179
+ },
180
+ get COLBERT_ORT_DIR() {
181
+ return path.join(appDir(), "colbert", "onnxruntime");
182
+ },
183
+ /**
184
+ * Value passed to colgrep as `COLGREP_DATA_DIR`. colgrep keys the
185
+ * physical index dir by `xxh3(canonical_project_path | model)` under
186
+ * here, so every distinct workspace gets its own index dir for free.
187
+ * The router-owned freshness sidecar lives at
188
+ * `<COLBERT_INDICES_DIR>/.gh-router-meta/<hash>.json`.
189
+ */
190
+ get COLBERT_INDICES_DIR() {
191
+ return path.join(appDir(), "colbert", "indices");
192
+ },
193
+ get COLBERT_META_DIR() {
194
+ return path.join(appDir(), "colbert", "indices", ".gh-router-meta");
195
+ }
196
+ };
9
197
  /**
10
- * Grammar key wasm filename under `node_modules/tree-sitter-wasms/out/`.
11
- * Resolved at runtime from `node_modules`; the file paths are stable
12
- * because `tree-sitter-wasms` ships prebuilt binaries (no per-install
13
- * codegen).
198
+ * Per-launch suffix for `PATHS.CLAUDE_CONFIG_DIR`. Lazily generated on
199
+ * first access and cached for the lifetime of the process so every
200
+ * caller (env-var injection in `getClaudeCodeEnvVars`,
201
+ * `ensureClaudeConfigMirror` provisioning, peer-agent `.md` writes
202
+ * under `<dir>/agents/`, the shutdown cleanup) resolves the same path.
203
+ *
204
+ * Shape: `<pid>-<8 hex>`. The PID prefix is what
205
+ * `sweepStaleClaudeConfigMirrors` keys off to drop orphans from
206
+ * crashed prior sessions; the 8-hex random suffix prevents collision
207
+ * if a future caller (tests, internal relaunch) ever clears the cache
208
+ * within a single PID lifetime.
209
+ *
210
+ * NOT exported — every consumer should go through `PATHS.CLAUDE_CONFIG_DIR`
211
+ * so the homedir-mock pattern used in the test suite keeps working.
14
212
  */
15
- const GRAMMAR_FILES = {
213
+ let _claudeConfigDirSuffix;
214
+ function claudeConfigDirSuffix() {
215
+ if (_claudeConfigDirSuffix === void 0) _claudeConfigDirSuffix = `${process.pid}-${randomBytes(4).toString("hex")}`;
216
+ return _claudeConfigDirSuffix;
217
+ }
218
+ const CLAUDE_HOME_POLICY = /* @__PURE__ */ new Map([
219
+ [".credentials.json", "ISOLATED"],
220
+ [".credentials.json.lock", "ISOLATED"],
221
+ [".oauth_refresh.lock", "ISOLATED"],
222
+ [".github-router-managed", "ISOLATED"],
223
+ ["statsig", "ISOLATED"],
224
+ ["cache", "ISOLATED"],
225
+ ["logs", "ISOLATED"],
226
+ ["paste-cache", "ISOLATED"],
227
+ ["jobs", "ISOLATED"],
228
+ ["daemon", "ISOLATED"],
229
+ ["daemon.log", "ISOLATED"],
230
+ ["projects", "SHARED"],
231
+ ["sessions", "SHARED"],
232
+ ["tasks", "SHARED"],
233
+ ["todos", "SHARED"],
234
+ ["transcripts", "SHARED"],
235
+ ["shell-snapshots", "SHARED"],
236
+ ["shell_snapshots", "SHARED"],
237
+ ["plans", "SHARED"],
238
+ ["file-history", "SHARED"],
239
+ ["backups", "SHARED"]
240
+ ]);
241
+ Array.from(CLAUDE_HOME_POLICY.entries()).filter(([, kind]) => kind === "SHARED").map(([name]) => name);
242
+ //#endregion
243
+ //#region src/lib/tree-sitter-assets/files.ts
244
+ /** WASM assets code search loads from tree-sitter-wasms. */
245
+ const TREE_SITTER_GRAMMAR_FILES = {
16
246
  typescript: "tree-sitter-typescript.wasm",
17
247
  tsx: "tree-sitter-tsx.wasm",
18
248
  javascript: "tree-sitter-javascript.wasm",
@@ -23,6 +253,36 @@ const GRAMMAR_FILES = {
23
253
  c: "tree-sitter-c.wasm",
24
254
  cpp: "tree-sitter-cpp.wasm"
25
255
  };
256
+ const TREE_SITTER_RUNTIME_FILE = "tree-sitter.wasm";
257
+ //#endregion
258
+ //#region src/lib/tree-sitter-assets/provision.ts
259
+ /**
260
+ * Whether the stable directory holds the COMPLETE asset set: the runtime plus
261
+ * every grammar.
262
+ *
263
+ * Callers must gate on the whole set, never on the one file they are about to
264
+ * read. web-tree-sitter enforces a language-ABI version between the runtime and
265
+ * the grammars, so adopting a stable runtime while grammars still come from the
266
+ * package tree (or the reverse) can pair mismatched builds. That surfaces as a
267
+ * caught `Language.load` failure, which silently disables structural ranking —
268
+ * the exact silent degradation this whole change is meant to end.
269
+ */
270
+ function stableTreeSitterAssetsComplete() {
271
+ const dir = PATHS.TREE_SITTER_ASSETS_DIR;
272
+ return [TREE_SITTER_RUNTIME_FILE, ...Object.values(TREE_SITTER_GRAMMAR_FILES)].every((name) => {
273
+ try {
274
+ return statSync(path.join(dir, name)).isFile();
275
+ } catch {
276
+ return false;
277
+ }
278
+ });
279
+ }
280
+ /**
281
+ * Grammar key → wasm filename. The stable APP_DIR copy is preferred; the
282
+ * original `node_modules/tree-sitter-wasms/out/` directory remains the
283
+ * first-run fallback while background provisioning completes.
284
+ */
285
+ const GRAMMAR_FILES = TREE_SITTER_GRAMMAR_FILES;
26
286
  /**
27
287
  * Per-language definition-shape node types. When a matched identifier
28
288
  * sits inside one of these nodes AND is at the node's "name" position,
@@ -147,20 +407,33 @@ const IDENTIFIER_NODE_TYPES = /* @__PURE__ */ new Set([
147
407
  ]);
148
408
  let _grammarBundle;
149
409
  /**
150
- * Resolve the `tree-sitter-wasms/out/` directory at the package root.
151
- * `require.resolve` is used through a try/catch — the bundled-only
152
- * fallback runs in environments where node_modules has been pruned to
153
- * just runtime deps.
410
+ * Resolve the grammar directory. The stable APP_DIR copy wins only when the
411
+ * COMPLETE set is present; otherwise `require.resolve` supplies the
412
+ * package-tree fallback for first launch or a best-effort provisioning failure.
413
+ *
414
+ * All-or-nothing on purpose — see `stableTreeSitterAssetsComplete()`: mixing a
415
+ * stable runtime with package-tree grammars can pair mismatched ABI builds and
416
+ * silently disable structural ranking.
154
417
  */
155
418
  function resolveGrammarRoot() {
419
+ if (stableTreeSitterAssetsComplete()) return PATHS.TREE_SITTER_ASSETS_DIR;
156
420
  try {
157
421
  const pkgPath = __require.resolve("tree-sitter-wasms/package.json");
158
- return path.join(path.dirname(pkgPath), "out");
422
+ return path$1.join(path$1.dirname(pkgPath), "out");
159
423
  } catch {
160
424
  return null;
161
425
  }
162
426
  }
163
427
  /**
428
+ * web-tree-sitter normally resolves this sidecar relative to its JS module.
429
+ * Prefer the durable copy, but only under the same all-present gate the
430
+ * grammars use, so the runtime and the grammars always come from one install.
431
+ */
432
+ function parserInitOptions() {
433
+ if (!stableTreeSitterAssetsComplete()) return void 0;
434
+ return { locateFile: () => path$1.join(PATHS.TREE_SITTER_ASSETS_DIR, TREE_SITTER_RUNTIME_FILE) };
435
+ }
436
+ /**
164
437
  * Pre-load all grammars at module-init time so the first search
165
438
  * doesn't pay a ~500ms cold-start cost. The Promise is captured at
166
439
  * import time and awaited per-call; per-grammar failures are caught
@@ -171,7 +444,7 @@ function getGrammarBundle() {
171
444
  _grammarBundle = { ready: (async () => {
172
445
  const out = /* @__PURE__ */ new Map();
173
446
  try {
174
- await Parser.init();
447
+ await Parser.init(parserInitOptions());
175
448
  } catch (err) {
176
449
  consola.warn(`[code_search] tree-sitter Parser.init failed; structural ranking disabled: ${err.message}`);
177
450
  return out;
@@ -182,7 +455,7 @@ function getGrammarBundle() {
182
455
  return out;
183
456
  }
184
457
  for (const [key, filename] of Object.entries(GRAMMAR_FILES)) {
185
- const wasmPath = path.join(root, filename);
458
+ const wasmPath = path$1.join(root, filename);
186
459
  try {
187
460
  const lang = await Language.load(wasmPath);
188
461
  out.set(key, lang);
@@ -552,4 +825,4 @@ port.on("message", (msg) => {
552
825
  port.postMessage(ready);
553
826
  })();
554
827
  //#endregion
555
- export {};
828
+ export { PATHS as t };
@@ -1,4 +1,4 @@
1
- import { d as writeRuntimeFileSecure, t as PATHS } from "./paths-j2B7b0DZ.js";
1
+ import { d as writeRuntimeFileSecure, t as PATHS } from "./paths-wLC0InjX.js";
2
2
  import path from "node:path";
3
3
  import { randomBytes, randomUUID } from "node:crypto";
4
4
  import fs from "node:fs/promises";
@@ -327,4 +327,4 @@ async function sweepStaleWorktreesAtBoot() {
327
327
  //#endregion
328
328
  export { sweepRegistry as a, registerExitHandlers as i, getInstanceUuid as n, sweepStaleWorktreesAtBoot as o, recordWorkerRepo as r, WorktreeRegistry as t };
329
329
 
330
- //# sourceMappingURL=lifecycle-LA4rAuAL.js.map
330
+ //# sourceMappingURL=lifecycle-B7CHqKlF.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle-LA4rAuAL.js","names":[],"sources":["../src/lib/worker-agent/lifecycle.ts"],"sourcesContent":["/**\n * Lifecycle plumbing for worker worktrees: in-memory registry, signal\n * handlers, ledger of repos touched, and the boot-time PID+instance\n * safety net.\n *\n * Plan: see `plans/we-have-added-a-dreamy-tide.md` (\"Worktree mode\" →\n * \"Cleanup paths\"). Three layers cooperate, none of them sufficient\n * alone:\n *\n * 1. Per-call cleanup (`engine.ts` finally block invoking\n * `WorktreeHandle.remove()`) — covers the happy path.\n *\n * 2. Session-end signal sweep (this file, registered via\n * `registerExitHandlers`) — covers Ctrl+C, service-manager stop,\n * and (in `github-router claude` mode) the spawned child's exit.\n * Synchronous `execFileSync` is intentional: exit handlers can't\n * reliably await async work.\n *\n * 3. Boot-time PID+instance sweep (`sweepStaleWorktreesAtBoot`) —\n * covers SIGKILL, OOM, container restart. Walks the ledger of\n * repos this proxy has touched and removes worktree dirs whose\n * `<pid>` is dead OR whose `<instance>` UUID doesn't match the\n * current proxy's UUID.\n *\n * Ledger writes are ATOMIC (temp + rename) per peer review — a\n * concurrent-RMW corruption would silently strand worktrees because\n * the boot sweep can't find their repo roots.\n */\n\nimport { execFileSync } from \"node:child_process\"\nimport { randomBytes, randomUUID } from \"node:crypto\"\nimport fs from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport { PATHS, writeRuntimeFileSecure } from \"../paths\"\n\n/**\n * Same regex worktree.ts uses for its per-call age sweep — kept in\n * sync intentionally. `<pid>-<uuid>-<8hex>` strictly.\n */\nconst WORKTREE_DIR_NAME_RE =\n /^(\\d+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-([0-9a-f]{8})$/\n\n/**\n * Cap on the ledger: how many repos we remember across boots, and how\n * old an entry may be before it's pruned. Both are belt-and-suspenders\n * — the per-call age sweep is the primary guard against accumulation\n * inside any single repo.\n */\nconst LEDGER_MAX_ENTRIES = 100\nconst LEDGER_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000\n\nexport interface WorktreeRegistryEntry {\n repoRoot: string\n dir: string\n branch: string\n}\n\n/**\n * Set-like in-memory registry of worktrees this proxy created. Engine\n * passes it to `createWorktree` so per-call cleanup deletes the entry\n * on success; the signal handlers walk what's left at shutdown.\n *\n * Not a bare `Set` because we want to expose only the operations we\n * actually use, and we want a stable testable surface.\n */\nexport class WorktreeRegistry {\n private readonly entries = new Set<WorktreeRegistryEntry>()\n\n add(entry: WorktreeRegistryEntry): void {\n this.entries.add(entry)\n }\n delete(entry: WorktreeRegistryEntry): void {\n this.entries.delete(entry)\n }\n has(entry: WorktreeRegistryEntry): boolean {\n return this.entries.has(entry)\n }\n values(): IterableIterator<WorktreeRegistryEntry> {\n return this.entries.values()\n }\n get size(): number {\n return this.entries.size\n }\n clear(): void {\n this.entries.clear()\n }\n}\n\n// ---------------------------------------------------------------------\n// Per-launch instance UUID\n// ---------------------------------------------------------------------\n\nlet _instanceUuid: string | null = null\n\n/**\n * Stable UUID4 generated once per proxy process. Used in worktree\n * dir/branch names so the boot sweep can reliably distinguish \"this\n * proxy's still-live worktrees\" from \"stranded dirs from a prior\n * proxy that happens to have a recycled PID\" — Docker PID-1 across\n * container restarts is the classic case (peer-review HIGH finding).\n */\nexport function getInstanceUuid(): string {\n if (_instanceUuid === null) {\n _instanceUuid = randomUUID()\n }\n return _instanceUuid\n}\n\n/** Test-only: reset the cached UUID. */\nexport function __resetInstanceUuidForTests(): void {\n _instanceUuid = null\n}\n\n// ---------------------------------------------------------------------\n// Signal handlers + sweepRegistry\n// ---------------------------------------------------------------------\n\nlet _registered = false\nlet _activeRegistry: WorktreeRegistry | null = null\nlet _exitHandler: (() => void) | null = null\nlet _sigintHandler: (() => void) | null = null\nlet _sigtermHandler: (() => void) | null = null\n\n/**\n * Synchronous cleanup of every registry entry. Best-effort:\n * `execFileSync` failures are swallowed (the dir may have been\n * removed already, or git may not be on PATH any more in some\n * environments). After a successful removal we drop the entry from\n * the registry so a second call is a true no-op.\n *\n * Synchronous on purpose — exit handlers can't reliably await async\n * work; the process would die before the promise settled.\n */\nexport function sweepRegistry(): void {\n if (!_activeRegistry) return\n // Snapshot the values first so we can mutate the underlying set\n // during iteration without skipping entries.\n const snapshot = [..._activeRegistry.values()]\n for (const entry of snapshot) {\n try {\n // `-C entry.repoRoot` is load-bearing: without it git resolves\n // the worktree path relative to the proxy's cwd (which is the\n // user's launch dir, typically NOT inside the target repo), and\n // fails with `fatal: '<path>' is not a working tree`. The E2E\n // boot-sweep test (worker-agent-boot-sweep.test.ts) is what\n // caught the missing flag.\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"worktree\", \"remove\", \"--force\", entry.dir],\n { stdio: \"ignore\", timeout: 10_000, windowsHide: true },\n )\n } catch {\n // Already gone, EBUSY, or git not on PATH — best effort.\n }\n try {\n execFileSync(\"git\", [\"-C\", entry.repoRoot, \"branch\", \"-D\", entry.branch], {\n stdio: \"ignore\",\n timeout: 5_000,\n windowsHide: true,\n })\n } catch {\n // Same as above.\n }\n _activeRegistry.delete(entry)\n }\n}\n\n/**\n * Windows ConPTY / node-pty signal behavior:\n *\n * When a ConPTY host (VS Code terminal, Windows Terminal, node-pty) closes\n * the pseudo-console, the ConPTY layer sends CTRL_CLOSE_EVENT to the\n * process group. Node.js translates this into SIGINT (NOT SIGTERM). The\n * process has a ~5-second window before forced termination.\n *\n * Implication: the SIGTERM handler below may NEVER fire in node-pty\n * environments. This is by design — the three-layer cleanup architecture\n * ensures coverage:\n * 1. Per-call cleanup (engine.ts finally block) — happy path\n * 2. SIGINT handler (this file) — ConPTY close, Ctrl+C\n * 3. `exit` handler (this file) — unconditional, fires on any exit\n * 4. Boot-time PID+instance sweep (sweepStaleWorktreesAtBoot) — crash recovery\n *\n * Layers 1+2+3 cover ConPTY; layer 4 covers SIGKILL/OOM/container restart.\n */\n\n/**\n * Wire up SIGINT/SIGTERM/exit handlers that walk the registry and\n * remove every entry. Idempotent: subsequent calls swap the registry\n * pointer but do NOT register additional process listeners (otherwise\n * we'd leak listeners on every `runWorkerAgent`).\n *\n * Signal handlers re-raise the signal after sweeping. Naively running\n * the sweep on SIGINT/SIGTERM and returning would *suppress* the\n * signal: Node defaults to terminating the process on these, but only\n * if no user listener is attached. Once we attach a listener, the\n * default action is cancelled and the process keeps running — which\n * means Ctrl-C would clean worktrees but not actually exit, leaving\n * orphan processes in dev. The `process.kill(pid, sig)` re-raise\n * after removing our own listener restores the default behaviour\n * (the second delivery now hits an empty listener list, so Node\n * terminates with the conventional `128 + signum` exit code).\n */\nexport function registerExitHandlers(registry: WorktreeRegistry): void {\n _activeRegistry = registry\n if (_registered) return\n _registered = true\n _exitHandler = () => sweepRegistry()\n _sigintHandler = () => {\n sweepRegistry()\n if (_sigintHandler) process.off(\"SIGINT\", _sigintHandler)\n process.kill(process.pid, \"SIGINT\")\n }\n _sigtermHandler = () => {\n sweepRegistry()\n if (_sigtermHandler) process.off(\"SIGTERM\", _sigtermHandler)\n process.kill(process.pid, \"SIGTERM\")\n }\n process.on(\"SIGINT\", _sigintHandler)\n process.on(\"SIGTERM\", _sigtermHandler)\n // `exit` handlers can only run synchronous code — exactly what\n // sweepRegistry does. Async work here would never complete.\n process.on(\"exit\", _exitHandler)\n}\n\n/**\n * Test-only: unregister the handlers and reset module state. Tests\n * that want to verify `registerExitHandlers` semantics must clean up\n * after themselves or future tests in the same process inherit the\n * (now stale) registry pointer.\n */\nexport function __unregisterExitHandlersForTests(): void {\n if (_sigintHandler) {\n process.off(\"SIGINT\", _sigintHandler)\n _sigintHandler = null\n }\n if (_sigtermHandler) {\n process.off(\"SIGTERM\", _sigtermHandler)\n _sigtermHandler = null\n }\n if (_exitHandler) {\n process.off(\"exit\", _exitHandler)\n _exitHandler = null\n }\n _registered = false\n _activeRegistry = null\n}\n\n// ---------------------------------------------------------------------\n// Ledger: which repos has this proxy touched?\n// ---------------------------------------------------------------------\n\ninterface LedgerEntry {\n repoRoot: string\n lastSeenMs: number\n}\n\ninterface LedgerFile {\n entries: Array<LedgerEntry>\n}\n\nfunction ledgerPath(): string {\n return path.join(PATHS.APP_DIR, \"worker-repos.json\")\n}\n\nasync function readLedger(): Promise<LedgerFile> {\n let raw: string\n try {\n raw = await fs.readFile(ledgerPath(), \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { entries: [] }\n }\n return { entries: [] }\n }\n try {\n const parsed = JSON.parse(raw) as Partial<LedgerFile>\n if (!parsed || !Array.isArray(parsed.entries)) return { entries: [] }\n const cleaned: Array<LedgerEntry> = []\n for (const e of parsed.entries) {\n if (\n e &&\n typeof e === \"object\" &&\n typeof (e as LedgerEntry).repoRoot === \"string\" &&\n typeof (e as LedgerEntry).lastSeenMs === \"number\"\n ) {\n cleaned.push({\n repoRoot: (e as LedgerEntry).repoRoot,\n lastSeenMs: (e as LedgerEntry).lastSeenMs,\n })\n }\n }\n return { entries: cleaned }\n } catch {\n // Corrupted JSON — start fresh rather than crashing the proxy.\n return { entries: [] }\n }\n}\n\n/**\n * Per-process serializer for ledger writes. Multiple concurrent\n * `recordWorkerRepo` calls (legitimate: several workers may start at\n * once) would otherwise race read-modify-write on the JSON file. Each\n * call chains onto the previous so the on-disk sequence is\n * deterministic from this process's perspective.\n *\n * Cross-process safety is provided by the atomic temp+rename below,\n * which makes the final state of the file always be a well-formed\n * full snapshot from ONE writer — never a partial write or\n * interleaved JSON.\n */\nlet _ledgerChain: Promise<void> = Promise.resolve()\n\n/**\n * Append `repoRoot` to the ledger (or update its `lastSeenMs`).\n * Atomic temp+rename per peer review.\n */\nexport function recordWorkerRepo(repoRoot: string): Promise<void> {\n const next = _ledgerChain.then(async () => {\n await fs.mkdir(PATHS.APP_DIR, { recursive: true })\n const current = await readLedger()\n // Dedup: drop any existing entry for this root before appending\n // the fresh one so the array doesn't grow unbounded with repeats.\n const filtered = current.entries.filter((e) => e.repoRoot !== repoRoot)\n filtered.push({ repoRoot, lastSeenMs: Date.now() })\n // Prune by age and cap entry count (newest wins).\n const now = Date.now()\n const pruned = filtered\n .filter((e) => now - e.lastSeenMs < LEDGER_MAX_AGE_MS)\n .slice(-LEDGER_MAX_ENTRIES)\n const ledger: LedgerFile = { entries: pruned }\n\n // Atomic temp+rename. The temp filename is unique per call\n // (PID + 8 random hex chars) so concurrent processes don't\n // collide on the temp name; the final `rename` is atomic on\n // POSIX and on Windows (both with same filesystem).\n const tmp = `${ledgerPath()}.tmp.${process.pid}.${randomBytes(4).toString(\n \"hex\",\n )}`\n try {\n await writeRuntimeFileSecure(tmp, JSON.stringify(ledger, null, 2))\n await fs.rename(tmp, ledgerPath())\n } catch (err) {\n // Clean up the temp file if rename failed midway.\n await fs.unlink(tmp).catch(() => {})\n throw err\n }\n })\n // Swallow chain-internal errors so one failed write doesn't poison\n // the chain for every subsequent caller. Each call still sees its\n // own rejection (we return `next`, not the catch-handler chain).\n _ledgerChain = next.catch(() => undefined)\n return next\n}\n\nfunction isPidAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false\n try {\n process.kill(pid, 0)\n return true\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n // EPERM = process exists but we can't signal it — still alive\n // for our purposes (we just need to know whether to clean up).\n if (code === \"EPERM\") return true\n return false\n }\n}\n\n/**\n * Boot-time sweep. For every repo we recorded in the ledger,\n * enumerate `<repoRoot>/.git/worker-worktrees/` (the conventional\n * location — for repos already inside a worktree, the actual\n * `git-common-dir` may differ, in which case we'll miss this batch\n * and the per-call age sweep will catch them within 7 days) and\n * remove dirs that aren't owned by THIS proxy.\n *\n * Ownership rule: dir is \"ours\" iff its embedded PID is alive AND\n * its embedded UUID equals `getInstanceUuid()`. Either condition\n * failing → remove.\n */\nexport async function sweepStaleWorktreesAtBoot(): Promise<void> {\n const ledger = await readLedger()\n if (ledger.entries.length === 0) return\n const currentUuid = getInstanceUuid()\n for (const entry of ledger.entries) {\n const parent = path.join(entry.repoRoot, \".git\", \"worker-worktrees\")\n let names: Array<string>\n try {\n names = await fs.readdir(parent)\n } catch {\n continue\n }\n for (const name of names) {\n const m = WORKTREE_DIR_NAME_RE.exec(name)\n if (!m) continue\n const pid = Number.parseInt(m[1], 10)\n const uuid = m[2]\n const isOurs = isPidAlive(pid) && uuid === currentUuid\n if (isOurs) continue\n\n const fullDir = path.join(parent, name)\n const branch = `worker/${pid}-${uuid}-${m[3]}`\n try {\n // `-C entry.repoRoot` is load-bearing here too — see the\n // matching comment in `sweepRegistry`. The boot sweep runs\n // BEFORE any worker tool has set cwd, so the proxy's cwd is\n // the user's launch dir, which is almost never inside the\n // target repo.\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"worktree\", \"remove\", \"--force\", fullDir],\n { stdio: \"ignore\", timeout: 10_000, windowsHide: true },\n )\n } catch {\n // ignore\n }\n try {\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"branch\", \"-D\", branch],\n { stdio: \"ignore\", timeout: 5_000, windowsHide: true },\n )\n } catch {\n // ignore\n }\n try {\n await fs.rm(fullDir, { recursive: true, force: true })\n } catch {\n // ignore — git may have removed it already\n }\n }\n }\n}\n\n/** Test-only: clear the ledger file (does NOT remove on-disk worktrees). */\nexport async function __clearLedgerForTests(): Promise<void> {\n await fs.unlink(ledgerPath()).catch(() => {})\n}\n\n/** Test-only: read the ledger as a plain array (no side effects). */\nexport async function __readLedgerForTests(): Promise<Array<LedgerEntry>> {\n return (await readLedger()).entries\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"lifecycle-B7CHqKlF.js","names":[],"sources":["../src/lib/worker-agent/lifecycle.ts"],"sourcesContent":["/**\n * Lifecycle plumbing for worker worktrees: in-memory registry, signal\n * handlers, ledger of repos touched, and the boot-time PID+instance\n * safety net.\n *\n * Plan: see `plans/we-have-added-a-dreamy-tide.md` (\"Worktree mode\" →\n * \"Cleanup paths\"). Three layers cooperate, none of them sufficient\n * alone:\n *\n * 1. Per-call cleanup (`engine.ts` finally block invoking\n * `WorktreeHandle.remove()`) — covers the happy path.\n *\n * 2. Session-end signal sweep (this file, registered via\n * `registerExitHandlers`) — covers Ctrl+C, service-manager stop,\n * and (in `github-router claude` mode) the spawned child's exit.\n * Synchronous `execFileSync` is intentional: exit handlers can't\n * reliably await async work.\n *\n * 3. Boot-time PID+instance sweep (`sweepStaleWorktreesAtBoot`) —\n * covers SIGKILL, OOM, container restart. Walks the ledger of\n * repos this proxy has touched and removes worktree dirs whose\n * `<pid>` is dead OR whose `<instance>` UUID doesn't match the\n * current proxy's UUID.\n *\n * Ledger writes are ATOMIC (temp + rename) per peer review — a\n * concurrent-RMW corruption would silently strand worktrees because\n * the boot sweep can't find their repo roots.\n */\n\nimport { execFileSync } from \"node:child_process\"\nimport { randomBytes, randomUUID } from \"node:crypto\"\nimport fs from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport { PATHS, writeRuntimeFileSecure } from \"../paths\"\n\n/**\n * Same regex worktree.ts uses for its per-call age sweep — kept in\n * sync intentionally. `<pid>-<uuid>-<8hex>` strictly.\n */\nconst WORKTREE_DIR_NAME_RE =\n /^(\\d+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-([0-9a-f]{8})$/\n\n/**\n * Cap on the ledger: how many repos we remember across boots, and how\n * old an entry may be before it's pruned. Both are belt-and-suspenders\n * — the per-call age sweep is the primary guard against accumulation\n * inside any single repo.\n */\nconst LEDGER_MAX_ENTRIES = 100\nconst LEDGER_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000\n\nexport interface WorktreeRegistryEntry {\n repoRoot: string\n dir: string\n branch: string\n}\n\n/**\n * Set-like in-memory registry of worktrees this proxy created. Engine\n * passes it to `createWorktree` so per-call cleanup deletes the entry\n * on success; the signal handlers walk what's left at shutdown.\n *\n * Not a bare `Set` because we want to expose only the operations we\n * actually use, and we want a stable testable surface.\n */\nexport class WorktreeRegistry {\n private readonly entries = new Set<WorktreeRegistryEntry>()\n\n add(entry: WorktreeRegistryEntry): void {\n this.entries.add(entry)\n }\n delete(entry: WorktreeRegistryEntry): void {\n this.entries.delete(entry)\n }\n has(entry: WorktreeRegistryEntry): boolean {\n return this.entries.has(entry)\n }\n values(): IterableIterator<WorktreeRegistryEntry> {\n return this.entries.values()\n }\n get size(): number {\n return this.entries.size\n }\n clear(): void {\n this.entries.clear()\n }\n}\n\n// ---------------------------------------------------------------------\n// Per-launch instance UUID\n// ---------------------------------------------------------------------\n\nlet _instanceUuid: string | null = null\n\n/**\n * Stable UUID4 generated once per proxy process. Used in worktree\n * dir/branch names so the boot sweep can reliably distinguish \"this\n * proxy's still-live worktrees\" from \"stranded dirs from a prior\n * proxy that happens to have a recycled PID\" — Docker PID-1 across\n * container restarts is the classic case (peer-review HIGH finding).\n */\nexport function getInstanceUuid(): string {\n if (_instanceUuid === null) {\n _instanceUuid = randomUUID()\n }\n return _instanceUuid\n}\n\n/** Test-only: reset the cached UUID. */\nexport function __resetInstanceUuidForTests(): void {\n _instanceUuid = null\n}\n\n// ---------------------------------------------------------------------\n// Signal handlers + sweepRegistry\n// ---------------------------------------------------------------------\n\nlet _registered = false\nlet _activeRegistry: WorktreeRegistry | null = null\nlet _exitHandler: (() => void) | null = null\nlet _sigintHandler: (() => void) | null = null\nlet _sigtermHandler: (() => void) | null = null\n\n/**\n * Synchronous cleanup of every registry entry. Best-effort:\n * `execFileSync` failures are swallowed (the dir may have been\n * removed already, or git may not be on PATH any more in some\n * environments). After a successful removal we drop the entry from\n * the registry so a second call is a true no-op.\n *\n * Synchronous on purpose — exit handlers can't reliably await async\n * work; the process would die before the promise settled.\n */\nexport function sweepRegistry(): void {\n if (!_activeRegistry) return\n // Snapshot the values first so we can mutate the underlying set\n // during iteration without skipping entries.\n const snapshot = [..._activeRegistry.values()]\n for (const entry of snapshot) {\n try {\n // `-C entry.repoRoot` is load-bearing: without it git resolves\n // the worktree path relative to the proxy's cwd (which is the\n // user's launch dir, typically NOT inside the target repo), and\n // fails with `fatal: '<path>' is not a working tree`. The E2E\n // boot-sweep test (worker-agent-boot-sweep.test.ts) is what\n // caught the missing flag.\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"worktree\", \"remove\", \"--force\", entry.dir],\n { stdio: \"ignore\", timeout: 10_000, windowsHide: true },\n )\n } catch {\n // Already gone, EBUSY, or git not on PATH — best effort.\n }\n try {\n execFileSync(\"git\", [\"-C\", entry.repoRoot, \"branch\", \"-D\", entry.branch], {\n stdio: \"ignore\",\n timeout: 5_000,\n windowsHide: true,\n })\n } catch {\n // Same as above.\n }\n _activeRegistry.delete(entry)\n }\n}\n\n/**\n * Windows ConPTY / node-pty signal behavior:\n *\n * When a ConPTY host (VS Code terminal, Windows Terminal, node-pty) closes\n * the pseudo-console, the ConPTY layer sends CTRL_CLOSE_EVENT to the\n * process group. Node.js translates this into SIGINT (NOT SIGTERM). The\n * process has a ~5-second window before forced termination.\n *\n * Implication: the SIGTERM handler below may NEVER fire in node-pty\n * environments. This is by design — the three-layer cleanup architecture\n * ensures coverage:\n * 1. Per-call cleanup (engine.ts finally block) — happy path\n * 2. SIGINT handler (this file) — ConPTY close, Ctrl+C\n * 3. `exit` handler (this file) — unconditional, fires on any exit\n * 4. Boot-time PID+instance sweep (sweepStaleWorktreesAtBoot) — crash recovery\n *\n * Layers 1+2+3 cover ConPTY; layer 4 covers SIGKILL/OOM/container restart.\n */\n\n/**\n * Wire up SIGINT/SIGTERM/exit handlers that walk the registry and\n * remove every entry. Idempotent: subsequent calls swap the registry\n * pointer but do NOT register additional process listeners (otherwise\n * we'd leak listeners on every `runWorkerAgent`).\n *\n * Signal handlers re-raise the signal after sweeping. Naively running\n * the sweep on SIGINT/SIGTERM and returning would *suppress* the\n * signal: Node defaults to terminating the process on these, but only\n * if no user listener is attached. Once we attach a listener, the\n * default action is cancelled and the process keeps running — which\n * means Ctrl-C would clean worktrees but not actually exit, leaving\n * orphan processes in dev. The `process.kill(pid, sig)` re-raise\n * after removing our own listener restores the default behaviour\n * (the second delivery now hits an empty listener list, so Node\n * terminates with the conventional `128 + signum` exit code).\n */\nexport function registerExitHandlers(registry: WorktreeRegistry): void {\n _activeRegistry = registry\n if (_registered) return\n _registered = true\n _exitHandler = () => sweepRegistry()\n _sigintHandler = () => {\n sweepRegistry()\n if (_sigintHandler) process.off(\"SIGINT\", _sigintHandler)\n process.kill(process.pid, \"SIGINT\")\n }\n _sigtermHandler = () => {\n sweepRegistry()\n if (_sigtermHandler) process.off(\"SIGTERM\", _sigtermHandler)\n process.kill(process.pid, \"SIGTERM\")\n }\n process.on(\"SIGINT\", _sigintHandler)\n process.on(\"SIGTERM\", _sigtermHandler)\n // `exit` handlers can only run synchronous code — exactly what\n // sweepRegistry does. Async work here would never complete.\n process.on(\"exit\", _exitHandler)\n}\n\n/**\n * Test-only: unregister the handlers and reset module state. Tests\n * that want to verify `registerExitHandlers` semantics must clean up\n * after themselves or future tests in the same process inherit the\n * (now stale) registry pointer.\n */\nexport function __unregisterExitHandlersForTests(): void {\n if (_sigintHandler) {\n process.off(\"SIGINT\", _sigintHandler)\n _sigintHandler = null\n }\n if (_sigtermHandler) {\n process.off(\"SIGTERM\", _sigtermHandler)\n _sigtermHandler = null\n }\n if (_exitHandler) {\n process.off(\"exit\", _exitHandler)\n _exitHandler = null\n }\n _registered = false\n _activeRegistry = null\n}\n\n// ---------------------------------------------------------------------\n// Ledger: which repos has this proxy touched?\n// ---------------------------------------------------------------------\n\ninterface LedgerEntry {\n repoRoot: string\n lastSeenMs: number\n}\n\ninterface LedgerFile {\n entries: Array<LedgerEntry>\n}\n\nfunction ledgerPath(): string {\n return path.join(PATHS.APP_DIR, \"worker-repos.json\")\n}\n\nasync function readLedger(): Promise<LedgerFile> {\n let raw: string\n try {\n raw = await fs.readFile(ledgerPath(), \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { entries: [] }\n }\n return { entries: [] }\n }\n try {\n const parsed = JSON.parse(raw) as Partial<LedgerFile>\n if (!parsed || !Array.isArray(parsed.entries)) return { entries: [] }\n const cleaned: Array<LedgerEntry> = []\n for (const e of parsed.entries) {\n if (\n e &&\n typeof e === \"object\" &&\n typeof (e as LedgerEntry).repoRoot === \"string\" &&\n typeof (e as LedgerEntry).lastSeenMs === \"number\"\n ) {\n cleaned.push({\n repoRoot: (e as LedgerEntry).repoRoot,\n lastSeenMs: (e as LedgerEntry).lastSeenMs,\n })\n }\n }\n return { entries: cleaned }\n } catch {\n // Corrupted JSON — start fresh rather than crashing the proxy.\n return { entries: [] }\n }\n}\n\n/**\n * Per-process serializer for ledger writes. Multiple concurrent\n * `recordWorkerRepo` calls (legitimate: several workers may start at\n * once) would otherwise race read-modify-write on the JSON file. Each\n * call chains onto the previous so the on-disk sequence is\n * deterministic from this process's perspective.\n *\n * Cross-process safety is provided by the atomic temp+rename below,\n * which makes the final state of the file always be a well-formed\n * full snapshot from ONE writer — never a partial write or\n * interleaved JSON.\n */\nlet _ledgerChain: Promise<void> = Promise.resolve()\n\n/**\n * Append `repoRoot` to the ledger (or update its `lastSeenMs`).\n * Atomic temp+rename per peer review.\n */\nexport function recordWorkerRepo(repoRoot: string): Promise<void> {\n const next = _ledgerChain.then(async () => {\n await fs.mkdir(PATHS.APP_DIR, { recursive: true })\n const current = await readLedger()\n // Dedup: drop any existing entry for this root before appending\n // the fresh one so the array doesn't grow unbounded with repeats.\n const filtered = current.entries.filter((e) => e.repoRoot !== repoRoot)\n filtered.push({ repoRoot, lastSeenMs: Date.now() })\n // Prune by age and cap entry count (newest wins).\n const now = Date.now()\n const pruned = filtered\n .filter((e) => now - e.lastSeenMs < LEDGER_MAX_AGE_MS)\n .slice(-LEDGER_MAX_ENTRIES)\n const ledger: LedgerFile = { entries: pruned }\n\n // Atomic temp+rename. The temp filename is unique per call\n // (PID + 8 random hex chars) so concurrent processes don't\n // collide on the temp name; the final `rename` is atomic on\n // POSIX and on Windows (both with same filesystem).\n const tmp = `${ledgerPath()}.tmp.${process.pid}.${randomBytes(4).toString(\n \"hex\",\n )}`\n try {\n await writeRuntimeFileSecure(tmp, JSON.stringify(ledger, null, 2))\n await fs.rename(tmp, ledgerPath())\n } catch (err) {\n // Clean up the temp file if rename failed midway.\n await fs.unlink(tmp).catch(() => {})\n throw err\n }\n })\n // Swallow chain-internal errors so one failed write doesn't poison\n // the chain for every subsequent caller. Each call still sees its\n // own rejection (we return `next`, not the catch-handler chain).\n _ledgerChain = next.catch(() => undefined)\n return next\n}\n\nfunction isPidAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false\n try {\n process.kill(pid, 0)\n return true\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n // EPERM = process exists but we can't signal it — still alive\n // for our purposes (we just need to know whether to clean up).\n if (code === \"EPERM\") return true\n return false\n }\n}\n\n/**\n * Boot-time sweep. For every repo we recorded in the ledger,\n * enumerate `<repoRoot>/.git/worker-worktrees/` (the conventional\n * location — for repos already inside a worktree, the actual\n * `git-common-dir` may differ, in which case we'll miss this batch\n * and the per-call age sweep will catch them within 7 days) and\n * remove dirs that aren't owned by THIS proxy.\n *\n * Ownership rule: dir is \"ours\" iff its embedded PID is alive AND\n * its embedded UUID equals `getInstanceUuid()`. Either condition\n * failing → remove.\n */\nexport async function sweepStaleWorktreesAtBoot(): Promise<void> {\n const ledger = await readLedger()\n if (ledger.entries.length === 0) return\n const currentUuid = getInstanceUuid()\n for (const entry of ledger.entries) {\n const parent = path.join(entry.repoRoot, \".git\", \"worker-worktrees\")\n let names: Array<string>\n try {\n names = await fs.readdir(parent)\n } catch {\n continue\n }\n for (const name of names) {\n const m = WORKTREE_DIR_NAME_RE.exec(name)\n if (!m) continue\n const pid = Number.parseInt(m[1], 10)\n const uuid = m[2]\n const isOurs = isPidAlive(pid) && uuid === currentUuid\n if (isOurs) continue\n\n const fullDir = path.join(parent, name)\n const branch = `worker/${pid}-${uuid}-${m[3]}`\n try {\n // `-C entry.repoRoot` is load-bearing here too — see the\n // matching comment in `sweepRegistry`. The boot sweep runs\n // BEFORE any worker tool has set cwd, so the proxy's cwd is\n // the user's launch dir, which is almost never inside the\n // target repo.\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"worktree\", \"remove\", \"--force\", fullDir],\n { stdio: \"ignore\", timeout: 10_000, windowsHide: true },\n )\n } catch {\n // ignore\n }\n try {\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"branch\", \"-D\", branch],\n { stdio: \"ignore\", timeout: 5_000, windowsHide: true },\n )\n } catch {\n // ignore\n }\n try {\n await fs.rm(fullDir, { recursive: true, force: true })\n } catch {\n // ignore — git may have removed it already\n }\n }\n }\n}\n\n/** Test-only: clear the ledger file (does NOT remove on-disk worktrees). */\nexport async function __clearLedgerForTests(): Promise<void> {\n await fs.unlink(ledgerPath()).catch(() => {})\n}\n\n/** Test-only: read the ledger as a plain array (no side effects). */\nexport async function __readLedgerForTests(): Promise<Array<LedgerEntry>> {\n return (await readLedger()).entries\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { o as sweepStaleWorktreesAtBoot } from "./lifecycle-B7CHqKlF.js";
2
+ export { sweepStaleWorktreesAtBoot };
@@ -1,4 +1,4 @@
1
- import { t as PATHS } from "./paths-j2B7b0DZ.js";
1
+ import { t as PATHS } from "./paths-wLC0InjX.js";
2
2
  import { n as killManagedTree } from "./exec-y8C_MU8A.js";
3
3
  import consola from "consola";
4
4
  import path from "node:path";
@@ -228,4 +228,4 @@ async function exists(p) {
228
228
  //#endregion
229
229
  export { sweepLiveChildren as a, sweepColbertStore as i, isPidAlive as n, sweepStaleColbertMetaAtBoot as o, registerColbertExitHandlers as r, trackChild as s, getColbertInstanceUuid as t };
230
230
 
231
- //# sourceMappingURL=lifecycle-AGXnd-ZY.js.map
231
+ //# sourceMappingURL=lifecycle-D-rL81tT.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle-AGXnd-ZY.js","names":[],"sources":["../src/lib/colbert/lifecycle.ts"],"sourcesContent":["/**\n * ColBERT sidecar lifecycle: in-memory PID ledger for the short-lived\n * `colgrep` children this proxy spawns, signal-handler tree-kill on\n * exit, and a boot-time metadata reclassification sweep.\n *\n * Because colgrep is CLI-per-invocation (no daemon), the lifecycle\n * problem is **process tracking + cancellation + boot/exit sweep**, NOT\n * keep-alive. Modeled on `worker-agent/lifecycle.ts` (PID ledger + boot\n * sweep + per-proxy-run instance UUID) and `exec.ts`'s tree-kill.\n *\n * Three cooperating layers (none sufficient alone):\n * 1. Per-call cleanup — the runner's `finally` force-kills the child\n * it spawned (handled in runner.ts).\n * 2. Session-end signal sweep (this file) — SIGINT/SIGTERM/exit kill\n * every still-tracked child of THIS run.\n * 3. Boot-time sweep (`sweepStaleColbertMetaAtBoot`) — reclassifies\n * `.gh-router-meta/*.json` entries whose `buildPid` is dead from\n * `building` → `failed`. It NEVER issues a kill to a PID from a\n * prior boot (a reused PID may belong to an unrelated process);\n * only the in-memory ledger (this run's spawns) is ever killed.\n */\n\nimport { spawn } from \"node:child_process\"\nimport { randomUUID } from \"node:crypto\"\nimport fs from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport consola from \"consola\"\n\nimport { killManagedTree } from \"../exec\"\nimport { PATHS } from \"../paths\"\n\n// ---------------------------------------------------------------------\n// Per-launch instance UUID (mirrors worker-agent/lifecycle.ts)\n// ---------------------------------------------------------------------\n\nlet _instanceUuid: string | null = null\n\n/**\n * Stable UUID4 generated once per proxy process. Written into the\n * sidecar metadata `ownerInstanceId` so the boot sweep can tell \"this\n * proxy's still-live build\" from \"a stranded `building` entry from a\n * prior proxy whose PID got recycled\" (Docker PID-1 across restarts).\n */\nexport function getColbertInstanceUuid(): string {\n if (_instanceUuid === null) _instanceUuid = randomUUID()\n return _instanceUuid\n}\n\n/** Test-only: reset the cached UUID. */\nexport function __resetColbertInstanceUuidForTests(): void {\n _instanceUuid = null\n}\n\n// ---------------------------------------------------------------------\n// In-memory PID ledger of THIS run's live colgrep children\n// ---------------------------------------------------------------------\n\ntype TrackedChild = ReturnType<typeof spawn>\n\nconst _liveChildren = new Set<TrackedChild>()\n\n/**\n * Register a freshly-spawned colgrep child so the exit sweep can reap\n * it. The runner also removes it on natural close via `untrackChild`.\n */\nexport function trackChild(child: TrackedChild): void {\n _liveChildren.add(child)\n child.once(\"close\", () => _liveChildren.delete(child))\n child.once(\"error\", () => _liveChildren.delete(child))\n}\n\n/** Remove a child from the ledger (e.g. after a clean per-call kill). */\nexport function untrackChild(child: TrackedChild): void {\n _liveChildren.delete(child)\n}\n\n/** Count of live tracked children (test/diagnostic). */\nexport function liveChildCount(): number {\n return _liveChildren.size\n}\n\n/**\n * Synchronous best-effort tree-kill of every tracked child. Called from\n * the signal/exit handlers. After killing, the set is cleared so a\n * second call is a no-op.\n */\nexport function sweepLiveChildren(): void {\n const isWin = process.platform === \"win32\"\n for (const child of _liveChildren) {\n try {\n killManagedTree(child, isWin)\n } catch {\n // already gone\n }\n }\n _liveChildren.clear()\n}\n\n// ---------------------------------------------------------------------\n// Signal handlers (mirror worker-agent/lifecycle.ts re-raise pattern)\n// ---------------------------------------------------------------------\n\nlet _registered = false\nlet _exitHandler: (() => void) | null = null\nlet _sigintHandler: (() => void) | null = null\nlet _sigtermHandler: (() => void) | null = null\n\n/**\n * Wire SIGINT/SIGTERM/exit handlers that tree-kill every tracked\n * colgrep child. Idempotent — subsequent calls are a no-op (we never\n * leak listeners). The signal handlers re-raise after sweeping so Node's\n * default terminate-on-signal behavior is restored (otherwise attaching\n * a listener cancels the default and Ctrl-C would clean but not exit).\n */\nexport function registerColbertExitHandlers(): void {\n if (_registered) return\n _registered = true\n _exitHandler = () => sweepLiveChildren()\n _sigintHandler = () => {\n sweepLiveChildren()\n if (_sigintHandler) process.off(\"SIGINT\", _sigintHandler)\n process.kill(process.pid, \"SIGINT\")\n }\n _sigtermHandler = () => {\n sweepLiveChildren()\n if (_sigtermHandler) process.off(\"SIGTERM\", _sigtermHandler)\n process.kill(process.pid, \"SIGTERM\")\n }\n process.on(\"SIGINT\", _sigintHandler)\n process.on(\"SIGTERM\", _sigtermHandler)\n process.on(\"exit\", _exitHandler)\n}\n\n/** Test-only: unregister handlers + reset module state. */\nexport function __unregisterColbertExitHandlersForTests(): void {\n if (_sigintHandler) {\n process.off(\"SIGINT\", _sigintHandler)\n _sigintHandler = null\n }\n if (_sigtermHandler) {\n process.off(\"SIGTERM\", _sigtermHandler)\n _sigtermHandler = null\n }\n if (_exitHandler) {\n process.off(\"exit\", _exitHandler)\n _exitHandler = null\n }\n _registered = false\n _liveChildren.clear()\n}\n\n// ---------------------------------------------------------------------\n// Boot-time metadata reclassification sweep\n// ---------------------------------------------------------------------\n\n/**\n * True iff `pid` names a live process. `process.kill(pid, 0)` probes\n * existence without signalling; `EPERM` means the process exists but is\n * owned by another user (still alive). Exported so the per-query freshness\n * verdict can mirror the boot sweep's liveness check.\n */\nexport function isPidAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false\n try {\n process.kill(pid, 0)\n return true\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"EPERM\") return true\n return false\n }\n}\n\n/**\n * Boot-time sweep. Walks `.gh-router-meta/*.json`; any entry stuck in\n * `status:\"building\"` whose `buildPid` is DEAD is a crashed-build\n * escapee → reset to `status:\"failed\"` so the next search re-kicks a\n * build instead of routing to a never-finishing one.\n *\n * It NEVER kills anything: a live PID matching a stale `buildPid` from a\n * prior boot may be a recycled PID belonging to an unrelated process, so\n * the boot sweep only RECLASSIFIES metadata. The in-memory ledger (this\n * run's spawns) is the only thing the SIGINT/SIGTERM handler ever kills.\n *\n * Best-effort; never throws (wrapped by the caller in `ensurePaths`).\n */\nexport async function sweepStaleColbertMetaAtBoot(): Promise<void> {\n const metaDir = PATHS.COLBERT_META_DIR\n let names: Array<string>\n try {\n names = await fs.readdir(metaDir)\n } catch {\n return // no meta dir yet — nothing to sweep\n }\n for (const name of names) {\n if (!name.endsWith(\".json\")) continue\n const file = path.join(metaDir, name)\n let meta: Record<string, unknown>\n try {\n meta = JSON.parse(await fs.readFile(file, \"utf8\")) as Record<string, unknown>\n } catch {\n continue // corrupt — leave it; index-store re-derives on next access\n }\n if (meta.status !== \"building\") continue\n const buildPid = typeof meta.buildPid === \"number\" ? meta.buildPid : 0\n if (buildPid > 0 && isPidAlive(buildPid)) {\n // A live PID — could be ours (this run re-kicked) or a recycled\n // unrelated PID. Either way: never kill from the boot sweep. Leave\n // the entry; the runner's own ownership check governs.\n continue\n }\n // Dead build PID → reclassify to failed (atomic temp+rename). Stamp the\n // crash class so the per-query self-heal treats it as transient (re-kick)\n // rather than operator-actionable.\n meta.status = \"failed\"\n meta.failureClass = \"crashed\"\n const tmp = `${file}.${process.pid}.tmp`\n try {\n await fs.writeFile(tmp, JSON.stringify(meta, null, 2))\n await fs.rename(tmp, file)\n } catch {\n await fs.rm(tmp, { force: true }).catch(() => {})\n }\n }\n await sweepColbertStore()\n}\n\n/** An orphan stub younger than this may be a build that just started. */\nconst ORPHAN_GRACE_MS = 60 * 60 * 1000\n\n/**\n * Reclaim disk in the colgrep store, ordered so it cannot destroy good data.\n *\n * This runs AFTER the identity fix in this same release, which matters: the\n * scanner that decides \"unreachable\" is the one that was wrong. Under the old\n * comparison a perfectly healthy index looked unreachable, and a sweep written\n * against that scanner would have deleted it. Everything below therefore\n * leans on evidence that does not depend on our path matching at all.\n *\n * Three rules, in increasing order of caution:\n *\n * 1. ORPHAN STUB — no `project.json` AND an empty-or-absent `index/` AND\n * older than the grace window. All three required. A dir with a\n * `project.json` is real data and is never touched by this rule; a dir\n * with shards is never touched even without one. Deleted outright,\n * because there is provably nothing in it.\n *\n * 2. DEAD META — a sidecar whose `workspace` no longer exists on disk. Pure\n * bookkeeping, never index data. (48 such entries had accumulated on the\n * machine where this was diagnosed; nothing ever reaped them.)\n *\n * 3. QUARANTINE, NEVER DIRECT DELETE, for anything holding real bytes. A\n * dir with a `project.json` is left alone entirely, however unreachable\n * it looks. Reclaiming those would mean deleting real index data on our\n * own inference about what colgrep would use — and this incident is the\n * proof that such inference can be wrong. If it is ever added it must\n * rename first and delete only after a retention window, so the whole\n * period stays reversible.\n *\n * Skips any dir with a live lock owner or an in-flight init, and logs a\n * one-line count so the accumulation is observable rather than silent.\n */\nexport async function sweepColbertStore(): Promise<void> {\n const indicesDir = PATHS.COLBERT_INDICES_DIR\n let names: Array<string>\n try {\n names = await fs.readdir(indicesDir)\n } catch {\n return\n }\n\n const now = Date.now()\n let orphans = 0\n let deadMeta = 0\n\n for (const name of names) {\n if (name === \".gh-router-meta\") continue\n // Quarantined husks are owned by the corrupt-repair path, which deletes\n // them out of band. Never race it.\n if (name.includes(\".corrupt-\")) continue\n const dir = path.join(indicesDir, name)\n try {\n const st = await fs.stat(dir)\n if (!st.isDirectory()) continue\n // A live build claims its dir with a `.lock`; another proxy may own it.\n if (await exists(path.join(dir, \".lock\"))) continue\n // Real index data — leave it. Reclaiming this needs quarantine-first\n // semantics, not a boot-time `rm`.\n if (await exists(path.join(dir, \"project.json\"))) continue\n // Anything with shards is real work in progress, even mid-build.\n let indexEntries: Array<string> = []\n try {\n indexEntries = await fs.readdir(path.join(dir, \"index\"))\n } catch {\n /* absent index/ is the orphan shape */\n }\n if (indexEntries.length > 0) continue\n if (now - st.mtimeMs < ORPHAN_GRACE_MS) continue\n await fs.rm(dir, { recursive: true, force: true })\n orphans++\n } catch {\n // Vanished mid-sweep or unreadable — skip.\n }\n }\n\n // Dead sidecars: the workspace they describe is gone.\n try {\n const metaDir = PATHS.COLBERT_META_DIR\n for (const f of await fs.readdir(metaDir)) {\n if (!f.endsWith(\".json\")) continue\n const file = path.join(metaDir, f)\n try {\n const meta = JSON.parse(await fs.readFile(file, \"utf8\")) as {\n workspace?: string\n status?: string\n }\n const ws = meta.workspace\n if (typeof ws !== \"string\" || ws.length === 0) continue\n // Never reap a live build's bookkeeping.\n if (meta.status === \"building\") continue\n // Nor an entry this very sweep just reclassified. The reclassification\n // pass above rewrites `building` + dead-PID to `failed` so the runner\n // can self-heal; deleting it in the same pass would erase that state\n // before anything could act on it. Only reap entries that have been\n // settled for a while.\n const stMeta = await fs.stat(file).catch(() => null)\n if (stMeta && now - stMeta.mtimeMs < ORPHAN_GRACE_MS) continue\n if (await exists(ws)) continue\n await fs.rm(file, { force: true })\n deadMeta++\n } catch {\n // Corrupt/unreadable — leave it; index-store re-derives on next access.\n }\n }\n } catch {\n // No meta dir yet.\n }\n\n if (orphans > 0 || deadMeta > 0) {\n consola.debug(\n `colbert: store sweep reclaimed ${orphans} orphan index dir(s), ${deadMeta} dead metadata entr(ies)`,\n )\n }\n}\n\nasync function exists(p: string): Promise<boolean> {\n try {\n await fs.stat(p)\n return true\n } catch {\n return false\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"lifecycle-D-rL81tT.js","names":[],"sources":["../src/lib/colbert/lifecycle.ts"],"sourcesContent":["/**\n * ColBERT sidecar lifecycle: in-memory PID ledger for the short-lived\n * `colgrep` children this proxy spawns, signal-handler tree-kill on\n * exit, and a boot-time metadata reclassification sweep.\n *\n * Because colgrep is CLI-per-invocation (no daemon), the lifecycle\n * problem is **process tracking + cancellation + boot/exit sweep**, NOT\n * keep-alive. Modeled on `worker-agent/lifecycle.ts` (PID ledger + boot\n * sweep + per-proxy-run instance UUID) and `exec.ts`'s tree-kill.\n *\n * Three cooperating layers (none sufficient alone):\n * 1. Per-call cleanup — the runner's `finally` force-kills the child\n * it spawned (handled in runner.ts).\n * 2. Session-end signal sweep (this file) — SIGINT/SIGTERM/exit kill\n * every still-tracked child of THIS run.\n * 3. Boot-time sweep (`sweepStaleColbertMetaAtBoot`) — reclassifies\n * `.gh-router-meta/*.json` entries whose `buildPid` is dead from\n * `building` → `failed`. It NEVER issues a kill to a PID from a\n * prior boot (a reused PID may belong to an unrelated process);\n * only the in-memory ledger (this run's spawns) is ever killed.\n */\n\nimport { spawn } from \"node:child_process\"\nimport { randomUUID } from \"node:crypto\"\nimport fs from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport consola from \"consola\"\n\nimport { killManagedTree } from \"../exec\"\nimport { PATHS } from \"../paths\"\n\n// ---------------------------------------------------------------------\n// Per-launch instance UUID (mirrors worker-agent/lifecycle.ts)\n// ---------------------------------------------------------------------\n\nlet _instanceUuid: string | null = null\n\n/**\n * Stable UUID4 generated once per proxy process. Written into the\n * sidecar metadata `ownerInstanceId` so the boot sweep can tell \"this\n * proxy's still-live build\" from \"a stranded `building` entry from a\n * prior proxy whose PID got recycled\" (Docker PID-1 across restarts).\n */\nexport function getColbertInstanceUuid(): string {\n if (_instanceUuid === null) _instanceUuid = randomUUID()\n return _instanceUuid\n}\n\n/** Test-only: reset the cached UUID. */\nexport function __resetColbertInstanceUuidForTests(): void {\n _instanceUuid = null\n}\n\n// ---------------------------------------------------------------------\n// In-memory PID ledger of THIS run's live colgrep children\n// ---------------------------------------------------------------------\n\ntype TrackedChild = ReturnType<typeof spawn>\n\nconst _liveChildren = new Set<TrackedChild>()\n\n/**\n * Register a freshly-spawned colgrep child so the exit sweep can reap\n * it. The runner also removes it on natural close via `untrackChild`.\n */\nexport function trackChild(child: TrackedChild): void {\n _liveChildren.add(child)\n child.once(\"close\", () => _liveChildren.delete(child))\n child.once(\"error\", () => _liveChildren.delete(child))\n}\n\n/** Remove a child from the ledger (e.g. after a clean per-call kill). */\nexport function untrackChild(child: TrackedChild): void {\n _liveChildren.delete(child)\n}\n\n/** Count of live tracked children (test/diagnostic). */\nexport function liveChildCount(): number {\n return _liveChildren.size\n}\n\n/**\n * Synchronous best-effort tree-kill of every tracked child. Called from\n * the signal/exit handlers. After killing, the set is cleared so a\n * second call is a no-op.\n */\nexport function sweepLiveChildren(): void {\n const isWin = process.platform === \"win32\"\n for (const child of _liveChildren) {\n try {\n killManagedTree(child, isWin)\n } catch {\n // already gone\n }\n }\n _liveChildren.clear()\n}\n\n// ---------------------------------------------------------------------\n// Signal handlers (mirror worker-agent/lifecycle.ts re-raise pattern)\n// ---------------------------------------------------------------------\n\nlet _registered = false\nlet _exitHandler: (() => void) | null = null\nlet _sigintHandler: (() => void) | null = null\nlet _sigtermHandler: (() => void) | null = null\n\n/**\n * Wire SIGINT/SIGTERM/exit handlers that tree-kill every tracked\n * colgrep child. Idempotent — subsequent calls are a no-op (we never\n * leak listeners). The signal handlers re-raise after sweeping so Node's\n * default terminate-on-signal behavior is restored (otherwise attaching\n * a listener cancels the default and Ctrl-C would clean but not exit).\n */\nexport function registerColbertExitHandlers(): void {\n if (_registered) return\n _registered = true\n _exitHandler = () => sweepLiveChildren()\n _sigintHandler = () => {\n sweepLiveChildren()\n if (_sigintHandler) process.off(\"SIGINT\", _sigintHandler)\n process.kill(process.pid, \"SIGINT\")\n }\n _sigtermHandler = () => {\n sweepLiveChildren()\n if (_sigtermHandler) process.off(\"SIGTERM\", _sigtermHandler)\n process.kill(process.pid, \"SIGTERM\")\n }\n process.on(\"SIGINT\", _sigintHandler)\n process.on(\"SIGTERM\", _sigtermHandler)\n process.on(\"exit\", _exitHandler)\n}\n\n/** Test-only: unregister handlers + reset module state. */\nexport function __unregisterColbertExitHandlersForTests(): void {\n if (_sigintHandler) {\n process.off(\"SIGINT\", _sigintHandler)\n _sigintHandler = null\n }\n if (_sigtermHandler) {\n process.off(\"SIGTERM\", _sigtermHandler)\n _sigtermHandler = null\n }\n if (_exitHandler) {\n process.off(\"exit\", _exitHandler)\n _exitHandler = null\n }\n _registered = false\n _liveChildren.clear()\n}\n\n// ---------------------------------------------------------------------\n// Boot-time metadata reclassification sweep\n// ---------------------------------------------------------------------\n\n/**\n * True iff `pid` names a live process. `process.kill(pid, 0)` probes\n * existence without signalling; `EPERM` means the process exists but is\n * owned by another user (still alive). Exported so the per-query freshness\n * verdict can mirror the boot sweep's liveness check.\n */\nexport function isPidAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false\n try {\n process.kill(pid, 0)\n return true\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"EPERM\") return true\n return false\n }\n}\n\n/**\n * Boot-time sweep. Walks `.gh-router-meta/*.json`; any entry stuck in\n * `status:\"building\"` whose `buildPid` is DEAD is a crashed-build\n * escapee → reset to `status:\"failed\"` so the next search re-kicks a\n * build instead of routing to a never-finishing one.\n *\n * It NEVER kills anything: a live PID matching a stale `buildPid` from a\n * prior boot may be a recycled PID belonging to an unrelated process, so\n * the boot sweep only RECLASSIFIES metadata. The in-memory ledger (this\n * run's spawns) is the only thing the SIGINT/SIGTERM handler ever kills.\n *\n * Best-effort; never throws (wrapped by the caller in `ensurePaths`).\n */\nexport async function sweepStaleColbertMetaAtBoot(): Promise<void> {\n const metaDir = PATHS.COLBERT_META_DIR\n let names: Array<string>\n try {\n names = await fs.readdir(metaDir)\n } catch {\n return // no meta dir yet — nothing to sweep\n }\n for (const name of names) {\n if (!name.endsWith(\".json\")) continue\n const file = path.join(metaDir, name)\n let meta: Record<string, unknown>\n try {\n meta = JSON.parse(await fs.readFile(file, \"utf8\")) as Record<string, unknown>\n } catch {\n continue // corrupt — leave it; index-store re-derives on next access\n }\n if (meta.status !== \"building\") continue\n const buildPid = typeof meta.buildPid === \"number\" ? meta.buildPid : 0\n if (buildPid > 0 && isPidAlive(buildPid)) {\n // A live PID — could be ours (this run re-kicked) or a recycled\n // unrelated PID. Either way: never kill from the boot sweep. Leave\n // the entry; the runner's own ownership check governs.\n continue\n }\n // Dead build PID → reclassify to failed (atomic temp+rename). Stamp the\n // crash class so the per-query self-heal treats it as transient (re-kick)\n // rather than operator-actionable.\n meta.status = \"failed\"\n meta.failureClass = \"crashed\"\n const tmp = `${file}.${process.pid}.tmp`\n try {\n await fs.writeFile(tmp, JSON.stringify(meta, null, 2))\n await fs.rename(tmp, file)\n } catch {\n await fs.rm(tmp, { force: true }).catch(() => {})\n }\n }\n await sweepColbertStore()\n}\n\n/** An orphan stub younger than this may be a build that just started. */\nconst ORPHAN_GRACE_MS = 60 * 60 * 1000\n\n/**\n * Reclaim disk in the colgrep store, ordered so it cannot destroy good data.\n *\n * This runs AFTER the identity fix in this same release, which matters: the\n * scanner that decides \"unreachable\" is the one that was wrong. Under the old\n * comparison a perfectly healthy index looked unreachable, and a sweep written\n * against that scanner would have deleted it. Everything below therefore\n * leans on evidence that does not depend on our path matching at all.\n *\n * Three rules, in increasing order of caution:\n *\n * 1. ORPHAN STUB — no `project.json` AND an empty-or-absent `index/` AND\n * older than the grace window. All three required. A dir with a\n * `project.json` is real data and is never touched by this rule; a dir\n * with shards is never touched even without one. Deleted outright,\n * because there is provably nothing in it.\n *\n * 2. DEAD META — a sidecar whose `workspace` no longer exists on disk. Pure\n * bookkeeping, never index data. (48 such entries had accumulated on the\n * machine where this was diagnosed; nothing ever reaped them.)\n *\n * 3. QUARANTINE, NEVER DIRECT DELETE, for anything holding real bytes. A\n * dir with a `project.json` is left alone entirely, however unreachable\n * it looks. Reclaiming those would mean deleting real index data on our\n * own inference about what colgrep would use — and this incident is the\n * proof that such inference can be wrong. If it is ever added it must\n * rename first and delete only after a retention window, so the whole\n * period stays reversible.\n *\n * Skips any dir with a live lock owner or an in-flight init, and logs a\n * one-line count so the accumulation is observable rather than silent.\n */\nexport async function sweepColbertStore(): Promise<void> {\n const indicesDir = PATHS.COLBERT_INDICES_DIR\n let names: Array<string>\n try {\n names = await fs.readdir(indicesDir)\n } catch {\n return\n }\n\n const now = Date.now()\n let orphans = 0\n let deadMeta = 0\n\n for (const name of names) {\n if (name === \".gh-router-meta\") continue\n // Quarantined husks are owned by the corrupt-repair path, which deletes\n // them out of band. Never race it.\n if (name.includes(\".corrupt-\")) continue\n const dir = path.join(indicesDir, name)\n try {\n const st = await fs.stat(dir)\n if (!st.isDirectory()) continue\n // A live build claims its dir with a `.lock`; another proxy may own it.\n if (await exists(path.join(dir, \".lock\"))) continue\n // Real index data — leave it. Reclaiming this needs quarantine-first\n // semantics, not a boot-time `rm`.\n if (await exists(path.join(dir, \"project.json\"))) continue\n // Anything with shards is real work in progress, even mid-build.\n let indexEntries: Array<string> = []\n try {\n indexEntries = await fs.readdir(path.join(dir, \"index\"))\n } catch {\n /* absent index/ is the orphan shape */\n }\n if (indexEntries.length > 0) continue\n if (now - st.mtimeMs < ORPHAN_GRACE_MS) continue\n await fs.rm(dir, { recursive: true, force: true })\n orphans++\n } catch {\n // Vanished mid-sweep or unreadable — skip.\n }\n }\n\n // Dead sidecars: the workspace they describe is gone.\n try {\n const metaDir = PATHS.COLBERT_META_DIR\n for (const f of await fs.readdir(metaDir)) {\n if (!f.endsWith(\".json\")) continue\n const file = path.join(metaDir, f)\n try {\n const meta = JSON.parse(await fs.readFile(file, \"utf8\")) as {\n workspace?: string\n status?: string\n }\n const ws = meta.workspace\n if (typeof ws !== \"string\" || ws.length === 0) continue\n // Never reap a live build's bookkeeping.\n if (meta.status === \"building\") continue\n // Nor an entry this very sweep just reclassified. The reclassification\n // pass above rewrites `building` + dead-PID to `failed` so the runner\n // can self-heal; deleting it in the same pass would erase that state\n // before anything could act on it. Only reap entries that have been\n // settled for a while.\n const stMeta = await fs.stat(file).catch(() => null)\n if (stMeta && now - stMeta.mtimeMs < ORPHAN_GRACE_MS) continue\n if (await exists(ws)) continue\n await fs.rm(file, { force: true })\n deadMeta++\n } catch {\n // Corrupt/unreadable — leave it; index-store re-derives on next access.\n }\n }\n } catch {\n // No meta dir yet.\n }\n\n if (orphans > 0 || deadMeta > 0) {\n consola.debug(\n `colbert: store sweep reclaimed ${orphans} orphan index dir(s), ${deadMeta} dead metadata entr(ies)`,\n )\n }\n}\n\nasync function exists(p: string): Promise<boolean> {\n try {\n await fs.stat(p)\n return true\n } catch {\n return false\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { o as sweepStaleColbertMetaAtBoot } from "./lifecycle-D-rL81tT.js";
2
+ export { sweepStaleColbertMetaAtBoot };
@@ -0,0 +1,16 @@
1
+ import { a as __esmMin, i as init_dist, n as init_paths } from "./hooks.mjs";
2
+ import "node:path";
3
+ import "node:process";
4
+ import "node:child_process";
5
+ import "node:crypto";
6
+ import "node:fs/promises";
7
+ __esmMin((() => {
8
+ init_paths();
9
+ Promise.resolve();
10
+ }));
11
+ __esmMin((() => {
12
+ init_dist();
13
+ init_paths();
14
+ }));
15
+ //#endregion
16
+ export {};