@otto-code/protocol 0.8.19 → 0.9.2

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 (50) hide show
  1. package/dist/agent-labels.d.ts +22 -0
  2. package/dist/agent-labels.js +42 -1
  3. package/dist/agent-profiles.d.ts +1 -1
  4. package/dist/agent-profiles.js +3 -3
  5. package/dist/agent-rate-limit.d.ts +13 -0
  6. package/dist/agent-rate-limit.js +18 -0
  7. package/dist/agent-types.d.ts +6 -0
  8. package/dist/architectural-views/rpc-schemas.d.ts +465 -0
  9. package/dist/architectural-views/rpc-schemas.js +205 -0
  10. package/dist/artifacts/rpc-schemas.d.ts +360 -2
  11. package/dist/artifacts/rpc-schemas.js +75 -1
  12. package/dist/artifacts/types.d.ts +71 -0
  13. package/dist/artifacts/types.js +27 -0
  14. package/dist/binary-frames/terminal.d.ts +1 -1
  15. package/dist/brain.d.ts +3 -3
  16. package/dist/browser-automation/capabilities.d.ts +1 -1
  17. package/dist/browser-automation/rpc-schemas.d.ts +16 -16
  18. package/dist/code-intelligence.d.ts +5 -5
  19. package/dist/daemon-config.d.ts +66 -0
  20. package/dist/daemon-config.js +76 -3
  21. package/dist/file-operations.d.ts +1 -1
  22. package/dist/generated/validation/ws-outbound.aot.js +81113 -69135
  23. package/dist/loop/rpc-schemas.d.ts +6 -6
  24. package/dist/messages.d.ts +11477 -1097
  25. package/dist/messages.js +633 -117
  26. package/dist/model-preferences.d.ts +26 -0
  27. package/dist/model-preferences.js +47 -0
  28. package/dist/model-tiers.js +2 -0
  29. package/dist/personality-schemas.d.ts +4 -4
  30. package/dist/project-knowledge.d.ts +38 -16
  31. package/dist/project-knowledge.js +17 -1
  32. package/dist/provider-config.js +1 -1
  33. package/dist/provider-icon-names.js +1 -0
  34. package/dist/schedule/rpc-schemas.d.ts +92 -12
  35. package/dist/schedule/rpc-schemas.js +2 -0
  36. package/dist/schedule/types.d.ts +47 -3
  37. package/dist/schedule/types.js +22 -0
  38. package/dist/search/text-match.d.ts +16 -0
  39. package/dist/search/text-match.js +31 -2
  40. package/dist/suggested-tasks.d.ts +7 -3
  41. package/dist/suggested-tasks.js +2 -0
  42. package/dist/validation/ws-outbound-schema-metadata.d.ts +2348 -111
  43. package/dist/websocket-control.d.ts +39 -0
  44. package/dist/websocket-control.js +36 -0
  45. package/dist/workflow.d.ts +3060 -0
  46. package/dist/{orchestration.js → workflow.js} +461 -29
  47. package/dist/workspace-labels.d.ts +9 -0
  48. package/dist/workspace-labels.js +19 -0
  49. package/package.json +1 -1
  50. package/dist/orchestration.d.ts +0 -1148
@@ -5,7 +5,7 @@ import { JudgeVerdictSchema } from "./judge-verdict.js";
5
5
  // See projects/agent-orchestration/agent-orchestration.md. This is Otto's
6
6
  // provider-agnostic answer to a harness "Workflow": the conductor (an
7
7
  // orchestrator-role agent) DECLARES the shape (typed phases, assignments, the
8
- // loop target) via `start_run`, and the daemon runtime drives control flow -
8
+ // loop target) via `start_workflow`, and the daemon runtime drives control flow -
9
9
  // fan-out, gather-barrier, gate, loop - in code, so orchestrating is cheaper
10
10
  // than hand-tracking N agent ids across async notifications.
11
11
  //
@@ -57,6 +57,7 @@ export const RUN_PHASE_STATUSES = [
57
57
  "done",
58
58
  "failed",
59
59
  "skipped",
60
+ "canceled", // stopped by the user (run cancel or gate rejection), not by an error
60
61
  ];
61
62
  export const RUN_STATUSES = [
62
63
  "draft", // a user orchestration created by the dialog, graph not yet executed
@@ -81,9 +82,9 @@ export function isTerminalRunStatus(value) {
81
82
  }
82
83
  /** Terminal phase statuses - the phase will not change again on its own. */
83
84
  export function isTerminalPhaseStatus(value) {
84
- return value === "done" || value === "failed" || value === "skipped";
85
+ return value === "done" || value === "failed" || value === "skipped" || value === "canceled";
85
86
  }
86
- // ── Declaration schema (the `start_run` input) ──────────────────────────────
87
+ // ── Declaration schema (the `start_workflow` input) ─────────────────────────
87
88
  // What the conductor DECLARES. Kept minimal and schema-validated so a bad plan
88
89
  // is rejected at the tool boundary. `role` overrides the phase-type default;
89
90
  // `fanOut` spawns N parallel candidates; `judge` attaches a verify sub-step so a
@@ -129,6 +130,21 @@ export const RunPlanSchema = z
129
130
  phases: z.array(RunPhaseDeclarationSchema).min(1).max(64),
130
131
  })
