@kici-dev/orchestrator 0.4.0 → 0.5.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 (67) hide show
  1. package/dist/__test-helpers__/mock-db.d.ts +4 -0
  2. package/dist/agent/agent-version.d.ts +34 -0
  3. package/dist/agent/dispatcher.d.ts +70 -0
  4. package/dist/app.d.ts +23 -1
  5. package/dist/cache/global-eval-round-cache.d.ts +88 -0
  6. package/dist/cache/index.d.ts +3 -0
  7. package/dist/cache/pending-global-evals.d.ts +42 -0
  8. package/dist/cache/pending-inits.d.ts +10 -0
  9. package/dist/cli/commands/cluster-settings.d.ts +41 -3
  10. package/dist/cli/commands/runs.d.ts +1 -0
  11. package/dist/cli.js +1205 -620
  12. package/dist/cluster/cluster-settings-reader.d.ts +53 -1
  13. package/dist/config.d.ts +27 -0
  14. package/dist/content-requirements-cache.d.ts +55 -0
  15. package/dist/db/migrations/109_cluster_settings_cache_knobs.d.ts +4 -0
  16. package/dist/db/migrations/110_cluster_settings_global_eval_knobs.d.ts +4 -0
  17. package/dist/db/migrations/111_cluster_settings_global_eval_wait.d.ts +4 -0
  18. package/dist/db/migrations/112_execution_runs_workflow_repo.d.ts +4 -0
  19. package/dist/db/migrations/113_execution_runs_workflow_repo_index.d.ts +30 -0
  20. package/dist/db/migrations/114_ingest_queue_claim.d.ts +4 -0
  21. package/dist/db/migrations/115_global_workflows_cluster_switch.d.ts +9 -0
  22. package/dist/db/types.d.ts +60 -2
  23. package/dist/metrics/agent-metrics-aggregator.d.ts +2 -2
  24. package/dist/metrics/prometheus.d.ts +59 -0
  25. package/dist/orchestrator-core.d.ts +12 -1
  26. package/dist/pipeline/content-filter.d.ts +71 -0
  27. package/dist/pipeline/dispatch-matched-workflow.d.ts +247 -8
  28. package/dist/pipeline/global-eval-round.d.ts +293 -0
  29. package/dist/pipeline/job-contexts.d.ts +16 -17
  30. package/dist/pipeline/process-webhook.d.ts +7 -0
  31. package/dist/pipeline/processor.d.ts +56 -2
  32. package/dist/pipeline/route-or-dispatch-jobs.d.ts +6 -0
  33. package/dist/pipeline/test-pipeline.d.ts +12 -0
  34. package/dist/pipeline/webhook-payload-store.d.ts +20 -0
  35. package/dist/provenance/backfill-run.d.ts +10 -1
  36. package/dist/provider-registry.d.ts +38 -3
  37. package/dist/providers/github/check-status-poster.d.ts +22 -3
  38. package/dist/providers/github/commit-message.d.ts +20 -0
  39. package/dist/providers/github/file-contents.d.ts +40 -0
  40. package/dist/providers/github/index.d.ts +2 -0
  41. package/dist/providers/universal-git/config.d.ts +2 -0
  42. package/dist/providers/universal-git/normalizer.d.ts +10 -0
  43. package/dist/queue/cleanup.d.ts +7 -1
  44. package/dist/queue/job-queue.d.ts +69 -6
  45. package/dist/queue/terminalize-unroutable.d.ts +13 -0
  46. package/dist/registration/registration-run-match.d.ts +47 -0
  47. package/dist/reporting/check-run-reporter.d.ts +52 -1
  48. package/dist/reporting/execution-tracker.d.ts +117 -7
  49. package/dist/reporting/log-chunk-sink.d.ts +8 -5
  50. package/dist/routes/admin-org-settings.d.ts +5 -0
  51. package/dist/routes/admin.d.ts +6 -0
  52. package/dist/scaler/manager.d.ts +10 -8
  53. package/dist/security/global-workflow-policy.d.ts +52 -12
  54. package/dist/server.js +27553 -23779
  55. package/dist/standalone.js +5840 -2207
  56. package/dist/webhook/ingest-accept.d.ts +70 -0
  57. package/dist/webhook/ingest-overflow-buffer.d.ts +35 -4
  58. package/dist/webhook/ingest-overflow-replayer.d.ts +50 -6
  59. package/dist/ws/agent-handler.d.ts +3 -0
  60. package/dist/ws/dashboard-global-workflows-handler.d.ts +30 -9
  61. package/dist/ws/execution-status-frame.d.ts +32 -0
  62. package/dist/ws/platform-client.d.ts +14 -0
  63. package/dist/ws/test-relay-handlers.d.ts +35 -10
  64. package/installer-image-digests.json +3 -3
  65. package/package.json +4 -4
  66. package/sbom.spdx.json +57 -52
  67. package/dist/pipeline/inline-eval.d.ts +0 -44
@@ -30,10 +30,17 @@ import { type ProcessingDeps } from './processor.js';
30
30
  * ingress route return `{ duplicate: true }` to GitHub's Recent Deliveries
31
31
  * panel; `skipped` covers unknown provider / unknown event / no-repo paths;
32
32
  * `processed` means the pipeline matched and dispatched (or recorded a run).
