@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.
- package/catalog/AGENTS.md.tpl +6 -10
- package/catalog/agent/commands/verify.md +2 -2
- package/catalog/agent/rules/changeset-release.md +4 -2
- package/catalog/agent/rules/workflow-skills-routing.md +6 -3
- package/catalog/agent/skills/autopilot/SKILL.md +48 -11
- package/catalog/agent/skills/claude/SKILL.md +7 -0
- package/catalog/agent/skills/codex/SKILL.md +7 -0
- package/catalog/agent/skills/fix/SKILL.md +33 -8
- package/catalog/agent/skills/grok/SKILL.md +7 -0
- package/catalog/agent/skills/investigate/SKILL.md +12 -1
- package/catalog/agent/skills/pll/SKILL.md +53 -48
- package/catalog/agent/skills/team/SKILL.md +26 -9
- package/catalog/agent/skills/ultragoal/SKILL.md +147 -19
- package/catalog/agent/skills/verify/SKILL.md +71 -51
- package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +8 -0
- package/dist/esm/audit/blueprint-lifecycle-sql.js +25 -1
- package/dist/esm/ci/native-session-memory-cache.d.ts +9 -1
- package/dist/esm/ci/native-session-memory-cache.js +9 -1
- package/dist/esm/ci/vitest-ci-shards.d.ts +6 -4
- package/dist/esm/ci/vitest-ci-shards.js +6 -4
- package/dist/esm/cli/auto-update/guard-tombstone.d.ts +25 -0
- package/dist/esm/cli/auto-update/guard-tombstone.js +67 -0
- package/dist/esm/cli/auto-update/installer.d.ts +4 -17
- package/dist/esm/cli/auto-update/installer.js +6 -57
- package/dist/esm/cli/auto-update/self-invocation.d.ts +37 -0
- package/dist/esm/cli/auto-update/self-invocation.js +62 -0
- package/dist/esm/cli/commands/audit.js +1 -0
- package/dist/esm/cli/commands/dash/index.js +3 -3
- package/dist/esm/cli/commands/dash/plan-once-attention.d.ts +32 -0
- package/dist/esm/cli/commands/dash/plan-once-attention.js +50 -0
- package/dist/esm/cli/commands/dash/plan-once-schema.d.ts +73 -0
- package/dist/esm/cli/commands/dash/plan-once-schema.js +119 -0
- package/dist/esm/cli/commands/dash/plan-once.d.ts +122 -0
- package/dist/esm/cli/commands/dash/plan-once.js +224 -0
- package/dist/esm/cli/commands/dash/tui/ipc-protocol.d.ts +87 -0
- package/dist/esm/cli/commands/dash/tui/ipc-protocol.js +128 -0
- package/dist/esm/cli/commands/dash/tui/native-host.d.ts +34 -0
- package/dist/esm/cli/commands/dash/tui/native-host.js +256 -0
- package/dist/esm/cli/commands/dash/tui/native-path.d.ts +11 -0
- package/dist/esm/cli/commands/dash/tui/native-path.js +41 -0
- package/dist/esm/cli/commands/dash/tui/runtime.js +72 -0
- package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.js +1 -1
- package/dist/esm/cli/commands/package-manager.js +2 -1
- package/dist/esm/cli/commands/self-install-guard.d.ts +14 -32
- package/dist/esm/cli/commands/self-install-guard.js +17 -63
- package/dist/esm/hooks/guard-switch/index.js +8 -4
- package/dist/esm/hooks/post-tool/lint-after-edit.js +6 -4
- package/dist/esm/hooks/precompact/index.js +5 -3
- package/dist/esm/hooks/sessionstart/index.d.ts +10 -0
- package/dist/esm/hooks/sessionstart/index.js +61 -4
- package/dist/esm/hooks/shared/types.d.ts +11 -0
- package/dist/esm/hooks/shared/types.js +21 -0
- package/dist/esm/paths/state-root.js +6 -0
- package/dist/esm/session-memory/current-session.js +2 -0
- package/dist/esm/session-memory/native-runtime.js +5 -3
- package/dist/esm/session-memory/native-warm-markers.d.ts +18 -0
- package/dist/esm/session-memory/native-warm-markers.js +22 -0
- package/dist/esm/session-memory/sync/types.d.ts +4 -0
- package/dist/esm/session-memory/types.d.ts +3 -1
- package/dist/esm/session-memory/types.js +2 -0
- package/dist/esm/ultragoal/runtime.js +44 -2
- 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/
|
|
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 {
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
//
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
-
...(
|
|
137
|
+
...(routingSessionId === undefined ? {} : { sessionId: routingSessionId }),
|
|
134
138
|
...(input.turn_id === undefined ? {} : { turnId: input.turn_id }),
|
|
135
139
|
};
|
|
136
140
|
if (routing !== null) {
|