@vzn/vx 0.0.15 → 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.
Files changed (111) hide show
  1. package/README.md +91 -130
  2. package/index.ts +10 -0
  3. package/launcher.mjs +10 -11
  4. package/package.json +19 -5
  5. package/plugins/local-cache/index.ts +2 -0
  6. package/plugins/local-executor/index.ts +2 -0
  7. package/plugins/schedule-history/index.ts +2 -0
  8. package/src/bin.ts +2 -2
  9. package/src/cache/archive.ts +557 -0
  10. package/src/cache/cache.ts +929 -250
  11. package/src/cache/cas-backend.ts +20 -2
  12. package/src/cache/chained-cache.ts +202 -0
  13. package/src/cache/digest.ts +0 -15
  14. package/src/cache/index.ts +13 -3
  15. package/src/cache/inputs.ts +445 -41
  16. package/src/cache/layered-cache.ts +129 -30
  17. package/src/cache/tar-stream.ts +358 -0
  18. package/src/cli/cache.ts +36 -10
  19. package/src/cli/help.ts +167 -128
  20. package/src/cli/index.ts +74 -43
  21. package/src/cli/info.ts +30 -0
  22. package/src/cli/last.ts +140 -0
  23. package/src/cli/migrate-emit.ts +21 -0
  24. package/src/cli/migrate-nx.ts +23 -0
  25. package/src/cli/migrate-scripts.ts +127 -0
  26. package/src/cli/migrate-turbo.ts +43 -6
  27. package/src/cli/migrate.ts +119 -33
  28. package/src/cli/plan-format.ts +58 -3
  29. package/src/cli/plugin-commands.ts +96 -0
  30. package/src/cli/prune.ts +291 -0
  31. package/src/cli/run.ts +272 -80
  32. package/src/cli/upgrade.ts +7 -6
  33. package/src/cli/watch.ts +259 -61
  34. package/src/cli/why.ts +193 -0
  35. package/src/config.ts +40 -23
  36. package/src/exec/env.ts +7 -1
  37. package/src/exec/executor.ts +239 -0
  38. package/src/exec/index.ts +19 -1
  39. package/src/exec/runner.ts +116 -29
  40. package/src/exec/sandbox-runtime.ts +194 -24
  41. package/src/graph/index.ts +3 -1
  42. package/src/graph/scheduler.ts +92 -18
  43. package/src/graph/task-graph.ts +220 -6
  44. package/src/index.ts +82 -84
  45. package/src/orchestrator/deferred-outputs.ts +144 -0
  46. package/src/orchestrator/download-policy.ts +171 -0
  47. package/src/orchestrator/events.ts +30 -0
  48. package/src/orchestrator/execute-task.ts +328 -94
  49. package/src/orchestrator/failure-mode.ts +61 -0
  50. package/src/orchestrator/framed-output.ts +87 -35
  51. package/src/orchestrator/history.ts +78 -31
  52. package/src/orchestrator/index.ts +43 -90
  53. package/src/orchestrator/local-shortcircuit.ts +22 -0
  54. package/src/orchestrator/logger.ts +196 -14
  55. package/src/orchestrator/metrics.ts +57 -1376
  56. package/src/orchestrator/missing-plugin.ts +9 -0
  57. package/src/orchestrator/options.ts +22 -4
  58. package/src/orchestrator/plan.ts +144 -1
  59. package/src/orchestrator/plugin-host.ts +199 -119
  60. package/src/orchestrator/plugin.ts +144 -57
  61. package/src/orchestrator/prepare.ts +220 -69
  62. package/src/orchestrator/remote-prefetch.ts +27 -8
  63. package/src/orchestrator/resources.ts +20 -34
  64. package/src/orchestrator/run-artifacts.ts +41 -16
  65. package/src/orchestrator/run-context.ts +167 -29
  66. package/src/orchestrator/run-report.ts +73 -46
  67. package/src/orchestrator/run.ts +590 -122
  68. package/src/orchestrator/stable-keys.ts +74 -33
  69. package/src/orchestrator/status-line.ts +47 -8
  70. package/src/orchestrator/summary.ts +22 -1
  71. package/src/orchestrator/tally.ts +78 -35
  72. package/src/orchestrator/task-hash.ts +105 -11
  73. package/src/orchestrator/task-log-buffer.ts +291 -0
  74. package/src/orchestrator/telemetry-host.ts +24 -6
  75. package/src/orchestrator/telemetry.ts +181 -21
  76. package/src/orchestrator/upstream.ts +72 -31
  77. package/src/orchestrator/verify.ts +12 -2
  78. package/src/plugins/local-cache/index.ts +11 -0
  79. package/src/plugins/local-executor/index.ts +45 -0
  80. package/src/plugins/schedule-history/index.ts +109 -0
  81. package/src/util/edit-distance.ts +21 -0
  82. package/src/util/errors.ts +15 -1
  83. package/src/util/index.ts +7 -2
  84. package/src/util/num.ts +37 -0
  85. package/src/util/paths.ts +43 -0
  86. package/src/util/settle.ts +48 -0
  87. package/src/util/size.ts +5 -1
  88. package/src/util/tail.ts +66 -0
  89. package/src/util/timing.ts +63 -0
  90. package/src/workspace/affected.ts +142 -15
  91. package/src/workspace/config-cache.ts +304 -0
  92. package/src/workspace/config-eval.ts +210 -0
  93. package/src/workspace/config-imports.ts +161 -0
  94. package/src/workspace/filter.ts +22 -4
  95. package/src/workspace/fingerprint.ts +16 -1
  96. package/src/workspace/index.ts +15 -3
  97. package/src/workspace/lockfile.ts +13 -2
  98. package/src/workspace/nested-dirs.ts +21 -7
  99. package/src/workspace/package-graph.ts +23 -20
  100. package/src/workspace/project-loader.ts +503 -59
  101. package/src/workspace/workspace.ts +213 -72
  102. package/src/cache/tar.ts +0 -343
  103. package/src/cli/backend.ts +0 -23
  104. package/src/cli/mcp-rpc.ts +0 -292
  105. package/src/cli/mcp.ts +0 -92
  106. package/src/orchestrator/devframe-surface.ts +0 -74
  107. package/src/orchestrator/predict.ts +0 -106
  108. package/src/orchestrator/protocol.ts +0 -156
  109. package/src/orchestrator/run-state.ts +0 -145
  110. package/src/orchestrator/wire-render.ts +0 -92
  111. 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
- }