@kici-dev/orchestrator 0.1.3 → 0.1.6

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 (73) hide show
  1. package/dist/agent/dispatcher.d.ts +26 -0
  2. package/dist/agent/ownership-tracker.d.ts +45 -3
  3. package/dist/app.d.ts +9 -3
  4. package/dist/audit/access-log.d.ts +7 -0
  5. package/dist/cli/api-client.d.ts +14 -0
  6. package/dist/cli/commands/cluster-name.d.ts +18 -0
  7. package/dist/cli/commands/db.d.ts +9 -6
  8. package/dist/cli/commands/org-settings.d.ts +2 -1
  9. package/dist/cli/commands/shared/secret-input.d.ts +46 -0
  10. package/dist/cli/commands/variable.d.ts +18 -0
  11. package/dist/cli/service/index.d.ts +1 -0
  12. package/dist/cli/service/launchd.d.ts +19 -0
  13. package/dist/cli/service/privilege.d.ts +21 -0
  14. package/dist/cli/wizard/orchestrator-wizard.d.ts +4 -2
  15. package/dist/cli.js +1074 -225
  16. package/dist/cluster/coordinator.d.ts +3 -3
  17. package/dist/cluster/join-token.d.ts +21 -13
  18. package/dist/cluster/peer-client.d.ts +9 -0
  19. package/dist/config/cluster-id.d.ts +23 -0
  20. package/dist/config/cluster-name.d.ts +39 -0
  21. package/dist/config.d.ts +8 -0
  22. package/dist/dashboard/handler.d.ts +54 -2
  23. package/dist/db/migrations/019_generic_sources_change_notify.d.ts +25 -0
  24. package/dist/db/migrations/020_org_settings_dashboard_write_policy.d.ts +17 -0
  25. package/dist/db/migrations/021_check_run_tracking.d.ts +26 -0
  26. package/dist/db/migrations/022_scaler_manager_state.d.ts +29 -0
  27. package/dist/db/migrations/023_dispatch_queue_recovery_deadline.d.ts +26 -0
  28. package/dist/db/types.d.ts +137 -0
  29. package/dist/events/event-store.d.ts +9 -2
  30. package/dist/events/trust-store.d.ts +7 -0
  31. package/dist/events/types.d.ts +33 -0
  32. package/dist/metrics/scheduled-jobs.d.ts +26 -5
  33. package/dist/orchestrator-core.d.ts +16 -2
  34. package/dist/pipeline/processor.d.ts +2 -2
  35. package/dist/pipeline/rerun.d.ts +2 -2
  36. package/dist/pipeline/test-pipeline.d.ts +2 -2
  37. package/dist/policy/dashboard-write-policy.d.ts +115 -0
  38. package/dist/queue/job-queue.d.ts +54 -1
  39. package/dist/reporting/{commit-status.d.ts → check-run-reporter.d.ts} +94 -16
  40. package/dist/reporting/check-run-tracking-store.d.ts +133 -0
  41. package/dist/routes/admin-access-log.d.ts +1 -0
  42. package/dist/routes/admin-backends.d.ts +9 -1
  43. package/dist/routes/admin-cluster-name.d.ts +50 -0
  44. package/dist/routes/admin-db.d.ts +9 -1
  45. package/dist/routes/admin-environments.d.ts +10 -6
  46. package/dist/routes/admin-event-dlq.d.ts +1 -0
  47. package/dist/routes/admin-event-log.d.ts +1 -0
  48. package/dist/routes/admin-events.d.ts +19 -0
  49. package/dist/routes/admin-maintenance.d.ts +9 -1
  50. package/dist/routes/admin-org-settings.d.ts +10 -1
  51. package/dist/routes/admin-queue-execution.d.ts +1 -0
  52. package/dist/routes/admin-registrations.d.ts +1 -0
  53. package/dist/routes/admin-runs.d.ts +1 -0
  54. package/dist/routes/admin-scheduled-jobs.d.ts +1 -0
  55. package/dist/routes/admin-sources.d.ts +24 -1
  56. package/dist/routes/admin.d.ts +22 -0
  57. package/dist/scaler/manager.d.ts +38 -0
  58. package/dist/scaler/scaler-state-store.d.ts +102 -0
  59. package/dist/secrets/routing-key-scope.d.ts +43 -0
  60. package/dist/server.js +10098 -6961
  61. package/dist/sources/build-platform-sources.d.ts +26 -0
  62. package/dist/sources/source-manager.d.ts +14 -0
  63. package/dist/stale-detector/stale-run-detector.d.ts +17 -5
  64. package/dist/standalone.js +7525 -5172
  65. package/dist/webhook/generic-sources-listener.d.ts +89 -0
  66. package/dist/webhook/register-source-bundle.d.ts +57 -0
  67. package/dist/ws/dashboard-backends-handler.d.ts +14 -0
  68. package/dist/ws/dashboard-env-handler.d.ts +7 -0
  69. package/dist/ws/dashboard-global-workflows-handler.d.ts +6 -0
  70. package/dist/ws/dashboard-registrations-handler.d.ts +7 -0
  71. package/dist/ws/platform-client.d.ts +57 -2
  72. package/package.json +3 -3
  73. package/sbom.spdx.json +40 -35