131
132
  .passthrough();
133
+ /**
134
+ * A Workflow with this many known agents needs an explicit user confirmation
135
+ * before it starts. This is an agent-count boundary, not a price estimate:
136
+ * providers do not expose one reliable comparable cost signal.
137
+ */
138
+ export const WORKFLOW_START_CONFIRMATION_AGENT_THRESHOLD = 4;
139
+ /** Count the children a declared AI plan asks the daemon to start initially. */
140
+ export function describeRunPlanStart(plan) {
141
+ const workerPhases = plan.phases.filter((phase) => phase.type !== "gate");
142
+ return {
143
+ plannedAgentCount: workerPhases.reduce((total, phase) => total + (phase.fanOut ?? 1), 0),
144
+ fanOutPhaseCount: workerPhases.filter((phase) => (phase.fanOut ?? 1) > 1).length,
145
+ phaseCount: plan.phases.length,
146
+ };
147
+ }
132
148
  // ── Projection schema (the Run the daemon persists + pushes to clients) ─────
133
149
  // One spawned candidate for a phase: the observable child agent plus, when the
134
150
  // phase judged it, that candidate's verdict.
@@ -141,6 +157,9 @@ export const RunPhaseCandidateSchema = z
141
157
  // The candidate's final message (synthesis input); may be large - clients
142
158
  // truncate for display.
143
159
  summary: z.string().optional(),
160
+ // A durable terminal error when this candidate could not produce a result.
161
+ // Optional so persisted runs from older daemons continue to parse.
162
+ error: z.string().optional(),
144
163
  // Validated output fields, when the node declared them (GraphNode.output).
145
164
  // Values only - anything large belongs in a file the next node reads.
146
165
  outputFields: z.record(z.string(), z.unknown()).optional(),
@@ -181,26 +200,104 @@ export const RunPhaseSchema = z
181
200
  // mean an upstream node was itself skipped or failed. Plain string on the wire.
182
201
  export const GRAPH_SKIP_REASONS = [
183
202
  "condition",
203
+ "port",
184
204
  "upstream-skipped",
185
205
  "upstream-failed",
186
206
  "canceled",
187
207
  ];
