@kici-dev/orchestrator 0.1.20 → 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 (71) 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 +54 -1
  4. package/dist/agent/registry.d.ts +22 -0
  5. package/dist/app.d.ts +5 -0
  6. package/dist/approvals/step-approval-bridge.d.ts +5 -0
  7. package/dist/cli/commands/source-manifest.d.ts +42 -0
  8. package/dist/cli/loopback-callback.d.ts +24 -0
  9. package/dist/cli/open-browser.d.ts +12 -0
  10. package/dist/cli/service/compose.d.ts +13 -0
  11. package/dist/cli/service/deploy-env.d.ts +31 -0
  12. package/dist/cli.js +1204 -256
  13. package/dist/cluster/coordinator.d.ts +5 -3
  14. package/dist/cluster/index.d.ts +2 -0
  15. package/dist/cluster/join-token.d.ts +27 -5
  16. package/dist/cluster/peer-auth-coordinator.d.ts +36 -0
  17. package/dist/cluster/peer-client.d.ts +34 -14
  18. package/dist/cluster/peer-credentials.d.ts +14 -2
  19. package/dist/cluster/peer-handler.d.ts +2 -2
  20. package/dist/cluster/rerouted-job-guard.d.ts +39 -0
  21. package/dist/config.d.ts +6 -0
  22. package/dist/dashboard/needs-edges.d.ts +4 -3
  23. package/dist/db/migrations/043_rerouted_to_peer.d.ts +4 -0
  24. package/dist/db/migrations/044_check_mode.d.ts +4 -0
  25. package/dist/db/migrations/045_host_properties.d.ts +16 -0
  26. package/dist/db/migrations/046_join_token_consumed_by_instance.d.ts +17 -0
  27. package/dist/db/migrations/047_needs_run_on.d.ts +4 -0
  28. package/dist/db/migrations/048_host_reboot_pending.d.ts +19 -0
  29. package/dist/db/migrations/049_held_runs_payload.d.ts +14 -0
  30. package/dist/db/migrations/050_sources_slug.d.ts +17 -0
  31. package/dist/db/types.d.ts +60 -5
  32. package/dist/deployment/deployment-identity.d.ts +9 -0
  33. package/dist/entry-helpers.d.ts +7 -0
  34. package/dist/environments/held-runs.d.ts +7 -1
  35. package/dist/github-app-name-refresher/github-app-name-refresher.d.ts +77 -0
  36. package/dist/index.d.ts +5 -1
  37. package/dist/index.js +1187 -33
  38. package/dist/lockfile-validate.d.ts +24 -0
  39. package/dist/orchestrator-core.d.ts +13 -1
  40. package/dist/pipeline/decorating-secret-resolver.d.ts +32 -0
  41. package/dist/pipeline/dispatch-matched-workflow.d.ts +127 -3
  42. package/dist/pipeline/install-secrets-resolver.d.ts +2 -2
  43. package/dist/pipeline/needs-scheduler.d.ts +22 -13
  44. package/dist/pipeline/processor.d.ts +2 -2
  45. package/dist/pipeline/test-pipeline.d.ts +37 -58
  46. package/dist/providers/github/manifest-form.d.ts +15 -0
  47. package/dist/providers/github/manifest.d.ts +103 -0
  48. package/dist/reporting/execution-tracker.d.ts +10 -1
  49. package/dist/routes/admin-sources.d.ts +18 -0
  50. package/dist/routes/admin.d.ts +8 -0
  51. package/dist/secrets/pg-secret-store.d.ts +9 -0
  52. package/dist/secrets/secret-resolver.d.ts +16 -1
  53. package/dist/server.js +4373 -2322
  54. package/dist/sources/source-store.d.ts +4 -0
  55. package/dist/sources/source-validator.d.ts +2 -0
  56. package/dist/stale-detector/reboot-deadline-sweep.d.ts +29 -0
  57. package/dist/stale-detector/stale-run-detector.d.ts +8 -0
  58. package/dist/standalone.js +3582 -1932
  59. package/dist/worker/in-memory-job-queue.d.ts +17 -0
  60. package/dist/worker/peer-outbox.d.ts +36 -0
  61. package/dist/worker/worker-outbox-relay.d.ts +13 -0
  62. package/dist/ws/agent-handler.d.ts +11 -6
  63. package/dist/ws/dashboard-fleet-handler.d.ts +33 -0
  64. package/dist/ws/dashboard-fleet-write-handler.d.ts +60 -0
  65. package/dist/ws/fleet-runs-on-all.d.ts +16 -0
  66. package/dist/ws/inventory-api.d.ts +17 -0
  67. package/dist/ws/platform-client.d.ts +13 -1
  68. package/dist/ws/test-relay-handlers.d.ts +4 -2
  69. package/installer-image-digests.json +3 -3
  70. package/package.json +4 -4
  71. package/sbom.spdx.json +77 -128
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import * as path$1 from "node:path";
5
5
  import path, { join, resolve } from "node:path";
6
6
  import * as fs$1 from "node:fs";
7
- import fs, { chmodSync, closeSync, createReadStream, createWriteStream, existsSync, openSync, promises, readFileSync, realpathSync, statSync, unwatchFile, watchFile, writeFileSync } from "node:fs";
7
+ import fs, { chmodSync, closeSync, createReadStream, createWriteStream, existsSync, mkdtempSync, openSync, promises, readFileSync, realpathSync, statSync, unwatchFile, watchFile, writeFileSync } from "node:fs";
8
8
  import { Command } from "commander";
9
9
  import { BaseColdStore, ChunkLru, addLogsToArchive, chunkObjectKey, clearDispatchQueueDirect, computeChunkId, computeMigrationsHash, createDb, createDbRole, createEnvironmentTemplateDirect, createLogger, createPool, createReadOnlyDbUser, decodeChunk, deleteEnvironmentDirect, dropAndCreateDatabase, emitKiciEventDirect, encodeKeySegment, ensureDatabase, formatBytes, formatUptime, isSchemaCurrent, listEnvironmentsDirect, listExecutionRunsDirect, listQueueDirect, listRegistrationsDirect, maskDatabaseUrl, parseDatabaseUrl, parseManifest, prunePeerCredentialsDirect, purgeScopedSecretsDirect, purgeSecretBackendsDirect, purgeStaleExecutionDirect, purgeStaleSourcesDirect, registerWorkflowManualDirect, resetRaftStateDirect, seedEnvironmentBindingDirect, seedEnvironmentDirect, setEnvironmentPolicyDirect, setEnvironmentSecretDirect, sha256, showEnvironmentDirect, showExecutionRunDirect, showQueueEntryDirect, showRegistrationDirect, storeMigrationContentHash, tablePrefix, toErrorMessage } from "@kici-dev/shared";
10
10
  import crypto, { createDecipheriv, createHash, hkdfSync, randomBytes, randomUUID } from "node:crypto";
@@ -12,22 +12,24 @@ import { createInterface } from "node:readline";
12
12
  import { access, chmod, constants, copyFile, link, mkdir, open, readFile, readdir, rm, writeFile } from "node:fs/promises";
13
13
  import { parse, stringify } from "yaml";
14
14
  import { z } from "zod";
15
- import os from "node:os";
15
+ import os, { platform, tmpdir } from "node:os";
16
16
  import { getDatabaseCollationDrift, refreshDatabaseCollationVersion, reindexDatabaseConcurrently } from "@kici-dev/shared/db-collation";
17
17
  import { sql } from "kysely";
18
18
  import { Migrator } from "kysely/migration";
19
19
  import "jsonpath-plus";
20
- import { execFile, execFileSync, execSync, spawn } from "node:child_process";
20
+ import { exec, execFile, execFileSync, execSync, spawn } from "node:child_process";
21
21
  import { promisify } from "node:util";
22
+ import { KICI_AGENT_ENV_PREFIX, KNOWN_ROLES, ScalerBackendType, ScalerEventType, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES, WS_MAX_PAYLOAD_BYTES, accessLogWarmSqlCase, getAccessLogColdDays, getSecretAuditLogColdDays, matcherSatisfiedBy, minAccessLogWarmDays, minSecretAuditLogWarmDays, parseHostPropertyAssignments, scalerAgentLabels, secretAuditLogWarmSqlCase } from "@kici-dev/engine";
23
+ import { Octokit } from "@octokit/rest";
24
+ import { createAppAuth } from "@octokit/auth-app";
25
+ import http, { createServer } from "node:http";
22
26
  import { ZipArchive } from "archiver";
23
27
  import { confirm, input, password, select } from "@inquirer/prompts";
24
28
  import JSZip from "jszip";
25
29
  import { pipeline } from "node:stream/promises";
26
30
  import "pg";
27
- import { KICI_AGENT_ENV_PREFIX, KNOWN_ROLES, ScalerBackendType, ScalerEventType, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES, WS_MAX_PAYLOAD_BYTES, accessLogWarmSqlCase, getAccessLogColdDays, getSecretAuditLogColdDays, matcherSatisfiedBy, minAccessLogWarmDays, minSecretAuditLogWarmDays, scalerAgentLabels, secretAuditLogWarmSqlCase } from "@kici-dev/engine";
28
31
  import { DASHBOARD_WRITE_OPERATIONS, DASHBOARD_WRITE_OPERATIONS_BY_NAME, DashboardWriteCategory, DashboardWriteSensitivity } from "@kici-dev/engine/protocol/dashboard-write-operations";
29
32
  import { CLUSTER_NAME_FORMAT_MESSAGE } from "@kici-dev/engine/protocol/cluster-name";
30
- import http from "node:http";
31
33
  import { PassThrough } from "node:stream";
32
34
  import Docker from "dockerode";
33
35
  var __defProp = Object.defineProperty;
