@vecteur/cli 0.5.5 → 0.5.8

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.8",
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:e474079a0453c07e8249eddfc6781bb0c053ee81d08b0c7988e0670ad14d302b";
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
@@ -90,17 +90,133 @@ const CAUSE_WORD = Object.freeze({
90
90
  answer_now: "stopped to answer now; what it printed stands",
91
91
  });
92
92
 
93
+ /**
94
+ * THE ENGINEERING DOOR, SPOKEN BY THIS DOOR TOO (v2.4 S3; the web reads the same bytes in
95
+ * `frontend/src/run/door.ts`, and both readers are held to `tests/fixtures/door-observations.json`).
96
+ *
97
+ * A `python` call whose summary is `run <verb>` is a call of one published engineering method;
98
+ * its result's stdout is the bounded observation `laplace_ope/door_tail.py` prints. The reader is
99
+ * tolerant: the excerpt keeps a long stream's first and last lines around `[…]`, so every section
100
+ * is optional and the last `RESULT:` line is the headline of last resort.
101
+ */
102
+ const DOOR_SUMMARY = /^run ([a-z][a-z0-9]*(?:_[a-z0-9]+)+)$/;
103
+ const DOOR_HEAD = /^([a-z][a-z0-9_]*): ok\b/;
104
+ const QUANTITY_LINE = /^ {2}(\S+) = (.*?)(?: {2}-- (.*))?$/;
105
+ const SECTION_OPEN = /^(inputs \(as given\):|inputs the owner defaulted|not set \(|quantities:|validity:|warnings:|files:|RESULT:)/;
106
+ const NUMBER = /^(-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?|true|false|nan|-?inf)(?: (.+))?$/;
107
+
108
+ function quantityParts(text) {
109
+ const trimmed = text.trim();
110
+ if (trimmed.startsWith("[") || trimmed.startsWith("{")) return { value: trimmed, unit: "" };
111
+ const number = NUMBER.exec(trimmed);
112
+ return number ? { value: number[1], unit: (number[2] ?? "").trim() } : { value: trimmed, unit: "" };
113
+ }
114
+
115
+ /** `{verb, headline: [{id, label, value, unit}], warnings}` of an observation, or null. */
116
+ export function doorObservation(text) {
117
+ const lines = String(text ?? "").split("\n").map(line => line.replace(/\s+$/, ""));
118
+ const head = DOOR_HEAD.exec(lines.find(line => line.trim()) ?? "");
119
+ if (!head) return null;
120
+ const cut = new Set();
121
+ lines.forEach((line, index) => {
122
+ if (line.trim() === "[…]") [index - 1, index, index + 1].forEach(n => cut.add(n));
123
+ });
124
+ const quantities = [];
125
+ const warnings = [];
126
+ let resultLine = "";
127
+ let section = "";
128
+ for (const [index, line] of lines.entries()) {
129
+ if (index === 0 || cut.has(index)) continue;
130
+ const open = SECTION_OPEN.exec(line);
131
+ if (open) {
132
+ section = open[1];
133
+ if (section === "warnings:" && line.includes("none")) section = "";
134
+ if (section === "RESULT:") resultLine = line.slice("RESULT:".length).trim();
135
+ continue;
136
+ }
137
+ if (section === "quantities:") {
138
+ const row = QUANTITY_LINE.exec(line);
139
+ if (row) quantities.push({ id: row[1], label: row[3]?.trim() || row[1].replaceAll("_", " "), ...quantityParts(row[2]) });
140
+ } else if (section === "warnings:" && /^ {2}- /.test(line)) {
141
+ warnings.push(line.replace(/^ {2}- /, "").trim());
142
+ }
143
+ }
144
+ const byId = new Map(quantities.map(quantity => [quantity.id, quantity]));
145
+ const headline = [];
146
+ for (const part of resultLine.split(/; (?=\S+ = )/)) {
147
+ const row = /^(\S+) = (.*)$/.exec(part.trim());
148
+ if (!row) continue;
149
+ headline.push({ id: row[1], label: byId.get(row[1])?.label ?? row[1].replaceAll("_", " "), ...quantityParts(row[2]) });
150
+ }
151
+ // No parsable RESULT line: the last three physical quantities stand in (else the first three).
152
+ if (!headline.length) headline.push(...quantities.filter(quantity => quantity.unit).slice(-3));
153
+ if (!headline.length) headline.push(...quantities.slice(0, 3));
154
+ return { verb: head[1], headline, warnings };
155
+ }
156
+
157
+ /** The door calls of each row, by `step_id`: `{purpose, verb, observation, refusal}` in call order. */
158
+ function doorCallsByStep(events) {
159
+ const results = new Map();
160
+ for (const event of events) {
161
+ if (event?.kind === "tool_result" && typeof event.payload?.call_id === "string") {
162
+ results.set(event.payload.call_id, event.payload);
163
+ }
164
+ }
165
+ const rows = new Map();
166
+ for (const event of events) {
167
+ const call = event?.kind === "tool_call" ? event.payload : null;
168
+ const verb = call?.tool === "python" ? DOOR_SUMMARY.exec(String(call.summary ?? "").trim())?.[1] : null;
169
+ if (!verb) continue;
170
+ const result = results.get(call.call_id);
171
+ const observation = result?.status === "ok" ? doorObservation(result.stdout_excerpt) : null;
172
+ if (result?.status === "ok" && observation?.verb !== verb) continue; // free code that opened with `# run x_y`
173
+ const refusal = result && result.status !== "ok"
174
+ ? String(result.stderr_excerpt || result.stdout_excerpt || "").trim().split("\n").filter(Boolean).slice(-3).join(" · ") : "";
175
+ const list = rows.get(call.step_id) ?? [];
176
+ list.push({ purpose: String(call.title ?? "").trim(), verb, observation, refusal, running: !result });
177
+ rows.set(call.step_id, list);
178
+ }
179
+ return rows;
180
+ }
181
+
182
+ function doorLine({ purpose, verb, observation, refusal, running }) {
183
+ const said = observation
184
+ ? observation.headline.map(quantity => `${quantity.label} ${quantity.value}${quantity.unit ? ` ${quantity.unit}` : ""}`).join(", ")
185
+ : (refusal ? `refused: ${refusal}` : running ? "running" : "computed");
186
+ const lines = [` ▸ ${plainLine(purpose || verb)} — ${verb}: ${plainLine(said)}`];
187
+ for (const warning of (observation?.warnings ?? []).slice(0, 2)) lines.push(` ! ${plainLine(warning)}`);
188
+ return lines.join("\n");
189
+ }
190
+
93
191
  export function taskLines(events) {
192
+ const doors = doorCallsByStep(events);
193
+ // The conclusion is engineering work (it prints the answer from evidence); its text is the
194
+ // synthesis, so the row stays one line.
195
+ const concluded = new Set(events.filter((event) => event?.kind === "tool_call" && event.payload?.tool === "conclude")
196
+ .map((event) => event.payload.step_id));
197
+ // Only a run that called the door tells engineering rows from execution rows; any other run
198
+ // prints exactly as before.
199
+ const distinguishes = doors.size > 0 || concluded.size > 0;
94
200
  return events.flatMap((event) => {
95
201
  if (event?.kind !== "step_finished") return [];
96
202
  const row = event.payload;
97
203
  if (!row || typeof row !== "object" || typeof row.step_id !== "string") return [];
204
+ const outcome = CAUSE_WORD[row.cause] ?? row.status;
205
+ const result = typeof row.result_line === "string" && row.result_line.trim() ? row.result_line : "";
206
+ if (distinguishes) {
207
+ const calls = doors.get(row.step_id);
208
+ if (calls?.length) {
209
+ const rest = row.status === "computed" ? [] : [` (${plainLine(row.title)} — ${outcome})`];
210
+ return [[...calls.map(doorLine), ...rest].join("\n")];
211
+ }
212
+ if (row.status === "asked" || concluded.has(row.step_id)) return [` ▸ ${plainLine(`${row.title} — ${outcome}`)}`];
213
+ // Execution, compressed to one line: what ran, how it ended, what it printed.
214
+ return [` · ${plainLine(`${row.title} — ${outcome}${result ? `: ${result}` : ""}`)}`];
215
+ }
98
216
  const attempts = Number.isInteger(row.attempts) && row.attempts > 0
99
217
  ? ` · ${row.attempts} ${row.attempts === 1 ? "attempt" : "attempts"}` : "";
100
- const result = typeof row.result_line === "string" && row.result_line.trim()
101
- ? `\n ${plainLine(row.result_line)}` : "";
102
- const outcome = CAUSE_WORD[row.cause] ?? row.status;
103
- return [` ${plainLine(`${row.title} — ${outcome}${attempts}`)}${result}`];
218
+ const printed = result ? `\n ${plainLine(result)}` : "";
219
+ return [` ${plainLine(`${row.title} — ${outcome}${attempts}`)}${printed}`];
104
220
  });
105
221
  }
106
222
 
@@ -207,8 +323,30 @@ function recoveryLine(action) {
207
323
  return `Recovery: ${action.capability}${args ? ` ${args}` : ""}`;
208
324
  }
209
325
 
210
- export function humanRun(value, { projectId, cost } = {}) {
211
- const terminal = terminalPresentation(value);
326
+ export function currentTransferTables(events) {
327
+ const candidates = new Map();
328
+ const cleared = new Map();
329
+ for (const event of events) {
330
+ if (event.kind !== "engineering_projection") continue;
331
+ const projection = event.payload;
332
+ if (projection?.kind === "projection_clear" && projection.clear_kind === "transfer_table") {
333
+ const ids = cleared.get(projection.node_id) ?? new Set();
334
+ ids.add(projection.execution_id);
335
+ cleared.set(projection.node_id, ids);
336
+ } else if (projection?.kind === "transfer_table") {
337
+ const rows = candidates.get(projection.node_id) ?? [];
338
+ rows.push(projection);
339
+ candidates.set(projection.node_id, rows);
340
+ }
341
+ }
342
+ return [...candidates.entries()].flatMap(([node, rows]) => {
343
+ const current = [...rows].reverse().find(row => !cleared.get(node)?.has(row.execution_id));
344
+ return current ? [current] : [];
345
+ });
346
+ }
347
+
348
+ export function humanRun(value, { projectId, cost, currentTransferIds } = {}) {
349
+ const terminal = value.terminal === "" ? null : terminalPresentation(value);
212
350
  const origins = value.events.flatMap(event => event.kind === "step_finished"
213
351
  ? inputLines(event.payload?.step?.inputs ?? event.payload?.inputs) : []);
214
352
  const artifacts = value.events.filter(event => event.kind === "artifact").map(event => event.payload);
@@ -217,11 +355,25 @@ export function humanRun(value, { projectId, cost } = {}) {
217
355
  }
218
356
  const tasks = taskLines(value.events);
219
357
  const assumptions = assumptionLines(value.events);
358
+ const currentTables = currentTransferTables(value.events).filter(projection =>
359
+ currentTransferIds?.has(projection.projection_id));
220
360
  const lines = [];
221
- if (terminal.answer !== undefined) lines.push(terminal.answer, "");
361
+ if (terminal?.answer !== undefined) lines.push(terminal.answer, "");
222
362
  if (tasks.length) lines.push("Steps:", ...tasks, "");
223
363
  if (origins.length) lines.push("Inputs and origins:", ...origins, "");
224
364
  if (assumptions.length) lines.push("Assumptions:", ...assumptions, "");
365
+ for (const projection of currentTables) {
366
+ if (typeof projection.search_status !== "string" || projection.origin !== "unattributed"
367
+ || typeof projection.validation_status !== "string" || typeof projection.withheld !== "boolean") {
368
+ throw new CliError("projection_invalid", "Transfer table metadata is malformed");
369
+ }
370
+ lines.push(`Transfer table: ${plainLine(projection.search_status)} · input origins ${projection.origin}`
371
+ + ` · validation ${plainLine(projection.validation_status)}`
372
+ + ` · path ${projection.withheld ? "withheld" : "shown"}`);
373
+ const project = projectId ? ` --project ${shellQuote(projectId)}` : "";
374
+ lines.push(` vecteur projection get ${shellQuote(value.run_id)} ${shellQuote(projection.projection_id)}`
375
+ + ` --output ${shellQuote("transfer-table.json")}${project}`, "");
376
+ }
225
377
  if (artifacts.length) {
226
378
  lines.push("Artifacts:");
227
379
  for (const artifact of artifacts) {
@@ -231,9 +383,12 @@ export function humanRun(value, { projectId, cost } = {}) {
231
383
  }
232
384
  lines.push("");
233
385
  }
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));
386
+ lines.push(`Run: ${plainLine(value.run_id)}`);
387
+ if (terminal) {
388
+ lines.push(`Terminal: ${terminal.state}`);
389
+ if (terminal.defect !== undefined) lines.push(`Cause: ${terminal.defect}`);
390
+ lines.push(...terminal.recovery.map(recoveryLine));
391
+ }
237
392
  if (cost) lines.push("", humanCost(cost).trimEnd());
238
393
  return `${lines.join("\n")}\n`;
239
394
  }
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,