@kici-dev/orchestrator 0.4.0 → 0.6.0

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 (183) hide show
  1. package/dist/__test-helpers__/fake-scaler-state-store.d.ts +24 -0
  2. package/dist/__test-helpers__/mock-db-query.d.ts +130 -0
  3. package/dist/__test-helpers__/mock-db.d.ts +27 -0
  4. package/dist/agent/agent-version.d.ts +34 -0
  5. package/dist/agent/dispatcher.d.ts +151 -13
  6. package/dist/agent/host-roster.d.ts +37 -5
  7. package/dist/agent/registry.d.ts +2 -0
  8. package/dist/app.d.ts +56 -7
  9. package/dist/approvals/apply-decision.d.ts +67 -19
  10. package/dist/approvals/held-run-release-wiring.d.ts +45 -0
  11. package/dist/approvals/independent-wiring.d.ts +55 -0
  12. package/dist/approvals/resume-router.d.ts +9 -0
  13. package/dist/approvals/triggerer-subject.d.ts +37 -0
  14. package/dist/cache/dep-cache.d.ts +54 -10
  15. package/dist/cache/global-eval-round-cache.d.ts +95 -0
  16. package/dist/cache/index.d.ts +3 -0
  17. package/dist/cache/pending-global-evals.d.ts +42 -0
  18. package/dist/cache/pending-inits.d.ts +10 -0
  19. package/dist/cli/api-client.d.ts +18 -0
  20. package/dist/cli/commands/cluster-settings.d.ts +52 -3
  21. package/dist/cli/commands/diagnose.d.ts +6 -0
  22. package/dist/cli/commands/held-run.d.ts +58 -0
  23. package/dist/cli/commands/runs.d.ts +1 -0
  24. package/dist/cli/commands/trust-policy.d.ts +117 -12
  25. package/dist/cli/kici-admin.d.ts +10 -0
  26. package/dist/cli.js +2770 -938
  27. package/dist/cluster/cluster-identity.d.ts +3 -3
  28. package/dist/cluster/cluster-settings-reader.d.ts +59 -1
  29. package/dist/cluster/coordinator.d.ts +44 -0
  30. package/dist/cluster/index.d.ts +4 -0
  31. package/dist/cluster/peer-handler.d.ts +19 -0
  32. package/dist/cluster/peer-registry.d.ts +17 -0
  33. package/dist/cluster/plan-headroom-store.d.ts +26 -0
  34. package/dist/cluster/worker-eviction.d.ts +69 -0
  35. package/dist/cold-store/load-event-log-range.d.ts +10 -3
  36. package/dist/cold-store/load-secret-audit-log-range.d.ts +4 -3
  37. package/dist/cold-store/tables/secret-audit-log.d.ts +4 -3
  38. package/dist/config.d.ts +48 -21
  39. package/dist/content-requirements-cache.d.ts +55 -0
  40. package/dist/contexts/held-runs.d.ts +123 -16
  41. package/dist/contexts/protection/aggregate.d.ts +3 -2
  42. package/dist/contexts/protection/branch-gate.d.ts +15 -0
  43. package/dist/contexts/protection/concurrency-gate.d.ts +8 -2
  44. package/dist/contexts/protection/pipeline.d.ts +18 -0
  45. package/dist/contexts/release-queued-holds.d.ts +35 -0
  46. package/dist/dashboard/attestation-filters.d.ts +8 -0
  47. package/dist/dashboard/handler.d.ts +6 -24
  48. package/dist/db/migrations/109_cluster_settings_cache_knobs.d.ts +4 -0
  49. package/dist/db/migrations/110_cluster_settings_global_eval_knobs.d.ts +4 -0
  50. package/dist/db/migrations/111_cluster_settings_global_eval_wait.d.ts +4 -0
  51. package/dist/db/migrations/112_execution_runs_workflow_repo.d.ts +4 -0
  52. package/dist/db/migrations/113_execution_runs_workflow_repo_index.d.ts +30 -0
  53. package/dist/db/migrations/114_ingest_queue_claim.d.ts +4 -0
  54. package/dist/db/migrations/115_global_workflows_cluster_switch.d.ts +9 -0
  55. package/dist/db/migrations/116_org_plan_headroom.d.ts +4 -0
  56. package/dist/db/migrations/117_invoke_gate_proxy_jobs.d.ts +4 -0
  57. package/dist/db/migrations/118_invoke_gate_scheduling.d.ts +4 -0
  58. package/dist/db/migrations/119_scaler_ha_ownership.d.ts +4 -0
  59. package/dist/db/migrations/120_cluster_settings_scaler_reaper.d.ts +4 -0
  60. package/dist/db/migrations/121_org_settings_allow_untrusted_dockerfile_builds.d.ts +19 -0
  61. package/dist/db/migrations/122_host_roster_scaler_managed.d.ts +25 -0
  62. package/dist/db/migrations/123_workflow_registrations_default_branch.d.ts +24 -0
  63. package/dist/db/migrations/124_cluster_settings_provision_backoff.d.ts +4 -0
  64. package/dist/db/migrations/125_org_trust_directory.d.ts +30 -0
  65. package/dist/db/migrations/126_held_runs_posted_pending_check.d.ts +28 -0
  66. package/dist/db/migrations/127_org_trust_policy_approval_expiry_seconds.d.ts +26 -0
  67. package/dist/db/migrations/128_execution_runs_global_eval_round.d.ts +23 -0
  68. package/dist/db/migrations/129_execution_runs_dispatch_routing_key.d.ts +24 -0
  69. package/dist/db/migrations/130_scaler_provision_outcomes.d.ts +32 -0
  70. package/dist/db/types.d.ts +428 -12
  71. package/dist/entry-helpers.d.ts +28 -0
  72. package/dist/events/event-emitter.d.ts +22 -0
  73. package/dist/events/event-router.d.ts +87 -1
  74. package/dist/events/types.d.ts +2 -23
  75. package/dist/git/credential-broker.d.ts +94 -0
  76. package/dist/git/inherited-secret.d.ts +24 -0
  77. package/dist/git/installation-token.d.ts +38 -0
  78. package/dist/git/job-context.d.ts +13 -0
  79. package/dist/index.js +324 -84
  80. package/dist/metrics/agent-metrics-aggregator.d.ts +4 -4
  81. package/dist/metrics/prometheus.d.ts +170 -3
  82. package/dist/oidc/oidc-mint-registration.d.ts +7 -3
  83. package/dist/orchestrator-core.d.ts +91 -24
  84. package/dist/pipeline/content-filter.d.ts +71 -0
  85. package/dist/pipeline/direct-ingress-deps.d.ts +100 -0
  86. package/dist/pipeline/dispatch-matched-workflow.d.ts +521 -11
  87. package/dist/pipeline/global-eval-round.d.ts +307 -0
  88. package/dist/pipeline/internal-event-pipeline.d.ts +140 -0
  89. package/dist/pipeline/invoke-gate.d.ts +164 -0
  90. package/dist/pipeline/job-contexts.d.ts +16 -17
  91. package/dist/pipeline/needs-scheduler.d.ts +13 -0
  92. package/dist/pipeline/process-webhook.d.ts +126 -31
  93. package/dist/pipeline/processor.d.ts +282 -23
  94. package/dist/pipeline/rerun.d.ts +21 -0
  95. package/dist/pipeline/resume-workflow.d.ts +40 -7
  96. package/dist/pipeline/route-or-dispatch-jobs.d.ts +15 -0
  97. package/dist/pipeline/security-hold-check.d.ts +217 -0
  98. package/dist/pipeline/test-pipeline.d.ts +12 -0
  99. package/dist/pipeline/undispatched-hold-checks.d.ts +23 -0
  100. package/dist/pipeline/webhook-payload-store.d.ts +20 -0
  101. package/dist/policy/dashboard-write-policy-listener.d.ts +62 -0
  102. package/dist/policy/dashboard-write-policy.d.ts +14 -0
  103. package/dist/provenance/backfill-run.d.ts +10 -1
  104. package/dist/provider-registry.d.ts +47 -4
  105. package/dist/providers/github/check-status-poster.d.ts +33 -3
  106. package/dist/providers/github/commit-message.d.ts +20 -0
  107. package/dist/providers/github/file-contents.d.ts +40 -0
  108. package/dist/providers/github/index.d.ts +2 -2
  109. package/dist/providers/github/normalizer.d.ts +3 -2
  110. package/dist/providers/local/normalizer.d.ts +3 -5
  111. package/dist/providers/universal-git/config.d.ts +2 -0
  112. package/dist/providers/universal-git/index.d.ts +11 -2
  113. package/dist/providers/universal-git/normalizer.d.ts +10 -0
  114. package/dist/queue/cleanup.d.ts +7 -1
  115. package/dist/queue/job-queue.d.ts +112 -7
  116. package/dist/queue/terminalize-unroutable.d.ts +13 -0
  117. package/dist/registration/extractor.d.ts +3 -3
  118. package/dist/registration/registration-index.d.ts +7 -0
  119. package/dist/registration/registration-run-match.d.ts +47 -0
  120. package/dist/registration/registration-store.d.ts +22 -0
  121. package/dist/reporting/check-run-reporter.d.ts +146 -2
  122. package/dist/reporting/check-run-summary.d.ts +25 -1
  123. package/dist/reporting/execution-tracker.d.ts +324 -9
  124. package/dist/reporting/log-chunk-sink.d.ts +8 -5
  125. package/dist/reporting/run-aggregator.d.ts +4 -14
  126. package/dist/reporting/step-display-order.d.ts +43 -0
  127. package/dist/routes/admin-held-runs.d.ts +142 -0
  128. package/dist/routes/admin-org-settings.d.ts +5 -0
  129. package/dist/routes/admin-trust-policy.d.ts +34 -6
  130. package/dist/routes/admin.d.ts +15 -0
  131. package/dist/scaler/backend-factory.d.ts +55 -0
  132. package/dist/scaler/bare-metal-backend.d.ts +43 -5
  133. package/dist/scaler/claim-store.d.ts +119 -0
  134. package/dist/scaler/config.d.ts +4 -0
  135. package/dist/scaler/container-backend.d.ts +18 -4
  136. package/dist/scaler/container-routing.d.ts +23 -0
  137. package/dist/scaler/container-spawn.d.ts +28 -0
  138. package/dist/scaler/event-backend.d.ts +124 -0
  139. package/dist/scaler/event-provision-reaper.d.ts +350 -0
  140. package/dist/scaler/failure-tracker.d.ts +1 -1
  141. package/dist/scaler/firecracker-backend.d.ts +33 -6
  142. package/dist/scaler/index.d.ts +11 -1
  143. package/dist/scaler/label-matcher.d.ts +4 -3
  144. package/dist/scaler/manager.d.ts +905 -76
  145. package/dist/scaler/resolve-container-auth.d.ts +45 -0
  146. package/dist/scaler/scaler-events.d.ts +11 -0
  147. package/dist/scaler/scaler-state-store.d.ts +294 -10
  148. package/dist/scaler/types.d.ts +131 -13
  149. package/dist/scaler/warm-pool.d.ts +132 -29
  150. package/dist/security/comment-handler.d.ts +50 -11
  151. package/dist/security/global-workflow-policy.d.ts +52 -12
  152. package/dist/security/identity-link.d.ts +60 -0
  153. package/dist/security/lock-source.d.ts +11 -13
  154. package/dist/security/reduced-privilege-note.d.ts +59 -0
  155. package/dist/security/trust-directory-store.d.ts +233 -0
  156. package/dist/security/trust-policy-gate.d.ts +94 -59
  157. package/dist/security/trust-policy-store.d.ts +3 -0
  158. package/dist/security/trust-resolver.d.ts +14 -98
  159. package/dist/server.d.ts +11 -1
  160. package/dist/server.js +56329 -42234
  161. package/dist/stale-detector/gate-deadline-detector.d.ts +52 -0
  162. package/dist/stale-detector/stale-run-detector.d.ts +61 -3
  163. package/dist/standalone.js +37619 -23570
  164. package/dist/webhook/ingest-accept.d.ts +70 -0
  165. package/dist/webhook/ingest-overflow-buffer.d.ts +35 -4
  166. package/dist/webhook/ingest-overflow-replayer.d.ts +50 -6
  167. package/dist/worker/in-memory-job-queue.d.ts +1 -1
  168. package/dist/ws/agent-handler.d.ts +31 -4
  169. package/dist/ws/dashboard-context-handler.d.ts +28 -4
  170. package/dist/ws/dashboard-global-workflows-handler.d.ts +36 -9
  171. package/dist/ws/execution-status-frame.d.ts +32 -0
  172. package/dist/ws/failure-messages.d.ts +2 -0
  173. package/dist/ws/git-credential-relay.d.ts +39 -0
  174. package/dist/ws/oidc-token-relay.d.ts +6 -11
  175. package/dist/ws/platform-client.d.ts +68 -9
  176. package/dist/ws/test-relay-handlers.d.ts +35 -10
  177. package/installer-image-digests.json +3 -3
  178. package/package.json +24 -19
  179. package/sbom.spdx.json +868 -821
  180. package/dist/approvals/team-membership-lookup.d.ts +0 -13
  181. package/dist/pipeline/inline-eval.d.ts +0 -44
  182. package/dist/providers/github/contributor-resolver.d.ts +0 -30
  183. package/dist/security/contributor-cache.d.ts +0 -83
