@kontextmind/kxm 0.7.0 → 0.7.10
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/.claude-plugin/marketplace.json +1 -1
- package/.kxm/roles/writer.yaml +2 -0
- package/docs/README.md +3 -0
- package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
- package/docs/agent-skills.md +19 -2
- package/docs/browser-automation.md +116 -0
- package/docs/configuration.md +10 -1
- package/docs/getting-started.md +21 -0
- package/docs/kb/how-credentials-retrieved-safely.md +31 -0
- package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
- package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
- package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
- package/docs/kb/how-to-resume-after-mfa.md +28 -0
- package/docs/kb/how-to-take-over-session.md +32 -0
- package/docs/kb/why-authentication-disappeared.md +32 -0
- package/docs/kb/why-automation-opened-different-browser.md +32 -0
- package/docs/kb/why-session-viewer-cannot-control.md +31 -0
- package/docs/operations.md +24 -0
- package/docs/prompts/browser-annotate-feedback.md +41 -0
- package/docs/prompts/browser-diagnose-recover.md +38 -0
- package/docs/prompts/browser-explore.md +42 -0
- package/docs/prompts/browser-repro-fix.md +48 -0
- package/docs/prompts/browser-start.md +41 -0
- package/docs/prompts/browser-takeover.md +50 -0
- package/docs/skills/repo-work-delivery.md +5 -0
- package/docs/skills.md +2 -0
- package/docs/troubleshooting.md +22 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +41203 -38364
- package/plugins/kxm/dist/core.js +57 -0
- package/plugins/kxm/dist/extension.js +40 -3
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime.js +1582 -81
- package/plugins/kxm/dist/server.js +129 -4
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +221 -41
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/hints.json +30 -0
- package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
- package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
- package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
- package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +9 -0
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +9 -2
- package/plugins/kxm/src/autocomplete.ts +1 -1
- package/plugins/kxm/src/browser.ts +603 -0
- package/plugins/kxm/src/cli/context-skills.ts +373 -0
- package/plugins/kxm/src/cli/hub.ts +614 -0
- package/plugins/kxm/src/cli/roles.ts +615 -0
- package/plugins/kxm/src/cli/system.ts +906 -0
- package/plugins/kxm/src/cli/tasks.ts +364 -0
- package/plugins/kxm/src/cli/types.ts +270 -0
- package/plugins/kxm/src/cli/vnext.ts +698 -0
- package/plugins/kxm/src/cli/workflows.ts +699 -0
- package/plugins/kxm/src/cli.ts +238 -3791
- package/plugins/kxm/src/completion-install.ts +223 -0
- package/plugins/kxm/src/database.ts +1 -1
- package/plugins/kxm/src/external-effects.ts +1 -1
- package/plugins/kxm/src/hub-env.ts +193 -0
- package/plugins/kxm/src/init-guide-setup.ts +547 -0
- package/plugins/kxm/src/local-snapshot.ts +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/model-inventory.ts +8 -8
- package/plugins/kxm/src/modes.ts +348 -0
- package/plugins/kxm/src/protocol.ts +111 -0
- package/plugins/kxm/src/role.ts +335 -0
- package/plugins/kxm/src/runtime.ts +4 -0
- package/plugins/kxm/src/safety-integrity.ts +76 -0
- package/plugins/kxm/src/sqlite.ts +76 -0
- package/plugins/kxm/src/ssh-remote.ts +560 -0
- package/plugins/kxm/src/store.ts +1 -1
- package/plugins/kxm/src/subagent-control.ts +312 -0
- package/plugins/kxm/src/vnext-bindings.ts +1 -1
- package/plugins/kxm/src/vnext-config.ts +38 -1
- package/plugins/kxm/src/vnext-engine-command.ts +2 -0
- package/plugins/kxm/src/vnext-engine.ts +16 -0
- package/plugins/kxm/src/vnext-harness.ts +92 -22
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +39 -7
- package/plugins/kxm/src/vnext-oneshot-process.ts +46 -8
- package/plugins/kxm/src/vnext-oneshot-producer.ts +22 -1
- package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
- package/plugins/kxm/src/vnext-runtime-store.ts +1 -1
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +5 -3
- package/plugins/kxm/src/workflow-tui.ts +1 -1
- package/plugins/kxm/src/workflow.ts +144 -0
- package/schemas/vnext/modes.schema.json +56 -0
- package/scripts/kxm-bump-version.mjs +146 -0
- package/scripts/kxm-hub.mjs +145 -3
- package/scripts/kxm-publish-npm.mjs +3 -1
- package/scripts/kxm-release-github.mjs +3 -1
- package/scripts/kxm.mjs +0 -0
|
@@ -3,10 +3,37 @@ import { lstat, mkdir, open, rename } from "node:fs/promises";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { VnextOneShotProcessResult } from "./vnext-oneshot-process.ts";
|
|
5
5
|
|
|
6
|
+
export const ONESHOT_EVIDENCE_SCHEMA = "kxm.oneshot-evidence.v2" as const;
|
|
7
|
+
const RETIRED_ONESHOT_EVIDENCE_SCHEMAS = new Set(["kxm.oneshot-evidence.v1"]);
|
|
8
|
+
|
|
6
9
|
const TEXT_LIMIT = 4 * 1024 * 1024;
|
|
10
|
+
const ARGV_LIMIT = 64 * 1024;
|
|
7
11
|
const RECORD_LIMIT = 16 * 1024 * 1024;
|
|
8
12
|
const digest = (value: string): string => `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
9
13
|
|
|
14
|
+
export interface OneShotEvidenceRecord {
|
|
15
|
+
schema: typeof ONESHOT_EVIDENCE_SCHEMA;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Brake: retired and unknown persisted schema ids are never aliased or upgraded. */
|
|
20
|
+
export function parseOneShotEvidenceRecord(bytes: string): OneShotEvidenceRecord {
|
|
21
|
+
let parsed: unknown;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(bytes) as unknown;
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error("oneshot_evidence_record_invalid");
|
|
26
|
+
}
|
|
27
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
28
|
+
throw new Error("oneshot_evidence_record_invalid");
|
|
29
|
+
}
|
|
30
|
+
const schema = (parsed as { schema?: unknown }).schema;
|
|
31
|
+
if (typeof schema !== "string") throw new Error("oneshot_evidence_record_invalid");
|
|
32
|
+
if (RETIRED_ONESHOT_EVIDENCE_SCHEMAS.has(schema)) throw new Error("oneshot_evidence_schema_retired");
|
|
33
|
+
if (schema !== ONESHOT_EVIDENCE_SCHEMA) throw new Error("oneshot_evidence_schema_unknown");
|
|
34
|
+
return parsed as OneShotEvidenceRecord;
|
|
35
|
+
}
|
|
36
|
+
|
|
10
37
|
/** Private diagnostic evidence, never a peer reply or acceptance authority.
|
|
11
38
|
* Retained until operator archival; no automatic deletion of failed attempts.
|
|
12
39
|
* Each record is bounded; inability to reserve/write evidence fails closed.
|
|
@@ -32,11 +59,13 @@ export async function beginOneShotEvidence(root: string, intent: {
|
|
|
32
59
|
const redact = (value: string): string => {
|
|
33
60
|
let text = value;
|
|
34
61
|
for (const secret of secrets) text = text.replaceAll(secret, "[REDACTED]");
|
|
35
|
-
return text
|
|
62
|
+
return text
|
|
63
|
+
.replace(/(\b(?:authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|password|secret)\b["']?\s*[:=]\s*["']?)(?:Bearer\s+)?[^\s"',;}]+/gi, "$1[REDACTED]")
|
|
64
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]");
|
|
36
65
|
};
|
|
37
|
-
const bounded = (value: string): { text: string; truncated: boolean; sha256: string } => {
|
|
66
|
+
const bounded = (value: string, max = TEXT_LIMIT): { text: string; truncated: boolean; sha256: string } => {
|
|
38
67
|
const bytes = Buffer.from(redact(value));
|
|
39
|
-
return { text: bytes.subarray(0,
|
|
68
|
+
return { text: bytes.subarray(0, max).toString("utf8"), truncated: bytes.length > max, sha256: digest(redact(value)) };
|
|
40
69
|
};
|
|
41
70
|
const write = async (name: string, value: unknown): Promise<string> => {
|
|
42
71
|
const current = await lstat(dir);
|
|
@@ -46,6 +75,7 @@ export async function beginOneShotEvidence(root: string, intent: {
|
|
|
46
75
|
}
|
|
47
76
|
const bytes = JSON.stringify(value);
|
|
48
77
|
if (Buffer.byteLength(bytes) > RECORD_LIMIT) throw new Error("oneshot_evidence_record_limit");
|
|
78
|
+
parseOneShotEvidenceRecord(bytes);
|
|
49
79
|
const target = join(dir, name);
|
|
50
80
|
const partial = `${target}.partial`;
|
|
51
81
|
const file = await open(partial, "wx", 0o600);
|
|
@@ -59,11 +89,12 @@ export async function beginOneShotEvidence(root: string, intent: {
|
|
|
59
89
|
return digest(bytes);
|
|
60
90
|
};
|
|
61
91
|
await write("intent.json", {
|
|
62
|
-
schema:
|
|
92
|
+
schema: ONESHOT_EVIDENCE_SCHEMA, id, recordedAt: new Date().toISOString(),
|
|
63
93
|
runId: intent.runId, stepId: intent.stepId, attemptId: intent.attemptId, assignmentId: intent.assignmentId,
|
|
64
94
|
harness: intent.harness, provider: intent.provider, requestedModel: intent.model,
|
|
65
|
-
cwd: intent.cwd, command: intent.command,
|
|
66
|
-
argv: bounded(JSON.stringify(intent.args)), stdin: intent.input === undefined ? null : bounded(intent.input),
|
|
95
|
+
cwd: intent.cwd, command: redact(intent.command),
|
|
96
|
+
argv: bounded(JSON.stringify(intent.args), ARGV_LIMIT), stdin: intent.input === undefined ? null : bounded(intent.input),
|
|
97
|
+
env: null,
|
|
67
98
|
acceptance: false,
|
|
68
99
|
});
|
|
69
100
|
let finished = false;
|
|
@@ -73,10 +104,11 @@ export async function beginOneShotEvidence(root: string, intent: {
|
|
|
73
104
|
if (finished) throw new Error("oneshot_evidence_already_finished");
|
|
74
105
|
finished = true;
|
|
75
106
|
return write("result.json", {
|
|
76
|
-
schema:
|
|
107
|
+
schema: ONESHOT_EVIDENCE_SCHEMA, id, recordedAt: new Date().toISOString(),
|
|
77
108
|
stdout: bounded(result.stdout), stderr: bounded(result.stderr),
|
|
78
109
|
code: result.code, signal: result.signal ?? null, started: result.started ?? null,
|
|
79
110
|
observedChildExit: result.observedChildExit ?? null, terminationRequested: result.terminationRequested ?? null,
|
|
111
|
+
unverifiedDescendants: result.unverifiedDescendants ?? null,
|
|
80
112
|
error: result.error ? bounded(result.error.message) : null,
|
|
81
113
|
observation: bounded(JSON.stringify(observation)), acceptance: false,
|
|
82
114
|
});
|
|
@@ -9,6 +9,11 @@ export interface VnextOneShotProcessResult {
|
|
|
9
9
|
started?: boolean | undefined;
|
|
10
10
|
/** Stop was requested; direct-child reaping cannot attest escaped descendants. */
|
|
11
11
|
terminationRequested?: boolean | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Direct-child close/reap is not descendant death. Unobserved process-group
|
|
14
|
+
* members stay unverified: never success, never a forged descendant failure.
|
|
15
|
+
*/
|
|
16
|
+
unverifiedDescendants?: boolean | undefined;
|
|
12
17
|
error?: Error | undefined;
|
|
13
18
|
}
|
|
14
19
|
|
|
@@ -18,6 +23,8 @@ export interface VnextOneShotSpawnOptions {
|
|
|
18
23
|
input?: string | undefined;
|
|
19
24
|
timeoutMs?: number | undefined;
|
|
20
25
|
signal?: AbortSignal | undefined;
|
|
26
|
+
/** Only allowlisted Windows `.cmd` probes may set this; never a user string. */
|
|
27
|
+
shell?: boolean | undefined;
|
|
21
28
|
}
|
|
22
29
|
export type VnextOneShotSpawn = (command: string, args: readonly string[], options: VnextOneShotSpawnOptions) => Promise<VnextOneShotProcessResult>;
|
|
23
30
|
|
|
@@ -26,6 +33,26 @@ const KILL_GRACE_MS = 250;
|
|
|
26
33
|
const DRAIN_GRACE_MS = 500;
|
|
27
34
|
const REAP_GRACE_MS = 1000;
|
|
28
35
|
|
|
36
|
+
/**
|
|
37
|
+
* POSIX process-group tree kill with fallback to direct child kill.
|
|
38
|
+
* Ensures orphaned subshells, test workers, and background daemons are pruned on cancellation/timeout.
|
|
39
|
+
*/
|
|
40
|
+
export function killProcessTree(child: ReturnType<typeof spawn>, signal: NodeJS.Signals = "SIGKILL"): void {
|
|
41
|
+
if (process.platform !== "win32" && child.pid) {
|
|
42
|
+
try {
|
|
43
|
+
process.kill(-child.pid, signal);
|
|
44
|
+
return;
|
|
45
|
+
} catch {
|
|
46
|
+
// Fall through to direct child kill if process group is unavailable
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
child.kill(signal);
|
|
51
|
+
} catch {
|
|
52
|
+
// Direct kill error if child already exited
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
/** Bound both execution and pipe drain. Observing exit is not proof of descendant death. */
|
|
30
57
|
export function defaultSpawn(command: string, args: readonly string[], options: VnextOneShotSpawnOptions): Promise<VnextOneShotProcessResult> {
|
|
31
58
|
if (options.signal?.aborted) {
|
|
@@ -51,6 +78,9 @@ export function defaultSpawn(command: string, args: readonly string[], options:
|
|
|
51
78
|
let child: ReturnType<typeof spawn>;
|
|
52
79
|
const finish = (): void => {
|
|
53
80
|
if (finished) return;
|
|
81
|
+
// Direct-child close must not cancel SIGTERM → SIGKILL. Refuse to settle
|
|
82
|
+
// until the group kill has been sent, then reap on the existing timers.
|
|
83
|
+
if (stopping && !killSent) return;
|
|
54
84
|
finished = true;
|
|
55
85
|
for (const timer of [wallTimer, killTimer, drainTimer, reapTimer]) clearTimeout(timer);
|
|
56
86
|
options.signal?.removeEventListener("abort", onAbort);
|
|
@@ -60,13 +90,21 @@ export function defaultSpawn(command: string, args: readonly string[], options:
|
|
|
60
90
|
child?.unref();
|
|
61
91
|
const started = Boolean(child?.pid);
|
|
62
92
|
if (started && !observedChildExit) error ??= new Error("process_exit_unobserved");
|
|
63
|
-
|
|
93
|
+
const unverifiedDescendants = stopping || (started && !observedChildExit);
|
|
94
|
+
resolve({
|
|
95
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
96
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
97
|
+
code,
|
|
98
|
+
signal,
|
|
99
|
+
started,
|
|
100
|
+
observedChildExit,
|
|
101
|
+
terminationRequested: stopping,
|
|
102
|
+
...(unverifiedDescendants ? { unverifiedDescendants: true } : {}),
|
|
103
|
+
...(error ? { error } : {}),
|
|
104
|
+
});
|
|
64
105
|
};
|
|
65
106
|
const kill = (requested: NodeJS.Signals): void => {
|
|
66
|
-
|
|
67
|
-
if (process.platform !== "win32" && child.pid) process.kill(-child.pid, requested);
|
|
68
|
-
else child.kill(requested);
|
|
69
|
-
} catch { /* Reaping, not kill() success, establishes direct-child exit. */ }
|
|
107
|
+
killProcessTree(child, requested);
|
|
70
108
|
};
|
|
71
109
|
const stop = (reason: string): void => {
|
|
72
110
|
if (finished) return;
|
|
@@ -91,7 +129,7 @@ export function defaultSpawn(command: string, args: readonly string[], options:
|
|
|
91
129
|
cwd: options.cwd,
|
|
92
130
|
env: options.env ?? process.env,
|
|
93
131
|
stdio: ["pipe", "pipe", "pipe"],
|
|
94
|
-
shell:
|
|
132
|
+
shell: options.shell === true,
|
|
95
133
|
windowsHide: true,
|
|
96
134
|
detached: process.platform !== "win32",
|
|
97
135
|
});
|
|
@@ -136,8 +174,8 @@ export function defaultSpawn(command: string, args: readonly string[], options:
|
|
|
136
174
|
code = exitCode;
|
|
137
175
|
signal = exitSignal;
|
|
138
176
|
// The leader can exit and close its pipes while a descendant ignores TERM.
|
|
139
|
-
//
|
|
140
|
-
|
|
177
|
+
// finish() no-ops until SIGKILL when escalation is in flight.
|
|
178
|
+
finish();
|
|
141
179
|
});
|
|
142
180
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
143
181
|
if (options.signal?.aborted) onAbort();
|
|
@@ -195,6 +195,24 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
|
|
|
195
195
|
args = ["--model", resolved.model,
|
|
196
196
|
...(resolved.thinking ? ["--reasoning-effort", resolved.thinking] : []),
|
|
197
197
|
...permissionArgs, "--output-format", "json"];
|
|
198
|
+
} else if (harness === "agy") {
|
|
199
|
+
args = [
|
|
200
|
+
"-m",
|
|
201
|
+
resolved.model,
|
|
202
|
+
...permissionArgs,
|
|
203
|
+
"--output-format",
|
|
204
|
+
"json",
|
|
205
|
+
"-p",
|
|
206
|
+
];
|
|
207
|
+
} else if (harness === "kimi") {
|
|
208
|
+
args = [
|
|
209
|
+
"-m",
|
|
210
|
+
resolved.model,
|
|
211
|
+
...permissionArgs,
|
|
212
|
+
"--output-format",
|
|
213
|
+
"stream-json",
|
|
214
|
+
"-p",
|
|
215
|
+
];
|
|
198
216
|
} else {
|
|
199
217
|
throw new Error(`oneshot_harness_unsupported: ${harness} permission_profile_unaudited`);
|
|
200
218
|
}
|
|
@@ -234,9 +252,11 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
|
|
|
234
252
|
const aborted = request.signal.aborted || procResult.error?.message === "process_aborted";
|
|
235
253
|
// Process groups are cleanup, not containment. A detached descendant or
|
|
236
254
|
// provider leader may outlive the client; interrupted effects need recovery.
|
|
237
|
-
const
|
|
255
|
+
const unverifiedDescendants = procResult.unverifiedDescendants === true
|
|
256
|
+
|| procResult.terminationRequested === true
|
|
238
257
|
|| procResult.error?.message === "process_exit_unobserved"
|
|
239
258
|
|| (procResult.observedChildExit === false && procResult.started !== false);
|
|
259
|
+
const effectUncertain = unverifiedDescendants || Boolean(procResult.signal);
|
|
240
260
|
const transportFailed = procResult.code !== 0 || Boolean(procResult.error) || effectUncertain;
|
|
241
261
|
const outcome = aborted ? "cancelled" : transportFailed || parsed.isError ? "failed"
|
|
242
262
|
: determineOutcome(parsed.text, request.allowedOutcomes);
|
|
@@ -249,6 +269,7 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
|
|
|
249
269
|
if (procResult.observedChildExit !== undefined) providerMetadata.observedChildExit = procResult.observedChildExit;
|
|
250
270
|
if (procResult.started !== undefined) providerMetadata.processStarted = procResult.started;
|
|
251
271
|
if (procResult.terminationRequested !== undefined) providerMetadata.terminationRequested = procResult.terminationRequested;
|
|
272
|
+
if (unverifiedDescendants) providerMetadata.descendantEffects = "unverified";
|
|
252
273
|
if (procResult.error) {
|
|
253
274
|
const reason = procResult.error.message;
|
|
254
275
|
providerMetadata.processError = /^process_(aborted|timeout|output_limit|stdin_error|stdio_error|stdio_unclosed|exit_unobserved)$/.test(reason) ? reason : "process_error";
|
|
@@ -8,8 +8,10 @@ import {
|
|
|
8
8
|
type VnextProducerResult,
|
|
9
9
|
} from "./vnext-engine.ts";
|
|
10
10
|
import {
|
|
11
|
-
|
|
11
|
+
probeHarnessAssignmentAsync,
|
|
12
|
+
type HarnessAssignmentProbeOptions,
|
|
12
13
|
type HarnessInventory,
|
|
14
|
+
type HarnessStatus,
|
|
13
15
|
} from "./vnext-harness.ts";
|
|
14
16
|
import {
|
|
15
17
|
calculateModelCost,
|
|
@@ -386,7 +388,7 @@ export interface VnextPiProducerOptions {
|
|
|
386
388
|
priceCatalog?: PriceCatalog | undefined;
|
|
387
389
|
env?: NodeJS.ProcessEnv | undefined;
|
|
388
390
|
spawnProcess?: ((command: string, args: readonly string[], options: Record<string, unknown>) => PiRpcProcess) | undefined;
|
|
389
|
-
probeHarness?:
|
|
391
|
+
probeHarness?: ((options: Omit<HarnessAssignmentProbeOptions, "runCommand"> & { signal?: AbortSignal | undefined }) => HarnessStatus | Promise<HarnessStatus>) | undefined;
|
|
390
392
|
resolveModel?: ((agentId: string, runId: string) => { provider?: string | undefined; model?: string | undefined; thinking?: string | undefined } | undefined) | undefined;
|
|
391
393
|
defaultModel?: string | undefined;
|
|
392
394
|
defaultProvider?: string | undefined;
|
|
@@ -438,7 +440,7 @@ export function createVnextPiProducer(options: VnextPiProducerOptions = {}): Vne
|
|
|
438
440
|
return { provider: parsed.provider, model: parsed.model };
|
|
439
441
|
}
|
|
440
442
|
|
|
441
|
-
function checkPiAuth(provider: string, model: string): void {
|
|
443
|
+
async function checkPiAuth(provider: string, model: string, signal: AbortSignal): Promise<void> {
|
|
442
444
|
if (options.inventory) {
|
|
443
445
|
const piEntry = options.inventory.harnesses.find((h) => h.id === "pi");
|
|
444
446
|
if (!piEntry || !piEntry.detected || piEntry.authenticated === false) {
|
|
@@ -446,12 +448,14 @@ export function createVnextPiProducer(options: VnextPiProducerOptions = {}): Vne
|
|
|
446
448
|
}
|
|
447
449
|
}
|
|
448
450
|
|
|
449
|
-
const probeFn = options.probeHarness ??
|
|
450
|
-
const probe = probeFn({
|
|
451
|
+
const probeFn = options.probeHarness ?? probeHarnessAssignmentAsync;
|
|
452
|
+
const probe = await probeFn({
|
|
451
453
|
harness: "pi",
|
|
452
454
|
provider,
|
|
453
455
|
model,
|
|
454
456
|
env: options.env,
|
|
457
|
+
signal,
|
|
458
|
+
timeoutMs: 10_000,
|
|
455
459
|
});
|
|
456
460
|
|
|
457
461
|
if (!probe.detected) {
|
|
@@ -538,8 +542,8 @@ export function createVnextPiProducer(options: VnextPiProducerOptions = {}): Vne
|
|
|
538
542
|
const startTime = Date.now();
|
|
539
543
|
const resolved = resolveModelForRequest(request);
|
|
540
544
|
|
|
541
|
-
// Preflight auth check - fails closed
|
|
542
|
-
checkPiAuth(resolved.provider, resolved.model);
|
|
545
|
+
// Preflight auth check - fails closed via the async bounded probe
|
|
546
|
+
await checkPiAuth(resolved.provider, resolved.model, request.signal);
|
|
543
547
|
|
|
544
548
|
// Get or create session
|
|
545
549
|
const session = await getOrCreateSession(request, resolved.model);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { DatabaseSync } from "
|
|
4
|
+
import { DatabaseSync } from "./sqlite.ts";
|
|
5
5
|
import { VnextConfigError, validateRunEvent, vnextCanonicalJson, type JsonValue, type VnextConfigIssue, type VnextConfigOptions } from "./vnext-config.ts";
|
|
6
6
|
import { vnextUserStateRoot } from "./vnext-bindings.ts";
|
|
7
7
|
|
|
@@ -476,7 +476,7 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
476
476
|
});
|
|
477
477
|
})();
|
|
478
478
|
|
|
479
|
-
let drivePromise: Promise<unknown
|
|
479
|
+
let drivePromise: Promise<unknown> | undefined;
|
|
480
480
|
let earlyError: unknown;
|
|
481
481
|
try {
|
|
482
482
|
const scheduler = VnextRunScheduler.for(context, bundle);
|
|
@@ -484,7 +484,6 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
484
484
|
drivePromise.catch((err) => { earlyError = err; });
|
|
485
485
|
} catch (err) {
|
|
486
486
|
earlyError = err;
|
|
487
|
-
drivePromise = Promise.reject(err);
|
|
488
487
|
}
|
|
489
488
|
|
|
490
489
|
// Yield microtask to catch synchronous duplicate queue check (e.g. run_busy)
|
|
@@ -503,11 +502,14 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
503
502
|
throw earlyError;
|
|
504
503
|
}
|
|
505
504
|
|
|
506
|
-
void drivePromise
|
|
505
|
+
void drivePromise!.finally(async () => {
|
|
507
506
|
cleanupActive();
|
|
508
507
|
if ("close" in producer && typeof producer.close === "function") {
|
|
509
508
|
try { await producer.close(); } catch { /* ignore */ }
|
|
510
509
|
}
|
|
510
|
+
}).catch(() => {
|
|
511
|
+
// Derived finally() re-rejects when the admitted drive or cleanup
|
|
512
|
+
// throws; void does not consume that. Keep the request 202.
|
|
511
513
|
});
|
|
512
514
|
|
|
513
515
|
sendJson(response, 202, {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
-
import { DatabaseSync } from "
|
|
8
|
+
import { DatabaseSync } from "./sqlite.ts";
|
|
9
9
|
import { repoConfigDirectory } from "./config.ts";
|
|
10
10
|
import { readRoutingRecords, telemetryPath } from "./telemetry.ts";
|
|
11
11
|
|
|
@@ -3,11 +3,17 @@ import {
|
|
|
3
3
|
MAX_MESSAGE_TTL_MS,
|
|
4
4
|
MIN_MESSAGE_TTL_MS,
|
|
5
5
|
ProtocolError,
|
|
6
|
+
TERMINAL_RECEIPT_SCHEMA,
|
|
6
7
|
newId,
|
|
7
8
|
requireString,
|
|
9
|
+
validateTerminalReceipt,
|
|
8
10
|
type ImprovementArea,
|
|
9
11
|
type JournalCategory,
|
|
10
12
|
type MessageRecord,
|
|
13
|
+
type TerminalReceipt,
|
|
14
|
+
type TerminalReceiptEvidence,
|
|
15
|
+
type TerminalReceiptMetrics,
|
|
16
|
+
type TerminalReceiptStatus,
|
|
11
17
|
type WorkflowCheckpointStatus,
|
|
12
18
|
type WorkflowEvidenceInput,
|
|
13
19
|
type WorkflowEvidenceReference,
|
|
@@ -18,11 +24,16 @@ import {
|
|
|
18
24
|
export type {
|
|
19
25
|
ImprovementArea,
|
|
20
26
|
JournalCategory,
|
|
27
|
+
TerminalReceipt,
|
|
28
|
+
TerminalReceiptEvidence,
|
|
29
|
+
TerminalReceiptMetrics,
|
|
30
|
+
TerminalReceiptStatus,
|
|
21
31
|
WorkflowCheckpointStatus,
|
|
22
32
|
WorkflowEvidenceInput,
|
|
23
33
|
WorkflowEvidenceReference,
|
|
24
34
|
WorkflowEvidenceReferenceInput,
|
|
25
35
|
} from "./protocol.ts";
|
|
36
|
+
export { TERMINAL_RECEIPT_SCHEMA, validateTerminalReceipt } from "./protocol.ts";
|
|
26
37
|
|
|
27
38
|
export type WorkflowRunStatus = "running" | "waiting" | "completed" | "failed";
|
|
28
39
|
export type WorkflowStageStatus = "pending" | "in_progress" | "waiting" | WorkflowCheckpointStatus;
|
|
@@ -153,6 +164,8 @@ export interface WorkflowStageDefinition {
|
|
|
153
164
|
instructions: string;
|
|
154
165
|
requiredEvidence: string[];
|
|
155
166
|
maxAttempts: number;
|
|
167
|
+
/** Bounded in-place retry limit before audit escalation (default: 2). */
|
|
168
|
+
autoResumeLimit?: number;
|
|
156
169
|
area?: ImprovementArea;
|
|
157
170
|
evidencePolicies?: WorkflowEvidencePolicies;
|
|
158
171
|
/** Typed outcome map (v0.5). Keys are outcome identities ("passed",
|
|
@@ -250,6 +263,13 @@ export interface WorkflowStageState extends WorkflowStageDefinition {
|
|
|
250
263
|
startedAt?: string;
|
|
251
264
|
completedAt?: string;
|
|
252
265
|
updatedAt?: string;
|
|
266
|
+
receipt?: TerminalReceipt;
|
|
267
|
+
auditEscalation?: {
|
|
268
|
+
reason: string;
|
|
269
|
+
timestamp: string;
|
|
270
|
+
ruling?: string;
|
|
271
|
+
receipt?: TerminalReceipt;
|
|
272
|
+
};
|
|
253
273
|
}
|
|
254
274
|
|
|
255
275
|
export interface CapturedOracle {
|
|
@@ -1003,12 +1023,18 @@ export function parseWorkflowDefinitions(
|
|
|
1003
1023
|
&& (!Number.isInteger(stageMaxTransitions) || (stageMaxTransitions as number) < 1 || (stageMaxTransitions as number) > 100)) {
|
|
1004
1024
|
throw new Error(`stage ${stageId} maxTransitions must be an integer between 1 and 100`);
|
|
1005
1025
|
}
|
|
1026
|
+
const autoResumeLimit = stage.autoResumeLimit;
|
|
1027
|
+
if (autoResumeLimit !== undefined
|
|
1028
|
+
&& (!Number.isInteger(autoResumeLimit) || (autoResumeLimit as number) < 1 || (autoResumeLimit as number) > 20)) {
|
|
1029
|
+
throw new Error(`stage ${stageId} autoResumeLimit must be an integer between 1 and 20`);
|
|
1030
|
+
}
|
|
1006
1031
|
return {
|
|
1007
1032
|
id: stageId,
|
|
1008
1033
|
label: requireString(stage.label ?? stageId, "stage.label", { max: 128 }),
|
|
1009
1034
|
instructions: requireString(stage.instructions, "stage.instructions", { max: 4_000 }),
|
|
1010
1035
|
requiredEvidence,
|
|
1011
1036
|
maxAttempts: maxAttempts as number,
|
|
1037
|
+
...(autoResumeLimit !== undefined ? { autoResumeLimit: autoResumeLimit as number } : {}),
|
|
1012
1038
|
...(area ? { area } : {}),
|
|
1013
1039
|
...(evidencePolicies ? { evidencePolicies } : {}),
|
|
1014
1040
|
...(on ? { on } : {}),
|
|
@@ -1377,6 +1403,31 @@ export function checkpointRun(
|
|
|
1377
1403
|
delete run.currentStage;
|
|
1378
1404
|
return { retry: false, completed: false, run };
|
|
1379
1405
|
}
|
|
1406
|
+
if (stage.autoResumeLimit !== undefined && stage.attempts >= stage.autoResumeLimit) {
|
|
1407
|
+
stage.status = "in_progress";
|
|
1408
|
+
const reason = summary || `autoResumeLimit of ${stage.autoResumeLimit} reached on stage ${stage.id}`;
|
|
1409
|
+
const receipt = validateTerminalReceipt({
|
|
1410
|
+
schema: TERMINAL_RECEIPT_SCHEMA,
|
|
1411
|
+
status: "audit_escalation",
|
|
1412
|
+
seat: stage.id,
|
|
1413
|
+
runId: run.id,
|
|
1414
|
+
stageId: stage.id,
|
|
1415
|
+
timestamp,
|
|
1416
|
+
host: "pi",
|
|
1417
|
+
model: "default",
|
|
1418
|
+
escalationReason: reason,
|
|
1419
|
+
});
|
|
1420
|
+
const expiresAt = new Date(Date.parse(timestamp) + 24 * 60 * 60 * 1000).toISOString();
|
|
1421
|
+
waitForWorkflowSignal(run, stage.id, "audit_escalation", reason, timestamp, expiresAt);
|
|
1422
|
+
stage.receipt = receipt;
|
|
1423
|
+
stage.auditEscalation = {
|
|
1424
|
+
reason,
|
|
1425
|
+
timestamp,
|
|
1426
|
+
receipt,
|
|
1427
|
+
};
|
|
1428
|
+
return { retry: false, completed: false, run };
|
|
1429
|
+
}
|
|
1430
|
+
stage.status = status;
|
|
1380
1431
|
// Declared failure outcomes create typed transitions (bounded); the
|
|
1381
1432
|
// outcome identity defaults to the checkpoint status.
|
|
1382
1433
|
const outcomeKey = outcome ?? status;
|
|
@@ -1546,3 +1597,96 @@ export function approveWorkflowDegradation(
|
|
|
1546
1597
|
run.updatedAt = timestamp;
|
|
1547
1598
|
return { run, approval, created: true };
|
|
1548
1599
|
}
|
|
1600
|
+
|
|
1601
|
+
export function escalateWorkflowStage(
|
|
1602
|
+
run: WorkflowRun,
|
|
1603
|
+
stageId: string,
|
|
1604
|
+
reason: string,
|
|
1605
|
+
timestamp: string,
|
|
1606
|
+
options: {
|
|
1607
|
+
seat?: string;
|
|
1608
|
+
host?: string;
|
|
1609
|
+
model?: string;
|
|
1610
|
+
evidence?: TerminalReceiptEvidence;
|
|
1611
|
+
metrics?: TerminalReceiptMetrics;
|
|
1612
|
+
} = {},
|
|
1613
|
+
): { run: WorkflowRun; receipt: TerminalReceipt } {
|
|
1614
|
+
if (run.status !== "running") throw new ProtocolError(409, `workflow is ${run.status}`, "workflow_terminal");
|
|
1615
|
+
const stage = run.stages.find((s) => s.id === stageId);
|
|
1616
|
+
if (!stage) throw new ProtocolError(404, `workflow stage not found: ${stageId}`, "workflow_stage_not_found");
|
|
1617
|
+
if (stage.id !== run.currentStage || stage.status !== "in_progress") {
|
|
1618
|
+
throw new ProtocolError(409, `stage ${stageId} is not currently active`, "workflow_stage_out_of_order");
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const receipt: TerminalReceipt = validateTerminalReceipt({
|
|
1622
|
+
schema: TERMINAL_RECEIPT_SCHEMA,
|
|
1623
|
+
status: "audit_escalation",
|
|
1624
|
+
seat: options.seat ?? stage.id,
|
|
1625
|
+
runId: run.id,
|
|
1626
|
+
stageId,
|
|
1627
|
+
timestamp,
|
|
1628
|
+
host: options.host ?? "pi",
|
|
1629
|
+
model: options.model ?? "default",
|
|
1630
|
+
escalationReason: reason,
|
|
1631
|
+
...(options.evidence ? { evidence: options.evidence } : {}),
|
|
1632
|
+
...(options.metrics ? { metrics: options.metrics } : {}),
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
const expiresAt = new Date(Date.parse(timestamp) + 24 * 60 * 60 * 1000).toISOString();
|
|
1636
|
+
waitForWorkflowSignal(run, stageId, "audit_escalation", reason, timestamp, expiresAt);
|
|
1637
|
+
|
|
1638
|
+
stage.receipt = receipt;
|
|
1639
|
+
stage.auditEscalation = {
|
|
1640
|
+
reason,
|
|
1641
|
+
timestamp,
|
|
1642
|
+
receipt,
|
|
1643
|
+
};
|
|
1644
|
+
|
|
1645
|
+
return { run, receipt };
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
export function resumeWorkflowFromRuling(
|
|
1649
|
+
run: WorkflowRun,
|
|
1650
|
+
ruling: string,
|
|
1651
|
+
timestamp: string,
|
|
1652
|
+
options: {
|
|
1653
|
+
status?: WorkflowCheckpointStatus;
|
|
1654
|
+
evidence?: WorkflowEvidenceInput;
|
|
1655
|
+
} = {},
|
|
1656
|
+
): { retry: boolean; completed: boolean; run: WorkflowRun; stageId: string } {
|
|
1657
|
+
if (run.status !== "waiting" || !run.waiting) {
|
|
1658
|
+
throw new ProtocolError(409, `workflow is ${run.status}`, "workflow_not_waiting");
|
|
1659
|
+
}
|
|
1660
|
+
if (run.waiting.signalKey !== "audit_escalation") {
|
|
1661
|
+
throw new ProtocolError(
|
|
1662
|
+
409,
|
|
1663
|
+
`workflow is waiting for signal '${run.waiting.signalKey}', not 'audit_escalation'`,
|
|
1664
|
+
"workflow_signal_mismatch",
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
const stageId = run.waiting.stageId;
|
|
1669
|
+
const stage = run.stages.find((s) => s.id === stageId);
|
|
1670
|
+
if (!stage) throw new ProtocolError(404, `workflow stage not found: ${stageId}`, "workflow_stage_not_found");
|
|
1671
|
+
|
|
1672
|
+
const status = options.status ?? "passed";
|
|
1673
|
+
const evidence = options.evidence ?? {};
|
|
1674
|
+
|
|
1675
|
+
if (stage.auditEscalation) {
|
|
1676
|
+
stage.auditEscalation.ruling = ruling;
|
|
1677
|
+
if (stage.auditEscalation.receipt) {
|
|
1678
|
+
stage.auditEscalation.receipt.ruling = ruling;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
stage.summary = `Resumed by operator ruling: ${ruling}`;
|
|
1682
|
+
|
|
1683
|
+
return resumeWorkflowFromSignal(
|
|
1684
|
+
run,
|
|
1685
|
+
"audit_escalation",
|
|
1686
|
+
status,
|
|
1687
|
+
`Resumed: ${ruling}`,
|
|
1688
|
+
evidence,
|
|
1689
|
+
timestamp,
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://schemas.kxm.dev/vnext/modes.schema.json",
|
|
4
|
+
"title": "KXM Modes Configuration",
|
|
5
|
+
"description": "Declarative major modes and domain modules for selective tool loading and context scoping",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schema", "majorModes"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema": { "const": "kxm.modes.v1" },
|
|
10
|
+
"majorModes": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"additionalProperties": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"required": ["baseTools"],
|
|
15
|
+
"properties": {
|
|
16
|
+
"description": { "type": "string" },
|
|
17
|
+
"baseTools": {
|
|
18
|
+
"type": "array",
|
|
19
|
+
"items": { "type": "string" }
|
|
20
|
+
},
|
|
21
|
+
"contextFiles": {
|
|
22
|
+
"type": "array",
|
|
23
|
+
"items": { "type": "string" }
|
|
24
|
+
},
|
|
25
|
+
"thinkingLevel": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["low", "medium", "high", "xhigh"]
|
|
28
|
+
},
|
|
29
|
+
"model": { "type": "string" }
|
|
30
|
+
},
|
|
31
|
+
"additionalProperties": false
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"domains": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"additionalProperties": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"required": ["tools"],
|
|
39
|
+
"properties": {
|
|
40
|
+
"description": { "type": "string" },
|
|
41
|
+
"tools": {
|
|
42
|
+
"type": "array",
|
|
43
|
+
"items": { "type": "string" }
|
|
44
|
+
},
|
|
45
|
+
"contextFiles": {
|
|
46
|
+
"type": "array",
|
|
47
|
+
"items": { "type": "string" }
|
|
48
|
+
},
|
|
49
|
+
"promptSnippet": { "type": "string" }
|
|
50
|
+
},
|
|
51
|
+
"additionalProperties": false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"additionalProperties": false
|
|
56
|
+
}
|