33
+ *
34
+ * `queued` means the delivery is durably stored and its pipeline will run after
35
+ * the response — the direct-ingress accept path's success outcome. It maps to
36
+ * the same 202 `processed` always did, because from the sender's side both mean
37
+ * "accepted, nothing more to do"; the distinction exists so the accept path can
38
+ * be asserted on directly rather than through the HTTP status.
33
39
  */
34
40
  export declare const WebhookIngestOutcome: z.ZodEnum<{
35
41
  duplicate: "duplicate";
36
42
  processed: "processed";
43
+ queued: "queued";
37
44
  shed: "shed";
38
45
  skipped: "skipped";
39
46
  }>;
@@ -18,6 +18,7 @@ import type { WebhookInfo } from '../webhook/handler.js';
18
18
  import type { DedupCache } from '../webhook/dedup.js';
19
19
  import type { ProviderRegistry, ProviderBundle } from '../provider-registry.js';
20
20
  import type { LockFileCache } from '../lockfile-cache.js';
21
+ import type { ContentRequirementsCache } from '../content-requirements-cache.js';
21
22
  import type { Dispatcher } from '../agent/dispatcher.js';
22
23
  import type { PlatformClient } from '../ws/platform-client.js';
23
24
  import type { QueuedJobInput } from '../queue/job-queue.js';
@@ -27,6 +28,8 @@ import type { DepCache } from '../cache/index.js';
27
28
  import type { PendingBuildTracker } from '../cache/index.js';
28
29
  import type { PendingInitTracker } from '../cache/pending-inits.js';
29
30
  import type { PendingDynamicTracker } from '../cache/pending-dynamics.js';
31
+ import type { PendingGlobalEvalTracker } from '../cache/pending-global-evals.js';
32
+ import type { GlobalEvalRoundCache } from '../cache/global-eval-round-cache.js';
30
33
  import type { CheckRunReporter } from '../reporting/check-run-reporter.js';
31
34
  import type { ExecutionTracker } from '../reporting/execution-tracker.js';
32
35
  import type { AgentRegistry } from '../agent/registry.js';
@@ -35,6 +38,7 @@ import type { RunCoordinator } from '../cluster/coordinator.js';
35
38
  import type { ClusterSettingsReader } from '../cluster/cluster-settings-reader.js';
36
39
  import type { TeamMembershipLookup } from '../approvals/team-membership-lookup.js';
37
40
  import type { LogStorage } from '../reporting/log-storage.js';
41
+ import type { LogWriter } from '../reporting/log-writer.js';
38
42
  import type { SecretResolverApi } from '../secrets/secret-resolver.js';
39
43
  import type { ContributorCache } from '../security/contributor-cache.js';
40
44
  import type { AccessLogWriter } from '../audit/access-log.js';
