@webpresso/agent-kit 3.1.26 → 3.1.28

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 (62) hide show
  1. package/catalog/AGENTS.md.tpl +6 -10
  2. package/catalog/agent/commands/verify.md +2 -2
  3. package/catalog/agent/rules/changeset-release.md +4 -2
  4. package/catalog/agent/rules/workflow-skills-routing.md +6 -3
  5. package/catalog/agent/skills/autopilot/SKILL.md +48 -11
  6. package/catalog/agent/skills/claude/SKILL.md +7 -0
  7. package/catalog/agent/skills/codex/SKILL.md +7 -0
  8. package/catalog/agent/skills/fix/SKILL.md +33 -8
  9. package/catalog/agent/skills/grok/SKILL.md +7 -0
  10. package/catalog/agent/skills/investigate/SKILL.md +12 -1
  11. package/catalog/agent/skills/pll/SKILL.md +53 -48
  12. package/catalog/agent/skills/team/SKILL.md +26 -9
  13. package/catalog/agent/skills/ultragoal/SKILL.md +147 -19
  14. package/catalog/agent/skills/verify/SKILL.md +71 -51
  15. package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +8 -0
  16. package/dist/esm/audit/blueprint-lifecycle-sql.js +25 -1
  17. package/dist/esm/ci/native-session-memory-cache.d.ts +9 -1
  18. package/dist/esm/ci/native-session-memory-cache.js +9 -1
  19. package/dist/esm/ci/vitest-ci-shards.d.ts +6 -4
  20. package/dist/esm/ci/vitest-ci-shards.js +6 -4
  21. package/dist/esm/cli/auto-update/guard-tombstone.d.ts +25 -0
  22. package/dist/esm/cli/auto-update/guard-tombstone.js +67 -0
  23. package/dist/esm/cli/auto-update/installer.d.ts +4 -17
  24. package/dist/esm/cli/auto-update/installer.js +6 -57
  25. package/dist/esm/cli/auto-update/self-invocation.d.ts +37 -0
  26. package/dist/esm/cli/auto-update/self-invocation.js +62 -0
  27. package/dist/esm/cli/commands/audit.js +1 -0
  28. package/dist/esm/cli/commands/dash/index.js +3 -3
  29. package/dist/esm/cli/commands/dash/plan-once-attention.d.ts +32 -0
  30. package/dist/esm/cli/commands/dash/plan-once-attention.js +50 -0
  31. package/dist/esm/cli/commands/dash/plan-once-schema.d.ts +73 -0
  32. package/dist/esm/cli/commands/dash/plan-once-schema.js +119 -0
  33. package/dist/esm/cli/commands/dash/plan-once.d.ts +122 -0
  34. package/dist/esm/cli/commands/dash/plan-once.js +224 -0
  35. package/dist/esm/cli/commands/dash/tui/ipc-protocol.d.ts +87 -0
  36. package/dist/esm/cli/commands/dash/tui/ipc-protocol.js +128 -0
  37. package/dist/esm/cli/commands/dash/tui/native-host.d.ts +34 -0
  38. package/dist/esm/cli/commands/dash/tui/native-host.js +256 -0
  39. package/dist/esm/cli/commands/dash/tui/native-path.d.ts +11 -0
  40. package/dist/esm/cli/commands/dash/tui/native-path.js +41 -0
  41. package/dist/esm/cli/commands/dash/tui/runtime.js +72 -0
  42. package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.js +1 -1
  43. package/dist/esm/cli/commands/package-manager.js +2 -1
  44. package/dist/esm/cli/commands/self-install-guard.d.ts +14 -32
  45. package/dist/esm/cli/commands/self-install-guard.js +17 -63
  46. package/dist/esm/hooks/guard-switch/index.js +8 -4
  47. package/dist/esm/hooks/post-tool/lint-after-edit.js +6 -4
  48. package/dist/esm/hooks/precompact/index.js +5 -3
  49. package/dist/esm/hooks/sessionstart/index.d.ts +10 -0
  50. package/dist/esm/hooks/sessionstart/index.js +61 -4
  51. package/dist/esm/hooks/shared/types.d.ts +11 -0
  52. package/dist/esm/hooks/shared/types.js +21 -0
  53. package/dist/esm/paths/state-root.js +6 -0
  54. package/dist/esm/session-memory/current-session.js +2 -0
  55. package/dist/esm/session-memory/native-runtime.js +5 -3
  56. package/dist/esm/session-memory/native-warm-markers.d.ts +18 -0
  57. package/dist/esm/session-memory/native-warm-markers.js +22 -0
  58. package/dist/esm/session-memory/sync/types.d.ts +4 -0
  59. package/dist/esm/session-memory/types.d.ts +3 -1
  60. package/dist/esm/session-memory/types.js +2 -0
  61. package/dist/esm/ultragoal/runtime.js +44 -2
  62. package/package.json +15 -12
