@cat-factory/contracts 0.151.0 → 0.152.1

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 (42) hide show
  1. package/dist/bootstrap.js +1 -1
  2. package/dist/bootstrap.js.map +1 -1
  3. package/dist/companion.js +1 -1
  4. package/dist/companion.js.map +1 -1
  5. package/dist/entities.d.ts +14 -3925
  6. package/dist/entities.d.ts.map +1 -1
  7. package/dist/entities.js +17 -1284
  8. package/dist/entities.js.map +1 -1
  9. package/dist/env-config-repair.js +1 -1
  10. package/dist/env-config-repair.js.map +1 -1
  11. package/dist/events.d.ts +2 -1
  12. package/dist/events.d.ts.map +1 -1
  13. package/dist/execution.d.ts +3882 -0
  14. package/dist/execution.d.ts.map +1 -0
  15. package/dist/execution.js +1290 -0
  16. package/dist/execution.js.map +1 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/observability.js +1 -1
  22. package/dist/observability.js.map +1 -1
  23. package/dist/requests.d.ts +1 -0
  24. package/dist/requests.d.ts.map +1 -1
  25. package/dist/requests.js +5 -0
  26. package/dist/requests.js.map +1 -1
  27. package/dist/routes/agent-runs.js +1 -1
  28. package/dist/routes/agent-runs.js.map +1 -1
  29. package/dist/routes/board.d.ts +1 -0
  30. package/dist/routes/board.d.ts.map +1 -1
  31. package/dist/routes/execution.d.ts.map +1 -1
  32. package/dist/routes/execution.js +2 -1
  33. package/dist/routes/execution.js.map +1 -1
  34. package/dist/routes/human-review.js +1 -1
  35. package/dist/routes/human-review.js.map +1 -1
  36. package/dist/routes/human-test.js +1 -1
  37. package/dist/routes/human-test.js.map +1 -1
  38. package/dist/routes/visual-confirm.js +1 -1
  39. package/dist/routes/visual-confirm.js.map +1 -1
  40. package/dist/snapshot.js +2 -1
  41. package/dist/snapshot.js.map +1 -1
  42. package/package.json +6 -3
package/dist/entities.js CHANGED
@@ -5,19 +5,13 @@ import { pipelinePurposeSchema } from './pipeline-purpose.js';
5
5
  import { workspaceAccessModeSchema } from './workspace-members.js';
6
6
  import { subscriptionVendorSchema } from './vendor-credentials.js';
7
7
  import { agentConfigValuesSchema } from './agent-config.js';
8
- import { testConcernSchema, testReportSchema, testerInfraSetupSchema } from './testing.js';
9
8
  import { consensusStepConfigSchema, stepGatingSchema, taskEstimateSchema } from './consensus.js';
10
- import { followUpsStepStateSchema } from './followUp.js';
11
- import { forkDecisionStepStateSchema } from './forkDecision.js';
12
- import { ralphStepStateSchema } from './ralph.js';
13
- import { prReviewStepStateSchema } from './prReview.js';
14
9
  import { cloudProviderSchema, instanceSizeSchema } from './compute-provisioning.js';
15
- import { releaseSignalSchema } from './release.js';
16
- import { environmentStatusSchema, infraEngineSchema, provisionTypeSchema, serviceProvisioningSchema, } from './environments.js';
10
+ import { serviceProvisioningSchema } from './environments.js';
17
11
  import { documentSourceKindSchema } from './documents.js';
18
- import { frontendConfigSchema, resolvedFrontendBindingSchema } from './frontend.js';
12
+ import { frontendConfigSchema } from './frontend.js';
19
13
  import { serviceConnectionsSchema } from './service-connections.js';
20
- import { agentKindSchema, agentStateSchema, blockLevelSchema, blockStatusSchema, blockTypeSchema, positionSchema, sizeSchema, taskTypeFieldsSchema, taskTypeSchema, } from './primitives.js';
14
+ import { agentKindSchema, blockLevelSchema, blockStatusSchema, blockTypeSchema, positionSchema, sizeSchema, taskTypeFieldsSchema, taskTypeSchema, } from './primitives.js';
21
15
  // ---------------------------------------------------------------------------
22
16
  // Entity schemas: the single source of truth for the data shapes that travel
23
17
  // over the wire. Domain types in @cat-factory/kernel are derived from these, and
