@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
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* releases the lease. Concurrency control, the observability tombstone,
|
|
14
14
|
* verification, and repair all live in the guard — this module only decides
|
|
15
15
|
* WHETHER to spawn and wires the detached stdio to auto-update.log.
|
|
16
|
+
* Guard argv uses the cycle-free self-invocation leaf (never a full import of
|
|
17
|
+
* the guard command module).
|
|
16
18
|
*
|
|
17
19
|
* Invariants:
|
|
18
20
|
* - Synchronous: returns before the install starts.
|
|
@@ -24,14 +26,15 @@
|
|
|
24
26
|
* `logUpdateError` and swallowed; the user never sees an exception
|
|
25
27
|
* from a successful CLI run.
|
|
26
28
|
*/
|
|
27
|
-
import { closeSync, existsSync, mkdirSync, openSync
|
|
29
|
+
import { closeSync, existsSync, mkdirSync, openSync } from "node:fs";
|
|
28
30
|
import { spawn } from "node:child_process";
|
|
29
31
|
import { dirname } from "node:path";
|
|
30
32
|
import { getSurfacePath } from "#paths/state-root.js";
|
|
31
|
-
import { writeJsonFile } from "#shared-utils/write-json-file.js";
|
|
32
|
-
import { buildSelfInvocationCommand } from "#cli/commands/self-install-guard.js";
|
|
33
33
|
import { logUpdateError } from "./log.js";
|
|
34
|
+
import { buildSelfInvocationCommand } from "./self-invocation.js";
|
|
34
35
|
export { isProcessAlive } from "./install-lock.js";
|
|
36
|
+
/** Re-export for existing call sites and tests (implementation in guard-tombstone). */
|
|
37
|
+
export { buildTombstone, writeGuardTombstone } from "./guard-tombstone.js";
|
|
35
38
|
/**
|
|
36
39
|
* Schedule a deferred install for the supplied package-manager command,
|
|
37
40
|
* routed through the self-install guard. Synchronous — spawn() returns
|
|
@@ -66,37 +69,6 @@ export function scheduleDeferredInstall(plan, options = {}) {
|
|
|
66
69
|
return { spawned: false, reason: "spawn error" };
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Observability tombstone written by the self-install guard when it takes
|
|
71
|
-
* the lease: records the guard's pid + start time in the notifier cache for
|
|
72
|
-
* humans and `wp update`'s cache-reconcile step. NOT a concurrency gate —
|
|
73
|
-
* the heartbeat lease in install-lock.ts owns mutual exclusion. Best-effort.
|
|
74
|
-
*/
|
|
75
|
-
export function writeGuardTombstone() {
|
|
76
|
-
try {
|
|
77
|
-
const configPath = resolveConfigPath();
|
|
78
|
-
if (configPath === null)
|
|
79
|
-
return;
|
|
80
|
-
writeTombstone(configPath, buildTombstone(process.pid, Date.now()));
|
|
81
|
-
}
|
|
82
|
-
catch (err) {
|
|
83
|
-
logUpdateError(err);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Build the canonical tombstone shape. Exported for testability.
|
|
88
|
-
*/
|
|
89
|
-
export function buildTombstone(pid, ts) {
|
|
90
|
-
return { autoInstallInProgress: { pid, ts } };
|
|
91
|
-
}
|
|
92
|
-
function resolveConfigPath() {
|
|
93
|
-
try {
|
|
94
|
-
return getSurfacePath("update-notifier-cache.json", "user");
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
72
|
function resolveLogPath() {
|
|
101
73
|
try {
|
|
102
74
|
return getSurfacePath("auto-update.log", "user");
|
|
@@ -105,29 +77,6 @@ function resolveLogPath() {
|
|
|
105
77
|
return null;
|
|
106
78
|
}
|
|
107
79
|
}
|
|
108
|
-
function readRaw(configPath) {
|
|
109
|
-
if (!existsSync(configPath))
|
|
110
|
-
return {};
|
|
111
|
-
try {
|
|
112
|
-
const text = readFileSync(configPath, "utf-8");
|
|
113
|
-
if (text.trim().length === 0)
|
|
114
|
-
return {};
|
|
115
|
-
const parsed = JSON.parse(text);
|
|
116
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
117
|
-
return {};
|
|
118
|
-
return parsed;
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
return {};
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
function writeTombstone(configPath, tombstone) {
|
|
125
|
-
const dir = dirname(configPath);
|
|
126
|
-
if (!existsSync(dir))
|
|
127
|
-
mkdirSync(dir, { recursive: true });
|
|
128
|
-
const merged = { ...readRaw(configPath), ...tombstone };
|
|
129
|
-
writeJsonFile(configPath, merged, { atomic: true, indent: 0, trailingNewline: false });
|
|
130
|
-
}
|
|
131
80
|
function openLogForAppend(logPath) {
|
|
132
81
|
const dir = dirname(logPath);
|
|
133
82
|
if (!existsSync(dir))
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cycle-free leaf: how `wp` re-invokes itself for self-install-guard.
|
|
3
|
+
*
|
|
4
|
+
* Lives outside `self-install-guard.ts` so deferred install, `wp update`, and
|
|
5
|
+
* setup can build guard argv without importing the full guard command module
|
|
6
|
+
* (which would re-create installer ↔ guard cycles and tempt dynamic imports).
|
|
7
|
+
*
|
|
8
|
+
* Survival rule: prefer a **stable** global launcher (`~/.vite-plus/bin/wp` or
|
|
9
|
+
* PATH `wp` outside an installId package tree). Re-execing
|
|
10
|
+
* `node …/agent-kit#id/…/cli.js` dies after `vp update -g` deletes the pre-swap
|
|
11
|
+
* tree mid-guard.
|
|
12
|
+
*/
|
|
13
|
+
export interface SelfInvocationOptions {
|
|
14
|
+
readonly execPath?: string;
|
|
15
|
+
readonly argv1?: string;
|
|
16
|
+
/** Injectable stable launcher for tests; `null` forces legacy argv1 path. */
|
|
17
|
+
readonly stableWp?: string | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build argv to re-invoke wp with the given subcommand args.
|
|
21
|
+
* - Prefer stable non-versioned launcher when available.
|
|
22
|
+
* - Bun-compiled binary: process.execPath IS the binary (argv[1] is /$bunfs).
|
|
23
|
+
* - Source/dev lane without a stable launcher: `<runtime> <script>`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildSelfInvocationCommand(args: readonly string[], options?: SelfInvocationOptions): string[];
|
|
26
|
+
export interface ResolveStableWpLauncherOptions {
|
|
27
|
+
readonly home?: string;
|
|
28
|
+
readonly pathEnv?: string;
|
|
29
|
+
readonly existsSync?: (path: string) => boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A launcher that survives installId package-tree deletion during global update.
|
|
33
|
+
* Rejects paths under `agent-kit#<uuid>/` (versioned store payload).
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveStableWpLauncher(options?: ResolveStableWpLauncherOptions): string | null;
|
|
36
|
+
/** True when path lives inside a vite-plus installId package tree. */
|
|
37
|
+
export declare function isVersionedAgentKitPayloadPath(filePath: string): boolean;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cycle-free leaf: how `wp` re-invokes itself for self-install-guard.
|
|
3
|
+
*
|
|
4
|
+
* Lives outside `self-install-guard.ts` so deferred install, `wp update`, and
|
|
5
|
+
* setup can build guard argv without importing the full guard command module
|
|
6
|
+
* (which would re-create installer ↔ guard cycles and tempt dynamic imports).
|
|
7
|
+
*
|
|
8
|
+
* Survival rule: prefer a **stable** global launcher (`~/.vite-plus/bin/wp` or
|
|
9
|
+
* PATH `wp` outside an installId package tree). Re-execing
|
|
10
|
+
* `node …/agent-kit#id/…/cli.js` dies after `vp update -g` deletes the pre-swap
|
|
11
|
+
* tree mid-guard.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
/**
|
|
16
|
+
* Build argv to re-invoke wp with the given subcommand args.
|
|
17
|
+
* - Prefer stable non-versioned launcher when available.
|
|
18
|
+
* - Bun-compiled binary: process.execPath IS the binary (argv[1] is /$bunfs).
|
|
19
|
+
* - Source/dev lane without a stable launcher: `<runtime> <script>`.
|
|
20
|
+
*/
|
|
21
|
+
export function buildSelfInvocationCommand(args, options = {}) {
|
|
22
|
+
const stableWp = options.stableWp !== undefined ? options.stableWp : resolveStableWpLauncher();
|
|
23
|
+
if (typeof stableWp === "string" && stableWp.length > 0) {
|
|
24
|
+
return [stableWp, ...args];
|
|
25
|
+
}
|
|
26
|
+
const execPath = options.execPath ?? process.execPath;
|
|
27
|
+
const argv1 = options.argv1 ?? process.argv[1] ?? "";
|
|
28
|
+
if (argv1.startsWith("/$bunfs"))
|
|
29
|
+
return [execPath, ...args];
|
|
30
|
+
return [execPath, argv1, ...args];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A launcher that survives installId package-tree deletion during global update.
|
|
34
|
+
* Rejects paths under `agent-kit#<uuid>/` (versioned store payload).
|
|
35
|
+
*/
|
|
36
|
+
export function resolveStableWpLauncher(options = {}) {
|
|
37
|
+
const exists = options.existsSync ?? existsSync;
|
|
38
|
+
const home = options.home ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
39
|
+
const candidates = [];
|
|
40
|
+
if (home) {
|
|
41
|
+
candidates.push(path.join(home, ".vite-plus", "bin", "wp"));
|
|
42
|
+
}
|
|
43
|
+
const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
|
|
44
|
+
for (const dir of pathEnv.split(path.delimiter)) {
|
|
45
|
+
if (!dir)
|
|
46
|
+
continue;
|
|
47
|
+
candidates.push(path.join(dir, "wp"));
|
|
48
|
+
}
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
if (!exists(candidate))
|
|
51
|
+
continue;
|
|
52
|
+
if (isVersionedAgentKitPayloadPath(candidate))
|
|
53
|
+
continue;
|
|
54
|
+
return candidate;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/** True when path lives inside a vite-plus installId package tree. */
|
|
59
|
+
export function isVersionedAgentKitPayloadPath(filePath) {
|
|
60
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
61
|
+
return /\/@webpresso\/agent-kit#[^/]+\//u.test(normalized);
|
|
62
|
+
}
|
|
@@ -27,6 +27,7 @@ export const REPO_AUDIT_REGISTRY = {
|
|
|
27
27
|
includeRuntimePlans: options.runtimePlans,
|
|
28
28
|
baseRef: options.baseRef ?? options.base,
|
|
29
29
|
changedOnly: options.changedOnly,
|
|
30
|
+
affectedFiles: options.affectedFiles,
|
|
30
31
|
}),
|
|
31
32
|
"blueprint-trust": async (root) => (await import("#audit/blueprint-trust")).auditBlueprintTrust(root),
|
|
32
33
|
"roadmap-links": async (root, options) => (await import("#audit/roadmap-links")).auditRoadmapLinks(root, {
|
|
@@ -121,8 +121,8 @@ async function runMenuMode(cwd, launch) {
|
|
|
121
121
|
writeStartupFrame(label);
|
|
122
122
|
try {
|
|
123
123
|
const { runMenu } = await import("./menu.js");
|
|
124
|
-
//
|
|
125
|
-
// single menu at the
|
|
124
|
+
// Clear the startup ghost frame so the TUI (native Ratatui or pure-ANSI)
|
|
125
|
+
// paints a single menu at the home cursor instead of stacking below it.
|
|
126
126
|
if (process.stdout.isTTY)
|
|
127
127
|
process.stdout.write("\u001B[2J\u001B[3J\u001B[H");
|
|
128
128
|
const exitCode = await runMenu({
|
|
@@ -136,7 +136,7 @@ async function runMenuMode(cwd, launch) {
|
|
|
136
136
|
return exitCode;
|
|
137
137
|
}
|
|
138
138
|
catch (error) {
|
|
139
|
-
// The startup frame hid the cursor;
|
|
139
|
+
// The startup frame hid the cursor; restore it if the menu never paints.
|
|
140
140
|
if (process.stdout.isTTY)
|
|
141
141
|
process.stdout.write("\u001B[?25h");
|
|
142
142
|
throw error;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-run fan-out attention projection — derives HUD/attention from
|
|
3
|
+
* registry-visible runs for a plan-once set. Does not introduce a parallel store.
|
|
4
|
+
*/
|
|
5
|
+
import { type AttentionItem, type AttentionSource } from "#cli/commands/dash/attention.js";
|
|
6
|
+
export type FanOutRunObservation = {
|
|
7
|
+
readonly runId: string;
|
|
8
|
+
readonly provider: string;
|
|
9
|
+
/** Registry / lifecycle state string (failed, running, starting, …). */
|
|
10
|
+
readonly state: string;
|
|
11
|
+
readonly error?: string | null;
|
|
12
|
+
readonly observedAt?: string;
|
|
13
|
+
readonly source?: Exclude<AttentionSource, "derived">;
|
|
14
|
+
};
|
|
15
|
+
export type FanOutAttentionProjection = {
|
|
16
|
+
readonly planId: string;
|
|
17
|
+
readonly total: number;
|
|
18
|
+
readonly failedCount: number;
|
|
19
|
+
readonly runningCount: number;
|
|
20
|
+
readonly items: readonly AttentionItem[];
|
|
21
|
+
/** One-line operator summary for attention strip. */
|
|
22
|
+
readonly summary: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Project multi-run fan-out state for attention/HUD consumption.
|
|
26
|
+
* Failed hosts are never dropped: they appear with inspect-failure action.
|
|
27
|
+
*/
|
|
28
|
+
export declare function projectFanOutAttention(input: {
|
|
29
|
+
readonly planId: string;
|
|
30
|
+
readonly runs: readonly FanOutRunObservation[];
|
|
31
|
+
readonly now?: string;
|
|
32
|
+
}): FanOutAttentionProjection;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-run fan-out attention projection — derives HUD/attention from
|
|
3
|
+
* registry-visible runs for a plan-once set. Does not introduce a parallel store.
|
|
4
|
+
*/
|
|
5
|
+
import { normalizeSessionState, projectAttention, } from "#cli/commands/dash/attention.js";
|
|
6
|
+
/**
|
|
7
|
+
* Project multi-run fan-out state for attention/HUD consumption.
|
|
8
|
+
* Failed hosts are never dropped: they appear with inspect-failure action.
|
|
9
|
+
*/
|
|
10
|
+
export function projectFanOutAttention(input) {
|
|
11
|
+
const now = input.now ?? new Date().toISOString();
|
|
12
|
+
const sessions = input.runs.map((run) => normalizeSessionState({
|
|
13
|
+
runId: run.runId,
|
|
14
|
+
provider: run.provider,
|
|
15
|
+
observations: [
|
|
16
|
+
{
|
|
17
|
+
source: run.source ?? "registry",
|
|
18
|
+
value: run.state,
|
|
19
|
+
observedAt: run.observedAt ?? now,
|
|
20
|
+
confidence: "high",
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
}));
|
|
24
|
+
const signals = new Map(input.runs.flatMap((run) => {
|
|
25
|
+
if (run.state === "failed" || run.error) {
|
|
26
|
+
return [[run.runId, { failedVerification: true }]];
|
|
27
|
+
}
|
|
28
|
+
return [];
|
|
29
|
+
}));
|
|
30
|
+
const items = projectAttention(sessions, signals);
|
|
31
|
+
const failedCount = items.filter((item) => item.state === "failed").length;
|
|
32
|
+
const runningCount = items.filter((item) => item.state === "running" || item.state === "starting").length;
|
|
33
|
+
const failureBits = items
|
|
34
|
+
.filter((item) => item.state === "failed")
|
|
35
|
+
.map((item) => {
|
|
36
|
+
const err = input.runs.find((r) => r.runId === item.runId)?.error;
|
|
37
|
+
return err ? `${item.provider}:${err}` : `${item.provider}:failed`;
|
|
38
|
+
});
|
|
39
|
+
const summary = failedCount > 0
|
|
40
|
+
? `fan-out ${input.planId.slice(0, 8)} · ${runningCount}/${input.runs.length} ok · ${failedCount} failed · ${failureBits.join("; ")}`
|
|
41
|
+
: `fan-out ${input.planId.slice(0, 8)} · ${runningCount}/${input.runs.length} hosts · all ok`;
|
|
42
|
+
return {
|
|
43
|
+
planId: input.planId,
|
|
44
|
+
total: input.runs.length,
|
|
45
|
+
failedCount,
|
|
46
|
+
runningCount,
|
|
47
|
+
items,
|
|
48
|
+
summary,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type DashProvider } from "#cli/commands/dash/provider-capabilities.js";
|
|
3
|
+
/** Bump only with a coordinated migration; validators reject unknown versions. */
|
|
4
|
+
export declare const PLAN_ONCE_SCHEMA_VERSION: 1;
|
|
5
|
+
/** Agent hosts only — never fleet (fleet is a registry-derived provider, not a launch host). */
|
|
6
|
+
export declare const PLAN_ONCE_HOSTS: readonly ["claude", "codex", "grok", "opencode"];
|
|
7
|
+
export type PlanOnceHost = DashProvider;
|
|
8
|
+
export declare const planOnceHostSchema: z.ZodEnum<{
|
|
9
|
+
claude: "claude";
|
|
10
|
+
codex: "codex";
|
|
11
|
+
grok: "grok";
|
|
12
|
+
opencode: "opencode";
|
|
13
|
+
}>;
|
|
14
|
+
export declare const planOnceHostEntrySchema: z.ZodObject<{
|
|
15
|
+
host: z.ZodEnum<{
|
|
16
|
+
claude: "claude";
|
|
17
|
+
codex: "codex";
|
|
18
|
+
grok: "grok";
|
|
19
|
+
opencode: "opencode";
|
|
20
|
+
}>;
|
|
21
|
+
workdir: z.ZodString;
|
|
22
|
+
brief: z.ZodOptional<z.ZodString>;
|
|
23
|
+
label: z.ZodOptional<z.ZodString>;
|
|
24
|
+
extraArgs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
25
|
+
}, z.core.$strict>;
|
|
26
|
+
export type PlanOnceHostEntry = z.infer<typeof planOnceHostEntrySchema>;
|
|
27
|
+
export declare const multiHostPlanProposalSchema: z.ZodObject<{
|
|
28
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
29
|
+
planId: z.ZodString;
|
|
30
|
+
hosts: z.ZodArray<z.ZodObject<{
|
|
31
|
+
host: z.ZodEnum<{
|
|
32
|
+
claude: "claude";
|
|
33
|
+
codex: "codex";
|
|
34
|
+
grok: "grok";
|
|
35
|
+
opencode: "opencode";
|
|
36
|
+
}>;
|
|
37
|
+
workdir: z.ZodString;
|
|
38
|
+
brief: z.ZodOptional<z.ZodString>;
|
|
39
|
+
label: z.ZodOptional<z.ZodString>;
|
|
40
|
+
extraArgs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
41
|
+
}, z.core.$strict>>;
|
|
42
|
+
proposedAt: z.ZodOptional<z.ZodString>;
|
|
43
|
+
}, z.core.$strict>;
|
|
44
|
+
export type MultiHostPlanProposal = z.infer<typeof multiHostPlanProposalSchema>;
|
|
45
|
+
export declare const planOnceDecisionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
46
|
+
kind: z.ZodLiteral<"confirm">;
|
|
47
|
+
planId: z.ZodString;
|
|
48
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
49
|
+
kind: z.ZodLiteral<"cancel">;
|
|
50
|
+
planId: z.ZodString;
|
|
51
|
+
}, z.core.$strict>], "kind">;
|
|
52
|
+
export type PlanOnceDecision = z.infer<typeof planOnceDecisionSchema>;
|
|
53
|
+
export type PlanOnceValidationIssue = {
|
|
54
|
+
readonly code: "schema" | "bad-schema-version" | "empty-hosts" | "unknown-host" | "bad-workdir" | "plan-id";
|
|
55
|
+
readonly message: string;
|
|
56
|
+
readonly path?: string;
|
|
57
|
+
};
|
|
58
|
+
export type PlanOnceParseResult = {
|
|
59
|
+
readonly ok: true;
|
|
60
|
+
readonly plan: MultiHostPlanProposal;
|
|
61
|
+
} | {
|
|
62
|
+
readonly ok: false;
|
|
63
|
+
readonly issues: readonly PlanOnceValidationIssue[];
|
|
64
|
+
};
|
|
65
|
+
/** Parse + structural validate an untrusted plan proposal. */
|
|
66
|
+
export declare function parseMultiHostPlanProposal(input: unknown): PlanOnceParseResult;
|
|
67
|
+
export declare function parsePlanOnceDecision(input: unknown): {
|
|
68
|
+
readonly ok: true;
|
|
69
|
+
readonly decision: PlanOnceDecision;
|
|
70
|
+
} | {
|
|
71
|
+
readonly ok: false;
|
|
72
|
+
readonly issues: readonly PlanOnceValidationIssue[];
|
|
73
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-first wire format for multi-host plan-once propose / confirm / cancel.
|
|
3
|
+
* Pure Zod surface — no I/O. Fan-out apply lives in plan-once.ts.
|
|
4
|
+
*/
|
|
5
|
+
import { isAbsolute } from "node:path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { DASH_PROVIDERS } from "#cli/commands/dash/provider-capabilities.js";
|
|
8
|
+
/** Bump only with a coordinated migration; validators reject unknown versions. */
|
|
9
|
+
export const PLAN_ONCE_SCHEMA_VERSION = 1;
|
|
10
|
+
/** Agent hosts only — never fleet (fleet is a registry-derived provider, not a launch host). */
|
|
11
|
+
export const PLAN_ONCE_HOSTS = DASH_PROVIDERS;
|
|
12
|
+
export const planOnceHostSchema = z.enum(PLAN_ONCE_HOSTS);
|
|
13
|
+
export const planOnceHostEntrySchema = z
|
|
14
|
+
.object({
|
|
15
|
+
host: planOnceHostSchema,
|
|
16
|
+
/** Absolute workdir for this host launch. */
|
|
17
|
+
workdir: z
|
|
18
|
+
.string()
|
|
19
|
+
.min(1)
|
|
20
|
+
.refine(isAbsolute, { message: "Plan host workdir must be absolute" }),
|
|
21
|
+
brief: z.string().min(1).optional(),
|
|
22
|
+
label: z.string().min(1).optional(),
|
|
23
|
+
/** Extra provider args after permission mode (not a free-form command bag). */
|
|
24
|
+
extraArgs: z.array(z.string()).optional(),
|
|
25
|
+
})
|
|
26
|
+
.strict();
|
|
27
|
+
export const multiHostPlanProposalSchema = z
|
|
28
|
+
.object({
|
|
29
|
+
schemaVersion: z.literal(PLAN_ONCE_SCHEMA_VERSION),
|
|
30
|
+
planId: z.string().uuid(),
|
|
31
|
+
hosts: z.array(planOnceHostEntrySchema).min(1),
|
|
32
|
+
proposedAt: z.string().datetime().optional(),
|
|
33
|
+
})
|
|
34
|
+
.strict();
|
|
35
|
+
export const planOnceDecisionSchema = z.discriminatedUnion("kind", [
|
|
36
|
+
z
|
|
37
|
+
.object({
|
|
38
|
+
kind: z.literal("confirm"),
|
|
39
|
+
planId: z.string().uuid(),
|
|
40
|
+
})
|
|
41
|
+
.strict(),
|
|
42
|
+
z
|
|
43
|
+
.object({
|
|
44
|
+
kind: z.literal("cancel"),
|
|
45
|
+
planId: z.string().uuid(),
|
|
46
|
+
})
|
|
47
|
+
.strict(),
|
|
48
|
+
]);
|
|
49
|
+
function issueFromZod(error) {
|
|
50
|
+
return error.issues.map((item) => {
|
|
51
|
+
const path = item.path.join(".") || undefined;
|
|
52
|
+
const message = item.message;
|
|
53
|
+
if (path === "schemaVersion" || message.toLowerCase().includes("schemaversion")) {
|
|
54
|
+
return { code: "bad-schema-version", message, path };
|
|
55
|
+
}
|
|
56
|
+
if (path === "hosts" && message.toLowerCase().includes("at least")) {
|
|
57
|
+
return { code: "empty-hosts", message, path };
|
|
58
|
+
}
|
|
59
|
+
// Check workdir before host: path "hosts.0.workdir" contains the substring "host".
|
|
60
|
+
if (path?.includes("workdir") || message.toLowerCase().includes("absolute")) {
|
|
61
|
+
return { code: "bad-workdir", message, path };
|
|
62
|
+
}
|
|
63
|
+
const lastSegment = item.path[item.path.length - 1];
|
|
64
|
+
if (lastSegment === "host" || message.toLowerCase().includes("invalid enum")) {
|
|
65
|
+
return { code: "unknown-host", message, path };
|
|
66
|
+
}
|
|
67
|
+
if (path === "planId") {
|
|
68
|
+
return { code: "plan-id", message, path };
|
|
69
|
+
}
|
|
70
|
+
return { code: "schema", message, path };
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** Parse + structural validate an untrusted plan proposal. */
|
|
74
|
+
export function parseMultiHostPlanProposal(input) {
|
|
75
|
+
if (input !== null &&
|
|
76
|
+
typeof input === "object" &&
|
|
77
|
+
!Array.isArray(input) &&
|
|
78
|
+
"schemaVersion" in input &&
|
|
79
|
+
input.schemaVersion !== PLAN_ONCE_SCHEMA_VERSION) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
issues: [
|
|
83
|
+
{
|
|
84
|
+
code: "bad-schema-version",
|
|
85
|
+
message: `Unsupported plan schemaVersion (expected ${PLAN_ONCE_SCHEMA_VERSION})`,
|
|
86
|
+
path: "schemaVersion",
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const parsed = multiHostPlanProposalSchema.safeParse(input);
|
|
92
|
+
if (!parsed.success) {
|
|
93
|
+
const issues = issueFromZod(parsed.error);
|
|
94
|
+
// Empty array is a common operator mistake — ensure empty-hosts is explicit.
|
|
95
|
+
if (input !== null &&
|
|
96
|
+
typeof input === "object" &&
|
|
97
|
+
!Array.isArray(input) &&
|
|
98
|
+
Array.isArray(input.hosts) &&
|
|
99
|
+
input.hosts.length === 0 &&
|
|
100
|
+
!issues.some((i) => i.code === "empty-hosts")) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
issues: [
|
|
104
|
+
{ code: "empty-hosts", message: "Plan must include at least one host", path: "hosts" },
|
|
105
|
+
...issues,
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { ok: false, issues };
|
|
110
|
+
}
|
|
111
|
+
return { ok: true, plan: parsed.data };
|
|
112
|
+
}
|
|
113
|
+
export function parsePlanOnceDecision(input) {
|
|
114
|
+
const parsed = planOnceDecisionSchema.safeParse(input);
|
|
115
|
+
if (!parsed.success) {
|
|
116
|
+
return { ok: false, issues: issueFromZod(parsed.error) };
|
|
117
|
+
}
|
|
118
|
+
return { ok: true, decision: parsed.data };
|
|
119
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-host plan-once confirm/cancel reducers and fan-out apply.
|
|
3
|
+
*
|
|
4
|
+
* Design:
|
|
5
|
+
* - Schema-first parse lives in plan-once-schema.ts (pure).
|
|
6
|
+
* - Confirm-once: one human decision for the full host set; cancel never launches.
|
|
7
|
+
* - Fan-out reuses DashLaunchCoordinator + existing selectAgent path only.
|
|
8
|
+
* - **Registry ownership:** `DashPaneController.select` (via selectAgent) is the
|
|
9
|
+
* sole createDashRun owner for launches. plan-once must NOT pre-create runs
|
|
10
|
+
* (newRun + runId is rejected; dual create would orphan plan-once rows without
|
|
11
|
+
* tmuxPaneId). After success, plan-once only **labels** the select-created run.
|
|
12
|
+
* - Per-host permission via resolveLaunchPermissionMode (no policy fork).
|
|
13
|
+
* - Concurrency: sequential (required by DashLaunchCoordinator single-pending).
|
|
14
|
+
* - Partial failure: continue remaining hosts; failed hosts reported honestly.
|
|
15
|
+
*/
|
|
16
|
+
import type { DashLaunchCoordinator, DashLaunchResult } from "#cli/commands/dash/launch-coordinator.js";
|
|
17
|
+
import { type MultiHostPlanProposal, type PlanOnceDecision, type PlanOnceHostEntry, type PlanOnceValidationIssue } from "#cli/commands/dash/plan-once-schema.js";
|
|
18
|
+
import { type DashPermissionMode, type DashProvider } from "#cli/commands/dash/provider-capabilities.js";
|
|
19
|
+
import { type CreateDashRunInput } from "#cli/commands/dash/run-registry.js";
|
|
20
|
+
import type { DashRun, DashRunState } from "#cli/commands/dash/run-types.js";
|
|
21
|
+
export { PLAN_ONCE_SCHEMA_VERSION, parseMultiHostPlanProposal, parsePlanOnceDecision, multiHostPlanProposalSchema, planOnceDecisionSchema, type MultiHostPlanProposal, type PlanOnceDecision, type PlanOnceHostEntry, type PlanOnceValidationIssue, } from "#cli/commands/dash/plan-once-schema.js";
|
|
22
|
+
/** Documented fan-out concurrency — sequential because coordinator admits one pending launch. */
|
|
23
|
+
export declare const PLAN_ONCE_FANOUT_CONCURRENCY: "sequential";
|
|
24
|
+
export type PlanOnceFanOutConcurrency = typeof PLAN_ONCE_FANOUT_CONCURRENCY;
|
|
25
|
+
export type PlanOnceSessionStatus = "proposed" | "confirmed" | "cancelled" | "applying" | "applied" | "apply-partial" | "apply-failed";
|
|
26
|
+
export type PlanOnceSession = {
|
|
27
|
+
readonly status: PlanOnceSessionStatus;
|
|
28
|
+
readonly plan: MultiHostPlanProposal;
|
|
29
|
+
};
|
|
30
|
+
export type PlanOnceReduceResult = {
|
|
31
|
+
readonly ok: true;
|
|
32
|
+
readonly session: PlanOnceSession;
|
|
33
|
+
} | {
|
|
34
|
+
readonly ok: false;
|
|
35
|
+
readonly error: string;
|
|
36
|
+
};
|
|
37
|
+
/** Pure confirm-once / cancel reducer — no registry side effects. */
|
|
38
|
+
export declare function createPlanOnceSession(plan: MultiHostPlanProposal): PlanOnceSession;
|
|
39
|
+
export declare function reducePlanOnceDecision(session: PlanOnceSession, decision: PlanOnceDecision): PlanOnceReduceResult;
|
|
40
|
+
/** Parse decision then reduce — convenience for IPC/menu handlers. */
|
|
41
|
+
export declare function applyPlanOnceDecision(session: PlanOnceSession, rawDecision: unknown): PlanOnceReduceResult;
|
|
42
|
+
export type FanOutHostResult = {
|
|
43
|
+
readonly host: DashProvider;
|
|
44
|
+
readonly workdir: string;
|
|
45
|
+
/** Dash run id from selectAgent (sole registry create path). */
|
|
46
|
+
readonly runId: string | null;
|
|
47
|
+
readonly status: "launched" | "failed" | "discarded";
|
|
48
|
+
readonly permissionMode: DashPermissionMode;
|
|
49
|
+
readonly command: string;
|
|
50
|
+
readonly error: string | null;
|
|
51
|
+
};
|
|
52
|
+
export type FanOutApplyResult = {
|
|
53
|
+
readonly planId: string;
|
|
54
|
+
readonly concurrency: PlanOnceFanOutConcurrency;
|
|
55
|
+
readonly hosts: readonly FanOutHostResult[];
|
|
56
|
+
readonly launchedCount: number;
|
|
57
|
+
readonly failedCount: number;
|
|
58
|
+
readonly discardedCount: number;
|
|
59
|
+
};
|
|
60
|
+
export type PlanOnceLaunchPort = {
|
|
61
|
+
launch(command: string, context?: {
|
|
62
|
+
readonly cwd?: string;
|
|
63
|
+
readonly newRun?: boolean;
|
|
64
|
+
readonly prepared?: boolean;
|
|
65
|
+
readonly provider?: DashProvider;
|
|
66
|
+
readonly argv?: readonly string[];
|
|
67
|
+
/** Never pass with newRun:true — pane-controller rejects that combo. */
|
|
68
|
+
readonly runId?: string;
|
|
69
|
+
}): Promise<DashLaunchResult>;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Registry port used **only** to label/update the run selectAgent already created.
|
|
73
|
+
* createRun is available for tests that simulate selectAgent; apply must not call it.
|
|
74
|
+
*/
|
|
75
|
+
export type PlanOnceRegistryPort = {
|
|
76
|
+
createRun(input: CreateDashRunInput): Promise<DashRun>;
|
|
77
|
+
updateRun(id: string, update: {
|
|
78
|
+
readonly state?: DashRunState;
|
|
79
|
+
readonly error?: string | null;
|
|
80
|
+
readonly label?: string | null;
|
|
81
|
+
readonly tmuxPaneId?: string | null;
|
|
82
|
+
readonly providerSessionId?: string | null;
|
|
83
|
+
}, options?: {
|
|
84
|
+
readonly cwd?: string;
|
|
85
|
+
}): Promise<DashRun>;
|
|
86
|
+
};
|
|
87
|
+
export type PlanOnceApplyDeps = {
|
|
88
|
+
readonly launch: PlanOnceLaunchPort;
|
|
89
|
+
readonly registry: PlanOnceRegistryPort;
|
|
90
|
+
readonly resolvePermissionMode: (args?: readonly string[], provider?: DashProvider) => DashPermissionMode;
|
|
91
|
+
/** Registry file scope (dash instance cwd). Host workdirs may differ. */
|
|
92
|
+
readonly registryCwd: string;
|
|
93
|
+
};
|
|
94
|
+
export declare function planOnceRunLabel(planId: string, host: PlanOnceHostEntry): string;
|
|
95
|
+
/**
|
|
96
|
+
* Apply a confirmed plan: sequential fan-out through launch coordinator.
|
|
97
|
+
*
|
|
98
|
+
* Does **not** pre-create registry rows. selectAgent (pane-controller) creates
|
|
99
|
+
* exactly one dash run per host on newRun. On success, plan-once labels that run.
|
|
100
|
+
*/
|
|
101
|
+
export declare function applyConfirmedMultiHostPlan(input: {
|
|
102
|
+
readonly session: PlanOnceSession;
|
|
103
|
+
readonly launch: PlanOnceLaunchPort | DashLaunchCoordinator;
|
|
104
|
+
readonly registryCwd: string;
|
|
105
|
+
readonly registry?: PlanOnceRegistryPort;
|
|
106
|
+
readonly resolvePermissionMode?: PlanOnceApplyDeps["resolvePermissionMode"];
|
|
107
|
+
}): Promise<{
|
|
108
|
+
readonly ok: true;
|
|
109
|
+
readonly result: FanOutApplyResult;
|
|
110
|
+
readonly session: PlanOnceSession;
|
|
111
|
+
} | {
|
|
112
|
+
readonly ok: false;
|
|
113
|
+
readonly error: string;
|
|
114
|
+
}>;
|
|
115
|
+
/** Propose → parse helper for callers that only have untrusted JSON. */
|
|
116
|
+
export declare function proposeMultiHostPlan(raw: unknown): {
|
|
117
|
+
readonly ok: true;
|
|
118
|
+
readonly session: PlanOnceSession;
|
|
119
|
+
} | {
|
|
120
|
+
readonly ok: false;
|
|
121
|
+
readonly issues: readonly PlanOnceValidationIssue[];
|
|
122
|
+
};
|