@kici-dev/orchestrator 0.1.21 → 0.1.22

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 (44) hide show
  1. package/dist/agent/dispatcher.d.ts +44 -0
  2. package/dist/agent/host-roster-reaper.d.ts +2 -1
  3. package/dist/agent/host-roster.d.ts +18 -0
  4. package/dist/approvals/step-approval-bridge.d.ts +5 -0
  5. package/dist/cli/commands/source-manifest.d.ts +8 -0
  6. package/dist/cli/service/compose.d.ts +13 -0
  7. package/dist/cli/service/deploy-env.d.ts +31 -0
  8. package/dist/cli.js +515 -209
  9. package/dist/config.d.ts +6 -0
  10. package/dist/dashboard/needs-edges.d.ts +4 -3
  11. package/dist/db/migrations/047_needs_run_on.d.ts +4 -0
  12. package/dist/db/migrations/048_host_reboot_pending.d.ts +19 -0
  13. package/dist/db/migrations/049_held_runs_payload.d.ts +14 -0
  14. package/dist/db/migrations/050_sources_slug.d.ts +17 -0
  15. package/dist/db/types.d.ts +27 -4
  16. package/dist/deployment/deployment-identity.d.ts +9 -0
  17. package/dist/entry-helpers.d.ts +7 -0
  18. package/dist/environments/held-runs.d.ts +7 -1
  19. package/dist/github-app-name-refresher/github-app-name-refresher.d.ts +77 -0
  20. package/dist/index.js +21 -2
  21. package/dist/orchestrator-core.d.ts +13 -1
  22. package/dist/pipeline/decorating-secret-resolver.d.ts +32 -0
  23. package/dist/pipeline/dispatch-matched-workflow.d.ts +75 -3
  24. package/dist/pipeline/install-secrets-resolver.d.ts +2 -2
  25. package/dist/pipeline/needs-scheduler.d.ts +22 -13
  26. package/dist/pipeline/processor.d.ts +2 -2
  27. package/dist/pipeline/test-pipeline.d.ts +31 -59
  28. package/dist/providers/github/manifest.d.ts +25 -0
  29. package/dist/routes/admin-sources.d.ts +7 -0
  30. package/dist/secrets/secret-resolver.d.ts +16 -1
  31. package/dist/server.js +2728 -1725
  32. package/dist/sources/source-store.d.ts +4 -0
  33. package/dist/sources/source-validator.d.ts +2 -0
  34. package/dist/stale-detector/reboot-deadline-sweep.d.ts +29 -0
  35. package/dist/standalone.js +1307 -677
  36. package/dist/ws/agent-handler.d.ts +11 -6
  37. package/dist/ws/dashboard-fleet-handler.d.ts +33 -0
  38. package/dist/ws/dashboard-fleet-write-handler.d.ts +60 -0
  39. package/dist/ws/fleet-runs-on-all.d.ts +16 -0
  40. package/dist/ws/platform-client.d.ts +13 -1
  41. package/dist/ws/test-relay-handlers.d.ts +4 -2
  42. package/installer-image-digests.json +3 -3
  43. package/package.json +4 -4
  44. package/sbom.spdx.json +50 -50
