@vecteur/cli 0.5.5 → 0.5.6

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/bin/vecteur.js CHANGED
@@ -9,9 +9,9 @@ import {
9
9
  import {
10
10
  clearCredentials, credentialsPath, readCredentials, setCurrentOrigin, writeCredentials, selectProject,
11
11
  } from "../src/credentials.js";
12
- import { downloadArtifact } from "../src/download.js";
12
+ import { downloadArtifact, downloadProjection } from "../src/download.js";
13
13
  import { humanProjects, resolveNamed, resolveRunProject, resolveWorkspace } from "../src/projects.js";
14
- import { canonicalJson, human, humanCost, progress } from "../src/format.js";
14
+ import { canonicalJson, currentTransferTables, human, humanCost, progress } from "../src/format.js";
15
15
 
16
16
  function manifest() {
17
17
  return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
@@ -43,7 +43,7 @@ function refuseOldNode() {
43
43
 
44
44
  function helpText(version) {
45
45
  return `vecteur ${version}
46
- login | logout | whoami | project list | project create <name> [--workspace <name-or-id>] | project use <name-or-id> | project get <id> | run submit [-p <id>] -a <ask> | run get <id> | run events <id> | artifact get <id> [--output <path>] [-p <id>] | mcp
46
+ login | logout | whoami | project list | project create <name> [--workspace <name-or-id>] | project use <name-or-id> | project get <id> | run submit [-p <id>] -a <ask> | run get <id> | run events <id> | artifact get <id> [--output <path>] [-p <id>] | projection get <run-id> <projection-id> [--output <path>] [-p <id>] | mcp
47
47
  --help --version --json --origin <url> --no-browser --local -p/--project -a/--ask --output <path>
48
48
  `;
49
49
  }
@@ -156,6 +156,10 @@ function parseCommand(argv) {
156
156
  && ask === undefined) {
157
157
  return { kind: "artifact_get", id: args[2], projectId, output, origin, json };
158
158
  }
159
+ if (args.length === 4 && args[0] === "projection" && args[1] === "get"
160
+ && value(args[2]) && value(args[3]) && ask === undefined) {
161
+ return { kind: "projection_get", runId: args[2], id: args[3], projectId, output, origin, json };
162
+ }
159
163
  throw new CliError("usage_invalid", "Invalid command grammar");
160
164
  }
161
165
 
@@ -319,11 +323,23 @@ async function main() {
319
323
  : `Downloaded ${value.bytes} bytes to ${value.path} (${value.content_type})\n`);
320
324
  return;
321
325
  }
326
+ else if (command.kind === "projection_get") {
327
+ const projectId = await resolveRunProject(client, command.projectId);
328
+ const run = await client.runGet(command.runId);
329
+ const matches = run.events.filter(event => event.kind === "engineering_projection"
330
+ && event.payload?.projection_id === command.id && event.payload?.kind === "transfer_table");
331
+ if (matches.length !== 1) throw new CliError("projection_invalid", "Run has no unique transfer table projection");
332
+ value = await downloadProjection(client, projectId, command.runId, matches[0].payload, command.output);
333
+ process.stdout.write(command.json ? canonicalJson(value)
334
+ : `Downloaded ${value.bytes} bytes to ${value.path} (${value.content_type}; ${value.disposition})\n`);
335
+ return;
336
+ }
322
337
  else if (command.kind === "run_submit") {
323
338
  const projectId = await resolveRunProject(client, command.projectId);
324
339
  value = await client.runSubmit(projectId, command.ask, onEvent);
325
340
  if (!command.json) {
326
- process.stdout.write(human(value, { projectId }));
341
+ const currentTransferIds = await verifiedTransferIds(client, value);
342
+ process.stdout.write(human(value, { projectId, currentTransferIds }));
327
343
  try {
328
344
  const cost = await client.customerCost(projectId);
329
345
  process.stdout.write(`\n${humanCost(cost)}`);
@@ -341,9 +357,23 @@ async function main() {
341
357
  if (!command.json && !renderOptions.projectId && config.projectId) {
342
358
  renderOptions = { projectId: config.projectId };
343
359
  }
360
+ if (!command.json && Array.isArray(value?.events)) {
361
+ renderOptions.currentTransferIds = await verifiedTransferIds(client, value);
362
+ }
344
363
  process.stdout.write(command.json ? canonicalJson(value) : human(value, renderOptions));
345
364
  }
346
365
 
366
+ async function verifiedTransferIds(client, run) {
367
+ const ids = new Set();
368
+ for (const projection of currentTransferTables(run.events ?? [])) {
369
+ const read = await client.projectionGet(
370
+ projection.project_id, run.run_id, projection, async () => {},
371
+ );
372
+ if (read.disposition === "current") ids.add(projection.projection_id);
373
+ }
374
+ return ids;
375
+ }
376
+
347
377
  main().catch((error) => {
348
378
  const code = error instanceof CliError ? error.code : "internal_error";
349
379
  process.stderr.write(`${code}: ${redact(error?.message ?? "CLI failed")}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vecteur/cli",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "vecteur": "bin/vecteur.js"
package/src/client.js CHANGED
@@ -535,6 +535,56 @@ export class VecteurClient {
535
535
  }
536
536
  }
537
537
 
538
+ async projectionGet(projectId, runId, projection, onChunk, signal = undefined) {
539
+ const ref = projection?.payload_ref;
540
+ if (typeof projectId !== "string" || !projectId || typeof runId !== "string" || !runId
541
+ || projection?.kind !== "transfer_table" || projection?.run_id !== runId
542
+ || projection?.project_id !== projectId
543
+ || !/^projection-[0-9a-f]{64}$/.test(projection?.projection_id ?? "")
544
+ || ref?.schema !== "vecteur.owner-transfer-table.v1"
545
+ || ref?.media_type !== "application/vnd.vecteur.owner-transfer-table+json"
546
+ || !/^[0-9a-f]{64}$/.test(ref?.payload_sha256 ?? "")
547
+ || typeof onChunk !== "function") {
548
+ throw new CliError("projection_invalid", "projection get requires a sealed transfer table on this Run");
549
+ }
550
+ const route = `/api/projects/${encodeURIComponent(projectId)}/runs/${encodeURIComponent(runId)}`
551
+ + `/projections/${encodeURIComponent(projection.projection_id)}/payload`;
552
+ const response = await this.request("GET", route, {
553
+ accept: ref.media_type, acceptEncoding: "identity", signal,
554
+ });
555
+ try {
556
+ if (response.headers.get("content-type")?.split(";", 1)[0] !== ref.media_type
557
+ || (response.headers.get("content-encoding") ?? "identity").toLowerCase() !== "identity") {
558
+ throw new CliError("projection_invalid", "Server returned the wrong projection media type");
559
+ }
560
+ const disposition = response.headers.get("x-vecteur-projection-disposition");
561
+ if (disposition !== "current" && disposition !== "historical") {
562
+ throw new CliError("projection_invalid", "Server returned no valid projection disposition");
563
+ }
564
+ const declared = response.headers.get("content-length");
565
+ if (declared === null || !/^(?:0|[1-9][0-9]*)$/.test(declared)
566
+ || Number(declared) > 256 * 1024 || !response.body) {
567
+ throw new CliError("projection_invalid", "Projection byte length is invalid");
568
+ }
569
+ const digest = createHash("sha256");
570
+ let bytes = 0;
571
+ for await (const chunk of response.body) {
572
+ response[RESPONSE_ACTIVITY]?.();
573
+ bytes += chunk.byteLength;
574
+ if (bytes > Number(declared)) throw new CliError("projection_invalid", "Projection exceeds its declared size");
575
+ digest.update(chunk);
576
+ await onChunk(chunk);
577
+ }
578
+ if (bytes !== Number(declared) || digest.digest("hex") !== ref.payload_sha256) {
579
+ throw new CliError("projection_invalid", "Projection bytes differ from RunWire");
580
+ }
581
+ return { projection_id: projection.projection_id, project_id: projectId,
582
+ run_id: runId, content_type: ref.media_type, bytes, disposition };
583
+ } finally {
584
+ response[RELEASE_TIMEOUT]?.();
585
+ }
586
+ }
587
+
538
588
  async runSubmit(projectId, ask, onEvent = null, signal = undefined) {
539
589
  const value = await this.#json("POST", `/api/projects/${encodeURIComponent(projectId)}/runs`, {
540
590
  body: { ask, attachments: [], parent_state_hash: null, ui_context: null },
package/src/contract.js CHANGED
@@ -1,9 +1,10 @@
1
1
  // GENERATED by scripts/gen-contract.mjs from owner TypeScript projections.
2
- export const OWNER_CONTRACT_IDENTITY = "api-sha256:642fb8f171f5b1ea89188ca730ef37f5f6611b4c5439b57c388333ab342b94d2;run-wire-sha256:ea1a9b7b5867f56968246f9e5fdad6af45095d6b478c3dfdca09d8234ea1eb6a";
2
+ export const OWNER_CONTRACT_IDENTITY = "api-sha256:8f81ea544eecd9e81da3fd56ad816ac24991cb0e68f3b1cb45180711eaf16056;run-wire-sha256:823acb39f2ff5bca7ea54db4beb4589d681bedc5673867a6e6dcdf5c212cf7db";
3
3
  export const RECOVERY_ACTIONS = Object.freeze({"delete_project":{"label":"Delete a project","href":"/projects"},"upgrade_plan":{"label":"Upgrade your plan","href":"/dashboard/billing"},"wait_for_reset":{"label":"Wait until the UTC reset","href":null}});
4
- export const EVENT_KINDS = Object.freeze(["source","step_started","step_finished","tool_call","tool_result","synthesis","artifact","terminal","heartbeat","assistant_delta","knowledge_binding","engineering_projection","provider_attempt","clarification","input_rows"]);
4
+ export const EVENT_KINDS = Object.freeze(["source","step_started","step_finished","tool_call","tool_result","synthesis","artifact","terminal","heartbeat","assistant_delta","knowledge_binding","engineering_projection","provider_attempt","clarification","clarification_evidence","input_rows"]);
5
5
  export const RETIRED_EVENT_KINDS = Object.freeze(["context_frame","cta","step_label"]);
6
6
  export const TERMINAL_FIELDS = Object.freeze(["answer","defect","deliverable_asked","evidence_dag","instruction_source","intent","missing_input_need","missing_inputs","provider_retry_offer","recovery","refusal_unfounded","state","steps","unmet_wanted"]);
7
+ export const TERMINAL_STATES = Object.freeze(["ok","partial","blocked","refused","infeasible_physics","capability_gap"]);
7
8
  export const ARTIFACT_MEDIA_TYPES = Object.freeze({"binary":"application/octet-stream","code":"text/plain","csv":"text/csv","docx":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","drive":"application/vnd.google-drive","html":"text/html","jpeg":"image/jpeg","json":"application/json","md":"text/markdown","pdf":"application/pdf","png":"image/png","svg":"image/svg+xml","xlsx":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"});
8
9
  export const TERMINAL_RECOVERY_DECLARATIONS = Object.freeze({"answer_now":[["start_run",["project_id"]]],"artifact_publication_missing":[["start_run",["project_id"]]],"artifact_store_unavailable":[["start_run",["project_id"]]],"budget_exhausted":[["start_run",["project_id"]]],"cancelled":[["start_run",["project_id"]]],"cause_unrecorded":[["start_run",["project_id"]]],"clarification_custody_invalid":[["start_run",["project_id"]]],"clarification_custody_lost":[["start_run",["project_id"]]],"clarification_resume_invalid":[["start_run",["project_id"]]],"clarification_unpersisted":[["start_run",["project_id"]]],"context_unselected":[["start_run",["project_id"]]],"credential_class_disclosure":[["start_run",["project_id"]]],"deliverable_missing":[["start_run",["project_id"]]],"design_state_missing":[["start_run",["project_id"]]],"evidence_admission_invalid":[["start_run",["project_id"]]],"evidence_artifact_invalid":[["start_run",["project_id"]]],"evidence_artifact_unbound":[["start_run",["project_id"]]],"evidence_binding_unresolved":[["start_run",["project_id"]]],"evidence_context_invalid":[["start_run",["project_id"]]],"evidence_extract_unreached":[["start_run",["project_id"]]],"evidence_missing":[["start_run",["project_id"]]],"evidence_owner_receipt_mismatch":[["start_run",["project_id"]]],"evidence_owner_receipt_missing":[["start_run",["project_id"]]],"evidence_payload_invalid":[["start_run",["project_id"]]],"evidence_reached_invalid":[["start_run",["project_id"]]],"evidence_replay_invalid":[["start_run",["project_id"]]],"evidence_run_mismatch":[["start_run",["project_id"]]],"evidence_seal_collision":[["start_run",["project_id"]]],"evidence_selected_unreached":[["start_run",["project_id"]]],"execution_route_invalid":[["start_run",["project_id"]]],"executor_root_invalid":[["start_run",["project_id"]]],"extraction_offer_invalid":[["start_run",["project_id"]]],"extraction_offer_tampered":[["start_run",["project_id"]]],"fact_support_unread":[["start_run",["project_id"]]],"harness_arm_invalid":[["start_run",["project_id"]]],"harness_event_unmapped":[["start_run",["project_id"]]],"harness_exited":[["start_run",["project_id"]]],"harness_mapping_failed":[["start_run",["project_id"]]],"harness_session_invalid":[["start_run",["project_id"]]],"harness_session_missing":[["start_run",["project_id"]]],"harness_timeout":[["start_run",["project_id"]]],"infeasible_physics":[["start_run",["project_id"]]],"lab_denied":[["start_run",["project_id"]]],"lab_failed":[["start_run",["project_id"]]],"lab_not_reproduced":[["start_run",["project_id"]]],"lease_interval_invalid":[["start_run",["project_id"]]],"ledger_malformed":[["start_run",["project_id"]]],"max_tokens":[["start_run",["project_id"]]],"no_facts":[["start_run",["project_id"]]],"not_a_decision_i_can_make":[["start_run",["project_id"]]],"out_of_physics":[["start_run",["project_id"]]],"profile_eligibility_changed":[["start_run",["project_id"]]],"prompt_provenance_unavailable":[["start_run",["project_id"]]],"provider_budget_exhausted":[["start_run",["project_id"]]],"provider_budget_missing":[["start_run",["project_id"]]],"provider_budget_unavailable":[["start_run",["project_id"]]],"provider_relay_absent":[["start_run",["project_id"]]],"provider_route_control_invalid":[["start_run",["project_id"]]],"provider_route_exhausted":[["start_run",["project_id"]],["retry_run",["source_run_id","offer_id","generation"]]],"provider_route_failure_unobserved":[["start_run",["project_id"]]],"provider_route_reconciliation_invalid":[["start_run",["project_id"]]],"provider_route_restart_terminal":[["start_run",["project_id"]]],"provider_training_opted_in":[["start_run",["project_id"]]],"publish_seal_missing":[["start_run",["project_id"]]],"refusal_unfounded":[["start_run",["project_id"]]],"replay_capture_digest_mismatch":[["start_run",["project_id"]]],"replay_capture_invalid":[["start_run",["project_id"]]],"replay_config_incomplete":[["start_run",["project_id"]]],"replay_failed":[["start_run",["project_id"]]],"replay_no_requests":[["start_run",["project_id"]]],"replay_patch_invalid":[["start_run",["project_id"]]],"replay_plugin_absent":[["start_run",["project_id"]]],"replay_provider_socket":[["start_run",["project_id"]]],"replay_receipt_invalid":[["start_run",["project_id"]]],"replay_receipt_owner_chown":[["start_run",["project_id"]]],"replay_receipt_owner_incomplete":[["start_run",["project_id"]]],"replay_receipt_owner_invalid":[["start_run",["project_id"]]],"replay_request_observation_count":[["start_run",["project_id"]]],"replay_request_observation_invalid":[["start_run",["project_id"]]],"replay_request_observation_missing":[["start_run",["project_id"]]],"replay_request_owner_chown":[["start_run",["project_id"]]],"replay_socket_observation_invalid":[["start_run",["project_id"]]],"replay_socket_observation_missing":[["start_run",["project_id"]]],"replay_socket_owner_chown":[["start_run",["project_id"]]],"row_budget":[["start_run",["project_id"]]],"run_database_capability_refused":[["start_run",["project_id"]]],"runtime_unavailable":[["start_run",["project_id"]]],"sealed_contract_violated":[["start_run",["project_id"]]],"sealed_file_missing":[["start_run",["project_id"]]],"seat_patch_absent":[["start_run",["project_id"]]],"seat_patch_drift":[["start_run",["project_id"]]],"selection_admission_invalid":[["start_run",["project_id"]]],"selection_context_invalid":[["start_run",["project_id"]]],"selection_receipt_conflict":[["start_run",["project_id"]]],"selection_receipt_invalid":[["start_run",["project_id"]]],"selection_receipt_missing":[["start_run",["project_id"]]],"selection_required":[["start_run",["project_id"]]],"state_corrupt":[["start_run",["project_id"]]],"state_file_invalid":[["start_run",["project_id"]]],"state_invalid":[["start_run",["project_id"]]],"state_payload_overflow":[["start_run",["project_id"]]],"state_unencodable":[["start_run",["project_id"]]],"state_version_unknown":[["start_run",["project_id"]]],"unreceipted_numbers":[["start_run",["project_id"]]],"usage_egress_mismatch":[["start_run",["project_id"]]],"wall_budget":[["start_run",["project_id"]]],"wanted_not_computed":[["start_run",["project_id"]]]});
9
10
  export const ORIGIN_KINDS = Object.freeze(["user","hypothesis","derived","knowledge","file","catalog","default"]);
@@ -47,6 +48,6 @@ const dimension=(v)=>shape(v,["uncached_input","cache_read","cache_write","outpu
47
48
  const usage=(v)=>exact(v,["coverage","provider_id","model_id","observed_at","billable_dimensions"])&&["complete","incomplete"].includes(v.coverage)&&[v.provider_id,v.model_id].every(text)&&integer(v.observed_at)&&v.observed_at>=0&&Array.isArray(v.billable_dimensions)&&v.billable_dimensions.length>0&&v.billable_dimensions.every(dimension);
48
49
  const control=v=>ownerRunControl(v)&&/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(v.id)&&integer(v.sequence)&&v.sequence>=1&&v.sequence<=32&&(v.kind==="steer"?typeof v.text==="string"&&v.text.trim().length>0&&Buffer.byteLength(v.text,"utf8")<=8192:v.text===undefined||v.text===null)&&((v.attempt===null&&v.step===null)||(integer(v.attempt)&&v.attempt>=0&&integer(v.step)&&v.step>0))&&(v.egress===null||(integer(v.egress.candidate)&&v.egress.candidate>=0&&integer(v.egress.call)&&v.egress.call>=0))&&(v.state==="queued"?v.attempt===null&&v.egress===null:v.state==="claimed"?v.attempt!==null&&v.egress===null:v.state==="dispatched"?v.attempt!==null&&v.egress!==null:v.egress===null);
49
50
  const controls=v=>Array.isArray(v)&&v.length<=32&&v.every(control)&&new Set(v.map(x=>x.id)).size===v.length&&v.every((x,i)=>i===0||v[i-1].sequence<x.sequence);
50
- export const validateAdmitted=(v)=>shape(v,["run_id","state_hash","result_hash","requested_route_id","profile","terminal","usage","events","controls"],["duration_seconds"])&&[v.run_id,v.state_hash,v.result_hash,v.requested_route_id,v.profile].every(text)&&["ok","partial","blocked","refused","infeasible_physics","capability_gap"].includes(v.terminal)&&usage(v.usage)&&Array.isArray(v.events)&&controls(v.controls)&&(v.duration_seconds===undefined||v.duration_seconds===null||(integer(v.duration_seconds)&&v.duration_seconds>=0));
51
+ export const validateAdmitted=(v)=>shape(v,["run_id","state_hash","result_hash","requested_route_id","profile","terminal","usage","events","controls"],["duration_seconds"])&&[v.run_id,v.state_hash,v.requested_route_id,v.profile].every(text)&&((v.terminal===""&&v.result_hash===""&&Array.isArray(v.events)&&(v.events.length===0||v.events.at(-1)?.kind!=="terminal"))||(TERMINAL_STATES.includes(v.terminal)&&text(v.result_hash)))&&usage(v.usage)&&Array.isArray(v.events)&&controls(v.controls)&&(v.duration_seconds===undefined||v.duration_seconds===null||(integer(v.duration_seconds)&&v.duration_seconds>=0));
51
52
  export const validatePatCreated=(v)=>exact(v,["id","name","token","prefix","project_id","scopes","created_at","expires_at"])&&[v.id,v.name,v.token,v.prefix].every(text)&&(v.project_id===null||text(v.project_id))&&Array.isArray(v.scopes)&&v.scopes.every(s=>["project:read","project:execute"].includes(s))&&integer(v.created_at)&&integer(v.expires_at);
52
53
  export const validateDeviceAuthorization=(v)=>exact(v,["device_code","user_code","verification_uri","verification_uri_complete","expires_at","interval"])&&[v.device_code,v.user_code,v.verification_uri,v.verification_uri_complete].every(text)&&integer(v.expires_at)&&integer(v.interval)&&v.interval>=0;
package/src/download.js CHANGED
@@ -14,12 +14,22 @@ function defaultName(casRef) {
14
14
  * a digest-derived basename, so traversal and shell metacharacters cannot arrive from RunWire.
15
15
  */
16
16
  export async function downloadArtifact(client, casRef, projectId, output) {
17
- const target = resolve(output ?? defaultName(casRef));
17
+ return downloadTo(output ?? defaultName(casRef),
18
+ chunk => client.artifactGet(casRef, projectId, chunk));
19
+ }
20
+
21
+ export async function downloadProjection(client, projectId, runId, projection, output) {
22
+ return downloadTo(output ?? `transfer-table-${projection.projection_id.slice(11, 23)}.json`,
23
+ chunk => client.projectionGet(projectId, runId, projection, chunk));
24
+ }
25
+
26
+ async function downloadTo(filename, fetchBytes) {
27
+ const target = resolve(filename);
18
28
  const temporary = resolve(dirname(target), `.vecteur-${basename(target)}-${randomUUID()}.part`);
19
29
  let handle;
20
30
  try {
21
31
  handle = await open(temporary, "wx", 0o600);
22
- const result = await client.artifactGet(casRef, projectId, chunk => handle.writeFile(chunk));
32
+ const result = await fetchBytes(chunk => handle.writeFile(chunk));
23
33
  await handle.sync();
24
34
  await handle.close();
25
35
  handle = undefined;
package/src/format.js CHANGED
@@ -207,8 +207,30 @@ function recoveryLine(action) {
207
207
  return `Recovery: ${action.capability}${args ? ` ${args}` : ""}`;
208
208
  }
209
209
 
210
- export function humanRun(value, { projectId, cost } = {}) {
211
- const terminal = terminalPresentation(value);
210
+ export function currentTransferTables(events) {
211
+ const candidates = new Map();
212
+ const cleared = new Map();
213
+ for (const event of events) {
214
+ if (event.kind !== "engineering_projection") continue;
215
+ const projection = event.payload;
216
+ if (projection?.kind === "projection_clear" && projection.clear_kind === "transfer_table") {
217
+ const ids = cleared.get(projection.node_id) ?? new Set();
218
+ ids.add(projection.execution_id);
219
+ cleared.set(projection.node_id, ids);
220
+ } else if (projection?.kind === "transfer_table") {
221
+ const rows = candidates.get(projection.node_id) ?? [];
222
+ rows.push(projection);
223
+ candidates.set(projection.node_id, rows);
224
+ }
225
+ }
226
+ return [...candidates.entries()].flatMap(([node, rows]) => {
227
+ const current = [...rows].reverse().find(row => !cleared.get(node)?.has(row.execution_id));
228
+ return current ? [current] : [];
229
+ });
230
+ }
231
+
232
+ export function humanRun(value, { projectId, cost, currentTransferIds } = {}) {
233
+ const terminal = value.terminal === "" ? null : terminalPresentation(value);
212
234
  const origins = value.events.flatMap(event => event.kind === "step_finished"
213
235
  ? inputLines(event.payload?.step?.inputs ?? event.payload?.inputs) : []);
214
236
  const artifacts = value.events.filter(event => event.kind === "artifact").map(event => event.payload);
@@ -217,11 +239,25 @@ export function humanRun(value, { projectId, cost } = {}) {
217
239
  }
218
240
  const tasks = taskLines(value.events);
219
241
  const assumptions = assumptionLines(value.events);
242
+ const currentTables = currentTransferTables(value.events).filter(projection =>
243
+ currentTransferIds?.has(projection.projection_id));
220
244
  const lines = [];
221
- if (terminal.answer !== undefined) lines.push(terminal.answer, "");
245
+ if (terminal?.answer !== undefined) lines.push(terminal.answer, "");
222
246
  if (tasks.length) lines.push("Steps:", ...tasks, "");
223
247
  if (origins.length) lines.push("Inputs and origins:", ...origins, "");
224
248
  if (assumptions.length) lines.push("Assumptions:", ...assumptions, "");
249
+ for (const projection of currentTables) {
250
+ if (typeof projection.search_status !== "string" || projection.origin !== "unattributed"
251
+ || typeof projection.validation_status !== "string" || typeof projection.withheld !== "boolean") {
252
+ throw new CliError("projection_invalid", "Transfer table metadata is malformed");
253
+ }
254
+ lines.push(`Transfer table: ${plainLine(projection.search_status)} · input origins ${projection.origin}`
255
+ + ` · validation ${plainLine(projection.validation_status)}`
256
+ + ` · path ${projection.withheld ? "withheld" : "shown"}`);
257
+ const project = projectId ? ` --project ${shellQuote(projectId)}` : "";
258
+ lines.push(` vecteur projection get ${shellQuote(value.run_id)} ${shellQuote(projection.projection_id)}`
259
+ + ` --output ${shellQuote("transfer-table.json")}${project}`, "");
260
+ }
225
261
  if (artifacts.length) {
226
262
  lines.push("Artifacts:");
227
263
  for (const artifact of artifacts) {
@@ -231,9 +267,12 @@ export function humanRun(value, { projectId, cost } = {}) {
231
267
  }
232
268
  lines.push("");
233
269
  }
234
- lines.push(`Run: ${plainLine(value.run_id)}`, `Terminal: ${terminal.state}`);
235
- if (terminal.defect !== undefined) lines.push(`Cause: ${terminal.defect}`);
236
- lines.push(...terminal.recovery.map(recoveryLine));
270
+ lines.push(`Run: ${plainLine(value.run_id)}`);
271
+ if (terminal) {
272
+ lines.push(`Terminal: ${terminal.state}`);
273
+ if (terminal.defect !== undefined) lines.push(`Cause: ${terminal.defect}`);
274
+ lines.push(...terminal.recovery.map(recoveryLine));
275
+ }
237
276
  if (cost) lines.push("", humanCost(cost).trimEnd());
238
277
  return `${lines.join("\n")}\n`;
239
278
  }
package/src/mcp.js CHANGED
@@ -3,7 +3,7 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
3
3
  import * as z from "zod/v4";
4
4
 
5
5
  import { CliError, VecteurClient, redact } from "./client.js";
6
- import { EVENT_KINDS, RETIRED_EVENT_KINDS } from "./contract.js";
6
+ import { EVENT_KINDS, RETIRED_EVENT_KINDS, TERMINAL_STATES } from "./contract.js";
7
7
  import { canonicalJson } from "./format.js";
8
8
 
9
9
  const projectInput = z.object({ project_id: z.string().min(1) }).strict();
@@ -53,9 +53,9 @@ const runControlOutput = z.object({
53
53
  text: z.string().nullable().optional(),
54
54
  }).strict();
55
55
  const admittedOutput = z.object({
56
- run_id: z.string().min(1), state_hash: z.string().min(1), result_hash: z.string().min(1),
56
+ run_id: z.string().min(1), state_hash: z.string().min(1), result_hash: z.string(),
57
57
  requested_route_id: z.string().min(1), profile: z.string().min(1),
58
- terminal: z.enum(["ok", "partial", "blocked", "refused", "infeasible_physics", "capability_gap"]),
58
+ terminal: z.union([z.enum([...TERMINAL_STATES]), z.literal("")]),
59
59
  duration_seconds: z.number().int().safe().nonnegative().nullable().optional(),
60
60
  usage: z.object({
61
61
  coverage: z.enum(["complete", "incomplete"]), provider_id: z.string().min(1),
@@ -64,7 +64,9 @@ const admittedOutput = z.object({
64
64
  }).strict(),
65
65
  events: z.array(eventOutput),
66
66
  controls: z.array(runControlOutput),
67
- }).strict();
67
+ }).strict().refine(value => value.terminal === ""
68
+ ? value.result_hash === "" && value.events.at(-1)?.kind !== "terminal"
69
+ : value.result_hash.length > 0, "Run terminal and result hash are not paired");
68
70
 
69
71
  function success(value) {
70
72
  return {
@@ -114,7 +116,7 @@ export function createVecteurMcpServer(client = new VecteurClient(), shutdownSig
114
116
  return client.runSubmit(projectId, ask, onEvent, signal(context));
115
117
  });
116
118
 
117
- tool(server, "vecteur_run_get", "Get one terminal Vecteur run record.", runInput, admittedOutput,
119
+ tool(server, "vecteur_run_get", "Get one Vecteur run record, including an open run.", runInput, admittedOutput,
118
120
  ({ run_id: runId }, context) => client.runGet(runId, signal(context)));
119
121
 
120
122
  tool(server, "vecteur_run_events", "Validate run events against its direct record.", runInput,