@@ -23,6 +23,20 @@ export interface EventRouterConfig {
23
23
  retryMaxBackoffMs: number;
24
24
  /** Interval at which the leader-only retry scanner ticks (default: 10_000) */
25
25
  retryScanIntervalMs: number;
26
+ /**
27
+ * **Test-only.** Per-event-name fault injection: when `attempts <= N`,
28
+ * the EventRouter throws a synthetic dispatch error to drive the retry /
29
+ * DLQ path. Used by the fault-injection E2E to prove the lease + retry
30
+ * loop dispatches a real run when the inner dispatch eventually
31
+ * succeeds, and lands the row in the DLQ when N exceeds
32
+ * `maxDispatchAttempts`.
33
+ *
34
+ * Only honoured when `KICI_TEST_MODE=1` is set at config-load time —
35
+ * production deployments never see this knob even if the env var is
36
+ * planted by accident. Source the value from
37
+ * `KICI_TEST_EVENT_FAIL_FIRST_N` (a JSON object literal).
38
+ */
39
+ debugFailFirstNAttemptsByEvent?: Record<string, number>;
26
40
  }
27
41
  /**
28
42
  * Reason an event landed in the DLQ.
@@ -54,6 +68,25 @@ export interface StoredEvent {
54
68
  dlqAt: Date | null;
55
69
  dlqReason: DlqReason | null;
56
70
  }
71
+ /**
72
+ * Parse the `KICI_TEST_EVENT_FAIL_FIRST_N` JSON payload, gated by
73
+ * `KICI_TEST_MODE`. Returns the parsed map or `undefined` when:
74
+ *
75
+ * - `testMode` is false (the master switch). The JSON value is ignored
76
+ * entirely; production deployments never see fault-injection even if
77
+ * the per-event env var is accidentally planted.
78
+ * - the JSON value is absent / empty.
79
+ * - the JSON value is malformed or carries a wrong-shape entry. We
80
+ * refuse to fall back to a partial map: a typo on one line silently
81
+ * skipping that test would be confusing in CI logs.
82
+ *
83
+ * The accepted shape is `{ "<eventName>": <number> }`; non-string keys
84
+ * and non-number values are rejected. The numeric value is the inclusive
85
+ * upper bound of attempts to fail (so `1` fails the first attempt and
86
+ * succeeds on retry; `99` exceeds `maxDispatchAttempts` and lands the
87
+ * row in the DLQ).
88
+ */
89
+ export declare function parseFaultInjectionMap(testMode: boolean, raw: string | undefined): Record<string, number> | undefined;
57
90
  /**
58
91
  * Default event router configuration.
59
92
  */
@@ -1,18 +1,39 @@
1
- /** Success/failure counter for each tick of each scheduled job. */
1
+ /**
2
+ * Success/failure counter for each tick of each scheduled job.
3
+ * Labels:
4
+ * - job: scheduled-job name (one of `OrchestratorScheduledJobName`)
5
+ * - result: success | failure
6
+ */
2
7
  export declare const jobRunsTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
3
- /** Histogram of per-tick duration, seconds. */
8
+ /**
9
+ * Histogram of per-tick duration, seconds.
10
+ * Labels:
11
+ * - job: scheduled-job name (one of `OrchestratorScheduledJobName`)
12
+ */
4
13
  export declare const jobDurationSeconds: import("@opentelemetry/api").Histogram<import("@opentelemetry/api").Attributes>;
5
- /** Unix timestamp (seconds) of the most recent successful tick. */
14
+ /**
15
+ * Unix timestamp (seconds) of the most recent successful tick.
16
+ * Labels:
17
+ * - job: scheduled-job name (one of `OrchestratorScheduledJobName`)
18
+ */
6
19
  export declare const jobLastSuccessTimestamp: {
7
20
  set(labels: Record<string, string>, value: number): void;
8
21
  reset(): void;
9
22
  };
