@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
@@ -13,15 +13,17 @@
13
13
  * exported function is a narrative orchestrator that threads the typed
14
14
  * results through the pipeline.
15
15
  */
16
- import { ExecutionJobStatus, InitFailureCategory, CacheRefScope } from '@kici-dev/engine';
17
- import type { LabelMatcher, LockWorkflow, LockJob, HostTargetSelector, SimulatedEvent, WorkflowDecision, MaterializedJob, ResolvedHostAgent, HostFacts } from '@kici-dev/engine';
16
+ import { ExecutionJobStatus, InitFailureCategory, CacheRefScope, HoldScope, TriggerSource } from '@kici-dev/engine';
17
+ import type { LabelMatcher, LockWorkflow, LockJob, ApprovalRequirement, HostTargetSelector, SimulatedEvent, WorkflowDecision, MaterializedJob, ResolvedHostAgent, HostFacts } from '@kici-dev/engine';
18
18
  import { type HostRosterStore } from '../agent/host-roster.js';
19
+ import { gatherInvokeResults } from '../orchestrator-core.js';
19
20
  import type { WebhookInfo } from '../webhook/handler.js';
20
21
  import type { ProviderBundle } from '../provider-registry.js';
21
22
  import type { Dispatcher } from '../agent/dispatcher.js';
22
23
  import type { QueuedJobInput } from '../queue/job-queue.js';
23
24
  import type { TrustResolution } from '../security/trust-resolver.js';
24
25
  import type { TrustPolicyOutcome } from '../security/trust-policy-gate.js';
26
+ import type { CreateHeldRunData } from '../contexts/held-runs.js';
25
27
  import { type ProcessingDeps } from './processor.js';
26
28
  /**
27
29
  * Trusted refs (write+ contributor, default-branch) get the org-shared cache
@@ -55,6 +57,24 @@ export interface WorkflowDispatchContext {
55
57
  bundle?: ProviderBundle;
56
58
  payload: unknown;
57
59
  repoIdentifier: string;
60
+ /**
61
+ * The repository that DEFINES the workflow being dispatched. `repoIdentifier`
62
+ * is the repository the run acts on; for an organization-wide workflow the
63
+ * two are different repositories, and every run row this dispatch writes has
64
+ * to say which one defined it.
65
+ *
66
+ * REQUIRED, for the same reason `securityDecision` is: a dispatch path that
67
+ * does not state it must not compile. Left optional, a new caller omits it
68
+ * silently and every row it records claims the workflow lives in the
69
+ * repository the run acted on — a null marker is read as that fact, not as
70
+ * "unknown" (`registration/registration-run-match.ts`).
71
+ *
72
+ * Every caller today states `repoIdentifier` or a value equal to it, because
73
+ * no cross-repository global dispatch enters this function — the global path
74
+ * builds its job inputs directly and dispatches them itself. The recording
75
+ * sites narrow, so stating the acted-on repository records nothing.
76
+ */
77
+ workflowRepoIdentifier: string;
58
78
  credentials: Record<string, unknown>;
59
79
  event: SimulatedEvent;
60
80
  eventWithFiles: SimulatedEvent;
@@ -67,6 +87,21 @@ export interface WorkflowDispatchContext {
67
87
  };
68
88
  };
69
89
  resolvedOrgId: string;
90
+ /**
91
+ * Siblings admitted against a context concurrency limit during THIS dispatch
92
+ * pass, keyed by {@link concurrencyAdmissionKey}.
93
+ *
94
+ * The gate's running count counts jobs whose status is already `running`, so
95
+ * it cannot see anything this pass has only just admitted. Without this tally
96
+ * every child of a fan-out is evaluated against that one blind value and all
97
+ * N are admitted against a single slot.
98
+ *
99
+ * Created lazily on first use and dropped with the ctx. It is per-pass state,
100
+ * never persisted and never shared between passes; two concurrent passes
101
+ * still race, which is accepted — this is a throughput control, not an
102
+ * isolation boundary.
103
+ */
104
+ concurrencyAdmissions?: Map<string, number>;
70
105
  workflow: LockWorkflow;
71
106
  decision: WorkflowDecision;
72
107
  runId: string;