188
- export const RunSchema = z
208
+ // The exact Graph document captured when a Graph Run is drafted or started.
209
+ // This must remain declared before RunSchema: the protocol's generated
210
+ // validators load schemas eagerly. Graph documents evolve independently, so
211
+ // their nested fields stay open here; the graph was already validated against
212
+ // OrchestrationGraphSchema before the daemon persisted this snapshot.
213
+ export const RunGraphSnapshotSchema = z
214
+ .object({
215
+ id: z.string().min(1),
216
+ name: z.string().min(1),
217
+ description: z.string().optional(),
218
+ inputs: z.array(z.unknown()).optional(),
219
+ nodes: z.array(z.unknown()),
220
+ edges: z.array(z.unknown()).optional(),
221
+ builtIn: z.boolean().optional(),
222
+ createdAt: z.string().optional(),
223
+ updatedAt: z.string().optional(),
224
+ })
225
+ .passthrough();
226
+ // New Workflow records carry this only when created through the category store.
227
+ // Optional means legacy daemon-global records remain readable and visible.
228
+ // COMPAT(categoryStorageResolver): added in v0.9.0, remove after 2027-02-28.
229
+ export const WorkflowStorageProvenanceSchema = z
230
+ .object({
231
+ schemaVersion: z.number().int().min(1),
232
+ projectRoot: z.string().min(1).optional(),
233
+ projectId: z.string().min(1).optional(),
234
+ projectKey: z.string().min(1).optional(),
235
+ location: z.enum(["repository", "host"]),
236
+ storeKey: z.string().min(1),
237
+ hostId: z.string().min(1).optional(),
238
+ hostName: z.string().min(1).optional(),
239
+ source: z.enum(["project-store", "legacy-host-library"]),
240
+ })
241
+ .passthrough();
242
+ /** Selects only the destination for future project-owned Workflow writes. */
243
+ export const ProjectWorkflowStoreSetRequestSchema = z.object({
244
+ type: z.literal("project.workflow.store.set.request"),
245
+ projectId: z.string(),
246
+ // Null inherits the independent host-wide Workflow default.
247
+ location: z.enum(["repository", "host"]).nullable(),
248
+ requestId: z.string(),
249
+ });
250
+ export const ProjectWorkflowStoreSetResponseSchema = z.object({
251
+ type: z.literal("project.workflow.store.set.response"),
252
+ payload: z.object({
253
+ requestId: z.string(),
254
+ projectId: z.string(),
255
+ accepted: z.boolean(),
256
+ error: z.string().nullable(),
257
+ }),
258
+ });
259
+ /** The durable Schedule fire that launched this Workflow, when applicable. */
260
+ export const WorkflowScheduleSourceSchema = z
261
+ .object({
262
+ scheduleId: z.string().min(1),
263
+ scheduleRunId: z.string().min(1),
264
+ })
265
+ .passthrough();
266
+ /**
267
+ * A daemon-owned start boundary, separate from an ordinary declared gate.
268
+ * `model-plan-declared` pauses before any child agent starts; `agent-threshold`
269
+ * is used by the Graph start review. The client presents the known shape and
270
+ * sends an explicit decision back to the daemon.
271
+ */
272
+ export const WorkflowStartConfirmationSchema = z
273
+ .object({
274
+ reason: z.string().min(1),
275
+ plannedAgentCount: z.number().int().min(0),
276
+ fanOutPhaseCount: z.number().int().min(0),
277
+ phaseCount: z.number().int().min(0),
278
+ agentCap: z.number().int().min(1),
279
+ threshold: z.number().int().min(1),
280
+ })
281
+ .passthrough();
282
+ export const WorkflowSchema = z
189
283
  .object({
190
284
  id: z.string().min(1),
191
285
  title: z.string().min(1),
192
286
  // User-authored description from the New Orchestration dialog (what this
193
287
  // orchestration is for). Distinct from `summary`, which is AI-generated
194
- // after the run settles. Absent on conductor-declared (start_run) runs.
288
+ // after the run settles. Absent on conductor-declared (start_workflow) runs.
195
289
  description: z.string().optional(),
196
290
  status: z.string().min(1),
197
291
  // Which engine drives this orchestration: absent/"phases" = the conductor
198
292
  // -declared phase plan; "graph" = a user-authored deterministic graph
199
293
  // (projects/orchestration-graphs). Open vocabulary, plain string on the wire.
200
294
  kind: z.string().optional(),
201
- // Graph runs only: the executed graph template and the fill-in values the
202
- // user supplied for its declared inputs.
295
+ // Graph runs only: the graph template id, its exact source document, and
296
+ // the fill-in values the user supplied for inputs. A draft may be re-saved;
297
+ // an execution keeps this source document as immutable history. Optional so
298
+ // older persisted runs and clients continue to parse.
203
299
  graphId: z.string().optional(),
300
+ graphSnapshot: RunGraphSnapshotSchema.optional(),
204
301
  graphInputs: z.record(z.string(), z.string()).optional(),
205
302
  // Immutable requirements block (see RunPlan.requirements).
206
303
  requirements: z.array(z.string().min(1)).optional(),
@@ -222,6 +319,14 @@ export const RunSchema = z
222
319
  // or runs without the run-summary feature.
223
320
  summary: z.string().optional(),
224
321
  summaryStatus: z.string().optional(),
322
+ // A pending cost/agent confirmation before an AI-declared plan starts.
323
+ // This is not a Graph/phase gate and never changes the plan's autopilot or
324
+ // permission mode.
325
+ startConfirmation: WorkflowStartConfirmationSchema.optional(),
326
+ workflowStorage: WorkflowStorageProvenanceSchema.optional(),
327
+ // A Schedule may start a saved definition, but it must not erase the
328
+ // source identity that explains why this durable run exists.
329
+ scheduleSource: WorkflowScheduleSourceSchema.optional(),
225
330
  // Total child agents this run spawned (makers + judgers) - a complexity
226
331
  // signal surfaced in the Runs display. Grows as the run executes.
227
332
  agentCount: z.number().int().min(0).optional(),
@@ -229,6 +334,9 @@ export const RunSchema = z
229
334
  updatedAt: z.string().optional(),
230
335
  })
231
336
  .passthrough();
337
+ // COMPAT(runDomainType): renamed to Workflow in v0.9.0; remove after
338
+ // 2027-02-28 once downstream extensions have moved to the Workflow API.
339
+ export const RunSchema = WorkflowSchema;
232
340
  // Summary generation lifecycle (plain-string on the wire; see RunSchema.summaryStatus).
233
341
  export const RUN_SUMMARY_STATUSES = ["pending", "ready", "failed"];
234
342
  // ── Orchestration graphs (user orchestrations) ──────────────────────────────
@@ -271,6 +379,7 @@ export const PromptTemplateSchema = z
271
379
  builtIn: z.boolean().optional(),
272
380
  createdAt: z.string().optional(),
273
381
  updatedAt: z.string().optional(),
382
+ workflowStorage: WorkflowStorageProvenanceSchema.optional(),
274
383
  })
275
384
  .passthrough();
276
385
  // A node's binding to a stored template. A value is a literal, `$inputs.<key>`
@@ -283,8 +392,10 @@ export const NodePromptTemplateRefSchema = z
283
392
  })
284
393
  .passthrough();
285
394
  // Node kinds (open vocabulary): "orchestrator" - the single root that hosts
286
- // the orchestration chat and anchors the Visualizer; "agent" - a worker node.
287
- export const GRAPH_NODE_KINDS = ["orchestrator", "agent"];
395
+ // the orchestration chat and anchors the Visualizer; "agent" - a worker node;
396
+ // "gate" - an attended human approval boundary; "check" - a deterministic
397
+ // JSONata assertion over upstream output. Gates and checks make no model call.
398
+ export const GRAPH_NODE_KINDS = ["orchestrator", "agent", "gate", "check"];
288
399
  // Loop annotation - exactly one of `times` (fixed repeat) or `until` (bounded