@@ -0,0 +1,256 @@
1
+ // Domain host for the native Ratatui shell: maps AppViewModel ↔ IPC and keys → reduceTuiKey.
2
+ import { spawn } from "node:child_process";
3
+ import { emitDashE2eState } from "#cli/commands/dash/e2e-state.js";
4
+ import { buildRows } from "#cli/commands/dash/menu-state.js";
5
+ import { filterPaletteCommands, groupPaletteCommands } from "#cli/commands/dash/tui/palette.js";
6
+ import { reduceTuiKey } from "#cli/commands/dash/tui/state.js";
7
+ import { decodeIpcFrames, encodeIpcFrame, parseClientMessage, } from "#cli/commands/dash/tui/ipc-protocol.js";
8
+ import { sanitizeTuiText } from "#cli/commands/dash/tui/sanitize.js";
9
+ function hostAvailabilityNote(agentKey, availability) {
10
+ const row = availability.find((item) => item.id === agentKey);
11
+ if (!row || row.available)
12
+ return null;
13
+ return row.reason ?? "unavailable";
14
+ }
15
+ export function projectNativeViewModel(state, input) {
16
+ const rows = buildRows(state.agents, state.menu.expandedAgent);
17
+ const menuLines = rows.map((row) => {
18
+ const mark = row.key === state.menu.selectedKey ? "▸" : " ";
19
+ const note = row.kind === "agent" ? hostAvailabilityNote(row.agentKey, state.hostAvailability) : null;
20
+ const label = note ? `${row.label} · ${note}` : row.label;
21
+ return sanitizeTuiText(`${mark} ${label}`);
22
+ });
23
+ let overlayKind = state.overlay.kind;
24
+ let overlayLines = [];
25
+ if (state.overlay.kind === "palette") {
26
+ const matches = filterPaletteCommands(state.overlay.query);
27
+ const groups = groupPaletteCommands(matches);
28
+ overlayLines = [sanitizeTuiText(`query: ${state.overlay.query || "…"}`)];
29
+ for (const group of groups) {
30
+ overlayLines.push(sanitizeTuiText(`— ${group.category}`));
31
+ for (const [index, command] of group.commands.entries()) {
32
+ // Approximate global cursor: mark the selected match row.
33
+ const globalIndex = matches.indexOf(command);
34
+ const mark = globalIndex === state.overlay.cursor ? "▸" : " ";
35
+ overlayLines.push(sanitizeTuiText(`${mark} ${command.title} · ${command.keys}`));
36
+ void index;
37
+ }
38
+ }
39
+ if (matches.length === 0) {
40
+ overlayLines.push("(no matches)");
41
+ }
42
+ }
43
+ else if (state.overlay.kind === "help") {
44
+ overlayLines = [
45
+ "^p palette",
46
+ "^o mode",
47
+ "^s sessions",
48
+ "^x help",
49
+ "j/k move · h/l fold · g/G jump",
50
+ "q quit",
51
+ ];
52
+ }
53
+ else if (state.overlay.kind === "wizard") {
54
+ overlayLines = state.overlay.lines.map((line) => sanitizeTuiText(line));
55
+ }
56
+ else if (state.overlay.kind === "browser") {
57
+ overlayLines = state.overlay.lines.map((line) => sanitizeTuiText(line));
58
+ }
59
+ else if (state.overlay.kind === "lifecycle") {
60
+ overlayLines = [
61
+ sanitizeTuiText(`target ${state.overlay.target}`),
62
+ "d detach · s stop · f force-kill · a archive",
63
+ "Backspace delete · y confirm · Esc cancel",
64
+ ];
65
+ }
66
+ return {
67
+ repoLabel: sanitizeTuiText(input.repoLabel),
68
+ width: input.width,
69
+ height: input.height,
70
+ permissionMode: state.permissionMode,
71
+ hostAvailability: state.hostAvailability.map((h) => ({
72
+ id: h.id,
73
+ available: h.available,
74
+ ...(h.reason ? { reason: sanitizeTuiText(h.reason) } : {}),
75
+ })),
76
+ status: state.status ? sanitizeTuiText(state.status) : null,
77
+ warnings: state.warnings.map((w) => sanitizeTuiText(w)),
78
+ menuLines,
79
+ overlayKind,
80
+ overlayLines,
81
+ paletteQuery: state.overlay.kind === "palette" ? state.overlay.query : null,
82
+ };
83
+ }
84
+ export function clientMessageToParsedKey(message) {
85
+ switch (message.kind) {
86
+ case "char":
87
+ return message.value ? { kind: "char", value: message.value } : null;
88
+ case "up":
89
+ case "down":
90
+ case "left":
91
+ case "right":
92
+ case "enter":
93
+ case "escape":
94
+ case "tab":
95
+ case "backspace":
96
+ return { kind: message.kind };
97
+ case "ctrl":
98
+ return message.value ? { kind: "ctrl", value: message.value } : null;
99
+ default:
100
+ return null;
101
+ }
102
+ }
103
+ /**
104
+ * Drive pure TUI state from a native binary over length-prefixed JSON frames.
105
+ * State is owned by the caller (runtime) so launch/HUD/browser updates paint.
106
+ */
107
+ export async function runNativeTuiHost(input) {
108
+ const spawnFn = input.spawnImpl ?? spawn;
109
+ const child = spawnFn(input.binaryPath, [], {
110
+ stdio: ["pipe", "pipe", "inherit"],
111
+ });
112
+ if (!child.stdin || !child.stdout) {
113
+ throw new Error("native shell spawn did not provide stdio pipes");
114
+ }
115
+ let stdinBuf = Buffer.alloc(0);
116
+ let width = 80;
117
+ let height = 24;
118
+ const childStdin = child.stdin;
119
+ const childStdout = child.stdout;
120
+ const pushView = () => {
121
+ if (!childStdin.writable)
122
+ return;
123
+ const view = projectNativeViewModel(input.getState(), {
124
+ repoLabel: input.repoLabel,
125
+ width,
126
+ height,
127
+ });
128
+ childStdin.write(encodeIpcFrame({ type: "view", view }));
129
+ };
130
+ input.onBridge?.({ pushView });
131
+ childStdin.write(encodeIpcFrame({ type: "hello", schemaVersion: 1 }));
132
+ pushView();
133
+ /**
134
+ * Handshake hang detection: fail closed if the shell never greets.
135
+ * After ready, probe process liveness so a zombie/stuck native binary
136
+ * cannot pin the domain host forever (runtime falls back to pure-ANSI).
137
+ */
138
+ const HANDSHAKE_MS = 30_000;
139
+ const LIVENESS_MS = 5_000;
140
+ return await new Promise((resolve) => {
141
+ let settled = false;
142
+ let handshakeDone = false;
143
+ let handshakeTimer = setTimeout(() => {
144
+ if (handshakeDone || settled)
145
+ return;
146
+ input.handlers.onQuit();
147
+ child.kill("SIGTERM");
148
+ setTimeout(() => child.kill("SIGKILL"), 2_000);
149
+ finish(1);
150
+ }, HANDSHAKE_MS);
151
+ let livenessTimer;
152
+ const finish = (code) => {
153
+ if (settled)
154
+ return;
155
+ settled = true;
156
+ if (handshakeTimer)
157
+ clearTimeout(handshakeTimer);
158
+ if (livenessTimer)
159
+ clearInterval(livenessTimer);
160
+ resolve(code);
161
+ };
162
+ const armLiveness = () => {
163
+ if (livenessTimer)
164
+ return;
165
+ livenessTimer = setInterval(() => {
166
+ if (settled)
167
+ return;
168
+ // exitCode set when the process has already exited without us finishing.
169
+ if (child.exitCode !== null || child.signalCode !== null) {
170
+ finish(child.exitCode ?? 1);
171
+ return;
172
+ }
173
+ const pid = child.pid;
174
+ if (pid === undefined) {
175
+ finish(1);
176
+ return;
177
+ }
178
+ try {
179
+ process.kill(pid, 0);
180
+ }
181
+ catch {
182
+ finish(1);
183
+ }
184
+ }, LIVENESS_MS);
185
+ };
186
+ childStdout.on("data", (chunk) => {
187
+ stdinBuf = Buffer.concat([stdinBuf, chunk]);
188
+ let decoded;
189
+ try {
190
+ decoded = decodeIpcFrames(stdinBuf);
191
+ }
192
+ catch {
193
+ // Drop a corrupted frame prefix rather than crashing the domain host.
194
+ // Keep a bounded tail so a subsequent well-formed frame can resync.
195
+ stdinBuf = stdinBuf.length > 64 * 1024 ? stdinBuf.subarray(-4096) : Buffer.alloc(0);
196
+ return;
197
+ }
198
+ stdinBuf = Buffer.from(decoded.rest);
199
+ for (const raw of decoded.messages) {
200
+ let msg;
201
+ try {
202
+ msg = parseClientMessage(raw);
203
+ }
204
+ catch {
205
+ continue;
206
+ }
207
+ if (msg.type === "hello" || msg.type === "ready") {
208
+ handshakeDone = true;
209
+ if (handshakeTimer)
210
+ clearTimeout(handshakeTimer);
211
+ armLiveness();
212
+ pushView();
213
+ continue;
214
+ }
215
+ if (msg.type === "resize") {
216
+ width = msg.width;
217
+ height = msg.height;
218
+ pushView();
219
+ continue;
220
+ }
221
+ if (msg.type === "key") {
222
+ const key = clientMessageToParsedKey(msg);
223
+ if (!key)
224
+ continue;
225
+ const result = reduceTuiKey(input.getState(), key);
226
+ input.setState(result.state);
227
+ // Match pure-ANSI path: selection transitions ride the E2E channel so
228
+ // CI tmux harness can assert menu navigation without screen scrapes.
229
+ if (result.state.menu.selectedKey) {
230
+ emitDashE2eState({ kind: "selection", row: result.state.menu.selectedKey });
231
+ }
232
+ for (const effect of result.effects) {
233
+ void input.handlers.onEffect(effect);
234
+ if (effect.kind === "quit") {
235
+ input.handlers.onQuit();
236
+ child.kill("SIGTERM");
237
+ setTimeout(() => child.kill("SIGKILL"), 2_000);
238
+ finish(0);
239
+ return;
240
+ }
241
+ }
242
+ if (result.state.quit) {
243
+ input.handlers.onQuit();
244
+ child.kill("SIGTERM");
245
+ setTimeout(() => child.kill("SIGKILL"), 2_000);
246
+ finish(0);
247
+ return;
248
+ }
249
+ pushView();
250
+ }
251
+ }
252
+ });
253
+ child.on("exit", (code) => finish(code ?? 0));
254
+ child.on("error", () => finish(1));
255
+ });
256
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Env contract:
3
+ * - `WP_DASH_NATIVE=0|false|off` → force pure-ANSI
4
+ * - `WP_DASH_NATIVE=1|true|on` → prefer native when binary present
5
+ * - unset → prefer native when binary present (cutover default)
6
+ * - `WP_DASH_TUI_BIN` → absolute path override
7
+ * - `WP_DASH_FORCE_ANSI=1` → runtime force pure-ANSI even if a binary resolves
8
+ */
9
+ export declare function shouldPreferNativeDashTui(env?: NodeJS.ProcessEnv): boolean;
10
+ export declare function resolveNativeDashTuiBinary(env?: NodeJS.ProcessEnv, candidates?: readonly string[]): string | null;
11
+ export declare function defaultBinaryCandidates(cwd?: string): readonly string[];
@@ -0,0 +1,41 @@
1
+ // Resolve the optional native wp-dash-tui binary for the control-plane shell.
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { findPackageAsset } from "#utils/package-assets.js";
5
+ /**
6
+ * Env contract:
7
+ * - `WP_DASH_NATIVE=0|false|off` → force pure-ANSI
8
+ * - `WP_DASH_NATIVE=1|true|on` → prefer native when binary present
9
+ * - unset → prefer native when binary present (cutover default)
10
+ * - `WP_DASH_TUI_BIN` → absolute path override
11
+ * - `WP_DASH_FORCE_ANSI=1` → runtime force pure-ANSI even if a binary resolves
12
+ */
13
+ export function shouldPreferNativeDashTui(env = process.env) {
14
+ const flag = env["WP_DASH_NATIVE"]?.trim().toLowerCase();
15
+ if (flag === "0" || flag === "false" || flag === "off" || flag === "no") {
16
+ return false;
17
+ }
18
+ return true;
19
+ }
20
+ export function resolveNativeDashTuiBinary(env = process.env, candidates = defaultBinaryCandidates()) {
21
+ const override = env["WP_DASH_TUI_BIN"]?.trim();
22
+ if (override && existsSync(override))
23
+ return override;
24
+ for (const candidate of candidates) {
25
+ if (existsSync(candidate))
26
+ return candidate;
27
+ }
28
+ return null;
29
+ }
30
+ export function defaultBinaryCandidates(cwd = process.cwd()) {
31
+ const fromPackage = [
32
+ findPackageAsset("native/dash-tui/target/release/wp-dash-tui", { cwd }),
33
+ findPackageAsset("native/dash-tui/target/debug/wp-dash-tui", { cwd }),
34
+ ].filter((path) => path !== null);
35
+ return [
36
+ join(cwd, "native/dash-tui/target/release/wp-dash-tui"),
37
+ join(cwd, "native/dash-tui/target/debug/wp-dash-tui"),
38
+ ...fromPackage,
39
+ join(cwd, "bin/wp-dash-tui"),
40
+ ];
41
+ }
@@ -234,6 +234,8 @@ export async function runTuiMenu(input) {
234
234
  let dirty = true;
235
235
  let lastFrame = "";
236
236
  let running = true;
237
+ /** When set, domain state paints via the native shell instead of pure-ANSI. */
238
+ let nativePushView = null;
237
239
  /** Once true, no new launch effects may call selectAgent. */
238
240
  let shuttingDown = false;
239
241
  let initialLaunchConsumed = false;
@@ -261,6 +263,14 @@ export async function runTuiMenu(input) {
261
263
  const paint = () => {
262
264
  if (shuttingDown)
263
265
  return;
266
+ // Native shell owns the terminal; never write pure-ANSI frames over it.
267
+ if (nativePushView) {
268
+ if (!dirty && !input.smokeOnce)
269
+ return;
270
+ nativePushView();
271
+ dirty = false;
272
+ return;
273
+ }
264
274
  if (!dirty && !input.smokeOnce)
265
275
  return;
266
276
  const frame = renderFrame(toViewModel(state, {
@@ -621,6 +631,68 @@ export async function runTuiMenu(input) {
621
631
  stdout.write(`\n${note}\n`);
622
632
  return 0;
623
633
  }
634
+ // Primary path: native Ratatui shell when binary is available (WP_DASH_NATIVE=0 forces pure-ANSI).
635
+ const { shouldPreferNativeDashTui, resolveNativeDashTuiBinary } = await import("#cli/commands/dash/tui/native-path.js");
636
+ const nativeBinary = shouldPreferNativeDashTui() && !process.env["WP_DASH_FORCE_ANSI"]
637
+ ? resolveNativeDashTuiBinary()
638
+ : null;
639
+ if (nativeBinary) {
640
+ try {
641
+ const { runNativeTuiHost } = await import("#cli/commands/dash/tui/native-host.js");
642
+ process.stderr.write(`wp dash: native control-plane shell (${nativeBinary})\n`);
643
+ // Ensure agent list is loaded before first native frame when possible.
644
+ state = { ...state, loading: false };
645
+ const code = await runNativeTuiHost({
646
+ binaryPath: nativeBinary,
647
+ repoLabel: input.repoLabel,
648
+ getState: () => state,
649
+ setState: (next) => {
650
+ state = next;
651
+ },
652
+ onBridge: (bridge) => {
653
+ nativePushView = bridge.pushView;
654
+ },
655
+ handlers: {
656
+ onEffect: (effect) => {
657
+ // Match pure-ANSI: repaint after async effects settle so launch /
658
+ // browser / status mutations reach the native shell without waiting
659
+ // for the next key or timer tick.
660
+ void enqueueEffects([effect]).then(() => {
661
+ if (!shuttingDown)
662
+ paint();
663
+ });
664
+ },
665
+ onQuit: () => {
666
+ shuttingDown = true;
667
+ running = false;
668
+ coordinator.discardPending();
669
+ },
670
+ },
671
+ });
672
+ nativePushView = null;
673
+ if (code !== 0) {
674
+ throw new Error(`native shell exited with code ${code}`);
675
+ }
676
+ shuttingDown = true;
677
+ running = false;
678
+ coordinator.discardPending();
679
+ clearInterval(usageTimer);
680
+ clearInterval(ultragoalTimer);
681
+ clearInterval(statusTimer);
682
+ stopObservingLayout?.();
683
+ const note = await controller?.dispose();
684
+ await effectQueue.catch(() => {
685
+ /* best-effort drain after dispose */
686
+ });
687
+ if (note)
688
+ stdout.write(`\n${note}\n`);
689
+ return code;
690
+ }
691
+ catch (error) {
692
+ nativePushView = null;
693
+ process.stderr.write(`wp dash: native shell failed (${error instanceof Error ? error.message : String(error)}); falling back to pure-ANSI\n`);
694
+ }
695
+ }
624
696
  const wasRaw = stdin.isRaw;
625
697
  if (typeof stdin.setRawMode === "function")
626
698
  stdin.setRawMode(true);
@@ -26,7 +26,7 @@ import { spawnSync } from "node:child_process";
26
26
  import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync, } from "node:fs";
27
27
  import { dirname, join } from "node:path";
28
28
  import { buildVpGlobalInstallCommand, confirmInstalledGlobally, PUBLIC_PACKAGE_NAME, resolveBundledVpCommand, } from "#cli/auto-update/detect-pm.js";
29
- import { buildSelfInvocationCommand } from "#cli/commands/self-install-guard.js";
29
+ import { buildSelfInvocationCommand } from "#cli/auto-update/self-invocation.js";
30
30
  import { findAgentKitPackageRoot, resolveAgentKitPackageRoot, } from "#cli/commands/init/package-root";
31
31
  import { makeNoopSpinnerFactory, } from "#cli/commands/init/scaffolders/spinner";
32
32
  import { UPDATE_CACHE_FILENAME, readFreshCachedLatestVersion } from "#cli/auto-update/cache.js";
@@ -7,7 +7,8 @@ import { readToolingOwnershipState, tryReadRepoKey, writeToolingOwnershipState,
7
7
  import { resolveGlobalCapableVpCommand } from "#cli/global-vp.js";
8
8
  import { UPDATE_CACHE_FILENAME, formatStaleAgentKitVersionWarning, reconcileUpdateNotifierCache, } from "#cli/auto-update/cache.js";
9
9
  import { buildVpGlobalUpdateCommand, resolveBundledVpCommand } from "#cli/auto-update/detect-pm.js";
10
- import { GUARD_EXIT_CHILD_FAILED, GUARD_EXIT_LEASE_HELD, GUARD_EXIT_OWNERSHIP_LOST, GUARD_EXIT_UNRESOLVABLE, buildSelfInvocationCommand, } from "#cli/commands/self-install-guard.js";
10
+ import { buildSelfInvocationCommand } from "#cli/auto-update/self-invocation.js";
11
+ import { GUARD_EXIT_CHILD_FAILED, GUARD_EXIT_LEASE_HELD, GUARD_EXIT_OWNERSHIP_LOST, GUARD_EXIT_UNRESOLVABLE, } from "#cli/commands/self-install-guard.js";
11
12
  import { readPackageVersion } from "#cli/utils";
12
13
  import { resolveFreshAgentKitPackageRoot } from "#cli/commands/init/package-root";
13
14
  import { DEFAULT_PLUGIN_CACHE_PRUNE_HOSTS, pruneOutdatedAgentKitPluginCaches, } from "#cli/commands/init/plugin-cache-prune.js";
@@ -30,13 +30,17 @@
30
30
  * chain unresolvable after repair; 12 lease held; 13 ownership lost.
31
31
  * 7. Release the lease owner-checked in a finally.
32
32
  *
33
- * Post-swap import rule: the child `vp update -g @webpresso/agent-kit` may
34
- * delete this process's on-disk package tree (old installId) before probe/
35
- * repair run. Post-child helpers MUST be static imports loaded at process
36
- * start never `await import("#cli/…")` after the child exits, or Node
37
- * re-resolves against a removed path (`Cannot find module …/global-vp.js`).
38
- * Probe/repair logic itself reads the vite-plus store SSOT + PATH, not this
39
- * process's dying package root.
33
+ * Survival layers (in-repo SOTA bar):
34
+ * 1. Prefer a **stable** non-versioned launcher when re-execing the guard
35
+ * (`#cli/auto-update/self-invocation.js`) primary defense.
36
+ * 2. Self-package helpers (probe, repair, tombstone) MUST be static imports
37
+ * loaded at process start never package-local dynamic import, or Node
38
+ * re-resolves against a removed installId path after `vp update -g`.
39
+ * 3. Argument builders depend on cycle-free leaves (self-invocation,
40
+ * guard-tombstone), never on this command module solely for argv.
41
+ *
42
+ * Probe/repair reads the vite-plus store SSOT + PATH, not this process's
43
+ * dying package root.
40
44
  *
41
45
  * Custody boundary (stated exactly, mirrored from install-lock.ts): a stale,
42
46
  * OS-suspended holder whose child completes a mutation AFTER the successor
@@ -45,6 +49,9 @@
45
49
  * resource-side fencing.
46
50
  */
47
51
  import type { CAC } from "cac";
52
+ import { buildSelfInvocationCommand, isVersionedAgentKitPayloadPath, resolveStableWpLauncher } from "#cli/auto-update/self-invocation.js";
53
+ /** Re-export leaf helpers for public ergonomics / existing imports. */
54
+ export { buildSelfInvocationCommand, isVersionedAgentKitPayloadPath, resolveStableWpLauncher };
48
55
  export declare function registerSelfInstallGuardCommand(cli: CAC): void;
49
56
  export declare const GUARD_EXIT_CHILD_FAILED = 10;
50
57
  export declare const GUARD_EXIT_UNRESOLVABLE = 11;
@@ -85,28 +92,3 @@ export interface SelfInstallGuardDeps {
85
92
  * Testable core. The CLI action wires `process.exit(await runSelfInstallGuard(...))`.
86
93
  */
87
94
  export declare function runSelfInstallGuard(rawArgs: readonly string[], deps?: SelfInstallGuardDeps): Promise<number>;
88
- /**
89
- * How wp re-invokes itself:
90
- * - Prefer a **stable** global launcher (`~/.vite-plus/bin/wp` or PATH `wp`
91
- * outside an installId package tree). Re-execing `node …/agent-kit#id/…/cli.js`
92
- * dies after `vp update -g` deletes the pre-swap tree mid-guard.
93
- * - Bun-compiled binary: process.execPath IS the binary (argv[1] is /$bunfs).
94
- * - Source/dev lane without a stable launcher: `<runtime> <script>`.
95
- */
96
- export declare function buildSelfInvocationCommand(args: readonly string[], options?: {
97
- readonly execPath?: string;
98
- readonly argv1?: string;
99
- /** Injectable stable launcher for tests; `null` forces legacy argv1 path. */
100
- readonly stableWp?: string | null;
101
- }): string[];
102
- /**
103
- * A launcher that survives installId package-tree deletion during global update.
104
- * Rejects paths under `agent-kit#<uuid>/` (versioned store payload).
105
- */
106
- export declare function resolveStableWpLauncher(options?: {
107
- readonly home?: string;
108
- readonly pathEnv?: string;
109
- readonly existsSync?: (path: string) => boolean;
110
- }): string | null;
111
- /** True when path lives inside a vite-plus installId package tree. */
112
- export declare function isVersionedAgentKitPayloadPath(filePath: string): boolean;
@@ -30,13 +30,17 @@
30
30
  * chain unresolvable after repair; 12 lease held; 13 ownership lost.
31
31
  * 7. Release the lease owner-checked in a finally.
32
32
  *
33
- * Post-swap import rule: the child `vp update -g @webpresso/agent-kit` may
34
- * delete this process's on-disk package tree (old installId) before probe/
35
- * repair run. Post-child helpers MUST be static imports loaded at process
36
- * start never `await import("#cli/…")` after the child exits, or Node
37
- * re-resolves against a removed path (`Cannot find module …/global-vp.js`).
38
- * Probe/repair logic itself reads the vite-plus store SSOT + PATH, not this
39
- * process's dying package root.
33
+ * Survival layers (in-repo SOTA bar):
34
+ * 1. Prefer a **stable** non-versioned launcher when re-execing the guard
35
+ * (`#cli/auto-update/self-invocation.js`) primary defense.
36
+ * 2. Self-package helpers (probe, repair, tombstone) MUST be static imports
37
+ * loaded at process start never package-local dynamic import, or Node
38
+ * re-resolves against a removed installId path after `vp update -g`.
39
+ * 3. Argument builders depend on cycle-free leaves (self-invocation,
40
+ * guard-tombstone), never on this command module solely for argv.
41
+ *
42
+ * Probe/repair reads the vite-plus store SSOT + PATH, not this process's
43
+ * dying package root.
40
44
  *
41
45
  * Custody boundary (stated exactly, mirrored from install-lock.ts): a stale,
42
46
  * OS-suspended holder whose child completes a mutation AFTER the successor
@@ -45,13 +49,15 @@
45
49
  * resource-side fencing.
46
50
  */
47
51
  import { spawn } from "node:child_process";
48
- import { existsSync } from "node:fs";
49
- import path from "node:path";
50
52
  import { buildVpGlobalInstallCommand, resolveBundledVpCommand, } from "#cli/auto-update/detect-pm.js";
53
+ import { writeGuardTombstone } from "#cli/auto-update/guard-tombstone.js";
51
54
  import { INSTALL_LOCK_HEARTBEAT_MS, claimInstallLock, refreshInstallLock, releaseInstallLock, resolveInstallLockPath, } from "#cli/auto-update/install-lock.js";
55
+ import { buildSelfInvocationCommand, isVersionedAgentKitPayloadPath, resolveStableWpLauncher, } from "#cli/auto-update/self-invocation.js";
52
56
  import { probeGlobalWpResolvable } from "#cli/commands/init/package-root.js";
53
57
  import { resolveGlobalCapableVpCommand } from "#cli/global-vp.js";
54
58
  import { resolveEffectiveRunnerRoot } from "#tool-runtime";
59
+ /** Re-export leaf helpers for public ergonomics / existing imports. */
60
+ export { buildSelfInvocationCommand, isVersionedAgentKitPayloadPath, resolveStableWpLauncher };
55
61
  export function registerSelfInstallGuardCommand(cli) {
56
62
  cli
57
63
  .command("self-install-guard [...args]", "Serialize and verify a global agent-kit self-install (internal)")
@@ -147,56 +153,6 @@ function extractChildCommand(rawArgs) {
147
153
  return [];
148
154
  return rawArgs.slice(separator + 1);
149
155
  }
150
- /**
151
- * How wp re-invokes itself:
152
- * - Prefer a **stable** global launcher (`~/.vite-plus/bin/wp` or PATH `wp`
153
- * outside an installId package tree). Re-execing `node …/agent-kit#id/…/cli.js`
154
- * dies after `vp update -g` deletes the pre-swap tree mid-guard.
155
- * - Bun-compiled binary: process.execPath IS the binary (argv[1] is /$bunfs).
156
- * - Source/dev lane without a stable launcher: `<runtime> <script>`.
157
- */
158
- export function buildSelfInvocationCommand(args, options = {}) {
159
- const stableWp = options.stableWp !== undefined ? options.stableWp : resolveStableWpLauncher();
160
- if (typeof stableWp === "string" && stableWp.length > 0) {
161
- return [stableWp, ...args];
162
- }
163
- const execPath = options.execPath ?? process.execPath;
164
- const argv1 = options.argv1 ?? process.argv[1] ?? "";
165
- if (argv1.startsWith("/$bunfs"))
166
- return [execPath, ...args];
167
- return [execPath, argv1, ...args];
168
- }
169
- /**
170
- * A launcher that survives installId package-tree deletion during global update.
171
- * Rejects paths under `agent-kit#<uuid>/` (versioned store payload).
172
- */
173
- export function resolveStableWpLauncher(options = {}) {
174
- const exists = options.existsSync ?? existsSync;
175
- const home = options.home ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
176
- const candidates = [];
177
- if (home) {
178
- candidates.push(path.join(home, ".vite-plus", "bin", "wp"));
179
- }
180
- const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
181
- for (const dir of pathEnv.split(path.delimiter)) {
182
- if (!dir)
183
- continue;
184
- candidates.push(path.join(dir, "wp"));
185
- }
186
- for (const candidate of candidates) {
187
- if (!exists(candidate))
188
- continue;
189
- if (isVersionedAgentKitPayloadPath(candidate))
190
- continue;
191
- return candidate;
192
- }
193
- return null;
194
- }
195
- /** True when path lives inside a vite-plus installId package tree. */
196
- export function isVersionedAgentKitPayloadPath(filePath) {
197
- const normalized = filePath.replaceAll("\\", "/");
198
- return /\/@webpresso\/agent-kit#[^/]+\//u.test(normalized);
199
- }
200
156
  function defaultSpawnChild(command) {
201
157
  const [executable, ...args] = command;
202
158
  // detached: own process group, so lost-ownership kills reap descendants.
@@ -251,8 +207,6 @@ async function defaultRunRepair() {
251
207
  return child.exited;
252
208
  }
253
209
  function writeObservabilityTombstone() {
254
- // Lazy import keeps the guard's cold path minimal; best-effort by contract.
255
- void import("#cli/auto-update/installer.js")
256
- .then(({ writeGuardTombstone }) => writeGuardTombstone())
257
- .catch(() => undefined);
210
+ // Static import only leaf module; see survival layers in the file header.
211
+ writeGuardTombstone();
258
212
  }
@@ -6,9 +6,10 @@ import { isDirectEntrypoint } from "#hooks/shared/direct-entrypoint";
6
6
  import { runHook } from "#hooks/shared/hook-bootstrap";
7
7
  import { resolveHookProjectDir } from "#hooks/shared/project-dir.js";
8
8
  import { createManagedHookExecutionContext, reportSessionMemoryHookDegradation, sessionMemoryHookStoreOptions, } from "#hooks/shared/session-memory-deadline.js";
9
+ import { resolveHookSessionId } from "#hooks/shared/types.js";
9
10
  import { getSurfacePath, NotInGitRepoError } from "#paths/state-root.js";
10
11
  import { buildContinuityEvent, buildPromptContinuityEvents } from "#session-memory/hook-capture.js";
11
- import { repoHashFromRoot } from "#session-memory/repo-hash.js";
12
+ import { computeRepoHash, repoHashFromRoot } from "#session-memory/repo-hash.js";
12
13
  import { SessionMemorySessionStore, } from "#session-memory/session.js";
13
14
  import { classifyPromptRoutingIntent, } from "#hooks/prompt-routing";
14
15
  import { setActivePromptRouting, setGuardEnabled } from "./state.js";
@@ -67,13 +68,15 @@ function capturePromptContinuity(input, projectDir, env, deps, routing) {
67
68
  const store = deps.createStore?.(dbPath, deps.storeOptions) ??
68
69
  new SessionMemorySessionStore(dbPath, deps.storeOptions);
69
70
  try {
70
- const repoHash = deps.repoHash?.(projectDir) ?? repoHashFromRoot(projectDir);
71
+ // Match readCurrentSessionId hashing (computeRepoHash), not path-only.
72
+ const repoHash = deps.repoHash?.(projectDir) ?? computeRepoHash(projectDir);
71
73
  const timestamp = (deps.now?.() ?? new Date()).toISOString();
74
+ const sessionId = resolveHookSessionId(input, env);
72
75
  for (const event of events) {
73
76
  store.captureEvent({
74
77
  repoHash,
75
78
  agentId: input.agent_id ?? input.agent_type ?? "UserPromptSubmit",
76
- ...(input.session_id === undefined ? {} : { sessionId: input.session_id }),
79
+ ...(sessionId === undefined ? {} : { sessionId }),
77
80
  event: {
78
81
  ...event,
79
82
  ts: timestamp,
@@ -124,13 +127,14 @@ export function processGuardSwitchInput(inputValue, cwd, env = process.env, deps
124
127
  reason: "🛡️ Guard enabled — pretool validators active",
125
128
  };
126
129
  }
130
+ const routingSessionId = resolveHookSessionId(input, env);
127
131
  const routing = normalized.length === 0
128
132
  ? null
129
133
  : {
130
134
  intent: deps.classifyPromptRoutingIntent?.(normalized) ??
131
135
  classifyPromptRoutingIntent(normalized),
132
136
  updatedAt: (deps.now?.() ?? new Date()).toISOString(),
133
- ...(input.session_id === undefined ? {} : { sessionId: input.session_id }),
137
+ ...(routingSessionId === undefined ? {} : { sessionId: routingSessionId }),
134
138
  ...(input.turn_id === undefined ? {} : { turnId: input.turn_id }),
135
139
  };
136
140
  if (routing !== null) {