@@ -228,8 +232,12 @@ export declare function buildTriggerEvent(event: string, action: string | null |
228
232
  */
229
233
  export declare function extractInboundRepoIdentifier(payload: unknown): string | null;
230
234
  /**
231
- * Extract the first line of the commit message from a webhook payload.
232
- * Handles push (head_commit.message) and PR (pull_request.title) events.
235
+ * Extract the first line of the commit message from a webhook payload, for run
236
+ * display. Handles push (head_commit.message), PR (pull_request.title) and
237
+ * issue_comment (issue.title) events.
238
+ *
239
+ * The Tier-0 `commitMessage` trigger filter deliberately reads a DIFFERENT text
240
+ * (the full message, and PR title + body) — see `githubFilterText`.
233
241
  */
234
242
  export declare function extractCommitMessage(event: string, payload: unknown): string | undefined;
235
243
  /**
@@ -267,7 +275,31 @@ export declare function summarizeApprovalClauses(clauses: ReadonlyArray<{
267
275
  export interface ProcessingDeps {
268
276
  dedup: DedupCache;
269
277
  providerRegistry: ProviderRegistry;
278
+ /**
279
+ * Re-register the provider bundle for a generic routing key from server
280
+ * truth, returning true when a bundle is now present.
281
+ *
282
+ * The registry is an in-memory CACHE of `generic_webhook_sources`, populated
283
+ * by three independent paths (startup enumeration, the admin write handler,
284
+ * and the LISTEN/NOTIFY drain). None of them can guarantee the entry is
285
+ * present for a delivery that arrives at an arbitrary moment, and a miss is
286
+ * not benign: `getByRoutingKey` used to substitute an unrelated `generic:`
287
+ * bundle whose normalizer reports "this payload has no repository", so the
288
+ * delivery was discarded with nothing above `debug` to say why.
289
+ *
290
+ * Optional — hand-built test deps and wirings with no generic-source manager
291
+ * keep the previous behaviour (a miss stays a miss, reported loudly).
292
+ */
293
+ ensureProviderBundle?: (routingKey: string) => Promise<boolean>;
270
294
  lockFileCache: LockFileCache;
295
+ /**
296
+ * Cache for the Tier-1 `requires` static content filter (source-file bytes at
297
+ * a ref, keyed by (repo, sha, path)). Optional so hand-built test deps and
298
+ * independent deployments that never use `requires` keep working; when absent,
299
+ * a candidate carrying `requires` is dropped fail-visible rather than
300
+ * dispatched unfiltered (see content-filter.ts).
301
+ */
302
+ contentRequirementsCache?: ContentRequirementsCache;
271
303
  dispatcher: Dispatcher;
272
304
  /** Null/undefined in Independent mode. send() buffers when disconnected. */
273
305
  platformClient?: PlatformClient;
@@ -285,6 +317,21 @@ export interface ProcessingDeps {
285
317
  pendingInits?: PendingInitTracker;
286
318
  /** Pending dynamic tracker -- waits for agents to evaluate DynamicJobFn and return generated LockJob[]. */
287
319
  pendingDynamics?: PendingDynamicTracker;
320
+ /** Pending global-eval tracker -- waits for the pre-run round that decides which global workflows apply. */
321
+ pendingGlobalEvals?: PendingGlobalEvalTracker;
322
+ /** Round-result cache for the pre-run global eval round. Optional -- if not set, every round re-runs. */
323
+ globalEvalCache?: GlobalEvalRoundCache;
324
+ /** Cluster default for the whole-round budget handed to the eval agent (ms).
325
+ * The live per-cluster override is `cluster_settings.global_eval_round_timeout_ms`. */
326
+ globalEvalRoundTimeoutMs?: number;
327
+ /** Cluster default for the per-candidate budget handed to the eval agent (ms).
328
+ * The live per-cluster override is `cluster_settings.global_eval_candidate_timeout_ms`. */
329
+ globalEvalCandidateTimeoutMs?: number;
330
+ /** Cluster default for the orchestrator's own ceiling on awaiting a round (ms).
331
+ * Unlike the two budgets above, this one is enforced here rather than by the
332
+ * agent, so it also bounds a round no agent ever picked up. The live
333
+ * per-cluster override is `cluster_settings.global_eval_wait_timeout_ms`. */
334
+ globalEvalWaitTimeoutMs?: number;
288
335
  /** Commit status reporter for setting pending/success/failure/error on commits. Optional. */
289
336
  checkRunReporter?: CheckRunReporter;
290
337
  /** Execution tracker for DB persistence. Optional -- if not set, execution tracking is skipped. */
@@ -321,6 +368,13 @@ export interface ProcessingDeps {
321
368
  secretKey?: string;
322
369
  /** Log storage backend for persisting webhook payloads. Optional -- if not set, payload storage is skipped. */
323
370
  logStorage?: LogStorage;
371
+ /**
372
+ * Durable step-log writer. Optional -- when present, the deferred-init path
373
+ * uses it to surface a post-init env warning on the run's log stream so a
374
+ * blocking `kici run remote` test run prints it (the accept response has
375
+ * already been returned by the time the init round resolves).
376
+ */
377
+ logWriter?: Pick<LogWriter, 'appendChunk' | 'drain'>;
324
378
  /** Context store for looking up deployment contexts. Optional -- if not set, context features are inactive. */
325
379
  contextStore?: ContextStore;
326
380
  /** Variable store for resolving context variables. Optional -- if not set, context vars are not merged. */
@@ -19,6 +19,12 @@ export interface DispatchedJobEntry {
19
19
  jobName: string;
20
20
  matrixValues?: Record<string, unknown>;
21
21
  runsOnLabels?: string[];
22
+ /**
23
+ * The unexpanded job name a materialized child came from. Persisted to
24
+ * `execution_jobs.base_job_name`, which is the key the rolling-wave scheduler
25
+ * groups a wave's children by — a NULL there makes the wave gate bail.
26
+ */
27
+ baseJobName?: string;
22
28
  }
23
29
  export interface RejectedJobEntry {
24
30
  jobId: string;
@@ -22,6 +22,7 @@
22
22
  * - direct workflow execution (bypass trigger matching) is supported.
23
23
  */
24
24
  import { type ProcessingDeps } from './processor.js';
25
+ import { type ActorPrincipal } from '@kici-dev/engine';
25
26
  import type { CheckMode, HostTargetSelector } from '@kici-dev/engine';
26
27
  /**
27
28
  * Input for a test trigger request.
@@ -58,6 +59,17 @@ export interface TestTriggerInput {
58
59
  };
59
60
  /** Request trace ID from the HTTP request. */
60
61
  requestId: string;
62
+ /**
63
+ * The principal that initiated this run, relayed by the Platform on
64
+ * `test.relay.trigger` (where the wire schema has always required it).
65
+ *
66
+ * Required rather than optional: the relay handler is the only production
67
+ * caller, so there is no path that legitimately lacks an actor, and an
68
+ * optional field would let a future caller silently drop attribution — which
69
+ * is exactly how `execution_runs.triggered_by` stayed NULL for every remote
70
+ * test run while the column claimed to hold the initiator.
71
+ */
72
+ actor: ActorPrincipal;
61
73
  /** JSON-stringified lock file content for local repos with no remote. */
62
74
  inlineLockFile?: string;
63
75
  /** When true, repo has no remote -- skip provider lookup, skip clone. */
@@ -0,0 +1,20 @@
1
+ import type { LogStorage } from '../reporting/log-storage.js';
2
+ /**
3
+ * Object-storage key holding the webhook payload for a run. Shared by the
4
+ * writers and by the re-run path that copies a payload forward, so the layout
5
+ * is stated once.
6
+ */
7
+ export declare function webhookPayloadPath(runId: string): string;
8
+ /**
9
+ * Store a run's triggering webhook payload, best-effort.
10
+ *
11
+ * A failure is logged and swallowed: the payload is a debugging aid, and losing
12
+ * it must never cost the run that was about to execute. A no-op when the
13
+ * orchestrator has no object storage configured.
14
+ */
15
+ export declare function storeWebhookPayload(args: {
16
+ logStorage: LogStorage | undefined;
17
+ runId: string;
18
+ payload: unknown;
19
+ }): Promise<void>;
20
+ //# sourceMappingURL=webhook-payload-store.d.ts.map
@@ -6,6 +6,12 @@ export interface BackfillRunRow {
6
6
  status: string;
7
7
  routing_key: string | null;
8
8
  repo_identifier: string | null;
9
+ /**
10
+ * The repository that DEFINES the workflow, recorded only when it differs
11
+ * from `repo_identifier`. A cross-repository global run backfilled without it
12
+ * lands in the Platform mirror as an ordinary per-repository run.
13
+ */
14
+ workflow_repo_identifier: string | null;
9
15
  provider: string | null;
10
16
  local_working_tree: boolean | null;
11
17
  sha: string | null;
@@ -33,7 +39,10 @@ export interface BackfillRunDeps {
33
39
  }
34
40
  /**
35
41
  * Send one `execution.status` (terminal) followed by one `job.status.forward`
36
- * per job, populating exactly the fields the Platform's `handler.ts` upserts.
42
+ * per job, populating the run and job fields the Platform's `handler.ts` upserts
43
+ * that the local rows can answer for (the trigger metadata a live run carries in
44
+ * memory — trigger event, commit message, re-run lineage — has no column here
45
+ * and is left to the live path).
37
46
  * Ordered execution.status FIRST so the run row exists before the job rows
38
47
  * reference it. A no-op (throws) when the run is not found locally — the caller
39
48
  * leaves the pending row `deferred` with a clear error.
@@ -12,7 +12,7 @@
12
12
  * webhook normalization, lock file fetching, changed files retrieval,
13
13
  * clone token creation, and URL building.
14
14
  */
15
- import type { WebhookNormalizer, LockFileFetcher, ChangedFilesFetcher, CloneTokenProvider, RepoUrlBuilder, ContributorResolver, CheckStatusPoster, ProviderType } from '@kici-dev/engine';
15
+ import type { WebhookNormalizer, LockFileFetcher, FileContentsFetcher, ChangedFilesFetcher, CloneTokenProvider, RepoUrlBuilder, ContributorResolver, CheckStatusPoster, ProviderType } from '@kici-dev/engine';
16
16
  /**
17
17
  * Complete set of provider capabilities.
18
18
  *
@@ -24,6 +24,23 @@ import type { WebhookNormalizer, LockFileFetcher, ChangedFilesFetcher, CloneToke
24
24
  export interface ProviderBundle {
25
25
  normalizer: WebhookNormalizer;
26
26
  lockFileFetcher?: LockFileFetcher;
27
+ /**
28
+ * Fetches arbitrary file contents at a ref (content-match triggers). Unlike
29
+ * the other fetchers, a GitHub instance is scoped to one installation, so the
30
+ * per-delivery installation id must be known to construct it -- it is wired
31
+ * where that id is in scope, not in the source-level bundle build. Providers
32
+ * that can prebuild one (no per-delivery credential) may set this directly;
33
+ * providers scoped per installation supply {@link fileContentsFetcherFactory}
34
+ * instead.
35
+ */
36
+ fileContentsFetcher?: FileContentsFetcher;
37
+ /**
38
+ * Builds a per-delivery {@link FileContentsFetcher} from the event
39
+ * credentials (e.g. a GitHub installation id). Returns undefined when the
40
+ * credentials do not carry what the provider needs. The webhook pipeline
41
+ * calls this once per delivery, where the credentials are already resolved.
42
+ */
43
+ fileContentsFetcherFactory?: (credentials: Record<string, unknown>) => FileContentsFetcher | undefined;
27
44
  changedFilesFetcher?: ChangedFilesFetcher;
28
45
  cloneTokenProvider?: CloneTokenProvider;
29
46
  repoUrlBuilder?: RepoUrlBuilder;
@@ -79,12 +96,30 @@ export declare class ProviderRegistry {
79
96
  * prefix. For multi-app, use getByRoutingKey() instead.
80
97
  */
81
98
  get(type: ProviderType): ProviderBundle | undefined;
99
+ /**
100
+ * Whether a bundle is registered at EXACTLY this routing key.
101
+ *
102
+ * `getByRoutingKey` cannot answer this: it falls back to a provider-type
103
+ * lookup, so it returns a bundle for a key it has never seen. A caller that
104
+ * needs to know whether the source's OWN bundle is present — rather than
105
+ * whether some bundle can be produced — has to ask here.
106
+ */
107
+ hasExact(routingKey: string): boolean;
82
108
  /**
83
109
  * Get the provider bundle by routing key.
84
110
  * Routing keys have the format "{provider}:{id}" (e.g., "github:12345").
85
111
  *
86
- * Falls back to get(providerType) if exact key is not found,
87
- * for backward compatibility with single-app registration.
112
+ * Falls back to get(providerType) if the exact key is not found, for
113
+ * backward compatibility with single-app registration.
114
+ *
115
+ * The fallback is deliberately narrower for a `generic:` key. Such a key is
116
+ * fully qualified (`generic:{orgId}:{sourceId}`), so the type-prefix scan in
117
+ * `get()` cannot be a "the single configured app" shortcut the way it is for
118
+ * `github:` — it returns whichever `generic:`-prefixed bundle happens to sit
119
+ * first in insertion order, which may belong to a different source, or to a
120
+ * different ORGANIZATION. Only the shared default bundle (`generic:default`,
121
+ * the one that genuinely stands in for every plain generic source) is an
122
+ * acceptable stand-in, so that is the only fallback offered here.
88
123
  */
89
124
  getByRoutingKey(routingKey: string): ProviderBundle | undefined;
90
125
  /**
@@ -5,11 +5,13 @@
5
5
  * - Security holds (pending): "KiCI Security" — "Held for approval"
6
6
  * - Workflow modifications (neutral): "KiCI: Workflow changes"
7
7
  * - Org globals skipped by the trust policy (neutral): "KiCI: Organization workflows"
8
+ * - Org global evaluation failed (failure): "KiCI: Organization workflow evaluation"
8
9
  * - Approved runs (success) / Rejected/expired runs (failure)
9
10
  *
10
- * `postWorkflowModificationCheck` and `postGlobalWorkflowsSkippedCheck` are
11
- * interface-backed (the `CheckStatusPoster` contract in `@kici-dev/engine`) and
12
- * post on their own check names so neither overwrites the security-hold check.
11
+ * `postWorkflowModificationCheck`, `postGlobalWorkflowsSkippedCheck`, and
12
+ * `postGlobalEvalFailedCheck` are interface-backed (the `CheckStatusPoster`
13
+ * contract in `@kici-dev/engine`) and post on their own check names so none of
14
+ * them overwrites the security-hold check.
13
15
  *
14
16
  * Reuses the Octokit infrastructure from auth.ts via createInstallationOctokit.
15
17
  * Uses a fixed check name per category so that subsequent updates (approve/reject)
@@ -47,5 +49,22 @@ export declare class GitHubCheckStatusPoster implements CheckStatusPoster {
47
49
  * while the run is still held. Always posted as neutral/completed.
48
50
  */
49
51
  postGlobalWorkflowsSkippedCheck(repoIdentifier: string, commitSha: string, summary: string, credentials: unknown): Promise<void>;
52
+ /**
53
+ * Post the check recording that the pre-run evaluation of the organization's
54
+ * global workflows failed, so none of the workflows it was deciding on ran.
55
+ *
56
+ * Its own check name ("KiCI: Organization workflow evaluation") for the same
57
+ * reason as the notice above: the security-hold check is a single named run
58
+ * per commit which a hold posts as pending and approve / reject later
59
+ * complete, so writing this through `postCheckStatus` would resolve a
60
+ * still-held run's check and unblock a branch protection rule that requires
61
+ * it.
62
+ *
63
+ * Posted as a failure, not a neutral: work the organization asked for did not
64
+ * run, and the reader has to be able to tell that from a clean commit. The
65
+ * name is new, so no existing branch protection rule requires it — a rule
66
+ * only requires check names it was configured with.
67
+ */
68
+ postGlobalEvalFailedCheck(repoIdentifier: string, commitSha: string, summary: string, credentials: unknown): Promise<void>;
50
69
  }
51
70
  //# sourceMappingURL=check-status-poster.d.ts.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Where the commit message lives in a GitHub webhook payload, and the two
3
+ * different readings of it.
4
+ *
5
+ * They differ deliberately. `githubFilterText` feeds the Tier-0 `commitMessage`
6
+ * trigger filter, so it must be the WHOLE message — truncating to the subject
7
+ * line would make a `[skip ci]` marker written in the body invisible to a filter
8
+ * that names it. `githubDisplayMessage` feeds run-display metadata, where a
9
+ * one-line summary is the point.
10
+ */
11
+ /** Full text a `commitMessage` trigger filter is tested against; undefined when the payload carries none. */
12
+ export declare function githubFilterText(event: string, payload: unknown): string | undefined;
13
+ /**
14
+ * One-line message for run display: the subject (first line) of a push's
15
+ * head-commit message, the PR title, or the issue title. This is the reading
16
+ * run-display metadata uses, distinct from the full text `githubFilterText`
17
+ * feeds the Tier-0 filter.
18
+ */
19
+ export declare function githubDisplayMessage(event: string, payload: unknown): string | undefined;
20
+ //# sourceMappingURL=commit-message.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * GitHub file contents fetcher.
3
+ *
4
+ * Implements the FileContentsFetcher interface from @kici-dev/engine for GitHub.
5
+ * Fetches an arbitrary file's contents from a GitHub repository at a specific
6
+ * ref via the Contents API, so the orchestrator can evaluate content-match
7
+ * triggers without cloning the repo.
8
+ */
9
+ import type { FileContentsFetcher } from '@kici-dev/engine';
10
+ import { type GitHubAppConfig } from './auth.js';
11
+ /**
12
+ * GitHub-specific implementation of FileContentsFetcher.
13
+ *
14
+ * The installation id is baked in at construction because the
15
+ * FileContentsFetcher interface is credential-less at call time -- a fetcher
16
+ * instance is scoped to one GitHub App installation.
17
+ */
18
+ export declare class GitHubFileContentsFetcher implements FileContentsFetcher {
19
+ private readonly config;
20
+ private readonly installationId;
21
+ readonly provider: 'github';
22
+ constructor(config: GitHubAppConfig, installationId: number);
23
+ /**
24
+ * Fetch a file's contents from a GitHub repository at a specific ref.
25
+ *
26
+ * @param owner - Repository owner (e.g. "my-org")
27
+ * @param repo - Repository name (e.g. "my-app")
28
+ * @param path - Repo-relative file path (e.g. ".kici/workflows/ci.ts")
29
+ * @param ref - Git ref (branch, tag, or SHA)
30
+ * @returns `{ present: false }` on 404 / directory; `{ present: true, bytes }`
31
+ * when inline content is returned; `{ present: true }` (no bytes)
32
+ * when the file exists but GitHub omits inline content (files over
33
+ * 1 MiB).
34
+ */
35
+ getFileContents(owner: string, repo: string, path: string, ref: string): Promise<{
36
+ present: boolean;
37
+ bytes?: string;
38
+ }>;
39
+ }
40
+ //# sourceMappingURL=file-contents.d.ts.map
@@ -4,6 +4,7 @@
4
4
  * Each class implements a corresponding interface from @kici-dev/engine:
5
5
  * - GitHubWebhookNormalizer -> WebhookNormalizer
6
6
  * - GitHubLockFileFetcher -> LockFileFetcher
7
+ * - GitHubFileContentsFetcher -> FileContentsFetcher
7
8
  * - GitHubChangedFilesFetcher -> ChangedFilesFetcher
8
9
  * - GitHubCloneTokenProvider -> CloneTokenProvider
9
10
  * - GitHubRepoUrlBuilder -> RepoUrlBuilder
@@ -11,6 +12,7 @@
11
12
  */
12
13
  export { GitHubWebhookNormalizer } from './normalizer.js';
13
14
  export { GitHubLockFileFetcher } from './lock-file.js';
15
+ export { GitHubFileContentsFetcher } from './file-contents.js';
14
16
  export { GitHubChangedFilesFetcher } from './changed-files.js';
15
17
  export { GitHubCloneTokenProvider, createInstallationOctokit } from './auth.js';
16
18
  export type { GitHubAppConfig, GitHubCredentials } from './auth.js';
@@ -72,6 +72,7 @@ export declare const UniversalGitPayloadPathsSchema: z.ZodObject<{
72
72
  commitsAdded: z.ZodString;
73
73
  commitsModified: z.ZodString;
74
74
  commitsRemoved: z.ZodString;
75
+ commitMessage: z.ZodOptional<z.ZodString>;
75
76
  }, z.core.$strip>;
76
77
  export type UniversalGitPayloadPaths = z.infer<typeof UniversalGitPayloadPathsSchema>;
77
78
  /**
@@ -129,6 +130,7 @@ export declare const UniversalGitConfigSchema: z.ZodObject<{
129
130
  commitsAdded: z.ZodString;
130
131
  commitsModified: z.ZodString;
131
132
  commitsRemoved: z.ZodString;
133
+ commitMessage: z.ZodOptional<z.ZodString>;
132
134
  }, z.core.$strip>>;
133
135
  eventMapping: z.ZodOptional<z.ZodObject<{
134
136
  push: z.ZodArray<z.ZodString>;
@@ -84,6 +84,16 @@ export declare class UniversalGitWebhookNormalizer implements WebhookNormalizer
84
84
  * is safe to ship ahead of the interface change in Phase 3.
85
85
  */
86
86
  extractDefaultBranch(payload: unknown): string | null;
87
+ /**
88
+ * Text a `commitMessage` trigger filter is tested against.
89
+ *
90
+ * Push/tag reads the configured JSONPath, because forges genuinely differ on
91
+ * where the head commit lives. The PR read is STRUCTURAL rather than
92
+ * configured, matching how this normalizer already resolves base/head refs —
93
+ * `pull_request` on Gitea-family forges, `object_attributes` on GitLab, whose
94
+ * body field is spelled `description`.
95
+ */
96
+ private extractCommitMessage;
87
97
  /** Classify the raw event header against the source's eventMapping. */
88
98
  private classifyEvent;
89
99
  }
@@ -6,7 +6,7 @@ import type { CheckRunTrackingStore } from '../reporting/check-run-tracking-stor
6
6
  import type { ClusterSettingsReader } from '../cluster/cluster-settings-reader.js';
7
7
  import type { LogStorage } from '../reporting/log-storage.js';
8
8
  import { JobQueue } from './job-queue.js';
9
- import { type CanRouteLabels } from './terminalize-unroutable.js';
9
+ import { type CanRouteLabels, type TerminalizeDeps } from './terminalize-unroutable.js';
10
10
  /**
11
11
  * Optional cleanup dependencies + retention knobs. Present in platform/hybrid
12
12
  * mode (the only wiring site, `orchestrator-core`, always supplies them); the
@@ -67,6 +67,12 @@ export interface CleanupExtras {
67
67
  * safety: the job is terminal either way and the run fails either way.
68
68
  */
69
69
  canRouteLabels?: CanRouteLabels;
70
+ /**
71
+ * The Tier-2 global-eval tracker, forwarded to
72
+ * {@link terminalizeUnroutableJob} so a round job this sweep settles also
73
+ * settles the webhook request awaiting its verdict.
74
+ */
75
+ pendingGlobalEvals?: TerminalizeDeps['pendingGlobalEvals'];
70
76
  }
71
77
  /**
72
78
  * Run a single cleanup pass: remove expired dedup_cache entries and
@@ -239,18 +239,59 @@ export declare class JobQueue {
239
239
  * @returns The matching job, or null if none found.
240
240
  */
241
241
  dequeueForLabels(agentLabels: string[], agentMandatoryLabels?: string[], agentId?: string): Promise<QueuedJob | null>;
242
+ /**
243
+ * The column writes that constitute a claim. Identical to what
244
+ * {@link markDispatched} sets, so a claim and the caller's follow-up
245
+ * markDispatched are the same transition applied twice rather than two
246
+ * different half-transitions — and a row is never observable as Dispatched
247
+ * with no owner.
248
+ *
249
+ * `agentId` is optional only because `dequeueForLabels` accepts it optionally;
250
+ * every production caller supplies it.
251
+ */
252
+ private claimTransition;
253
+ /**
254
+ * Conditionally claim one row by id: flip Pending -> Dispatched, returning
255
+ * whether this caller won. The `status = Pending` guard is the arbiter — a
256
+ * loser updates zero rows and must treat that as "someone else took it",
257
+ * never as an error.
258
+ *
259
+ * Used by every claim path that has to run a JS post-filter before claiming
260
+ * (the regex matchers), since that filter runs after the SELECT's
261
+ * per-statement lock window has already closed.
262
+ */
263
+ private claimRowById;
242
264
  /**
243
265
  * Build the shared drain WHERE chain (status / expiry / exact-label @> /
244
266
  * exclude-label / pin / mandatory-label gate) common to both drain passes.
245
267
  * The pattern columns are NOT filtered here — each pass adds its own
246
268
  * pattern-free / pattern-bearing guard on top.
269
+ *
270
+ * No projection is attached: the pattern-free pass selects `id` alone (it
271
+ * embeds this as the sub-select of its claiming UPDATE), while the pattern
272
+ * pass selects every column so it can run the JS matcher post-filter.
247
273
  */
248
274
  private drainBaseQuery;
249
275
  /**
250
276
  * Fast path: claim the oldest pending pattern-free row. The
251
277
  * `runs_on_patterns = '[]' AND exclude_patterns = '[]'` guard restricts this
252
- * pass to rows that need no JS post-filter, so the single-row atomic claim
253
- * (FOR UPDATE SKIP LOCKED) keeps the original hot-path semantics intact.
278
+ * pass to rows that need no JS post-filter, which is what lets the whole
279
+ * claim be ONE statement.
280
+ *
281
+ * Selecting a row and transitioning it in two statements is not a claim.
282
+ * Outside an explicit transaction the `FOR UPDATE` lock lives only for the
283
+ * duration of its own SELECT, so a second agent arriving between the SELECT
284
+ * and the UPDATE reads the row still Pending, skips nothing, and dispatches
285
+ * the same job — which is one job executing twice on two agents, side effects
286
+ * and all.
287
+ *
288
+ * So the sub-select is embedded in the claiming UPDATE: its `FOR UPDATE SKIP
289
+ * LOCKED` row lock is now taken inside the UPDATE's own transaction and held
290
+ * until commit. That buys both halves at once — exactly one claimant can win
291
+ * a given row, and a concurrent claimant SKIPs the locked row and takes the
292
+ * *next* one instead of coming back empty-handed, which a select-then-claim
293
+ * retry loop would not preserve. The redundant outer `status = Pending` is
294
+ * belt-and-braces on the arbiter.
254
295
  */
255
296
  private claimPatternFree;
256
297
  /**
@@ -263,6 +304,9 @@ export declare class JobQueue {
263
304
  * Pending` makes exactly one of them win. The claim transitions the row to
264
305
  * Dispatched, matching the value the caller-side markDispatched would set
265
306
  * (which then re-sets it idempotently).
307
+ *
308
+ * Losing the claim continues to the next candidate rather than returning
309
+ * null, so a lost race costs this agent a candidate and not a whole drain.
266
310
  */
267
311
  private claimWithPatterns;
268
312
  /**
@@ -276,6 +320,13 @@ export declare class JobQueue {
276
320
  * `runsOn`/`exclude` patterns no longer match the agent's current labels must
277
321
  * not be claimed. The single matching authority is the engine's
278
322
  * `matcherSatisfiedBy` (never a Postgres `~`).
323
+ *
324
+ * The claim is the conditional UPDATE, not the SELECT: the JS post-filter has
325
+ * to run first (claiming and then releasing a pattern-rejected row would
326
+ * strand it as Dispatched), which puts the filter outside the SELECT's
327
+ * per-statement lock window. Losing that claim returns null, and
328
+ * `onAgentAvailable` then falls through to the generic label drain — which
329
+ * also matches jobs pinned to this agent — so a lost race is not a stall.
279
330
  */
280
331
  dequeueByPinnedAgent(agentId: string, agentLabels?: string[]): Promise<QueuedJob | null>;
281
332
  /**
@@ -292,10 +343,22 @@ export declare class JobQueue {
292
343
  * spawned the agent and was reassigned to a different queued job).
293
344
  *
294
345
  * Returns null if the job is gone, no longer pending, expired, its label
295
- * requirements are no longer satisfied by the agent, or the agent's gate
296
- * is not satisfied by the job's `runsOn`.
297
- */
298
- dequeueById(jobId: string, agentLabels: string[], agentMandatoryLabels?: string[]): Promise<QueuedJob | null>;
346
+ * requirements are no longer satisfied by the agent, the agent's gate is not
347
+ * satisfied by the job's `runsOn`, or another claimant won the row first.
348
+ *
349
+ * That last case is the one this shares with every other claim path: the
350
+ * eager bound claim and the generic drain can target the same row moments
351
+ * apart, and a SELECT that returns the row still Pending lets both dispatch
352
+ * it. The conditional UPDATE below is the arbiter, and it runs after the JS
353
+ * post-filter so a pattern-rejected row is never claimed and stranded. A
354
+ * loser returns null, and `dispatchBoundJob` then falls back to the generic
355
+ * `onAgentAvailable` drain exactly as it does for an already-gone job.
356
+ *
357
+ * @param claimingAgentId Recorded as the row's durable owner as part of the
358
+ * claim. Optional so existing 3-arg callers keep working; the caller's
359
+ * markDispatched sets the same column immediately afterwards either way.
360
+ */
361
+ dequeueById(jobId: string, agentLabels: string[], agentMandatoryLabels?: string[], claimingAgentId?: string): Promise<QueuedJob | null>;
299
362
  /**
300
363
  * Insert a job directly with status='dispatched' (bypasses the queue).
301
364
  * Used when an agent is immediately available and the job doesn't need to wait.
@@ -3,6 +3,7 @@ import { ExecutionJobStatus, type LabelMatcher } from '@kici-dev/engine';
3
3
  import type { Database } from '../db/types.js';
4
4
  import type { ExecutionTracker } from '../reporting/execution-tracker.js';
5
5
  import type { CheckRunReporter } from '../reporting/check-run-reporter.js';
6
+ import type { PendingGlobalEvalTracker } from '../cache/pending-global-evals.js';
6
7
  import type { ExpiredJobInfo } from './job-queue.js';
7
8
  /**
8
9
  * Whether ANYTHING could ever run a job with these selectors — a registered
@@ -23,6 +24,18 @@ export interface TerminalizeDeps {
23
24
  executionTracker: ExecutionTracker;
24
25
  checkRunReporter?: Pick<CheckRunReporter, 'updateJobStatus'>;
25
26
  canRouteLabels?: CanRouteLabels;
27
+ /**
28
+ * The Tier-2 global-eval tracker, so a round job settled here also settles
29
+ * the webhook request awaiting its verdict.
30
+ *
31
+ * A round job is the one queue entry with an in-process awaiter and no
32
+ * `execution_runs` row — the round decides whether any run exists at all —
33
+ * so the rest of this function skips it entirely and the awaiter would
34
+ * otherwise wait out its full ceiling for a job the queue has already
35
+ * declared dead. With the shipped defaults that is a 120s definitive
36
+ * fast-fail followed by a 240s wait, twice.
37
+ */
38
+ pendingGlobalEvals?: Pick<PendingGlobalEvalTracker, 'reject'>;
26
39
  }
27
40
  export declare function unroutableMessage(job: JobRoutingFacts): string;
28
41
  /**
@@ -0,0 +1,47 @@
1
+ import type { ExpressionBuilder, ExpressionWrapper, SqlBool } from 'kysely';
2
+ import type { Database } from '../db/types.js';
3
+ /**
4
+ * Match `execution_runs` rows to the registrations that define their workflow.
5
+ *
6
+ * A registration always names the repository the workflow is DEFINED in. A run
7
+ * names two repositories: `repo_identifier` is the one it acted on and whose
8
+ * code its jobs checked out, and `workflow_repo_identifier` is the one that
9
+ * defines the workflow — recorded only when the two differ, which is exactly an
10
+ * organization-wide workflow dispatched against another repository.
11
+ *
12
+ * So the repository a registration must be matched on is
13
+ * `workflow_repo_identifier ?? repo_identifier`. Matching on `repo_identifier`
14
+ * alone is wrong in both directions at once: a global registration matches none
15
+ * of its own runs, and a same-named registration in the acted-on repository
16
+ * matches all of them.
17
+ *
18
+ * Both consumers — the "last triggered" enrichment and the delete path's
19
+ * in-flight cancellation — share this one predicate deliberately. Fixing one
20
+ * and not the other would leave the dashboard showing a global workflow as
21
+ * triggered while deleting it still cancelled nothing.
22
+ *
23
+ * A NULL marker is evidence of a per-repository run only because every
24
+ * recording site states which repository defines the workflow it is recording,
25
+ * and the sites narrow that to NULL exactly when it is the repository the run
26
+ * acted on. The predicate never infers the answer, and the recording sites do
27
+ * not leave it to a default: `recordInitFailureRun`, `recordRunHeld` and
28
+ * `recordGlobalEvalRoundFailureRun` take the defining repository as a required
29
+ * argument, and `WorkflowDispatchContext` carries it as a required field, so a
30
+ * dispatch path that does not state it does not compile. The one run-start
31
+ * recorder that takes it as a trailing positional, `onExecutionStarted`, is
32
+ * supplied by the global dispatch path — the only path where the two
33
+ * repositories differ.
34
+ */
35
+ export declare function runsDefinedByRepos(eb: ExpressionBuilder<Database, 'execution_runs'>, repoIdentifiers: readonly string[]): ExpressionWrapper<Database, 'execution_runs', SqlBool>;
36
+ /**
37
+ * The repository that defines the workflow a run executed — the read-side
38
+ * counterpart of {@link runsDefinedByRepos}, applied to a row the query
39
+ * returned. Selecting `workflow_repo_identifier` alongside `repo_identifier`
40
+ * and folding here keeps the grouping key the same expression the predicate
41
+ * filters on.
42
+ */
43
+ export declare function definingRepoOfRun(row: {
44
+ repo_identifier: string;
45
+ workflow_repo_identifier: string | null;
46
+ }): string;
47
+ //# sourceMappingURL=registration-run-match.d.ts.map