289
400
  // retry graded by a structured judge between iterations; self-grading is not
290
401
  // an exit test). `max` is a hard cap in both readings.
@@ -366,6 +477,22 @@ export const GraphNodeRetrySchema = z
366
477
  multiplier: z.number().min(1).max(4).optional(),
367
478
  })
368
479
  .passthrough();
480
+ // A deterministic assertion over the named upstream material that reached a
481
+ // Check node. JSONata keeps user-authored graph data out of JavaScript `eval`.
482
+ // `message` is the actionable failure text shown on the durable Run.
483
+ export const GraphNodeCheckSchema = z
484
+ .object({
485
+ expression: z.string().min(1),
486
+ message: z.string().min(1).optional(),
487
+ })
488
+ .passthrough();
489
+ /**
490
+ * A Check settles on exactly one named control-flow output. These are open
491
+ * wire strings on GraphEdge so a newer client can still parse on an older
492
+ * daemon, but the shared validator rejects an unsupported Check port before a
493
+ * run begins.
494
+ */
495
+ export const GRAPH_CHECK_OUTPUT_PORTS = ["pass", "fail"];
369
496
  export const GraphNodeSchema = z
370
497
  .object({
371
498
  id: z.string().min(1),
@@ -415,6 +542,9 @@ export const GraphNodeSchema = z
415
542
  // node including its loop, and every attempt is charged to the run's agent
416
543
  // cap - a retry is never a private allowance.
417
544
  retry: GraphNodeRetrySchema.optional(),
545
+ // Check nodes only: a deterministic pass/fail assertion over the named
546
+ // upstream output material. It never dispatches an agent.
547
+ check: GraphNodeCheckSchema.optional(),
418
548
  // Wall-clock ceiling for one attempt of this node. On expiry the agent is
419
549
  // really cancelled (not merely stopped being awaited) and the node fails,
420
550
  // which its retry policy may then catch.
@@ -466,8 +596,22 @@ export const GraphEdgeSchema = z
466
596
  label: z.string().optional(),
467
597
  })
468
598
  .passthrough();
599
+ // ── Graph document compatibility ────────────────────────────────────────────
600
+ //
601
+ // Graphs were originally daemon-local records, so their persisted shape has no
602
+ // document version. Keep that legacy shape executable, but give caller-supplied
603
+ // documents a stable compatibility boundary before import/export exists. The
604
+ // schema remains additive and parser-safe; version interpretation happens here,
605
+ // after parsing, rather than in a wire-schema transform.
606
+ export const GRAPH_DOCUMENT_FORMAT = "otto.workflow.graph";
607
+ export const GRAPH_DOCUMENT_FORMAT_VERSION = 1;
469
608
  export const OrchestrationGraphSchema = z
470
609
  .object({
610
+ // A missing format/version is a legacy daemon-local Graph. New portable
611
+ // documents write both fields; their semantics are checked explicitly by
612
+ // `validateGraphDocument` below.
613
+ format: z.string().min(1).optional(),
614
+ formatVersion: z.number().int().min(1).optional(),
471
615
  id: z.string().min(1),
472
616
  name: z.string().min(1),
473
617
  description: z.string().optional(),
@@ -476,10 +620,107 @@ export const OrchestrationGraphSchema = z
476
620
  edges: z.array(GraphEdgeSchema).optional(),
477
621
  // Bundled starter graphs; copy-on-edit, never deleted in place.
478
622
  builtIn: z.boolean().optional(),
623
+ // Capability declarations are reserved for portable documents. They are
624
+ // intentionally open strings so a newer exporter still parses on an older
625
+ // peer; execution compatibility is a daemon-side preflight concern.
626
+ requires: z.array(z.string().min(1)).optional(),
479
627
  createdAt: z.string().optional(),
480
628
  updatedAt: z.string().optional(),
629
+ workflowStorage: WorkflowStorageProvenanceSchema.optional(),
630
+ })
631
+ .passthrough();
632
+ // A portable Graph package is intentionally data-only. Its source descriptor is
633
+ // display/audit provenance supplied by the exporter, never an authority grant:
634
+ // the destination daemon independently validates the Graph and only persists it
635
+ // after the caller confirms the review response.
636
+ export const WorkflowGraphShareLocationSchema = z
637
+ .object({
638
+ storeKey: z.string().min(1),
639
+ location: z.enum(["repository", "host"]),
640
+ hostName: z.string().min(1).optional(),
641
+ source: z.enum(["project-store", "legacy-host-library"]),
642
+ })
643
+ .passthrough();
644
+ export const WorkflowGraphExportSchema = z
645
+ .object({
646
+ schemaVersion: z.literal(1),
647
+ graph: OrchestrationGraphSchema,
648
+ source: WorkflowGraphShareLocationSchema,
649
+ exportedAt: z.string().min(1),
650
+ contentHash: z.string().regex(/^[a-f0-9]{64}$/),
481
651
  })
482
652
  .passthrough();