@@ -251,18 +245,23 @@ export const blockSchema = v.object({
251
245
  */
252
246
  technical: v.optional(v.nullable(v.boolean())),
253
247
  /**
254
- * Ids of curated best-practice prompt fragments selected for this block. Their
255
- * bodies are composed into the agent system prompt at run time. The catalog
256
- * itself lives in @cat-factory/prompt-fragments and is served separately.
248
+ * Ids of curated best-practice prompt fragments selected for this block — the block's OWN,
249
+ * authoritative selection. On a TASK this is what the engine folds into its `code-aware` /
250
+ * `doc-aware` steps (the enclosing service's fragments are NOT re-unioned at run time), seeded
251
+ * from the service at creation and then freely add/removable per task. Their bodies are composed
252
+ * into the agent system prompt at run time; the catalog itself lives in
253
+ * @cat-factory/prompt-fragments (+ the tenant library) and is served separately.
257
254
  */
258
255
  fragmentIds: v.optional(v.array(v.string())),
259
256
  /**
260
- * Service-level (frame-only): ids of the best-practice / guideline prompt fragments
261
- * selected as this service's programming standards (drawn from the universal pool in
262
- * @cat-factory/prompt-fragments). At run time the execution engine folds their bodies
263
- * into the system prompt of every agent under this service that carries the
264
- * `code-aware` trait. Seeded from the workspace default on new services; absent no
265
- * service-level fragments (only the block's own `fragmentIds` apply).
257
+ * Service-level (frame-only): ids of the best-practice / guideline prompt fragments selected as
258
+ * this service's programming standards (drawn from the universal pool in
259
+ * @cat-factory/prompt-fragments). They serve two roles: (1) they SEED a new task's own
260
+ * `fragmentIds` at creation (materialised onto the task, which owns its selection from then on —
261
+ * an existing task is NOT retroactively updated when this list later changes; a new fragment is
262
+ * picked up by adding it to the task by hand), and (2) at run time the engine folds them into the
263
+ * frame's OWN `code-aware` runs (e.g. `blueprints`). Seeded from the workspace default on new
264
+ * services; a task does not re-read them at run time.
266
265
  */
267
266
  serviceFragmentIds: v.optional(v.array(v.string())),
268
267
  /**
@@ -766,1272 +765,6 @@ export const pipelineSchema = v.object({
766
765
  // on. Absent ⇒ unclassified (pre-1.0, no back-fill): unrestricted, but hidden from a document task.
767
766
  purpose: v.optional(pipelinePurposeSchema),
768
767
  });
769
- export const decisionSchema = v.object({
770
- id: v.string(),
771
- question: v.string(),
772
- options: v.array(v.string()),
773
- chosen: v.nullable(v.string()),
774
- });
775
- /** One entry of a running step's todo list — its label and current status. */
776
- export const stepSubtaskItemSchema = v.object({
777
- /** The task's human-readable subject, as the agent wrote it. */
778
- label: v.string(),
779
- status: v.picklist(['pending', 'in_progress', 'completed']),
780
- });
781
- /**
782
- * Live subtask counts for a running step, reported by the container agent from
783
- * the coding tool's own todo list (e.g. "3/8 done, 1 in progress"). Present only
784
- * while an async job is in flight and the agent maintains a todo list; the board
785
- * renders it as a finer-grained progress indicator than `progress` alone.
786
- *
787
- * `items` carries the individual todo entries (label + status) so a zoomed-in
788
- * card can render the actual task list, not just the count. It is optional — an
789
- * older agent/poll that only reported counts, or the simpler `todos[].done`
790
- * fallback shape, still validates without it.
791
- */
792
- export const stepSubtasksSchema = v.object({
793
- completed: v.number(),
794
- inProgress: v.number(),
795
- total: v.number(),
796
- items: v.optional(v.array(stepSubtaskItemSchema)),
797
- });
798
- /**
799
- * One GitHub-review-style comment left on a specific block or item of an agent's
800
- * proposal — either by a human reviewing an approval gate, or by a quality
801
- * companion (e.g. the Spec Reviewer) grading a structured output. `quotedSource`
802
- * is the verbatim raw markdown of the block the comment targets (sliced from the
803
- * proposal by its source line range), so a "request changes" re-run can quote the
804
- * agent's own text back to it rather than a re-rendered approximation. It is
805
- * OPTIONAL because a comment may instead anchor to a structured item via
806
- * {@link anchorId} (e.g. a spec requirement / acceptance-criterion id), where the
807
- * reviewed output is rendered as discrete items rather than free prose and there is
808
- * no quoted source range — the shape a companion returns.
809
- */
810
- export const stepReviewCommentSchema = v.object({
811
- /**
812
- * Verbatim raw-markdown source of the commented prose block. Optional: a comment
813
- * may instead anchor to a structured item via {@link anchorId}, where there is no
814
- * prose source to quote.
815
- */
816
- quotedSource: v.optional(v.string()),
817
- /**
818
- * 0-based source line range [start, end) of the commented prose block, for
819
- * best-effort re-anchoring. Optional: a comment may instead anchor to a structured
820
- * item via {@link anchorId} (e.g. a spec requirement/acceptance-criterion id), where
821
- * there is no prose line range.
822
- */
823
- srcStart: v.optional(v.number()),
824
- srcEnd: v.optional(v.number()),
825
- /**
826
- * Stable id of the structured item the comment targets (e.g. a spec
827
- * requirement/criterion id), when the reviewed output is rendered as structured
828
- * items rather than free prose. Absent for prose-range comments.
829
- */
830
- anchorId: v.optional(v.string()),
831
- /** The reviewer's note on this block / item. */
832
- body: v.string(),
833
- });
834
- /**
835
- * The standardized, stored verdict a quality companion produced for an output it
836
- * graded — shared by every companion site (the pipeline companion step and the
837
- * requirements-rework gate). The raw model response is {@link companionAssessmentSchema}
838
- * (rating + summary + comments); this is the persisted, self-describing record of how
839
- * that assessment was applied: the `rating`, the `threshold` it was judged against,
840
- * whether it `passed`, and the `feedback` surfaced to the human / fed into a rework.
841
- */
842
- export const companionVerdictSchema = v.object({
843
- /** Overall quality of the graded output (0..1, higher = better). */
844
- rating: v.pipe(v.number(), v.minValue(0), v.maxValue(1)),
845
- /** The quality bar the rating had to reach to pass. */
846
- threshold: v.pipe(v.number(), v.minValue(0), v.maxValue(1)),
847
- /** Whether the rating met the threshold. */
848
- passed: v.boolean(),
849
- /** The companion's challenge / justification (its assessment summary). */
850
- feedback: v.string(),
851
- });
852
- /**
853
- * A human approval gate raised after a step whose pipeline marked it
854
- * `requiresApproval`. Unlike a {@link Decision} (which an agent raises and which
855
- * re-runs the same step on resolution), an approval gate fires once the step has
856
- * already produced its `proposal`; approving advances the run (carrying the —
857
- * possibly edited — proposal forward as context), requesting changes re-runs the
858
- * same step with the human's `feedback` (+ per-block `comments`), and rejecting
859
- * stops the run entirely (a terminal `rejected` failure the board can retry).
860
- */
861
- export const stepApprovalSchema = v.object({
862
- /** Unique id of this gate; the durable run parks on it like a decision. */
863
- id: v.string(),
864
- /** `pending` while awaiting the human; terminal `approved`/`rejected`; `changes_requested` re-runs the step. */
865
- status: v.picklist(['pending', 'approved', 'changes_requested', 'rejected']),
866
- /** The agent's output the human is reviewing (editable before approval). */
867
- proposal: v.string(),
868
- /** When changes were requested, the human's freeform guidance fed into the re-run. */
869
- feedback: v.optional(v.string()),
870
- /** When changes were requested, per-block review comments fed into the re-run. */
871
- comments: v.optional(v.array(stepReviewCommentSchema)),
872
- });
873
- /**
874
- * The agent flows that produce an "agent run" (a container-backed job whose
875
- * lifecycle, progress and failure the board surfaces uniformly):
876
- * - `bootstrap` — a "bootstrap repo" run that scaffolds/adapts a new repo.
877
- * - `execution` — a task pipeline run that implements a board task.
878
- * - `env-config-repair` — a coding agent that repairs an environment-provider
879
- * config file in an existing repo (no board block; surfaced on the infra window).
880
- */
881
- export const agentRunKindSchema = v.picklist(['bootstrap', 'execution', 'env-config-repair']);
882
- /**
883
- * How an agent run faulted, so the board can classify the failure (and hint
884
- * whether a retry is likely to help). The union spans both flows; a given flow
885
- * only ever produces a subset:
886
- * - `preflight` — rejected before dispatch (repo missing/not empty, not connected). [bootstrap]
887
- * - `dispatch` — the container accept-request itself failed (HTTP / network). [bootstrap]
888
- * - `evicted` — the container vanished mid-run (eviction/crash). Retrying spins a fresh one.
889
- * - `timeout` — a container watchdog fired (inactivity or max-duration).
890
- * - `agent` — the agent / git push reported a failure.
891
- * - `job_failed` — an async container job came back failed. [execution]
892
- * - `rejected` — a human rejected a gated proposal, stopping the run. [execution]
893
- * - `cancelled` — the user (or an orphan sweep) explicitly stopped the run.
894
- * - `unknown` — anything not otherwise classified.
895
- */
896
- export const agentFailureKindSchema = v.picklist([
897
- 'preflight',
898
- 'dispatch',
899
- // A `deployer` step's ephemeral-environment provisioning failed (the EnvironmentProvider
900
- // threw or returned `status:'failed'`) — distinct from `dispatch` (a container/runner
901
- // never accepting the job). The provider's verbatim error rides the failure `detail`.
902
- 'environment',
903
- 'evicted',
904
- 'timeout',
905
- 'agent',
906
- 'job_failed',
907
- 'rejected',
908
- // A companion agent could not return a parseable quality assessment (truncated /
909
- // malformed) even after a repair retry, so the run was failed for human attention.
910
- // (Exhausting the automatic rework budget no longer fails the run — it parks on the
911
- // companion iteration-cap gate for a human; see `companion.exceeded`.)
912
- 'companion_rejected',
913
- // The run was still `running` in storage but its durable driver was gone (a crashed /
914
- // restarted orchestrator left the advance job orphaned), and the stale-run sweeper could
915
- // not recover it within the hard-stall deadline — so it is failed for human attention
916
- // instead of spinning `running` forever with no progress. Retry spins a fresh run.
917
- 'stalled',
918
- 'cancelled',
919
- 'unknown',
920
- ]);
921
- /**
922
- * Structured diagnostics captured when an agent run fails, stored on the run and
923
- * surfaced on the board so a crash isn't just a one-line message. The container's
924
- * stdout/stderr can't always be pulled into this record (an evicted container is
925
- * gone), so for `evicted`/`timeout` failures the `hint` points at where to look.
926
- */
927
- export const agentFailureSchema = v.object({
928
- kind: agentFailureKindSchema,
929
- /** Human-readable summary (mirrors the run's `error` for back-compat). */
930
- message: v.string(),
931
- /** Extended detail when available (the harness's reason, an HTTP body, …). */
932
- detail: v.nullable(v.string()),
933
- /** Where to look next (e.g. "check the container logs for this job id"). */
934
- hint: v.nullable(v.string()),
935
- /**
936
- * Optional machine-readable cause code so the SPA can render precise, actionable guidance
937
- * without string-matching the prose `message`/`detail` (the failure analogue of a
938
- * {@link ConflictReason}). Kind-scoped: an `environment` failure carries an
939
- * {@link EnvironmentFailureReason} (e.g. `deploy_runner_unwired`). Absent when the cause has
940
- * no client-specific handling.
941
- */
942
- reason: v.optional(v.nullable(v.string())),
943
- /** Epoch ms the failure was recorded. */
944
- occurredAt: v.number(),
945
- /** Last subtask counts seen before the failure, for context (null if none). */
946
- lastSubtasks: v.nullable(stepSubtasksSchema),
947
- /**
948
- * Index of the pipeline step that was in flight when the run failed (the run's
949
- * `currentStep` at fail time), so the per-attempt failure trail can be attributed to a
950
- * specific step — the step-detail overlay filters its "execution history" to the failures
951
- * recorded for that step. Absent on a bootstrap failure (no steps) and on legacy records.
952
- */
953
- stepIndex: v.optional(v.number()),
954
- });
955
- /**
956
- * A SUCCESSFUL step attempt whose output a restart later superseded — the positive
957
- * complement of {@link agentFailureSchema}. When a run is restarted from a step, that
958
- * step and every later one are reset and their `output` dropped; the ones that had
959
- * already succeeded are recorded here so the step-detail overlay's "execution history"
960
- * surfaces what a superseded attempt PRODUCED, not only the errors. Attributed to a
961
- * `stepIndex` exactly like a failure, and rides in the run's `detail` JSON (no column).
962
- */
963
- export const priorStepOutputSchema = v.object({
964
- /** Index of the pipeline step that produced this output (see {@link agentFailureSchema} `stepIndex`). */
965
- stepIndex: v.number(),
966
- /** Epoch ms the superseded attempt finished (its `finishedAt`, else when it was recorded). */
967
- occurredAt: v.number(),
968
- /** The attempt's prose/JSON output, clipped to a stored-size bound when {@link truncated}. */
969
- output: v.string(),
970
- /** Whether {@link output} was clipped because the original exceeded the per-entry size bound. */
971
- truncated: v.optional(v.boolean()),
972
- });
973
- /**
974
- * State a polling **gate** step carries (today `ci` and `conflicts`). A gate is
975
- * special (like a `deployer` step): it is NOT itself an LLM/container agent. It
976
- * runs a programmatic precheck against a provider (CI check runs / PR mergeability)
977
- * for the PR head commit and only escalates to a helper container agent (`ci-fixer`
978
- * / `conflict-resolver`) on a negative verdict, looping until the precheck passes or
979
- * the attempt budget is spent. Which gate a step is comes from its `agentKind`, so it
980
- * is not duplicated here. See the engine's `GateDefinition` registry.
981
- * - `phase: 'checking'` — running the precheck / waiting for the provider.
982
- * - `phase: 'working'` — a helper agent is in flight (tracked via the step's
983
- * `jobId`); on completion the gate returns to `checking`.
984
- */
985
- /** One failing check the CI gate's precheck saw, flattened for display. */
986
- export const gateFailingCheckSchema = v.object({
987
- name: v.string(),
988
- /** GitHub conclusion (e.g. `failure`, `timed_out`), or null when not reported. */
989
- conclusion: v.nullable(v.string()),
990
- /**
991
- * The check run's GitHub web URL (`html_url`), so the UI can link straight to the
992
- * failed run's logs. Null when GitHub didn't report one.
993
- */
994
- url: v.optional(v.nullable(v.string())),
995
- /**
996
- * The repo (owner/name) this check belongs to, on a MULTI-REPO block — so the UI can group
997
- * failing checks by service. Absent on a single-repo block (there is only the own repo).
998
- */
999
- repo: v.optional(v.string()),
1000
- });
1001
- /**
1002
- * One helper-agent attempt the gate dispatched (a ci-fixer / conflict-resolver run),
1003
- * recorded when the job finishes so the UI can show what each attempt tried and how it
1004
- * ended — detail that used to be discarded the moment the gate re-probed.
1005
- */
1006
- export const gateAttemptSchema = v.object({
1007
- /** 1-based attempt number (matches `attempts` at the time the helper was dispatched). */
1008
- attempt: v.number(),
1009
- /** Epoch ms when the helper job finished. */
1010
- at: v.number(),
1011
- /**
1012
- * How the helper job ended:
1013
- * - `completed` — the container finished (it may or may not have fully fixed the
1014
- * issue; the gate's next precheck is the source of truth, and `summary` carries
1015
- * the agent's own account, e.g. which files it left conflicting).
1016
- * - `failed` — the job errored / was evicted without finishing.
1017
- */
1018
- outcome: v.picklist(['completed', 'failed']),
1019
- /** The PR head commit the helper worked against, when known. */
1020
- headSha: v.optional(v.nullable(v.string())),
1021
- /**
1022
- * The fixing instructions handed to the helper for this round — the failing-check
1023
- * summary the CI gate fed the `ci-fixer`, the conflict reason / human-review comments
1024
- * the other gates fed their fixer. Stashed at dispatch and recorded with the attempt so
1025
- * the run-detail UI can show WHAT each round was asked to fix (not only that a round
1026
- * happened) — the gate analogue of the Tester attempt's `concerns`. Null when the gate
1027
- * hands its fixer no textual instructions (the conflicts gate: GitHub reports mergeability
1028
- * as a single bit and the harness leaves the conflict markers for the resolver).
1029
- */
1030
- instructions: v.optional(v.nullable(v.string())),
1031
- /**
1032
- * Structured failing checks handed to this attempt's helper (the CI gate's red check runs
1033
- * behind {@link instructions}), snapshotted at dispatch so each attempt shows the checks it
1034
- * set out to fix. Absent for the conflicts gate (no file-level detail) and when the round
1035
- * carried no structured checks.
1036
- */
1037
- failingChecks: v.optional(v.nullable(v.array(gateFailingCheckSchema))),
1038
- /** The helper's own summary (or the failure reason), naming what it did / what remains. */
1039
- summary: v.optional(v.nullable(v.string())),
1040
- });
1041
- export const gateStepStateSchema = v.object({
1042
- phase: v.picklist(['checking', 'working']),
1043
- /** How many helper-agent attempts have been dispatched so far. */
1044
- attempts: v.number(),
1045
- /** Ceiling on attempts, resolved from the task's merge preset at step start. */
1046
- maxAttempts: v.number(),
1047
- /** The PR head commit being gated, once resolved (the own-service PR on a multi-repo block). */
1048
- headSha: v.optional(v.nullable(v.string())),
1049
- /**
1050
- * Per-PR head commits for a MULTI-REPO block (service-connections phase 4), keyed by repo
1051
- * full name (owner/name) — own-service PR plus each peer-service PR. Set by the CI /
1052
- * conflicts gates whose precheck aggregates across every PR the task opened. Absent for a
1053
- * single-repo block (the scalar {@link headSha} is the only head).
1054
- */
1055
- headShas: v.optional(v.nullable(v.record(v.string(), v.string()))),
1056
- /**
1057
- * The repo the conflicts gate's most recent `fail` verdict found conflicted, so the
1058
- * single-repo conflict-resolver is dispatched at THAT repo (own-service or a peer) rather
1059
- * than always the own-service one. Absent ⇒ the own-service repo. Only the conflicts gate
1060
- * sets it (the CI-fixer runs across all repos, so the CI gate leaves it undefined).
1061
- */
1062
- conflictTarget: v.optional(v.nullable(v.object({
1063
- repo: v.string(),
1064
- frameId: v.optional(v.string()),
1065
- branch: v.optional(v.string()),
1066
- }))),
1067
- /**
1068
- * The most recent precheck verdict, so the UI can show why the gate is looping
1069
- * (failing → a helper is fixing) vs idle-passing. Set on every probe.
1070
- */
1071
- lastVerdict: v.optional(v.nullable(v.picklist(['pass', 'pending', 'fail']))),
1072
- /**
1073
- * Human-readable summary of the latest failing precheck (the failing CI checks /
1074
- * the conflict reason) — the conclusion detail that used to be fed only to the
1075
- * helper agent and then discarded. Carried across the helper dispatch so the
1076
- * window keeps showing what is being fixed. Null when the last probe passed.
1077
- */
1078
- lastFailureSummary: v.optional(v.nullable(v.string())),
1079
- /**
1080
- * Structured failing checks behind {@link lastFailureSummary} for the CI gate, so
1081
- * the UI can list each red check by name + conclusion. Absent for the conflicts
1082
- * gate (GitHub reports no file-level detail) and when the last probe passed.
1083
- */
1084
- failingChecks: v.optional(v.nullable(v.array(gateFailingCheckSchema))),
1085
- /**
1086
- * The fixing instructions handed to the most-recently dispatched helper (the failing-check
1087
- * summary / conflict reason / human fix prompt), stashed at dispatch so the attempt recorded
1088
- * when that helper's job settles can carry WHAT the round was asked to fix onto its
1089
- * {@link gateAttemptSchema} entry. Transient bookkeeping — the durable per-round history lives
1090
- * on {@link attemptLog}. Null when the gate hands its fixer no textual instructions.
1091
- */
1092
- lastDispatchedInstructions: v.optional(v.nullable(v.string())),
1093
- /**
1094
- * Epoch ms of the release marker for a time-windowed gate (post-release-health) — the
1095
- * moment it began watching the deployed release. The gate keeps polling `pending`
1096
- * until this + the preset's watch window has elapsed (then a clean run passes) or a
1097
- * monitor/SLO regresses (then it escalates to the on-call agent). Absent for the
1098
- * CI/conflicts gates.
1099
- */
1100
- watchSince: v.optional(v.nullable(v.number())),
1101
- /**
1102
- * The watch-window length (minutes) for a time-windowed gate (post-release-health),
1103
- * resolved from the task's merge preset ONCE on first entry (alongside `maxAttempts`)
1104
- * so the probe doesn't re-load the block + re-resolve the preset on every poll. Absent
1105
- * for the CI/conflicts gates.
1106
- */
1107
- watchWindowMinutes: v.optional(v.nullable(v.number())),
1108
- /**
1109
- * The regressed signals captured when the post-release-health gate escalated to the
1110
- * on-call agent, so the agent's completion handler can build the `release_regression`
1111
- * notification + incident enrichment from the SAME evidence the agent investigated
1112
- * — rather than re-reading Datadog (a third round-trip that could also disagree with
1113
- * what the agent saw if the window moved). Absent for the CI/conflicts gates.
1114
- */
1115
- regressedSignals: v.optional(v.nullable(v.array(releaseSignalSchema))),
1116
- /**
1117
- * Append-only history of the helper-agent attempts this gate dispatched (ci-fixer /
1118
- * conflict-resolver runs), each recorded when its job finished. Lets the UI show what
1119
- * every attempt tried and how it ended, instead of only a bare `attempts` count.
1120
- * Absent for the post-release-health gate (its on-call helper is resolved specially).
1121
- */
1122
- attemptLog: v.optional(v.nullable(v.array(gateAttemptSchema))),
1123
- // ---- human-review gate only (absent for the CI/conflicts/post-release-health gates) ----
1124
- /**
1125
- * The number of approving reviews the PR had at the last probe, so the UI can show
1126
- * "1 / N approvals". The "required" side is derived from {@link requiredApprovingReviewCount}
1127
- * via the same `max(1, …)` floor the gate applies (see review.logic.ts) rather than persisted
1128
- * a second time. Absent for the other gates.
1129
- */
1130
- lastApprovals: v.optional(v.nullable(v.number())),
1131
- /**
1132
- * The raw branch-protection required-approving-review count, cached after the FIRST probe
1133
- * resolves it so subsequent polls skip the static protection read (branch protection is repo
1134
- * config, not PR activity — re-reading it every poll over a multi-day review only burns GitHub
1135
- * rate budget). The UI's displayed "required" count is `max(1, this)` (the gate's effective
1136
- * floor). Absent for the other gates.
1137
- */
1138
- requiredApprovingReviewCount: v.optional(v.nullable(v.number())),
1139
- /**
1140
- * The GraphQL ids of the review threads the gate just handed the `fixer`, stashed at
1141
- * dispatch so the helper-completion hook can post a reply + RESOLVE exactly those threads
1142
- * on GitHub before the next probe reads them. Absent for the other gates.
1143
- */
1144
- pendingThreadIds: v.optional(v.nullable(v.array(v.string()))),
1145
- /**
1146
- * Epoch ms of the newest plain PR comment the gate has already handed the `fixer`. Plain
1147
- * conversation comments (unlike review threads) can't be "resolved" on GitHub, so they are
1148
- * tracked by timestamp: a comment newer than this is outstanding; the dispatch advances it to
1149
- * the batch max. A reviewer's later comment (newer timestamp) re-opens the work. Absent for
1150
- * the other gates.
1151
- */
1152
- lastAddressedCommentAt: v.optional(v.nullable(v.number())),
1153
- /**
1154
- * The grace window (minutes) the human-review gate waits after the latest review comment
1155
- * before dispatching the fixer, resolved from the task's merge preset ONCE on first entry
1156
- * (alongside `maxAttempts`) so the probe doesn't re-resolve the preset every poll. Absent
1157
- * for the other gates.
1158
- */
1159
- humanReviewGraceMinutes: v.optional(v.nullable(v.number())),
1160
- /**
1161
- * A human-initiated freeform fix request parked on the gate (an in-app prompt). Consumed at
1162
- * the top of the next `evaluateGate` pass, which dispatches the fixer with these instructions
1163
- * folded in — bypassing the grace window. Absent for the other gates.
1164
- */
1165
- pendingFix: v.optional(v.nullable(v.object({
1166
- instructions: v.string(),
1167
- at: v.number(),
1168
- }))),
1169
- });
1170
- /**
1171
- * State a `tester` step carries while it runs the Tester → Fixer loop. Unlike `ci`,
1172
- * the gate's own work IS a container job (the Tester); on a withheld greenlight the
1173
- * engine loops a `fixer` container agent and re-tests.
1174
- * - `phase: 'testing'` — a Tester job is in flight (tracked via the step's `jobId`).
1175
- * - `phase: 'fixing'` — a Fixer job is in flight; on completion the step returns to
1176
- * `testing` and a fresh Tester job is dispatched.
1177
- */
1178
- /**
1179
- * One round of the Tester→Fixer loop, recorded when a `fixer` job finishes so the test
1180
- * window can show what each fixer attempt set out to fix and how it ended — the analogue of
1181
- * a polling gate's {@link gateAttemptSchema}, since a fixer run is otherwise an opaque
1182
- * sub-job with no surface of its own (only a bare `attempts` count).
1183
- */
1184
- export const testerAttemptSchema = v.object({
1185
- /** 1-based fixer round (matches `attempts` after the fixer for this round was dispatched). */
1186
- attempt: v.number(),
1187
- /** Epoch ms when the fixer job finished. */
1188
- at: v.number(),
1189
- /** Whether the fixer container finished (`completed`) or errored/was evicted (`failed`). */
1190
- outcome: v.picklist(['completed', 'failed']),
1191
- /** The fixer's own summary (or the failure reason), naming what it changed / what failed. */
1192
- summary: v.optional(v.nullable(v.string())),
1193
- /**
1194
- * The concerns the fixer was handed for this round (from the Tester report that withheld
1195
- * its greenlight), so the window can show WHAT each round tried to address — not only that
1196
- * a round happened.
1197
- */
1198
- concerns: v.optional(v.nullable(v.array(testConcernSchema))),
1199
- });
1200
- export const testerStepStateSchema = v.object({
1201
- phase: v.picklist(['testing', 'fixing']),
1202
- /** How many `fixer` attempts have been dispatched so far. */
1203
- attempts: v.number(),
1204
- /** Ceiling on fixer attempts, resolved from the task's merge preset at step start. */
1205
- maxAttempts: v.number(),
1206
- /** The most recent Tester report (what was tested, outcomes, concerns, greenlight). */
1207
- lastReport: v.optional(v.nullable(testReportSchema)),
1208
- /**
1209
- * Append-only history of the `fixer` rounds this Tester step looped through, each recorded
1210
- * when its job finished. Lets the test window surface an inspectable timeline of the fixer
1211
- * attempts (what each addressed, how it ended) instead of only a bare `attempts` count.
1212
- */
1213
- attemptLog: v.optional(v.nullable(v.array(testerAttemptSchema))),
1214
- /**
1215
- * The most recent in-container docker-compose dependency stand-up record (local-infra
1216
- * tester): whether the dependencies came up and the captured (redacted, bounded)
1217
- * `docker compose up` logs. Refreshed on each Tester round (it stands the infra up anew),
1218
- * so the test window can surface WHY local infra failed to come up — the failure-class
1219
- * artifact the orchestrator-side provisioning logs can't see. Absent for ephemeral /
1220
- * no-infra runs. See {@link testerInfraSetupSchema}.
1221
- */
1222
- infraSetup: v.optional(v.nullable(testerInfraSetupSchema)),
1223
- });
1224
- /**
1225
- * One test quality-control companion verdict, recorded per QC evaluation of a Tester
1226
- * report (in order; newest last). `adequate` is the QC's judgement that the report is
1227
- * complete enough to conclude testing / go to the fixer; when false, `gaps` lists the
1228
- * concrete things the Tester still needs to exercise and `feedback` is the prose the
1229
- * Tester is handed on its re-run.
1230
- */
1231
- export const testerQualityVerdictSchema = v.object({
1232
- /** Whether the report is complete/coherent enough to proceed (no QC re-run needed). */
1233
- adequate: v.boolean(),
1234
- /** The QC's prose challenge / justification, folded into the Tester's re-run context. */
1235
- feedback: v.string(),
1236
- /** Concrete coverage gaps the Tester must still address (empty when adequate). */
1237
- gaps: v.array(v.string()),
1238
- /** Epoch ms the verdict was produced. */
1239
- at: v.number(),
1240
- /** The model that produced the verdict, for transparency. */
1241
- model: v.optional(v.nullable(v.string())),
1242
- });
1243
- /**
1244
- * Live test quality-control loop state carried on a run's Tester step, copied from the
1245
- * pipeline's per-step {@link testerQualityConfigSchema} at run start. The QC companion reads
1246
- * each Tester report BEFORE the greenlight/fixer decision; when the report is inadequate and
1247
- * `attempts < maxAttempts` it loops the Tester (folding the prior report + `feedback` in),
1248
- * bounded independently of the fixer budget. `verdicts` records each evaluation for the UI.
1249
- */
1250
- export const testerQualityStepStateSchema = v.object({
1251
- /** Whether the QC companion is active on this Tester step (the builder toggle). */
1252
- enabled: v.boolean(),
1253
- /** How many QC-driven Tester re-runs have been dispatched so far. */
1254
- attempts: v.optional(v.number(), 0),
1255
- /** Ceiling on QC-driven re-runs, from the task's merge preset (`maxTesterQualityIterations`). */
1256
- maxAttempts: v.number(),
1257
- /** Optional estimate gating copied from the pipeline; evaluated against the block estimate. */
1258
- gating: v.optional(v.nullable(stepGatingSchema)),
1259
- /** One verdict per QC evaluation, in order (newest last). Empty before the first grade. */
1260
- verdicts: v.array(testerQualityVerdictSchema),
1261
- /** Set true once the QC budget was spent with the report still judged inadequate. */
1262
- exceeded: v.optional(v.boolean()),
1263
- });
1264
- /**
1265
- * The compact ephemeral-environment view a `human-test` gate carries on its step, so the
1266
- * dedicated window can surface the live URL/status without a second fetch. The full record
1267
- * (with encrypted access creds) lives in the `environments` table; this is the non-secret
1268
- * projection. Null in degraded manual mode (no env provider wired) or after the human
1269
- * destroys the env from the gate.
1270
- */
1271
- /**
1272
- * The compact, non-secret projection of the ephemeral environment a run's step is
1273
- * associated with — its lifecycle state, public URL, TTL, and (when failed) the
1274
- * exact provider error. Surfaced in a run's details (esp. the Tester step) so the
1275
- * env's spinning-up / running / shut-down / errored state is visible without a
1276
- * second fetch. The full record (with encrypted creds) lives in the `environments`
1277
- * table. {@link humanTestEnvironmentSchema} is the human-test gate's subset of this.
1278
- */
1279
- export const runEnvironmentSchema = v.object({
1280
- /** The `environments` row id (lets a window fetch access creds / re-poll status). */
1281
- id: v.string(),
1282
- /** The provisioned public URL (null while still provisioning). */
1283
- url: v.nullable(v.string()),
1284
- /** The environment lifecycle status; see {@link environmentStatusSchema}. */
1285
- status: environmentStatusSchema,
1286
- /** Epoch ms the environment expires (TTL), when known. */
1287
- expiresAt: v.optional(v.nullable(v.number())),
1288
- /** The verbatim provider error when the environment failed/expired, else null. */
1289
- lastError: v.optional(v.nullable(v.string())),
1290
- /**
1291
- * The service's declared provision type this environment was stood up for
1292
- * (`kubernetes` | `docker-compose` | `custom` | `infraless`), recorded at provision
1293
- * time so a run's details show exactly what was provisioned. Null for legacy rows /
1294
- * pre-resolution.
1295
- */
1296
- provisionType: v.optional(v.nullable(provisionTypeSchema)),
1297
- /**
1298
- * The resolved engine that handled the provisioning (`local-docker` | `local-k3s` |
1299
- * `remote-kubernetes` | `remote-custom` | `none`), surfaced in run details alongside the
1300
- * environment state. Null for legacy rows / pre-resolution.
1301
- */
1302
- engine: v.optional(v.nullable(infraEngineSchema)),
1303
- });
1304
- /**
1305
- * The lifecycle status of the per-run container backing a container agent step:
1306
- * `starting` (dispatching / cold-booting), `up` (running the agent's job),
1307
- * `errored` (the container failed to start, was evicted, or its job faulted), and
1308
- * `destroyed` (the run's container has been reclaimed). The SPA additionally derives
1309
- * `destroyed` for a finished run's container steps (the container is reclaimed as a
1310
- * unit when the run terminates), so the backend only ever persists the first three.
1311
- */
1312
- export const runContainerStatusSchema = v.picklist(['starting', 'up', 'errored', 'destroyed']);
1313
- /**
1314
- * The compact, non-secret projection of the per-run container a container agent step
1315
- * runs in, so a run's details can show WHAT the container is doing and WHERE it lives
1316
- * instead of a step's "spinning up container…" badge vanishing into a blank "working"
1317
- * state once the container is up. Populated by the engine across the dispatch + poll
1318
- * lifecycle of an async (container) step; only ever set on container-backed steps.
1319
- */
1320
- export const runContainerSchema = v.object({
1321
- /** The container lifecycle status; see {@link runContainerStatusSchema}. */
1322
- status: runContainerStatusSchema,
1323
- /**
1324
- * The coarse phase the agent's job is in while the container is `up` (`clone` →
1325
- * `agent` → `push`, seeded `starting`), forwarded from the harness. Lets the details
1326
- * distinguish "still preparing the checkout" from "the agent is making calls". Absent
1327
- * until the first poll, or when the runner doesn't report a phase.
1328
- */
1329
- phase: v.optional(v.nullable(v.string())),
1330
- /** Provider container/runner id (Cloudflare DO id, docker container id), when known. */
1331
- id: v.optional(v.nullable(v.string())),
1332
- /** A reachable address for the running container (the local docker host URL), when one exists. */
1333
- url: v.optional(v.nullable(v.string())),
1334
- });
1335
- /** The web-search backend a run's container searches through, when search is available. */
1336
- export const webSearchProviderSchema = v.picklist(['brave', 'searxng']);
1337
- /**
1338
- * Narrow a free-text stored value (a telemetry `provider` column, which is plain TEXT) back
1339
- * to the {@link WebSearchProvider} union, or null when it isn't one. The single source of
1340
- * truth both telemetry stores use to map their rows, so the union is defined once.
1341
- */
1342
- export function isWebSearchProvider(value) {
1343
- return value === 'brave' || value === 'searxng';
1344
- }
1345
- /**
1346
- * Whether a container agent had web search available for its run, and — when it did —
1347
- * which upstream backend served it (resolved backend-side at dispatch from the run's
1348
- * account keys, else the deployment default). Surfaced on a container step so the run
1349
- * details can say "Web search: SearXNG" vs "Web search: unavailable"; it is a static
1350
- * dispatch-time fact, NOT gated by prompt-recording telemetry (the performed queries
1351
- * are — see the agent-search-query observability sink). `provider` is null when search
1352
- * was unavailable.
1353
- */
1354
- export const webSearchAvailabilitySchema = v.object({
1355
- available: v.boolean(),
1356
- provider: v.nullable(webSearchProviderSchema),
1357
- });
1358
- /**
1359
- * The TERMINAL per-frame outcome of one environment a `deployer` step provisioned during a
1360
- * multi-env fan-out (the task's own service frame + every involved-service frame): `ready`
1361
- * (a live env, `url` set), `failed` (the provision broke, `error` carries the cause), or
1362
- * `skipped` (the frame is `infraless`, nothing stood up). The IN-FLIGHT frame is not recorded
1363
- * here — it lives on `step.jobId`/`step.deployFrameId` until it settles. See
1364
- * {@link pipelineStepSchema.entries.deployEnvs}.
1365
- */
1366
- export const deployEnvStateSchema = v.object({
1367
- status: v.picklist(['ready', 'failed', 'skipped']),
1368
- /** The provisioned URL for a `ready` env (absent for `failed`/`skipped`). */
1369
- url: v.optional(v.nullable(v.string())),
1370
- /** The verbatim provider error for a `failed` env. */
1371
- error: v.optional(v.nullable(v.string())),
1372
- });
1373
- /** Per-frame deploy outcomes keyed by service-frame block id; see {@link deployEnvStateSchema}. */
1374
- export const deployEnvsSchema = v.record(v.string(), deployEnvStateSchema);
1375
- export const humanTestEnvironmentSchema = v.object({
1376
- /** The `environments` row id, so the window can fetch access creds / re-poll status. */
1377
- id: v.string(),
1378
- /** The provisioned public URL the human tests against (null while still provisioning). */
1379
- url: v.nullable(v.string()),
1380
- /** The environment lifecycle status; see {@link environmentStatusSchema}. */
1381
- status: environmentStatusSchema,
1382
- /** Epoch ms the environment expires (TTL), when known. */
1383
- expiresAt: v.optional(v.nullable(v.number())),
1384
- });
1385
- /**
1386
- * One round of human-driven remediation on a `human-test` gate: the human wrote findings and
1387
- * asked for a fix (helper `fixer`), or pulled main and hit a conflict (helper
1388
- * `conflict-resolver`). Appended when the round opens and stamped with its outcome once the
1389
- * helper job settles, so the window can show the full history of what was asked and how it ended.
1390
- */
1391
- export const humanTestRoundSchema = v.object({
1392
- /** The kind of round — a findings-driven fix or a pull-main-with-conflicts resolve. */
1393
- kind: v.picklist(['fix', 'pull-main']),
1394
- /** The human's findings prompt (fix), or a one-line note for the pull-main round. */
1395
- findings: v.string(),
1396
- /** The helper container kind this round dispatched (`fixer` / `conflict-resolver`). */
1397
- helperKind: v.string(),
1398
- /** The helper job's id while it ran, for cross-referencing the run timeline. */
1399
- jobId: v.optional(v.nullable(v.string())),
1400
- /** How the helper ended once its job settled. Absent while still in flight. */
1401
- outcome: v.optional(v.nullable(v.picklist(['completed', 'failed']))),
1402
- /** Epoch ms the round opened (the human clicked Request fix / Pull main). */
1403
- at: v.number(),
1404
- });
1405
- /**
1406
- * State a `human-test` gate carries while it runs. Unlike a polling gate (`ci`/`conflicts`)
1407
- * there is no programmatic verdict — the HUMAN is the verdict — so the step spins up an
1408
- * ephemeral environment, parks for a person to validate it, and on demand dispatches the same
1409
- * helpers the other gates use (the Tester's `fixer` for findings; the `conflict-resolver` for a
1410
- * conflicting pull-main). Phases:
1411
- * - `provisioning` — an environment is being stood up (the driver polls until ready).
1412
- * - `awaiting_human` — parked: the human tests the env and confirms / requests a fix / etc.
1413
- * - `fixing` — a `fixer` job (from the human's findings) is in flight.
1414
- * - `resolving_conflicts` — a `conflict-resolver` job (from a conflicting pull-main) is in flight.
1415
- * - `passed` — the human confirmed; the env is torn down and the run advances.
1416
- */
1417
- export const humanTestStepStateSchema = v.object({
1418
- phase: v.picklist(['provisioning', 'awaiting_human', 'fixing', 'resolving_conflicts', 'passed']),
1419
- /** The live ephemeral environment (null in degraded manual mode / after destroy). */
1420
- environment: v.optional(v.nullable(humanTestEnvironmentSchema)),
1421
- /**
1422
- * Why no environment was auto-provisioned — set in degraded manual mode (no env provider
1423
- * wired, or provisioning errored) so the window can explain it and let the human test
1424
- * against the PR branch manually. Absent when an env was provisioned.
1425
- */
1426
- degradedReason: v.optional(v.nullable(v.string())),
1427
- /** How many helper (fixer / conflict-resolver) attempts have been dispatched so far. */
1428
- attempts: v.number(),
1429
- /** Ceiling on helper attempts, resolved from the task's merge preset (`ciMaxAttempts`). */
1430
- maxAttempts: v.number(),
1431
- /** The PR head commit being tested, when known. */
1432
- headSha: v.optional(v.nullable(v.string())),
1433
- /** Append-only history of fix / pull-main rounds; see {@link humanTestRoundSchema}. */
1434
- rounds: v.optional(v.array(humanTestRoundSchema)),
1435
- /**
1436
- * Transient action the human requested while the gate is parked — recorded on the parked
1437
- * step and consumed by the durable driver when it re-enters the gate (the analogue of
1438
- * `pendingIncorporation` on a requirements gate). Cleared once the driver acts on it.
1439
- */
1440
- pendingAction: v.optional(v.nullable(v.object({
1441
- type: v.picklist(['confirm', 'request-fix', 'pull-main', 'recreate']),
1442
- /** The findings prompt for a `request-fix` action. */
1443
- findings: v.optional(v.string()),
1444
- }))),
1445
- });
1446
- /**
1447
- * One actual-vs-reference pairing the visual-confirmation gate shows the human: a logical
1448
- * view, the screenshot the UI tester captured of it (`actualArtifactId`), and the reference
1449
- * design image for the same view when one was uploaded (`referenceArtifactId`). Either side
1450
- * may be absent (a captured view with no reference, or a reference whose view wasn't captured).
1451
- */
1452
- export const visualConfirmPairSchema = v.object({
1453
- view: v.string(),
1454
- actualArtifactId: v.optional(v.nullable(v.string())),
1455
- referenceArtifactId: v.optional(v.nullable(v.string())),
1456
- });
1457
- /** One human-requested fix round on a visual-confirmation gate (dispatches the `fixer`). */
1458
- export const visualConfirmRoundSchema = v.object({
1459
- findings: v.string(),
1460
- helperKind: v.string(),
1461
- jobId: v.optional(v.nullable(v.string())),
1462
- outcome: v.optional(v.nullable(v.picklist(['completed', 'failed']))),
1463
- at: v.number(),
1464
- });
1465
- /**
1466
- * State a `visual-confirmation` gate carries while it runs. Like `human-test` there is no
1467
- * programmatic verdict — a HUMAN reviews the UI tester's screenshots against the uploaded
1468
- * reference designs and approves, or requests a fix (which dispatches the `fixer` and then
1469
- * re-captures via the UI tester). Phases:
1470
- * - `awaiting_human`— parked: the human reviews actual-vs-reference and approves / requests a fix.
1471
- * - `fixing` — a `fixer` job (from the human's findings) is in flight.
1472
- * - `approved` — the human approved; the run advances.
1473
- *
1474
- * (A dedicated `capturing` phase for an auto re-run of the UI tester after a fix is deferred
1475
- * until that loop is wired — see the visual-confirmation handover doc — so it is intentionally
1476
- * absent from the picklist rather than carried as dead state.)
1477
- */
1478
- export const visualConfirmStepStateSchema = v.object({
1479
- phase: v.picklist(['awaiting_human', 'fixing', 'approved']),
1480
- /** The actual-vs-reference pairs the human reviews, refreshed on each (re)capture. */
1481
- pairs: v.optional(v.array(visualConfirmPairSchema)),
1482
- /** Set when no screenshots could be gathered (no UI tester ran / no storage) — manual mode. */
1483
- degradedReason: v.optional(v.nullable(v.string())),
1484
- /** How many fixer attempts have been dispatched so far. */
1485
- attempts: v.number(),
1486
- /** Ceiling on fixer attempts, resolved from the task's merge preset (`ciMaxAttempts`). */
1487
- maxAttempts: v.number(),
1488
- /** Append-only history of fix rounds; see {@link visualConfirmRoundSchema}. */
1489
- rounds: v.optional(v.array(visualConfirmRoundSchema)),
1490
- /**
1491
- * Transient action the human requested while parked — consumed by the durable driver
1492
- * when it re-enters the gate. Cleared once acted on.
1493
- */
1494
- pendingAction: v.optional(v.nullable(v.object({
1495
- type: v.picklist(['approve', 'request-fix', 'recapture']),
1496
- /** The findings prompt for a `request-fix` action. */
1497
- findings: v.optional(v.string()),
1498
- }))),
1499
- });
1500
- /**
1501
- * Per-step LLM observability rollup: a compact aggregate over every model call the
1502
- * step's container made, recorded by the LLM proxy and summed by the engine for the
1503
- * board. It surfaces, at a glance, token usage, how close the step ran to its
1504
- * output-token limit (truncation), the latency split between transport/proxy
1505
- * overhead and actual model execution, and any errors/warnings. The full per-call
1506
- * detail (prompts + responses) is fetched on demand for the drill-down panel.
1507
- * Absent when the observability sink is not wired.
1508
- */
1509
- export const stepMetricsSchema = v.object({
1510
- /** Number of model calls recorded for this step. */
1511
- calls: v.number(),
1512
- /** Sum of prompt (input) tokens across the step's calls. */
1513
- promptTokens: v.number(),
1514
- /**
1515
- * Sum of prompt tokens served from the provider's prefix cache. A subset of
1516
- * promptTokens on OpenAI/DeepSeek, but on Anthropic cache reads are reported
1517
- * separately from input tokens, so this can exceed promptTokens. 0 on a cache-less
1518
- * flavour (Workers AI); the metrics bar shows the cached split when present. Absent ⇒
1519
- * unknown (older snapshot).
1520
- */
1521
- cachedPromptTokens: v.optional(v.number()),
1522
- /** Sum of completion (output) tokens across the step's calls. */
1523
- completionTokens: v.number(),
1524
- /** Largest single completion the model produced (closest approach to the limit). */
1525
- peakCompletionTokens: v.number(),
1526
- /** The output ceiling in effect (max requested `max_tokens`), or null when unknown. */
1527
- maxOutputTokens: v.nullable(v.number()),
1528
- /** Calls cut short by the output limit (`finish_reason === 'length'`). */
1529
- truncatedCalls: v.number(),
1530
- /** Sum of model execution time (ms) — the "actual prompt/tool execution" slice. */
1531
- upstreamMs: v.number(),
1532
- /** Sum of transport/proxy overhead (ms) — the interim-layer cost. */
1533
- overheadMs: v.number(),
1534
- /** Calls that failed (non-2xx / refused / in-process error). */
1535
- errors: v.number(),
1536
- /** Successful calls that warned (truncated or content-filtered). */
1537
- warnings: v.number(),
1538
- });
1539
- export const pipelineStepSchema = v.object({
1540
- /**
1541
- * Id of the execution run (the {@link executionInstanceSchema} `id`) this step
1542
- * belongs to — surfaced on every step so a lone step in a log line or a detail view
1543
- * can name its run, for easier debugging. A projection that always equals the parent
1544
- * instance's `id`: stamped from the enclosing instance when the run is read or
1545
- * emitted, not persisted independently. Absent only on steps not yet round-tripped.
1546
- */
1547
- runId: v.optional(v.string()),
1548
- agentKind: agentKindSchema,
1549
- state: agentStateSchema,
1550
- progress: v.number(),
1551
- /** LLM observability rollup for this step; see {@link stepMetricsSchema}. */
1552
- metrics: v.optional(v.nullable(stepMetricsSchema)),
1553
- /**
1554
- * Live gate state while a polling gate step (`ci` / `conflicts`) runs its
1555
- * precheck-or-escalate loop; see {@link gateStepStateSchema}. The gate kind is
1556
- * `agentKind`.
1557
- */
1558
- gate: v.optional(v.nullable(gateStepStateSchema)),
1559
- /** Live Tester→Fixer loop state while a `tester` step runs/fixes; see {@link testerStepStateSchema}. */
1560
- test: v.optional(v.nullable(testerStepStateSchema)),
1561
- /**
1562
- * Live test quality-control companion state on a `tester-api`/`tester-ui` step, copied
1563
- * from the pipeline's per-step `testerQuality` config at run start. Drives the QC loop that
1564
- * gates each Tester report for completeness before the greenlight/fixer decision. Absent
1565
- * for non-Tester steps / when the companion is disabled. See {@link testerQualityStepStateSchema}.
1566
- */
1567
- testerQuality: v.optional(v.nullable(testerQualityStepStateSchema)),
1568
- /**
1569
- * Live state of a `human-test` gate (ephemeral env + human validation loop); see
1570
- * {@link humanTestStepStateSchema}. Absent for every other step kind.
1571
- */
1572
- humanTest: v.optional(v.nullable(humanTestStepStateSchema)),
1573
- /**
1574
- * Live state of a `visual-confirmation` gate (screenshot review + fix loop); see
1575
- * {@link visualConfirmStepStateSchema}. Absent for every other step kind.
1576
- */
1577
- visualConfirm: v.optional(v.nullable(visualConfirmStepStateSchema)),
1578
- /**
1579
- * The ephemeral environment this step runs against (when the block has one), so a
1580
- * run's details can show its spinning-up / running / shut-down / errored state +
1581
- * the exact error. Populated by the engine for container/deployer steps from the
1582
- * block's live environment; see {@link runEnvironmentSchema}. The `human-test` gate
1583
- * keeps its own richer `humanTest.environment` and is not double-populated here.
1584
- */
1585
- environment: v.optional(v.nullable(runEnvironmentSchema)),
1586
- /** Live subtask counts while an async (container) step runs; see {@link stepSubtasksSchema}. */
1587
- subtasks: v.optional(stepSubtasksSchema),
1588
- /**
1589
- * The per-run container this async (container) step runs in — its lifecycle status
1590
- * (starting / up / errored), the agent's current phase (clone / agent / push), and
1591
- * the container's id + reachable URL once up. Lets a run's details surface what the
1592
- * container is doing and where it lives, so the board shows an explicit "Spinning up
1593
- * container…" → live-phase progression instead of a blank "working" state. Set the
1594
- * moment the job is dispatched (the dispatch blocks until the container accepts the
1595
- * job) and refined on each poll. Only ever set on async (container) steps; absent on
1596
- * non-container steps and steps not yet dispatched. See {@link runContainerSchema}.
1597
- */
1598
- container: v.optional(v.nullable(runContainerSchema)),
1599
- /**
1600
- * Whether web search was available to this container step, and which upstream backend
1601
- * served it. Set at dispatch (a static per-run fact resolved from the account's
1602
- * web-search keys, else the deployment default). Only ever set on async (container)
1603
- * steps; absent on non-container steps and steps not yet dispatched. Distinct from the
1604
- * telemetry-gated per-query log — this is always surfaced. See {@link webSearchAvailabilitySchema}.
1605
- */
1606
- search: v.optional(v.nullable(webSearchAvailabilitySchema)),
1607
- decision: v.nullable(decisionSchema),
1608
- /**
1609
- * Whether a human approval gate fires after this step completes. Copied from
1610
- * the pipeline's `gates` at run start; absent means no gate.
1611
- */
1612
- requiresApproval: v.optional(v.boolean()),
1613
- /**
1614
- * The live approval gate for this step (see {@link stepApprovalSchema}). Set
1615
- * once the step's proposal is ready and `requiresApproval` is true; null/absent
1616
- * otherwise.
1617
- */
1618
- approval: v.optional(v.nullable(stepApprovalSchema)),
1619
- /**
1620
- * Live state of a companion step that reviews a preceding producer step. Set when
1621
- * this step's `agentKind` is a companion kind. `threshold` is the quality bar the
1622
- * companion's latest rating (the last `verdicts` entry) must reach; `attempts`
1623
- * counts only the AUTOMATIC reworks performed, and once it reaches `maxAttempts` the
1624
- * step parks on the iteration-cap gate (`exceeded`) for a human rather than failing.
1625
- * A human "request changes" on the companion's gate also re-runs the producer but does
1626
- * NOT consume `attempts` (only the automatic loop is budgeted). Absent for non-companion steps.
1627
- */
1628
- companion: v.optional(v.nullable(v.object({
1629
- /** The quality bar (0..1) the latest verdict's rating must reach; seeded from the pipeline. */
1630
- threshold: v.number(),
1631
- /** The automatic rework budget: once `attempts` reaches this the gate parks for a human (`exceeded`). */
1632
- maxAttempts: v.number(),
1633
- /**
1634
- * How many AUTOMATIC reworks the companion has driven so far (the producer is
1635
- * looped back once per failed verdict). Human "request changes" cycles are not
1636
- * counted. Defaults to 0; once it reaches `maxAttempts` the step parks on the
1637
- * iteration-cap gate (`exceeded`) — an "extra round" raises `maxAttempts` by one.
1638
- */
1639
- attempts: v.optional(v.number(), 0),
1640
- /**
1641
- * One standardized {@link companionVerdictSchema} per grading cycle, in order —
1642
- * the full sequence of correction iterations (the producer is re-run after each
1643
- * rejected verdict), including any human-driven ones. Empty before the first
1644
- * grade; the last entry is the latest.
1645
- */
1646
- verdicts: v.array(companionVerdictSchema),
1647
- /**
1648
- * Set true when the automatic rework budget (`maxAttempts`) was spent with the
1649
- * rating still below the bar: instead of failing the run, the step parks on its
1650
- * approval gate for a human to resolve via the shared iteration-cap surface
1651
- * (one more round / proceed anyway / stop & reset). Cleared once the human grants
1652
- * an extra round (the loop resumes). Absent until/unless the cap is hit.
1653
- */
1654
- exceeded: v.optional(v.boolean()),
1655
- }))),
1656
- /**
1657
- * Live Follow-up companion state while a `coder` step runs/parks: the items the Coder
1658
- * streamed (loose ends / side-tasks / questions), whether the companion is enabled, and
1659
- * the send-back loop budget. Items accrue live as the harness streams them (the blinking
1660
- * companion); at the step's completion the engine parks the run while any item is
1661
- * `pending`, then loops the Coder for any `queued` follow-up / `answered` question. See
1662
- * {@link followUpsStepStateSchema}. Absent for non-`coder` steps / when the companion is off.
1663
- */
1664
- followUps: v.optional(v.nullable(followUpsStepStateSchema)),
1665
- /**
1666
- * Live implementation-fork decision state while a `coder` step runs its optional
1667
- * two-phase flow: the proposer explore job (`proposing`), the human park
1668
- * (`awaiting_choice` / `answering`), the resolved choice (`chosen`), or one of the
1669
- * pass-through terminals (`single_path` / `skipped`). Created lazily by the engine
1670
- * when the phase activates — the config lives on the block + the risk policy, never
1671
- * on the step. Absent for non-`coder` steps / when the phase never activated. See
1672
- * {@link forkDecisionStepStateSchema}.
1673
- */
1674
- forkDecision: v.optional(v.nullable(forkDecisionStepStateSchema)),
1675
- /**
1676
- * Live "Ralph loop" state carried on a `ralph` step: the persistent retry-until-done
1677
- * loop's iteration count, budget, validation command, and per-iteration history. Seeded
1678
- * from the block's per-task agent config at step start, then advanced each iteration by
1679
- * the engine's `RalphController`. Because it rides the run's persisted `detail` blob, both
1680
- * durable drivers + both stale-run sweepers re-drive a mid-loop run from exactly this
1681
- * state after a restart. Absent for non-`ralph` steps. See {@link ralphStepStateSchema}.
1682
- */
1683
- ralph: v.optional(v.nullable(ralphStepStateSchema)),
1684
- /**
1685
- * Transient re-entry marker carried on a parked `coder` step whose fork decision is
1686
- * `answering`: set when the human sends a chat message so the run is signalled to
1687
- * wake and the durable driver, on re-entering, runs the inline chat LLM and appends
1688
- * the assistant reply (the LLM work that must not block the HTTP request). Cleared
1689
- * once that async cycle completes. Documented beside `pendingIncorporation` /
1690
- * `pendingInterview`. Absent when no chat turn is pending.
1691
- */
1692
- pendingForkChat: v.optional(v.nullable(v.object({ messageId: v.string() }))),
1693
- /**
1694
- * Live PR deep-review state carried on a `pr-reviewer` step: the sliced, severity-ordered
1695
- * findings the read-only reviewer produced, the human's curated selection, and how it was
1696
- * resolved. Recorded by the engine when the reviewer container job completes; the run then
1697
- * parks (`awaiting_selection`) for the human to select findings through the dedicated
1698
- * window and resolve. Absent for non-`pr-reviewer` steps. See {@link prReviewStepStateSchema}.
1699
- */
1700
- prReview: v.optional(v.nullable(prReviewStepStateSchema)),
1701
- /**
1702
- * The at-most-once driver marker for the PR-review "post" resolution: set when the human
1703
- * resolves a parked review with `post`, so the durable driver — on re-entry, off the HTTP
1704
- * request — publishes the selected findings as inline PR review comments (via
1705
- * `RepoFiles.createReview`) exactly once. Consumed (cleared + persisted) BEFORE the posting
1706
- * side effect so a Workflows retry/replay can't post the review twice. Cleared once posted.
1707
- */
1708
- pendingPrReviewPost: v.optional(v.nullable(v.boolean())),
1709
- /**
1710
- * Transient rework feedback carried on a PRODUCER step while it is being re-run by
1711
- * a downstream companion (the analogue of an approval's `changes_requested`
1712
- * feedback for the automatic path). Folded into the agent's revision context on the
1713
- * re-run, then cleared. Absent when no companion rework is in flight.
1714
- */
1715
- rework: v.optional(v.nullable(v.object({
1716
- /** The producer's previous proposal the companion challenged. */
1717
- previousProposal: v.string(),
1718
- /** The companion's prose feedback driving the rework. */
1719
- feedback: v.string(),
1720
- /** Optional per-item / per-block challenges to address. */
1721
- comments: v.optional(v.array(stepReviewCommentSchema)),
1722
- }))),
1723
- /**
1724
- * Transient incorporation intent carried on a parked `requirements-review` gate step.
1725
- * Set when the human answers the findings and asks to incorporate: the run is signalled
1726
- * to wake and the durable driver, on re-entering the gate, folds the answers into a
1727
- * document and re-reviews it (the LLM work that used to block the HTTP request). Cleared
1728
- * once that async cycle completes. `feedback` is the human's optional "do it differently"
1729
- * direction (a redo). Absent when no incorporation is pending.
1730
- */
1731
- pendingIncorporation: v.optional(v.nullable(v.object({ feedback: v.optional(v.string()) }))),
1732
- /**
1733
- * Transient recommendation intent carried on a parked `requirements-review` gate step.
1734
- * Set when the human asks the Requirement Writer to suggest answers for a batch of findings
1735
- * (or re-requests one): the run is signalled to wake and the durable driver, on re-entering
1736
- * the gate, runs the Writer per finding — filling in the `pending` placeholder
1737
- * recommendations — then re-parks (recommendations never advance the run). Cleared once that
1738
- * async batch completes. `itemIds` are the findings to recommend for; `note` steers the
1739
- * whole batch. Absent when no recommendation batch is pending.
1740
- */
1741
- pendingRecommendation: v.optional(v.nullable(v.object({ itemIds: v.array(v.string()), note: v.optional(v.string()) }))),
1742
- /**
1743
- * Transient interview intent carried on a parked `initiative-interviewer` gate step. Set
1744
- * when the human has answered the planning questions and asked to continue (or proceed):
1745
- * the run is signalled to wake and the durable driver, on re-entering the gate, runs the
1746
- * interviewer LLM again against the answers — asking follow-ups (re-park) or synthesizing
1747
- * the goal/constraints brief and advancing. `proceed` skips any remaining questions.
1748
- * Cleared once that async re-entry completes. Absent when no continuation is pending.
1749
- */
1750
- pendingInterview: v.optional(v.nullable(v.object({ proceed: v.optional(v.boolean()) }))),
1751
- /**
1752
- * Consensus configuration for this step, copied from the pipeline's `consensus`
1753
- * array at run start. Present (with `enabled: true`) when this step should run
1754
- * through the multi-model consensus mechanism; read by the consensus executor
1755
- * (and to decide gating against the block estimate). Absent ⇒ standard agent.
1756
- * See {@link consensusStepConfigSchema}.
1757
- */
1758
- consensus: v.optional(v.nullable(consensusStepConfigSchema)),
1759
- /**
1760
- * Estimate-based gating for this step, copied from the pipeline's `gating` array at
1761
- * run start. When present (with `enabled: true`) the step is skipped at runtime unless
1762
- * the block's task estimate meets the threshold. Absent ⇒ always run. See
1763
- * {@link stepGatingSchema}.
1764
- */
1765
- gating: v.optional(v.nullable(stepGatingSchema)),
1766
- /**
1767
- * Per-step options bag copied from the pipeline's `stepOptions` array at run start (see
1768
- * {@link stepOptionsSchema}). Absent ⇒ all defaults for this step. Read by the engine —
1769
- * e.g. the requirements-review gate consults `stepOptions.autoRecommend`.
1770
- */
1771
- stepOptions: v.optional(v.nullable(stepOptionsSchema)),
1772
- /**
1773
- * True when this step was skipped at runtime because its `gating` was not satisfied
1774
- * (the task estimate fell below the threshold). The step's `state` is `done` with no
1775
- * output; the UI renders it as "skipped (gated)". Absent ⇒ the step ran normally.
1776
- */
1777
- skipped: v.optional(v.boolean()),
1778
- /**
1779
- * Set `true` on a `spec-writer` step that determined the task is purely technical and
1780
- * produced no business specs (its result's `noBusinessSpecs`). Recorded on the step so
1781
- * the spec-companion's convergence — the one point both signals coexist — can combine it
1782
- * with the companion's `technicalCorroborated` verdict to infer the block's `technical`
1783
- * label. Absent for every other kind / a writer that produced specs.
1784
- */
1785
- noBusinessSpecs: v.optional(v.boolean()),
1786
- /**
1787
- * Set on a `spec-companion` step from its `technicalCorroborated` verdict (whether it
1788
- * agreed the task is purely technical). Recorded on the step — not just read off the
1789
- * live assessment — so the engine can infer the block's `technical` label both on the
1790
- * companion's automatic convergence AND on a human "proceed" past the iteration cap,
1791
- * where only the persisted step survives. Absent for every other kind / no opinion.
1792
- */
1793
- technicalCorroborated: v.optional(v.boolean()),
1794
- /** Text the agent produced for this step (when LLM execution is enabled). */
1795
- output: v.optional(v.string()),
1796
- /**
1797
- * The structured JSON a registered CUSTOM kind's agent step returned (the generic
1798
- * manifest-driven `agent` dispatch's `custom` channel). Recorded so the SPA can render
1799
- * it in the `generic-structured` result view (and a post-op already consumed it
1800
- * server-side). Absent for built-in / prose kinds.
1801
- */
1802
- custom: v.optional(v.unknown()),
1803
- /** Identifier of the model that produced `output`, for transparency. */
1804
- model: v.optional(v.string()),
1805
- /**
1806
- * Ids of the prompt-fragment library entries that were folded into this step's
1807
- * system prompt — the manual selection on the block unioned with the relevance
1808
- * selector's pick. Recorded for observability and replay-stability; absent when
1809
- * the fragment-library module is not configured.
1810
- */
1811
- selectedFragmentIds: v.optional(v.array(v.string())),
1812
- /**
1813
- * The repo-sourced Claude Skill this step was PINNED to at dispatch (a `skill` step; see
1814
- * `docs/initiatives/repo-skills.md`). Recorded so a run executes a stable version of the
1815
- * skill even if its source resyncs mid-run, and so a later investigation knows exactly
1816
- * which skill (and at which commit / manifest blob) ran. `commit` is the source dir's head
1817
- * commit the resources were fetched at (null if the skill was never synced to a commit);
1818
- * `sha` is the `SKILL.md` blob sha. Absent for every non-`skill` step.
1819
- */
1820
- skillVersion: v.optional(v.object({
1821
- skillId: v.string(),
1822
- commit: v.nullable(v.string()),
1823
- sha: v.string(),
1824
- })),
1825
- /**
1826
- * Identifier of an in-flight asynchronous agent job (a container run polled by
1827
- * the durable driver). Set while the step is dispatched-but-not-yet-finished so
1828
- * a Workflows replay re-attaches to the running job instead of starting a new
1829
- * one; cleared once the job's result is recorded.
1830
- */
1831
- jobId: v.optional(v.string()),
1832
- /**
1833
- * Epoch ms the step first began executing (transitioned to `working`). Set once
1834
- * and never overwritten on subsequent state changes, so a re-run/replay keeps the
1835
- * original start. Absent until the step starts.
1836
- */
1837
- startedAt: v.optional(v.nullable(v.number())),
1838
- /**
1839
- * Epoch ms the step finished (transitioned to `done`). With {@link startedAt}
1840
- * this yields the step's execution duration. Absent until the step completes.
1841
- */
1842
- finishedAt: v.optional(v.nullable(v.number())),
1843
- /**
1844
- * Epoch ms the step parked on a human (an approval gate, a raised decision, or an
1845
- * iteration-cap gate), freezing its duration clock: while parked, elapsed time stops
1846
- * accruing — the symmetric counterpart of {@link finishedAt}'s terminal freeze, so a
1847
- * step waiting on input is not billed for the human's deliberation. Set once on park,
1848
- * cleared (null) when the step resumes working or finishes. Absent until first parked.
1849
- */
1850
- pausedAt: v.optional(v.nullable(v.number())),
1851
- /**
1852
- * How many times this step's container was evicted/crashed and recovered by
1853
- * automatically re-dispatching a fresh container (bounded by
1854
- * `MAX_EVICTION_RECOVERIES`). Once spent, a further eviction fails the run as
1855
- * `evicted` rather than looping. Absent/0 until the first eviction.
1856
- */
1857
- evictionRecoveries: v.optional(v.number()),
1858
- /**
1859
- * How many times this step's container was evicted by *transient infrastructure
1860
- * churn* — an event the runtime facade flags as not-a-crash (e.g. a deploy
1861
- * draining the sandbox) — and recovered by re-dispatching a fresh container.
1862
- * Counted separately from {@link evictionRecoveries} and bounded by a larger
1863
- * `MAX_TRANSIENT_EVICTION_RECOVERIES`, since such churn can recur several times in
1864
- * a short window, unlike a crash. Absent/0 until the first transient eviction.
1865
- */
1866
- transientEvictionRecoveries: v.optional(v.number()),
1867
- /**
1868
- * The service-provisioning config a `deployer` step PINNED when it dispatched its async,
1869
- * container-backed deploy job, so the later poll/finalize maps the job against the same config
1870
- * the container was built from — NOT a fresh read of the service frame (which a person may have
1871
- * edited mid-flight, e.g. flipping it to `infraless`, which would otherwise fail a deploy whose
1872
- * container already succeeded). Absent for the synchronous raw-manifest path and the undeclared
1873
- * legacy single-connection path (re-resolution is harmless there). See {@link serviceProvisioningSchema}.
1874
- */
1875
- deployProvisioning: v.optional(serviceProvisioningSchema),
1876
- /**
1877
- * A `deployer` step fanning out over several service frames (the task's own frame + each
1878
- * involved-service frame; see the connections initiative) records each frame's TERMINAL
1879
- * outcome here, keyed by frame block id — so a durable replay knows which frames are already
1880
- * provisioned and only the remaining ones are dispatched. The in-flight frame is tracked by
1881
- * {@link deployFrameId} + {@link jobId} until it settles into this map. Absent for a
1882
- * single-frame deploy that never fanned out. See {@link deployEnvsSchema}.
1883
- */
1884
- deployEnvs: v.optional(deployEnvsSchema),
1885
- /**
1886
- * The service FRAME the deployer step's currently in-flight deploy job ({@link jobId}) is
1887
- * provisioning, during a multi-env fan-out — so the poll/finalize maps the settled job onto the
1888
- * right frame's {@link deployEnvs} entry. Cleared once that frame settles; absent when no deploy
1889
- * job is in flight or the step never fanned out.
1890
- */
1891
- deployFrameId: v.optional(v.string()),
1892
- /**
1893
- * The task's OWN (primary) service frame, pinned on the FIRST target resolution of a `deployer`
1894
- * fan-out and reused on every re-entry/replay. Keeps the primary classification STABLE against a
1895
- * mid-flight reparent (which would otherwise re-derive a different own frame and flip an
1896
- * own-service provisioning failure from terminal to a non-terminal peer failure — completing the
1897
- * run `done` despite a failed deploy). Absent until the first resolution / for a step that never
1898
- * fanned out.
1899
- */
1900
- deployPrimaryFrameId: v.optional(v.string()),
1901
- });
1902
- export const executionStatusSchema = v.picklist(['running', 'blocked', 'done', 'paused', 'failed']);
1903
- /**
1904
- * Per-run diagnostic context captured for AFTER-THE-FACT investigation of a run (esp. a
1905
- * failure) — the "where/what did this run actually execute on" facts that were previously
1906
- * spread across the DB (repo↔service↔installation joins), the harness transcript (model), or
1907
- * lost entirely (which backend a step ran on). Stamped by the engine at dispatch and refined
1908
- * on the first poll; it reflects the MOST RECENT container-step dispatch (the step most likely
1909
- * relevant to a failure), not a per-step history. Rides in the run's `detail` JSON (no dedicated
1910
- * column), like {@link ExecutionInstance.notes}/`frontendBindings`. Absent on legacy runs and on
1911
- * runs with no container step (pure inline/gate pipelines). NEVER carries a token or secret.
1912
- */
1913
- export const runDiagnosticsSchema = v.object({
1914
- /** Context of the most recent container-step dispatch. */
1915
- lastDispatch: v.optional(v.object({
1916
- /** Index of the dispatched step within the pipeline. */
1917
- stepIndex: v.number(),
1918
- /** The step's agent kind (`coder`, `merger`, a custom kind, …). */
1919
- agentKind: v.string(),
1920
- /** Resolved model ref `provider:model` (e.g. `anthropic:claude-opus-4-8`); null if unresolved. */
1921
- model: v.optional(v.nullable(v.string())),
1922
- /**
1923
- * Which runner backend the step actually ran on — the datum that distinguishes a native
1924
- * host-process run from a sandboxed container: `local-native` | `local-container` |
1925
- * `runner-pool` | `cloudflare-container`. Filled on the first poll (the transport reports
1926
- * it); absent until then or on an older runtime.
1927
- */
1928
- executionBackend: v.optional(v.string()),
1929
- /** The repo the step operated on. */
1930
- repo: v.optional(v.object({
1931
- owner: v.string(),
1932
- name: v.string(),
1933
- /** The base branch the work branched from. */
1934
- baseBranch: v.optional(v.string()),
1935
- /** VCS provider (`github` | `gitlab`), resolved from the run's repo origin. */
1936
- provider: v.optional(v.string()),
1937
- })),
1938
- /** Epoch ms the dispatch was recorded. */
1939
- at: v.number(),
1940
- })),
1941
- /**
1942
- * The control-plane (orchestrator) host running the engine — NOT necessarily where the agent
1943
- * ran (a container step runs elsewhere; see `lastDispatch.executionBackend`). `platform` is the
1944
- * orchestrator's `process.platform` (e.g. `win32` pins a Windows local deployment — the class
1945
- * of host that surfaced the native-Windows git-auth break). Best-effort.
1946
- */
1947
- host: v.optional(v.object({
1948
- platform: v.optional(v.string()),
1949
- })),
1950
- });
1951
- export const executionInstanceSchema = v.object({
1952
- id: v.string(),
1953
- blockId: v.string(),
1954
- pipelineId: v.string(),
1955
- pipelineName: v.string(),
1956
- steps: v.array(pipelineStepSchema),
1957
- currentStep: v.number(),
1958
- status: executionStatusSchema,
1959
- /**
1960
- * Structured failure diagnostics when `status` is `failed`; absent/null
1961
- * otherwise. Lets a failed task surface the same failure banner + retry as a
1962
- * failed bootstrap (shared {@link agentFailureSchema}).
1963
- */
1964
- failure: v.optional(v.nullable(agentFailureSchema)),
1965
- /**
1966
- * Failures from the run's PRIOR attempts, oldest→newest. Each retry/restart appends
1967
- * the then-current {@link failure} here and clears `failure` on the fresh attempt, so
1968
- * the top failure banner (keyed on `status === 'failed'`) disappears once the task is
1969
- * restarted while the full error trail stays viewable in the "previous errors" history.
1970
- * Absent/empty for a run that has never been failed-then-retried.
1971
- */
1972
- failureHistory: v.optional(v.array(agentFailureSchema)),
1973
- /**
1974
- * Successful outputs from the run's PRIOR attempts that a restart discarded, oldest→newest —
1975
- * the positive complement of {@link failureHistory}. A restart-from-step resets the chosen
1976
- * step and every later one, dropping their `output`; those that had already SUCCEEDED are
1977
- * recorded here (attributed by `stepIndex`) so the step-detail overlay's execution history
1978
- * surfaces the successful outputs a restart superseded, not only the errors. Bounded in count
1979
- * and per-entry size so the run's `detail` JSON doesn't bloat. Absent/empty for a run never
1980
- * restarted past a completed step (a plain retry re-runs only unfinished steps, so it records
1981
- * nothing).
1982
- */
1983
- outputHistory: v.optional(v.array(priorStepOutputSchema)),
1984
- /**
1985
- * Non-fatal advisories computed once at run start — today the frontend UI-test flow's
1986
- * resolved-binding notes ({@link buildFrontendRunNotes}: duplicate env vars, or a partial-live
1987
- * set of bound services where some fall back to WireMock). Mirrors the harness's own
1988
- * `buildInfraNotes` but surfaced on the RUN so the SPA renders it in the run/step detail
1989
- * (distinct from a `failure`, which aborts the run). Absent/empty when there is nothing to
1990
- * flag. Rides in the `detail` JSON column (no dedicated column), reflecting the start-time
1991
- * state even after the underlying envs change.
1992
- */
1993
- notes: v.optional(v.array(v.string())),
1994
- /**
1995
- * The frontend UI-test flow's backend bindings RESOLVED once at run start (env var → the bound
1996
- * service's live ephemeral URL, or absent ⇒ mocked; see {@link resolveFrontendBindings}). Stamped
1997
- * on the run so the SPA's run/step detail projects what the run ACTUALLY drove against — a frozen
1998
- * snapshot that stays truthful after the underlying envs are torn down, rather than re-resolving
1999
- * against current live state (which for a finished run could disagree with the co-located
2000
- * start-time {@link notes}). Rides in the `detail` JSON column; absent for a non-frontend run.
2001
- */
2002
- frontendBindings: v.optional(v.array(resolvedFrontendBindingSchema)),
2003
- /**
2004
- * Internal user id (`usr_*`) of whoever started this run (or retried it). Recorded
2005
- * so the individual-usage restricted mode can use the initiator's OWN personal
2006
- * subscription (e.g. Claude) for the run's steps — a personal credential is never
2007
- * shared, so only its owner's runs may use it. Absent for runs started without a
2008
- * signed-in user (auth-disabled/local dev) and for legacy runs.
2009
- */
2010
- initiatedBy: v.optional(v.nullable(v.string())),
2011
- /**
2012
- * Epoch-ms creation time, stamped when the run is first started. Gives a run a stable
2013
- * creation timestamp independent of when its first step actually starts (the public-API
2014
- * job view reports it as `createdAt`). Absent on legacy runs persisted before this field.
2015
- */
2016
- createdAt: v.optional(v.number()),
2017
- /**
2018
- * Optimistic-concurrency token: a monotonic revision of the persisted run row,
2019
- * bumped on every write. Read back by the repository and used by
2020
- * `compareAndSwap` so a human-action write (resolve decision / approve /
2021
- * request changes) that raced another writer is detected and retried on fresh
2022
- * state instead of silently clobbering it. Defaults to 0 for a run that has
2023
- * never been persisted. The SPA's execution store also keys its monotonic
2024
- * reconcile on it, so a lagging snapshot refresh can't regress a run a live
2025
- * event already advanced.
2026
- */
2027
- rev: v.optional(v.number()),
2028
- /**
2029
- * After-the-fact investigation context — where/what the run's most recent container step
2030
- * executed on (backend, model, repo) plus the control-plane host. Rides in the `detail` JSON
2031
- * (see {@link runDiagnosticsSchema}); absent on legacy runs and pure inline pipelines.
2032
- */
2033
- diagnostics: v.optional(runDiagnosticsSchema),
2034
- });
2035
768
  export const workspaceSchema = v.object({
2036
769
  id: v.string(),
2037
770
  name: v.string(),