package/dist/config.d.ts CHANGED
@@ -68,6 +68,7 @@ declare const configSchema: z.ZodObject<{
68
68
  staleDetectorScanIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
69
69
  staleDetectorThresholdMultiplier: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
70
70
  jobHeartbeatIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
71
+ githubAppNameRefreshIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
71
72
  secretKey: z.ZodOptional<z.ZodString>;
72
73
  secretKeyFile: z.ZodOptional<z.ZodString>;
73
74
  secretKeyOld: z.ZodOptional<z.ZodString>;
@@ -84,6 +85,8 @@ declare const configSchema: z.ZodObject<{
84
85
  agentTokenTtlMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
85
86
  rosterGraceMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
86
87
  rosterTtlMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
88
+ hostRebootDeadlineMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
89
+ orchestratorHostAgentId: z.ZodOptional<z.ZodString>;
87
90
  maxFanoutHosts: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
88
91
  eventRouterMaxChainDepth: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
89
92
  eventRouterRateLimitPerWorkflowPerMinute: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
@@ -197,11 +200,13 @@ export declare const envDef: import("@kici-dev/shared/env").DefineEnvResult<{
197
200
  staleDetectorScanIntervalMs: number;
198
201
  staleDetectorThresholdMultiplier: number;
199
202
  jobHeartbeatIntervalMs: number;
203
+ githubAppNameRefreshIntervalMs: number;
200
204
  pgCustomerSecrets: boolean;
201
205
  agentAuth: "token" | "none";
202
206
  agentTokenTtlMs: number;
203
207
  rosterGraceMs: number;
204
208
  rosterTtlMs: number;
209
+ hostRebootDeadlineMs: number;
205
210
  maxFanoutHosts: number;
206
211
  eventRouterMaxChainDepth: number;
207
212
  eventRouterRateLimitPerWorkflowPerMinute: number;
@@ -265,6 +270,7 @@ export declare const envDef: import("@kici-dev/shared/env").DefineEnvResult<{
265
270
  secretKeyOld?: string | undefined;
266
271
  secretKeyFileOld?: string | undefined;
267
272
  bootstrapAdminToken?: string | undefined;
273
+ orchestratorHostAgentId?: string | undefined;
268
274
  testEventFailFirstN?: string | undefined;
269
275
  otelExporterOtlpEndpoint?: string | undefined;
270
276
  clusterName?: string | undefined;
@@ -1,13 +1,14 @@
1
- import { IfFailedPolicy } from '@kici-dev/engine';
1
+ import type { ExecutionJobStatus } from '@kici-dev/engine';
2
2
  /** A single resolved upstream dependency edge for a job. */
3
3
  export interface JobNeedEdge {
4
4
  upstreamName: string;
5
- ifFailed: IfFailedPolicy;
5
+ /** Upstream terminal statuses that satisfy this edge (the run-on set). */
6
+ runOn: ExecutionJobStatus[];
6
7
  }
7
8
  /** Group raw execution_job_needs rows by downstream job_name. */
8
9
  export declare function groupNeedsByJobName(rows: ReadonlyArray<{
9
10
  job_name: string;
10
11
  upstream_name: string;
11
- if_failed: string;
12
+ run_on: string;
12
13
  }>): Map<string, JobNeedEdge[]>;
13
14
  //# sourceMappingURL=needs-edges.d.ts.map
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ export declare function up(db: Kysely<unknown>): Promise<void>;
3
+ export declare function down(db: Kysely<unknown>): Promise<void>;
4
+ //# sourceMappingURL=047_needs_run_on.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add `host_roster.reboot_pending_until timestamptz NULL` — the persisted
4
+ * reboot-pending flag for workflow-level host restart.
5
+ *
6
+ * Set when an agent's `restartHost()` step calls `host.requestReboot` (so it
7
+ * survives an orchestrator restart during the host's reboot window). While the
8
+ * value is in the future it (1) makes the agent's imminent disconnect an
9
+ * expected reboot rather than a recovery-fail, (2) gates the pinned-drain off
10
+ * so the post-restart job is not dispatched into the about-to-reboot box, and
11
+ * (3) clears on the next reconnect (down-then-up), releasing the held job. NULL
12
+ * = no reboot pending.
13
+ *
14
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
15
+ * preserved.
16
+ */
17
+ export declare function up(db: Kysely<unknown>): Promise<void>;
18
+ export declare function down(db: Kysely<unknown>): Promise<void>;
19
+ //# sourceMappingURL=048_host_reboot_pending.d.ts.map
@@ -0,0 +1,14 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add `held_runs.payload jsonb NULL` — the drift payload captured when a
4
+ * `when: 'drift'` step-approval gate fires. Holds `{ summaryMarkdown, drift }`:
5
+ * the author's `summarize(drift)` rendering plus the structured drift blob, so
6
+ * the dashboard approval queue and the CLI render the computed diff the
7
+ * operator approves. NULL for every non-drift hold.
8
+ *
9
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
10
+ * preserved.
11
+ */
12
+ export declare function up(db: Kysely<unknown>): Promise<void>;
13
+ export declare function down(db: Kysely<unknown>): Promise<void>;
14
+ //# sourceMappingURL=049_held_runs_payload.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add `sources.slug TEXT NULL` — the GitHub App slug (the URL-safe identifier
4
+ * GitHub assigns, e.g. `my-kici-app`).
5
+ *
6
+ * For GitHub-App sources GitHub is the source of truth for both the display
7
+ * `name` and the `slug`: both are captured at creation and kept fresh by the
8
+ * daily refresher + `kici-admin source refresh`. NULL when the identity fetch
9
+ * hasn't populated it yet (manual `--app-id` flow whose initial fetch failed,
10
+ * or a row created before the rollout).
11
+ *
12
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
13
+ * preserved.
14
+ */
15
+ export declare function up(db: Kysely<unknown>): Promise<void>;
16
+ export declare function down(db: Kysely<unknown>): Promise<void>;
17
+ //# sourceMappingURL=050_sources_slug.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import type { ColumnType, Generated, Insertable, Selectable, Updateable } from 'kysely';
2
- import type { ApprovalRequirement, ApproverClause, InitFailure } from '@kici-dev/engine';
2
+ import type { ApprovalRequirement, ApproverClause, InitFailure, StepApprovalPayload } from '@kici-dev/engine';
3
3
  /**
4
4
  * PostgreSQL-only database types.
5
5
  * Column names use snake_case matching the actual database column names.
@@ -486,8 +486,13 @@ export interface ExecutionJobNeedsTable {
486
486
  job_name: string;
487
487
  /** Upstream job name (the job that must complete first) */
488
488
  upstream_name: string;
489
- /** Per-edge failure policy: 'skip' (default) or 'run' */
490
- if_failed: Generated<string>;
489
+ /**
490
+ * Per-edge run-on status-set: a JSON-encoded array of upstream terminal
491
+ * statuses (ExecutionJobStatus[]) that satisfy the edge. Default
492
+ * `'["success"]'`. The downstream dispatches when the upstream's terminal
493
+ * status is a member of this set.
494
+ */
495
+ run_on: Generated<string>;
491
496
  }
492
497
  export type ExecutionJobNeeds = Selectable<ExecutionJobNeedsTable>;
493
498
  export type NewExecutionJobNeeds = Insertable<ExecutionJobNeedsTable>;
@@ -695,7 +700,7 @@ export interface HeldRunsTable {
695
700
  step_index: number | null;
696
701
  /**
697
702
  * What created the hold: 'environment' (mandatory env policy) | 'explicit'
698
- * (SDK `requireApproval`). Engine `TriggerSource`.
703
+ * (SDK `approval`). Engine `TriggerSource`.
699
704
  */
700
705
  trigger_source: Generated<string>;
701
706
  /**
@@ -703,6 +708,11 @@ export interface HeldRunsTable {
703
708
  * must satisfy. Null for legacy rows that predate the approval model.
704
709
  */
705
710
  approval_requirement: ColumnType<ApprovalRequirement | null, ApprovalRequirement | string | null | undefined, ApprovalRequirement | string | null>;
711
+ /**
712
+ * Drift payload `{ summaryMarkdown, drift }` captured when a `when: 'drift'`
713
+ * step-approval gate fires. Null for every non-drift hold.
714
+ */
715
+ payload: ColumnType<StepApprovalPayload | null, StepApprovalPayload | string | null | undefined, StepApprovalPayload | string | null>;
706
716
  }
707
717
  export type HeldRun = Selectable<HeldRunsTable>;
708
718
  export type NewHeldRun = Insertable<HeldRunsTable>;
@@ -1215,6 +1225,12 @@ export interface SourcesTable {
1215
1225
  routing_key: string;
1216
1226
  /** JSONB config (non-sensitive, e.g. { appId: '12345' }) */
1217
1227
  config: string;
1228
+ /**
1229
+ * GitHub App slug (the URL-safe identifier GitHub assigns, e.g.
1230
+ * `my-kici-app`). NULL until the GitHub identity fetch populates it. GitHub
1231
+ * is the source of truth for both `name` and `slug` on GitHub-App sources.
1232
+ */
1233
+ slug: string | null;
1218
1234
  /** Customer/org identifier for secret and environment scoping */
1219
1235
  customer_id: Generated<string>;
1220
1236
  /** When this source was created */
@@ -1616,6 +1632,13 @@ export interface HostRosterTable {
1616
1632
  */
1617
1633
  host_properties: ColumnType<Record<string, string | number | boolean>, Record<string, string | number | boolean> | string | undefined, Record<string, string | number | boolean> | string>;
1618
1634
  last_seen: ColumnType<Date, Date | string | undefined, Date | string>;
1635
+ /**
1636
+ * Reboot-pending flag for workflow-level host restart. When set to a future
1637
+ * timestamp, the host's imminent disconnect is an expected reboot (not a
1638
+ * recovery-fail) and its pinned post-restart job is held until the host
1639
+ * reconnects (down-then-up). NULL = no reboot pending.
1640
+ */
1641
+ reboot_pending_until: ColumnType<Date | null, Date | string | null, Date | string | null>;
1619
1642
  created_at: Generated<Date>;
1620
1643
  updated_at: ColumnType<Date, Date | string | undefined, Date | string>;
1621
1644
  }
@@ -0,0 +1,9 @@
1
+ import { type DeploymentIdentity } from '@kici-dev/engine';
2
+ /**
3
+ * Read the orchestrator's deployment shape from the env the installer injects
4
+ * (`KICI_DEPLOY_MODE` / `KICI_DEPLOY_CONTAINER` / `KICI_DEPLOY_CONTAINER_RUNTIME`).
5
+ * Hand-run / dev orchestrators carry no `KICI_DEPLOY_*` env and report `unknown`.
6
+ * Container fields are kept only for the `compose` mode.
7
+ */
8
+ export declare function readDeploymentIdentity(env?: NodeJS.ProcessEnv): DeploymentIdentity;
9
+ //# sourceMappingURL=deployment-identity.d.ts.map
@@ -19,6 +19,13 @@ export interface ProviderSource {
19
19
  routingKey: string;
20
20
  name: string;
21
21
  subtype: SourceSubtype;
22
+ /**
23
+ * GitHub App slug (URL-safe identifier GitHub assigns). Only set for
24
+ * GitHub-App sources, where it propagates orchestrator → Platform → dashboard
25
+ * alongside the display `name`. Undefined for generic / universal-git / local
26
+ * sources, and for a GitHub source whose identity fetch hasn't run yet.
27
+ */
28
+ slug?: string;
22
29
  }
23
30
  /**
24
31
  * Map a generic_webhook_sources `provider_type` (plus optional `git_config`
@@ -4,7 +4,7 @@
4
4
  * Manages the lifecycle: pending -> approved/rejected/expired.
5
5
  */
6
6
  import { type Kysely } from 'kysely';
7
- import { type ApprovalRequirement, type ApproverClause, ApprovalDecision, HoldScope, TriggerSource } from '@kici-dev/engine';
7
+ import { type ApprovalRequirement, type ApproverClause, type StepApprovalPayload, ApprovalDecision, HoldScope, TriggerSource } from '@kici-dev/engine';
8
8
  import type { Database, HeldRun, HeldRunApproval } from '../db/types.js';
9
9
  /** Status values for held runs (held_runs table). */
10
10
  export declare enum HeldRunStatus {
@@ -51,6 +51,12 @@ export interface CreateHoldData {
51
51
  * the automated release sweeps can find their rows.
52
52
  */
53
53
  holdType?: string;
54
+ /**
55
+ * Drift payload `{ summaryMarkdown, drift }` captured for a `when: 'drift'`
56
+ * step gate; persisted to `held_runs.payload` and surfaced in the dashboard
57
+ * approval queue + the CLI. Omit for non-drift holds.
58
+ */
59
+ payload?: StepApprovalPayload;
54
60
  }
55
61
  /** A single decision to record against a hold. */
56
62
  export interface RecordDecisionData {
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Daily refresh of every GitHub source's display name + slug from GitHub.
3
+ *
4
+ * GitHub is the source of truth for a GitHub-App source's display name and
5
+ * slug. They are captured at creation, but an operator can rename the App in
6
+ * the GitHub UI afterwards — this periodic task re-fetches `GET /app` for each
7
+ * GitHub source and, when the name or slug drifted, writes the new values to
8
+ * the `sources` row. The `sources_change` DB trigger then fans the change out
9
+ * (SourceManager reload → `platformClient.updateSources()` → re-register), so
10
+ * the Platform `webhook_sources` row and the dashboard Sources tab pick up the
11
+ * new name/slug without any extra plumbing here.
12
+ *
13
+ * Lifecycle mirrors `StaleRunDetector`: `start()` runs an immediate refresh
14
+ * (so a rename made while the orchestrator was down propagates on next boot)
15
+ * then a `setInterval`; `stop()` clears it. Per-source errors are logged and
16
+ * never abort the loop.
17
+ */
18
+ /** The slice of {@link SourceStore} this module needs. */
19
+ export interface RefreshableSourceStore {
20
+ listSources(): Promise<Array<{
21
+ routing_key: string;
22
+ provider: string;
23
+ name: string;
24
+ slug: string | null;
25
+ }>>;
26
+ getSourceWithSecrets(routingKey: string): Promise<{
27
+ provider: string;
28
+ config: string;
29
+ privateKey: string;
30
+ } | null>;
31
+ updateSource(routingKey: string, updates: {
32
+ name?: string;
33
+ slug?: string | null;
34
+ }): Promise<unknown>;
35
+ }
36
+ /** Fetch a GitHub App's authoritative identity. Matches `fetchGithubAppIdentity`. */
37
+ export type FetchGithubAppIdentity = (creds: {
38
+ appId: string;
39
+ privateKey: string;
40
+ }) => Promise<{
41
+ name: string;
42
+ slug: string;
43
+ }>;
44
+ /** Outcome of a single source refresh. */
45
+ export interface RefreshResult {
46
+ routingKey: string;
47
+ changed: boolean;
48
+ oldName: string;
49
+ newName: string;
50
+ oldSlug: string | null;
51
+ newSlug: string;
52
+ }
53
+ /**
54
+ * Re-fetch one GitHub source's identity from GitHub and persist it when the
55
+ * name or slug drifted. Shared by the daily task and `kici-admin source
56
+ * refresh`. Throws for a missing or non-GitHub routing key.
57
+ */
58
+ export declare function refreshGithubSourceIdentity(sourceStore: RefreshableSourceStore, routingKey: string, fetchIdentity: FetchGithubAppIdentity): Promise<RefreshResult>;
59
+ export interface GithubAppNameRefresherDeps {
60
+ sourceStore: RefreshableSourceStore;
61
+ fetchIdentity: FetchGithubAppIdentity;
62
+ /** Refresh cadence in ms. Default cluster value: 24h (`config.githubAppNameRefreshIntervalMs`). */
63
+ scanIntervalMs: number;
64
+ }
65
+ export declare class GithubAppNameRefresher {
66
+ private readonly sourceStore;
67
+ private readonly fetchIdentity;
68
+ private readonly scanIntervalMs;
69
+ private interval;
70
+ constructor(deps: GithubAppNameRefresherDeps);
71
+ /** Immediate refresh then periodic scans. */
72
+ start(): Promise<void>;
73
+ stop(): void;
74
+ /** Refresh every GitHub source once. Per-source failures are isolated. */
75
+ refresh(): Promise<void>;
76
+ }
77
+ //# sourceMappingURL=github-app-name-refresher.d.ts.map
package/dist/index.js CHANGED
@@ -1892,6 +1892,23 @@ function createInstallationOctokit(config, installationId) {
1892
1892
  * registration and returns it on the conversion response, so both GitHub and
1893
1893
  * the Platform end up sharing the same secret with zero operator effort.
1894
1894
  */
1895
+ /**
1896
+ * Validate a self-hosted webhook URL supplied via `source add github
1897
+ * --webhook-url`. Must be a well-formed absolute `https://` URL. Returns the URL
1898
+ * verbatim on success; throws a clear error otherwise. The validated URL is
1899
+ * baked into `manifest.hook_attributes.url` as-is — KiCI adds no ingress and
1900
+ * does not receive events at it; the operator owns delivery.
1901
+ */
1902
+ function validateWebhookUrl(value) {
1903
+ let url;
1904
+ try {
1905
+ url = new URL(value);
1906
+ } catch {
1907
+ throw new Error(`--webhook-url must be a valid absolute URL: ${value}`);
1908
+ }
1909
+ if (url.protocol !== "https:") throw new Error(`--webhook-url must be an https:// URL (got ${url.protocol}//…)`);
1910
+ return value;
1911
+ }
1895
1912
  function buildGithubAppManifest(input) {
1896
1913
  return {
1897
1914
  name: input.name,
@@ -1930,6 +1947,7 @@ async function convertManifestCode(code, deps = {}) {
1930
1947
  return {
1931
1948
  appId: String(d.id),
1932
1949
  slug: d.slug,
1950
+ name: d.name,
1933
1951
  privateKey: d.pem,
1934
1952
  webhookSecret: d.webhook_secret,
1935
1953
  clientId: d.client_id,
@@ -2178,7 +2196,8 @@ async function storeAndRegister(opts, client, deps, creds) {
2178
2196
  try {
2179
2197
  return await client.post("/api/v1/admin/sources", {
2180
2198
  provider: "github",
2181
- name: opts.name,
2199
+ name: creds.name,
2200
+ slug: creds.slug,
2182
2201
  appId: creds.appId,
2183
2202
  privateKey: creds.privateKey,
2184
2203
  webhookSecret: creds.webhookSecret
@@ -2195,7 +2214,7 @@ async function storeAndRegister(opts, client, deps, creds) {
2195
2214
  }
2196
2215
  }
2197
2216
  async function runGithubManifestSetup(opts, client, deps = realManifestSetupDeps) {
2198
- const webhookUrl = await resolveWebhookUrl(client);
2217
+ const webhookUrl = opts.webhookUrl ? validateWebhookUrl(opts.webhookUrl) : await resolveWebhookUrl(client);
2199
2218
  const state = randomBytes(16).toString("hex");
2200
2219
  const createUrl = manifestCreateUrl(opts.githubOrg);
2201
2220
  const loopback = await deps.startLoopback({
@@ -23,7 +23,7 @@ import { DedupCache } from './webhook/dedup.js';
23
23
  import { ObserverRegistry } from './ws/observer-registry.js';
24
24
  import { AgentMetricsAggregator } from './metrics/agent-metrics-aggregator.js';
25
25
  import { SourceLocationStore } from './app.js';
26
- import { type LabelMatcher, type PeerHeartbeat, type PeerLogsCollectRequest, type PeerToPeerMessage } from '@kici-dev/engine';
26
+ import { ExecutionJobStatus, type LabelMatcher, type PeerHeartbeat, type PeerLogsCollectRequest, type PeerToPeerMessage } from '@kici-dev/engine';
27
27
  import { ScalerManager } from './scaler/index.js';
28
28
  import type { ScalerConfig } from './scaler/index.js';
29
29
  import type { CacheStorage } from './storage/index.js';
@@ -253,6 +253,17 @@ export declare function buildUpstreamOutputsByBase(baseNames: string[], rows: Ar
253
253
  variant_label?: string | null;
254
254
  status?: string | null;
255
255
  }>): Record<string, Record<string, unknown>> | undefined;
256
+ /**
257
+ * Build the downstream `upstreamJobStatuses` map keyed by each upstream job
258
+ * row's name. A single non-fanned upstream is keyed by its base name; a
259
+ * fanned-out upstream contributes one entry per expanded child name (`base
260
+ * (child)`). The agent uses this to stamp `ctx.needs.<job>.status` (single) and
261
+ * the per-child status of group / matrix / host-fanout entries.
262
+ */
263
+ export declare function buildUpstreamStatusesByBase(rows: Array<{
264
+ job_name: string;
265
+ status?: string | null;
266
+ }>): Record<string, ExecutionJobStatus> | undefined;
256
267
  /**
257
268
  * Fold a `runsOnAll` upstream's host children into the `byHost` envelope
258
269
  * `{ byHost: { '<host>': outputs }, summary: { succeededHosts, failedHosts, outputs } }`.
@@ -302,6 +313,7 @@ export declare function buildMatrixOutputsEnvelope(baseName: string, children: A
302
313
  export declare function mergeUpstreamOutputs(db: Kysely<Database>, runId: string, jobName: string, needs: unknown, dispatchSecrets: Record<string, string> | undefined, secretKey: string): Promise<{
303
314
  mergedSecrets: Record<string, string> | undefined;
304
315
  upstreamJobOutputs: Record<string, Record<string, unknown>> | undefined;
316
+ upstreamJobStatuses: Record<string, ExecutionJobStatus> | undefined;
305
317
  }>;
306
318
  export declare function bootstrapOrchestrator(config: AppConfig, hooks: OrchestratorHooks, options?: {
307
319
  otelSdk?: {
@@ -0,0 +1,32 @@
1
+ /**
2
+ * CLI-secret overlay for `kici run` test dispatch.
3
+ *
4
+ * Wraps the orchestrator's environment `SecretResolver` and overlays the
5
+ * developer's CLI-uploaded local secrets on top of the env-resolved secrets,
6
+ * with CLI winning on collision. Passed to the shared dispatch core via
7
+ * `ProcessingDeps.secretResolver`, so the core's secret-resolution path is
8
+ * unchanged and oblivious to "test secrets".
9
+ *
10
+ * Precedence: env-resolved secrets → CLI context for the requested environment
11
+ * → CLI flat. The CLI flat overlay is applied last so a CLI flat key always
12
+ * wins.
13
+ */
14
+ import type { SecretResolverApi, ResolvedSecretMeta } from '../secrets/secret-resolver.js';
15
+ /** Decrypted CLI-uploaded local secrets: flat keys + per-context namespaces. */
16
+ export interface CliSecrets {
17
+ flat: Record<string, string>;
18
+ contexts: Record<string, Record<string, string>>;
19
+ }
20
+ export declare class DecoratingSecretResolver implements SecretResolverApi {
21
+ private readonly base;
22
+ private readonly cli;
23
+ constructor(base: SecretResolverApi, cli: CliSecrets);
24
+ resolveForJob(orgId: string, environmentName: string): Promise<Record<string, string>>;
25
+ resolveNamed(orgId: string, scope: string, key: string, opts?: {
26
+ store?: string;
27
+ runId?: string;
28
+ jobId?: string;
29
+ }): Promise<string | null>;
30
+ resolveForJobWithMeta(orgId: string, environmentName: string): Promise<Record<string, ResolvedSecretMeta>>;
31
+ }
32
+ //# sourceMappingURL=decorating-secret-resolver.d.ts.map
@@ -13,8 +13,8 @@
13
13
  * exported function is a narrative orchestrator that threads the typed
14
14
  * results through the pipeline.
15
15
  */
16
- import { CacheRefScope } from '@kici-dev/engine';
17
- import type { LabelMatcher, LockWorkflow, LockJob, SimulatedEvent, WorkflowDecision, MaterializedJob } from '@kici-dev/engine';
16
+ import { ExecutionJobStatus, InitFailureCategory, CacheRefScope } from '@kici-dev/engine';
17
+ import type { LabelMatcher, LockWorkflow, LockJob, HostTargetSelector, SimulatedEvent, WorkflowDecision, MaterializedJob, ResolvedHostAgent } from '@kici-dev/engine';
18
18
  import { type HostRosterStore } from '../agent/host-roster.js';
19
19
  import type { WebhookInfo } from '../webhook/handler.js';
20
20
  import type { ProviderBundle } from '../provider-registry.js';
@@ -41,7 +41,15 @@ export declare function deriveCacheRefScope(trust: TrustResolution | undefined):
41
41
  export interface WorkflowDispatchContext {
42
42
  info: WebhookInfo;
43
43
  deps: ProcessingDeps;
44
- bundle: ProviderBundle;
44
+ /**
45
+ * Provider bundle for the matched source. Undefined for local-repo test runs
46
+ * (`kici run` against an inline lock file with no remote provider): in that
47
+ * mode there is no clone-url builder / check-status poster / clone-token
48
+ * provider, and `repoUrl` falls back to `''` (the agent treats a missing url
49
+ * as a local/`fullRepo` clone). The webhook adapter always passes a defined
50
+ * bundle, so its dispatch behavior is unchanged.
51
+ */
52
+ bundle?: ProviderBundle;
45
53
  payload: unknown;
46
54
  repoIdentifier: string;
47
55
  credentials: Record<string, unknown>;
@@ -88,10 +96,36 @@ export interface WorkflowDispatchContext {
88
96
  * correct clone + logging.
89
97
  */
90
98
  extraJobConfig?: Record<string, unknown>;
99
+ /**
100
+ * Test-run provenance. Present only for `kici run` / test-trigger dispatches.
101
+ * When set, `recordRunStart` stamps `is_test_run = true` and
102
+ * `fixture_id = testRun.fixtureId` on the `execution_runs` row. Undefined for
103
+ * webhook runs (the stamp block is skipped).
104
+ */
105
+ testRun?: {
106
+ fixtureId: string;
107
+ };
108
+ /**
109
+ * Run-wide flat secrets layered onto EVERY dispatched job's `jobConfig.secrets`
110
+ * (env-declaring or not). Used by the test path to deliver `kici run --secret`
111
+ * / `--env` CLI flat secrets, which must reach a job regardless of whether it
112
+ * declares an `environment:`. Merged UNDER the per-job env-resolved secrets so
113
+ * the CLI value wins on a key collision (matching the prior B1-env -> A-CLI
114
+ * precedence). Undefined for webhook runs.
115
+ */
116
+ runWideFlatSecrets?: Record<string, string>;
117
+ /**
118
+ * Runtime host narrowing from `kici run --target` (Ansible `--limit`). Applied
119
+ * as a post-filter over each runsOnAll job's matched roster: effective hosts =
120
+ * runsOnAll ∩ target. Narrow-only. Undefined for webhook runs (no narrowing).
121
+ */
122
+ target?: HostTargetSelector;
91
123
  }
92
124
  export interface DispatchMatchedWorkflowResult {
93
125
  /** Number of jobs successfully dispatched (non-rejected). */
94
126
  dispatchedJobCount: number;
127
+ /** Execution job ids of every dispatched/tracked job (root, gated, synthetic). */
128
+ dispatchedJobIds: string[];
95
129
  /** True when the workflow install gate paused the dispatch (held run). */
96
130
  held?: boolean;
97
131
  }
@@ -110,6 +144,39 @@ export interface DispatchMatchedWorkflowOptions {
110
144
  */
111
145
  reuseRunId?: string;
112
146
  }
147
+ interface RejectedJob {
148
+ jobId: string;
149
+ jobName: string;
150
+ reason: string;
151
+ /** Explicit init-failure category override; inferred from reason when absent. */
152
+ category?: InitFailureCategory;
153
+ /**
154
+ * Terminal status to record for this job. Defaults to `failed`. A zeroed
155
+ * `runsOnAll` that intentionally narrowed to no hosts is recorded as `skipped`
156
+ * (no init-failure) so its downstreams' `when` sets govern propagation.
157
+ */
158
+ terminalStatus?: ExecutionJobStatus;
159
+ }
160
+ /**
161
+ * Phase B orchestrator: probe caches, dispatch the build job (if needed),
162
+ * and surface enough state for downstream phases to skip / continue
163
+ * appropriately.
164
+ */
165
+ /**
166
+ * Materialize each static job's matrix into dispatchable children. A job whose
167
+ * matrix is invalid (zero combinations / over the cap) is dropped from the
168
+ * dispatch list and recorded as a `matrix_expansion` matrix failure so the run's
169
+ * other jobs still proceed. Dynamic-matrix jobs pass through with a
170
+ * `pendingDynamicMatrix` marker for the eval flow.
171
+ */
172
+ /**
173
+ * Resolve a `runsOnAll` lock job against the declared host roster and partition
174
+ * the matched hosts into the target set per the `onUnreachable` policy (R2):
175
+ * `ready` hosts always run; unreachable durable (`static`) hosts hold / fail /
176
+ * skip; stale ephemeral hosts are always skipped. Throws {@link FanoutError}
177
+ * when the run can't proceed (fail policy with an absent host, or zero targets).
178
+ */
179
+ export declare function resolveHostFanoutTargets(lockJob: LockJob, deps: ProcessingDeps, target?: HostTargetSelector): Promise<ResolvedHostAgent[]>;
113
180
  /** Exact labels + regex patterns partitioned from a lock job's selectors. */
114
181
  interface JobRoutingSelectors {
115
182
  runsOnLabels: string[];
@@ -169,6 +236,11 @@ export interface WavePlan {
169
236
  * job (single child) or one without `maxParallel` contributes nothing.
170
237
  */
171
238
  export declare function computeWavePlan(materializedJobs: readonly MaterializedJob[]): WavePlan;
239
+ export declare function materializeStaticJobsSafe(staticJobs: readonly LockJob[], deps: ProcessingDeps, target?: HostTargetSelector): Promise<{
240
+ materializedJobs: MaterializedJob[];
241
+ expansionMap: Map<string, readonly string[]>;
242
+ matrixFailures: RejectedJob[];
243
+ }>;
172
244
  export interface GeneratedJobConfig {
173
245
  /**
174
246
  * The generated lock job with its `name` and `needs` rewritten to expanded
@@ -31,7 +31,7 @@
31
31
  */
32
32
  import type { ApproverClause, LockRegistry } from '@kici-dev/engine';
33
33
  import type { TrustResolution } from '../security/trust-resolver.js';
34
- import type { SecretResolver } from '../secrets/secret-resolver.js';
34
+ import type { SecretResolverApi } from '../secrets/secret-resolver.js';
35
35
  import type { EnvironmentStore } from '../environments/environment-store.js';
36
36
  import { type JobDispatchContext } from '../environments/protection/pipeline.js';
37
37
  /** Registry spec carried on the dispatch message (token already resolved). */
@@ -48,7 +48,7 @@ export interface ResolveInstallSecretsArgs {
48
48
  resolvedOrgId: string;
49
49
  trustResolution: TrustResolution | undefined;
50
50
  environmentStore: EnvironmentStore | undefined;
51
- secretResolver: SecretResolver | undefined;
51
+ secretResolver: SecretResolverApi | undefined;
52
52
  protectionContext: JobDispatchContext;
53
53
  /**
54
54
  * Resume path: skip the protection-rule gate (already satisfied) and resolve
@@ -1,24 +1,29 @@
1
1
  /**
2
- * DB-backed needs-aware dispatch scheduler ( through).
2
+ * DB-backed needs-aware dispatch scheduler.
3
3
  *
4
- * This module is the core of 's behavioral change: it replaces the
5
- * "concurrent dispatch" model with event-driven scheduling that gates ALL
6
- * needs edges (static-to-static, static-to-dyn-group, dyn-to-static, dyn-to-dyn).
4
+ * Gates ALL needs edges (static-to-static, static-to-dyn-group, dyn-to-static,
5
+ * dyn-to-dyn) with event-driven scheduling instead of concurrent dispatch.
7
6
  *
8
7
  * The scheduler is pure DB — no in-memory state. Every scheduling decision is
9
8
  * a fresh DB query against execution_jobs + execution_job_needs. This means
10
9
  * zero recovery code on orchestrator restart.
11
10
  *
11
+ * Each edge carries a `run_on` status-set (the upstream terminal statuses that
12
+ * satisfy the edge). A downstream edge is dispatch-satisfied when the upstream's
13
+ * terminal status is a member of the edge's run_on set; otherwise the downstream
14
+ * is skipped. A downstream dispatches only when every edge is satisfied.
15
+ *
12
16
  * Entry points:
13
17
  * - insertEdgesForRun: called at run start for static-to-static edges
14
18
  * - resolveGroupEdges: called on dynamic-eval completion
15
19
  * - evaluateDownstreams: called from onJobStatus(terminal)
16
20
  * - recomputeNeedsSatisfied: batch recompute after group resolution
17
- * - checkSchedulerInvariant: Layer 3 defensive check
21
+ * - checkSchedulerInvariant: defensive stuck-job check
18
22
  * - getFailurePropagationTargets: cascade for transitive skip
19
23
  */
20
24
  import type { Kysely } from 'kysely';
21
25
  import type { Database } from '../db/types.js';
26
+ import { ExecutionJobStatus } from '@kici-dev/engine';
22
27
  import type { MaterializedJob } from '@kici-dev/engine';
23
28
  /** Result of evaluating downstream jobs after an upstream completes. */
24
29
  export interface SchedulerResult {
@@ -48,21 +53,22 @@ export declare function insertEdgesForRun(db: Kysely<Database>, runId: string, j
48
53
  * edge row. Empty groups (0 members) trigger immediate needs_satisfied=true
49
54
  * for dependents.
50
55
  *
51
- * CRITICAL: dependentStaticJobs carries per-job ifFailed policy from the
56
+ * CRITICAL: dependentStaticJobs carries the per-job run_on status-set from the
52
57
  * NeedsGroupEntry in the lock file. Without this, all group edges would
53
- * silently default to 'skip'.
58
+ * silently default to success-only.
54
59
  */
55
60
  export declare function resolveGroupEdges(db: Kysely<Database>, runId: string, groupName: string, memberJobNames: string[], dependentStaticJobs: Array<{
56
61
  jobName: string;
57
- ifFailed: 'skip' | 'run';
62
+ runOn: ExecutionJobStatus[];
58
63
  }>): Promise<void>;
59
64
  /**
60
65
  * Evaluate downstream jobs after an upstream reaches terminal state.
61
66
  *
62
67
  * This is the core scheduler hook. For each downstream of the completed job:
63
- * 1. If upstream failed and edge has if_failed='skip', mark downstream as 'skip'
64
- * 2. Otherwise, check if ALL upstreams are terminal
65
- * 3. If all satisfied, mark needs_satisfied=true and return for dispatch
68
+ * 1. If the completed status is not in this edge's run_on set, mark the
69
+ * downstream as 'skip' immediately.
70
+ * 2. Otherwise, check if ALL upstreams are terminal and satisfied.
71
+ * 3. If all satisfied, mark needs_satisfied=true and return for dispatch.
66
72
  */
67
73
  export declare function evaluateDownstreams(db: Kysely<Database>, runId: string, completedJobName: string, completedStatus: string): Promise<SchedulerResult[]>;
68
74
  /**
@@ -80,8 +86,11 @@ export declare function recomputeNeedsSatisfied(db: Kysely<Database>, runId: str
80
86
  */
81
87
  export declare function checkSchedulerInvariant(db: Kysely<Database>, runId: string): Promise<string[]>;
82
88
  /**
83
- * cascade: find all transitive downstreams that should be skipped
84
- * due to failure propagation. Only follows edges where if_failed='skip'.
89
+ * Failure-propagation cascade: find all transitive downstreams that should be
90
+ * skipped because a terminal upstream's status is not in their edge's run_on
91
+ * set. At each hop, the propagating job's actual terminal status decides which
92
+ * downstream edges propagate (status not in run_on → the downstream skips and
93
+ * propagates further).
85
94
  */
86
95
  export declare function getFailurePropagationTargets(db: Kysely<Database>, runId: string, failedJobName: string): Promise<string[]>;
87
96
  //# sourceMappingURL=needs-scheduler.d.ts.map