653
+ export const WorkflowGraphImportResultSchema = z
654
+ .object({
655
+ status: z.enum(["review_required", "imported", "failed"]),
656
+ graph: OrchestrationGraphSchema.optional(),
657
+ source: WorkflowGraphShareLocationSchema.optional(),
658
+ destination: WorkflowGraphShareLocationSchema.optional(),
659
+ contentHash: z
660
+ .string()
661
+ .regex(/^[a-f0-9]{64}$/)
662
+ .optional(),
663
+ remediation: z.string().min(1),
664
+ })
665
+ .passthrough();
666
+ /**
667
+ * Validate the portable-document wrapper without changing a Graph's content.
668
+ * A caller can use this in a local, read-only validation path; it never
669
+ * resolves templates, evaluates expressions, or consults a daemon.
670
+ */
671
+ export function validateGraphDocument(graph) {
672
+ const diagnostics = [];
673
+ // `passthrough()` preserved arbitrary fields before this contract existed.
674
+ // Treat a pre-existing unrelated `format` key as legacy unless its companion
675
+ // version, or our exact portable format marker, says it is a document header.
676
+ const declaresPortableFormat = graph.formatVersion !== undefined || graph.format === GRAPH_DOCUMENT_FORMAT;
677
+ if (!declaresPortableFormat) {
678
+ diagnostics.push({
679
+ code: "GRAPH_DOCUMENT_LEGACY_UNVERSIONED",
680
+ severity: "warning",
681
+ path: "",
682
+ message: "This Graph has no portable document format version.",
683
+ recovery: `Export it as ${GRAPH_DOCUMENT_FORMAT} v${GRAPH_DOCUMENT_FORMAT_VERSION} before sharing it.`,
684
+ });
685
+ return diagnostics;
686
+ }
687
+ if (graph.format !== GRAPH_DOCUMENT_FORMAT) {
688
+ diagnostics.push({
689
+ code: "GRAPH_DOCUMENT_FORMAT_UNSUPPORTED",
690
+ severity: "error",
691
+ path: "/format",
692
+ message: `Graph format "${graph.format ?? "(missing)"}" is not supported.`,
693
+ recovery: `Use format "${GRAPH_DOCUMENT_FORMAT}".`,
694
+ });
695
+ }
696
+ if (graph.formatVersion === undefined) {
697
+ diagnostics.push({
698
+ code: "GRAPH_DOCUMENT_VERSION_MISSING",
699
+ severity: "error",
700
+ path: "/formatVersion",
701
+ message: "A portable Graph document needs formatVersion.",
702
+ recovery: `Use formatVersion ${GRAPH_DOCUMENT_FORMAT_VERSION}.`,
703
+ });
704
+ }
705
+ else if (graph.formatVersion > GRAPH_DOCUMENT_FORMAT_VERSION) {
706
+ diagnostics.push({
707
+ code: "GRAPH_DOCUMENT_VERSION_UNSUPPORTED",
708
+ severity: "error",
709
+ path: "/formatVersion",
710
+ message: `Graph document version ${graph.formatVersion} is newer than this Otto host supports.`,
711
+ recovery: "Update Otto, or export the Graph in a supported format version.",
712
+ });
713
+ }
714
+ return diagnostics;
715
+ }
716
+ // A Graph id becomes a file name in every Graph store (`{id}.json`) and, since
717
+ // Graph packages can be imported from another host, it is untrusted input.
718
+ // Reject anything that is not one plain path segment so an id can never leave
719
+ // its store directory or shadow another store's file.
720
+ const SAFE_GRAPH_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
721
+ export function isSafeGraphId(id) {
722
+ return SAFE_GRAPH_ID.test(id) && id !== "." && id !== "..";
723
+ }
483
724
  // ── Graph structural validation ──────────────────────────────────────────────
484
725
  // Shared by the daemon (hard gate before execute) and the designer (live
485
726
  // feedback). Returns human-readable problems; empty ⇒ executable. Split into
