@vzn/vx 0.0.16 → 0.0.18
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/README.md +66 -89
- package/index.ts +10 -0
- package/launcher.mjs +10 -11
- package/package.json +19 -5
- package/plugins/local-cache/index.ts +2 -0
- package/plugins/local-executor/index.ts +2 -0
- package/plugins/schedule-history/index.ts +2 -0
- package/src/bin.ts +2 -2
- package/src/cache/archive.ts +557 -0
- package/src/cache/cache.ts +929 -250
- package/src/cache/cas-backend.ts +20 -2
- package/src/cache/chained-cache.ts +202 -0
- package/src/cache/digest.ts +0 -15
- package/src/cache/index.ts +13 -3
- package/src/cache/inputs.ts +445 -41
- package/src/cache/layered-cache.ts +88 -30
- package/src/cache/tar-stream.ts +358 -0
- package/src/cli/cache.ts +36 -10
- package/src/cli/help.ts +167 -128
- package/src/cli/index.ts +74 -43
- package/src/cli/info.ts +30 -0
- package/src/cli/last.ts +140 -0
- package/src/cli/migrate-emit.ts +21 -0
- package/src/cli/migrate-nx.ts +23 -0
- package/src/cli/migrate-scripts.ts +127 -0
- package/src/cli/migrate-turbo.ts +43 -6
- package/src/cli/migrate.ts +119 -33
- package/src/cli/plan-format.ts +58 -3
- package/src/cli/plugin-commands.ts +96 -0
- package/src/cli/prune.ts +291 -0
- package/src/cli/run.ts +272 -80
- package/src/cli/upgrade.ts +7 -6
- package/src/cli/watch.ts +259 -61
- package/src/cli/why.ts +193 -0
- package/src/config.ts +40 -23
- package/src/exec/env.ts +7 -1
- package/src/exec/executor.ts +239 -0
- package/src/exec/index.ts +19 -1
- package/src/exec/runner.ts +116 -29
- package/src/exec/sandbox-runtime.ts +194 -24
- package/src/graph/index.ts +3 -1
- package/src/graph/scheduler.ts +92 -18
- package/src/graph/task-graph.ts +220 -6
- package/src/index.ts +82 -84
- package/src/orchestrator/deferred-outputs.ts +144 -0
- package/src/orchestrator/download-policy.ts +171 -0
- package/src/orchestrator/events.ts +30 -0
- package/src/orchestrator/execute-task.ts +328 -94
- package/src/orchestrator/failure-mode.ts +61 -0
- package/src/orchestrator/framed-output.ts +87 -35
- package/src/orchestrator/history.ts +78 -31
- package/src/orchestrator/index.ts +43 -90
- package/src/orchestrator/local-shortcircuit.ts +22 -0
- package/src/orchestrator/logger.ts +196 -14
- package/src/orchestrator/metrics.ts +57 -1376
- package/src/orchestrator/missing-plugin.ts +9 -0
- package/src/orchestrator/options.ts +22 -4
- package/src/orchestrator/plan.ts +144 -1
- package/src/orchestrator/plugin-host.ts +199 -119
- package/src/orchestrator/plugin.ts +144 -57
- package/src/orchestrator/prepare.ts +220 -69
- package/src/orchestrator/remote-prefetch.ts +12 -8
- package/src/orchestrator/resources.ts +20 -34
- package/src/orchestrator/run-artifacts.ts +41 -16
- package/src/orchestrator/run-context.ts +167 -29
- package/src/orchestrator/run-report.ts +73 -46
- package/src/orchestrator/run.ts +590 -122
- package/src/orchestrator/stable-keys.ts +74 -33
- package/src/orchestrator/status-line.ts +47 -8
- package/src/orchestrator/summary.ts +22 -1
- package/src/orchestrator/tally.ts +78 -35
- package/src/orchestrator/task-hash.ts +105 -11
- package/src/orchestrator/task-log-buffer.ts +291 -0
- package/src/orchestrator/telemetry-host.ts +24 -6
- package/src/orchestrator/telemetry.ts +181 -21
- package/src/orchestrator/upstream.ts +72 -31
- package/src/orchestrator/verify.ts +12 -2
- package/src/plugins/local-cache/index.ts +11 -0
- package/src/plugins/local-executor/index.ts +45 -0
- package/src/plugins/schedule-history/index.ts +109 -0
- package/src/util/edit-distance.ts +21 -0
- package/src/util/errors.ts +15 -1
- package/src/util/index.ts +7 -2
- package/src/util/num.ts +37 -0
- package/src/util/paths.ts +43 -0
- package/src/util/settle.ts +48 -0
- package/src/util/size.ts +5 -1
- package/src/util/tail.ts +66 -0
- package/src/util/timing.ts +63 -0
- package/src/workspace/affected.ts +142 -15
- package/src/workspace/config-cache.ts +304 -0
- package/src/workspace/config-eval.ts +210 -0
- package/src/workspace/config-imports.ts +161 -0
- package/src/workspace/filter.ts +22 -4
- package/src/workspace/fingerprint.ts +16 -1
- package/src/workspace/index.ts +15 -3
- package/src/workspace/lockfile.ts +13 -2
- package/src/workspace/nested-dirs.ts +21 -7
- package/src/workspace/package-graph.ts +23 -20
- package/src/workspace/project-loader.ts +503 -59
- package/src/workspace/workspace.ts +213 -72
- package/src/cache/tar.ts +0 -343
- package/src/cli/backend.ts +0 -23
- package/src/cli/mcp-rpc.ts +0 -292
- package/src/cli/mcp.ts +0 -92
- package/src/orchestrator/devframe-surface.ts +0 -74
- package/src/orchestrator/predict.ts +0 -106
- package/src/orchestrator/protocol.ts +0 -156
- package/src/orchestrator/run-state.ts +0 -145
- package/src/orchestrator/wire-render.ts +0 -92
- package/src/orchestrator/wire.ts +0 -212
package/src/cli/mcp.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
// `vx mcp` — Model Context Protocol server adapter.
|
|
2
|
-
//
|
|
3
|
-
// Exposes vx as a typed tool surface to AI agents (Claude Code, Cursor,
|
|
4
|
-
// Continue.dev, etc.). The MCP envelope is JSON-RPC 2.0 — the same
|
|
5
|
-
// envelope wire-protocol-2026-06.md commits to — so this adapter is a
|
|
6
|
-
// thin mapping layer: MCP tool calls dispatch to internal RPC methods.
|
|
7
|
-
//
|
|
8
|
-
// stdio transport in v1: every relevant agent client runs servers over
|
|
9
|
-
// stdio. Streamable HTTP is a follow-up; the SDK supports both, dispatch
|
|
10
|
-
// is identical, only the transport object changes.
|
|
11
|
-
//
|
|
12
|
-
// Tools exposed:
|
|
13
|
-
// runTasks(tasks: string[], cwd?: string)
|
|
14
|
-
// getCacheStats(scope?: 'all' | { project: string })
|
|
15
|
-
// getRunHistory({ project?, task?, limit? })
|
|
16
|
-
// explainCacheKey(taskId)
|
|
17
|
-
// whyDidThisRerun({ runId, taskId })
|
|
18
|
-
//
|
|
19
|
-
// The implementations live in `src/cli/mcp-rpc.ts` so a future WS-side
|
|
20
|
-
// inspector can reuse them without duplicating logic.
|
|
21
|
-
|
|
22
|
-
import { UserError } from '../util/index.js'
|
|
23
|
-
import { VERSION } from '../version.js'
|
|
24
|
-
import { handleMcpRequest, listMcpTools, type McpToolDef } from './mcp-rpc.js'
|
|
25
|
-
|
|
26
|
-
export interface McpArgs {
|
|
27
|
-
/** stdio (default) | http (deferred to a follow-up) */
|
|
28
|
-
transport: 'stdio'
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function parseMcpArgs(args: readonly string[]): McpArgs {
|
|
32
|
-
for (const a of args) {
|
|
33
|
-
if (a === '--http') {
|
|
34
|
-
throw new UserError('vx mcp --http is not yet implemented; use stdio (default)')
|
|
35
|
-
}
|
|
36
|
-
if (a !== '--stdio') {
|
|
37
|
-
throw new UserError(`vx mcp: unknown flag ${a}`)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return { transport: 'stdio' }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function mcpCmd(args: readonly string[]): Promise<number> {
|
|
44
|
-
parseMcpArgs(args) // validate flags; defaults to stdio
|
|
45
|
-
let StdioServerTransport: any
|
|
46
|
-
let McpServer: any
|
|
47
|
-
try {
|
|
48
|
-
const sdkServer = await import('@modelcontextprotocol/sdk/server/index.js')
|
|
49
|
-
const sdkStdio = await import('@modelcontextprotocol/sdk/server/stdio.js')
|
|
50
|
-
McpServer = (sdkServer as { Server: any }).Server
|
|
51
|
-
StdioServerTransport = (sdkStdio as { StdioServerTransport: any }).StdioServerTransport
|
|
52
|
-
} catch {
|
|
53
|
-
throw new UserError(
|
|
54
|
-
`vx mcp requires @modelcontextprotocol/sdk to be installed. ` +
|
|
55
|
-
`Add it to your package.json or install with: bun add @modelcontextprotocol/sdk`,
|
|
56
|
-
)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const server = new McpServer(
|
|
60
|
-
{ name: 'vx', version: VERSION },
|
|
61
|
-
{ capabilities: { tools: {}, resources: {} } },
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
// Tool listing
|
|
65
|
-
const sdkTypes = await import('@modelcontextprotocol/sdk/types.js')
|
|
66
|
-
server.setRequestHandler(
|
|
67
|
-
(sdkTypes as { ListToolsRequestSchema: any }).ListToolsRequestSchema,
|
|
68
|
-
async () => ({
|
|
69
|
-
tools: listMcpTools().map((t: McpToolDef) => ({
|
|
70
|
-
name: t.name,
|
|
71
|
-
description: t.description,
|
|
72
|
-
inputSchema: t.inputSchema,
|
|
73
|
-
})),
|
|
74
|
-
}),
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
// Tool call dispatch
|
|
78
|
-
server.setRequestHandler(
|
|
79
|
-
(sdkTypes as { CallToolRequestSchema: any }).CallToolRequestSchema,
|
|
80
|
-
async (req: { params: { name: string; arguments?: unknown } }) => {
|
|
81
|
-
const result = await handleMcpRequest(req.params.name, req.params.arguments)
|
|
82
|
-
return {
|
|
83
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
const transport = new StdioServerTransport()
|
|
89
|
-
await server.connect(transport)
|
|
90
|
-
await new Promise(() => undefined) // run until stdin closes
|
|
91
|
-
return 0
|
|
92
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// The devframe surface for a vx run: one definition that exposes the run
|
|
2
|
-
// as a live streaming channel (`vx:events`, the raw WireEvent feed) plus a
|
|
3
|
-
// patch-synced shared state (`vx:run`, the reduced RunState aggregate).
|
|
4
|
-
// A host adapter (dev server / MCP — see cli wiring) mounts this same
|
|
5
|
-
// definition; this module owns only the translation from our in-process
|
|
6
|
-
// event bus into devframe's streaming + shared-state primitives.
|
|
7
|
-
//
|
|
8
|
-
// devframe is touched ONLY through type-only imports here, so core `vx`
|
|
9
|
-
// never gains a runtime dependency on it — the host that actually boots a
|
|
10
|
-
// devframe server imports the runtime adapter dynamically and is the
|
|
11
|
-
// optional, opt-in path. See docs/design/event-stream-2026-06.md.
|
|
12
|
-
|
|
13
|
-
import type { DevframeDefinition, DevframeNodeContext, RpcStreamingChannel } from 'devframe'
|
|
14
|
-
import { VERSION } from '../version.js'
|
|
15
|
-
import { toWireEvent, type EventBus, type WireEvent } from './events.js'
|
|
16
|
-
import { initRunState, reduce, type RunState } from './run-state.js'
|
|
17
|
-
|
|
18
|
-
// Type the `vx:run` shared-state key so `ctx.rpc.sharedState.get` is
|
|
19
|
-
// checked against our aggregate shape.
|
|
20
|
-
declare module 'devframe' {
|
|
21
|
-
interface DevframeRpcSharedStates {
|
|
22
|
-
'vx:run': RunState
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface VxSurfaceOptions {
|
|
27
|
-
/** Stream id for the run's event feed. One run → one stream. */
|
|
28
|
-
streamId?: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Build a devframe definition that mirrors a single run's bus onto
|
|
33
|
-
* devframe's wire. `setup` runs at host boot (before the run emits), so
|
|
34
|
-
* it subscribes to the bus, forwards every event onto the `vx:events`
|
|
35
|
-
* stream, and folds it into the `vx:run` shared state. The host syncs
|
|
36
|
-
* both to any connected client (web SPA, MCP resource).
|
|
37
|
-
*/
|
|
38
|
-
export function createVxSurface(bus: EventBus, options: VxSurfaceOptions = {}): DevframeDefinition {
|
|
39
|
-
const streamId = options.streamId ?? 'run'
|
|
40
|
-
return {
|
|
41
|
-
id: 'vx',
|
|
42
|
-
name: 'vx',
|
|
43
|
-
version: VERSION,
|
|
44
|
-
async setup(ctx: DevframeNodeContext) {
|
|
45
|
-
const channel: RpcStreamingChannel<WireEvent> = ctx.rpc.streaming.create('vx:events')
|
|
46
|
-
const stream = channel.start({ id: streamId })
|
|
47
|
-
const state = await ctx.rpc.sharedState.get('vx:run', { initialValue: initRunState() })
|
|
48
|
-
|
|
49
|
-
// Local snapshot fed to `reduce`; the shared state is updated by
|
|
50
|
-
// assigning the new snapshot onto its draft (pure reduce → diffable
|
|
51
|
-
// patch). Kept beside the shared state rather than read back from it
|
|
52
|
-
// so reduction never depends on devframe's immutable accessors.
|
|
53
|
-
let snapshot = initRunState()
|
|
54
|
-
// `run()` emits `run:end` more than once (the normal end + a `finally`
|
|
55
|
-
// safety call) plus summary `run:status` lines AFTER the first
|
|
56
|
-
// `run:end`. Once the stream is closed, ignore everything — writing
|
|
57
|
-
// to a closed stream throws (StreamClosedError), and the run is over.
|
|
58
|
-
let closed = false
|
|
59
|
-
|
|
60
|
-
bus.subscribe((event) => {
|
|
61
|
-
if (closed) return
|
|
62
|
-
stream.write(toWireEvent(event))
|
|
63
|
-
snapshot = reduce(snapshot, event)
|
|
64
|
-
state.mutate((draft) => {
|
|
65
|
-
Object.assign(draft, snapshot)
|
|
66
|
-
})
|
|
67
|
-
if (event.kind === 'run:end') {
|
|
68
|
-
stream.close()
|
|
69
|
-
closed = true
|
|
70
|
-
}
|
|
71
|
-
})
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
// Predictive scheduling — use HistoryTable to compute expected
|
|
2
|
-
// remaining critical-path duration per node. Replaces the scheduler's
|
|
3
|
-
// graph-counter heuristic priority for runs that opt in via
|
|
4
|
-
// defineWorkspace({ predictive: true }) (predictive-execution-2026-06.md
|
|
5
|
-
// Phase B + architecture-review-2026-06.md §8.4).
|
|
6
|
-
//
|
|
7
|
-
// Pure functions; no side effects; tested standalone. The scheduler
|
|
8
|
-
// reads the priorities at queue time, picks the max.
|
|
9
|
-
|
|
10
|
-
import type { TaskNode } from '../graph/index.js'
|
|
11
|
-
import type { HistoryTable } from './history.js'
|
|
12
|
-
|
|
13
|
-
/** Default duration when neither task history nor workspace median exists. */
|
|
14
|
-
const DEFAULT_DURATION_MS = 1000
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* For each node, the expected remaining critical-path duration (its own
|
|
18
|
-
* p50 + the max over its dependents). Cache hits are NOT modeled as
|
|
19
|
-
* zero-cost here because predicting cache state requires knowing the
|
|
20
|
-
* key + probing the layer — the run-time scheduler can flip the
|
|
21
|
-
* priority once a hit is observed; the upfront estimate is "what if
|
|
22
|
-
* everything ran."
|
|
23
|
-
*/
|
|
24
|
-
export function computePredictedPriorities(
|
|
25
|
-
nodes: readonly TaskNode[],
|
|
26
|
-
history: HistoryTable,
|
|
27
|
-
): ReadonlyMap<string, number> {
|
|
28
|
-
// Workspace-median fallback: across every history entry that has a
|
|
29
|
-
// p50, pick the median of those p50s. If history is sparse, fall
|
|
30
|
-
// back to DEFAULT_DURATION_MS.
|
|
31
|
-
const p50s: number[] = []
|
|
32
|
-
for (const h of history.values()) {
|
|
33
|
-
if (h.p50DurationMs !== undefined) p50s.push(h.p50DurationMs)
|
|
34
|
-
}
|
|
35
|
-
p50s.sort((a, b) => a - b)
|
|
36
|
-
const workspaceMedian =
|
|
37
|
-
p50s.length > 0
|
|
38
|
-
? (p50s[Math.floor(p50s.length / 2)] ?? DEFAULT_DURATION_MS)
|
|
39
|
-
: DEFAULT_DURATION_MS
|
|
40
|
-
|
|
41
|
-
// Build a reverse-adjacency map so we can resolve dependents per
|
|
42
|
-
// node in O(1). The TaskNode graph carries dependsOn (upstream); we
|
|
43
|
-
// invert.
|
|
44
|
-
const dependentsOf = buildDependentsIndex(nodes)
|
|
45
|
-
const ownDuration = (n: TaskNode): number => {
|
|
46
|
-
const h = history.get(n.id)
|
|
47
|
-
return h?.p50DurationMs ?? workspaceMedian
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Memoized topo-DP. visit[id] = the expected critical path FROM this
|
|
51
|
-
// node down to a leaf. We compute bottom-up using a stack-based
|
|
52
|
-
// walker — iterative to avoid V8 stack-frame ceilings on deep graphs.
|
|
53
|
-
const memo = new Map<string, number>()
|
|
54
|
-
const nodeById = new Map<string, TaskNode>(nodes.map((n) => [n.id, n]))
|
|
55
|
-
|
|
56
|
-
const stack: TaskNode[] = nodes.slice()
|
|
57
|
-
// First pass: ensure deepest-first ordering via post-order traversal.
|
|
58
|
-
const order: TaskNode[] = []
|
|
59
|
-
const visited = new Set<string>()
|
|
60
|
-
while (stack.length > 0) {
|
|
61
|
-
const top = stack[stack.length - 1]!
|
|
62
|
-
if (visited.has(top.id)) {
|
|
63
|
-
stack.pop()
|
|
64
|
-
continue
|
|
65
|
-
}
|
|
66
|
-
const deps = dependentsOf.get(top.id) ?? []
|
|
67
|
-
let pushed = false
|
|
68
|
-
for (const d of deps) {
|
|
69
|
-
const n = nodeById.get(d)
|
|
70
|
-
if (n && !visited.has(n.id) && !stack.includes(n)) {
|
|
71
|
-
stack.push(n)
|
|
72
|
-
pushed = true
|
|
73
|
-
break
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (!pushed) {
|
|
77
|
-
visited.add(top.id)
|
|
78
|
-
order.push(top)
|
|
79
|
-
stack.pop()
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
for (const n of order) {
|
|
84
|
-
const own = ownDuration(n)
|
|
85
|
-
let downstream = 0
|
|
86
|
-
for (const dep of dependentsOf.get(n.id) ?? []) {
|
|
87
|
-
const d = memo.get(dep) ?? 0
|
|
88
|
-
if (d > downstream) downstream = d
|
|
89
|
-
}
|
|
90
|
-
memo.set(n.id, own + downstream)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return memo
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function buildDependentsIndex(nodes: readonly TaskNode[]): Map<string, string[]> {
|
|
97
|
-
const out = new Map<string, string[]>()
|
|
98
|
-
for (const n of nodes) {
|
|
99
|
-
for (const upstreamId of n.deps) {
|
|
100
|
-
const list = out.get(upstreamId)
|
|
101
|
-
if (list) list.push(n.id)
|
|
102
|
-
else out.set(upstreamId, [n.id])
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return out
|
|
106
|
-
}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
// The client↔service wire contract. A `vx run` client submits a
|
|
2
|
-
// `RunRequest`; the service streams `WireEvent`s and returns a
|
|
3
|
-
// `RunResult`. Transport-agnostic by design — the same messages travel a
|
|
4
|
-
// local unix/ws hop today or a hosted `wss://` link tomorrow. Pure types +
|
|
5
|
-
// the RunOptions⇄RunRequest mapping; no transport, no execution here.
|
|
6
|
-
|
|
7
|
-
import type { CachePolicy } from '../cache/index.js'
|
|
8
|
-
import type { ContinueMode } from '../graph/index.js'
|
|
9
|
-
import type { OutcomeView, WireEvent } from './events.js'
|
|
10
|
-
import type { RunOptions } from './options.js'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The serializable subset of `RunOptions` a client sends to the service.
|
|
14
|
-
* Omits the non-serializable / host-side fields (`log`, `bus`,
|
|
15
|
-
* `handleSignals`) — those are the service's concern.
|
|
16
|
-
*/
|
|
17
|
-
export interface RunRequest {
|
|
18
|
-
tasks: readonly string[]
|
|
19
|
-
cwd: string
|
|
20
|
-
projects?: readonly string[]
|
|
21
|
-
concurrency?: number
|
|
22
|
-
cache?: CachePolicy
|
|
23
|
-
/** `--cache-dir <path>` override (cwd-relative). */
|
|
24
|
-
cacheDir?: string
|
|
25
|
-
continueMode?: ContinueMode
|
|
26
|
-
frozen?: boolean
|
|
27
|
-
/** Run-level retry default (`--retry <n>`); explicit `exec.retries` wins. */
|
|
28
|
-
retries?: number
|
|
29
|
-
/** Run-level task-timeout default (ms); per-task `exec.timeout` wins. */
|
|
30
|
-
timeout?: number
|
|
31
|
-
/** Explicit `--memory` budget (bytes) for `exec.resources` packing; the
|
|
32
|
-
* default (executing side's os.totalmem()) resolves where tasks run. */
|
|
33
|
-
memory?: number
|
|
34
|
-
/** Cache-correctness verification (`--verify`); `allow` as an array (Sets
|
|
35
|
-
* don't serialize). `fingerprint` is additive-optional — an old serve
|
|
36
|
-
* ignores it, so a delegated `=fingerprint` run against an old serve
|
|
37
|
-
* degrades to a harmless no-op verify. */
|
|
38
|
-
verify?: {
|
|
39
|
-
determinism: boolean
|
|
40
|
-
inputs: boolean
|
|
41
|
-
fingerprint?: boolean
|
|
42
|
-
allow: readonly string[]
|
|
43
|
-
}
|
|
44
|
-
flow?: 'focused' | 'broad'
|
|
45
|
-
outputLogs?: 'full' | 'errors-only' | 'none'
|
|
46
|
-
excludeDependencies?: 'all' | readonly string[]
|
|
47
|
-
forwardArgs?: readonly string[]
|
|
48
|
-
summarize?: string
|
|
49
|
-
profile?: string
|
|
50
|
-
/** Free-form key/value labels from `--tag`, persisted on the invocation row. */
|
|
51
|
-
tags?: Record<string, string>
|
|
52
|
-
/** The raw `vx run …` command string, recorded on the invocation row. */
|
|
53
|
-
command?: string
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface RunResult {
|
|
57
|
-
ok: boolean
|
|
58
|
-
outcomes: OutcomeView[]
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Where a `vx run` executes. The client submits a serializable
|
|
63
|
-
* `RunRequest` and gets back a `RunResult`; it neither knows nor cares
|
|
64
|
-
* whether the work happened in-process or was delegated to a service.
|
|
65
|
-
* This is the currency of the `backend` plugin capability — part of the
|
|
66
|
-
* public wire contract (it lives here, not in `cli`, so a plugin can
|
|
67
|
-
* reference it without a `cli` import).
|
|
68
|
-
*/
|
|
69
|
-
export interface RunBackend {
|
|
70
|
-
run(request: RunRequest): Promise<RunResult>
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* service → client. The submitter contract: an event stream, a final
|
|
75
|
-
* result, or an error. The distributed-execution variants
|
|
76
|
-
* (`task:assign`/`agent:refused`/`coord:drain`) live in a distribution
|
|
77
|
-
* plugin's protocol module — its WS handler unions this with those.
|
|
78
|
-
*/
|
|
79
|
-
export type ServerMessage =
|
|
80
|
-
| { t: 'event'; event: WireEvent }
|
|
81
|
-
| { t: 'result'; result: RunResult }
|
|
82
|
-
| { t: 'error'; message: string }
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* client → service. The submitter contract: a run submission. The
|
|
86
|
-
* agent-side `agent:*` family lives in a distribution plugin's protocol
|
|
87
|
-
* module.
|
|
88
|
-
*/
|
|
89
|
-
export type ClientMessage = { t: 'run'; request: RunRequest }
|
|
90
|
-
|
|
91
|
-
/** Project resolved `RunOptions` down to the serializable request. */
|
|
92
|
-
export function optionsToRequest(options: RunOptions): RunRequest {
|
|
93
|
-
const req: RunRequest = { tasks: options.tasks, cwd: options.cwd }
|
|
94
|
-
if (options.projects !== undefined) req.projects = options.projects
|
|
95
|
-
if (options.concurrency !== undefined) req.concurrency = options.concurrency
|
|
96
|
-
if (options.cache !== undefined) req.cache = options.cache
|
|
97
|
-
if (options.cacheDir !== undefined) req.cacheDir = options.cacheDir
|
|
98
|
-
if (options.continueMode !== undefined) req.continueMode = options.continueMode
|
|
99
|
-
if (options.frozen !== undefined) req.frozen = options.frozen
|
|
100
|
-
if (options.retries !== undefined) req.retries = options.retries
|
|
101
|
-
if (options.timeout !== undefined) req.timeout = options.timeout
|
|
102
|
-
if (options.memory !== undefined) req.memory = options.memory
|
|
103
|
-
if (options.verify !== undefined)
|
|
104
|
-
req.verify = {
|
|
105
|
-
determinism: options.verify.determinism,
|
|
106
|
-
inputs: options.verify.inputs,
|
|
107
|
-
fingerprint: options.verify.fingerprint,
|
|
108
|
-
allow: [...options.verify.allow],
|
|
109
|
-
}
|
|
110
|
-
if (options.flow !== undefined) req.flow = options.flow
|
|
111
|
-
if (options.outputLogs !== undefined) req.outputLogs = options.outputLogs
|
|
112
|
-
if (options.excludeDependencies !== undefined)
|
|
113
|
-
req.excludeDependencies = options.excludeDependencies
|
|
114
|
-
if (options.forwardArgs !== undefined) req.forwardArgs = options.forwardArgs
|
|
115
|
-
if (options.summarize !== undefined) req.summarize = options.summarize
|
|
116
|
-
if (options.profile !== undefined) req.profile = options.profile
|
|
117
|
-
if (options.tags !== undefined) req.tags = options.tags
|
|
118
|
-
if (options.command !== undefined) req.command = options.command
|
|
119
|
-
return req
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Rebuild `RunOptions` from a request, for the side that actually executes
|
|
124
|
-
* (the local backend, or the service). The caller adds the host-side
|
|
125
|
-
* fields it needs (`bus`, `log`, `handleSignals`).
|
|
126
|
-
*/
|
|
127
|
-
export function requestToOptions(request: RunRequest): RunOptions {
|
|
128
|
-
const options: RunOptions = { cwd: request.cwd, tasks: [...request.tasks] }
|
|
129
|
-
if (request.projects !== undefined) options.projects = [...request.projects]
|
|
130
|
-
if (request.concurrency !== undefined) options.concurrency = request.concurrency
|
|
131
|
-
if (request.cache !== undefined) options.cache = request.cache
|
|
132
|
-
if (request.cacheDir !== undefined) options.cacheDir = request.cacheDir
|
|
133
|
-
if (request.continueMode !== undefined) options.continueMode = request.continueMode
|
|
134
|
-
if (request.frozen !== undefined) options.frozen = request.frozen
|
|
135
|
-
if (request.retries !== undefined) options.retries = request.retries
|
|
136
|
-
if (request.timeout !== undefined) options.timeout = request.timeout
|
|
137
|
-
if (request.memory !== undefined) options.memory = request.memory
|
|
138
|
-
if (request.verify !== undefined)
|
|
139
|
-
options.verify = {
|
|
140
|
-
determinism: request.verify.determinism,
|
|
141
|
-
inputs: request.verify.inputs,
|
|
142
|
-
fingerprint: request.verify.fingerprint ?? false,
|
|
143
|
-
allow: new Set(request.verify.allow),
|
|
144
|
-
}
|
|
145
|
-
if (request.flow !== undefined) options.flow = request.flow
|
|
146
|
-
if (request.outputLogs !== undefined) options.outputLogs = request.outputLogs
|
|
147
|
-
if (request.excludeDependencies !== undefined) {
|
|
148
|
-
options.excludeDependencies = request.excludeDependencies
|
|
149
|
-
}
|
|
150
|
-
if (request.forwardArgs !== undefined) options.forwardArgs = request.forwardArgs
|
|
151
|
-
if (request.summarize !== undefined) options.summarize = request.summarize
|
|
152
|
-
if (request.profile !== undefined) options.profile = request.profile
|
|
153
|
-
if (request.tags !== undefined) options.tags = request.tags
|
|
154
|
-
if (request.command !== undefined) options.command = request.command
|
|
155
|
-
return options
|
|
156
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
// The derived run aggregate — the single view-model every surface holds.
|
|
2
|
-
// Raw RunEvents are the source of truth; this reduces them into the same
|
|
3
|
-
// counters/per-task status the terminal summary already renders, so a
|
|
4
|
-
// web/TUI/MCP surface can render reactively from one place instead of
|
|
5
|
-
// re-deriving. A devframe shared-state holds a RunState and re-reduces on
|
|
6
|
-
// each event; the terminal keeps its own inline counters (byte-identical,
|
|
7
|
-
// untouched). See docs/design/event-stream-2026-06.md.
|
|
8
|
-
|
|
9
|
-
import { isGroupTask, type TaskStatus } from '../graph/index.js'
|
|
10
|
-
import type { RunEvent } from './events.js'
|
|
11
|
-
|
|
12
|
-
/** Per-task lifecycle as seen by a surface. */
|
|
13
|
-
export type TaskState = 'running' | TaskStatus
|
|
14
|
-
|
|
15
|
-
export interface TaskRecord {
|
|
16
|
-
id: string
|
|
17
|
-
state: TaskState
|
|
18
|
-
/** Final duration once complete; absent while running. */
|
|
19
|
-
durationMs?: number
|
|
20
|
-
/** True for a persistent task left running after its outcome landed. */
|
|
21
|
-
persistent?: boolean
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Cache-miss duration spread — the same numbers the final summary shows. */
|
|
25
|
-
export interface Spread {
|
|
26
|
-
maxMs: number
|
|
27
|
-
minMs: number
|
|
28
|
-
sumMs: number
|
|
29
|
-
count: number
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface RunState {
|
|
33
|
-
total: number
|
|
34
|
-
done: number
|
|
35
|
-
failed: number
|
|
36
|
-
succeeded: number
|
|
37
|
-
upToDate: number
|
|
38
|
-
restoredLocal: number
|
|
39
|
-
restoredRemote: number
|
|
40
|
-
skipped: number
|
|
41
|
-
/** Ids currently executing (a worker holds them). */
|
|
42
|
-
running: string[]
|
|
43
|
-
/** Per-task records, keyed by id. Group tasks are excluded (no work). */
|
|
44
|
-
tasks: Record<string, TaskRecord>
|
|
45
|
-
spread: Spread | null
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function initRunState(total = 0): RunState {
|
|
49
|
-
return {
|
|
50
|
-
total,
|
|
51
|
-
done: 0,
|
|
52
|
-
failed: 0,
|
|
53
|
-
succeeded: 0,
|
|
54
|
-
upToDate: 0,
|
|
55
|
-
restoredLocal: 0,
|
|
56
|
-
restoredRemote: 0,
|
|
57
|
-
skipped: 0,
|
|
58
|
-
running: [],
|
|
59
|
-
tasks: {},
|
|
60
|
-
spread: null,
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Fold one event into the aggregate, returning a NEW state (pure — never
|
|
66
|
-
* mutates `prev`, so it's safe to feed a devframe `sharedState.mutate`
|
|
67
|
-
* that diffs old vs new). Mirrors `defaultLogger`'s inline bookkeeping:
|
|
68
|
-
* group tasks (no exec) are ignored, an `aborted` task reverts to pending
|
|
69
|
-
* (freed, never counted), and the spread tracks success+failed only.
|
|
70
|
-
*/
|
|
71
|
-
export function reduce(prev: RunState, event: RunEvent): RunState {
|
|
72
|
-
switch (event.kind) {
|
|
73
|
-
case 'run:start':
|
|
74
|
-
return initRunState(event.info.total)
|
|
75
|
-
|
|
76
|
-
case 'task:start': {
|
|
77
|
-
if (isGroupTask(event.node)) return prev
|
|
78
|
-
const id = event.node.id
|
|
79
|
-
return {
|
|
80
|
-
...prev,
|
|
81
|
-
running: [...prev.running, id],
|
|
82
|
-
tasks: { ...prev.tasks, [id]: { id, state: 'running' } },
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
case 'task:complete': {
|
|
87
|
-
if (isGroupTask(event.node)) return prev
|
|
88
|
-
const id = event.node.id
|
|
89
|
-
const { status, durationMs } = event.outcome
|
|
90
|
-
const running = prev.running.filter((r) => r !== id)
|
|
91
|
-
|
|
92
|
-
// Aborted (killed by a shutdown signal): free the slot, count
|
|
93
|
-
// nothing, drop the record — the run is tearing down.
|
|
94
|
-
if (status === 'aborted') {
|
|
95
|
-
const tasks = { ...prev.tasks }
|
|
96
|
-
delete tasks[id]
|
|
97
|
-
return { ...prev, running, tasks }
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const persistent = event.node.config.exec?.persistent !== undefined && status === 'success'
|
|
101
|
-
const next: RunState = {
|
|
102
|
-
...prev,
|
|
103
|
-
done: prev.done + 1,
|
|
104
|
-
running,
|
|
105
|
-
tasks: {
|
|
106
|
-
...prev.tasks,
|
|
107
|
-
[id]: { id, state: status, durationMs, ...(persistent ? { persistent: true } : {}) },
|
|
108
|
-
},
|
|
109
|
-
}
|
|
110
|
-
switch (status) {
|
|
111
|
-
case 'success':
|
|
112
|
-
next.succeeded = prev.succeeded + 1
|
|
113
|
-
break
|
|
114
|
-
case 'cache-hit':
|
|
115
|
-
if (event.outcome.restored === false) next.upToDate = prev.upToDate + 1
|
|
116
|
-
else next.restoredLocal = prev.restoredLocal + 1
|
|
117
|
-
break
|
|
118
|
-
case 'cache-hit-remote':
|
|
119
|
-
if (event.outcome.restored === false) next.upToDate = prev.upToDate + 1
|
|
120
|
-
else next.restoredRemote = prev.restoredRemote + 1
|
|
121
|
-
break
|
|
122
|
-
case 'failed':
|
|
123
|
-
next.failed = prev.failed + 1
|
|
124
|
-
break
|
|
125
|
-
case 'skipped':
|
|
126
|
-
next.skipped = prev.skipped + 1
|
|
127
|
-
break
|
|
128
|
-
}
|
|
129
|
-
if (status === 'success' || status === 'failed') {
|
|
130
|
-
const s = prev.spread
|
|
131
|
-
next.spread = {
|
|
132
|
-
maxMs: Math.max(s?.maxMs ?? 0, durationMs),
|
|
133
|
-
minMs: Math.min(s?.minMs ?? Infinity, durationMs),
|
|
134
|
-
sumMs: (s?.sumMs ?? 0) + durationMs,
|
|
135
|
-
count: (s?.count ?? 0) + 1,
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return next
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// stdout/stderr/status/end carry no aggregate signal.
|
|
142
|
-
default:
|
|
143
|
-
return prev
|
|
144
|
-
}
|
|
145
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
// The inverse of `wireForwarder`: turn a stream of `WireEvent`s back into
|
|
2
|
-
// `Logger` calls on a concrete renderer. A `vx serve` client feeds the
|
|
3
|
-
// service's event stream through this into a normal `defaultLogger`, so a
|
|
4
|
-
// DELEGATED run renders identically to a local one — the terminal
|
|
5
|
-
// renderer stays completely untouched (it just receives reconstructed
|
|
6
|
-
// node-shaped objects instead of live graph nodes). This file is the
|
|
7
|
-
// whole adapter; it can be rewritten or removed without touching the
|
|
8
|
-
// renderer or the producer side.
|
|
9
|
-
|
|
10
|
-
import type { TaskNode, TaskOutcome } from '../graph/index.js'
|
|
11
|
-
import type { Logger } from './logger.js'
|
|
12
|
-
import type { OutcomeView, TaskView, WireEvent } from './events.js'
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Rebuild a node-shaped object from a `TaskView`. The formatters only ever
|
|
16
|
-
* read `id` / `projectName` / `taskName` / `requested` / `surfaced` and
|
|
17
|
-
* `config.exec` (presence ⇒ not a group; `.command`; `.persistent`), so a
|
|
18
|
-
* structural stand-in is indistinguishable from a real `TaskNode` to them.
|
|
19
|
-
*/
|
|
20
|
-
function reconstructNode(view: TaskView): TaskNode {
|
|
21
|
-
const exec = view.isGroup
|
|
22
|
-
? undefined
|
|
23
|
-
: {
|
|
24
|
-
command: view.command ?? '',
|
|
25
|
-
...(view.persistent ? { persistent: { readyWhen: '' } } : {}),
|
|
26
|
-
}
|
|
27
|
-
return {
|
|
28
|
-
id: view.id,
|
|
29
|
-
projectName: view.project,
|
|
30
|
-
taskName: view.task,
|
|
31
|
-
requested: view.requested,
|
|
32
|
-
surfaced: view.surfaced,
|
|
33
|
-
config: exec === undefined ? {} : { exec },
|
|
34
|
-
} as unknown as TaskNode
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function reconstructOutcome(view: OutcomeView, node: TaskNode): TaskOutcome {
|
|
38
|
-
return {
|
|
39
|
-
node,
|
|
40
|
-
status: view.status,
|
|
41
|
-
exitCode: view.exitCode,
|
|
42
|
-
durationMs: view.durationMs,
|
|
43
|
-
...(view.restored !== undefined ? { restored: view.restored } : {}),
|
|
44
|
-
...(view.sandboxViolations !== undefined ? { sandboxViolations: view.sandboxViolations } : {}),
|
|
45
|
-
...(view.sandboxViolationLines !== undefined
|
|
46
|
-
? { sandboxViolationLines: view.sandboxViolationLines }
|
|
47
|
-
: {}),
|
|
48
|
-
} as TaskOutcome
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Build a `WireEvent` consumer that drives `sink` (typically a
|
|
53
|
-
* `defaultLogger`). Stateful: it accumulates the reconstructed nodes from
|
|
54
|
-
* `task:start` so later `task:*` events can resolve their node by id.
|
|
55
|
-
*/
|
|
56
|
-
export function createWireRenderer(sink: Logger): (event: WireEvent) => void {
|
|
57
|
-
const nodes = new Map<string, TaskNode>()
|
|
58
|
-
return (event) => {
|
|
59
|
-
switch (event.kind) {
|
|
60
|
-
case 'run:start':
|
|
61
|
-
sink.runStart?.(event.info)
|
|
62
|
-
return
|
|
63
|
-
case 'task:start': {
|
|
64
|
-
const node = reconstructNode(event.task)
|
|
65
|
-
nodes.set(node.id, node)
|
|
66
|
-
sink.taskStart?.(node)
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
case 'task:stdout': {
|
|
70
|
-
const node = nodes.get(event.taskId)
|
|
71
|
-
if (node) sink.taskStdout(node, event.chunk)
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
case 'task:stderr': {
|
|
75
|
-
const node = nodes.get(event.taskId)
|
|
76
|
-
if (node) sink.taskStderr(node, event.chunk)
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
case 'task:complete': {
|
|
80
|
-
const node = nodes.get(event.outcome.taskId)
|
|
81
|
-
if (node) sink.taskComplete(node, reconstructOutcome(event.outcome, node))
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
case 'run:status':
|
|
85
|
-
sink.status(event.line)
|
|
86
|
-
return
|
|
87
|
-
case 'run:end':
|
|
88
|
-
sink.runEnd?.()
|
|
89
|
-
return
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|