@@ -58,6 +58,17 @@ interface BuildCheckRunSummaryOptions {
58
58
  };
59
59
  /** Total job duration in milliseconds. */
60
60
  jobDurationMs?: number;
61
+ /**
62
+ * Bytes a caller will add around the returned summary — the reduced-privilege
63
+ * note it prepends, the annotation-count line it appends — subtracted from
64
+ * the byte budget below.
65
+ *
66
+ * Without it the budget is spent in full here and the caller's own bytes push
67
+ * the final string over the API's cap, which rejects the entire update and
68
+ * leaves the check run unresolved. Reserving instead drops one more tier of
69
+ * log lines, which costs context rather than the whole report.
70
+ */
71
+ reservedBytes?: number;
61
72
  }
62
73
  interface BuildAnnotationsOptions {
63
74
  stepResults: StepResultData[];
@@ -71,12 +82,25 @@ interface BuildProgressTextOptions {
71
82
  runId: string;
72
83
  };
73
84
  }
85
+ /** GitHub Checks API output.summary byte limit. */
86
+ export declare const SUMMARY_BYTE_LIMIT = 65535;
87
+ /**
88
+ * Cut `summary` so it fits the API's cap, whatever a caller wrapped around it.
89
+ *
90
+ * The last line of defence, not the primary mechanism: `reservedBytes` is what
91
+ * keeps a summary readable by dropping log tiers, and this only runs when the
92
+ * final string is still over. Truncating at a UTF-8 boundary matters — a cut
93
+ * through a multi-byte sequence produces a replacement character and can leave
94
+ * the string one byte over the cap it was cut to fit.
95
+ */
96
+ export declare function clampSummaryToLimit(summary: string, limit?: number): string;
74
97
  export { formatDuration } from '@kici-dev/shared';