@@ -74,8 +109,93 @@ export interface WorkflowDispatchContext {
74
109
  lockFileSource: string | undefined;
75
110
  /** True when this run executes an uploaded local working tree (CLI remote run). */
76
111
  localWorkingTree: boolean;
112
+ /**
113
+ * Identity that initiated this run, for `execution_runs.triggered_by`.
114
+ *
115
+ * Set by the CLI remote-run path, which knows its caller: the Platform relays
116
+ * the developer's actor on `test.relay.trigger` and the test pipeline renders
117
+ * it here. Undefined on the webhook path, where the initiator is a provider
118
+ * account rather than a KiCI principal — that attribution is carried by
119
+ * `triggerActorUsername` / `triggerActorUserId` instead.
120
+ */
121
+ triggeredBy?: string | null;
122
+ /** Agent provenance label when the run was initiated through an agent credential. */
123
+ triggeredByAgentLabel?: string | null;
77
124
  /** True only when invoked from the cross-source dispatch shell. */
78
125
  crossSource: boolean;
126
+ /**
127
+ * Trigger-event string for the run's `triggerEvent`, stated explicitly rather
128
+ * than derived from `event.type`.
129
+ *
130
+ * The internal-event adapter needs this because a user `kiciEvent()` renders
131
+ * two different values: `jobConfig.event.type` is the literal `kici_event`,
132
+ * while the run carries the raw event name. `event.type` carries the former,
133
+ * so the latter has to be stated.
134
+ *
135
+ * Absent ⇒ the value derived from the event, unchanged for the webhook,
136
+ * CLI-remote-run and resume callers.
137
+ */
138
+ triggerEventOverride?: string;
139
+ /**
140
+ * The exact `jobConfig.event` envelope to ship, stated explicitly instead of
141
+ * spreading the `SimulatedEvent` this dispatch matched against.
142
+ *
143
+ * Same reason as `triggerEventOverride`: the matcher's event shape and the
144
+ * shape user code observes are not the same object. `SimulatedEvent` requires
145
+ * `targetBranch` and carries `changedFiles`; the envelope omits both. An
146
+ * internally triggered run genuinely has no changed files, and its branch is
147
+ * provenance the orchestrator evaluates (the trigger matcher and the context
148
+ * branch gate) rather than a field user code asked for — the envelope becomes
149
+ * `RuleContext.event` on the agent and the `event` half of
150
+ * `buildConcurrencyGroupContext`, so publishing the branch would silently
151
+ * re-key the documented `ctx.event.targetBranch ?? 'default'` concurrency
152
+ * group of every existing internal workflow.
153
+ *
154
+ * Absent ⇒ the envelope derived from the event, unchanged for the webhook,
155
+ * CLI-remote-run and resume callers.
156
+ */
157
+ eventEnvelopeOverride?: Record<string, unknown>;
158
+ /**
159
+ * Chain depth to stamp on the started run. A run summoned by an invoke gate
160
+ * carries its summoner's depth + 1, which is what bounds the chain-depth
161
+ * circuit breaker. Absent ⇒ 0, the column default (a webhook-triggered run
162
+ * starts a chain).
163
+ *
164
+ * Threading this is load-bearing: the breaker fails OPEN if the value is
165
+ * lost, so an unbounded summon recursion would go undetected. It is stamped
166
+ * as soon as the run row exists, because `releaseInvokeGate` reads the column
167
+ * back at gate-release time — which happens inside this same dispatch.
168
+ */
169
+ chainDepth?: number;
170
+ /**
171
+ * Marks a run dispatched by a failure-lifecycle trigger, so its own
172
+ * completion is excluded from batch accumulation and a broken notifier
173
+ * cannot re-trigger itself (`EventRouter.isFailureLifecycleRun`).
174
+ *
175
+ * Persisted as a field INSIDE the `trigger_decision` JSON blob, merged onto
176
+ * the decision summary rather than replacing it.
177
+ */
178
+ dispatchedByFailureLifecycle?: boolean;
179
+ /**
180
+ * Marks a run the orchestrator triggered itself — a schedule fire, a
181
+ * workflow/job completion, a failure batch, a user `kiciEvent()`, or an
182
+ * invoke-gate summon — as opposed to one a provider webhook triggered.
183
+ *
184
+ * Read by the context branch gate, and by nothing else. Such a run usually
185
+ * carries a real branch in `event.targetBranch` — a schedule fire presents
186
+ * its registration's default branch, every other internal trigger inherits
187
+ * the branch of the run that emitted its event — and the gate matches it like
188
+ * any other run's. The flag singles out the runs whose `targetBranch` is
189
+ * EMPTY: a failure batch or a scaler event (many runs behind it, or none), a
190
+ * registration whose default branch has never been captured, an emitting run
191
+ * that is gone. The gate rejects those naming that cause, instead of quoting
192
+ * an empty value as though it were a branch name. It does NOT weaken the
193
+ * gate — a run with no branch cannot satisfy a restriction, `*` included.
194
+ *
195
+ * Absent ⇒ webhook-triggered, unchanged for the webhook, CLI-remote-run and
196
+ * resume callers.
197
+ */
198
+ internallyTriggered?: boolean;
79
199
  /**
80
200
  * Outcome of the org trust-policy gate for this PR event. `pass` dispatches
81
201
  * normally; `hold` parks the run in the security queue; `reject` fails it
@@ -91,13 +211,35 @@ export interface WorkflowDispatchContext {
91
211
  */
92
212
  securityDecision: TrustPolicyOutcome;
93
213
  /**
94
- * Set when this dispatch call took a pending-jobs token for the source-pack
95
- * build window. Tokens are fungible, so the `finally` must release only one
96
- * it actually took an unpaired release would consume a token held by a
97
- * deferred init / dynamic task and un-hold the run while its jobs are still
98
- * being registered.
214
+ * Set when this dispatch call took a pending-jobs token covering the window
215
+ * between registering the run and registering its jobs the source-pack
216
+ * build window, or the plain dispatch window when there is no build. Tokens
217
+ * are fungible, so the `finally` must release only one it actually took an
218
+ * unpaired release would consume a token held by a deferred init / dynamic
219
+ * task and un-hold the run while its jobs are still being registered.
220
+ */
221
+ dispatchWindowTokenHeld?: boolean;
222
+ /**
223
+ * Set when this dispatch call inserted the `execution_runs` row before
224
+ * handing the first job to an agent. A row that exists with zero jobs can
225
+ * never complete (`isRunComplete` ends `run.jobs.size > 0`) and no sweeper
226
+ * reaps it, so a throw inside that window has to terminalize the run
227
+ * explicitly rather than leave it `pending` forever.
99
228
  */
100
- buildWindowTokenHeld?: boolean;
229
+ runRegisteredBeforeDispatch?: boolean;
230
+ /**
231
+ * Set once `setupDispatchContext` has posted this dispatch's queued
232
+ * `kici/<workflow>` check and one `kici/<workflow>/job/<name>` per static job.
233
+ *
234
+ * Those checks go up BEFORE anything decides whether the run will start, so
235
+ * every exit after setup owes them a conclusion. The named early exits each
236
+ * complete their own; a THROW does not, and nothing else can — the workflow
237
+ * check keys off a run whose jobs never registered, and the stale sweep only
238
+ * touches check runs already `in_progress`. Left alone they stay `queued`
239
+ * forever, which on a pull request is a check that never finishes and a
240
+ * branch-protection blocker.
241
+ */
242
+ pendingChecksPosted?: boolean;
101
243
  /** Composite dedup key `${info.deliveryId}:${reg.id}` (cross-source only). */
102
244
  crossSourceDeliveryId?: string;
103
245
  /**
@@ -154,6 +296,14 @@ export interface WorkflowDispatchContext {
154
296
  */
155
297
  dispatchInputs?: Record<string, unknown>;
156
298
  }