10
- /** Unix timestamp (seconds) of the most recent failed tick. */
23
+ /**
24
+ * Unix timestamp (seconds) of the most recent failed tick.
25
+ * Labels:
26
+ * - job: scheduled-job name (one of `OrchestratorScheduledJobName`)
27
+ */
11
28
  export declare const jobLastFailureTimestamp: {
12
29
  set(labels: Record<string, string>, value: number): void;
13
30
  reset(): void;
14
31
  };
15
- /** Count of consecutive failures since last success. Resets to 0 on success. */
32
+ /**
33
+ * Count of consecutive failures since last success. Resets to 0 on success.
34
+ * Labels:
35
+ * - job: scheduled-job name (one of `OrchestratorScheduledJobName`)
36
+ */
16
37
  export declare const jobConsecutiveFailures: {
17
38
  set(labels: Record<string, string>, value: number): void;
18
39
  reset(): void;
@@ -27,7 +27,7 @@ import { ScalerManager } from './scaler/index.js';
27
27
  import type { ScalerConfig } from './scaler/index.js';
28
28
  import type { CacheStorage } from './storage/index.js';
29
29
  import { SourceCache, BuildCoordinator, DepCache, PendingBuildTracker, PendingInitTracker, PendingDynamicTracker } from './cache/index.js';
30
- import { CommitStatusReporter } from './reporting/commit-status.js';
30
+ import { CheckRunReporter } from './reporting/check-run-reporter.js';
31
31
  import { StepLogBuffer } from './reporting/step-log-buffer.js';
32
32
  import { type LogStorage } from './reporting/log-storage.js';
33
33
  import { ExecutionTracker, type ExecutionTrackerDeps } from './reporting/execution-tracker.js';
@@ -39,10 +39,12 @@ import type { SecretStore } from '@kici-dev/engine';
39
39
  import type { AdminRouteDeps } from './routes/admin.js';
40
40
  import { AgentTokenStore } from './agent/token-store.js';
41
41
  import { OwnershipTracker } from './agent/ownership-tracker.js';
42
+ import { EventStore } from './events/event-store.js';
42
43
  import { EventRouter } from './events/event-router.js';
43
44
  import { EventEmitter } from './events/event-emitter.js';
44
45
  import { TrustStore } from './events/trust-store.js';
45
46
  import { GenericSourceManager } from './webhook/generic-sources.js';
47
+ import { GenericSourcesChangeListener } from './webhook/generic-sources-listener.js';
46
48
  import { RegistrationStore } from './registration/registration-store.js';
47
49
  import { RegistrationIndex } from './registration/registration-index.js';
48
50
  import { CronScheduler } from './cron/cron-scheduler.js';
@@ -72,7 +74,7 @@ export interface OrchestratorSubsystems {
72
74
  pendingBuilds: PendingBuildTracker | undefined;
73
75
  pendingInits: PendingInitTracker;
74
76
  pendingDynamics: PendingDynamicTracker;
75
- commitStatusReporter: CommitStatusReporter;
77
+ checkRunReporter: CheckRunReporter;
76
78
  stepLogBuffer: StepLogBuffer;
77
79
  sourceLocationStore: SourceLocationStore;
78
80
  logStorage: LogStorage;
@@ -87,6 +89,12 @@ export interface OrchestratorSubsystems {
87
89
  lockFileCache: LockFileCache;
88
90
  dedup: DedupCache;
89
91
  eventRouter: EventRouter;
92
+ /**
93
+ * Event store for custom internal events (system + custom). Exposed
94
+ * here so the dashboard handler can serve the per-org DLQ surface
95
+ * (list / count / retry / discard) over the WS relay.
96
+ */
97
+ eventStore: EventStore;
90
98
  eventEmitter: EventEmitter;
91
99
  genericSourceManager: GenericSourceManager;
92
100
  trustStore: TrustStore;
@@ -107,6 +115,12 @@ export interface OrchestratorSubsystems {
107
115
  localConfigVersion: number;
108
116
  sourceStore: SourceStore;
109
117
  sourceManager: SourceManager;
118
+ /**
119
+ * Generic-webhook-source hot-reload listener. Exposed so the platform-mode
120
+ * boot can wire its `onChange` to re-push the full source list to the
121
+ * Platform when a generic source is added/removed at runtime.
122
+ */
123
+ genericSourcesChangeListener: GenericSourcesChangeListener;
110
124
  /** Inbound webhook delivery log writer (event_log table + object-storage payloads). */
111
125
  eventLogWriter: EventLogWriter;
112
126
  /** Access log writer (read + mutation attribution for dashboard + admin). */
@@ -26,7 +26,7 @@ import type { DepCache } from '../cache/index.js';
26
26
  import type { PendingBuildTracker } from '../cache/index.js';
27
27
  import type { PendingInitTracker } from '../cache/pending-inits.js';
28
28
  import type { PendingDynamicTracker } from '../cache/pending-dynamics.js';
29
- import type { CommitStatusReporter } from '../reporting/commit-status.js';
29
+ import type { CheckRunReporter } from '../reporting/check-run-reporter.js';
30
30
  import type { ExecutionTracker } from '../reporting/execution-tracker.js';
31
31
  import type { AgentRegistry } from '../agent/registry.js';
32
32
  import type { RunCoordinator } from '../cluster/coordinator.js';
@@ -237,7 +237,7 @@ export interface ProcessingDeps {
237
237
  /** Pending dynamic tracker -- waits for agents to evaluate DynamicJobFn and return generated LockJob[]. */
238
238
  pendingDynamics?: PendingDynamicTracker;
239
239
  /** Commit status reporter for setting pending/success/failure/error on commits. Optional. */
240
- commitStatusReporter?: CommitStatusReporter;
240
+ checkRunReporter?: CheckRunReporter;
241
241
  /** Execution tracker for DB persistence. Optional -- if not set, execution tracking is skipped. */
242
242
  executionTracker?: ExecutionTracker;
243
243
  /** Agent registry for determining execution target platform/arch. Optional -- if not set, defaults to linux/x64. */
@@ -18,7 +18,7 @@ import type { Dispatcher } from '../agent/dispatcher.js';
18
18
  import type { ExecutionTracker } from '../reporting/execution-tracker.js';
19
19
  import type { JobQueue } from '../queue/job-queue.js';
20
20
  import type { PlatformClient } from '../ws/platform-client.js';
21
- import type { CommitStatusReporter } from '../reporting/commit-status.js';
21
+ import type { CheckRunReporter } from '../reporting/check-run-reporter.js';
22
22
  import type { RunCoordinator } from '../cluster/coordinator.js';
23
23
  import type { SecretResolver } from '../secrets/secret-resolver.js';
24
24
  import type { EventRouter } from '../events/event-router.js';
@@ -50,7 +50,7 @@ export interface RerunDeps {
50
50
  dispatcher: Dispatcher;
51
51
  jobQueue: JobQueue;
52
52
  platformClient: PlatformClient | null;
53
- commitStatusReporter: CommitStatusReporter | null;
53
+ checkRunReporter: CheckRunReporter | null;
54
54
  coordinator: RunCoordinator | null;
55
55
  secretResolver: SecretResolver | null;
56
56
  eventRouter: EventRouter | null;
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import type { LockFileCache } from '../lockfile-cache.js';
18
18
  import type { Dispatcher } from '../agent/dispatcher.js';
19
- import type { CommitStatusReporter } from '../reporting/commit-status.js';
19
+ import type { CheckRunReporter } from '../reporting/check-run-reporter.js';
20
20
  import type { ExecutionTracker } from '../reporting/execution-tracker.js';
21
21
  import type { AgentRegistry } from '../agent/registry.js';
22
22
  import type { ProviderRegistry } from '../provider-registry.js';
@@ -85,7 +85,7 @@ export interface TestPipelineDeps {
85
85
  lockFileCache: LockFileCache;
86
86
  dispatcher: Dispatcher;
87
87
  executionTracker?: ExecutionTracker;
88
- commitStatusReporter?: CommitStatusReporter;
88
+ checkRunReporter?: CheckRunReporter;
89
89
  sourceCache?: SourceCache;
90
90
  buildCoordinator?: BuildCoordinator;
91
91
  depCache?: DepCache;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Per-orchestrator policy controlling which dashboard.* write operations
3
+ * the orch accepts from Platform.
4
+ *
5
+ * Storage: a JSONB column on the existing `org_settings` table, keyed by
6
+ * `customer_id`. Empty object means every operation is enabled
7
+ * (permissive default at first-boot). Operators flip individual
8
+ * operations off via `kici-admin org-settings dashboard-writes set`.
9
+ *
10
+ * Three callers:
11
+ * - The kici-admin CLI mutates via `setDashboardWritePolicy`.
12
+ * - Mutating dashboard.* handlers check via `assertDashboardWriteAllowed`.
13
+ * - The Platform-bound WS publisher reads the full map via
14
+ * `getDashboardWritePolicy` to broadcast `orch.capabilities`.
15
+ *
16
+ * Reads are cached in-process (30 s TTL) — the policy changes
17
+ * infrequently and reading on every dashboard.* request is wasted IO.
18
+ * Writes invalidate the cache and emit a change event so the WS
19
+ * broadcaster can republish.
20
+ */
21
+ import { EventEmitter } from 'node:events';
22
+ import { type Kysely } from 'kysely';
23
+ import { DashboardWriteOperation, type DashboardWritePolicyMap } from '@kici-dev/engine/protocol/dashboard-write-operations';
24
+ import type { Database } from '../db/types.js';
25
+ import type { ActorPrincipal } from '@kici-dev/engine';
26
+ /**
27
+ * Event bus for policy-change notifications. The WS publisher subscribes
28
+ * to `'changed'` so it can broadcast a fresh `orch.capabilities` to
29
+ * Platform whenever the operator flips a switch.
30
+ */
31
+ export declare const dashboardWritePolicyEvents: EventEmitter<[never]>;
32
+ /**
33
+ * Error thrown by `assertDashboardWriteAllowed` when an operation is
34
+ * disabled. Carries the operation name + the descriptor's CLI hint so
35
+ * callers can surface a structured error to Platform / dashboard.
36
+ */
37
+ export declare class DashboardWritePolicyDisabledError extends Error {
38
+ readonly operation: DashboardWriteOperation;
39
+ readonly cliEquivalent: string;
40
+ readonly code: "operation_disabled";
41
+ constructor(operation: DashboardWriteOperation, cliEquivalent: string);
42
+ }
43
+ /**
44
+ * Structured response envelope sent back to Platform when a mutating
45
+ * dashboard.* handler is rejected by the policy gate. Mirrors the
46
+ * Platform-side 403 body so the dashboard renders one consistent
47
+ * "operation disabled" affordance no matter which layer fires first.
48
+ */
49
+ export declare function buildPolicyDeniedResponse(op: DashboardWriteOperation, responseType: string, requestId: string): Record<string, unknown>;
50
+ /**
51
+ * Clear the cache. Public for tests and for the rare case where an
52
+ * operator wants to force a fresh read after manual DB surgery.
53
+ */
54
+ export declare function invalidateDashboardWritePolicyCache(customerId?: string): void;
55
+ /**
56
+ * Read the full policy map for a customer. Hits the in-process cache
57
+ * for 30 s after the first read; falls back to the DB on cache miss.
58
+ * Returns an empty map (everything enabled) when no `org_settings` row
59
+ * exists for the customer.
60
+ */
61
+ export declare function getDashboardWritePolicy(db: Kysely<Database>, customerId: string): Promise<DashboardWritePolicyMap>;
62
+ /**
63
+ * Single-operation check. Convenience wrapper around
64
+ * `getDashboardWritePolicy` + the engine-side resolver.
65
+ */
66
+ export declare function isDashboardWriteEnabled(db: Kysely<Database>, customerId: string, op: DashboardWriteOperation): Promise<boolean>;
67
+ /**
68
+ * Defense-in-depth gate for orch-side dashboard.* handlers. Throws
69
+ * `DashboardWritePolicyDisabledError` when the operation is disabled.
70
+ * Callers translate the error into the structured error envelope they
71
+ * send back to Platform.
72
+ */
73
+ export declare function assertDashboardWriteAllowed(db: Kysely<Database>, customerId: string, op: DashboardWriteOperation): Promise<void>;
74
+ /**
75
+ * Merge `updates` into the persisted policy and persist the result.
76
+ * Unknown keys in `updates` are rejected at the Zod schema layer
77
+ * before this function runs (kici-admin already validates). Any
78
+ * operation explicitly set to `true` is normalized away — the
79
+ * permissive default lives in the absence of the key, keeping the
80
+ * JSONB shape minimal.
81
+ *
82
+ * Each changed operation invokes the optional `onChange` callback
83
+ * once — the caller decides what to do with it (typically: write
84
+ * one `access_log` row per change).
85
+ *
86
+ * Emits `'changed'` on `dashboardWritePolicyEvents` with the new map
87
+ * so the WS publisher can rebroadcast capabilities.
88
+ */
89
+ export declare function setDashboardWritePolicy(db: Kysely<Database>, customerId: string, updates: DashboardWritePolicyMap, options: {
90
+ actor: ActorPrincipal;
91
+ onChange?: (event: PolicyChangeEvent) => Promise<void>;
92
+ }): Promise<DashboardWritePolicyMap>;
93
+ /**
94
+ * Per-operation change event fired by `setDashboardWritePolicy` for
95
+ * each switch that actually flipped. The caller (admin HTTP route,
96
+ * test, future automation) decides what to do — typically writes one
97
+ * `access_log` row carrying `op`, `prior`, and `next` in `meta`.
98
+ */
99
+ export interface PolicyChangeEvent {
100
+ actor: ActorPrincipal;
101
+ customerId: string;
102
+ op: DashboardWriteOperation;
103
+ prior: boolean;
104
+ next: boolean;
105
+ }
106
+ /**
107
+ * Reset to the permissive defaults (everything enabled). Useful for
108
+ * the `kici-admin org-settings dashboard-writes reset` subcommand.
109
+ */
110
+ export declare function resetDashboardWritePolicy(db: Kysely<Database>, customerId: string, options: {
111
+ actor: ActorPrincipal;
112
+ onChange?: (event: PolicyChangeEvent) => Promise<void>;
113
+ }): Promise<DashboardWritePolicyMap>;
114
+ export { resolveFullPolicyView } from '@kici-dev/engine/protocol/dashboard-write-operations';
115
+ //# sourceMappingURL=dashboard-write-policy.d.ts.map
@@ -298,8 +298,43 @@ export declare class JobQueue {
298
298
  /**
299
299
  * Mark a job as recovering (agent disconnected, within grace period).
300
300
  * Only transitions from 'dispatched' state for safety.
301
+ *
302
+ * When `deadline` and `agentId` are provided, persists them so a
303
+ * replacement coord on Raft leader switch can recreate the recovery
304
+ * timer (via `getRecoveringJobs()` on boot) or expire the row in
305
+ * the leader-gated sweep (`sweepExpiredRecoveries()`).
301
306
  */
302
- markRecovering(jobId: string): Promise<void>;
307
+ markRecovering(jobId: string, deadline?: Date, agentId?: string): Promise<void>;
308
+ /**
309
+ * List every job currently in `recovering` state with its persisted
310
+ * recovery deadline. Used at coord boot (`Dispatcher.recoverState()`)
311
+ * to recreate the in-memory `recoveringJobs` Map with fresh timers.
312
+ *
313
+ * Returns rows whose `recovery_deadline` is non-null (the populated
314
+ * subset). Recovering rows from before the migration carry NULL and
315
+ * are handled by the leader-gated sweep on its next pass.
316
+ */
317
+ getRecoveringJobs(): Promise<Array<{
318
+ id: string;
319
+ runId: string;
320
+ agentId: string | null;
321
+ deadline: Date | null;
322
+ }>>;
323
+ /**
324
+ * Sweep every `recovering` row whose `recovery_deadline` is in the
325
+ * past, marking them `failed`. Returns the rows that flipped so the
326
+ * caller can fire the per-job `onRecoveryTimeout` hook in process.
327
+ *
328
+ * Intended for the leader-gated `Dispatcher.sweepExpiredRecoveries`
329
+ * tick — running on N coords would still be correct (the WHERE
330
+ * `status='recovering'` clause prevents double-failure) but only one
331
+ * needs to do the work.
332
+ */
333
+ sweepExpiredRecoveries(now: Date): Promise<Array<{
334
+ id: string;
335
+ runId: string;
336
+ agentId: string | null;
337
+ }>>;
303
338
  /**
304
339
  * Mark a job as failed only if it is still in 'recovering' state.
305
340
  * Uses optimistic concurrency to avoid failing jobs that were reclaimed.
@@ -321,6 +356,24 @@ export declare class JobQueue {
321
356
  runId: string;
322
357
  status: DispatchQueueStatus;
323
358
  } | null>;
359
+ /**
360
+ * HA-safe ownership check. Returns true if the DB shows that
361
+ * `agentId` previously held `jobId` according to any of:
362
+ *
363
+ * - `status='dispatched'` AND the registry-managed bookkeeping
364
+ * records the agent assignment (caller-side `agentJobs` map),
365
+ * - `status='recovering'` AND `recovery_agent_id = <agent>` (so a
366
+ * replacement coord still recognises in-flight chunks), OR
367
+ * - the row is already terminal (`completed` / `failed` /
368
+ * `expired`) — late `log.chunk` chunks from the agent's drain
369
+ * window are accepted as benign duplicates rather than
370
+ * rejected.
371
+ *
372
+ * Used by `OwnershipTracker.validateAsync` so a Raft leader switch
373
+ * doesn't turn the next 30s of legitimate per-job chunks into a
374
+ * stream of ownership violations.
375
+ */
376
+ hasAgentOwnedJob(agentId: string, jobId: string): Promise<boolean>;
324
377
  /**
325
378
  * Get all jobs matching a given status.
326
379
  * Used on startup to find 'dispatched' jobs from a previous instance for recovery.
@@ -12,7 +12,7 @@
12
12
  * Check run IDs are tracked in-memory after creation so that subsequent
13
13
  * updates (job complete, workflow complete) can reference them via checks.update().
14
14
  *
15
- * Enriched output (Phase 14):
15
+ * Enriched output:
16
16
  * - Live step progress with checklist-style updates (debounced at 5s)
17
17
  * - Failed check runs include step names, error messages, exit codes, and log context
18
18
  * - Check run annotations link failures to step source locations in workflow files (.kici/workflows/*.ts)
@@ -22,18 +22,19 @@
22
22
  * Note: The engine-level CheckStatusPoster interface (packages/engine/src/provider/check-status-poster.ts)
23
23
  * provides a provider-agnostic API for posting security-related check statuses (holds, approvals,
24
24
  * workflow modifications). The GitHub implementation is at packages/orchestrator/src/providers/github/check-status-poster.ts.
25
- * This CommitStatusReporter handles execution lifecycle checks (queued, in_progress, completed per job/workflow).
25
+ * This CheckRunReporter handles execution lifecycle checks (queued, in_progress, completed per job/workflow).
26
26
  * Future cleanup may unify both under CheckStatusPoster, but they serve different purposes today.
27
27
  */
28
28
  import { type GitHubAppConfig } from '../providers/github/auth.js';
29
29
  import type { ProviderRegistry } from '../provider-registry.js';
30
30
  import type { StepLogBuffer } from './step-log-buffer.js';
31
31
  import { type SourceLocationData } from './check-run-summary.js';
32
+ import type { CheckRunTrackingStore } from './check-run-tracking-store.js';
32
33
  import { ExecutionJobStatus } from '@kici-dev/engine';
33
34
  /**
34
- * Dependencies for the CommitStatusReporter.
35
+ * Dependencies for the CheckRunReporter.
35
36
  */
36
- interface CommitStatusReporterDeps {
37
+ interface CheckRunReporterDeps {
37
38
  /**
38
39
  * Provider registry for per-routing-key credential lookup.
39
40
  * When provided, the reporter resolves GitHub App credentials by routing key
@@ -48,6 +49,15 @@ interface CommitStatusReporterDeps {
48
49
  githubConfig?: GitHubAppConfig;
49
50
  /** Step log buffer for enriched failure summaries. */
50
51
  stepLogBuffer?: StepLogBuffer;
52
+ /**
53
+ * DB-backed tracking store. When provided, every per-key state mutation
54
+ * (check-run ID, step-progress array, build creation marker,
55
+ * in-progress-sent flag) is written through to the store so a replacement
56
+ * coord on Raft leader switch can recover the state. When omitted (the
57
+ * back-compat path used by unit tests that don't need HA correctness),
58
+ * the reporter operates entirely from in-memory `Map`s.
59
+ */
60
+ trackingStore?: CheckRunTrackingStore;
51
61
  /** Resolver for step source locations from the lock file (for annotations). */
52
62
  getStepSourceLocations?: (workflowName: string, jobName: string) => SourceLocationData[] | undefined;
53
63
  /**
@@ -196,19 +206,45 @@ interface UpdateStepProgressOptions {
196
206
  * All public methods are fire-and-forget: they return void immediately
197
207
  * and log errors internally without propagating them.
198
208
  */
199
- export declare class CommitStatusReporter {
209
+ export declare class CheckRunReporter {
200
210
  private deps;
201
- /** Map from composite key to check run ID for subsequent updates. */
211
+ /**
212
+ * L1 cache: composite key → check run ID.
213
+ *
214
+ * Backed by `check_run_tracking.check_run_id` when `deps.trackingStore`
215
+ * is wired. On a miss the cache falls through to the store; on a store
216
+ * miss the lookup returns undefined and the caller logs + skips.
217
+ *
218
+ * Without the store this Map IS the source of truth (single-coord
219
+ * deployments, unit tests).
220
+ */
202
221
  private readonly checkRunIds;
203
- /** In-flight build check run creation promises, keyed by composite key. */
222
+ /**
223
+ * L1 cache: in-flight build-creation promises. The DB-backed counterpart
224
+ * lives in `check_run_tracking.build_creation_state`; this Map is needed
225
+ * locally so a same-process `setBuildComplete` can await the
226
+ * in-progress `setBuildPending` promise (the DB column is a state
227
+ * marker, not an awaitable).
228
+ */
204
229
  private readonly pendingBuildCreations;
205
- /** Step progress entries per check run key. */
230
+ /** L1 cache: step-progress entries (synced to `check_run_tracking.step_progress_json`). */
206
231
  private readonly stepProgress;
207
- /** Debounce timers for in_progress updates per check run key. */
232
+ /**
233
+ * L1 cache: per-key debounce timers. NOT persisted — on coord failover
234
+ * the next update either flushes immediately (because the DB row's
235
+ * `updated_at` is older than the debounce window) or starts a fresh
236
+ * timer.
237
+ */
208
238
  private readonly progressTimers;
209
- /** Track whether the first in_progress transition has been sent per check run key. */
239
+ /** L1 cache: first in-progress sent flag (synced to `check_run_tracking.in_progress_sent_at`). */
210
240
  private readonly inProgressSent;
211
- constructor(deps: CommitStatusReporterDeps);
241
+ /**
242
+ * L1 cache: runId → set of check-run composite keys. Synced to the
243
+ * indexed `check_run_tracking.run_id` column so a replacement coord can
244
+ * still find every key for a runId at cleanup time.
245
+ */
246
+ private readonly runIdToKeys;
247
+ constructor(deps: CheckRunReporterDeps);
212
248
  /**
213
249
  * Update the provider registry used for per-routing-key credential lookup.
214
250
  * Called after config reload when the provider registry is rebuilt.
@@ -298,14 +334,56 @@ export declare class CommitStatusReporter {
298
334
  */
299
335
  setBuildComplete(opts: SetBuildCompleteOptions): void;
300
336
  /**
301
- * Clean up step progress entries and pending timers for a completed run.
302
- * Called when execution tracker prunes a run from memory.
337
+ * Clean up step-progress entries, debounce timers, and DB rows for a
338
+ * completed run. Called when the execution tracker prunes the run.
339
+ *
340
+ * In-memory cleanup is synchronous; the DB cleanup is fire-and-forget
341
+ * because the caller (run-pruning hook) is on the response-shaping path
342
+ * and shouldn't block on a network round-trip. Failure logs but does
343
+ * not propagate.
303
344
  */
304
345
  cleanupRun(runId: string): void;
305
- /** Map from runId to check run keys for cleanup. */
306
- private readonly runIdToKeys;
346
+ /**
347
+ * Hydrate the L1 caches from the DB after a leader switch (or any
348
+ * boot-time recovery). Called once on coord become-leader so the
349
+ * runIdToKeys reverse map is populated for any future cleanupRun calls
350
+ * without requiring a DB round-trip per cleanup. If no store is wired,
351
+ * this is a no-op.
352
+ */
353
+ recoverState(): Promise<void>;
307
354
  /** Track a check run key associated with a runId for later cleanup. */
308
355
  private trackRunKey;
356
+ /**
357
+ * Parse a composite L1 cache key back into the (provider, owner, repo,
358
+ * sha, check_name) tuple used by the store. The key format is fixed by
359
+ * `checkRunKey()`; provider defaults to 'github' because today's
360
+ * reporter only writes check runs for GitHub.
361
+ */
362
+ private parseKey;
363
+ /**
364
+ * Write-through helper: persist a check-run ID to L1 + the store.
365
+ * Used by `setPending` / `setBuildPending` after a successful
366
+ * `checks.create()`.
367
+ */
368
+ private persistCheckRunId;
369
+ /**
370
+ * Read-through helper: look up a check-run ID. Checks L1 first, falls
371
+ * through to the store on miss, caches the result on hit. Returns
372
+ * undefined when neither layer has the ID — the caller logs + skips.
373
+ */
374
+ private resolveCheckRunId;
375
+ /**
376
+ * Write-through helper: persist updated step-progress entries.
377
+ */
378
+ private persistStepProgress;
379
+ /**
380
+ * Write-through helper: mark the first running-step transition as sent.
381
+ */
382
+ private persistInProgressSent;
383
+ /**
384
+ * Write-through helper: stamp `build_creation_state = 'pending'`.
385
+ */
386
+ private persistBuildCreationPending;
309
387
  /**
310
388
  * Resolve GitHub App credentials for a given routing key.
311
389
  *
@@ -379,4 +457,4 @@ export declare class CommitStatusReporter {
379
457
  */
380
458
  export declare function buildJobFailureDescription(data: Record<string, unknown>): string;
381
459
  export {};
382
- //# sourceMappingURL=commit-status.d.ts.map
460
+ //# sourceMappingURL=check-run-reporter.d.ts.map