75
98
  /**
76
99
  * Build a rich markdown summary for a GitHub Check run output.summary field.
77
100
  *
78
101
  * Includes: headline, step table, failure details with log context, trace footer.
79
- * Respects the 65535 byte limit with progressive log truncation.
102
+ * Respects the 65535 byte limit with progressive log truncation, minus whatever
103
+ * `reservedBytes` the caller will wrap around the result.
80
104
  */
81
105
  export declare function buildCheckRunSummary(opts: BuildCheckRunSummaryOptions): string;
82
106
  /**
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { type Kysely } from 'kysely';
15
15
  import type { Database } from '../db/types.js';
16
+ import { JobKind } from '../db/types.js';
16
17
  import { ExecutionRunStatus, type InitFailure, RunFailureClass, ScalerEventType, OrchLogPhase } from '@kici-dev/engine';
17
18
  import type { ObserverRegistry } from '../ws/observer-registry.js';
18
19
  import type { LogStorage } from './log-storage.js';
@@ -28,6 +29,20 @@ export interface ExecutionContext {
28
29
  installationId?: number;
29
30
  requestId?: string;
30
31
  routingKey?: string;
32
+ /**
33
+ * The repository that DEFINES the workflow, when that is not
34
+ * `repoIdentifier` — a global workflow authored in one repository and
35
+ * dispatched against another. Set only when the two differ, so "present"
36
+ * marks a cross-repository global run; forwarded to the Platform on
37
+ * `execution.status`.
38
+ */
39
+ workflowRepoIdentifier?: string;
40
+ /**
41
+ * True when this run records a global evaluation round rather than a
42
+ * workflow. Forwarded to the Platform so its own re-run refusal can admit the
43
+ * round's re-evaluation.
44
+ */
45
+ isGlobalEvalRound?: boolean;
31
46
  /** Git branch or tag (e.g. "main", "feature/foo"). */
32
47
  ref?: string;
33
48
  /** Trigger event type (e.g. "push", "pr:open"). */