@@ -1554,8 +1556,8 @@ var init_client = __esmMin((() => {
1554
1556
  //#region src/db/migrations/001_initial.ts
1555
1557
  init_client();
1556
1558
  var _001_initial_exports = /* @__PURE__ */ __exportAll({
1557
- down: () => down$41,
1558
- up: () => up$41
1559
+ down: () => down$49,
1560
+ up: () => up$49
1559
1561
  });
1560
1562
  /**
1561
1563
  * Squashed initial migration -- creates the complete Orchestrator database schema.
@@ -2247,7 +2249,7 @@ const DDL_STATEMENTS = [
2247
2249
  `ALTER TABLE ONLY public.held_runs
2248
2250
  ADD CONSTRAINT held_runs_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES public.environments(id);`
2249
2251
  ];
2250
- async function up$41(db) {
2252
+ async function up$49(db) {
2251
2253
  for (const stmt of DDL_STATEMENTS) await sql.raw(stmt).execute(db);
2252
2254
  await sql`
2253
2255
  INSERT INTO cluster_meta (key, value)
@@ -2269,7 +2271,7 @@ async function up$41(db) {
2269
2271
  * Rollback drops everything created above. Uses CASCADE on table drops to cut
2270
2272
  * through the FK graph without relying on exact topological order.
2271
2273
  */
2272
- async function down$41(db) {
2274
+ async function down$49(db) {
2273
2275
  for (const [trig, tbl] of [["source_secrets_change_trigger", "scoped_secrets"], ["sources_change_trigger", "sources"]]) await sql.raw(`DROP TRIGGER IF EXISTS ${trig} ON public.${tbl}`).execute(db);
2274
2276
  for (const table of [
2275
2277
  "workflow_registrations",
@@ -2316,8 +2318,8 @@ async function down$41(db) {
2316
2318
  //#endregion
2317
2319
  //#region src/db/migrations/002_config_versions_key_version.ts
2318
2320
  var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
2319
- down: () => down$40,
2320
- up: () => up$40
2321
+ down: () => down$48,
2322
+ up: () => up$48
2321
2323
  });
2322
2324
  /**
2323
2325
  * Add key_version column to config_versions so that sensitive-field encryption
@@ -2331,13 +2333,13 @@ var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
2331
2333
  * No index is needed: rotation does a full-table scan; reads are by
2332
2334
  * `version` primary key and never filter on `key_version`.
2333
2335
  */
2334
- async function up$40(db) {
2336
+ async function up$48(db) {
2335
2337
  await sql`
2336
2338
  ALTER TABLE public.config_versions
2337
2339
  ADD COLUMN key_version integer NOT NULL DEFAULT 1
2338
2340
  `.execute(db);
2339
2341
  }
2340
- async function down$40(db) {
2342
+ async function down$48(db) {
2341
2343
  await sql`
2342
2344
  ALTER TABLE public.config_versions
2343
2345
  DROP COLUMN key_version
@@ -2346,8 +2348,8 @@ async function down$40(db) {
2346
2348
  //#endregion
2347
2349
  //#region src/db/migrations/003_access_log.ts
2348
2350
  var _003_access_log_exports = /* @__PURE__ */ __exportAll({
2349
- down: () => down$39,
2350
- up: () => up$39
2351
+ down: () => down$47,
2352
+ up: () => up$47
2351
2353
  });
2352
2354
  /**
2353
2355
  * Access log: one row per read or orchestrator-admin mutation attributable
@@ -2371,7 +2373,7 @@ var _003_access_log_exports = /* @__PURE__ */ __exportAll({
2371
2373
  * Retention is TTL-based via expires_at; packages/orchestrator/src/queue/
2372
2374
  * cleanup.ts picks up the prune pass.
2373
2375
  */
2374
- async function up$39(db) {
2376
+ async function up$47(db) {
2375
2377
  await sql`
2376
2378
  CREATE TABLE public.access_log (
2377
2379
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -2409,28 +2411,28 @@ async function up$39(db) {
2409
2411
  ON public.access_log (actor_type, actor_id, created_at DESC)
2410
2412
  `.execute(db);
2411
2413
  }
2412
- async function down$39(db) {
2414
+ async function down$47(db) {
2413
2415
  await sql`DROP TABLE IF EXISTS public.access_log`.execute(db);
2414
2416
  }
2415
2417
  //#endregion
2416
2418
  //#region src/db/migrations/004_rename_bundle_to_source.ts
2417
2419
  var _004_rename_bundle_to_source_exports = /* @__PURE__ */ __exportAll({
2418
- down: () => down$38,
2419
- up: () => up$38
2420
+ down: () => down$46,
2421
+ up: () => up$46
2420
2422
  });
2421
- async function up$38(db) {
2423
+ async function up$46(db) {
2422
2424
  await db.schema.alterTable("dispatch_queue").renameColumn("bundle_url", "source_tar_url").execute();
2423
2425
  await db.schema.alterTable("dispatch_queue").renameColumn("bundle_hash", "source_tar_hash").execute();
2424
2426
  }
2425
- async function down$38(db) {
2427
+ async function down$46(db) {
2426
2428
  await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_url", "bundle_url").execute();
2427
2429
  await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_hash", "bundle_hash").execute();
2428
2430
  }
2429
2431
  //#endregion
2430
2432
  //#region src/db/migrations/005_cold_store_chunk_counter.ts
2431
2433
  var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
2432
- down: () => down$37,
2433
- up: () => up$37
2434
+ down: () => down$45,
2435
+ up: () => up$45
2434
2436
  });
2435
2437
  /**
2436
2438
  * Cold-store chunk counter table.
@@ -2447,7 +2449,7 @@ var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
2447
2449
  *
2448
2450
  * sections 5 and 8.
2449
2451
  */
2450
- async function up$37(db) {
2452
+ async function up$45(db) {
2451
2453
  await sql`
2452
2454
  CREATE TABLE public.cold_store_chunk_counts (
2453
2455
  db TEXT NOT NULL,
@@ -2465,14 +2467,14 @@ async function up$37(db) {
2465
2467
  ON public.cold_store_chunk_counts (db, table_name)
2466
2468
  `.execute(db);
2467
2469
  }
2468
- async function down$37(db) {
2470
+ async function down$45(db) {
2469
2471
  await sql`DROP TABLE IF EXISTS public.cold_store_chunk_counts`.execute(db);
2470
2472
  }
2471
2473
  //#endregion
2472
2474
  //#region src/db/migrations/006_runs_jobs_steps_archived_at.ts
2473
2475
  var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
2474
- down: () => down$36,
2475
- up: () => up$36
2476
+ down: () => down$44,
2477
+ up: () => up$44
2476
2478
  });
2477
2479
  /**
2478
2480
  * `execution_runs` / `execution_jobs` / `execution_steps` cold-store
@@ -2508,7 +2510,7 @@ var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
2508
2510
  * - `idx_execution_jobs_routing_key_created (routing_key, created_at)`
2509
2511
  * - `idx_execution_steps_routing_key_created (routing_key, created_at)`
2510
2512
  */
2511
- async function up$36(db) {
2513
+ async function up$44(db) {
2512
2514
  await sql`
2513
2515
  ALTER TABLE public.execution_runs
2514
2516
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2553,7 +2555,7 @@ async function up$36(db) {
2553
2555
  ON public.execution_steps (routing_key, created_at)
2554
2556
  `.execute(db);
2555
2557
  }
2556
- async function down$36(db) {
2558
+ async function down$44(db) {
2557
2559
  await sql`DROP INDEX IF EXISTS public.idx_execution_steps_routing_key_created`.execute(db);
2558
2560
  await sql`
2559
2561
  ALTER TABLE public.execution_steps
@@ -2578,8 +2580,8 @@ async function down$36(db) {
2578
2580
  //#endregion
2579
2581
  //#region src/db/migrations/007_audit_logs_archived_at.ts
2580
2582
  var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
2581
- down: () => down$35,
2582
- up: () => up$35
2583
+ down: () => down$43,
2584
+ up: () => up$43
2583
2585
  });
2584
2586
  /**
2585
2587
  * `secret_audit_log` and `access_log` cold-store schema additions, plus
@@ -2619,7 +2621,7 @@ var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
2619
2621
  * `down()` would not have meaningful retention bounds. Acceptable for
2620
2622
  * staging.
2621
2623
  */
2622
- async function up$35(db) {
2624
+ async function up$43(db) {
2623
2625
  await sql`
2624
2626
  ALTER TABLE public.secret_audit_log
2625
2627
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2640,7 +2642,7 @@ async function up$35(db) {
2640
2642
  DROP COLUMN IF EXISTS expires_at
2641
2643
  `.execute(db);
2642
2644
  }
2643
- async function down$35(db) {
2645
+ async function down$43(db) {
2644
2646
  await sql`
2645
2647
  ALTER TABLE public.access_log
2646
2648
  ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '90 days')
@@ -2668,8 +2670,8 @@ async function down$35(db) {
2668
2670
  //#endregion
2669
2671
  //#region src/db/migrations/008_event_log_archived_at.ts
2670
2672
  var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
2671
- down: () => down$34,
2672
- up: () => up$34
2673
+ down: () => down$42,
2674
+ up: () => up$42
2673
2675
  });
2674
2676
  /**
2675
2677
  * `event_log` cold-store schema additions plus removal of the
@@ -2707,7 +2709,7 @@ var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
2707
2709
  * — best effort; rows inserted between `up()` and a hypothetical
2708
2710
  * `down()` would not have meaningful retention bounds.
2709
2711
  */
2710
- async function up$34(db) {
2712
+ async function up$42(db) {
2711
2713
  await sql`
2712
2714
  ALTER TABLE public.event_log
2713
2715
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2723,7 +2725,7 @@ async function up$34(db) {
2723
2725
  DROP COLUMN IF EXISTS expires_at
2724
2726
  `.execute(db);
2725
2727
  }
2726
- async function down$34(db) {
2728
+ async function down$42(db) {
2727
2729
  await sql`
2728
2730
  ALTER TABLE public.event_log
2729
2731
  ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '30 days')
@@ -2742,8 +2744,8 @@ async function down$34(db) {
2742
2744
  //#endregion
2743
2745
  //#region src/db/migrations/009_access_log_trigram.ts
2744
2746
  var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
2745
- down: () => down$33,
2746
- up: () => up$33
2747
+ down: () => down$41,
2748
+ up: () => up$41
2747
2749
  });
2748
2750
  /**
2749
2751
  * Trigram (pg_trgm) index on access_log.error_message for the federated
@@ -2756,7 +2758,7 @@ var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
2756
2758
  * EXISTS` are both safe to re-run. No CONCURRENTLY because Kysely runs
2757
2759
  * migrations inside a transaction; the lock is brief on a sampled table.
2758
2760
  */
2759
- async function up$33(db) {
2761
+ async function up$41(db) {
2760
2762
  await sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`.execute(db);
2761
2763
  await sql`
2762
2764
  CREATE INDEX IF NOT EXISTS access_log_error_message_trgm_idx
@@ -2765,14 +2767,14 @@ async function up$33(db) {
2765
2767
  WHERE error_message IS NOT NULL
2766
2768
  `.execute(db);
2767
2769
  }
2768
- async function down$33(db) {
2770
+ async function down$41(db) {
2769
2771
  await sql`DROP INDEX IF EXISTS public.access_log_error_message_trgm_idx`.execute(db);
2770
2772
  }
2771
2773
  //#endregion
2772
2774
  //#region src/db/migrations/010_cold_store_chunks.ts
2773
2775
  var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
2774
- down: () => down$32,
2775
- up: () => up$32
2776
+ down: () => down$40,
2777
+ up: () => up$40
2776
2778
  });
2777
2779
  /**
2778
2780
  * Cold-store chunk index — Phase 2 (cold-store purge).
@@ -2804,7 +2806,7 @@ var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
2804
2806
  * forever. Adapters that don't opt into per-bucket archival via
2805
2807
  * `coldTtlDays` don't insert here either.
2806
2808
  */
2807
- async function up$32(db) {
2809
+ async function up$40(db) {
2808
2810
  await sql`
2809
2811
  CREATE TABLE public.cold_store_chunks (
2810
2812
  db TEXT NOT NULL,
@@ -2831,14 +2833,14 @@ async function up$32(db) {
2831
2833
  ON public.cold_store_chunks (db, table_name, tenant_id, archived_at DESC)
2832
2834
  `.execute(db);
2833
2835
  }
2834
- async function down$32(db) {
2836
+ async function down$40(db) {
2835
2837
  await sql`DROP TABLE IF EXISTS public.cold_store_chunks`.execute(db);
2836
2838
  }
2837
2839
  //#endregion
2838
2840
  //#region src/db/migrations/011_drop_source_secrets_notify.ts
2839
2841
  var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
2840
- down: () => down$31,
2841
- up: () => up$31
2842
+ down: () => down$39,
2843
+ up: () => up$39
2842
2844
  });
2843
2845
  /**
2844
2846
  * Drop the `source_secrets_change_trigger` and the
@@ -2857,11 +2859,11 @@ var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
2857
2859
  * in `001_initial.ts`. They wake up no consumer until the `WebhookSecretManager`
2858
2860
  * is restored, so this migration is safe to roll back.
2859
2861
  */
2860
- async function up$31(db) {
2862
+ async function up$39(db) {
2861
2863
  await sql`DROP TRIGGER IF EXISTS source_secrets_change_trigger ON public.scoped_secrets`.execute(db);
2862
2864
  await sql`DROP FUNCTION IF EXISTS public.notify_source_secrets_change() CASCADE`.execute(db);
2863
2865
  }
2864
- async function down$31(db) {
2866
+ async function down$39(db) {
2865
2867
  await sql`
2866
2868
  CREATE OR REPLACE FUNCTION public.notify_source_secrets_change() RETURNS trigger
2867
2869
  LANGUAGE plpgsql
@@ -2900,8 +2902,8 @@ async function down$31(db) {
2900
2902
  //#endregion
2901
2903
  //#region src/db/migrations/012_peer_credentials_active_uniq.ts
2902
2904
  var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
2903
- down: () => down$30,
2904
- up: () => up$30
2905
+ down: () => down$38,
2906
+ up: () => up$38
2905
2907
  });
2906
2908
  /**
2907
2909
  * Add a partial unique index on `peer_credentials (instance_id) WHERE
@@ -2927,7 +2929,7 @@ var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
2927
2929
  * `down()` only drops the index; it does NOT undo the dedupe (there's no
2928
2930
  * safe way to recreate revoked rows, and the dedupe is monotonic).
2929
2931
  */
2930
- async function up$30(db) {
2932
+ async function up$38(db) {
2931
2933
  await sql`
2932
2934
  UPDATE public.peer_credentials
2933
2935
  SET revoked_at = NOW()
@@ -2945,14 +2947,14 @@ async function up$30(db) {
2945
2947
  WHERE revoked_at IS NULL
2946
2948
  `.execute(db);
2947
2949
  }
2948
- async function down$30(db) {
2950
+ async function down$38(db) {
2949
2951
  await sql`DROP INDEX IF EXISTS public.peer_credentials_active_uniq`.execute(db);
2950
2952
  }
2951
2953
  //#endregion
2952
2954
  //#region src/db/migrations/013_execution_log_bytes.ts
2953
2955
  var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
2954
- down: () => down$29,
2955
- up: () => up$29
2956
+ down: () => down$37,
2957
+ up: () => up$37
2956
2958
  });
2957
2959
  /**
2958
2960
  * Add `log_bytes BIGINT NOT NULL DEFAULT 0` columns to `execution_runs` and
@@ -2971,7 +2973,7 @@ var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
2971
2973
  *
2972
2974
  * Idempotent (`ADD COLUMN IF NOT EXISTS`).
2973
2975
  */
2974
- async function up$29(db) {
2976
+ async function up$37(db) {
2975
2977
  await sql`
2976
2978
  ALTER TABLE public.execution_runs
2977
2979
  ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
@@ -2981,15 +2983,15 @@ async function up$29(db) {
2981
2983
  ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
2982
2984
  `.execute(db);
2983
2985
  }
2984
- async function down$29(db) {
2986
+ async function down$37(db) {
2985
2987
  await sql`ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS log_bytes`.execute(db);
2986
2988
  await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS log_bytes`.execute(db);
2987
2989
  }
2988
2990
  //#endregion
2989
2991
  //#region src/db/migrations/014_kici_events_lease_retry.ts
2990
2992
  var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
2991
- down: () => down$28,
2992
- up: () => up$28
2993
+ down: () => down$36,
2994
+ up: () => up$36
2993
2995
  });
2994
2996
  /**
2995
2997
  * Add lease + retry + DLQ columns to `kici_events` so the EventRouter can
@@ -3023,7 +3025,7 @@ var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
3023
3025
  *
3024
3026
  * Idempotent (`ADD COLUMN IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS`).
3025
3027
  */
3026
- async function up$28(db) {
3028
+ async function up$36(db) {
3027
3029
  await sql`
3028
3030
  ALTER TABLE public.kici_events
3029
3031
  ADD COLUMN IF NOT EXISTS claimed_at TIMESTAMPTZ,
@@ -3054,7 +3056,7 @@ async function up$28(db) {
3054
3056
  WHERE dlq_at IS NOT NULL
3055
3057
  `.execute(db);
3056
3058
  }
3057
- async function down$28(db) {
3059
+ async function down$36(db) {
3058
3060
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_dlq`.execute(db);
3059
3061
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_lease_expired`.execute(db);
3060
3062
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_retry_due`.execute(db);
@@ -3072,8 +3074,8 @@ async function down$28(db) {
3072
3074
  //#endregion
3073
3075
  //#region src/db/migrations/015_org_settings_customer_scoped.ts
3074
3076
  var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
3075
- down: () => down$27,
3076
- up: () => up$27
3077
+ down: () => down$35,
3078
+ up: () => up$35
3077
3079
  });
3078
3080
  /**
3079
3081
  * Org-scope `org_settings` and qualify each glob entry by source.
@@ -3101,7 +3103,7 @@ var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
3101
3103
  * Idempotent: a re-run on an already-migrated DB sees `customer_id` exists
3102
3104
  * and the list columns are already jsonb, so it is a no-op.
3103
3105
  */
3104
- async function up$27(db) {
3106
+ async function up$35(db) {
3105
3107
  if ((await sql`
3106
3108
  SELECT EXISTS (
3107
3109
  SELECT 1 FROM information_schema.columns
@@ -3226,7 +3228,7 @@ async function up$27(db) {
3226
3228
  await sql`DROP TABLE _org_settings_merged`.execute(db);
3227
3229
  await sql`DROP TABLE _org_settings_stage`.execute(db);
3228
3230
  }
3229
- async function down$27(db) {
3231
+ async function down$35(db) {
3230
3232
  if (!(await sql`
3231
3233
  SELECT EXISTS (
3232
3234
  SELECT 1 FROM information_schema.columns
@@ -3251,8 +3253,8 @@ async function down$27(db) {
3251
3253
  //#endregion
3252
3254
  //#region src/db/migrations/016_org_settings_allow_http_npm.ts
3253
3255
  var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
3254
- down: () => down$26,
3255
- up: () => up$26
3256
+ down: () => down$34,
3257
+ up: () => up$34
3256
3258
  });
3257
3259
  /**
3258
3260
  * Add `org_settings.allow_http_npm_registries boolean NOT NULL DEFAULT false`.
@@ -3265,7 +3267,7 @@ var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
3265
3267
  *
3266
3268
  * Idempotent: a re-run on a DB that already has the column is a no-op.
3267
3269
  */
3268
- async function up$26(db) {
3270
+ async function up$34(db) {
3269
3271
  if ((await sql`
3270
3272
  SELECT EXISTS (
3271
3273
  SELECT 1 FROM information_schema.columns
@@ -3279,7 +3281,7 @@ async function up$26(db) {
3279
3281
  ADD COLUMN allow_http_npm_registries boolean NOT NULL DEFAULT false
3280
3282
  `.execute(db);
3281
3283
  }
3282
- async function down$26(db) {
3284
+ async function down$34(db) {
3283
3285
  await sql`
3284
3286
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS allow_http_npm_registries
3285
3287
  `.execute(db);
@@ -3287,8 +3289,8 @@ async function down$26(db) {
3287
3289
  //#endregion
3288
3290
  //#region src/db/migrations/017_org_id_widen.ts
3289
3291
  var _017_org_id_widen_exports = /* @__PURE__ */ __exportAll({
3290
- down: () => down$25,
3291
- up: () => up$25
3292
+ down: () => down$33,
3293
+ up: () => up$33
3292
3294
  });
3293
3295
  /**
3294
3296
  * Widen every orchestrator-side `org_id varchar(12)` column to
@@ -3328,17 +3330,17 @@ const ORG_ID_TABLES$1 = [
3328
3330
  "held_runs",
3329
3331
  "scoped_secrets"
3330
3332
  ];
3331
- async function up$25(db) {
3333
+ async function up$33(db) {
3332
3334
  for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(16)`).execute(db);
3333
3335
  }
3334
- async function down$25(db) {
3336
+ async function down$33(db) {
3335
3337
  for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(12)`).execute(db);
3336
3338
  }
3337
3339
  //#endregion
3338
3340
  //#region src/db/migrations/018_org_id_prefix_backfill.ts
3339
3341
  var _018_org_id_prefix_backfill_exports = /* @__PURE__ */ __exportAll({
3340
- down: () => down$24,
3341
- up: () => up$24
3342
+ down: () => down$32,
3343
+ up: () => up$32
3342
3344
  });
3343
3345
  /**
3344
3346
  * Prefix every orchestrator-side tenant string with `org_` to align
@@ -3383,19 +3385,19 @@ const CUSTOMER_ID_TABLES = [
3383
3385
  "workflow_registrations",
3384
3386
  "org_settings"
3385
3387
  ];
3386
- async function up$24(db) {
3388
+ async function up$32(db) {
3387
3389
  for (const table of ORG_ID_TABLES) await sql.raw(`UPDATE public.${table} SET org_id = 'org_' || org_id WHERE org_id <> 'kici-admin' AND org_id NOT LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
3388
3390
  for (const table of CUSTOMER_ID_TABLES) await sql.raw(`UPDATE public.${table} SET customer_id = 'org_' || customer_id WHERE customer_id <> 'kici-admin' AND customer_id NOT LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
3389
3391
  }
3390
- async function down$24(db) {
3392
+ async function down$32(db) {
3391
3393
  for (const table of CUSTOMER_ID_TABLES) await sql.raw(`UPDATE public.${table} SET customer_id = substring(customer_id from 5) WHERE customer_id LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
3392
3394
  for (const table of ORG_ID_TABLES) await sql.raw(`UPDATE public.${table} SET org_id = substring(org_id from 5) WHERE org_id LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
3393
3395
  }
3394
3396
  //#endregion
3395
3397
  //#region src/db/migrations/019_generic_sources_change_notify.ts
3396
3398
  var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
3397
- down: () => down$23,
3398
- up: () => up$23
3399
+ down: () => down$31,
3400
+ up: () => up$31
3399
3401
  });
3400
3402
  /**
3401
3403
  * Add a Postgres trigger on `generic_webhook_sources` that emits
@@ -3418,7 +3420,7 @@ var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
3418
3420
  * (`notify_sources_change()` + `sources_change_trigger`, defined in
3419
3421
  * `001_initial.ts`).
3420
3422
  */
3421
- async function up$23(db) {
3423
+ async function up$31(db) {
3422
3424
  await sql`
3423
3425
  CREATE FUNCTION public.notify_generic_sources_change() RETURNS trigger
3424
3426
  LANGUAGE plpgsql
@@ -3439,15 +3441,15 @@ async function up$23(db) {
3439
3441
  FOR EACH ROW EXECUTE FUNCTION public.notify_generic_sources_change()
3440
3442
  `.execute(db);
3441
3443
  }
3442
- async function down$23(db) {
3444
+ async function down$31(db) {
3443
3445
  await sql`DROP TRIGGER IF EXISTS generic_sources_change_trigger ON public.generic_webhook_sources`.execute(db);
3444
3446
  await sql`DROP FUNCTION IF EXISTS public.notify_generic_sources_change()`.execute(db);
3445
3447
  }
3446
3448
  //#endregion
3447
3449
  //#region src/db/migrations/020_org_settings_dashboard_write_policy.ts
3448
3450
  var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportAll({
3449
- down: () => down$22,
3450
- up: () => up$22
3451
+ down: () => down$30,
3452
+ up: () => up$30
3451
3453
  });
3452
3454
  /**
3453
3455
  * Add `org_settings.dashboard_write_policy jsonb NOT NULL DEFAULT '{}'`.
@@ -3462,7 +3464,7 @@ var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportA
3462
3464
  *
3463
3465
  * Idempotent: a re-run on a DB that already has the column is a no-op.
3464
3466
  */
3465
- async function up$22(db) {
3467
+ async function up$30(db) {
3466
3468
  if ((await sql`
3467
3469
  SELECT EXISTS (
3468
3470
  SELECT 1 FROM information_schema.columns
@@ -3476,7 +3478,7 @@ async function up$22(db) {
3476
3478
  ADD COLUMN dashboard_write_policy jsonb NOT NULL DEFAULT '{}'::jsonb
3477
3479
  `.execute(db);
3478
3480
  }
3479
- async function down$22(db) {
3481
+ async function down$30(db) {
3480
3482
  await sql`
3481
3483
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS dashboard_write_policy
3482
3484
  `.execute(db);
@@ -3484,8 +3486,8 @@ async function down$22(db) {
3484
3486
  //#endregion
3485
3487
  //#region src/db/migrations/021_check_run_tracking.ts
3486
3488
  var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
3487
- down: () => down$21,
3488
- up: () => up$21
3489
+ down: () => down$29,
3490
+ up: () => up$29
3489
3491
  });
3490
3492
  /**
3491
3493
  * Add `check_run_tracking` table for HA-safe check-run state persistence.
@@ -3509,7 +3511,7 @@ var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
3509
3511
  *
3510
3512
  * Idempotent: a re-run on a DB that already has the table is a no-op.
3511
3513
  */
3512
- async function up$21(db) {
3514
+ async function up$29(db) {
3513
3515
  if ((await sql`
3514
3516
  SELECT EXISTS (
3515
3517
  SELECT 1 FROM information_schema.tables
@@ -3540,14 +3542,14 @@ async function up$21(db) {
3540
3542
  WHERE run_id IS NOT NULL
3541
3543
  `.execute(db);
3542
3544
  }
3543
- async function down$21(db) {
3545
+ async function down$29(db) {
3544
3546
  await sql`DROP TABLE IF EXISTS public.check_run_tracking`.execute(db);
3545
3547
  }
3546
3548
  //#endregion
3547
3549
  //#region src/db/migrations/022_scaler_manager_state.ts
3548
3550
  var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
3549
- down: () => down$20,
3550
- up: () => up$20
3551
+ down: () => down$28,
3552
+ up: () => up$28
3551
3553
  });
3552
3554
  /**
3553
3555
  * Add three tables persisting `ScalerManager` per-coord state:
@@ -3574,7 +3576,7 @@ var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
3574
3576
  * Idempotent: a re-run on a DB that already has any of these tables
3575
3577
  * leaves the existing one alone.
3576
3578
  */
3577
- async function up$20(db) {
3579
+ async function up$28(db) {
3578
3580
  const tableExists = async (name) => {
3579
3581
  return (await sql`
3580
3582
  SELECT EXISTS (
@@ -3624,7 +3626,7 @@ async function up$20(db) {
3624
3626
  `.execute(db);
3625
3627
  }
3626
3628
  }
3627
- async function down$20(db) {
3629
+ async function down$28(db) {
3628
3630
  await sql`DROP TABLE IF EXISTS public.scaler_reservations`.execute(db);
3629
3631
  await sql`DROP TABLE IF EXISTS public.scaler_agent_jobs`.execute(db);
3630
3632
  await sql`DROP TABLE IF EXISTS public.scaler_spawning_agents`.execute(db);
@@ -3632,8 +3634,8 @@ async function down$20(db) {
3632
3634
  //#endregion
3633
3635
  //#region src/db/migrations/023_dispatch_queue_recovery_deadline.ts
3634
3636
  var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll({
3635
- down: () => down$19,
3636
- up: () => up$19
3637
+ down: () => down$27,
3638
+ up: () => up$27
3637
3639
  });
3638
3640
  /**
3639
3641
  * Add `dispatch_queue.recovery_deadline TIMESTAMPTZ` and
@@ -3657,7 +3659,7 @@ var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll(
3657
3659
  * Idempotent: re-running on a DB that already has either column is a
3658
3660
  * no-op.
3659
3661
  */
3660
- async function up$19(db) {
3662
+ async function up$27(db) {
3661
3663
  const colExists = async (name) => {
3662
3664
  return (await sql`
3663
3665
  SELECT EXISTS (
@@ -3682,7 +3684,7 @@ async function up$19(db) {
3682
3684
  WHERE recovery_deadline IS NOT NULL
3683
3685
  `.execute(db);
3684
3686
  }
3685
- async function down$19(db) {
3687
+ async function down$27(db) {
3686
3688
  await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_recovery_deadline`.execute(db);
3687
3689
  await sql`
3688
3690
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS recovery_agent_id
@@ -3694,8 +3696,8 @@ async function down$19(db) {
3694
3696
  //#endregion
3695
3697
  //#region src/db/migrations/024_dispatch_queue_provisioning_error.ts
3696
3698
  var _024_dispatch_queue_provisioning_error_exports = /* @__PURE__ */ __exportAll({
3697
- down: () => down$18,
3698
- up: () => up$18
3699
+ down: () => down$26,
3700
+ up: () => up$26
3699
3701
  });
3700
3702
  /**
3701
3703
  * Add `dispatch_queue.last_provisioning_error TEXT` recording the most
@@ -3711,7 +3713,7 @@ var _024_dispatch_queue_provisioning_error_exports = /* @__PURE__ */ __exportAll
3711
3713
  *
3712
3714
  * Idempotent: re-running on a DB that already has the column is a no-op.
3713
3715
  */
3714
- async function up$18(db) {
3716
+ async function up$26(db) {
3715
3717
  const colExists = async (name) => {
3716
3718
  return (await sql`
3717
3719
  SELECT EXISTS (
@@ -3727,7 +3729,7 @@ async function up$18(db) {
3727
3729
  ADD COLUMN last_provisioning_error TEXT
3728
3730
  `.execute(db);
3729
3731
  }
3730
- async function down$18(db) {
3732
+ async function down$26(db) {
3731
3733
  await sql`
3732
3734
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS last_provisioning_error
3733
3735
  `.execute(db);
@@ -3735,8 +3737,8 @@ async function down$18(db) {
3735
3737
  //#endregion
3736
3738
  //#region src/db/migrations/025_init_failure.ts
3737
3739
  var _025_init_failure_exports = /* @__PURE__ */ __exportAll({
3738
- down: () => down$17,
3739
- up: () => up$17
3740
+ down: () => down$25,
3741
+ up: () => up$25
3740
3742
  });
3741
3743
  /**
3742
3744
  * Add `init_failure jsonb` columns to `execution_runs` and `execution_jobs`.
@@ -3750,7 +3752,7 @@ var _025_init_failure_exports = /* @__PURE__ */ __exportAll({
3750
3752
  * Idempotent: re-running on a DB that already has either column is a no-op
3751
3753
  * for that column.
3752
3754
  */
3753
- async function up$17(db) {
3755
+ async function up$25(db) {
3754
3756
  const colExists = async (table, name) => {
3755
3757
  return (await sql`
3756
3758
  SELECT EXISTS (
@@ -3770,7 +3772,7 @@ async function up$17(db) {
3770
3772
  ADD COLUMN init_failure JSONB DEFAULT NULL
3771
3773
  `.execute(db);
3772
3774
  }
3773
- async function down$17(db) {
3775
+ async function down$25(db) {
3774
3776
  await sql`
3775
3777
  ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS init_failure
3776
3778
  `.execute(db);
@@ -3781,8 +3783,8 @@ async function down$17(db) {
3781
3783
  //#endregion
3782
3784
  //#region src/db/migrations/026_event_log_lockfile_corrupt.ts
3783
3785
  var _026_event_log_lockfile_corrupt_exports = /* @__PURE__ */ __exportAll({
3784
- down: () => down$16,
3785
- up: () => up$16
3786
+ down: () => down$24,
3787
+ up: () => up$24
3786
3788
  });
3787
3789
  /**
3788
3790
  * Extend the event_log.status CHECK constraint with 'lockfile_corrupt' so the
@@ -3791,7 +3793,7 @@ var _026_event_log_lockfile_corrupt_exports = /* @__PURE__ */ __exportAll({
3791
3793
  *
3792
3794
  * Idempotent: the DROP ... IF EXISTS / re-ADD pair re-runs cleanly.
3793
3795
  */
3794
- async function up$16(db) {
3796
+ async function up$24(db) {
3795
3797
  await sql`ALTER TABLE event_log DROP CONSTRAINT IF EXISTS event_log_status_check`.execute(db);
3796
3798
  await sql`
3797
3799
  ALTER TABLE event_log ADD CONSTRAINT event_log_status_check
@@ -3801,7 +3803,7 @@ async function up$16(db) {
3801
3803
  ])))
3802
3804
  `.execute(db);
3803
3805
  }
3804
- async function down$16(db) {
3806
+ async function down$24(db) {
3805
3807
  await sql`ALTER TABLE event_log DROP CONSTRAINT IF EXISTS event_log_status_check`.execute(db);
3806
3808
  await sql`
3807
3809
  ALTER TABLE event_log ADD CONSTRAINT event_log_status_check
@@ -3814,8 +3816,8 @@ async function down$16(db) {
3814
3816
  //#endregion
3815
3817
  //#region src/db/migrations/027_workflow_timeout.ts
3816
3818
  var _027_workflow_timeout_exports = /* @__PURE__ */ __exportAll({
3817
- down: () => down$15,
3818
- up: () => up$15
3819
+ down: () => down$23,
3820
+ up: () => up$23
3819
3821
  });
3820
3822
  /**
3821
3823
  * Add `workflow_timeout_ms integer` to `execution_runs`.
@@ -3833,13 +3835,13 @@ var _027_workflow_timeout_exports = /* @__PURE__ */ __exportAll({
3833
3835
  *
3834
3836
  * Idempotent: re-running on a DB that already has the column is a no-op.
3835
3837
  */
3836
- async function up$15(db) {
3838
+ async function up$23(db) {
3837
3839
  await sql`
3838
3840
  ALTER TABLE public.execution_runs
3839
3841
  ADD COLUMN IF NOT EXISTS workflow_timeout_ms INTEGER DEFAULT NULL
3840
3842
  `.execute(db);
3841
3843
  }
3842
- async function down$15(db) {
3844
+ async function down$23(db) {
3843
3845
  await sql`
3844
3846
  ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS workflow_timeout_ms
3845
3847
  `.execute(db);
@@ -3847,8 +3849,8 @@ async function down$15(db) {
3847
3849
  //#endregion
3848
3850
  //#region src/db/migrations/028_org_settings_user_cache.ts
3849
3851
  var _028_org_settings_user_cache_exports = /* @__PURE__ */ __exportAll({
3850
- down: () => down$14,
3851
- up: () => up$14
3852
+ down: () => down$22,
3853
+ up: () => up$22
3852
3854
  });
3853
3855
  /**
3854
3856
  * Add `org_settings.user_cache_quota_bytes bigint` and
@@ -3877,7 +3879,7 @@ async function columnExists(db, column) {
3877
3879
  ) AS exists
3878
3880
  `.execute(db)).rows[0]?.exists ?? false;
3879
3881
  }
3880
- async function up$14(db) {
3882
+ async function up$22(db) {
3881
3883
  if (!await columnExists(db, "user_cache_quota_bytes")) await sql`
3882
3884
  ALTER TABLE public.org_settings
3883
3885
  ADD COLUMN user_cache_quota_bytes bigint
@@ -3887,7 +3889,7 @@ async function up$14(db) {
3887
3889
  ADD COLUMN user_cache_ttl_ms bigint
3888
3890
  `.execute(db);
3889
3891
  }
3890
- async function down$14(db) {
3892
+ async function down$22(db) {
3891
3893
  await sql`
3892
3894
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS user_cache_quota_bytes
3893
3895
  `.execute(db);
@@ -3898,8 +3900,8 @@ async function down$14(db) {
3898
3900
  //#endregion
3899
3901
  //#region src/db/migrations/029_dispatch_queue_attempts.ts
3900
3902
  var _029_dispatch_queue_attempts_exports = /* @__PURE__ */ __exportAll({
3901
- down: () => down$13,
3902
- up: () => up$13
3903
+ down: () => down$21,
3904
+ up: () => up$21
3903
3905
  });
3904
3906
  /**
3905
3907
  * Add `dispatch_queue.dispatch_attempts INT NOT NULL DEFAULT 0`.
@@ -3913,7 +3915,7 @@ var _029_dispatch_queue_attempts_exports = /* @__PURE__ */ __exportAll({
3913
3915
  *
3914
3916
  * Idempotent: re-running on a DB that already has the column is a no-op.
3915
3917
  */
3916
- async function up$13(db) {
3918
+ async function up$21(db) {
3917
3919
  if (!((await sql`
3918
3920
  SELECT EXISTS (
3919
3921
  SELECT 1 FROM information_schema.columns
@@ -3926,7 +3928,7 @@ async function up$13(db) {
3926
3928
  ADD COLUMN dispatch_attempts INT NOT NULL DEFAULT 0
3927
3929
  `.execute(db);
3928
3930
  }
3929
- async function down$13(db) {
3931
+ async function down$21(db) {
3930
3932
  await sql`
3931
3933
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS dispatch_attempts
3932
3934
  `.execute(db);
@@ -3934,8 +3936,8 @@ async function down$13(db) {
3934
3936
  //#endregion
3935
3937
  //#region src/db/migrations/030_held_runs_env_set_null.ts
3936
3938
  var _030_held_runs_env_set_null_exports = /* @__PURE__ */ __exportAll({
3937
- down: () => down$12,
3938
- up: () => up$12
3939
+ down: () => down$20,
3940
+ up: () => up$20
3939
3941
  });
3940
3942
  /**
3941
3943
  * held_runs.environment_id becomes nullable with ON DELETE SET NULL so
@@ -3946,14 +3948,14 @@ var _030_held_runs_env_set_null_exports = /* @__PURE__ */ __exportAll({
3946
3948
  * Idempotent: dropping the NOT NULL and the constraint are both no-ops on a
3947
3949
  * re-run, and the constraint is re-created with the SET NULL action.
3948
3950
  */
3949
- async function up$12(db) {
3951
+ async function up$20(db) {
3950
3952
  await sql`ALTER TABLE public.held_runs ALTER COLUMN environment_id DROP NOT NULL`.execute(db);
3951
3953
  await sql`ALTER TABLE public.held_runs DROP CONSTRAINT IF EXISTS held_runs_environment_id_fkey`.execute(db);
3952
3954
  await sql`ALTER TABLE public.held_runs
3953
3955
  ADD CONSTRAINT held_runs_environment_id_fkey
3954
3956
  FOREIGN KEY (environment_id) REFERENCES public.environments(id) ON DELETE SET NULL`.execute(db);
3955
3957
  }
3956
- async function down$12(db) {
3958
+ async function down$20(db) {
3957
3959
  await sql`ALTER TABLE public.held_runs DROP CONSTRAINT IF EXISTS held_runs_environment_id_fkey`.execute(db);
3958
3960
  await sql`ALTER TABLE public.held_runs
3959
3961
  ADD CONSTRAINT held_runs_environment_id_fkey
@@ -3963,8 +3965,8 @@ async function down$12(db) {
3963
3965
  //#endregion
3964
3966
  //#region src/db/migrations/031_dispatch_queue_ack_deadline.ts
3965
3967
  var _031_dispatch_queue_ack_deadline_exports = /* @__PURE__ */ __exportAll({
3966
- down: () => down$11,
3967
- up: () => up$11
3968
+ down: () => down$19,
3969
+ up: () => up$19
3968
3970
  });
3969
3971
  /**
3970
3972
  * Add `dispatch_queue.ack_deadline TIMESTAMPTZ` and
@@ -3981,7 +3983,7 @@ var _031_dispatch_queue_ack_deadline_exports = /* @__PURE__ */ __exportAll({
3981
3983
  *
3982
3984
  * Idempotent: re-running on a DB that already has either column is a no-op.
3983
3985
  */
3984
- async function up$11(db) {
3986
+ async function up$19(db) {
3985
3987
  const colExists = async (name) => {
3986
3988
  return (await sql`
3987
3989
  SELECT EXISTS (
@@ -4006,7 +4008,7 @@ async function up$11(db) {
4006
4008
  WHERE ack_deadline IS NOT NULL
4007
4009
  `.execute(db);
4008
4010
  }
4009
- async function down$11(db) {
4011
+ async function down$19(db) {
4010
4012
  await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_ack_deadline`.execute(db);
4011
4013
  await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS ack_agent_id`.execute(db);
4012
4014
  await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS ack_deadline`.execute(db);
@@ -4014,8 +4016,8 @@ async function down$11(db) {
4014
4016
  //#endregion
4015
4017
  //#region src/db/migrations/032_org_settings_dispatch_ack_timeout.ts
4016
4018
  var _032_org_settings_dispatch_ack_timeout_exports = /* @__PURE__ */ __exportAll({
4017
- down: () => down$10,
4018
- up: () => up$10
4019
+ down: () => down$18,
4020
+ up: () => up$18
4019
4021
  });
4020
4022
  /**
4021
4023
  * Add `org_settings.dispatch_ack_timeout_ms BIGINT` (nullable).
@@ -4027,7 +4029,7 @@ var _032_org_settings_dispatch_ack_timeout_exports = /* @__PURE__ */ __exportAll
4027
4029
  *
4028
4030
  * Idempotent: a re-run on a DB that already has the column is a no-op.
4029
4031
  */
4030
- async function up$10(db) {
4032
+ async function up$18(db) {
4031
4033
  if ((await sql`
4032
4034
  SELECT EXISTS (
4033
4035
  SELECT 1 FROM information_schema.columns
@@ -4041,7 +4043,7 @@ async function up$10(db) {
4041
4043
  ADD COLUMN dispatch_ack_timeout_ms BIGINT
4042
4044
  `.execute(db);
4043
4045
  }
4044
- async function down$10(db) {
4046
+ async function down$18(db) {
4045
4047
  await sql`
4046
4048
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS dispatch_ack_timeout_ms
4047
4049
  `.execute(db);
@@ -4049,8 +4051,8 @@ async function down$10(db) {
4049
4051
  //#endregion
4050
4052
  //#region src/db/migrations/033_org_settings_approval.ts
4051
4053
  var _033_org_settings_approval_exports = /* @__PURE__ */ __exportAll({
4052
- down: () => down$9,
4053
- up: () => up$9
4054
+ down: () => down$17,
4055
+ up: () => up$17
4054
4056
  });
4055
4057
  /**
4056
4058
  * Add the two approval-policy columns to `org_settings`:
@@ -4067,7 +4069,7 @@ var _033_org_settings_approval_exports = /* @__PURE__ */ __exportAll({
4067
4069
  * and the orchestrator admin route. Idempotent: a re-run on a DB that already
4068
4070
  * has the columns is a no-op (each column is guarded independently).
4069
4071
  */
4070
- async function up$9(db) {
4072
+ async function up$17(db) {
4071
4073
  const colExists = async (column) => {
4072
4074
  return (await sql`
4073
4075
  SELECT EXISTS (
@@ -4087,7 +4089,7 @@ async function up$9(db) {
4087
4089
  ADD COLUMN allow_self_approval BOOLEAN NOT NULL DEFAULT true
4088
4090
  `.execute(db);
4089
4091
  }
4090
- async function down$9(db) {
4092
+ async function down$17(db) {
4091
4093
  await sql`
4092
4094
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS approval_expiry_seconds
4093
4095
  `.execute(db);
@@ -4098,8 +4100,8 @@ async function down$9(db) {
4098
4100
  //#endregion
4099
4101
  //#region src/db/migrations/034_held_runs_generalize.ts
4100
4102
  var _034_held_runs_generalize_exports = /* @__PURE__ */ __exportAll({
4101
- down: () => down$8,
4102
- up: () => up$8
4103
+ down: () => down$16,
4104
+ up: () => up$16
4103
4105
  });
4104
4106
  /**
4105
4107
  * Generalize `held_runs` from an environment-only hold into the unified
@@ -4121,7 +4123,7 @@ var _034_held_runs_generalize_exports = /* @__PURE__ */ __exportAll({
4121
4123
  * New `held_run_approvals` table: one row per approver decision, FK to
4122
4124
  * `held_runs.id` (uuid) with ON DELETE CASCADE.
4123
4125
  */
4124
- async function up$8(db) {
4126
+ async function up$16(db) {
4125
4127
  const colExists = async (column) => {
4126
4128
  return (await sql`
4127
4129
  SELECT EXISTS (
@@ -4154,7 +4156,7 @@ async function up$8(db) {
4154
4156
  ON public.held_run_approvals USING btree (held_run_id)
4155
4157
  `.execute(db);
4156
4158
  }
4157
- async function down$8(db) {
4159
+ async function down$16(db) {
4158
4160
  await sql`DROP TABLE IF EXISTS public.held_run_approvals`.execute(db);
4159
4161
  await sql`ALTER TABLE public.held_runs DROP COLUMN IF EXISTS approval_requirement`.execute(db);
4160
4162
  await sql`ALTER TABLE public.held_runs DROP COLUMN IF EXISTS trigger_source`.execute(db);
@@ -4164,8 +4166,8 @@ async function down$8(db) {
4164
4166
  //#endregion
4165
4167
  //#region src/db/migrations/035_pending_workflow_contexts.ts
4166
4168
  var _035_pending_workflow_contexts_exports = /* @__PURE__ */ __exportAll({
4167
- down: () => down$7,
4168
- up: () => up$7
4169
+ down: () => down$15,
4170
+ up: () => up$15
4169
4171
  });
4170
4172
  /**
4171
4173
  * Pending workflow dispatch context — backs resume of a workflow whose install
@@ -4174,7 +4176,7 @@ var _035_pending_workflow_contexts_exports = /* @__PURE__ */ __exportAll({
4174
4176
  * wait-timer expiry, concurrency slot free). The row is deleted once the resume
4175
4177
  * dispatch has been kicked off.
4176
4178
  */
4177
- async function up$7(db) {
4179
+ async function up$15(db) {
4178
4180
  await sql`
4179
4181
  CREATE TABLE IF NOT EXISTS public.pending_workflow_contexts (
4180
4182
  run_id text PRIMARY KEY,
@@ -4184,14 +4186,14 @@ async function up$7(db) {
4184
4186
  )
4185
4187
  `.execute(db);
4186
4188
  }
4187
- async function down$7(db) {
4189
+ async function down$15(db) {
4188
4190
  await sql`DROP TABLE IF EXISTS public.pending_workflow_contexts`.execute(db);
4189
4191
  }
4190
4192
  //#endregion
4191
4193
  //#region src/db/migrations/036_attestations.ts
4192
4194
  var _036_attestations_exports = /* @__PURE__ */ __exportAll({
4193
- down: () => down$6,
4194
- up: () => up$6
4195
+ down: () => down$14,
4196
+ up: () => up$14
4195
4197
  });
4196
4198
  /**
4197
4199
  * Add the `attestations` table for build-provenance bundles.
@@ -4204,7 +4206,7 @@ var _036_attestations_exports = /* @__PURE__ */ __exportAll({
4204
4206
  *
4205
4207
  * Idempotent: a re-run on a DB that already has the table is a no-op.
4206
4208
  */
4207
- async function up$6(db) {
4209
+ async function up$14(db) {
4208
4210
  if ((await sql`
4209
4211
  SELECT EXISTS (
4210
4212
  SELECT 1 FROM information_schema.tables
@@ -4230,14 +4232,14 @@ async function up$6(db) {
4230
4232
  ON public.attestations (run_id, job_id)
4231
4233
  `.execute(db);
4232
4234
  }
4233
- async function down$6(db) {
4235
+ async function down$14(db) {
4234
4236
  await sql`DROP TABLE IF EXISTS public.attestations`.execute(db);
4235
4237
  }
4236
4238
  //#endregion
4237
4239
  //#region src/db/migrations/037_generic_sources_provider_type_local.ts
4238
4240
  var _037_generic_sources_provider_type_local_exports = /* @__PURE__ */ __exportAll({
4239
- down: () => down$5,
4240
- up: () => up$5
4241
+ down: () => down$13,
4242
+ up: () => up$13
4241
4243
  });
4242
4244
  /**
4243
4245
  * Replace the `generic_webhook_sources.provider_type` CHECK constraint so it
@@ -4254,7 +4256,7 @@ var _037_generic_sources_provider_type_local_exports = /* @__PURE__ */ __exportA
4254
4256
  * Idempotent: the constraint is dropped IF EXISTS and recreated; the data
4255
4257
  * backfill is a plain UPDATE that is a no-op once no `'internal'` rows remain.
4256
4258
  */
4257
- async function up$5(db) {
4259
+ async function up$13(db) {
4258
4260
  await sql`
4259
4261
  ALTER TABLE public.generic_webhook_sources
4260
4262
  DROP CONSTRAINT IF EXISTS generic_webhook_sources_provider_type_check
@@ -4270,7 +4272,7 @@ async function up$5(db) {
4270
4272
  CHECK (provider_type = ANY (ARRAY['generic'::text, 'local'::text]))
4271
4273
  `.execute(db);
4272
4274
  }
4273
- async function down$5(db) {
4275
+ async function down$13(db) {
4274
4276
  await sql`
4275
4277
  ALTER TABLE public.generic_webhook_sources
4276
4278
  DROP CONSTRAINT IF EXISTS generic_webhook_sources_provider_type_check
@@ -4289,8 +4291,8 @@ async function down$5(db) {
4289
4291
  //#endregion
4290
4292
  //#region src/db/migrations/038_remote_sources.ts
4291
4293
  var _038_remote_sources_exports = /* @__PURE__ */ __exportAll({
4292
- down: () => down$4,
4293
- up: () => up$4
4294
+ down: () => down$12,
4295
+ up: () => up$12
4294
4296
  });
4295
4297
  /**
4296
4298
  * `remote_sources` anchors a Platform-relayed `kici run remote` to its real
@@ -4302,7 +4304,7 @@ var _038_remote_sources_exports = /* @__PURE__ */ __exportAll({
4302
4304
  *
4303
4305
  * Idempotent: a re-run on a DB that already has the table is a no-op.
4304
4306
  */
4305
- async function up$4(db) {
4307
+ async function up$12(db) {
4306
4308
  if ((await sql`
4307
4309
  SELECT EXISTS (
4308
4310
  SELECT 1 FROM information_schema.tables
@@ -4321,14 +4323,14 @@ async function up$4(db) {
4321
4323
  )
4322
4324
  `.execute(db);
4323
4325
  }
4324
- async function down$4(db) {
4326
+ async function down$12(db) {
4325
4327
  await sql`DROP TABLE IF EXISTS public.remote_sources`.execute(db);
4326
4328
  }
4327
4329
  //#endregion
4328
4330
  //#region src/db/migrations/039_host_roster.ts
4329
4331
  var _039_host_roster_exports = /* @__PURE__ */ __exportAll({
4330
- down: () => down$3,
4331
- up: () => up$3
4332
+ down: () => down$11,
4333
+ up: () => up$11
4332
4334
  });
4333
4335
  /**
4334
4336
  * `host_roster` is KiCI's declared inventory: one durable row per agent the
@@ -4345,7 +4347,7 @@ var _039_host_roster_exports = /* @__PURE__ */ __exportAll({
4345
4347
  *
4346
4348
  * Idempotent: a re-run on a DB that already has the table is a no-op.
4347
4349
  */
4348
- async function up$3(db) {
4350
+ async function up$11(db) {
4349
4351
  if ((await sql`
4350
4352
  SELECT EXISTS (
4351
4353
  SELECT 1 FROM information_schema.tables
@@ -4375,14 +4377,14 @@ async function up$3(db) {
4375
4377
  await sql`CREATE INDEX idx_host_roster_reap
4376
4378
  ON public.host_roster (lifecycle_class, last_seen)`.execute(db);
4377
4379
  }
4378
- async function down$3(db) {
4380
+ async function down$11(db) {
4379
4381
  await sql`DROP TABLE IF EXISTS public.host_roster`.execute(db);
4380
4382
  }
4381
4383
  //#endregion
4382
4384
  //#region src/db/migrations/040_runsonall_pin.ts
4383
4385
  var _040_runsonall_pin_exports = /* @__PURE__ */ __exportAll({
4384
- down: () => down$2,
4385
- up: () => up$2
4386
+ down: () => down$10,
4387
+ up: () => up$10
4386
4388
  });
4387
4389
  /**
4388
4390
  * Add the `runsOnAll` host fan-out columns:
@@ -4399,7 +4401,7 @@ var _040_runsonall_pin_exports = /* @__PURE__ */ __exportAll({
4399
4401
  *
4400
4402
  * Idempotent: re-running on a DB that already has any column is a no-op.
4401
4403
  */
4402
- async function colExists$2(db, table, name) {
4404
+ async function colExists$5(db, table, name) {
4403
4405
  return (await sql`
4404
4406
  SELECT EXISTS (
4405
4407
  SELECT 1 FROM information_schema.columns
@@ -4409,18 +4411,18 @@ async function colExists$2(db, table, name) {
4409
4411
  ) AS exists
4410
4412
  `.execute(db)).rows[0]?.exists ?? false;
4411
4413
  }
4412
- async function up$2(db) {
4413
- if (!await colExists$2(db, "dispatch_queue", "pinned_agent_id")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN pinned_agent_id TEXT`.execute(db);
4414
- if (!await colExists$2(db, "execution_jobs", "base_job_name")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN base_job_name TEXT`.execute(db);
4415
- if (!await colExists$2(db, "execution_jobs", "variant_kind")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN variant_kind TEXT`.execute(db);
4416
- if (!await colExists$2(db, "execution_jobs", "variant_label")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN variant_label TEXT`.execute(db);
4414
+ async function up$10(db) {
4415
+ if (!await colExists$5(db, "dispatch_queue", "pinned_agent_id")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN pinned_agent_id TEXT`.execute(db);
4416
+ if (!await colExists$5(db, "execution_jobs", "base_job_name")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN base_job_name TEXT`.execute(db);
4417
+ if (!await colExists$5(db, "execution_jobs", "variant_kind")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN variant_kind TEXT`.execute(db);
4418
+ if (!await colExists$5(db, "execution_jobs", "variant_label")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN variant_label TEXT`.execute(db);
4417
4419
  await sql`
4418
4420
  CREATE INDEX IF NOT EXISTS idx_dispatch_queue_pinned_agent
4419
4421
  ON public.dispatch_queue (pinned_agent_id)
4420
4422
  WHERE pinned_agent_id IS NOT NULL
4421
4423
  `.execute(db);
4422
4424
  }
4423
- async function down$2(db) {
4425
+ async function down$10(db) {
4424
4426
  await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_pinned_agent`.execute(db);
4425
4427
  await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS pinned_agent_id`.execute(db);
4426
4428
  await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS variant_label`.execute(db);
@@ -4428,91 +4430,365 @@ async function down$2(db) {
4428
4430
  await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS base_job_name`.execute(db);
4429
4431
  }
4430
4432
  //#endregion
4431
- //#region src/db/migrations/041_wave_gated.ts
4432
- var _041_wave_gated_exports = /* @__PURE__ */ __exportAll({
4433
+ //#region src/db/migrations/041_wave_gated.ts
4434
+ var _041_wave_gated_exports = /* @__PURE__ */ __exportAll({
4435
+ down: () => down$9,
4436
+ up: () => up$9
4437
+ });
4438
+ /**
4439
+ * Add the rolling fan-out wave-gate columns:
4440
+ *
4441
+ * - `execution_jobs.wave_gated boolean NOT NULL DEFAULT false` — when a fan-out
4442
+ * job declares `maxParallel`, children beyond the sliding window are persisted
4443
+ * `wave_gated=true` (held, not enqueued). The dispatch loop skips them; the
4444
+ * wave-scheduler clears the flag one-per-terminal as siblings complete (or, on
4445
+ * `failFast`, skips the held remainder).
4446
+ * - `execution_jobs.wave_max_parallel int` / `wave_fail_fast boolean` — the
4447
+ * base's wave policy, stamped on every fan-out child so the wave-scheduler can
4448
+ * read it at terminal time without re-fetching the lock file (the tracker has
4449
+ * no lock access). NULL for any job not part of a bounded wave.
4450
+ *
4451
+ * A composite index on (run_id, base_job_name, wave_gated) supports the
4452
+ * wave-scheduler's "next held sibling of this base" lookups.
4453
+ *
4454
+ * Idempotent: re-running on a DB that already has the columns is a no-op.
4455
+ */
4456
+ async function colExists$4(db, table, name) {
4457
+ return (await sql`
4458
+ SELECT EXISTS (
4459
+ SELECT 1 FROM information_schema.columns
4460
+ WHERE table_schema = 'public'
4461
+ AND table_name = ${table}
4462
+ AND column_name = ${name}
4463
+ ) AS exists
4464
+ `.execute(db)).rows[0]?.exists ?? false;
4465
+ }
4466
+ async function up$9(db) {
4467
+ if (!await colExists$4(db, "execution_jobs", "wave_gated")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_gated boolean NOT NULL DEFAULT false`.execute(db);
4468
+ if (!await colExists$4(db, "execution_jobs", "wave_max_parallel")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_max_parallel integer`.execute(db);
4469
+ if (!await colExists$4(db, "execution_jobs", "wave_fail_fast")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_fail_fast boolean`.execute(db);
4470
+ await sql`
4471
+ CREATE INDEX IF NOT EXISTS idx_execution_jobs_wave
4472
+ ON public.execution_jobs (run_id, base_job_name, wave_gated)
4473
+ `.execute(db);
4474
+ }
4475
+ async function down$9(db) {
4476
+ await sql`DROP INDEX IF EXISTS public.idx_execution_jobs_wave`.execute(db);
4477
+ await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_fail_fast`.execute(db);
4478
+ await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_max_parallel`.execute(db);
4479
+ await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_gated`.execute(db);
4480
+ }
4481
+ //#endregion
4482
+ //#region src/db/migrations/042_dispatch_queue_patterns.ts
4483
+ var _042_dispatch_queue_patterns_exports = /* @__PURE__ */ __exportAll({
4484
+ down: () => down$8,
4485
+ up: () => up$8
4486
+ });
4487
+ /**
4488
+ * Add pattern columns to dispatch_queue. Exact labels stay in runs_on_labels /
4489
+ * exclude_labels (the SQL @> prefilter); regex matchers go here and are applied
4490
+ * as a JS post-filter at drain time, since Postgres `~` regex semantics differ
4491
+ * from JavaScript `RegExp` and the engine's `matcherSatisfiedBy` is the single
4492
+ * matching authority.
4493
+ *
4494
+ * - `runs_on_patterns jsonb NOT NULL DEFAULT '[]'` — regex matchers the agent's
4495
+ * labels must satisfy.
4496
+ * - `exclude_patterns jsonb NOT NULL DEFAULT '[]'` — regex matchers that
4497
+ * disqualify an agent.
4498
+ *
4499
+ * Idempotent: re-running on a DB that already has the columns is a no-op.
4500
+ */
4501
+ async function colExists$3(db, table, name) {
4502
+ return (await sql`
4503
+ SELECT EXISTS (
4504
+ SELECT 1 FROM information_schema.columns
4505
+ WHERE table_schema = 'public'
4506
+ AND table_name = ${table}
4507
+ AND column_name = ${name}
4508
+ ) AS exists
4509
+ `.execute(db)).rows[0]?.exists ?? false;
4510
+ }
4511
+ async function up$8(db) {
4512
+ if (!await colExists$3(db, "dispatch_queue", "runs_on_patterns")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN runs_on_patterns jsonb NOT NULL DEFAULT '[]'::jsonb`.execute(db);
4513
+ if (!await colExists$3(db, "dispatch_queue", "exclude_patterns")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN exclude_patterns jsonb NOT NULL DEFAULT '[]'::jsonb`.execute(db);
4514
+ }
4515
+ async function down$8(db) {
4516
+ await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS exclude_patterns`.execute(db);
4517
+ await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS runs_on_patterns`.execute(db);
4518
+ }
4519
+ //#endregion
4520
+ //#region src/db/migrations/043_rerouted_to_peer.ts
4521
+ var _043_rerouted_to_peer_exports = /* @__PURE__ */ __exportAll({
4522
+ down: () => down$7,
4523
+ up: () => up$7
4524
+ });
4525
+ /**
4526
+ * Add a nullable `rerouted_to_peer text` marker to execution_jobs. Non-null
4527
+ * means the job was rerouted to a remote worker peer (the value is that
4528
+ * peer's instance id); NULL means the job runs locally. The run-recovery
4529
+ * sweepers read it so a rerouted job whose worker is still connected is not
4530
+ * prematurely failed.
4531
+ *
4532
+ * Idempotent: re-running on a DB that already has the column is a no-op.
4533
+ */
4534
+ async function colExists$2(db, table, name) {
4535
+ return (await sql`
4536
+ SELECT EXISTS (
4537
+ SELECT 1 FROM information_schema.columns
4538
+ WHERE table_schema = 'public'
4539
+ AND table_name = ${table}
4540
+ AND column_name = ${name}
4541
+ ) AS exists
4542
+ `.execute(db)).rows[0]?.exists ?? false;
4543
+ }
4544
+ async function up$7(db) {
4545
+ if (!await colExists$2(db, "execution_jobs", "rerouted_to_peer")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN rerouted_to_peer text`.execute(db);
4546
+ }
4547
+ async function down$7(db) {
4548
+ await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS rerouted_to_peer`.execute(db);
4549
+ }
4550
+ //#endregion
4551
+ //#region src/db/migrations/044_check_mode.ts
4552
+ var _044_check_mode_exports = /* @__PURE__ */ __exportAll({
4553
+ down: () => down$6,
4554
+ up: () => up$6
4555
+ });
4556
+ /**
4557
+ * Add idempotent check-mode columns:
4558
+ *
4559
+ * - `execution_runs.check_mode text` — the run's mode (`apply` | `check` |
4560
+ * `check-fail-on-drift`). NULL means a legacy/apply run.
4561
+ * - `execution_steps.check_outcome text` — the per-step `CheckStepOutcome`
4562
+ * (`skipped` | `applied` | `declined` | `dry-run` | `no_check`). NULL when
4563
+ * the step ran without a check mode.
4564
+ * - `execution_steps.drift_summary text` — the human-readable `summarize(drift)`
4565
+ * line. NULL when no drift.
4566
+ * - `execution_steps.drift jsonb` — the structured drift value. NULL when no drift.
4567
+ *
4568
+ * All nullable. Idempotent: re-running on a DB that already has the columns is
4569
+ * a no-op.
4570
+ */
4571
+ async function colExists$1(db, table, name) {
4572
+ return (await sql`
4573
+ SELECT EXISTS (
4574
+ SELECT 1 FROM information_schema.columns
4575
+ WHERE table_schema = 'public'
4576
+ AND table_name = ${table}
4577
+ AND column_name = ${name}
4578
+ ) AS exists
4579
+ `.execute(db)).rows[0]?.exists ?? false;
4580
+ }
4581
+ async function up$6(db) {
4582
+ if (!await colExists$1(db, "execution_runs", "check_mode")) await sql`ALTER TABLE public.execution_runs ADD COLUMN check_mode text`.execute(db);
4583
+ if (!await colExists$1(db, "execution_steps", "check_outcome")) await sql`ALTER TABLE public.execution_steps ADD COLUMN check_outcome text`.execute(db);
4584
+ if (!await colExists$1(db, "execution_steps", "drift_summary")) await sql`ALTER TABLE public.execution_steps ADD COLUMN drift_summary text`.execute(db);
4585
+ if (!await colExists$1(db, "execution_steps", "drift")) await sql`ALTER TABLE public.execution_steps ADD COLUMN drift jsonb`.execute(db);
4586
+ }
4587
+ async function down$6(db) {
4588
+ await sql`ALTER TABLE public.execution_steps DROP COLUMN IF EXISTS drift`.execute(db);
4589
+ await sql`ALTER TABLE public.execution_steps DROP COLUMN IF EXISTS drift_summary`.execute(db);
4590
+ await sql`ALTER TABLE public.execution_steps DROP COLUMN IF EXISTS check_outcome`.execute(db);
4591
+ await sql`ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS check_mode`.execute(db);
4592
+ }
4593
+ //#endregion
4594
+ //#region src/db/migrations/045_host_properties.ts
4595
+ var _045_host_properties_exports = /* @__PURE__ */ __exportAll({
4596
+ down: () => down$5,
4597
+ up: () => up$5
4598
+ });
4599
+ /**
4600
+ * Add the typed host-vars dimension to the host roster:
4601
+ *
4602
+ * - `host_roster.host_properties jsonb NOT NULL DEFAULT '{}'` — the typed
4603
+ * property bag (`string | number | boolean` values) reported by the agent at
4604
+ * registration and/or declared by the operator (`kici-admin host declare
4605
+ * --prop`). Labels stay the flat-string grouping dimension; properties are
4606
+ * the separate queryable host-vars dimension.
4607
+ *
4608
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`): re-running on a DB that already has
4609
+ * the column is a no-op. Staging data is preserved (additive column).
4610
+ */
4611
+ async function up$5(db) {
4612
+ await sql`ALTER TABLE public.host_roster
4613
+ ADD COLUMN IF NOT EXISTS host_properties jsonb NOT NULL DEFAULT '{}'::jsonb`.execute(db);
4614
+ }
4615
+ async function down$5(db) {
4616
+ await sql`ALTER TABLE public.host_roster DROP COLUMN IF EXISTS host_properties`.execute(db);
4617
+ }
4618
+ //#endregion
4619
+ //#region src/db/migrations/046_join_token_consumed_by_instance.ts
4620
+ var _046_join_token_consumed_by_instance_exports = /* @__PURE__ */ __exportAll({
4621
+ down: () => down$4,
4622
+ up: () => up$4
4623
+ });
4624
+ /**
4625
+ * Record the joining peer's instanceId at first consumption of a join token:
4626
+ *
4627
+ * - `join_tokens.consumed_by_instance text` — the `instanceId` of the peer that
4628
+ * consumed the token. This is the *joining peer*, distinct from `consumed_by`
4629
+ * (the coordinator that processed the claim). It lets a token be reused by the
4630
+ * same peer instance until `expires_at`, so a peer that lost its credential
4631
+ * (transient outage / deleted credential file) self-heals by re-presenting the
4632
+ * join token already in its env — no operator action, no cluster redeploy.
4633
+ *
4634
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`): re-running on a DB that already has
4635
+ * the column is a no-op. Staging data is preserved (additive nullable column).
4636
+ */
4637
+ async function up$4(db) {
4638
+ await sql`ALTER TABLE public.join_tokens
4639
+ ADD COLUMN IF NOT EXISTS consumed_by_instance text`.execute(db);
4640
+ }
4641
+ async function down$4(db) {
4642
+ await sql`ALTER TABLE public.join_tokens DROP COLUMN IF EXISTS consumed_by_instance`.execute(db);
4643
+ }
4644
+ //#endregion
4645
+ //#region src/db/migrations/047_needs_run_on.ts
4646
+ var _047_needs_run_on_exports = /* @__PURE__ */ __exportAll({
4647
+ down: () => down$3,
4648
+ up: () => up$3
4649
+ });
4650
+ /**
4651
+ * Migrate `execution_job_needs` from the binary `if_failed` policy to a
4652
+ * programmable `run_on` status-set:
4653
+ *
4654
+ * - Add `run_on text` (JSON-encoded array of upstream terminal statuses) with a
4655
+ * default of `'["success"]'`.
4656
+ * - Backfill from the existing `if_failed`: `skip` → `["success"]`,
4657
+ * `run` → every terminal job status.
4658
+ * - Drop `if_failed`.
4659
+ *
4660
+ * Idempotent: column-exists guarded so re-running on a DB that already has
4661
+ * `run_on` (and no `if_failed`) is a no-op. Existing rows are preserved
4662
+ * (staging data is not dropped).
4663
+ */
4664
+ const ALL_TERMINAL_JSON = JSON.stringify([
4665
+ "success",
4666
+ "failed",
4667
+ "cancelled",
4668
+ "skipped",
4669
+ "timed_out_stale",
4670
+ "drift_dropped"
4671
+ ]);
4672
+ const SUCCESS_ONLY_JSON = JSON.stringify(["success"]);
4673
+ async function colExists(db, table, name) {
4674
+ return (await sql`
4675
+ SELECT EXISTS (
4676
+ SELECT 1 FROM information_schema.columns
4677
+ WHERE table_schema = 'public'
4678
+ AND table_name = ${table}
4679
+ AND column_name = ${name}
4680
+ ) AS exists
4681
+ `.execute(db)).rows[0]?.exists ?? false;
4682
+ }
4683
+ const SUCCESS_ONLY_DEFAULT = `'${SUCCESS_ONLY_JSON}'`;
4684
+ async function up$3(db) {
4685
+ if (!await colExists(db, "execution_job_needs", "run_on")) await sql`
4686
+ ALTER TABLE public.execution_job_needs
4687
+ ADD COLUMN run_on text NOT NULL DEFAULT ${sql.raw(SUCCESS_ONLY_DEFAULT)}
4688
+ `.execute(db);
4689
+ if (await colExists(db, "execution_job_needs", "if_failed")) {
4690
+ await sql`
4691
+ UPDATE public.execution_job_needs
4692
+ SET run_on = ${ALL_TERMINAL_JSON}
4693
+ WHERE if_failed = 'run'
4694
+ `.execute(db);
4695
+ await sql`
4696
+ UPDATE public.execution_job_needs
4697
+ SET run_on = ${SUCCESS_ONLY_JSON}
4698
+ WHERE if_failed = 'skip'
4699
+ `.execute(db);
4700
+ await sql`ALTER TABLE public.execution_job_needs DROP COLUMN if_failed`.execute(db);
4701
+ }
4702
+ }
4703
+ async function down$3(db) {
4704
+ if (!await colExists(db, "execution_job_needs", "if_failed")) await sql`
4705
+ ALTER TABLE public.execution_job_needs
4706
+ ADD COLUMN if_failed text NOT NULL DEFAULT 'skip'
4707
+ `.execute(db);
4708
+ if (await colExists(db, "execution_job_needs", "run_on")) {
4709
+ await sql`
4710
+ UPDATE public.execution_job_needs
4711
+ SET if_failed = CASE WHEN run_on = ${SUCCESS_ONLY_JSON} THEN 'skip' ELSE 'run' END
4712
+ `.execute(db);
4713
+ await sql`ALTER TABLE public.execution_job_needs DROP COLUMN run_on`.execute(db);
4714
+ }
4715
+ }
4716
+ //#endregion
4717
+ //#region src/db/migrations/048_host_reboot_pending.ts
4718
+ var _048_host_reboot_pending_exports = /* @__PURE__ */ __exportAll({
4719
+ down: () => down$2,
4720
+ up: () => up$2
4721
+ });
4722
+ /**
4723
+ * Add `host_roster.reboot_pending_until timestamptz NULL` — the persisted
4724
+ * reboot-pending flag for workflow-level host restart.
4725
+ *
4726
+ * Set when an agent's `restartHost()` step calls `host.requestReboot` (so it
4727
+ * survives an orchestrator restart during the host's reboot window). While the
4728
+ * value is in the future it (1) makes the agent's imminent disconnect an
4729
+ * expected reboot rather than a recovery-fail, (2) gates the pinned-drain off
4730
+ * so the post-restart job is not dispatched into the about-to-reboot box, and
4731
+ * (3) clears on the next reconnect (down-then-up), releasing the held job. NULL
4732
+ * = no reboot pending.
4733
+ *
4734
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
4735
+ * preserved.
4736
+ */
4737
+ async function up$2(db) {
4738
+ await sql`ALTER TABLE public.host_roster
4739
+ ADD COLUMN IF NOT EXISTS reboot_pending_until timestamptz`.execute(db);
4740
+ }
4741
+ async function down$2(db) {
4742
+ await sql`ALTER TABLE public.host_roster DROP COLUMN IF EXISTS reboot_pending_until`.execute(db);
4743
+ }
4744
+ //#endregion
4745
+ //#region src/db/migrations/049_held_runs_payload.ts
4746
+ var _049_held_runs_payload_exports = /* @__PURE__ */ __exportAll({
4433
4747
  down: () => down$1,
4434
4748
  up: () => up$1
4435
4749
  });
4436
4750
  /**
4437
- * Add the rolling fan-out wave-gate columns:
4438
- *
4439
- * - `execution_jobs.wave_gated boolean NOT NULL DEFAULT false` when a fan-out
4440
- * job declares `maxParallel`, children beyond the sliding window are persisted
4441
- * `wave_gated=true` (held, not enqueued). The dispatch loop skips them; the
4442
- * wave-scheduler clears the flag one-per-terminal as siblings complete (or, on
4443
- * `failFast`, skips the held remainder).
4444
- * - `execution_jobs.wave_max_parallel int` / `wave_fail_fast boolean` — the
4445
- * base's wave policy, stamped on every fan-out child so the wave-scheduler can
4446
- * read it at terminal time without re-fetching the lock file (the tracker has
4447
- * no lock access). NULL for any job not part of a bounded wave.
4448
- *
4449
- * A composite index on (run_id, base_job_name, wave_gated) supports the
4450
- * wave-scheduler's "next held sibling of this base" lookups.
4751
+ * Add `held_runs.payload jsonb NULL` — the drift payload captured when a
4752
+ * `when: 'drift'` step-approval gate fires. Holds `{ summaryMarkdown, drift }`:
4753
+ * the author's `summarize(drift)` rendering plus the structured drift blob, so
4754
+ * the dashboard approval queue and the CLI render the computed diff the
4755
+ * operator approves. NULL for every non-drift hold.
4451
4756
  *
4452
- * Idempotent: re-running on a DB that already has the columns is a no-op.
4757
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
4758
+ * preserved.
4453
4759
  */
4454
- async function colExists$1(db, table, name) {
4455
- return (await sql`
4456
- SELECT EXISTS (
4457
- SELECT 1 FROM information_schema.columns
4458
- WHERE table_schema = 'public'
4459
- AND table_name = ${table}
4460
- AND column_name = ${name}
4461
- ) AS exists
4462
- `.execute(db)).rows[0]?.exists ?? false;
4463
- }
4464
4760
  async function up$1(db) {
4465
- if (!await colExists$1(db, "execution_jobs", "wave_gated")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_gated boolean NOT NULL DEFAULT false`.execute(db);
4466
- if (!await colExists$1(db, "execution_jobs", "wave_max_parallel")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_max_parallel integer`.execute(db);
4467
- if (!await colExists$1(db, "execution_jobs", "wave_fail_fast")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_fail_fast boolean`.execute(db);
4468
- await sql`
4469
- CREATE INDEX IF NOT EXISTS idx_execution_jobs_wave
4470
- ON public.execution_jobs (run_id, base_job_name, wave_gated)
4471
- `.execute(db);
4761
+ await sql`ALTER TABLE public.held_runs
4762
+ ADD COLUMN IF NOT EXISTS payload jsonb`.execute(db);
4472
4763
  }
4473
4764
  async function down$1(db) {
4474
- await sql`DROP INDEX IF EXISTS public.idx_execution_jobs_wave`.execute(db);
4475
- await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_fail_fast`.execute(db);
4476
- await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_max_parallel`.execute(db);
4477
- await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_gated`.execute(db);
4765
+ await sql`ALTER TABLE public.held_runs DROP COLUMN IF EXISTS payload`.execute(db);
4478
4766
  }
4479
4767
  //#endregion
4480
- //#region src/db/migrations/042_dispatch_queue_patterns.ts
4481
- var _042_dispatch_queue_patterns_exports = /* @__PURE__ */ __exportAll({
4768
+ //#region src/db/migrations/050_sources_slug.ts
4769
+ var _050_sources_slug_exports = /* @__PURE__ */ __exportAll({
4482
4770
  down: () => down,
4483
4771
  up: () => up
4484
4772
  });
4485
4773
  /**
4486
- * Add pattern columns to dispatch_queue. Exact labels stay in runs_on_labels /
4487
- * exclude_labels (the SQL @> prefilter); regex matchers go here and are applied
4488
- * as a JS post-filter at drain time, since Postgres `~` regex semantics differ
4489
- * from JavaScript `RegExp` and the engine's `matcherSatisfiedBy` is the single
4490
- * matching authority.
4774
+ * Add `sources.slug TEXT NULL` the GitHub App slug (the URL-safe identifier
4775
+ * GitHub assigns, e.g. `my-kici-app`).
4491
4776
  *
4492
- * - `runs_on_patterns jsonb NOT NULL DEFAULT '[]'` regex matchers the agent's
4493
- * labels must satisfy.
4494
- * - `exclude_patterns jsonb NOT NULL DEFAULT '[]'` regex matchers that
4495
- * disqualify an agent.
4777
+ * For GitHub-App sources GitHub is the source of truth for both the display
4778
+ * `name` and the `slug`: both are captured at creation and kept fresh by the
4779
+ * daily refresher + `kici-admin source refresh`. NULL when the identity fetch
4780
+ * hasn't populated it yet (manual `--app-id` flow whose initial fetch failed,
4781
+ * or a row created before the rollout).
4496
4782
  *
4497
- * Idempotent: re-running on a DB that already has the columns is a no-op.
4783
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
4784
+ * preserved.
4498
4785
  */
4499
- async function colExists(db, table, name) {
4500
- return (await sql`
4501
- SELECT EXISTS (
4502
- SELECT 1 FROM information_schema.columns
4503
- WHERE table_schema = 'public'
4504
- AND table_name = ${table}
4505
- AND column_name = ${name}
4506
- ) AS exists
4507
- `.execute(db)).rows[0]?.exists ?? false;
4508
- }
4509
4786
  async function up(db) {
4510
- if (!await colExists(db, "dispatch_queue", "runs_on_patterns")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN runs_on_patterns jsonb NOT NULL DEFAULT '[]'::jsonb`.execute(db);
4511
- if (!await colExists(db, "dispatch_queue", "exclude_patterns")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN exclude_patterns jsonb NOT NULL DEFAULT '[]'::jsonb`.execute(db);
4787
+ await sql`ALTER TABLE public.sources
4788
+ ADD COLUMN IF NOT EXISTS slug text`.execute(db);
4512
4789
  }
4513
4790
  async function down(db) {
4514
- await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS exclude_patterns`.execute(db);
4515
- await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS runs_on_patterns`.execute(db);
4791
+ await sql`ALTER TABLE public.sources DROP COLUMN IF EXISTS slug`.execute(db);
4516
4792
  }
4517
4793
  //#endregion
4518
4794
  //#region src/db/migration-provider.ts
@@ -4560,7 +4836,15 @@ function createMigrationProvider() {
4560
4836
  "039_host_roster": _039_host_roster_exports,
4561
4837
  "040_runsonall_pin": _040_runsonall_pin_exports,
4562
4838
  "041_wave_gated": _041_wave_gated_exports,
4563
- "042_dispatch_queue_patterns": _042_dispatch_queue_patterns_exports
4839
+ "042_dispatch_queue_patterns": _042_dispatch_queue_patterns_exports,
4840
+ "043_rerouted_to_peer": _043_rerouted_to_peer_exports,
4841
+ "044_check_mode": _044_check_mode_exports,
4842
+ "045_host_properties": _045_host_properties_exports,
4843
+ "046_join_token_consumed_by_instance": _046_join_token_consumed_by_instance_exports,
4844
+ "047_needs_run_on": _047_needs_run_on_exports,
4845
+ "048_host_reboot_pending": _048_host_reboot_pending_exports,
4846
+ "049_held_runs_payload": _049_held_runs_payload_exports,
4847
+ "050_sources_slug": _050_sources_slug_exports
4564
4848
  };
4565
4849
  } };
4566
4850
  }
@@ -5187,6 +5471,419 @@ function installPostReceiveHook(repoPath, script) {
5187
5471
  return hookPath;
5188
5472
  }
5189
5473
  //#endregion
5474
+ //#region src/providers/github/auth.ts
5475
+ /**
5476
+ * GitHub clone token provider and authentication utilities.
5477
+ *
5478
+ * Implements the CloneTokenProvider interface from @kici-dev/engine for GitHub.
5479
+ * Uses GitHub App installation tokens for repository access.
5480
+ */
5481
+ /**
5482
+ * Create an Octokit instance authenticated as a GitHub App installation.
5483
+ *
5484
+ * Uses @octokit/auth-app to handle:
5485
+ * - JWT generation for app-level authentication
5486
+ * - Installation token creation and automatic refresh
5487
+ *
5488
+ * Each call creates a new Octokit instance scoped to the specific installation.
5489
+ * The auth-app strategy handles token caching internally per Octokit instance.
5490
+ *
5491
+ * @param config - GitHub App credentials (appId + privateKey)
5492
+ * @param installationId - GitHub App installation ID for the target account/org
5493
+ * @returns Octokit instance with auto-refreshing installation tokens
5494
+ */
5495
+ function createInstallationOctokit(config, installationId) {
5496
+ return new Octokit({
5497
+ authStrategy: createAppAuth,
5498
+ auth: {
5499
+ appId: config.appId,
5500
+ privateKey: config.privateKey,
5501
+ installationId
5502
+ }
5503
+ });
5504
+ }
5505
+ //#endregion
5506
+ //#region src/providers/github/manifest.ts
5507
+ /**
5508
+ * GitHub App Manifest flow helpers. The manifest encodes KiCI's exact
5509
+ * permissions + events + webhook config so the operator never picks them by
5510
+ * hand — GitHub creates a correctly-configured App from this object in one
5511
+ * click. See docs.github.com "Registering a GitHub App from a manifest".
5512
+ *
5513
+ * The webhook secret is NOT part of the manifest: GitHub generates it during
5514
+ * registration and returns it on the conversion response, so both GitHub and
5515
+ * the Platform end up sharing the same secret with zero operator effort.
5516
+ */
5517
+ /**
5518
+ * Validate a self-hosted webhook URL supplied via `source add github
5519
+ * --webhook-url`. Must be a well-formed absolute `https://` URL. Returns the URL
5520
+ * verbatim on success; throws a clear error otherwise. The validated URL is
5521
+ * baked into `manifest.hook_attributes.url` as-is — KiCI adds no ingress and
5522
+ * does not receive events at it; the operator owns delivery.
5523
+ */
5524
+ function validateWebhookUrl(value) {
5525
+ let url;
5526
+ try {
5527
+ url = new URL(value);
5528
+ } catch {
5529
+ throw new Error(`--webhook-url must be a valid absolute URL: ${value}`);
5530
+ }
5531
+ if (url.protocol !== "https:") throw new Error(`--webhook-url must be an https:// URL (got ${url.protocol}//…)`);
5532
+ return value;
5533
+ }
5534
+ function buildGithubAppManifest(input) {
5535
+ return {
5536
+ name: input.name,
5537
+ url: "https://kici.dev",
5538
+ hook_attributes: {
5539
+ url: input.webhookUrl,
5540
+ active: true
5541
+ },
5542
+ redirect_url: input.redirectUrl,
5543
+ ...input.setupUrl ? { setup_url: input.setupUrl } : {},
5544
+ public: false,
5545
+ default_permissions: {
5546
+ contents: "read",
5547
+ metadata: "read",
5548
+ pull_requests: "read",
5549
+ checks: "write",
5550
+ members: "read"
5551
+ },
5552
+ default_events: [
5553
+ "push",
5554
+ "pull_request",
5555
+ "check_run",
5556
+ "check_suite"
5557
+ ]
5558
+ };
5559
+ }
5560
+ /**
5561
+ * Exchange the short-lived manifest `code` for the App's id, private key, and
5562
+ * webhook secret. Runs server-to-server directly against GitHub — the private
5563
+ * key never transits the Platform.
5564
+ */
5565
+ async function convertManifestCode(code, deps = {}) {
5566
+ const { data } = await (deps.octokit ?? new Octokit()).request("POST /app-manifests/{code}/conversions", { code });
5567
+ const d = data;
5568
+ if (!d.webhook_secret) throw new Error("GitHub returned no webhook secret for the new App — cannot verify inbound events. Re-run the setup, or configure a webhook secret manually with source update.");
5569
+ return {
5570
+ appId: String(d.id),
5571
+ slug: d.slug,
5572
+ name: d.name,
5573
+ privateKey: d.pem,
5574
+ webhookSecret: d.webhook_secret,
5575
+ clientId: d.client_id,
5576
+ clientSecret: d.client_secret,
5577
+ htmlUrl: d.html_url
5578
+ };
5579
+ }
5580
+ function appOctokitFor(creds) {
5581
+ return new Octokit({
5582
+ authStrategy: createAppAuth,
5583
+ auth: {
5584
+ appId: creds.appId,
5585
+ privateKey: creds.privateKey
5586
+ }
5587
+ });
5588
+ }
5589
+ /**
5590
+ * Poll GitHub (as the App, via a JWT) until at least one installation exists,
5591
+ * returning the first installation's id + account login. Throws on timeout.
5592
+ */
5593
+ async function waitForInstallation(creds, opts) {
5594
+ const now = opts.now ?? Date.now;
5595
+ const octokit = opts.appOctokit ?? appOctokitFor(creds);
5596
+ const deadline = now() + opts.timeoutMs;
5597
+ for (;;) {
5598
+ const { data } = await octokit.request("GET /app/installations", { per_page: 1 });
5599
+ const installs = data;
5600
+ if (Array.isArray(installs) && installs.length > 0) return {
5601
+ installationId: installs[0].id,
5602
+ accountLogin: installs[0].account?.login ?? ""
5603
+ };
5604
+ if (now() >= deadline) throw new Error("Timed out waiting for the GitHub App to be installed");
5605
+ await new Promise((r) => setTimeout(r, opts.pollMs));
5606
+ }
5607
+ }
5608
+ /**
5609
+ * Mint an installation token from the captured private key and confirm the App
5610
+ * can reach repos — proves the key works end-to-end (the same path the agent's
5611
+ * clone uses at runtime).
5612
+ */
5613
+ async function verifyRepoAccess(creds, installationId, deps = {}) {
5614
+ const { data } = await (deps.octokit ?? createInstallationOctokit(creds, installationId)).request("GET /installation/repositories", { per_page: 1 });
5615
+ return { repoCount: data.total_count };
5616
+ }
5617
+ //#endregion
5618
+ //#region src/providers/github/manifest-form.ts
5619
+ /**
5620
+ * GitHub's create-from-manifest flow is an HTML form POST (the `manifest`
5621
+ * JSON cannot ride in a query string). These helpers render the auto-submitting
5622
+ * form and the headless code-display page. Pure string builders — no IO — so
5623
+ * both the CLI loopback server and the static marketing-site page can reuse the
5624
+ * same shapes.
5625
+ */
5626
+ function escapeHtml(s) {
5627
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
5628
+ }
5629
+ function manifestCreateUrl(githubOrg) {
5630
+ return githubOrg ? `https://github.com/organizations/${githubOrg}/settings/apps/new` : "https://github.com/settings/apps/new";
5631
+ }
5632
+ function renderManifestFormHtml(opts) {
5633
+ return `<!doctype html><html><head><meta charset="utf-8"><title>Create your KiCI GitHub App</title></head>
5634
+ <body>
5635
+ <p>Redirecting you to GitHub to create your KiCI GitHub App…</p>
5636
+ <form id="f" method="post" action="${escapeHtml(`${opts.createUrl}?state=${encodeURIComponent(opts.state)}`)}">
5637
+ <input type="hidden" name="manifest" value="${escapeHtml(opts.manifestJson)}">
5638
+ <noscript><button type="submit">Continue to GitHub</button></noscript>
5639
+ </form>
5640
+ <script>document.getElementById('f').submit();<\/script>
5641
+ </body></html>`;
5642
+ }
5643
+ function renderCodeDisplayHtml(code) {
5644
+ return `<!doctype html><html><head><meta charset="utf-8"><title>KiCI — copy your setup code</title></head>
5645
+ <body>
5646
+ <p>App created. Copy this code back into the <code>kici-admin</code> prompt:</p>
5647
+ <pre id="c" style="font-size:1.2rem;padding:12px;border:1px solid #ccc">${escapeHtml(code)}</pre>
5648
+ <button onclick="navigator.clipboard.writeText(document.getElementById('c').textContent)">Copy</button>
5649
+ </body></html>`;
5650
+ }
5651
+ //#endregion
5652
+ //#region src/cli/loopback-callback.ts
5653
+ function startManifestLoopback(opts) {
5654
+ return new Promise((resolveServer) => {
5655
+ let outcome;
5656
+ const waiters = [];
5657
+ const resolveCode = (value) => {
5658
+ if (outcome) return;
5659
+ outcome = {
5660
+ ok: true,
5661
+ value
5662
+ };
5663
+ for (const w of waiters) w.resolve(value);
5664
+ };
5665
+ const rejectCode = (error) => {
5666
+ if (outcome) return;
5667
+ outcome = {
5668
+ ok: false,
5669
+ error
5670
+ };
5671
+ for (const w of waiters) w.reject(error);
5672
+ };
5673
+ const server = createServer((req, res) => {
5674
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
5675
+ if (url.pathname === "/cb") {
5676
+ const code = url.searchParams.get("code") ?? "";
5677
+ const state = url.searchParams.get("state") ?? "";
5678
+ res.writeHead(200, { "content-type": "text/html" });
5679
+ res.end(renderCodeDisplayHtml(code));
5680
+ if (state !== opts.state) rejectCode(/* @__PURE__ */ new Error("OAuth state mismatch — aborting"));
5681
+ else resolveCode({
5682
+ code,
5683
+ state
5684
+ });
5685
+ return;
5686
+ }
5687
+ res.writeHead(200, { "content-type": "text/html" });
5688
+ res.end(renderManifestFormHtml({
5689
+ createUrl: opts.createUrl,
5690
+ state: opts.state,
5691
+ manifestJson: opts.manifestJson
5692
+ }));
5693
+ });
5694
+ server.on("clientError", () => {});
5695
+ server.listen(0, "127.0.0.1", () => {
5696
+ const base = `http://127.0.0.1:${server.address().port}`;
5697
+ resolveServer({
5698
+ redirectUrl: `${base}/cb`,
5699
+ formUrl: `${base}/`,
5700
+ waitForCode: (timeoutMs) => new Promise((resolve, reject) => {
5701
+ if (outcome) {
5702
+ outcome.ok ? resolve(outcome.value) : reject(outcome.error);
5703
+ return;
5704
+ }
5705
+ const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Timed out waiting for GitHub callback")), timeoutMs);
5706
+ waiters.push({
5707
+ resolve: (v) => {
5708
+ clearTimeout(timer);
5709
+ resolve(v);
5710
+ },
5711
+ reject: (e) => {
5712
+ clearTimeout(timer);
5713
+ reject(e);
5714
+ }
5715
+ });
5716
+ }),
5717
+ close: () => server.close()
5718
+ });
5719
+ });
5720
+ });
5721
+ }
5722
+ //#endregion
5723
+ //#region src/cli/open-browser.ts
5724
+ /**
5725
+ * Open a URL in the operator's default browser, best-effort. Used by the GitHub
5726
+ * App manifest setup flow to launch the create / install pages. Failure is
5727
+ * non-fatal — the caller always prints the URL too, so a headless host (or a
5728
+ * blocked launcher) just falls back to copy-paste.
5729
+ *
5730
+ * `KICI_BROWSER_CMD` overrides the launcher: `none` suppresses it entirely
5731
+ * (E2E / headless capture), any other value is run with `{url}` substituted —
5732
+ * mirroring the `kici login` convention.
5733
+ */
5734
+ function openBrowserBestEffort(url) {
5735
+ const override = process.env.KICI_BROWSER_CMD;
5736
+ if (override === "none") return;
5737
+ if (override) {
5738
+ exec(override.replace("{url}", url), () => {});
5739
+ return;
5740
+ }
5741
+ exec(platform() === "darwin" ? `open ${JSON.stringify(url)}` : platform() === "win32" ? `start "" ${JSON.stringify(url)}` : `xdg-open ${JSON.stringify(url)}`, () => {});
5742
+ }
5743
+ //#endregion
5744
+ //#region src/cli/commands/source-manifest.ts
5745
+ /**
5746
+ * `kici-admin source add github --manifest` — the one-click GitHub App setup
5747
+ * flow. Drives GitHub's App Manifest flow end-to-end: builds a pre-configured
5748
+ * manifest (KiCI's exact permissions/events/webhook URL), hands it to GitHub via
5749
+ * an auto-submitting form, catches the returned short-lived setup code over a
5750
+ * localhost loopback (or copy-paste in --no-browser mode), exchanges it for the
5751
+ * App's id + private key + webhook secret, then reuses the existing source
5752
+ * storage + Platform-registration path.
5753
+ *
5754
+ * The private-key-bearing conversion happens entirely here on the orchestrator
5755
+ * host — it never transits the Platform (sovereignty invariant).
5756
+ */
5757
+ /** Static marketing-site page the CLI points at in headless paste-code mode. */
5758
+ const STATIC_CALLBACK_URL = "https://kici.dev/gh-manifest-callback";
5759
+ /** How long to wait for the operator to click "Create" on GitHub. */
5760
+ const CREATE_TIMEOUT_MS = 5 * 6e4;
5761
+ /** How long to wait for the operator to install the App on at least one repo. */
5762
+ const INSTALL_TIMEOUT_MS = 5 * 6e4;
5763
+ const INSTALL_POLL_MS = 3e3;
5764
+ function defaultReadLine(prompt) {
5765
+ const rl = createInterface({
5766
+ input: process.stdin,
5767
+ output: process.stdout
5768
+ });
5769
+ return new Promise((resolve) => {
5770
+ rl.question(prompt, (answer) => {
5771
+ rl.close();
5772
+ resolve(answer.trim());
5773
+ });
5774
+ });
5775
+ }
5776
+ function defaultWriteRecoveryFile(appId, pem) {
5777
+ const file = join(mkdtempSync(join(tmpdir(), "kici-gh-app-")), `app-${appId}.private-key.pem`);
5778
+ writeFileSync(file, pem, { mode: 384 });
5779
+ return file;
5780
+ }
5781
+ const realManifestSetupDeps = {
5782
+ startLoopback: startManifestLoopback,
5783
+ openBrowser: openBrowserBestEffort,
5784
+ readLine: defaultReadLine,
5785
+ convert: (code) => convertManifestCode(code),
5786
+ waitForInstallation,
5787
+ verifyRepoAccess,
5788
+ writeRecoveryFile: defaultWriteRecoveryFile
5789
+ };
5790
+ /** Resolve the org-scoped webhook URL via the pre-flight route; abort if null. */
5791
+ async function resolveWebhookUrl(client) {
5792
+ const pre = await client.get("/api/v1/admin/sources/github-webhook-url");
5793
+ if (!pre.webhookUrl) throw new Error(`Cannot resolve the GitHub webhook URL${pre.webhookNote ? ` (${pre.webhookNote})` : ""}. The manifest flow needs it to configure the App. Ensure the orchestrator is connected to the Platform and has identified its org, then retry.`);
5794
+ return pre.webhookUrl;
5795
+ }
5796
+ /**
5797
+ * Catch the short-lived setup code: over the loopback (browser mode) or by
5798
+ * printing the static-site URL and reading the pasted code (headless mode).
5799
+ */
5800
+ async function captureCode(opts, deps, manifestJson, state, redirectUrl, createUrl, loopback) {
5801
+ if (opts.noBrowser) {
5802
+ const url = `${STATIC_CALLBACK_URL}#m=${Buffer.from(manifestJson, "utf-8").toString("base64url")}&state=${encodeURIComponent(state)}&createUrl=${encodeURIComponent(createUrl)}`;
5803
+ console.log("Open this URL in a browser to create your App:");
5804
+ console.log(` ${url}`);
5805
+ console.log("After clicking \"Create\", copy the setup code shown on the page.");
5806
+ const code = await deps.readLine("Paste the setup code here: ");
5807
+ if (!code) throw new Error("No setup code was provided");
5808
+ return code;
5809
+ }
5810
+ console.log("→ Opening GitHub to create your App…");
5811
+ await deps.openBrowser(loopback.formUrl);
5812
+ console.log(` (if your browser did not open, visit ${loopback.formUrl} )`);
5813
+ const { code } = await loopback.waitForCode(CREATE_TIMEOUT_MS);
5814
+ return code;
5815
+ }
5816
+ /** Store credentials + register; on failure, save the PEM so the App is not orphaned. */
5817
+ async function storeAndRegister(opts, client, deps, creds) {
5818
+ try {
5819
+ return await client.post("/api/v1/admin/sources", {
5820
+ provider: "github",
5821
+ name: creds.name,
5822
+ slug: creds.slug,
5823
+ appId: creds.appId,
5824
+ privateKey: creds.privateKey,
5825
+ webhookSecret: creds.webhookSecret
5826
+ });
5827
+ } catch (err) {
5828
+ const file = deps.writeRecoveryFile(creds.appId, creds.privateKey);
5829
+ console.error("");
5830
+ console.error("⚠ The GitHub App was created but storing it on the orchestrator failed.");
5831
+ console.error(` App id: ${creds.appId}`);
5832
+ console.error(` Private key saved to: ${file}`);
5833
+ console.error(" Recover with:");
5834
+ console.error(` kici-admin source add github --name "${opts.name}" --app-id ${creds.appId} --private-key @${file} --webhook-secret ${creds.webhookSecret}`);
5835
+ throw err;
5836
+ }
5837
+ }
5838
+ async function runGithubManifestSetup(opts, client, deps = realManifestSetupDeps) {
5839
+ const webhookUrl = opts.webhookUrl ? validateWebhookUrl(opts.webhookUrl) : await resolveWebhookUrl(client);
5840
+ const state = randomBytes(16).toString("hex");
5841
+ const createUrl = manifestCreateUrl(opts.githubOrg);
5842
+ const loopback = await deps.startLoopback({
5843
+ state,
5844
+ manifestJson: "{}",
5845
+ createUrl
5846
+ });
5847
+ try {
5848
+ const manifest = buildGithubAppManifest({
5849
+ name: opts.name,
5850
+ webhookUrl,
5851
+ redirectUrl: opts.noBrowser ? STATIC_CALLBACK_URL : loopback.redirectUrl
5852
+ });
5853
+ const manifestJson = JSON.stringify(manifest);
5854
+ loopback.close();
5855
+ const lb = await deps.startLoopback({
5856
+ state,
5857
+ manifestJson,
5858
+ createUrl
5859
+ });
5860
+ try {
5861
+ const code = await captureCode(opts, deps, manifestJson, state, lb.redirectUrl, createUrl, lb);
5862
+ const creds = await deps.convert(code);
5863
+ console.log(`→ ✓ App created (id ${creds.appId}), credentials captured`);
5864
+ const stored = await storeAndRegister(opts, client, deps, creds);
5865
+ console.log(`→ ✓ Stored on orchestrator (encrypted), registered as ${stored.routingKey}`);
5866
+ const installUrl = `https://github.com/apps/${creds.slug}/installations/new`;
5867
+ console.log(`→ Install the App on your repos: ${installUrl}`);
5868
+ if (!opts.noBrowser) await deps.openBrowser(installUrl);
5869
+ const { installationId, accountLogin } = await deps.waitForInstallation(creds, {
5870
+ timeoutMs: INSTALL_TIMEOUT_MS,
5871
+ pollMs: INSTALL_POLL_MS
5872
+ });
5873
+ console.log(`→ ✓ Installation detected (account ${accountLogin})`);
5874
+ const { repoCount } = await deps.verifyRepoAccess(creds, installationId);
5875
+ console.log(`→ ✓ Credentials verified (${repoCount} repositories reachable)`);
5876
+ console.log("");
5877
+ console.log(`GitHub App "${opts.name}" is live.`);
5878
+ console.log(` Webhook: ${webhookUrl}`);
5879
+ } finally {
5880
+ lb.close();
5881
+ }
5882
+ } finally {
5883
+ loopback.close();
5884
+ }
5885
+ }
5886
+ //#endregion
5190
5887
  //#region src/cli/commands/source.ts
5191
5888
  /**
5192
5889
  * Source management commands for kici-admin.
@@ -5268,6 +5965,16 @@ async function readFromStdin() {
5268
5965
  for await (const line of rl) chunks.push(line);
5269
5966
  return chunks.join("\n");
5270
5967
  }
5968
+ /** Print a `source refresh` result, showing old → new for name + slug. */
5969
+ function printRefreshResult(r) {
5970
+ if (!r.changed) {
5971
+ console.log(`${r.routingKey}: up to date (name "${r.newName}", slug "${r.newSlug}")`);
5972
+ return;
5973
+ }
5974
+ console.log(`${r.routingKey}: updated`);
5975
+ if (r.oldName !== r.newName) console.log(` name: ${r.oldName} → ${r.newName}`);
5976
+ if (r.oldSlug !== r.newSlug) console.log(` slug: ${r.oldSlug ?? "(none)"} → ${r.newSlug}`);
5977
+ }
5271
5978
  /**
5272
5979
  * Format a generic source for display.
5273
5980
  */
@@ -5375,8 +6082,26 @@ async function buildUniversalGitConfigFromOpts(opts) {
5375
6082
  function registerSourceCommands(program, getClient) {
5376
6083
  const src = program.command("source").description("Manage webhook sources");
5377
6084
  const add = src.command("add").description("Add a new webhook source");
5378
- add.command("github").description("Add a new GitHub App source").requiredOption("--name <name>", "Human-readable source name").requiredOption("--app-id <id>", "GitHub App ID").option("--private-key <pathOrValue>", "Private key (prefix with @ for file path)").option("--webhook-secret <secret>", "Webhook secret").option("--from-env <varName>", "Read private key from environment variable").option("--stdin", "Read private key from stdin").option("--json", "Emit raw JSON (the API response) instead of formatted text").action(async (opts) => {
6085
+ add.command("github").description("Add a new GitHub App source").requiredOption("--name <name>", "Human-readable source name").option("--app-id <id>", "GitHub App ID (omit when using --manifest)").option("--private-key <pathOrValue>", "Private key (prefix with @ for file path)").option("--webhook-secret <secret>", "Webhook secret").option("--from-env <varName>", "Read private key from environment variable").option("--stdin", "Read private key from stdin").option("--manifest", "One-click setup: create and configure a new GitHub App via the App Manifest flow").option("--no-browser", "Headless manifest setup: print a URL and paste the setup code back").option("--github-org <slug>", "Create the App under a GitHub org instead of your personal account").option("--webhook-url <url>", "Advanced/self-hosted: bake this https:// URL into the App webhook verbatim and skip platform-mode URL resolution. KiCI adds no ingress at this URL — your own infra owns delivery.").option("--json", "Emit raw JSON (the API response) instead of formatted text").action(async (opts) => {
6086
+ if (opts.manifest) {
6087
+ try {
6088
+ await runGithubManifestSetup({
6089
+ name: opts.name,
6090
+ noBrowser: !opts.browser,
6091
+ githubOrg: opts.githubOrg,
6092
+ webhookUrl: opts.webhookUrl
6093
+ }, getClient());
6094
+ } catch (err) {
6095
+ console.error(`Error: ${toErrorMessage(err)}`);
6096
+ process.exit(1);
6097
+ }
6098
+ return;
6099
+ }
5379
6100
  try {
6101
+ if (!opts.appId) {
6102
+ console.error("Error: --app-id is required (or use --manifest for one-click setup)");
6103
+ process.exit(1);
6104
+ }
5380
6105
  const privateKey = await resolveSecret({
5381
6106
  value: opts.privateKey,
5382
6107
  stdin: opts.stdin,
@@ -5605,6 +6330,34 @@ function registerSourceCommands(program, getClient) {
5605
6330
  process.exit(1);
5606
6331
  }
5607
6332
  });
6333
+ src.command("refresh [routingKey]").description("Re-sync a GitHub source's name and slug from GitHub (use --all for every source)").option("--all", "Refresh every GitHub source").option("--json", "Emit raw JSON instead of formatted text").action(async (routingKey, opts) => {
6334
+ try {
6335
+ if (opts.all) {
6336
+ const res = await getClient().post("/api/v1/admin/sources/refresh-all", {});
6337
+ if (opts.json) {
6338
+ console.log(JSON.stringify(res, null, 2));
6339
+ return;
6340
+ }
6341
+ for (const r of res.results) printRefreshResult(r);
6342
+ for (const e of res.errors) console.error(` ${e.routingKey}: ${e.error}`);
6343
+ if (res.results.length === 0 && res.errors.length === 0) console.log("No GitHub sources to refresh.");
6344
+ return;
6345
+ }
6346
+ if (!routingKey) {
6347
+ console.error("Error: provide a <routingKey> or use --all");
6348
+ process.exit(1);
6349
+ }
6350
+ const result = await getClient().post(`/api/v1/admin/sources/${encodeURIComponent(routingKey)}/refresh`, {});
6351
+ if (opts.json) {
6352
+ console.log(JSON.stringify(result, null, 2));
6353
+ return;
6354
+ }
6355
+ printRefreshResult(result);
6356
+ } catch (err) {
6357
+ console.error(`Error: ${toErrorMessage(err)}`);
6358
+ process.exit(1);
6359
+ }
6360
+ });
5608
6361
  src.command("update-generic <id>").description("Update a generic webhook source").option("--name <name>", "New name").option("--verification <method>", "Verification method: hmac_sha256, bearer_token, ip_allowlist, none").option("--secret <value>", "New verification secret (prefix with @ for file)").option("--from-env <varName>", "Read verification secret from environment variable").option("--stdin", "Read verification secret from stdin").option("--event-type-header <header>", "Header name for event type extraction").option("--event-type-path <jsonpath>", "JSONPath for event type extraction from body").option("--idempotency-key-header <header>", "Header name for idempotency key").option("--idempotency-key-path <jsonpath>", "JSONPath for idempotency key from body").option("--dedup-window <seconds>", "Dedup window in seconds", parseInt).option("--max-payload <bytes>", "Maximum payload size in bytes", parseInt).option("--allowed-events <events>", "Comma-separated list of allowed event types").option("--strip-headers <headers>", "Comma-separated list of headers to strip").option("--rate-limit <rpm>", "Rate limit in requests per minute", parseInt).option("--preset <name>", "Universal-git preset").option("--git-url-template <url>", "Clone URL template").option("--credential-ref <key>", "Secret key name").option("--credential-store <backend>", "Secret backend name").option("--credential-type <type>", "Credential type: pat, basic, ssh").option("--credential-user <user>", "Username for PAT/basic auth").option("--ssh-host-key-policy <policy>", "SSH host-key policy: accept-new, pinned").option("--ssh-known-hosts-pem <pathOrValue>", "Pinned SSH known_hosts (prefix with @ for file)").option("--clear-git-config", "Revert this source back to a payload-only generic webhook").option("--provider-type <type>", "Provider implementation: generic (default) or local (a git repo on the agent filesystem cloned via file://)").option("--json", "Emit raw JSON (the full source row) instead of formatted text").action(async (id, opts) => {
5609
6362
  try {
5610
6363
  const secret = await resolveSecret({
@@ -8597,11 +9350,17 @@ var init_image_digests = __esmMin((() => {
8597
9350
  * a container-based service management backend that works on any
8598
9351
  * platform with Docker or Podman installed.
8599
9352
  */
8600
- var compose_exports = /* @__PURE__ */ __exportAll({ ComposeServiceManager: () => ComposeServiceManager });
9353
+ var compose_exports = /* @__PURE__ */ __exportAll({
9354
+ ComposeServiceManager: () => ComposeServiceManager,
9355
+ detectRuntime: () => detectRuntime$1
9356
+ });
8601
9357
  /**
8602
9358
  * Detect which container runtime is available.
8603
9359
  * Tries podman compose first, then docker compose.
8604
9360
  *
9361
+ * Exported so the orchestrator installer can resolve the runtime once when
9362
+ * injecting the `KICI_DEPLOY_CONTAINER_RUNTIME` env var for a compose deploy.
9363
+ *
8605
9364
  * @throws When neither runtime is found.
8606
9365
  */
8607
9366
  function detectRuntime$1() {
@@ -8851,7 +9610,36 @@ function resolveServiceExecutable(opts) {
8851
9610
  };
8852
9611
  }
8853
9612
  //#endregion
9613
+ //#region src/cli/service/deploy-env.ts
9614
+ /** Prefix every line uses; also the strip marker for idempotent re-install. */
9615
+ const DEPLOY_ENV_PREFIX = "KICI_DEPLOY_";
9616
+ /**
9617
+ * Build the `KICI_DEPLOY_*` env lines for a given deployment shape. The mode
9618
+ * line is always emitted; container name + runtime are emitted only for the
9619
+ * `compose` platform (the only shape with a container to `exec` into).
9620
+ */
9621
+ function buildDeployEnvLines(input) {
9622
+ const lines = [`KICI_DEPLOY_MODE=${input.platform}`];
9623
+ if (input.platform === "compose") {
9624
+ lines.push(`KICI_DEPLOY_CONTAINER=${input.serviceName}`);
9625
+ if (input.containerRuntime) lines.push(`KICI_DEPLOY_CONTAINER_RUNTIME=${input.containerRuntime}`);
9626
+ }
9627
+ return lines;
9628
+ }
9629
+ /**
9630
+ * Append the freshly-computed deploy lines to existing env-file content,
9631
+ * idempotently: any pre-existing `KICI_DEPLOY_*` line is stripped first so a
9632
+ * re-install / upgrade heals the shape rather than duplicating it.
9633
+ */
9634
+ function upsertDeployEnvLines(existingContent, lines) {
9635
+ const kept = existingContent.split("\n").filter((line) => !line.trimStart().startsWith(DEPLOY_ENV_PREFIX));
9636
+ while (kept.length > 0 && kept[kept.length - 1].trim() === "") kept.pop();
9637
+ const body = kept.join("\n");
9638
+ return `${body.length > 0 ? `${body}\n` : ""}${lines.join("\n")}\n`;
9639
+ }
9640
+ //#endregion
8854
9641
  //#region src/cli/commands/shared/versioned-upgrade.ts
9642
+ init_compose();
8855
9643
  /**
8856
9644
  * Shared versioned directory upgrade logic for kici-admin upgrade commands.
8857
9645
  *
@@ -9663,6 +10451,18 @@ function registerOrchestratorInstall(orchestrator) {
9663
10451
  fs.appendFileSync(envFilePath, `\nKICI_DATABASE_URL=${devDbUrl}\n`);
9664
10452
  console.log(`Appended KICI_DATABASE_URL to ${envFilePath}`);
9665
10453
  }
10454
+ let composeRuntime;
10455
+ if (platform === "compose") try {
10456
+ composeRuntime = detectRuntime$1();
10457
+ } catch {
10458
+ composeRuntime = void 0;
10459
+ }
10460
+ const deployLines = buildDeployEnvLines({
10461
+ platform,
10462
+ serviceName,
10463
+ containerRuntime: composeRuntime
10464
+ });
10465
+ fs.writeFileSync(envFilePath, upsertDeployEnvLines(fs.readFileSync(envFilePath, "utf-8"), deployLines), "utf-8");
9666
10466
  const entryScript = opts.binary ? void 0 : fileURLToPath(import.meta.resolve(`@kici-dev/orchestrator/${selectServerEntry(fs.readFileSync(envFilePath, "utf-8"))}`));
9667
10467
  const { executablePath, args } = resolveServiceExecutable({
9668
10468
  binary: opts.binary ? path.resolve(opts.binary) : void 0,
@@ -10677,19 +11477,33 @@ var JoinTokenManager = class {
10677
11477
  * `peer_credentials_active_uniq` partial unique index).
10678
11478
  *
10679
11479
  * On a 0-row claim, a follow-up SELECT disambiguates not-found / expired
10680
- * / already-used so callers can branch on the specific reason — the
10681
- * recovery path in peer-handler.ts keys on the "already used" string
10682
- * specifically. The follow-up only fires on the unhappy path.
11480
+ * / already-used / reusable-by-same-instance so callers can branch on the
11481
+ * specific reason — the recovery path in peer-handler.ts keys on the
11482
+ * "already used" string specifically. The follow-up only fires on the
11483
+ * unhappy path.
11484
+ *
11485
+ * Self-healing reuse: a join token is re-consumable by the same joining
11486
+ * peer (`peerInstanceId`) until its `expires_at`. A peer that lost its
11487
+ * credential (transient outage / deleted credential file) re-presents the
11488
+ * still-valid join token already in its env; the coordinator accepts the
11489
+ * reuse and issues a fresh credential — no operator action, no cluster
11490
+ * redeploy. Reuse is bounded by BOTH `expires_at` AND the consuming
11491
+ * instanceId, so it never widens a leaked token's usefulness beyond the
11492
+ * instance that first consumed it.
10683
11493
  */
10684
- async validateAndConsumeToken(token, consumedBy) {
11494
+ async validateAndConsumeToken(token, consumedBy, peerInstanceId) {
10685
11495
  const parsed = parseToken(token);
10686
11496
  const keys = deriveKeys(Buffer.from(parsed.secretHex, "hex"));
10687
11497
  const updateResult = await this.deps.db.updateTable("join_tokens").set({
10688
11498
  consumed_at: /* @__PURE__ */ new Date(),
10689
- consumed_by: consumedBy
11499
+ consumed_by: consumedBy,
11500
+ consumed_by_instance: peerInstanceId
10690
11501
  }).where("token_hash", "=", keys.validationHash).where("consumed_at", "is", null).where("expires_at", ">", /* @__PURE__ */ new Date()).executeTakeFirst();
10691
11502
  if (Number(updateResult?.numUpdatedRows ?? 0n) > 0) {
10692
- joinTokenLogger.info("Consumed join token", { consumedBy });
11503
+ joinTokenLogger.info("Consumed join token", {
11504
+ consumedBy,
11505
+ peerInstanceId
11506
+ });
10693
11507
  return {
10694
11508
  routing: parsed.routing,
10695
11509
  keys
@@ -10697,8 +11511,21 @@ var JoinTokenManager = class {
10697
11511
  }
10698
11512
  const row = await this.deps.db.selectFrom("join_tokens").selectAll().where("token_hash", "=", keys.validationHash).executeTakeFirst();
10699
11513
  if (!row) throw new Error("Invalid join token");
10700
- if (row.consumed_at) throw new Error(TOKEN_ALREADY_USED_MESSAGE);
10701
- throw new Error("Join token has expired. Generate a new token with: kici admin create-join-token");
11514
+ if (new Date(row.expires_at).getTime() <= Date.now()) throw new Error("Join token has expired. Generate a new token with: kici admin create-join-token");
11515
+ if (row.consumed_at) {
11516
+ if (row.consumed_by_instance === peerInstanceId) {
11517
+ joinTokenLogger.info("Re-validated join token for returning instance", {
11518
+ consumedBy,
11519
+ peerInstanceId
11520
+ });
11521
+ return {
11522
+ routing: parsed.routing,
11523
+ keys
11524
+ };
11525
+ }
11526
+ throw new Error(TOKEN_ALREADY_USED_MESSAGE);
11527
+ }
11528
+ throw new Error("Invalid join token");
10702
11529
  }
10703
11530
  };
10704
11531
  /**
@@ -10765,14 +11592,24 @@ var PeerCredentialStore = class {
10765
11592
  this.db = db;
10766
11593
  }
10767
11594
  /**
10768
- * Save a new peer credential.
11595
+ * Save a new peer credential, revoking any prior active credential for the
11596
+ * same instanceId (the `peer_credentials_active_uniq` partial unique index
11597
+ * permits exactly one active credential per instanceId).
11598
+ *
11599
+ * Returns `{ revokedCount }` — how many previously-active credentials this
11600
+ * save revoked. Because a coordinator's peer-clients share one
11601
+ * identity-scoped credential, a `revokedCount > 0` here means every sibling
11602
+ * peer-client of `instanceId` that was authenticating with the old
11603
+ * credential is now invalidated and will be rejected on its next proof. The
11604
+ * caller logs this so a revoke that cascades sibling rejections is visible in
11605
+ * the orchestrator logs.
10769
11606
  */
10770
11607
  async save(opts) {
10771
11608
  const expiryDays = opts.expiryDays ?? DEFAULT_EXPIRY_DAYS;
10772
11609
  const expiresAt = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1e3);
10773
11610
  const runOnce = async () => {
10774
- await this.db.transaction().execute(async (trx) => {
10775
- await trx.updateTable("peer_credentials").set({ revoked_at: /* @__PURE__ */ new Date() }).where("instance_id", "=", opts.instanceId).where("revoked_at", "is", null).execute();
11611
+ return this.db.transaction().execute(async (trx) => {
11612
+ const revokeResult = await trx.updateTable("peer_credentials").set({ revoked_at: /* @__PURE__ */ new Date() }).where("instance_id", "=", opts.instanceId).where("revoked_at", "is", null).executeTakeFirst();
10776
11613
  await trx.insertInto("peer_credentials").values({
10777
11614
  instance_id: opts.instanceId,
10778
11615
  credential_hash: opts.credentialHash,
@@ -10781,15 +11618,13 @@ var PeerCredentialStore = class {
10781
11618
  source_token_hash: opts.sourceTokenHash ?? null,
10782
11619
  expires_at: expiresAt
10783
11620
  }).execute();
11621
+ return Number(revokeResult?.numUpdatedRows ?? 0n);
10784
11622
  });
10785
11623
  };
10786
11624
  try {
10787
- await runOnce();
11625
+ return { revokedCount: await runOnce() };
10788
11626
  } catch (err) {
10789
- if (err instanceof Error && "code" in err && err.code === "23505") {
10790
- await runOnce();
10791
- return;
10792
- }
11627
+ if (err instanceof Error && "code" in err && err.code === "23505") return { revokedCount: await runOnce() };
10793
11628
  throw err;
10794
11629
  }
10795
11630
  }
@@ -11005,6 +11840,23 @@ function deriveHostStatus(row, nowMs, graceMs) {
11005
11840
  return "unreachable";
11006
11841
  }
11007
11842
  /**
11843
+ * Normalize a stored `host_properties` value into a typed host-vars bag. The pg
11844
+ * driver returns parsed JSON for a `jsonb` column, but accept a JSON string too
11845
+ * (defensive for non-pg paths / tests). Non-object / null reads back as `{}`.
11846
+ */
11847
+ function parseHostProperties(value) {
11848
+ const parsed = typeof value === "string" ? safeJsonParse(value) : value;
11849
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
11850
+ return {};
11851
+ }
11852
+ function safeJsonParse(value) {
11853
+ try {
11854
+ return JSON.parse(value);
11855
+ } catch {
11856
+ return null;
11857
+ }
11858
+ }
11859
+ /**
11008
11860
  * Durable, cluster-shared roster of every agent the cluster has ever enrolled.
11009
11861
  *
11010
11862
  * The in-memory `AgentRegistry` reconciles into this table on register
@@ -11022,6 +11874,7 @@ var HostRosterStore = class {
11022
11874
  /** Idempotent upsert on agent_id; stamps connected_instance_id + last_seen. */
11023
11875
  async upsert(input) {
11024
11876
  const labelsJson = JSON.stringify(input.labels);
11877
+ const reportedJson = JSON.stringify(input.properties ?? {});
11025
11878
  await this.db.insertInto("host_roster").values({
11026
11879
  agent_id: input.agentId,
11027
11880
  token_id: input.tokenId,
@@ -11031,6 +11884,7 @@ var HostRosterStore = class {
11031
11884
  platform: input.platform,
11032
11885
  arch: input.arch,
11033
11886
  connected_instance_id: input.instanceId,
11887
+ host_properties: reportedJson,
11034
11888
  last_seen: sql`now()`,
11035
11889
  updated_at: sql`now()`
11036
11890
  }).onConflict((oc) => oc.column("agent_id").doUpdateSet({
@@ -11041,6 +11895,7 @@ var HostRosterStore = class {
11041
11895
  platform: input.platform,
11042
11896
  arch: input.arch,
11043
11897
  connected_instance_id: input.instanceId,
11898
+ host_properties: sql`COALESCE(host_roster.host_properties, '{}'::jsonb) || ${reportedJson}::jsonb`,
11044
11899
  last_seen: sql`now()`,
11045
11900
  updated_at: sql`now()`
11046
11901
  })).execute();
@@ -11065,6 +11920,7 @@ var HostRosterStore = class {
11065
11920
  labels: JSON.stringify(input.labels),
11066
11921
  hostname: input.hostname ?? null,
11067
11922
  connected_instance_id: null,
11923
+ host_properties: JSON.stringify(input.properties ?? {}),
11068
11924
  last_seen: sql`now()`,
11069
11925
  updated_at: sql`now()`
11070
11926
  }).onConflict((oc) => oc.column("agent_id").doNothing()).execute();
@@ -11099,13 +11955,51 @@ var HostRosterStore = class {
11099
11955
  connectedInstanceId: row.connected_instance_id,
11100
11956
  status: deriveHostStatus(row, now, graceMs),
11101
11957
  platform: row.platform,
11102
- arch: row.arch
11958
+ arch: row.arch,
11959
+ properties: parseHostProperties(row.host_properties)
11103
11960
  });
11104
11961
  }
11105
11962
  out.sort((a, b) => a.agentId.localeCompare(b.agentId));
11106
11963
  return out;
11107
11964
  }
11108
11965
  /**
11966
+ * Map a roster row to the canonical {@link HostInventoryEntry} — the queryable
11967
+ * shape returned by the `inventory.query`/`inventory.get` RPC and typed on the
11968
+ * SDK's `ctx.kici.inventory`. Status is derived the same way `findMatching`
11969
+ * derives it (live + fresh ⇒ ready; declared-but-absent static ⇒ unreachable;
11970
+ * ephemeral past ttl ⇒ stale).
11971
+ */
11972
+ toInventoryEntry(row, graceMs) {
11973
+ return {
11974
+ agentId: row.agent_id,
11975
+ labels: JSON.parse(row.labels),
11976
+ properties: parseHostProperties(row.host_properties),
11977
+ hostname: row.hostname,
11978
+ platform: row.platform,
11979
+ arch: row.arch,
11980
+ lifecycleClass: row.lifecycle_class,
11981
+ status: deriveHostStatus(row, Date.now(), graceMs),
11982
+ lastSeen: new Date(row.last_seen).toISOString()
11983
+ };
11984
+ }
11985
+ /**
11986
+ * Query the roster as canonical {@link HostInventoryEntry} records. With a
11987
+ * selector, reuses `findMatching`'s label filtering (server-side, glob/regex);
11988
+ * property filtering is done client-side in the workflow. Omit the selector ⇒
11989
+ * every host.
11990
+ */
11991
+ async queryInventory(selector, graceMs) {
11992
+ if (!selector || !selector.include && !selector.exclude) return (await this.listAll()).map((r) => this.toInventoryEntry(r, graceMs));
11993
+ const matched = await this.findMatching(selector.include ?? [], selector.exclude ?? [], graceMs);
11994
+ const byId = new Map(matched.map((m) => [m.agentId, m]));
11995
+ return (await this.listAll()).filter((r) => byId.has(r.agent_id)).map((r) => this.toInventoryEntry(r, graceMs));
11996
+ }
11997
+ /** Single-host inventory lookup; null when the agent is not in the roster. */
11998
+ async getInventory(agentId, graceMs) {
11999
+ const row = await this.get(agentId);
12000
+ return row ? this.toInventoryEntry(row, graceMs) : null;
12001
+ }
12002
+ /**
11109
12003
  * Count `static` (declared) hosts whose derived status is `unreachable` —
11110
12004
  * the "declared-but-absent" alarm population. Reuses the single-source
11111
12005
  * {@link deriveHostStatus} so the count never diverges from what
@@ -11118,14 +12012,54 @@ var HostRosterStore = class {
11118
12012
  const now = Date.now();
11119
12013
  return rows.filter((r) => deriveHostStatus(r, now, graceMs) === "unreachable").length;
11120
12014
  }
12015
+ /** Remove a host from the roster by agent id. Returns rows deleted. */
12016
+ async removeStatic(agentId) {
12017
+ const res = await this.db.deleteFrom("host_roster").where("agent_id", "=", agentId).executeTakeFirst();
12018
+ return Number(res.numDeletedRows ?? 0n);
12019
+ }
11121
12020
  /** Delete ephemeral rows whose last_seen is older than ttl. Returns count. */
11122
12021
  async reapEphemeralPastTtl(ttlMs) {
11123
12022
  const res = await this.db.deleteFrom("host_roster").where("lifecycle_class", "=", "ephemeral").where("last_seen", "<", sql`now() - (${ttlMs}::text || ' milliseconds')::interval`).executeTakeFirst();
11124
12023
  return Number(res.numDeletedRows ?? 0n);
11125
12024
  }
12025
+ /**
12026
+ * Mark a host as about-to-reboot until `until`. Set by the `host.requestReboot`
12027
+ * API handler when an agent's `restartHost()` step runs. While the value is in
12028
+ * the future, the host's disconnect is the expected reboot and its pinned
12029
+ * post-restart job is held.
12030
+ */
12031
+ async setRebootPending(agentId, until) {
12032
+ await this.db.updateTable("host_roster").set({
12033
+ reboot_pending_until: until,
12034
+ updated_at: sql`now()`
12035
+ }).where("agent_id", "=", agentId).execute();
12036
+ }
12037
+ /** Clear the reboot-pending flag (on reconnect, deadline expiry, or cancel). */
12038
+ async clearRebootPending(agentId) {
12039
+ await this.db.updateTable("host_roster").set({
12040
+ reboot_pending_until: null,
12041
+ updated_at: sql`now()`
12042
+ }).where("agent_id", "=", agentId).execute();
12043
+ }
12044
+ /** True when the host has a reboot-pending deadline still in the future at `nowMs`. */
12045
+ async isRebootPending(agentId, nowMs) {
12046
+ const row = await this.db.selectFrom("host_roster").select("reboot_pending_until").where("agent_id", "=", agentId).executeTakeFirst();
12047
+ return (row?.reboot_pending_until ? new Date(row.reboot_pending_until).getTime() : 0) > nowMs;
12048
+ }
12049
+ /**
12050
+ * Agent ids whose reboot-pending deadline has passed at `nowMs`. The deadline
12051
+ * sweep clears these; the held post-restart job then hits the queue timeout.
12052
+ */
12053
+ async listExpiredRebootPending(nowMs) {
12054
+ return (await this.db.selectFrom("host_roster").select("agent_id").where("reboot_pending_until", "is not", null).where("reboot_pending_until", "<=", new Date(nowMs)).execute()).map((r) => r.agent_id);
12055
+ }
11126
12056
  };
11127
12057
  //#endregion
11128
12058
  //#region src/cli/commands/host.ts
12059
+ /** Collect a repeatable `--prop key=value` flag into an array. */
12060
+ function collectProp(value, previous) {
12061
+ return [...previous, value];
12062
+ }
11129
12063
  /**
11130
12064
  * The CLI reads the roster DB directly (no full orchestrator config load), so
11131
12065
  * it sources the grace window from the same env var the orchestrator config
@@ -11179,13 +12113,15 @@ function registerHostCommands(program) {
11179
12113
  process.exit(1);
11180
12114
  }
11181
12115
  });
11182
- host.command("declare").description("Pre-declare a static host before it connects").requiredOption("--agent-id <id>", "Agent id the host will register as").option("--labels <labels>", "Comma-separated labels", "").option("--hostname <name>", "Hostname").action(async (opts) => {
12116
+ host.command("declare").description("Pre-declare a static host before it connects").requiredOption("--agent-id <id>", "Agent id the host will register as").option("--labels <labels>", "Comma-separated labels", "").option("--hostname <name>", "Hostname").option("--prop <key=value>", "Typed host property (repeatable; true/false ⇒ boolean, numeric ⇒ number)", collectProp, []).action(async (opts) => {
11183
12117
  try {
11184
12118
  const labels = opts.labels ? String(opts.labels).split(",").map((s) => s.trim()).filter(Boolean) : [];
12119
+ const properties = parseHostPropertyAssignments(opts.prop);
11185
12120
  await withDb((db) => new HostRosterStore(db).declareStatic({
11186
12121
  agentId: opts.agentId,
11187
12122
  labels,
11188
- hostname: opts.hostname
12123
+ hostname: opts.hostname,
12124
+ properties
11189
12125
  }));
11190
12126
  console.log(`Declared static host: ${opts.agentId}`);
11191
12127
  } catch (err) {
@@ -11193,6 +12129,18 @@ function registerHostCommands(program) {
11193
12129
  process.exit(1);
11194
12130
  }
11195
12131
  });
12132
+ host.command("remove").description("Remove a host from the roster").requiredOption("--agent-id <id>", "Agent id to remove").action(async (opts) => {
12133
+ try {
12134
+ if (await withDb((db) => new HostRosterStore(db).removeStatic(opts.agentId)) === 0) {
12135
+ console.error(`No host found: ${opts.agentId}`);
12136
+ process.exit(1);
12137
+ }
12138
+ console.log(`Removed host: ${opts.agentId}`);
12139
+ } catch (err) {
12140
+ console.error(`Error: ${toErrorMessage(err)}`);
12141
+ process.exit(1);
12142
+ }
12143
+ });
11196
12144
  }
11197
12145
  //#endregion
11198
12146
  //#region src/cli/commands/org-settings.ts