299
+ /**
300
+ * The minimal slice of a dispatch context the needs catch-up + ready-recompute
301
+ * helpers read. Both the per-repository `WorkflowDispatchContext` and the
302
+ * global-workflow dispatch path (which builds its own inputs and never
303
+ * constructs a full `WorkflowDispatchContext`) can drive the needs scheduler
304
+ * through this narrow shape.
305
+ */
306
+ export type NeedsSchedulingContext = Pick<WorkflowDispatchContext, 'deps' | 'runId'>;
157
307
  /**
158
308
  * Build the dispatch-envelope event, carrying the orchestrator's already-fetched
159
309
  * changed-files list + status from `eventWithFiles` as a fast-path (the agent
@@ -166,6 +316,14 @@ export interface DispatchMatchedWorkflowResult {
166
316
  dispatchedJobCount: number;
167
317
  /** Execution job ids of every dispatched/tracked job (root, gated, synthetic). */
168
318
  dispatchedJobIds: string[];
319
+ /**
320
+ * Jobs whose dispatch is deferred to the agent init round (a dynamic context
321
+ * or a deferred-init job) and therefore not yet in `dispatchedJobIds`. These
322
+ * still run — they are dispatched asynchronously by `startDeferredPhases` —
323
+ * so a caller must not treat a run with pending deferred work as "nothing
324
+ * dispatched". Absent/0 on the early-return paths.
325
+ */
326
+ deferredJobCount?: number;
169
327
  /** True when the workflow install gate paused the dispatch (held run). */