@@ -56,6 +71,15 @@ export interface ExecutionContext {
56
71
  * subscriptions can match on it. Null/undefined for success or non-terminal.
57
72
  */
58
73
  failureClass?: RunFailureClass | null;
74
+ /**
75
+ * Resolved trust tier of the run's ref, and which branch's lock file it was
76
+ * evaluated against. Mirror the `execution_runs.trust_tier` /
77
+ * `lock_file_source` columns; both absent for a run whose trust never
78
+ * resolved. Read by the check-run reporter to name the reduced-privilege
79
+ * posture on a job's completion summary.
80
+ */
81
+ trustTier?: string;
82
+ lockFileSource?: string;
59
83
  /** Workflow-level concurrency config from the lock file. */
60
84
  concurrency?: {
61
85
  cancelInProgress?: boolean;
@@ -71,6 +95,7 @@ interface WorkflowCompleteCallbackData {
71
95
  jobResults: Array<{
72
96
  name: string;
73
97
  status: string;
98
+ outputs?: Record<string, unknown>;
74
99
  }>;
75
100
  routingKey?: string;
76
101
  repo: string;
@@ -98,6 +123,24 @@ export interface ExecutionTrackerDeps {
98
123
  observerRegistry?: ObserverRegistry;
99
124
  /** Optional callback when execution reaches terminal state. */
100
125
  onExecutionComplete?: (runId: string, status: Extract<ExecutionRunStatus, 'success' | 'failed' | 'cancelled'>, context: ExecutionContext, description?: string) => void;
126
+ /**
127
+ * Drop the in-process state a run leaves behind — its pending job contexts
128
+ * and its still-open eval gates.
129
+ *
130
+ * Separate from {@link ExecutionTrackerDeps.onExecutionComplete} because that
131
+ * callback is NOT the terminalization chokepoint it reads as. It fires on the
132
+ * three paths that finish a run whose jobs ran (`completeRun`, and both
133
+ * stale-detector arms) and on none of the paths that terminalize a run whose
134
+ * jobs never did — `failRun` (the expired-approval sweep and every
135
+ * pre-dispatch abort) and `cancelHeldRun` (an install gate rejected). Those
136
+ * are exactly the runs most likely to be holding a pending job context, since
137
+ * a context is only stored for a job that was gated rather than dispatched.
138
+ *
139
+ * Synchronous by contract: the in-memory half of the cleanup must not be
140
+ * deferred behind an await the caller does not hold, and the DB half is
141
+ * fire-and-forget inside the callback.
142
+ */
143
+ onRunTerminalCleanup?: (runId: string) => void;
101
144
  /** Optional callback to forward step status to Platform. */
102
145
  onStepStatusForward?: (runId: string, jobId: string, jobName: string, stepIndex: number, stepName: string, state: string, timestamp: number, data?: Record<string, unknown>, requestId?: string) => void;
103
146
  /** Optional callback when a completed run is pruned from memory. */
@@ -194,6 +237,7 @@ export declare class ExecutionTracker {
194
237
  private readonly db;
195
238
  private readonly observerRegistry?;
196
239
  private readonly onExecutionComplete?;
240
+ private readonly onRunTerminalCleanup?;
197
241
  private readonly onStepStatusForward?;
198
242
  private readonly onRunPruned?;
199
243
  private readonly onWorkflowComplete?;
@@ -302,7 +346,14 @@ export declare class ExecutionTracker {
302
346
  /** Agent provenance label when triggered through an agent credential. */
303
347
  triggeredByAgentLabel?: string | null,
304
348
  /** Pull-request number for PR-triggered runs; null/omitted for non-PR runs. */
305
- prNumber?: number | null): Promise<void>;
349
+ prNumber?: number | null,
350
+ /**
351
+ * The repository that DEFINES the workflow, when that is not
352
+ * `repoIdentifier` — an organization-wide workflow authored in one
353
+ * repository and dispatched against another. Omitted/null for every
354
+ * per-repository run, where the two are the same repository.
355
+ */
356
+ workflowRepoIdentifier?: string | null): Promise<void>;
306
357
  /**
307
358
  * Upsert one execution_jobs row per dispatched job (idempotent on
308
359
  * (run_id, job_id) to tolerate a race with an early `onJobStatus`). The
@@ -335,6 +386,34 @@ export declare class ExecutionTracker {
335
386
  * after a peer ACKs a reroute.
336
387
  */
337
388
  markJobReroutedToPeer(runId: string, jobId: string, peerId: string): Promise<void>;
389
+ /**
390
+ * Whether a rerouted job has visibly started, read from the shared
391
+ * `execution_jobs` row rather than from a relayed progress frame.
392
+ *
393
+ * The coordinator's spawn-window backstop learns about progress from
394
+ * `job.progress`, which only a WORKER peer sends — a worker has no database,
395
+ * so relaying is its only channel. A peer COORDINATOR writes the job's status
396
+ * straight into this table instead, so silence on the wire says nothing about
397
+ * whether the job is running, and the backstop would cancel a healthy job.
398
+ *
399
+ * False when no row exists yet (the peer has not started the job) and when the
400
+ * row is still `pending`; the `started_at` half covers a status that has
401
+ * already moved on to a terminal value.
402
+ */
403
+ hasJobStarted(runId: string, jobId: string): Promise<boolean>;
404
+ /**
405
+ * Whether a job has reached a terminal state in the shared `execution_jobs`
406
+ * row.
407
+ *
408
+ * The companion read to {@link hasJobStarted}: a peer COORDINATOR runs a
409
+ * rerouted job against this same table and relays no terminal `job.progress`,
410
+ * so the row is the only signal the routing coordinator gets that the job is
411
+ * over and its reroute tracking can be released.
412
+ *
413
+ * False when no row exists yet — an absent row is a job that has not started,
414
+ * never a finished one.
415
+ */
416
+ isJobTerminal(runId: string, jobId: string): Promise<boolean>;
338
417
  /**
339
418
  * Run `fn` while holding a per-run lock, serializing the run-mutating methods
340
419
  * (`onJobStatus`, `addJobsToRun`) so a status reply cannot interleave with the
@@ -362,6 +441,9 @@ export declare class ExecutionTracker {
362
441
  contexts?: string[];
363
442
  skippedContexts?: string[];
364
443
  envWarning?: string;
444
+ jobKind?: JobKind;
445
+ summonedRunId?: string;
446
+ timeoutMs?: number;
365
447
  }>, dispatchedContexts?: string[],
366
448
  /** Synthetic job ID to replace (e.g. needs-pending-deploy-{uuid}). */
367
449
  replaceSyntheticId?: string): Promise<void>;
@@ -455,6 +537,13 @@ export declare class ExecutionTracker {
455
537
  * - `noop`: nothing — a later terminal will free the next slot.
456
538
  */
457
539
  private runWaveSchedulerHook;
540
+ /**
541
+ * When a proxy job of an invoke gate reaches terminal, aggregate the gate once
542
+ * every sibling proxy is terminal: the gate is set `failed` if any proxy
543
+ * failed, else `success`. Setting the gate terminal drives its own downstream
544
+ * `needs` release via the scheduler hook. A no-op for any non-proxy job.
545
+ */
546
+ private runInvokeGateAggregationHook;
458
547
  /**
459
548
  * Phase 9: stuck-jobs invariant check ( Layer 3).
460
549
  * Before declaring a run complete, verify no stuck jobs exist. If any are
@@ -469,6 +558,51 @@ export declare class ExecutionTracker {
469
558
  * observer callbacks, and schedules in-memory pruning.
470
559
  */
471
560
  private finalizeRunCompletion;
561
+ /**
562
+ * Map a terminal run status to the terminal job status a proxy should carry.
563
+ * A failure class collapses to `failed`; a cancel to `cancelled`; everything
564
+ * else (success) to `success`.
565
+ */
566
+ private mapRunStatusToProxyStatus;
567
+ /**
568
+ * Read a summoned run's non-secret declared outputs — the merged `outputs`
569
+ * JSONB of its jobs. Secret-masked outputs live in `run_secret_outputs` and
570
+ * are deliberately NOT read here, so a repo's secret output never crosses into
571
+ * the summoning global run.
572
+ */
573
+ private readNonSecretRunOutputs;
574
+ /**
575
+ * Normalize an `outputs` JSONB cell to a plain object. The Postgres driver may
576
+ * return it already parsed (object) or as a JSON string; both are accepted. A
577
+ * null / empty / malformed cell yields `null`.
578
+ */
579
+ private parseOutputsCell;
580
+ /**
581
+ * When `spawnedRunId` was summoned by an invoke gate, set the gate's proxy job
582
+ * to the run's mapped terminal status and attach its non-secret outputs. The
583
+ * proxy's terminal transition drives the summoning run's scheduler (release the
584
+ * next held proxy, and aggregate the gate once every proxy is terminal).
585
+ */
586
+ private mirrorSummonedRunOntoProxy;
587
+ /**
588
+ * Set a proxy job's timeline span to the summoned run's own started_at /
589
+ * completed_at so its dashboard bar reflects the run it mirrors. A run rejected
590
+ * before executing any step has no start; fall back to its completion (and, for
591
+ * a run with no completion recorded, to the mirror timestamp) so the proxy
592
+ * still renders a point-width bar rather than a status-only placeholder.
593
+ */
594
+ private mirrorSummonedRunSpanOntoProxy;
595
+ /**
596
+ * Reconcile a just-tagged summoned run against its proxy: if the run is already
597
+ * terminal, drive the mirror now. Closes the tag race — a run that finalizes
598
+ * before `summoned_by_run_id` lands (a synchronous all-jobs-rejected completion,
599
+ * or an HA peer finalizing it first) reads a null tag in `mirrorSummonedRunOntoProxy`
600
+ * and skips, so nothing else terminalizes its proxy and the gate hangs until
601
+ * its timeout. The invoke gate calls this after tagging each spawned run; the
602
+ * `onJobStatus` idempotency guard makes a double-fire with the normal finalize
603
+ * a no-op.
604
+ */
605
+ reconcileSummonedRunIfTerminal(spawnedRunId: string): Promise<void>;
472
606
  /**
473
607
  * Mark a run as failed when its build fails (timeout or error).
474
608
  *
@@ -483,8 +617,25 @@ export declare class ExecutionTracker {
483
617
  *
484
618
  * Inserts a minimal execution_runs row with status='failed' directly so the E2E
485
619
  * test (and dashboard) can observe the failure instead of a missing run.
486
- */
487
- onBuildFailedBeforeTracking(runId: string, workflowName: string, provider: string, repoIdentifier: string, ref: string, sha: string, deliveryId: string | null, providerContext: Record<string, unknown>, routingKey: string, triggerEvent?: string, commitMessage?: string, failureReason?: string, initFailure?: InitFailure): Promise<void>;
620
+ *
621
+ * The fifth pre-dispatch recording site, and the third that writes the row and
622
+ * returns. Its row is terminal with no resume path, so no invoke gate ever
623
+ * reads its `chain_depth` — the stamp is here for the same reason as on
624
+ * {@link ExecutionTracker.recordInitFailureRun}: this row is the run's ONLY
625
+ * record, and one that says `0` claims to have started the chain it actually
626
+ * died inside.
627
+ */
628
+ onBuildFailedBeforeTracking(runId: string, workflowName: string, provider: string, repoIdentifier: string, ref: string, sha: string, deliveryId: string | null, providerContext: Record<string, unknown>, routingKey: string, triggerEvent?: string, commitMessage?: string, failureReason?: string, initFailure?: InitFailure,
629
+ /**
630
+ * Internal-trigger provenance, as one object rather than two more
631
+ * positional args on an already 13-wide signature. Shape matches
632
+ * `preDispatchRunProvenance` in `dispatch-matched-workflow.ts`, which is
633
+ * the only caller.
634
+ */
635
+ provenance?: {
636
+ chainDepth?: number;
637
+ dispatchedByFailureLifecycle?: boolean;
638
+ }): Promise<void>;
488
639
  /**
489
640
  * Write a `failed` execution_runs row directly for an init failure, and drop
490
641
  * any in-memory run so the recorded failure is the run's final word. Also
@@ -515,6 +666,91 @@ export declare class ExecutionTracker {
515
666
  initFailure: InitFailure;
516
667
  triggerEvent?: string;
517
668
  commitMessage?: string;
669
+ /**
670
+ * The repository that DEFINES the workflow this run was about to execute.
671
+ * REQUIRED: every run has one, and a caller that does not state it must not
672
+ * compile. Left optional, a global dispatch path added later would record a
673
+ * null marker — and a null marker does not mean "unknown", it means "the
674
+ * workflow lives in this run's own repository", which the whole
675
+ * defining-repository predicate then reads as fact.
676
+ *
677
+ * Narrowed to the differing case here, exactly as `onExecutionStarted`
678
+ * does, so passing `repoIdentifier` (the per-repository case) is correct
679
+ * and records nothing.
680
+ */
681
+ workflowRepoIdentifier: string;
682
+ /**
683
+ * Inherited invoke-chain depth for a run summoned by an invoke gate.
684
+ *
685
+ * An init failure is terminal, so unlike the hold on
686
+ * {@link ExecutionTracker.recordRunHeld} this run never fires a gate of its
687
+ * own and nothing reads the column back to bound recursion. It is recorded
688
+ * because this row is the run's ONLY record: one that says `0` claims to
689
+ * have started the chain it actually died inside.
690
+ */
691
+ chainDepth?: number;
692
+ /**
693
+ * True when a failure-lifecycle trigger dispatched this run, recorded as
694
+ * the run's `trigger_decision` marker
695
+ * (`EventRouter.isFailureLifecycleRun`).
696
+ *
697
+ * Consistency rather than a live reader: this path emits no
698
+ * `__workflow_complete`, so no accumulator reads the marker back today. It
699
+ * is recorded so the row does not disagree with the one
700
+ * {@link ExecutionTracker.recordRunHeld} writes for the same dispatch, and
701
+ * so adding an emit here later cannot silently re-open the self-retrigger
702
+ * loop.
703
+ */
704
+ dispatchedByFailureLifecycle?: boolean;
705
+ }): Promise<void>;
706
+ /**
707
+ * Write one `failed` execution_runs row for a global eval round that never
708
+ * produced verdicts.
709
+ *
710
+ * The round decides which organization-wide workflows apply to an event, so a
711
+ * round that fails suppresses every workflow it was deciding on. Without a row
712
+ * that outcome is invisible: no run was ever created for those workflows, so
713
+ * there is nothing on the dashboard to explain why they did not appear.
714
+ *
715
+ * **One row for the whole round, not one per workflow.** The round exists to
716
+ * collapse N candidate workflows into a single pre-run job; fanning its
717
+ * failure back out into N rows would undo exactly that. The reason names every
718
+ * affected workflow instead.
719
+ *
720
+ * Its `runId` is the round job's own — the last attempt's — so the row, the
721
+ * `dispatch_queue` row, and the attempt's logs all carry one id. No conflict
722
+ * guard is needed beyond `doNothing`: the id was minted for this round and
723
+ * belongs to no other run.
724
+ */
725
+ recordGlobalEvalRoundFailureRun(args: {
726
+ runId: string;
727
+ workflowName: string;
728
+ provider: string;
729
+ repoIdentifier: string;
730
+ ref: string;
731
+ sha: string;
732
+ deliveryId: string | null;
733
+ providerContext: Record<string, unknown>;
734
+ routingKey: string;
735
+ /**
736
+ * The source `providerContext` was taken from, when a cross-provider
737
+ * lock-file fallback made it a different source from `routingKey`.
738
+ *
739
+ * Recorded because the two are a PAIR: a re-run of this round re-drives the
740
+ * organization-wide pass and must hand it the same bundle those credentials
741
+ * belong to. Pairing the stored context with the inbound routing key instead
742
+ * gives one source's credentials to another source's API client.
743
+ */
744
+ dispatchRoutingKey?: string;
745
+ failureReason: string;
746
+ triggerEvent?: string;
747
+ /**
748
+ * The repository whose global workflows the round was deciding. A round is
749
+ * definitionally the cross-repository case, so this is normally a different
750
+ * repository from `repoIdentifier` — recorded, like everywhere else, only
751
+ * when the two actually differ.
752
+ */
753
+ workflowRepoIdentifier: string;
518
754
  }): Promise<void>;
519
755
  /**
520
756
  * Record a run paused at the workflow install gate (a `registries:` /
@@ -549,6 +785,27 @@ export declare class ExecutionTracker {
549
785
  * security hold fail-closed unreachable by the comment path.
550
786
  */
551
787
  prNumber?: number | null;
788
+ /**
789
+ * The repository that DEFINES the held workflow. REQUIRED for the same
790
+ * reason as on {@link ExecutionTracker.recordInitFailureRun}: a hold is a
791
+ * live, resumable run, so a null marker here would misattribute it for the
792
+ * whole time it sits in the queue and for the run it resumes into.
793
+ */
794
+ workflowRepoIdentifier: string;
795
+ /**
796
+ * Inherited invoke-chain depth for a run summoned by an invoke gate.
797
+ * REQUIRED to be threaded by any caller that has one: a hold is resumable,
798
+ * so the resumed run can fire its own invoke gate, and the chain-depth
799
+ * circuit breaker reads this column back. Absent ⇒ the column's `0`
800
+ * default, which means "this run starts a chain".
801
+ */
802
+ chainDepth?: number;
803
+ /**
804
+ * True when a failure-lifecycle trigger dispatched this run, recorded as
805
+ * the run's `trigger_decision` marker so its eventual completion is
806
+ * excluded from batch accumulation (`EventRouter.isFailureLifecycleRun`).
807
+ */
808
+ dispatchedByFailureLifecycle?: boolean;
552
809
  }): Promise<void>;