@@ -487,6 +728,14 @@ export const OrchestrationGraphSchema = z
487
728
  export function validateOrchestrationGraph(graph) {
488
729
  const nodeIds = new Set();
489
730
  const problems = [];
731
+ if (!isSafeGraphId(graph.id)) {
732
+ problems.push(`Graph id "${graph.id}" must be a single file-name segment (letters, digits, "-", "_", ".").`);
733
+ }
734
+ for (const diagnostic of validateGraphDocument(graph)) {
735
+ if (diagnostic.severity === "error") {
736
+ problems.push(diagnostic.message);
737
+ }
738
+ }
490
739
  for (const node of graph.nodes) {
491
740
  if (nodeIds.has(node.id))
492
741
  problems.push(`Duplicate node id "${node.id}".`);
@@ -500,7 +749,13 @@ export function validateOrchestrationGraph(graph) {
500
749
  problems.push("The graph has more than one Orchestrator node.");
501
750
  }
502
751
  problems.push(...validateGraphEdges(graph, nodeIds));
503
- const declaredInputs = new Set((graph.inputs ?? []).map((i) => i.key));
752
+ const declaredInputs = new Set();
753
+ for (const input of graph.inputs ?? []) {
754
+ if (declaredInputs.has(input.key)) {
755
+ problems.push(`Duplicate Graph input key "${input.key}".`);
756
+ }
757
+ declaredInputs.add(input.key);
758
+ }
504
759
  for (const node of graph.nodes) {
505
760
  problems.push(...validateGraphNode(node, declaredInputs));
506
761
  }
@@ -519,6 +774,12 @@ function validateGraphEdges(graph, nodeIds) {
519
774
  problems.push(`Edge to unknown node "${edge.to}".`);
520
775
  if (edge.from === edge.to)
521
776
  problems.push(`Node "${edge.from}" connects to itself.`);
777
+ const source = graph.nodes.find((node) => node.id === edge.from);
778
+ if (source?.kind === "check" &&
779
+ edge.fromPort !== undefined &&
780
+ !GRAPH_CHECK_OUTPUT_PORTS.includes(edge.fromPort)) {
781
+ problems.push(`Check "${source.title}" has edge port "${edge.fromPort}"; use "pass" or "fail".`);
782
+ }
522
783
  // Edges INTO the orchestrator are passive answer-delivery, not execution
523
784
  // dependencies - excluding them here keeps "root kicks off A, A delivers
524
785
  // back to root" from reading as a cycle.
@@ -576,7 +837,9 @@ function hasGraphCycle(graph, outgoing, incoming) {
576
837
  const GRAPH_INPUT_REF = /\{\{\s*inputs\.([A-Za-z0-9_-]+)\s*\}\}/g;
577
838
  function validateGraphNode(node, declaredInputs) {
578
839
  const isRoot = node.kind === "orchestrator";
579
- if (!isRoot && node.kind !== "agent")
840
+ const isGate = node.kind === "gate";
841
+ const isCheck = node.kind === "check";
842
+ if (!isRoot && node.kind !== "agent" && !isGate && !isCheck)
580
843
  return []; // unknown kinds pass through
581
844
  const problems = [];
582
845
  // Autonomous nodes may feed results onward via edges; what they must not
@@ -585,9 +848,10 @@ function validateGraphNode(node, declaredInputs) {
585
848
  if (node.autonomous && isRoot) {
586
849
  problems.push("The Orchestrator node can't be autonomous.");
587
850
  }
588
- if (!isRoot && !node.prompt?.trim() && !node.promptFromInput) {
851
+ if (!isRoot && !isGate && !isCheck && !node.prompt?.trim() && !node.promptFromInput) {
589
852
  problems.push(`Node "${node.title}" has no prompt and no prompt input.`);
590
853
  }
854
+ problems.push(...validateGraphNodeCheck(node, isCheck));
591
855
  if (node.promptFromInput && !declaredInputs.has(node.promptFromInput)) {
592
856
  problems.push(`Node "${node.title}" reads input "${node.promptFromInput}", which isn't declared.`);
593
857
  }
@@ -600,6 +864,11 @@ function validateGraphNode(node, declaredInputs) {
600
864
  problems.push(...validateGraphNodeOutput(node));
601
865
  return problems;
602
866
  }
867
+ function validateGraphNodeCheck(node, isCheck) {
868
+ return isCheck && !node.check?.expression.trim()
869
+ ? [`Check "${node.title}" needs a JSONata expression.`]
870
+ : [];
871
+ }
603
872
  function validateGraphNodeOutput(node) {
604
873
  if (!node.output) {
605
874
  return [];
@@ -775,6 +1044,65 @@ export const RunsGraphsChangedNotificationSchema = z.object({
775
1044
  graphs: z.array(OrchestrationGraphSchema),
776
1045
  }),
777
1046
  });
1047
+ // Graph sharing is deliberately separate from save/run. Export is explicit;
1048
+ // import first returns a review and needs a second confirmed request before a
1049
+ // destination store is touched. New names use the dotted RPC contract.
1050
+ export const WorkflowsGraphsListRequestSchema = z.object({
1051
+ type: z.literal("workflows.graphs.list.request"),
1052
+ cwd: z.string().min(1),
1053
+ requestId: z.string(),
1054
+ });
1055
+ export const WorkflowsGraphsListResponseSchema = z.object({
1056
+ type: z.literal("workflows.graphs.list.response"),
1057
+ payload: z.object({
1058
+ graphs: z.array(OrchestrationGraphSchema),
1059
+ error: z.string().optional(),
1060
+ requestId: z.string(),
1061
+ }),
1062
+ });
1063
+ /** Project-scoped Graph writes. Legacy runs.graphs.* remains the visible library. */
1064
+ export const WorkflowsGraphSaveRequestSchema = z.object({
1065
+ type: z.literal("workflows.graph.save.request"),
1066
+ cwd: z.string().min(1),
1067
+ graph: OrchestrationGraphSchema,
1068
+ requestId: z.string(),
1069
+ });
1070
+ export const WorkflowsGraphSaveResponseSchema = z.object({
1071
+ type: z.literal("workflows.graph.save.response"),
1072
+ payload: z.object({
1073
+ graph: OrchestrationGraphSchema.optional(),
1074
+ error: z.string().optional(),
1075
+ requestId: z.string(),
1076
+ }),
1077
+ });
1078
+ export const WorkflowsGraphExportRequestSchema = z.object({
1079
+ type: z.literal("workflows.graph.export.request"),
1080
+ graphId: z.string().min(1),
1081
+ requestId: z.string(),
1082
+ });
1083
+ export const WorkflowsGraphExportResponseSchema = z.object({
1084
+ type: z.literal("workflows.graph.export.response"),
1085
+ payload: z.object({
1086
+ export: WorkflowGraphExportSchema.optional(),
1087
+ error: z.string().optional(),
1088
+ requestId: z.string(),
1089
+ }),
1090
+ });
1091
+ export const WorkflowsGraphImportRequestSchema = z.object({
1092
+ type: z.literal("workflows.graph.import.request"),
1093
+ cwd: z.string().min(1),
1094
+ export: WorkflowGraphExportSchema,
1095
+ confirmed: z.boolean(),
1096
+ requestId: z.string(),
1097
+ });
1098
+ export const WorkflowsGraphImportResponseSchema = z.object({
1099
+ type: z.literal("workflows.graph.import.response"),
1100
+ payload: z.object({
1101
+ result: WorkflowGraphImportResultSchema.optional(),
1102
+ error: z.string().optional(),
1103
+ requestId: z.string(),
1104
+ }),
1105
+ });
778
1106
  // ── Prompt templates ────────────────────────────────────────────────────────
779
1107
  // Host-level reusable prompts and snippets a graph node can bind to. Same shape
780
1108
  // as the graph trio above, for the same reason: one store, list/save/delete,
@@ -822,15 +1150,81 @@ export const RunsTemplatesChangedNotificationSchema = z.object({
822
1150
  templates: z.array(PromptTemplateSchema),
823
1151
  }),
824
1152
  });
825
- // Start (or draft) a user-initiated orchestration from the New Orchestration
826
- // dialog. `flavor` is an open vocabulary: "ai" (prompt-and-go - the daemon
827
- // spawns an orchestrator agent that declares its own plan via start_run) or
828
- // "graph" (deterministic - the daemon executes `graphId` with `graphInputs`).
829
- // `draft: true` creates the record without executing (the designer flow);
830
- // `runId` executes an existing draft in place - or, with `draft: true`, re-saves
831
- // that draft in place (Edit Orchestration).
832
- export const RunsStartRequestSchema = z.object({
833
- type: z.literal("runs.start.request"),
1153
+ export const WorkflowsTemplatesListRequestSchema = z.object({
1154
+ type: z.literal("workflows.templates.list.request"),
1155
+ cwd: z.string().min(1),
1156
+ requestId: z.string(),
1157
+ });
1158
+ export const WorkflowsTemplatesListResponseSchema = z.object({
1159
+ type: z.literal("workflows.templates.list.response"),
1160
+ payload: z.object({
1161
+ templates: z.array(PromptTemplateSchema),
1162
+ error: z.string().optional(),
1163
+ requestId: z.string(),
1164
+ }),
1165
+ });
1166
+ export const WorkflowsTemplateSaveRequestSchema = z.object({
1167
+ type: z.literal("workflows.template.save.request"),
1168
+ cwd: z.string().min(1),
1169
+ template: PromptTemplateSchema,
1170
+ requestId: z.string(),
1171
+ });
1172
+ export const WorkflowsTemplateSaveResponseSchema = z.object({
1173
+ type: z.literal("workflows.template.save.response"),
1174
+ payload: z.object({
1175
+ template: PromptTemplateSchema.optional(),
1176
+ error: z.string().optional(),
1177
+ requestId: z.string(),
1178
+ }),
1179
+ });
1180
+ // Transfer addresses records by stable id plus the current project scope. The
1181
+ // caller never receives a daemon file path, and a receipt is written before a
1182
+ // destination record so an interrupted attempt stays explainable.
1183
+ export const WorkflowTransferReceiptSchema = z.object({
1184
+ schemaVersion: z.literal(1),
1185
+ receiptId: z.string().min(1),
1186
+ recordKind: z.enum(["graph", "template", "run"]),
1187
+ recordId: z.string().min(1),
1188
+ mode: z.enum(["copy", "move"]),
1189
+ source: z.object({
1190
+ source: z.enum(["legacy-host-library", "repository", "host"]),
1191
+ storeKey: z.string().min(1),
1192
+ }),
1193
+ destination: z.object({
1194
+ location: z.enum(["repository", "host"]),
1195
+ storeKey: z.string().min(1),
1196
+ }),
1197
+ contentHash: z.string().regex(/^[a-f0-9]{64}$/),
1198
+ status: z.enum(["prepared", "verified", "moved", "source-retained", "failed"]),
1199
+ createdAt: z.string().min(1),
1200
+ updatedAt: z.string().min(1),
1201
+ recovery: z.string().optional(),
1202
+ });
1203
+ export const WorkflowsStorageTransferRequestSchema = z.object({
1204
+ type: z.literal("workflows.storage.transfer.request"),
1205
+ cwd: z.string().min(1),
1206
+ recordKind: z.enum(["graph", "template", "run"]),
1207
+ recordId: z.string().min(1),
1208
+ source: z.enum(["legacy-host-library", "repository", "host"]),
1209
+ destination: z.enum(["repository", "host"]),
1210
+ mode: z.enum(["copy", "move"]),
1211
+ requestId: z.string(),
1212
+ });
1213
+ export const WorkflowsStorageTransferResponseSchema = z.object({
1214
+ type: z.literal("workflows.storage.transfer.response"),
1215
+ payload: z.object({
1216
+ receipt: WorkflowTransferReceiptSchema.optional(),
1217
+ error: z.string().optional(),
1218
+ requestId: z.string(),
1219
+ }),
1220
+ });
1221
+ // Start (or draft) a user-initiated Workflow. `flavor` is an open vocabulary:
1222
+ // "ai" (prompt-and-go - the daemon spawns an orchestrator agent that declares
1223
+ // its own plan via start_workflow) or "graph" (deterministic - the daemon
1224
+ // executes `graphId` with `graphInputs`). `draft: true` creates the record
1225
+ // without executing (the designer flow); `runId` executes an existing draft in
1226
+ // place - or, with `draft: true`, re-saves that draft in place.
1227
+ const WorkflowStartRequestFieldsSchema = z.object({
834
1228
  flavor: z.string(),
835
1229
  cwd: z.string(),
836
1230
  workspaceId: z.string().optional(),
@@ -845,21 +1239,59 @@ export const RunsStartRequestSchema = z.object({
845
1239
  prompt: z.string().optional(),
846
1240
  graphId: z.string().optional(),
847
1241
  graphInputs: z.record(z.string(), z.string()).optional(),
1242
+ // A daemon-issued, request-bound token is required after Graph review. It
1243
+ // cannot be replaced by a client-side "confirmed" assertion.
1244
+ startConfirmationToken: z.string().min(1).optional(),
848
1245
  draft: z.boolean().optional(),
849
1246
  runId: z.string().optional(),
850
1247
  requestId: z.string(),
851
1248
  });
1249
+ const WorkflowStartResponsePayloadSchema = z.object({
1250
+ runId: z.string().optional(),
1251
+ // The root/orchestrator agent whose chat the client navigates to, and the
1252
+ // workspace the daemon resolved it into (the dialog only knows a project
1253
+ // target's cwd).
1254
+ agentId: z.string().optional(),
1255
+ workspaceId: z.string().optional(),
1256
+ // Returned without starting anything when the daemon requires an explicit
1257
+ // Graph-start confirmation. The caller renders this factual shape, then
1258
+ // resubmits the daemon-issued token with the unchanged launch request.
1259
+ confirmation: WorkflowStartConfirmationSchema.optional(),
1260
+ confirmationToken: z.string().min(1).optional(),
1261
+ error: z.string().optional(),
1262
+ requestId: z.string(),
1263
+ });
1264
+ /** The canonical Workflow launch RPC. */
1265
+ export const WorkflowsStartRequestSchema = WorkflowStartRequestFieldsSchema.extend({
1266
+ type: z.literal("workflows.start.request"),
1267
+ });
1268
+ export const WorkflowsStartResponseSchema = z.object({
1269
+ type: z.literal("workflows.start.response"),
1270
+ payload: WorkflowStartResponsePayloadSchema,
1271
+ });
1272
+ // COMPAT(runsStartRpc): renamed to workflows.start in v0.9.0; accept and
1273
+ // answer the legacy pair through 2027-02-28 so separately shipped apps and
1274
+ // daemons retain the established Workflow-launch behavior.
1275
+ export const RunsStartRequestSchema = WorkflowStartRequestFieldsSchema.extend({
1276
+ type: z.literal("runs.start.request"),
1277
+ });
852
1278
  export const RunsStartResponseSchema = z.object({
853
1279
  type: z.literal("runs.start.response"),
1280
+ payload: WorkflowStartResponsePayloadSchema,
1281
+ });
1282
+ /** Respond to an AI Workflow's daemon-owned start confirmation. */
1283
+ export const WorkflowsStartConfirmationRespondRequestSchema = z.object({
1284
+ type: z.literal("workflows.start_confirmation.respond.request"),
1285
+ runId: z.string().min(1),
1286
+ approved: z.boolean(),
1287
+ requestId: z.string(),
1288
+ });
1289
+ export const WorkflowsStartConfirmationRespondResponseSchema = z.object({
1290
+ type: z.literal("workflows.start_confirmation.respond.response"),
854
1291
  payload: z.object({
855
- runId: z.string().optional(),
856
- // The root/orchestrator agent whose chat the client navigates to, and the
857
- // workspace the daemon resolved it into (the dialog only knows a project
858
- // target's cwd).
859
- agentId: z.string().optional(),
860
- workspaceId: z.string().optional(),
861
- error: z.string().optional(),
1292
+ runId: z.string(),
1293
+ accepted: z.boolean(),
862
1294
  requestId: z.string(),
863
1295
  }),
864
1296
  });
865
- //# sourceMappingURL=orchestration.js.map
1297
+ //# sourceMappingURL=workflow.js.map