170
328
  held?: boolean;
171
329
  /**
@@ -182,8 +340,6 @@ export interface DispatchMatchedWorkflowOptions {
182
340
  * so secrets resolve directly and the dispatch flows into job dispatch.
183
341
  */
184
342
  skipInstallProtectionGate?: boolean;
185
- /** The released held-run id being resumed (for logging / correlation). */
186
- reuseHeldRunId?: string;
187
343
  /**
188
344
  * The run id whose `held` execution_runs row should be reused (flipped to
189
345
  * pending) instead of inserting a fresh row.
@@ -214,6 +370,120 @@ interface DispatchSetup {
214
370
  */
215
371
  checkMode: string | undefined;
216
372
  }
373
+ interface BuildPrepResult {
374
+ sourceTarUrl: string | undefined;
375
+ sourceTarHash: string | undefined;
376
+ depsUrl: string | undefined;
377
+ depsHash: string | undefined;
378
+ contentHash: string | undefined;
379
+ lockfileHash: string | undefined;
380
+ hasDynamicEntries: boolean;
381
+ dynamicEntries: ReadonlyArray<Extract<LockWorkflow['jobs'][number], {
382
+ _type: 'dynamic';
383
+ }>>;
384
+ staticJobs: readonly LockJob[];
385
+ /**
386
+ * Static jobs expanded into dispatchable children (matrix fan-out). Non-matrix
387
+ * jobs pass through 1:1. Every dispatch phase iterates this list, keying by
388
+ * `expandedName`. Dynamic-matrix jobs are flagged `pendingDynamicMatrix`.
389
+ */
390
+ materializedJobs: readonly MaterializedJob[];
391
+ /** baseName -> expanded child names; drives needs-edge expansion. */
392
+ expansionMap: ReadonlyMap<string, readonly string[]>;
393
+ /** Jobs whose matrix could not be materialized (cap / zero-combination). */
394
+ matrixFailures: readonly RejectedJob[];
395
+ targetPlatform: string;
396
+ targetArch: string;
397
+ buildJobId: string | undefined;
398
+ buildJobName: string | undefined;
399
+ buildJobLabels: string[] | undefined;
400
+ buildJobTrackedEarly: boolean;
401
+ /** True when the build failed but dynamic entries can still proceed. */
402
+ buildFailed: boolean;
403
+ /**
404
+ * True when the helper has fully short-circuited the dispatch (build failed
405
+ * and no dynamic entries to fall back on, or build job rejected). Caller
406
+ * MUST early-return with `dispatchedJobCount: 0`.
407
+ */
408
+ abort: boolean;
409
+ }
410
+ interface JobEnvData {
411
+ contextName?: string;
412
+ /** Configured env id matched for the first declared context name. */
413
+ contextId?: string;
414
+ /**
415
+ * Ordered bound-context names persisted on the job row (`(dynamic)`
416
+ * placeholder for elements unresolved at dispatch; overwritten with the
417
+ * agent-resolved list for dynamic contexts). Empty/undefined = no binding.
418
+ */
419
+ contextNames?: string[];
420
+ contextVars?: Record<string, string>;
421
+ jobEnv?: Record<string, string>;
422
+ jobSecrets?: Record<string, string>;
423
+ jobNamespacedSecrets?: Record<string, Record<string, string>>;
424
+ /**
425
+ * Registry credentials for this job's container image, resolved from the
426
+ * secret NAMES the lock carries. Lifted to a top-level dispatch field (and
427
+ * stripped from jobConfig) before the message reaches the agent.
428
+ */
429
+ containerRegistryAuth?: {
430
+ username: string;
431
+ password: string;
432
+ serveraddress: string;
433
+ };
434
+ held?: boolean;
435
+ /**
436
+ * Pending approval hold for this job, set when a context policy or
437
+ * explicit lock `approval` requires human sign-off. The dispatch loop turns
438
+ * this into a `held_runs` row + a stored pending job context so `release()`
439
+ * can re-dispatch after approval.
440
+ */
441
+ approvalHold?: PendingApprovalHold;
442
+ /**
443
+ * A non-reviewer hold (security / wait-timer / concurrency-queue) decided by a
444
+ * context's protection rules. Carried rather than written on the spot so
445
+ * `holdJobForApproval` can create the row and the job's resume path in ONE
446
+ * transaction — the reviewer branch uses `approvalHold` for the same purpose.
447
+ */
448
+ nonApprovalHold?: CreateHeldRunData;
449
+ rejected?: boolean;
450
+ rejectReason?: string;
451
+ pendingInit?: boolean;
452
+ /** Bound contexts skipped on a test/local run because they disallow local execution. */
453
+ skippedEnvs?: string[];
454
+ /**
455
+ * User-visible warning set whenever any bound context was unavailable for
456
+ * a test run (non-test or unconfigured) and skipped — surfaced on the CLI run
457
+ * output and the dashboard run view.
458
+ */
459
+ envWarning?: string;
460
+ }
461
+ /** A resolved approval requirement awaiting hold creation in the dispatch loop. */
462
+ interface PendingApprovalHold {
463
+ scope: HoldScope;
464
+ triggerSource: TriggerSource;
465
+ requirement: ApprovalRequirement;
466
+ contextId: string | null;
467
+ queueType: 'context' | 'security';
468
+ }
469
+ interface DeferredInitJob {
470
+ mat: MaterializedJob;
471
+ initJobInput: QueuedJobInput;
472
+ }
473
+ interface JobEnvEvalResult {
474
+ jobContextData: Map<string, JobEnvData>;
475
+ deferredInitJobs: DeferredInitJob[];
476
+ runContextName: string | undefined;
477
+ runContextId: string | undefined;
478
+ }
479
+ /**
480
+ * Synthetic job-id prefix every needs-gate site stamps on a job it holds back.
481
+ * The release path (`dispatchReadyJob` → `findSyntheticJobId` → `addJobsToRun`)
482
+ * keys on it, so it is load-bearing rather than cosmetic — which is what makes
483
+ * it a sound way to recover the gated set without threading a parallel list
484
+ * through both the single-orchestrator and cluster dispatch paths.
485
+ */
486
+ export declare const NEEDS_PENDING_JOB_ID_PREFIX = "needs-pending-";
217
487
  interface RejectedJob {
218
488
  jobId: string;
219
489
  jobName: string;
@@ -227,6 +497,32 @@ interface RejectedJob {
227
497
  */
228
498
  terminalStatus?: ExecutionJobStatus;
229
499
  }
500
+ /**
501
+ * Whether this dispatch must run a `__build__` job before its real jobs.
502
+ *
503
+ * Either cache missing is a reason to build. The two are keyed on different
504
+ * things — the source cache on the workflow source's contentHash, the dep cache
505
+ * on the lockfile hash — so they miss independently, and a dependency bump is
506
+ * exactly the case that leaves the source warm and the deps cold. Gating only on
507
+ * the source miss made that state permanent: no build job ran, so nothing ever
508
+ * uploaded the dep tarball, so every agent fell back to installing from the
509
+ * registry on every job — and an agent with no route to that registry (a
510
+ * cloud-hosted one-shot agent, an air-gapped runner) could not run the job at
511
+ * all. The `__build__` job already carries `buildSourceNeeded` /
512
+ * `buildDepsNeeded` separately and the agent already honors both, so a
513
+ * deps-only build was implemented and simply unreachable.
514
+ *
515
+ * A hash is required alongside its miss: with no hash there is no cache key to
516
+ * write, so building would produce an artifact nothing could ever look up.
517
+ */
518
+ export declare function buildIsNeeded(args: {
519
+ /** False when there is nothing to cache into (no bundle, no coordinator, …). */
520
+ cacheInfraAvailable: boolean;
521
+ sourceHit: boolean;
522
+ contentHash: string | undefined;
523
+ depHit: boolean;
524
+ lockfileHash: string | undefined;
525
+ }): boolean;
230
526
  /**
231
527
  * Build the QueuedJobInput for a synthetic `__bringup__` job: the orchestrator
232
528
  * dispatches one per declared-but-un-agented `includeUninitialized` child to an
@@ -278,6 +574,28 @@ export declare function runsOnSelectorsForLockJob(lockJob: {
278
574
  runsOn?: readonly LabelMatcher[];
279
575
  excludeLabels?: readonly LabelMatcher[];
280
576
  }): JobRoutingSelectors;
577
+ /**
578
+ * Runtime facts a job's own shape demands of the host that runs it.
579
+ *
580
+ * Only a `container.dockerfile` job gets one. Building shells out to a
581
+ * `docker` / `podman` CLI, so a host without one cannot run the job at all, and
582
+ * the agent self-reports `kici:runtime:container-build` when it has one.
583
+ *
584
+ * A job that names a finalized `image` deliberately gets NOTHING added. Adding
585
+ * an implicit requirement to jobs that already work is how container jobs were
586
+ * stranded once before: they had been running fine, and a routing gate the
587
+ * orchestrator could not actually evaluate made them match nothing. A dockerfile
588
+ * job is new, so requiring the fact strands no existing workflow — and an agent
589
+ * old enough not to report the fact is an agent that cannot build anyway.
590
+ *
591
+ * Applied when matching REGISTERED agents (the dispatcher), and deliberately
592
+ * NOT when consulting the scaler. A scaler backend is chosen by exact label-set
593
+ * containment, so a required label the operator never wrote in a pool's label
594
+ * set matches no backend at all — the job would be stranded `queued-no-backend`
595
+ * rather than spawned. The pool's hosts are the operator's to describe; what an
596
+ * agent can actually do is known only once it registers and says so.
597
+ */
598
+ export declare function requiredRuntimeLabelsFor(container: unknown): string[];
281
599
  /**
282
600
  * Resolve a generated job's single bare-`agentId` `runsOn` into a host pin.
283
601
  *
@@ -345,6 +663,187 @@ export declare function findInvalidApprovalTimeout(workflow: LockWorkflow): {
345
663
  * no host facts to scope by.
346
664
  */
347
665
  export declare function hostCtxFromMat(mat: MaterializedJob): HostFacts | undefined;
666
+ /**
667
+ * Key for the in-pass admission tally.
668
+ *
669
+ * The org id is part of the key so the tally inherits the cross-tenant scoping
670
+ * the running-count query already enforces on `execution_runs.customer_id`: a
671
+ * context name shared across tenants must not leak concurrency between them.
672
+ * One dispatch pass carries a single `resolvedOrgId`, so the org component is a
673
+ * standing invariant rather than a live discriminator — it is here so a future
674
+ * pass dispatching for more than one org cannot silently merge two orgs'
675
+ * tallies. `JSON.stringify` over the pair is used rather than string
676
+ * concatenation so no separator character can make two different pairs collide.
677
+ */
678
+ export declare function concurrencyAdmissionKey(orgId: string, concurrencyGroup: string): string;
679
+ /**
680
+ * Phase D — evaluate static jobs' context data, queue deferred-init jobs
681
+ * for jobs with dynamic fields, and pick the first `runContextName` for
682
+ * the run.
683
+ */
684
+ export declare function evaluateJobContexts(args: {
685
+ ctx: WorkflowDispatchContext;
686
+ setup: DispatchSetup;
687
+ buildPrep: BuildPrepResult;
688
+ }): Promise<JobEnvEvalResult>;
689
+ /**
690
+ * Refuse a Dockerfile build on an untrusted ref, unless the org opted in.
691
+ *
692
+ * A job may build its container image from a Dockerfile in the repository. That
693
+ * build runs arbitrary `RUN` commands on the agent host's daemon, OUTSIDE the
694
+ * hardened posture the job's own steps get — `docker build` cannot be
695
+ * capability-restricted the way a container run can. So an untrusted ref (a fork
696
+ * PR, an unresolved contributor, or an internally-triggered run without a
697
+ * trusted emitter — the same classification the user-cache write scope uses)
698
+ * reaches it only where the operator said so.
699
+ *
700
+ * "Without a trusted emitter" covers both halves of the internal case: a
701
+ * `kiciEvent()` subscriber that inherited a `known` / `unknown` tier, and one
702
+ * that inherited nothing at all (no emitting run, no persisted tier, a lookup
703
+ * that failed) — the strict fallback, which is not an "untrusted emitter".
704
+ *
705
+ * Enforced here, at dispatch, and nowhere else: the agent applies only what
706
+ * dispatch authorized, exactly as it does for the sandbox capability grant. Deny
707
+ * is loud and total — the build never starts.
708
+ */
709
+ export declare function resolveWorkflowDockerfileBuilds(workflow: LockWorkflow, opts: {
710
+ scope: CacheRefScope;
711
+ allowUntrusted: boolean;
712
+ }): {
713
+ allowed: true;
714
+ } | {
715
+ denied: {
716
+ reason: string;
717
+ };
718
+ };
719
+ /**
720
+ * Post the pending `KiCI Security` status, and record on every hold that gates
721
+ * this commit's check that the commit now carries it.
722
+ *
723
+ * The record is what a settle reads to decide whether a hold has a check to
724
+ * terminalize. Deriving that from the row's shape instead answers what the code
725
+ * INTENDED, and `postCheckStatus` CREATES the named run when it finds none — so
726
+ * a post the provider refused left a shape saying "posted" and a settle that
727
+ * put a completed `KiCI Security` run on a commit which never had one.
728
+ *
729
+ * `heldRunIds` is plural because the commit carries ONE check run: a job held on
730
+ * two independent requirements has both of them gating it, and marking only the
731
+ * one whose summary was rendered would let the first to end resolve a check the
732
+ * other is still gating.
733
+ *
734
+ * Awaited, where the post used to be fire-and-forget: the record can only be
735
+ * written once the provider has accepted, and a record racing the settle is a
736
+ * record the settle may not see. A failed post is still swallowed — the dispatch
737
+ * loop is never blocked by a provider error — but the round-trip is now
738
+ * serial and inside the per-job loop, so N held jobs cost N of them.
739
+ *
740
+ * On success and then a failed record the commit keeps a pending check the
741
+ * settle will decline to close. That is the residue the fire-and-forget post
742
+ * could already leave, now narrowed to whatever can fail between the accepted
743
+ * post and ONE statement. The record is therefore retried
744
+ * {@link PENDING_CHECK_MARK_ATTEMPTS} times before it is given up on, which
745
+ * closes the half of that window the doc above names as reachable without a
746
+ * process dying — a lost connection, a statement timeout, a deadlock. What is
747
+ * left is a process death inside the retry window, and nothing in reach closes
748
+ * that: a sweeper would have to ask the provider whether the check exists, and
749
+ * `CheckStatusPoster` has no read method. Adding one means a new method on a
750
+ * compat-protected engine interface, every implementation and every hand-built
751
+ * bundle, to recover a window measured in milliseconds — against a recovery
752
+ * that already exists, since pushing a new commit re-posts. A sweeper without
753
+ * that read could only guess, and a wrong guess FABRICATES a check, which is
754
+ * the worse direction either way: a fabricated failing check on a pull request
755
+ * is worse than a stuck one.
756
+ *
757
+ * The record is one statement over every id, never one per id. A partial mark
758
+ * would leave an unmarked hold uncounted by the contention query, so the first
759
+ * hold to end would terminalize the shared check while the other still gates
760
+ * the job — a fabricated PASSING check, the worse direction, and reachable
761
+ * without any process dying. See `markPendingCheckPosted`.
762
+ */
763
+ /**
764
+ * How many times {@link postPendingHoldCheck} tries to record an accepted post
765
+ * on the hold rows before giving up and leaving the check unclosable.
766
+ */
767
+ export declare const PENDING_CHECK_MARK_ATTEMPTS = 3;
768
+ /**
769
+ * Open the needs gate for any job whose upstreams already reached terminal
770
+ * before this run's edges existed.
771
+ *
772
+ * **The race this closes.** Root jobs are dispatched to agents inside the
773
+ * dispatch loop, but `execution_job_needs` is only written afterwards, here. An
774
+ * agent that reports a root job terminal in that window drives
775
+ * `evaluateDownstreams`, which reads zero edges, returns an empty result, and
776
+ * the gate never fires again — the downstream stays `pending` forever and the
777
+ * run hangs with no error anywhere. Nothing re-evaluates on its own: the
778
+ * scheduler is purely event-driven off job completion, and that event has
779
+ * already been consumed.
780
+ *
781
+ * The window is small (single-digit milliseconds) but entirely reachable: a job
782
+ * that fails immediately on arrival — a rejected dispatch, an init failure, a
783
+ * capability mismatch — reports terminal in about the time one DB write takes.
784
+ * It was invisible for as long as the webhook route answered only after the
785
+ * whole pipeline had run, because the caller could not learn a job had been
786
+ * dispatched until every edge was already committed.
787
+ *
788
+ * Recomputing here is the same guard the deferred result-aware eval registration
789
+ * already applies for its own edges, and it is safe to run unconditionally: the
790
+ * claim inside `recomputeNeedsSatisfied` is a conditional UPDATE, so a job the
791
+ * normal completion path already claimed is skipped rather than dispatched
792
+ * twice.
793
+ *
794
+ * Wave-held jobs share the synthetic-id prefix but are gated by the rolling-wave
795
+ * scheduler, not by needs, so they are excluded — opening their gate here would
796
+ * bypass the `maxParallel` window.
797
+ */
798
+ export declare function catchUpNeedsGatedJobs(args: {
799
+ ctx: NeedsSchedulingContext;
800
+ dispatchedJobs: readonly {
801
+ jobId: string;
802
+ jobName: string;
803
+ waveGated?: boolean;
804
+ }[];
805
+ }): Promise<void>;
806
+ /** Why an init result must not lead to a dispatch. */
807
+ export declare enum InitDispatchSuppression {
808
+ /** The workflow's own `filter` decided the workflow does not apply. */
809
+ Filter = "filter",
810
+ /** The job is already rejected by a context rule, or held for approval. */
811
+ Gated = "gated"
812
+ }
813
+ /**
814
+ * Decide whether an arrived init result may dispatch its job.
815
+ *
816
+ * `Filter` requires the workflow to actually declare a filter as well as the
817
+ * agent to have reported `false`: a buggy or rogue agent must not be able to
818
+ * suppress a filter-less workflow by inventing the field, and an agent that
819
+ * predates the filter reports no verdict at all — reading that absence as
820
+ * "suppress" would silently stop every dispatch it handles.
821
+ *
822
+ * `Gated` covers a job that is already rejected or held. Its two halves now have
823
+ * very different reachability, and saying so is the point of this paragraph:
824
+ *
825
+ * - **rejected** is live and load-bearing. The flow-back consumes it (that is
826
+ * the `suppression === Gated && jobEnvData.rejected` branch) to stop a job its
827
+ * context rules rejected from dispatching.
828
+ * - **held** is NOT consumed here any more. A held job deliberately DOES get an
829
+ * init job — nothing else can resolve a dynamic value, so suppressing the
830
+ * round would make the job undispatchable rather than merely gated. The
831
+ * flow-back handles it after resolution instead, routing it to
832
+ * `holdExecutionAfterInit` so the hold and its resolved dispatch context are
833
+ * stored together.
834
+ *
835
+ * So do not read this as a guarantee that a held job cannot dispatch: that
836
+ * guarantee lives at the call site's `jobEnvData.held` branch, and — for the
837
+ * needs-scheduler and cluster paths, which reach a job by other routes — in
838
+ * `dispatchReadyJob`'s pending-hold check and the cluster path's own
839
+ * `holdJobForApproval` call.
840
+ *
841
+ * Exported for its own test: inline, the second branch could not be exercised at
842
+ * all, and an untestable security check is one nobody can prove still works.
843
+ */
844
+ export declare function initDispatchSuppression(workflow: Pick<LockWorkflow, 'hasFilter'>, initResult: {
845
+ filterPassed?: boolean;
846
+ }, jobEnvData: Pick<JobEnvData, 'rejected' | 'held'>): InitDispatchSuppression | null;
348
847
  export interface GeneratedJobConfig {
349
848
  /**
350
849
  * The generated lock job with its `name` and `needs` rewritten to expanded
@@ -377,6 +876,18 @@ export declare function partitionGeneratedConfigsByPin(configs: readonly Generat
377
876
  pinnedConfigs: GeneratedJobConfig[];
378
877
  unpinnedConfigs: GeneratedJobConfig[];
379
878
  };
879
+ /**
880
+ * Recompute the needs gate for `jobNames` and act on whatever became ready:
881
+ * open the gate for a satisfied job, terminalize a job an upstream's status
882
+ * excluded.
883
+ *
884
+ * `recomputeNeedsSatisfied` claims each job with a conditional
885
+ * `needs_satisfied = false → true` UPDATE, so a job a concurrent
886
+ * `evaluateDownstreams` already claimed is not returned here and cannot be
887
+ * dispatched twice.
888
+ */
889
+ export declare function recomputeAndApplyReady(ctx: NeedsSchedulingContext, jobNames: readonly string[]): Promise<void>;
890
+ export { gatherInvokeResults };
380
891
  /**
381
892
  * Dispatch a single matched workflow.
382
893
  *
@@ -393,5 +904,4 @@ export declare function partitionGeneratedConfigsByPin(configs: readonly Generat
393
904
  * J. deferred dynamic dispatch (fire-and-forget per dynamic entry)
394
905
  */
395
906
  export declare function dispatchMatchedWorkflow(ctx: WorkflowDispatchContext, opts?: DispatchMatchedWorkflowOptions): Promise<DispatchMatchedWorkflowResult>;
396
- export {};
397
907
  //# sourceMappingURL=dispatch-matched-workflow.d.ts.map