553
810
  /**
554
811
  * Flip a `held` run back to `pending` so the resumed dispatch can proceed
@@ -593,11 +850,13 @@ export declare class ExecutionTracker {
593
850
  * registered. Returns true when a token was taken, false for an unknown or
594
851
  * already-completed run (nothing to hold).
595
852
  *
596
- * Three registration windows need this. A run whose source-pack `__build__`
853
+ * Four registration windows need this. A run whose source-pack `__build__`
597
854
  * job is dispatched first is registered with that job ALONE, and its real
598
- * jobs are only dispatched once the build finishes. A deferred init job and a
599
- * deferred dynamic entry each register their jobs from a fire-and-forget task
600
- * that outlives the dispatch call. In every case, without a token the already
855
+ * jobs are only dispatched once the build finishes; a run without a build is
856
+ * registered with NO jobs before its dispatch loop, so its jobs land once
857
+ * every dispatch has returned. A deferred init job and a deferred dynamic
858
+ * entry each register their jobs from a fire-and-forget task that outlives
859
+ * the dispatch call. In every case, without a token the already
601
860
  * -registered jobs reaching a terminal state satisfies {@link isRunComplete},
602
861
  * so the run is finalized early: a terminal run status is written, the
603
862
  * provider check is posted, and the status is forwarded to the Platform — all
@@ -612,9 +871,13 @@ export declare class ExecutionTracker {
612
871
  * and an off-by-one job count for the length of the window. A counter holds
613
872
  * the completion check open without being visible to any of them, and it
614
873
  * writes no `execution_jobs` row, so it never surfaces as a phantom job.
615
- * A token whose holder never settles keeps the run `running` until the stale
874
+ * A token whose holder never settles keeps the run open until the stale
616
875
  * detector reaps it — the same backstop that covers a job that never
617
- * reports.
876
+ * reports. That backstop only reaches a run that has at least one job: every
877
+ * sub-scan of the stale detector starts from `execution_jobs` or
878
+ * `dispatch_queue`. So a holder that registers NO jobs must terminalize the
879
+ * run itself rather than rely on the sweep — which is what
880
+ * `dispatchMatchedWorkflow` does when its dispatch loop throws.
618
881
  *
619
882
  * Each token must be paired with exactly one {@link releasePendingJobsHold}.
620
883
  */
@@ -645,6 +908,35 @@ export declare class ExecutionTracker {
645
908
  * Used by LogWriter to construct log paths.
646
909
  */
647
910
  getJobName(runId: string, jobId: string): string | undefined;
911
+ /**
912
+ * Resolve the job name that names a step-log storage path, with a durable
913
+ * fallback. The step-log path is `executions/{runId}/job-{jobName}/…` and it
914
+ * is written from two sites — the chunk writer (`log-chunk-sink`) and the
915
+ * `execution_steps.log_path` upsert in `onStepStatus`. A purely in-memory
916
+ * lookup (`getJobName`) returns `undefined` during the brief window between
917
+ * dispatch and `addJobsToRun` populating `run.jobs`, so an early chunk would
918
+ * land under `job-{jobId}` while the reader later keys on `job-{realName}` —
919
+ * silently losing those lines. Both write sites go through this resolver so
920
+ * they cannot disagree: in-memory first, then the authoritative
921
+ * `dispatch_queue.job_name` (the same source `recoverJobFromDispatchQueue`
922
+ * trusts), and only `jobId` when the job is genuinely unknown.
923
+ */
924
+ resolveJobName(runId: string, jobId: string): Promise<string>;
925
+ /**
926
+ * Seed the in-memory job name for a job the coordinator did not itself
927
+ * dispatch — a worker-rerouted job. Such a job is tracked in the cluster
928
+ * coordinator's own map, never enters this tracker's `run.jobs` via
929
+ * `addJobsToRun`, and has no coordinator-owned `dispatch_queue` row (the
930
+ * worker owns it under a fresh id), so `resolveJobName` would fall back to
931
+ * the bare `jobId` for its early log chunks while the reader keys on the real
932
+ * name — the same split-brain that loses lines. Seeding the name here, at
933
+ * reroute time, makes `resolveJobName`'s fast path correct from the first
934
+ * relayed chunk, independent of when the worker's `dispatch_queue` row becomes
935
+ * visible. In-memory only: the `execution_jobs` row is still created lazily by
936
+ * the worker's first status update. Guarded against clobbering an existing
937
+ * entry so a later terminal status is never overwritten.
938
+ */
939
+ registerJobName(runId: string, jobId: string, jobName: string): void;
648
940
  /**
649
941
  * Find the runId of the in-memory run that owns a given dispatched jobId.
650
942
  *
@@ -660,6 +952,28 @@ export declare class ExecutionTracker {
660
952
  * Used by commit status reporting to access provider/repo/sha info.
661
953
  */
662
954
  getExecutionContext(runId: string): ExecutionContext | undefined;
955
+ /**
956
+ * Stamp the run's resolved trust tier and lock-file branch onto the in-memory
957
+ * state, after `onExecutionStarted` recorded the row.
958
+ *
959
+ * Separate from the start call because trust resolves on its own path and is
960
+ * written to `execution_runs` by its own update. The dispatch site that
961
+ * writes those two columns calls this with the same values, but the two
962
+ * writes are independent and either can be lost: the DB update is
963
+ * fire-and-forget with its own `.catch()`, and this call is wrapped in a
964
+ * `try`/`catch` there. Both are deliberate — the note is worth less than the
965
+ * run — so the in-memory copy and the row CAN disagree, and neither is
966
+ * authoritative for the other. `recoverRunFromDb` reconciles by reading the
967
+ * row, so the durable value is the one that survives.
968
+ *
969
+ * A run this tracker does not know is a no-op: a missing run means the status
970
+ * updates that would read the fields are not coming either.
971
+ *
972
+ * Keep this synchronous. Its one caller wraps it in `try`/`catch`, which
973
+ * catches a throw but not a rejected promise, so an async body would put the
974
+ * failure back outside the guard.
975
+ */
976
+ setRunTrustContext(runId: string, trustTier: string, lockFileSource?: string): void;
663
977
  /**
664
978
  * Update the heartbeat timestamp for a running job.
665
979
  *
@@ -697,6 +1011,7 @@ export declare class ExecutionTracker {
697
1011
  status: ExecutionRunStatus;
698
1012
  routingKey?: string;
699
1013
  repoIdentifier?: string;
1014
+ workflowRepoIdentifier?: string;
700
1015
  sha?: string;
701
1016
  ref?: string;
702
1017
  triggerEvent?: string;
@@ -7,9 +7,12 @@
7
7
  * behave identically — a worker-dispatched job's logs are persisted, counted
8
8
  * and forwarded exactly like a locally-dispatched one's.
9
9
  *
10
- * The storage key is derived from `executionTracker.getJobName`, the same call
11
- * `ExecutionTracker.onStepStatus` uses to fill `execution_steps.log_path`, so
12
- * the reader and the writer cannot disagree about the naming rule.
10
+ * The storage key is derived from `executionTracker.resolveJobName`, the same
11
+ * call `ExecutionTracker.onStepStatus` uses to fill `execution_steps.log_path`,
12
+ * so the reader and the writer cannot disagree about the naming rule — the
13
+ * resolver falls back to `dispatch_queue.job_name` during the dispatch window
14
+ * before in-memory job state is populated, which is what stops an early chunk
15
+ * from being persisted under an unreadable `job-{jobId}` path.
13
16
  */
14
17
  import type { LogStream } from '@kici-dev/engine';
15
18
  import type { LogWriter } from './log-writer.js';
@@ -32,9 +35,9 @@ export interface LogChunkSinkDeps {
32
35
  stepLogBuffer?: StepLogBuffer;
33
36
  /** Durable step-log persistence. Absent when the orchestrator has no database. */
34
37
  logWriter?: LogWriter;
35
- /** Resolves the job name that names the storage path. */
38
+ /** Resolves the job name that names the storage path (durable fallback). */
36
39
  executionTracker?: {
37
- getJobName(runId: string, jobId: string): string | undefined;
40
+ resolveJobName(runId: string, jobId: string): Promise<string>;
38
41
  };
39
42
  /**
40
43
  * Forward to the Platform for browser fan-out. Absent in independent mode.
@@ -1,17 +1,3 @@
1
- /**
2
- * Shared run-detail aggregator.
3
- *
4
- * One source of truth for "DB rows → canonical nested run detail (jobs with
5
- * nested steps + needs + outputs)". The dashboard handler and the agent-facing
6
- * structured run-result route both build on the same `buildRunDetailJobs` /
7
- * `mapRunDetailStep` mapping, so the row→jobs shape never diverges between the
8
- * two read surfaces.
9
- *
10
- * `aggregateRunDetail` is the warm-path convenience used by the agent route: it
11
- * runs the run-header + per-run batch queries and returns the canonical detail.
12
- * (Cold-store fallback stays a dashboard-handler concern — the agent read path
13
- * is warm-only in v1.)
14
- */
15
1
  import type { Kysely } from 'kysely';
16
2
  import { ExecutionJobStatus, type InitFailure } from '@kici-dev/engine';
17
3
  import type { Database } from '../db/types.js';
@@ -58,6 +44,8 @@ export interface RunDetailJobRow {
58
44
  base_job_name: string | null;
59
45
  variant_kind: string | null;
60
46
  variant_label: string | null;
47
+ job_kind: string | null;
48
+ summoned_run_id: string | null;
61
49
  started_at: Date | null;
62
50
  completed_at: Date | null;
63
51
  duration_ms: number | null;
@@ -104,6 +92,8 @@ export declare function buildRunDetailJobs(jobs: RunDetailJobRow[], lookups: Run
104
92
  baseJobName: string | null;
105
93
  variantKind: string | null;
106
94
  variantLabel: string | null;
95
+ jobKind: string | null;
96
+ summonedRunId: string | null;
107
97
  startedAt: number | null;
108
98
  completedAt: number | null;
109
99
  durationMs: number | null;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The order a run's steps are shown in.
3
+ *
4
+ * Every reader of `execution_steps` sorted by `step_index` alone, which puts a
5
+ * pseudo-step wherever its index landed rather than where it ran. That is fine
6
+ * for the cache pseudo-steps, whose indices sit above the real steps and which
7
+ * mostly bracket the work — but a job's image build runs strictly FIRST, and
8
+ * rendering it last reads as though it happened last.
9
+ *
10
+ * The index cannot express this: `step.status` rejects a negative index (an
11
+ * agent sending one to an older orchestrator is disconnected mid-job), and two
12
+ * consumers treat a negative index as absent — the check-run reporter writes
13
+ * `steps[stepIndex]`, and the dashboard gates log fetching on `stepIndex >= 0`.
14
+ * So the ordering is expressed by TYPE, here, once, rather than by each reader
15
+ * inventing its own rule.
16
+ */
17
+ import { type Expression } from 'kysely';
18
+ /**
19
+ * Sort key placing setup pseudo-steps (the ones that ran before the job's real
20
+ * steps) ahead of everything else. Apply BEFORE the `step_index` sort:
21
+ *
22
+ * ```ts
23
+ * qb.orderBy(setupStepsFirst()).orderBy('step_index', 'asc')
24
+ * ```
25
+ *
26
+ * Steps with no type, and every real step, share the same key — so within each
27
+ * group `step_index` remains the only thing that orders them, exactly as before.
28
+ */
29
+ export declare function setupStepsFirst(): Expression<number>;
30
+ /**
31
+ * In-memory equivalent, for a caller that already holds the rows.
32
+ *
33
+ * Kept beside the SQL so the two cannot drift into disagreeing about what
34
+ * "setup" means.
35
+ */
36
+ export declare function compareStepsForDisplay(a: {
37
+ step_type?: string | null;
38
+ step_index: number;
39
+ }, b: {
40
+ step_type?: string | null;
41
+ step_index: number;
42
+ }): number;
43
+ //# sourceMappingURL=step-display-order.d.ts.map