@kici-dev/orchestrator 0.1.16 → 0.1.18

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 (83) hide show
  1. package/dist/agent/dispatcher.d.ts +27 -1
  2. package/dist/agent/host-roster-reaper.d.ts +42 -0
  3. package/dist/agent/host-roster.d.ts +110 -0
  4. package/dist/agent/registry.d.ts +65 -2
  5. package/dist/app.d.ts +3 -0
  6. package/dist/approvals/apply-decision.d.ts +11 -1
  7. package/dist/cache/pending-inits.d.ts +5 -0
  8. package/dist/cli/api-client.d.ts +11 -0
  9. package/dist/cli/commands/host.d.ts +13 -0
  10. package/dist/cli/commands/local-hook.d.ts +15 -0
  11. package/dist/cli/commands/local-trigger.d.ts +24 -0
  12. package/dist/cli/commands/remote-source.d.ts +17 -0
  13. package/dist/cli/commands/shared/versioned-upgrade.d.ts +37 -0
  14. package/dist/cli/kici-admin.d.ts +13 -0
  15. package/dist/cli/service/compose.d.ts +2 -1
  16. package/dist/cli/service/index.d.ts +2 -2
  17. package/dist/cli/service/instance/manifest.d.ts +12 -0
  18. package/dist/cli/service/instance/resolve.d.ts +12 -2
  19. package/dist/cli/service/launchd.d.ts +4 -1
  20. package/dist/cli/service/platform-detect.d.ts +5 -0
  21. package/dist/cli/service/systemd.d.ts +2 -1
  22. package/dist/cli/service/types.d.ts +37 -0
  23. package/dist/cli/service/windows.d.ts +2 -1
  24. package/dist/cli.js +1340 -198
  25. package/dist/cluster/coordinator.d.ts +5 -1
  26. package/dist/config/schema.d.ts +4 -0
  27. package/dist/config/types.d.ts +9 -0
  28. package/dist/config.d.ts +6 -4
  29. package/dist/dashboard/handler.d.ts +26 -2
  30. package/dist/dashboard/needs-edges.d.ts +13 -0
  31. package/dist/db/migrations/035_pending_workflow_contexts.d.ts +11 -0
  32. package/dist/db/migrations/036_attestations.d.ts +15 -0
  33. package/dist/db/migrations/037_generic_sources_provider_type_local.d.ts +19 -0
  34. package/dist/db/migrations/038_remote_sources.d.ts +14 -0
  35. package/dist/db/migrations/039_host_roster.d.ts +19 -0
  36. package/dist/db/migrations/040_runsonall_pin.d.ts +4 -0
  37. package/dist/db/migrations/041_wave_gated.d.ts +4 -0
  38. package/dist/db/migrations/042_dispatch_queue_patterns.d.ts +4 -0
  39. package/dist/db/types.d.ts +144 -10
  40. package/dist/diagnostics/fleet-collector.d.ts +1 -1
  41. package/dist/entry-helpers.d.ts +7 -29
  42. package/dist/environments/held-runs.d.ts +33 -0
  43. package/dist/index.js +1 -0
  44. package/dist/lockfile-redos-guard.d.ts +19 -0
  45. package/dist/metrics/prometheus.d.ts +17 -2
  46. package/dist/metrics/scheduled-jobs.d.ts +2 -2
  47. package/dist/orchestrator-core.d.ts +94 -1
  48. package/dist/pipeline/dispatch-matched-workflow.d.ts +60 -2
  49. package/dist/pipeline/install-secrets-resolver.d.ts +32 -5
  50. package/dist/pipeline/needs-scheduler.d.ts +12 -10
  51. package/dist/pipeline/pending-workflow-context.d.ts +44 -0
  52. package/dist/pipeline/processor.d.ts +31 -4
  53. package/dist/pipeline/remote-source-store.d.ts +21 -0
  54. package/dist/pipeline/resume-workflow.d.ts +26 -0
  55. package/dist/pipeline/wave-scheduler.d.ts +60 -0
  56. package/dist/providers/local/index.d.ts +33 -0
  57. package/dist/providers/local/local-source-config.d.ts +17 -0
  58. package/dist/providers/{internal → local}/lock-file-fetcher.d.ts +6 -6
  59. package/dist/providers/{internal → local}/normalizer.d.ts +28 -28
  60. package/dist/providers/{internal → local}/repo-url-builder.d.ts +6 -6
  61. package/dist/queue/job-queue.d.ts +61 -2
  62. package/dist/reporting/execution-tracker.d.ts +61 -0
  63. package/dist/routes/admin-events.d.ts +3 -4
  64. package/dist/routes/uploads.d.ts +36 -26
  65. package/dist/server.js +53755 -50888
  66. package/dist/sources/build-platform-sources.d.ts +4 -2
  67. package/dist/stale-detector/stale-run-detector.d.ts +14 -1
  68. package/dist/standalone.js +17234 -15725
  69. package/dist/webhook/generic-sources-listener.d.ts +4 -0
  70. package/dist/webhook/generic-sources.d.ts +37 -11
  71. package/dist/webhook/register-source-bundle.d.ts +14 -4
  72. package/dist/ws/agent-handler.d.ts +17 -0
  73. package/dist/ws/dashboard-dispatch-guard.d.ts +21 -0
  74. package/dist/ws/dashboard-env-handler.d.ts +13 -1
  75. package/dist/ws/oidc-token-relay.d.ts +59 -0
  76. package/dist/ws/platform-client.d.ts +30 -1
  77. package/dist/ws/test-relay-handlers.d.ts +112 -0
  78. package/installer-image-digests.json +3 -3
  79. package/package.json +22 -22
  80. package/sbom.spdx.json +3228 -6633
  81. package/dist/providers/internal/index.d.ts +0 -32
  82. package/dist/routes/test-trigger.d.ts +0 -41
  83. package/dist/ws/observer-handler.d.ts +0 -42
package/dist/cli.js CHANGED
@@ -3,9 +3,9 @@ import { createRequire } from "node:module";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import * as path$1 from "node:path";
5
5
  import path, { join, resolve } from "node:path";
6
- import { Command } from "commander";
7
6
  import * as fs$1 from "node:fs";
8
- import fs, { closeSync, createReadStream, createWriteStream, existsSync, openSync, promises, readFileSync, statSync, unwatchFile, watchFile, writeFileSync } from "node:fs";
7
+ import fs, { chmodSync, closeSync, createReadStream, createWriteStream, existsSync, openSync, promises, readFileSync, realpathSync, statSync, unwatchFile, watchFile, writeFileSync } from "node:fs";
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";
11
11
  import { createInterface } from "node:readline";
@@ -19,14 +19,14 @@ import { Migrator } from "kysely/migration";
19
19
  import "jsonpath-plus";
20
20
  import { execFile, execFileSync, execSync, spawn } from "node:child_process";
21
21
  import { promisify } from "node:util";
22
- import archiver from "archiver";
22
+ import { ZipArchive } from "archiver";
23
23
  import { confirm, input, password, select } from "@inquirer/prompts";
24
24
  import JSZip from "jszip";
25
25
  import { pipeline } from "node:stream/promises";
26
26
  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";
27
28
  import { DASHBOARD_WRITE_OPERATIONS, DASHBOARD_WRITE_OPERATIONS_BY_NAME, DashboardWriteCategory, DashboardWriteSensitivity } from "@kici-dev/engine/protocol/dashboard-write-operations";
28
29
  import { CLUSTER_NAME_FORMAT_MESSAGE } from "@kici-dev/engine/protocol/cluster-name";
29
- import { KICI_AGENT_ENV_PREFIX, KNOWN_ROLES, ScalerBackendType, ScalerEventType, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES, WS_MAX_PAYLOAD_BYTES, accessLogWarmSqlCase, getAccessLogColdDays, getSecretAuditLogColdDays, minAccessLogWarmDays, minSecretAuditLogWarmDays, scalerAgentLabels, secretAuditLogWarmSqlCase } from "@kici-dev/engine";
30
30
  import http from "node:http";
31
31
  import { PassThrough } from "node:stream";
32
32
  import Docker from "dockerode";
@@ -52,6 +52,8 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
52
52
  * error formatting, and URL construction.
53
53
  */
54
54
  var AdminApiClient = class {
55
+ baseUrl;
56
+ token;
55
57
  constructor(baseUrl, token) {
56
58
  this.baseUrl = baseUrl;
57
59
  this.token = token;
@@ -977,6 +979,8 @@ var init_schema = __esmMin((() => {
977
979
  }).optional(),
978
980
  agentAuth: z.enum(["token", "none"]).optional(),
979
981
  agentTokenTtlMs: z.coerce.number().optional(),
982
+ rosterGraceMs: z.coerce.number().optional(),
983
+ rosterTtlMs: z.coerce.number().optional(),
980
984
  queue: z.object({
981
985
  maxDepth: z.coerce.number().optional(),
982
986
  timeoutMs: z.coerce.number().optional(),
@@ -1053,6 +1057,8 @@ var init_schema = __esmMin((() => {
1053
1057
  }).optional(),
1054
1058
  agentAuth: z.enum(["token", "none"]).default("token"),
1055
1059
  agentTokenTtlMs: z.coerce.number().default(36e5),
1060
+ rosterGraceMs: z.coerce.number().default(3e5),
1061
+ rosterTtlMs: z.coerce.number().default(18e5),
1056
1062
  queueMaxDepth: z.coerce.number().default(1e3),
1057
1063
  queueTimeoutMs: z.coerce.number().default(36e5),
1058
1064
  /**
@@ -1548,8 +1554,8 @@ var init_client = __esmMin((() => {
1548
1554
  //#region src/db/migrations/001_initial.ts
1549
1555
  init_client();
1550
1556
  var _001_initial_exports = /* @__PURE__ */ __exportAll({
1551
- down: () => down$33,
1552
- up: () => up$33
1557
+ down: () => down$41,
1558
+ up: () => up$41
1553
1559
  });
1554
1560
  /**
1555
1561
  * Squashed initial migration -- creates the complete Orchestrator database schema.
@@ -2241,7 +2247,7 @@ const DDL_STATEMENTS = [
2241
2247
  `ALTER TABLE ONLY public.held_runs
2242
2248
  ADD CONSTRAINT held_runs_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES public.environments(id);`
2243
2249
  ];
2244
- async function up$33(db) {
2250
+ async function up$41(db) {
2245
2251
  for (const stmt of DDL_STATEMENTS) await sql.raw(stmt).execute(db);
2246
2252
  await sql`
2247
2253
  INSERT INTO cluster_meta (key, value)
@@ -2263,7 +2269,7 @@ async function up$33(db) {
2263
2269
  * Rollback drops everything created above. Uses CASCADE on table drops to cut
2264
2270
  * through the FK graph without relying on exact topological order.
2265
2271
  */
2266
- async function down$33(db) {
2272
+ async function down$41(db) {
2267
2273
  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);
2268
2274
  for (const table of [
2269
2275
  "workflow_registrations",
@@ -2310,8 +2316,8 @@ async function down$33(db) {
2310
2316
  //#endregion
2311
2317
  //#region src/db/migrations/002_config_versions_key_version.ts
2312
2318
  var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
2313
- down: () => down$32,
2314
- up: () => up$32
2319
+ down: () => down$40,
2320
+ up: () => up$40
2315
2321
  });
2316
2322
  /**
2317
2323
  * Add key_version column to config_versions so that sensitive-field encryption
@@ -2325,13 +2331,13 @@ var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
2325
2331
  * No index is needed: rotation does a full-table scan; reads are by
2326
2332
  * `version` primary key and never filter on `key_version`.
2327
2333
  */
2328
- async function up$32(db) {
2334
+ async function up$40(db) {
2329
2335
  await sql`
2330
2336
  ALTER TABLE public.config_versions
2331
2337
  ADD COLUMN key_version integer NOT NULL DEFAULT 1
2332
2338
  `.execute(db);
2333
2339
  }
2334
- async function down$32(db) {
2340
+ async function down$40(db) {
2335
2341
  await sql`
2336
2342
  ALTER TABLE public.config_versions
2337
2343
  DROP COLUMN key_version
@@ -2340,8 +2346,8 @@ async function down$32(db) {
2340
2346
  //#endregion
2341
2347
  //#region src/db/migrations/003_access_log.ts
2342
2348
  var _003_access_log_exports = /* @__PURE__ */ __exportAll({
2343
- down: () => down$31,
2344
- up: () => up$31
2349
+ down: () => down$39,
2350
+ up: () => up$39
2345
2351
  });
2346
2352
  /**
2347
2353
  * Access log: one row per read or orchestrator-admin mutation attributable
@@ -2365,7 +2371,7 @@ var _003_access_log_exports = /* @__PURE__ */ __exportAll({
2365
2371
  * Retention is TTL-based via expires_at; packages/orchestrator/src/queue/
2366
2372
  * cleanup.ts picks up the prune pass.
2367
2373
  */
2368
- async function up$31(db) {
2374
+ async function up$39(db) {
2369
2375
  await sql`
2370
2376
  CREATE TABLE public.access_log (
2371
2377
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -2403,28 +2409,28 @@ async function up$31(db) {
2403
2409
  ON public.access_log (actor_type, actor_id, created_at DESC)
2404
2410
  `.execute(db);
2405
2411
  }
2406
- async function down$31(db) {
2412
+ async function down$39(db) {
2407
2413
  await sql`DROP TABLE IF EXISTS public.access_log`.execute(db);
2408
2414
  }
2409
2415
  //#endregion
2410
2416
  //#region src/db/migrations/004_rename_bundle_to_source.ts
2411
2417
  var _004_rename_bundle_to_source_exports = /* @__PURE__ */ __exportAll({
2412
- down: () => down$30,
2413
- up: () => up$30
2418
+ down: () => down$38,
2419
+ up: () => up$38
2414
2420
  });
2415
- async function up$30(db) {
2421
+ async function up$38(db) {
2416
2422
  await db.schema.alterTable("dispatch_queue").renameColumn("bundle_url", "source_tar_url").execute();
2417
2423
  await db.schema.alterTable("dispatch_queue").renameColumn("bundle_hash", "source_tar_hash").execute();
2418
2424
  }
2419
- async function down$30(db) {
2425
+ async function down$38(db) {
2420
2426
  await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_url", "bundle_url").execute();
2421
2427
  await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_hash", "bundle_hash").execute();
2422
2428
  }
2423
2429
  //#endregion
2424
2430
  //#region src/db/migrations/005_cold_store_chunk_counter.ts
2425
2431
  var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
2426
- down: () => down$29,
2427
- up: () => up$29
2432
+ down: () => down$37,
2433
+ up: () => up$37
2428
2434
  });
2429
2435
  /**
2430
2436
  * Cold-store chunk counter table.
@@ -2441,7 +2447,7 @@ var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
2441
2447
  *
2442
2448
  * sections 5 and 8.
2443
2449
  */
2444
- async function up$29(db) {
2450
+ async function up$37(db) {
2445
2451
  await sql`
2446
2452
  CREATE TABLE public.cold_store_chunk_counts (
2447
2453
  db TEXT NOT NULL,
@@ -2459,14 +2465,14 @@ async function up$29(db) {
2459
2465
  ON public.cold_store_chunk_counts (db, table_name)
2460
2466
  `.execute(db);
2461
2467
  }
2462
- async function down$29(db) {
2468
+ async function down$37(db) {
2463
2469
  await sql`DROP TABLE IF EXISTS public.cold_store_chunk_counts`.execute(db);
2464
2470
  }
2465
2471
  //#endregion
2466
2472
  //#region src/db/migrations/006_runs_jobs_steps_archived_at.ts
2467
2473
  var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
2468
- down: () => down$28,
2469
- up: () => up$28
2474
+ down: () => down$36,
2475
+ up: () => up$36
2470
2476
  });
2471
2477
  /**
2472
2478
  * `execution_runs` / `execution_jobs` / `execution_steps` cold-store
@@ -2502,7 +2508,7 @@ var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
2502
2508
  * - `idx_execution_jobs_routing_key_created (routing_key, created_at)`
2503
2509
  * - `idx_execution_steps_routing_key_created (routing_key, created_at)`
2504
2510
  */
2505
- async function up$28(db) {
2511
+ async function up$36(db) {
2506
2512
  await sql`
2507
2513
  ALTER TABLE public.execution_runs
2508
2514
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2547,7 +2553,7 @@ async function up$28(db) {
2547
2553
  ON public.execution_steps (routing_key, created_at)
2548
2554
  `.execute(db);
2549
2555
  }
2550
- async function down$28(db) {
2556
+ async function down$36(db) {
2551
2557
  await sql`DROP INDEX IF EXISTS public.idx_execution_steps_routing_key_created`.execute(db);
2552
2558
  await sql`
2553
2559
  ALTER TABLE public.execution_steps
@@ -2572,8 +2578,8 @@ async function down$28(db) {
2572
2578
  //#endregion
2573
2579
  //#region src/db/migrations/007_audit_logs_archived_at.ts
2574
2580
  var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
2575
- down: () => down$27,
2576
- up: () => up$27
2581
+ down: () => down$35,
2582
+ up: () => up$35
2577
2583
  });
2578
2584
  /**
2579
2585
  * `secret_audit_log` and `access_log` cold-store schema additions, plus
@@ -2613,7 +2619,7 @@ var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
2613
2619
  * `down()` would not have meaningful retention bounds. Acceptable for
2614
2620
  * staging.
2615
2621
  */
2616
- async function up$27(db) {
2622
+ async function up$35(db) {
2617
2623
  await sql`
2618
2624
  ALTER TABLE public.secret_audit_log
2619
2625
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2634,7 +2640,7 @@ async function up$27(db) {
2634
2640
  DROP COLUMN IF EXISTS expires_at
2635
2641
  `.execute(db);
2636
2642
  }
2637
- async function down$27(db) {
2643
+ async function down$35(db) {
2638
2644
  await sql`
2639
2645
  ALTER TABLE public.access_log
2640
2646
  ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '90 days')
@@ -2662,8 +2668,8 @@ async function down$27(db) {
2662
2668
  //#endregion
2663
2669
  //#region src/db/migrations/008_event_log_archived_at.ts
2664
2670
  var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
2665
- down: () => down$26,
2666
- up: () => up$26
2671
+ down: () => down$34,
2672
+ up: () => up$34
2667
2673
  });
2668
2674
  /**
2669
2675
  * `event_log` cold-store schema additions plus removal of the
@@ -2701,7 +2707,7 @@ var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
2701
2707
  * — best effort; rows inserted between `up()` and a hypothetical
2702
2708
  * `down()` would not have meaningful retention bounds.
2703
2709
  */
2704
- async function up$26(db) {
2710
+ async function up$34(db) {
2705
2711
  await sql`
2706
2712
  ALTER TABLE public.event_log
2707
2713
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2717,7 +2723,7 @@ async function up$26(db) {
2717
2723
  DROP COLUMN IF EXISTS expires_at
2718
2724
  `.execute(db);
2719
2725
  }
2720
- async function down$26(db) {
2726
+ async function down$34(db) {
2721
2727
  await sql`
2722
2728
  ALTER TABLE public.event_log
2723
2729
  ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '30 days')
@@ -2736,8 +2742,8 @@ async function down$26(db) {
2736
2742
  //#endregion
2737
2743
  //#region src/db/migrations/009_access_log_trigram.ts
2738
2744
  var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
2739
- down: () => down$25,
2740
- up: () => up$25
2745
+ down: () => down$33,
2746
+ up: () => up$33
2741
2747
  });
2742
2748
  /**
2743
2749
  * Trigram (pg_trgm) index on access_log.error_message for the federated
@@ -2750,7 +2756,7 @@ var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
2750
2756
  * EXISTS` are both safe to re-run. No CONCURRENTLY because Kysely runs
2751
2757
  * migrations inside a transaction; the lock is brief on a sampled table.
2752
2758
  */
2753
- async function up$25(db) {
2759
+ async function up$33(db) {
2754
2760
  await sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`.execute(db);
2755
2761
  await sql`
2756
2762
  CREATE INDEX IF NOT EXISTS access_log_error_message_trgm_idx
@@ -2759,14 +2765,14 @@ async function up$25(db) {
2759
2765
  WHERE error_message IS NOT NULL
2760
2766
  `.execute(db);
2761
2767
  }
2762
- async function down$25(db) {
2768
+ async function down$33(db) {
2763
2769
  await sql`DROP INDEX IF EXISTS public.access_log_error_message_trgm_idx`.execute(db);
2764
2770
  }
2765
2771
  //#endregion
2766
2772
  //#region src/db/migrations/010_cold_store_chunks.ts
2767
2773
  var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
2768
- down: () => down$24,
2769
- up: () => up$24
2774
+ down: () => down$32,
2775
+ up: () => up$32
2770
2776
  });
2771
2777
  /**
2772
2778
  * Cold-store chunk index — Phase 2 (cold-store purge).
@@ -2798,7 +2804,7 @@ var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
2798
2804
  * forever. Adapters that don't opt into per-bucket archival via
2799
2805
  * `coldTtlDays` don't insert here either.
2800
2806
  */
2801
- async function up$24(db) {
2807
+ async function up$32(db) {
2802
2808
  await sql`
2803
2809
  CREATE TABLE public.cold_store_chunks (
2804
2810
  db TEXT NOT NULL,
@@ -2825,14 +2831,14 @@ async function up$24(db) {
2825
2831
  ON public.cold_store_chunks (db, table_name, tenant_id, archived_at DESC)
2826
2832
  `.execute(db);
2827
2833
  }
2828
- async function down$24(db) {
2834
+ async function down$32(db) {
2829
2835
  await sql`DROP TABLE IF EXISTS public.cold_store_chunks`.execute(db);
2830
2836
  }
2831
2837
  //#endregion
2832
2838
  //#region src/db/migrations/011_drop_source_secrets_notify.ts
2833
2839
  var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
2834
- down: () => down$23,
2835
- up: () => up$23
2840
+ down: () => down$31,
2841
+ up: () => up$31
2836
2842
  });
2837
2843
  /**
2838
2844
  * Drop the `source_secrets_change_trigger` and the
@@ -2851,11 +2857,11 @@ var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
2851
2857
  * in `001_initial.ts`. They wake up no consumer until the `WebhookSecretManager`
2852
2858
  * is restored, so this migration is safe to roll back.
2853
2859
  */
2854
- async function up$23(db) {
2860
+ async function up$31(db) {
2855
2861
  await sql`DROP TRIGGER IF EXISTS source_secrets_change_trigger ON public.scoped_secrets`.execute(db);
2856
2862
  await sql`DROP FUNCTION IF EXISTS public.notify_source_secrets_change() CASCADE`.execute(db);
2857
2863
  }
2858
- async function down$23(db) {
2864
+ async function down$31(db) {
2859
2865
  await sql`
2860
2866
  CREATE OR REPLACE FUNCTION public.notify_source_secrets_change() RETURNS trigger
2861
2867
  LANGUAGE plpgsql
@@ -2894,8 +2900,8 @@ async function down$23(db) {
2894
2900
  //#endregion
2895
2901
  //#region src/db/migrations/012_peer_credentials_active_uniq.ts
2896
2902
  var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
2897
- down: () => down$22,
2898
- up: () => up$22
2903
+ down: () => down$30,
2904
+ up: () => up$30
2899
2905
  });
2900
2906
  /**
2901
2907
  * Add a partial unique index on `peer_credentials (instance_id) WHERE
@@ -2921,7 +2927,7 @@ var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
2921
2927
  * `down()` only drops the index; it does NOT undo the dedupe (there's no
2922
2928
  * safe way to recreate revoked rows, and the dedupe is monotonic).
2923
2929
  */
2924
- async function up$22(db) {
2930
+ async function up$30(db) {
2925
2931
  await sql`
2926
2932
  UPDATE public.peer_credentials
2927
2933
  SET revoked_at = NOW()
@@ -2939,14 +2945,14 @@ async function up$22(db) {
2939
2945
  WHERE revoked_at IS NULL
2940
2946
  `.execute(db);
2941
2947
  }
2942
- async function down$22(db) {
2948
+ async function down$30(db) {
2943
2949
  await sql`DROP INDEX IF EXISTS public.peer_credentials_active_uniq`.execute(db);
2944
2950
  }
2945
2951
  //#endregion
2946
2952
  //#region src/db/migrations/013_execution_log_bytes.ts
2947
2953
  var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
2948
- down: () => down$21,
2949
- up: () => up$21
2954
+ down: () => down$29,
2955
+ up: () => up$29
2950
2956
  });
2951
2957
  /**
2952
2958
  * Add `log_bytes BIGINT NOT NULL DEFAULT 0` columns to `execution_runs` and
@@ -2965,7 +2971,7 @@ var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
2965
2971
  *
2966
2972
  * Idempotent (`ADD COLUMN IF NOT EXISTS`).
2967
2973
  */
2968
- async function up$21(db) {
2974
+ async function up$29(db) {
2969
2975
  await sql`
2970
2976
  ALTER TABLE public.execution_runs
2971
2977
  ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
@@ -2975,15 +2981,15 @@ async function up$21(db) {
2975
2981
  ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
2976
2982
  `.execute(db);
2977
2983
  }
2978
- async function down$21(db) {
2984
+ async function down$29(db) {
2979
2985
  await sql`ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS log_bytes`.execute(db);
2980
2986
  await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS log_bytes`.execute(db);
2981
2987
  }
2982
2988
  //#endregion
2983
2989
  //#region src/db/migrations/014_kici_events_lease_retry.ts
2984
2990
  var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
2985
- down: () => down$20,
2986
- up: () => up$20
2991
+ down: () => down$28,
2992
+ up: () => up$28
2987
2993
  });
2988
2994
  /**
2989
2995
  * Add lease + retry + DLQ columns to `kici_events` so the EventRouter can
@@ -3017,7 +3023,7 @@ var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
3017
3023
  *
3018
3024
  * Idempotent (`ADD COLUMN IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS`).
3019
3025
  */
3020
- async function up$20(db) {
3026
+ async function up$28(db) {
3021
3027
  await sql`
3022
3028
  ALTER TABLE public.kici_events
3023
3029
  ADD COLUMN IF NOT EXISTS claimed_at TIMESTAMPTZ,
@@ -3048,7 +3054,7 @@ async function up$20(db) {
3048
3054
  WHERE dlq_at IS NOT NULL
3049
3055
  `.execute(db);
3050
3056
  }
3051
- async function down$20(db) {
3057
+ async function down$28(db) {
3052
3058
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_dlq`.execute(db);
3053
3059
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_lease_expired`.execute(db);
3054
3060
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_retry_due`.execute(db);
@@ -3066,8 +3072,8 @@ async function down$20(db) {
3066
3072
  //#endregion
3067
3073
  //#region src/db/migrations/015_org_settings_customer_scoped.ts
3068
3074
  var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
3069
- down: () => down$19,
3070
- up: () => up$19
3075
+ down: () => down$27,
3076
+ up: () => up$27
3071
3077
  });
3072
3078
  /**
3073
3079
  * Org-scope `org_settings` and qualify each glob entry by source.
@@ -3095,7 +3101,7 @@ var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
3095
3101
  * Idempotent: a re-run on an already-migrated DB sees `customer_id` exists
3096
3102
  * and the list columns are already jsonb, so it is a no-op.
3097
3103
  */
3098
- async function up$19(db) {
3104
+ async function up$27(db) {
3099
3105
  if ((await sql`
3100
3106
  SELECT EXISTS (
3101
3107
  SELECT 1 FROM information_schema.columns
@@ -3220,7 +3226,7 @@ async function up$19(db) {
3220
3226
  await sql`DROP TABLE _org_settings_merged`.execute(db);
3221
3227
  await sql`DROP TABLE _org_settings_stage`.execute(db);
3222
3228
  }
3223
- async function down$19(db) {
3229
+ async function down$27(db) {
3224
3230
  if (!(await sql`
3225
3231
  SELECT EXISTS (
3226
3232
  SELECT 1 FROM information_schema.columns
@@ -3245,8 +3251,8 @@ async function down$19(db) {
3245
3251
  //#endregion
3246
3252
  //#region src/db/migrations/016_org_settings_allow_http_npm.ts
3247
3253
  var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
3248
- down: () => down$18,
3249
- up: () => up$18
3254
+ down: () => down$26,
3255
+ up: () => up$26
3250
3256
  });
3251
3257
  /**
3252
3258
  * Add `org_settings.allow_http_npm_registries boolean NOT NULL DEFAULT false`.
@@ -3259,7 +3265,7 @@ var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
3259
3265
  *
3260
3266
  * Idempotent: a re-run on a DB that already has the column is a no-op.
3261
3267
  */
3262
- async function up$18(db) {
3268
+ async function up$26(db) {
3263
3269
  if ((await sql`
3264
3270
  SELECT EXISTS (
3265
3271
  SELECT 1 FROM information_schema.columns
@@ -3273,7 +3279,7 @@ async function up$18(db) {
3273
3279
  ADD COLUMN allow_http_npm_registries boolean NOT NULL DEFAULT false
3274
3280
  `.execute(db);
3275
3281
  }
3276
- async function down$18(db) {
3282
+ async function down$26(db) {
3277
3283
  await sql`
3278
3284
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS allow_http_npm_registries
3279
3285
  `.execute(db);
@@ -3281,8 +3287,8 @@ async function down$18(db) {
3281
3287
  //#endregion
3282
3288
  //#region src/db/migrations/017_org_id_widen.ts
3283
3289
  var _017_org_id_widen_exports = /* @__PURE__ */ __exportAll({
3284
- down: () => down$17,
3285
- up: () => up$17
3290
+ down: () => down$25,
3291
+ up: () => up$25
3286
3292
  });
3287
3293
  /**
3288
3294
  * Widen every orchestrator-side `org_id varchar(12)` column to
@@ -3322,17 +3328,17 @@ const ORG_ID_TABLES$1 = [
3322
3328
  "held_runs",
3323
3329
  "scoped_secrets"
3324
3330
  ];
3325
- async function up$17(db) {
3331
+ async function up$25(db) {
3326
3332
  for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(16)`).execute(db);
3327
3333
  }
3328
- async function down$17(db) {
3334
+ async function down$25(db) {
3329
3335
  for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(12)`).execute(db);
3330
3336
  }
3331
3337
  //#endregion
3332
3338
  //#region src/db/migrations/018_org_id_prefix_backfill.ts
3333
3339
  var _018_org_id_prefix_backfill_exports = /* @__PURE__ */ __exportAll({
3334
- down: () => down$16,
3335
- up: () => up$16
3340
+ down: () => down$24,
3341
+ up: () => up$24
3336
3342
  });
3337
3343
  /**
3338
3344
  * Prefix every orchestrator-side tenant string with `org_` to align
@@ -3377,19 +3383,19 @@ const CUSTOMER_ID_TABLES = [
3377
3383
  "workflow_registrations",
3378
3384
  "org_settings"
3379
3385
  ];
3380
- async function up$16(db) {
3386
+ async function up$24(db) {
3381
3387
  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);
3382
3388
  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);
3383
3389
  }
3384
- async function down$16(db) {
3390
+ async function down$24(db) {
3385
3391
  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);
3386
3392
  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);
3387
3393
  }
3388
3394
  //#endregion
3389
3395
  //#region src/db/migrations/019_generic_sources_change_notify.ts
3390
3396
  var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
3391
- down: () => down$15,
3392
- up: () => up$15
3397
+ down: () => down$23,
3398
+ up: () => up$23
3393
3399
  });
3394
3400
  /**
3395
3401
  * Add a Postgres trigger on `generic_webhook_sources` that emits
@@ -3412,7 +3418,7 @@ var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
3412
3418
  * (`notify_sources_change()` + `sources_change_trigger`, defined in
3413
3419
  * `001_initial.ts`).
3414
3420
  */
3415
- async function up$15(db) {
3421
+ async function up$23(db) {
3416
3422
  await sql`
3417
3423
  CREATE FUNCTION public.notify_generic_sources_change() RETURNS trigger
3418
3424
  LANGUAGE plpgsql
@@ -3433,15 +3439,15 @@ async function up$15(db) {
3433
3439
  FOR EACH ROW EXECUTE FUNCTION public.notify_generic_sources_change()
3434
3440
  `.execute(db);
3435
3441
  }
3436
- async function down$15(db) {
3442
+ async function down$23(db) {
3437
3443
  await sql`DROP TRIGGER IF EXISTS generic_sources_change_trigger ON public.generic_webhook_sources`.execute(db);
3438
3444
  await sql`DROP FUNCTION IF EXISTS public.notify_generic_sources_change()`.execute(db);
3439
3445
  }
3440
3446
  //#endregion
3441
3447
  //#region src/db/migrations/020_org_settings_dashboard_write_policy.ts
3442
3448
  var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportAll({
3443
- down: () => down$14,
3444
- up: () => up$14
3449
+ down: () => down$22,
3450
+ up: () => up$22
3445
3451
  });
3446
3452
  /**
3447
3453
  * Add `org_settings.dashboard_write_policy jsonb NOT NULL DEFAULT '{}'`.
@@ -3456,7 +3462,7 @@ var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportA
3456
3462
  *
3457
3463
  * Idempotent: a re-run on a DB that already has the column is a no-op.
3458
3464
  */
3459
- async function up$14(db) {
3465
+ async function up$22(db) {
3460
3466
  if ((await sql`
3461
3467
  SELECT EXISTS (
3462
3468
  SELECT 1 FROM information_schema.columns
@@ -3470,7 +3476,7 @@ async function up$14(db) {
3470
3476
  ADD COLUMN dashboard_write_policy jsonb NOT NULL DEFAULT '{}'::jsonb
3471
3477
  `.execute(db);
3472
3478
  }
3473
- async function down$14(db) {
3479
+ async function down$22(db) {
3474
3480
  await sql`
3475
3481
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS dashboard_write_policy
3476
3482
  `.execute(db);
@@ -3478,8 +3484,8 @@ async function down$14(db) {
3478
3484
  //#endregion
3479
3485
  //#region src/db/migrations/021_check_run_tracking.ts
3480
3486
  var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
3481
- down: () => down$13,
3482
- up: () => up$13
3487
+ down: () => down$21,
3488
+ up: () => up$21
3483
3489
  });
3484
3490
  /**
3485
3491
  * Add `check_run_tracking` table for HA-safe check-run state persistence.
@@ -3503,7 +3509,7 @@ var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
3503
3509
  *
3504
3510
  * Idempotent: a re-run on a DB that already has the table is a no-op.
3505
3511
  */
3506
- async function up$13(db) {
3512
+ async function up$21(db) {
3507
3513
  if ((await sql`
3508
3514
  SELECT EXISTS (
3509
3515
  SELECT 1 FROM information_schema.tables
@@ -3534,14 +3540,14 @@ async function up$13(db) {
3534
3540
  WHERE run_id IS NOT NULL
3535
3541
  `.execute(db);
3536
3542
  }
3537
- async function down$13(db) {
3543
+ async function down$21(db) {
3538
3544
  await sql`DROP TABLE IF EXISTS public.check_run_tracking`.execute(db);
3539
3545
  }
3540
3546
  //#endregion
3541
3547
  //#region src/db/migrations/022_scaler_manager_state.ts
3542
3548
  var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
3543
- down: () => down$12,
3544
- up: () => up$12
3549
+ down: () => down$20,
3550
+ up: () => up$20
3545
3551
  });
3546
3552
  /**
3547
3553
  * Add three tables persisting `ScalerManager` per-coord state:
@@ -3568,7 +3574,7 @@ var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
3568
3574
  * Idempotent: a re-run on a DB that already has any of these tables
3569
3575
  * leaves the existing one alone.
3570
3576
  */
3571
- async function up$12(db) {
3577
+ async function up$20(db) {
3572
3578
  const tableExists = async (name) => {
3573
3579
  return (await sql`
3574
3580
  SELECT EXISTS (
@@ -3618,7 +3624,7 @@ async function up$12(db) {
3618
3624
  `.execute(db);
3619
3625
  }
3620
3626
  }
3621
- async function down$12(db) {
3627
+ async function down$20(db) {
3622
3628
  await sql`DROP TABLE IF EXISTS public.scaler_reservations`.execute(db);
3623
3629
  await sql`DROP TABLE IF EXISTS public.scaler_agent_jobs`.execute(db);
3624
3630
  await sql`DROP TABLE IF EXISTS public.scaler_spawning_agents`.execute(db);
@@ -3626,8 +3632,8 @@ async function down$12(db) {
3626
3632
  //#endregion
3627
3633
  //#region src/db/migrations/023_dispatch_queue_recovery_deadline.ts
3628
3634
  var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll({
3629
- down: () => down$11,
3630
- up: () => up$11
3635
+ down: () => down$19,
3636
+ up: () => up$19
3631
3637
  });
3632
3638
  /**
3633
3639
  * Add `dispatch_queue.recovery_deadline TIMESTAMPTZ` and
@@ -3651,7 +3657,7 @@ var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll(
3651
3657
  * Idempotent: re-running on a DB that already has either column is a
3652
3658
  * no-op.
3653
3659
  */
3654
- async function up$11(db) {
3660
+ async function up$19(db) {
3655
3661
  const colExists = async (name) => {
3656
3662
  return (await sql`
3657
3663
  SELECT EXISTS (
@@ -3676,7 +3682,7 @@ async function up$11(db) {
3676
3682
  WHERE recovery_deadline IS NOT NULL
3677
3683
  `.execute(db);
3678
3684
  }
3679
- async function down$11(db) {
3685
+ async function down$19(db) {
3680
3686
  await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_recovery_deadline`.execute(db);
3681
3687
  await sql`
3682
3688
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS recovery_agent_id
@@ -3688,8 +3694,8 @@ async function down$11(db) {
3688
3694
  //#endregion
3689
3695
  //#region src/db/migrations/024_dispatch_queue_provisioning_error.ts
3690
3696
  var _024_dispatch_queue_provisioning_error_exports = /* @__PURE__ */ __exportAll({
3691
- down: () => down$10,
3692
- up: () => up$10
3697
+ down: () => down$18,
3698
+ up: () => up$18
3693
3699
  });
3694
3700
  /**
3695
3701
  * Add `dispatch_queue.last_provisioning_error TEXT` recording the most
@@ -3705,7 +3711,7 @@ var _024_dispatch_queue_provisioning_error_exports = /* @__PURE__ */ __exportAll
3705
3711
  *
3706
3712
  * Idempotent: re-running on a DB that already has the column is a no-op.
3707
3713
  */
3708
- async function up$10(db) {
3714
+ async function up$18(db) {
3709
3715
  const colExists = async (name) => {
3710
3716
  return (await sql`
3711
3717
  SELECT EXISTS (
@@ -3721,7 +3727,7 @@ async function up$10(db) {
3721
3727
  ADD COLUMN last_provisioning_error TEXT
3722
3728
  `.execute(db);
3723
3729
  }
3724
- async function down$10(db) {
3730
+ async function down$18(db) {
3725
3731
  await sql`
3726
3732
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS last_provisioning_error
3727
3733
  `.execute(db);
@@ -3729,8 +3735,8 @@ async function down$10(db) {
3729
3735
  //#endregion
3730
3736
  //#region src/db/migrations/025_init_failure.ts
3731
3737
  var _025_init_failure_exports = /* @__PURE__ */ __exportAll({
3732
- down: () => down$9,
3733
- up: () => up$9
3738
+ down: () => down$17,
3739
+ up: () => up$17
3734
3740
  });
3735
3741
  /**
3736
3742
  * Add `init_failure jsonb` columns to `execution_runs` and `execution_jobs`.
@@ -3744,7 +3750,7 @@ var _025_init_failure_exports = /* @__PURE__ */ __exportAll({
3744
3750
  * Idempotent: re-running on a DB that already has either column is a no-op
3745
3751
  * for that column.
3746
3752
  */
3747
- async function up$9(db) {
3753
+ async function up$17(db) {
3748
3754
  const colExists = async (table, name) => {
3749
3755
  return (await sql`
3750
3756
  SELECT EXISTS (
@@ -3764,7 +3770,7 @@ async function up$9(db) {
3764
3770
  ADD COLUMN init_failure JSONB DEFAULT NULL
3765
3771
  `.execute(db);
3766
3772
  }
3767
- async function down$9(db) {
3773
+ async function down$17(db) {
3768
3774
  await sql`
3769
3775
  ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS init_failure
3770
3776
  `.execute(db);
@@ -3775,8 +3781,8 @@ async function down$9(db) {
3775
3781
  //#endregion
3776
3782
  //#region src/db/migrations/026_event_log_lockfile_corrupt.ts
3777
3783
  var _026_event_log_lockfile_corrupt_exports = /* @__PURE__ */ __exportAll({
3778
- down: () => down$8,
3779
- up: () => up$8
3784
+ down: () => down$16,
3785
+ up: () => up$16
3780
3786
  });
3781
3787
  /**
3782
3788
  * Extend the event_log.status CHECK constraint with 'lockfile_corrupt' so the
@@ -3785,7 +3791,7 @@ var _026_event_log_lockfile_corrupt_exports = /* @__PURE__ */ __exportAll({
3785
3791
  *
3786
3792
  * Idempotent: the DROP ... IF EXISTS / re-ADD pair re-runs cleanly.
3787
3793
  */
3788
- async function up$8(db) {
3794
+ async function up$16(db) {
3789
3795
  await sql`ALTER TABLE event_log DROP CONSTRAINT IF EXISTS event_log_status_check`.execute(db);
3790
3796
  await sql`
3791
3797
  ALTER TABLE event_log ADD CONSTRAINT event_log_status_check
@@ -3795,7 +3801,7 @@ async function up$8(db) {
3795
3801
  ])))
3796
3802
  `.execute(db);
3797
3803
  }
3798
- async function down$8(db) {
3804
+ async function down$16(db) {
3799
3805
  await sql`ALTER TABLE event_log DROP CONSTRAINT IF EXISTS event_log_status_check`.execute(db);
3800
3806
  await sql`
3801
3807
  ALTER TABLE event_log ADD CONSTRAINT event_log_status_check
@@ -3808,8 +3814,8 @@ async function down$8(db) {
3808
3814
  //#endregion
3809
3815
  //#region src/db/migrations/027_workflow_timeout.ts
3810
3816
  var _027_workflow_timeout_exports = /* @__PURE__ */ __exportAll({
3811
- down: () => down$7,
3812
- up: () => up$7
3817
+ down: () => down$15,
3818
+ up: () => up$15
3813
3819
  });
3814
3820
  /**
3815
3821
  * Add `workflow_timeout_ms integer` to `execution_runs`.
@@ -3827,13 +3833,13 @@ var _027_workflow_timeout_exports = /* @__PURE__ */ __exportAll({
3827
3833
  *
3828
3834
  * Idempotent: re-running on a DB that already has the column is a no-op.
3829
3835
  */
3830
- async function up$7(db) {
3836
+ async function up$15(db) {
3831
3837
  await sql`
3832
3838
  ALTER TABLE public.execution_runs
3833
3839
  ADD COLUMN IF NOT EXISTS workflow_timeout_ms INTEGER DEFAULT NULL
3834
3840
  `.execute(db);
3835
3841
  }
3836
- async function down$7(db) {
3842
+ async function down$15(db) {
3837
3843
  await sql`
3838
3844
  ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS workflow_timeout_ms
3839
3845
  `.execute(db);
@@ -3841,8 +3847,8 @@ async function down$7(db) {
3841
3847
  //#endregion
3842
3848
  //#region src/db/migrations/028_org_settings_user_cache.ts
3843
3849
  var _028_org_settings_user_cache_exports = /* @__PURE__ */ __exportAll({
3844
- down: () => down$6,
3845
- up: () => up$6
3850
+ down: () => down$14,
3851
+ up: () => up$14
3846
3852
  });
3847
3853
  /**
3848
3854
  * Add `org_settings.user_cache_quota_bytes bigint` and
@@ -3871,7 +3877,7 @@ async function columnExists(db, column) {
3871
3877
  ) AS exists
3872
3878
  `.execute(db)).rows[0]?.exists ?? false;
3873
3879
  }
3874
- async function up$6(db) {
3880
+ async function up$14(db) {
3875
3881
  if (!await columnExists(db, "user_cache_quota_bytes")) await sql`
3876
3882
  ALTER TABLE public.org_settings
3877
3883
  ADD COLUMN user_cache_quota_bytes bigint
@@ -3881,7 +3887,7 @@ async function up$6(db) {
3881
3887
  ADD COLUMN user_cache_ttl_ms bigint
3882
3888
  `.execute(db);
3883
3889
  }
3884
- async function down$6(db) {
3890
+ async function down$14(db) {
3885
3891
  await sql`
3886
3892
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS user_cache_quota_bytes
3887
3893
  `.execute(db);
@@ -3892,8 +3898,8 @@ async function down$6(db) {
3892
3898
  //#endregion
3893
3899
  //#region src/db/migrations/029_dispatch_queue_attempts.ts
3894
3900
  var _029_dispatch_queue_attempts_exports = /* @__PURE__ */ __exportAll({
3895
- down: () => down$5,
3896
- up: () => up$5
3901
+ down: () => down$13,
3902
+ up: () => up$13
3897
3903
  });
3898
3904
  /**
3899
3905
  * Add `dispatch_queue.dispatch_attempts INT NOT NULL DEFAULT 0`.
@@ -3907,7 +3913,7 @@ var _029_dispatch_queue_attempts_exports = /* @__PURE__ */ __exportAll({
3907
3913
  *
3908
3914
  * Idempotent: re-running on a DB that already has the column is a no-op.
3909
3915
  */
3910
- async function up$5(db) {
3916
+ async function up$13(db) {
3911
3917
  if (!((await sql`
3912
3918
  SELECT EXISTS (
3913
3919
  SELECT 1 FROM information_schema.columns
@@ -3920,7 +3926,7 @@ async function up$5(db) {
3920
3926
  ADD COLUMN dispatch_attempts INT NOT NULL DEFAULT 0
3921
3927
  `.execute(db);
3922
3928
  }
3923
- async function down$5(db) {
3929
+ async function down$13(db) {
3924
3930
  await sql`
3925
3931
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS dispatch_attempts
3926
3932
  `.execute(db);
@@ -3928,8 +3934,8 @@ async function down$5(db) {
3928
3934
  //#endregion
3929
3935
  //#region src/db/migrations/030_held_runs_env_set_null.ts
3930
3936
  var _030_held_runs_env_set_null_exports = /* @__PURE__ */ __exportAll({
3931
- down: () => down$4,
3932
- up: () => up$4
3937
+ down: () => down$12,
3938
+ up: () => up$12
3933
3939
  });
3934
3940
  /**
3935
3941
  * held_runs.environment_id becomes nullable with ON DELETE SET NULL so
@@ -3940,14 +3946,14 @@ var _030_held_runs_env_set_null_exports = /* @__PURE__ */ __exportAll({
3940
3946
  * Idempotent: dropping the NOT NULL and the constraint are both no-ops on a
3941
3947
  * re-run, and the constraint is re-created with the SET NULL action.
3942
3948
  */
3943
- async function up$4(db) {
3949
+ async function up$12(db) {
3944
3950
  await sql`ALTER TABLE public.held_runs ALTER COLUMN environment_id DROP NOT NULL`.execute(db);
3945
3951
  await sql`ALTER TABLE public.held_runs DROP CONSTRAINT IF EXISTS held_runs_environment_id_fkey`.execute(db);
3946
3952
  await sql`ALTER TABLE public.held_runs
3947
3953
  ADD CONSTRAINT held_runs_environment_id_fkey
3948
3954
  FOREIGN KEY (environment_id) REFERENCES public.environments(id) ON DELETE SET NULL`.execute(db);
3949
3955
  }
3950
- async function down$4(db) {
3956
+ async function down$12(db) {
3951
3957
  await sql`ALTER TABLE public.held_runs DROP CONSTRAINT IF EXISTS held_runs_environment_id_fkey`.execute(db);
3952
3958
  await sql`ALTER TABLE public.held_runs
3953
3959
  ADD CONSTRAINT held_runs_environment_id_fkey
@@ -3957,8 +3963,8 @@ async function down$4(db) {
3957
3963
  //#endregion
3958
3964
  //#region src/db/migrations/031_dispatch_queue_ack_deadline.ts
3959
3965
  var _031_dispatch_queue_ack_deadline_exports = /* @__PURE__ */ __exportAll({
3960
- down: () => down$3,
3961
- up: () => up$3
3966
+ down: () => down$11,
3967
+ up: () => up$11
3962
3968
  });
3963
3969
  /**
3964
3970
  * Add `dispatch_queue.ack_deadline TIMESTAMPTZ` and
@@ -3975,7 +3981,7 @@ var _031_dispatch_queue_ack_deadline_exports = /* @__PURE__ */ __exportAll({
3975
3981
  *
3976
3982
  * Idempotent: re-running on a DB that already has either column is a no-op.
3977
3983
  */
3978
- async function up$3(db) {
3984
+ async function up$11(db) {
3979
3985
  const colExists = async (name) => {
3980
3986
  return (await sql`
3981
3987
  SELECT EXISTS (
@@ -4000,7 +4006,7 @@ async function up$3(db) {
4000
4006
  WHERE ack_deadline IS NOT NULL
4001
4007
  `.execute(db);
4002
4008
  }
4003
- async function down$3(db) {
4009
+ async function down$11(db) {
4004
4010
  await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_ack_deadline`.execute(db);
4005
4011
  await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS ack_agent_id`.execute(db);
4006
4012
  await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS ack_deadline`.execute(db);
@@ -4008,8 +4014,8 @@ async function down$3(db) {
4008
4014
  //#endregion
4009
4015
  //#region src/db/migrations/032_org_settings_dispatch_ack_timeout.ts
4010
4016
  var _032_org_settings_dispatch_ack_timeout_exports = /* @__PURE__ */ __exportAll({
4011
- down: () => down$2,
4012
- up: () => up$2
4017
+ down: () => down$10,
4018
+ up: () => up$10
4013
4019
  });
4014
4020
  /**
4015
4021
  * Add `org_settings.dispatch_ack_timeout_ms BIGINT` (nullable).
@@ -4021,7 +4027,7 @@ var _032_org_settings_dispatch_ack_timeout_exports = /* @__PURE__ */ __exportAll
4021
4027
  *
4022
4028
  * Idempotent: a re-run on a DB that already has the column is a no-op.
4023
4029
  */
4024
- async function up$2(db) {
4030
+ async function up$10(db) {
4025
4031
  if ((await sql`
4026
4032
  SELECT EXISTS (
4027
4033
  SELECT 1 FROM information_schema.columns
@@ -4035,7 +4041,7 @@ async function up$2(db) {
4035
4041
  ADD COLUMN dispatch_ack_timeout_ms BIGINT
4036
4042
  `.execute(db);
4037
4043
  }
4038
- async function down$2(db) {
4044
+ async function down$10(db) {
4039
4045
  await sql`
4040
4046
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS dispatch_ack_timeout_ms
4041
4047
  `.execute(db);
@@ -4043,8 +4049,8 @@ async function down$2(db) {
4043
4049
  //#endregion
4044
4050
  //#region src/db/migrations/033_org_settings_approval.ts
4045
4051
  var _033_org_settings_approval_exports = /* @__PURE__ */ __exportAll({
4046
- down: () => down$1,
4047
- up: () => up$1
4052
+ down: () => down$9,
4053
+ up: () => up$9
4048
4054
  });
4049
4055
  /**
4050
4056
  * Add the two approval-policy columns to `org_settings`:
@@ -4061,7 +4067,7 @@ var _033_org_settings_approval_exports = /* @__PURE__ */ __exportAll({
4061
4067
  * and the orchestrator admin route. Idempotent: a re-run on a DB that already
4062
4068
  * has the columns is a no-op (each column is guarded independently).
4063
4069
  */
4064
- async function up$1(db) {
4070
+ async function up$9(db) {
4065
4071
  const colExists = async (column) => {
4066
4072
  return (await sql`
4067
4073
  SELECT EXISTS (
@@ -4081,7 +4087,7 @@ async function up$1(db) {
4081
4087
  ADD COLUMN allow_self_approval BOOLEAN NOT NULL DEFAULT true
4082
4088
  `.execute(db);
4083
4089
  }
4084
- async function down$1(db) {
4090
+ async function down$9(db) {
4085
4091
  await sql`
4086
4092
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS approval_expiry_seconds
4087
4093
  `.execute(db);
@@ -4092,8 +4098,8 @@ async function down$1(db) {
4092
4098
  //#endregion
4093
4099
  //#region src/db/migrations/034_held_runs_generalize.ts
4094
4100
  var _034_held_runs_generalize_exports = /* @__PURE__ */ __exportAll({
4095
- down: () => down,
4096
- up: () => up
4101
+ down: () => down$8,
4102
+ up: () => up$8
4097
4103
  });
4098
4104
  /**
4099
4105
  * Generalize `held_runs` from an environment-only hold into the unified
@@ -4115,7 +4121,7 @@ var _034_held_runs_generalize_exports = /* @__PURE__ */ __exportAll({
4115
4121
  * New `held_run_approvals` table: one row per approver decision, FK to
4116
4122
  * `held_runs.id` (uuid) with ON DELETE CASCADE.
4117
4123
  */
4118
- async function up(db) {
4124
+ async function up$8(db) {
4119
4125
  const colExists = async (column) => {
4120
4126
  return (await sql`
4121
4127
  SELECT EXISTS (
@@ -4148,7 +4154,7 @@ async function up(db) {
4148
4154
  ON public.held_run_approvals USING btree (held_run_id)
4149
4155
  `.execute(db);
4150
4156
  }
4151
- async function down(db) {
4157
+ async function down$8(db) {
4152
4158
  await sql`DROP TABLE IF EXISTS public.held_run_approvals`.execute(db);
4153
4159
  await sql`ALTER TABLE public.held_runs DROP COLUMN IF EXISTS approval_requirement`.execute(db);
4154
4160
  await sql`ALTER TABLE public.held_runs DROP COLUMN IF EXISTS trigger_source`.execute(db);
@@ -4156,6 +4162,359 @@ async function down(db) {
4156
4162
  await sql`ALTER TABLE public.held_runs DROP COLUMN IF EXISTS hold_scope`.execute(db);
4157
4163
  }
4158
4164
  //#endregion
4165
+ //#region src/db/migrations/035_pending_workflow_contexts.ts
4166
+ var _035_pending_workflow_contexts_exports = /* @__PURE__ */ __exportAll({
4167
+ down: () => down$7,
4168
+ up: () => up$7
4169
+ });
4170
+ /**
4171
+ * Pending workflow dispatch context — backs resume of a workflow whose install
4172
+ * gate held. One row per held run: the serializable dispatch inputs needed to
4173
+ * rebuild the WorkflowDispatchContext when the hold releases (reviewer approve,
4174
+ * wait-timer expiry, concurrency slot free). The row is deleted once the resume
4175
+ * dispatch has been kicked off.
4176
+ */
4177
+ async function up$7(db) {
4178
+ await sql`
4179
+ CREATE TABLE IF NOT EXISTS public.pending_workflow_contexts (
4180
+ run_id text PRIMARY KEY,
4181
+ org_id text NOT NULL,
4182
+ context jsonb NOT NULL,
4183
+ created_at timestamp with time zone DEFAULT now() NOT NULL
4184
+ )
4185
+ `.execute(db);
4186
+ }
4187
+ async function down$7(db) {
4188
+ await sql`DROP TABLE IF EXISTS public.pending_workflow_contexts`.execute(db);
4189
+ }
4190
+ //#endregion
4191
+ //#region src/db/migrations/036_attestations.ts
4192
+ var _036_attestations_exports = /* @__PURE__ */ __exportAll({
4193
+ down: () => down$6,
4194
+ up: () => up$6
4195
+ });
4196
+ /**
4197
+ * Add the `attestations` table for build-provenance bundles.
4198
+ *
4199
+ * When a workflow step calls `ctx.attestProvenance`, the agent uploads the
4200
+ * signed KiCI bundle to object storage under
4201
+ * `provenance/{run_id}/{job_id}/{subject_digest}.kici.json` and the
4202
+ * orchestrator records one row here so the dashboard can list and fetch
4203
+ * attestations per run/job.
4204
+ *
4205
+ * Idempotent: a re-run on a DB that already has the table is a no-op.
4206
+ */
4207
+ async function up$6(db) {
4208
+ if ((await sql`
4209
+ SELECT EXISTS (
4210
+ SELECT 1 FROM information_schema.tables
4211
+ WHERE table_schema = 'public'
4212
+ AND table_name = 'attestations'
4213
+ ) AS exists
4214
+ `.execute(db)).rows[0]?.exists) return;
4215
+ await sql`
4216
+ CREATE TABLE public.attestations (
4217
+ id TEXT PRIMARY KEY,
4218
+ run_id TEXT NOT NULL,
4219
+ job_id TEXT NOT NULL,
4220
+ subject_name TEXT NOT NULL,
4221
+ subject_digest TEXT NOT NULL,
4222
+ storage_key TEXT NOT NULL,
4223
+ mode TEXT NOT NULL,
4224
+ media_type TEXT NOT NULL,
4225
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
4226
+ )
4227
+ `.execute(db);
4228
+ await sql`
4229
+ CREATE INDEX idx_attestations_run_job
4230
+ ON public.attestations (run_id, job_id)
4231
+ `.execute(db);
4232
+ }
4233
+ async function down$6(db) {
4234
+ await sql`DROP TABLE IF EXISTS public.attestations`.execute(db);
4235
+ }
4236
+ //#endregion
4237
+ //#region src/db/migrations/037_generic_sources_provider_type_local.ts
4238
+ var _037_generic_sources_provider_type_local_exports = /* @__PURE__ */ __exportAll({
4239
+ down: () => down$5,
4240
+ up: () => up$5
4241
+ });
4242
+ /**
4243
+ * Replace the `generic_webhook_sources.provider_type` CHECK constraint so it
4244
+ * permits `'local'` instead of `'internal'`, and migrate any carried-over
4245
+ * `'internal'` rows to `'local'`.
4246
+ *
4247
+ * The local filesystem (`file://`) provider is `provider_type='local'`; the
4248
+ * value used to be `'internal'`. The original CHECK constraint
4249
+ * (`generic_webhook_sources_provider_type_check`) allowed only
4250
+ * `{'generic','internal'}`, so writing a `'local'` row fails. Universal-git
4251
+ * sources keep `provider_type='generic'` (discriminated by `git_config`), so
4252
+ * the new allowed set is `{'generic','local'}`.
4253
+ *
4254
+ * Idempotent: the constraint is dropped IF EXISTS and recreated; the data
4255
+ * backfill is a plain UPDATE that is a no-op once no `'internal'` rows remain.
4256
+ */
4257
+ async function up$5(db) {
4258
+ await sql`
4259
+ ALTER TABLE public.generic_webhook_sources
4260
+ DROP CONSTRAINT IF EXISTS generic_webhook_sources_provider_type_check
4261
+ `.execute(db);
4262
+ await sql`
4263
+ UPDATE public.generic_webhook_sources
4264
+ SET provider_type = 'local'
4265
+ WHERE provider_type = 'internal'
4266
+ `.execute(db);
4267
+ await sql`
4268
+ ALTER TABLE public.generic_webhook_sources
4269
+ ADD CONSTRAINT generic_webhook_sources_provider_type_check
4270
+ CHECK (provider_type = ANY (ARRAY['generic'::text, 'local'::text]))
4271
+ `.execute(db);
4272
+ }
4273
+ async function down$5(db) {
4274
+ await sql`
4275
+ ALTER TABLE public.generic_webhook_sources
4276
+ DROP CONSTRAINT IF EXISTS generic_webhook_sources_provider_type_check
4277
+ `.execute(db);
4278
+ await sql`
4279
+ UPDATE public.generic_webhook_sources
4280
+ SET provider_type = 'internal'
4281
+ WHERE provider_type = 'local'
4282
+ `.execute(db);
4283
+ await sql`
4284
+ ALTER TABLE public.generic_webhook_sources
4285
+ ADD CONSTRAINT generic_webhook_sources_provider_type_check
4286
+ CHECK (provider_type = ANY (ARRAY['generic'::text, 'internal'::text]))
4287
+ `.execute(db);
4288
+ }
4289
+ //#endregion
4290
+ //#region src/db/migrations/038_remote_sources.ts
4291
+ var _038_remote_sources_exports = /* @__PURE__ */ __exportAll({
4292
+ down: () => down$4,
4293
+ up: () => up$4
4294
+ });
4295
+ /**
4296
+ * `remote_sources` anchors a Platform-relayed `kici run remote` to its real
4297
+ * org. One row per org served by this orchestrator: routing key
4298
+ * `remote:<orgId>` maps to the canonical org id, so `resolveOrgId` resolves
4299
+ * the real tenant through the same local-source path a webhook takes — no
4300
+ * GitHub App, no webhook secret, no manual setup. The row is auto-provisioned
4301
+ * on Platform auth from the canonical org id carried on `auth.success`.
4302
+ *
4303
+ * Idempotent: a re-run on a DB that already has the table is a no-op.
4304
+ */
4305
+ async function up$4(db) {
4306
+ if ((await sql`
4307
+ SELECT EXISTS (
4308
+ SELECT 1 FROM information_schema.tables
4309
+ WHERE table_schema = 'public' AND table_name = 'remote_sources'
4310
+ ) AS exists
4311
+ `.execute(db)).rows[0]?.exists) return;
4312
+ await sql`
4313
+ CREATE TABLE public.remote_sources (
4314
+ customer_id VARCHAR(255) NOT NULL,
4315
+ routing_key VARCHAR(255) NOT NULL,
4316
+ cluster_id TEXT,
4317
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
4318
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
4319
+ CONSTRAINT remote_sources_customer_id_key UNIQUE (customer_id),
4320
+ CONSTRAINT remote_sources_routing_key_key UNIQUE (routing_key)
4321
+ )
4322
+ `.execute(db);
4323
+ }
4324
+ async function down$4(db) {
4325
+ await sql`DROP TABLE IF EXISTS public.remote_sources`.execute(db);
4326
+ }
4327
+ //#endregion
4328
+ //#region src/db/migrations/039_host_roster.ts
4329
+ var _039_host_roster_exports = /* @__PURE__ */ __exportAll({
4330
+ down: () => down$3,
4331
+ up: () => up$3
4332
+ });
4333
+ /**
4334
+ * `host_roster` is KiCI's declared inventory: one durable row per agent the
4335
+ * cluster has ever enrolled, reconciled from the in-memory AgentRegistry on
4336
+ * every register/unregister. `lifecycle_class` (snapshot of the auth token's
4337
+ * agent_type) drives reaping — `ephemeral` rows are GC'd past their TTL,
4338
+ * `static` rows persist and read as `unreachable` when their heartbeat goes
4339
+ * stale. `connected_instance_id` records which orchestrator holds the live WS
4340
+ * (cluster liveness + the host-fanout reroute target); NULL = disconnected.
4341
+ *
4342
+ * The roster lives in the shared cluster DB (one table, all instances). Status
4343
+ * is derived at read from the shared `last_seen` + `connected_instance_id`, so
4344
+ * every instance agrees regardless of which one holds the agent's live WS.
4345
+ *
4346
+ * Idempotent: a re-run on a DB that already has the table is a no-op.
4347
+ */
4348
+ async function up$3(db) {
4349
+ if ((await sql`
4350
+ SELECT EXISTS (
4351
+ SELECT 1 FROM information_schema.tables
4352
+ WHERE table_schema = 'public' AND table_name = 'host_roster'
4353
+ ) AS exists
4354
+ `.execute(db)).rows[0]?.exists) return;
4355
+ await sql`
4356
+ CREATE TABLE public.host_roster (
4357
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
4358
+ agent_id text NOT NULL,
4359
+ token_id uuid,
4360
+ lifecycle_class text NOT NULL,
4361
+ labels text NOT NULL DEFAULT '[]',
4362
+ hostname text,
4363
+ platform text,
4364
+ arch text,
4365
+ connected_instance_id text,
4366
+ last_seen timestamptz NOT NULL DEFAULT now(),
4367
+ created_at timestamptz NOT NULL DEFAULT now(),
4368
+ updated_at timestamptz NOT NULL DEFAULT now(),
4369
+ CONSTRAINT host_roster_pkey PRIMARY KEY (id),
4370
+ CONSTRAINT host_roster_agent_id_key UNIQUE (agent_id),
4371
+ CONSTRAINT host_roster_lifecycle_class_check
4372
+ CHECK (lifecycle_class = ANY (ARRAY['static'::text, 'ephemeral'::text]))
4373
+ )
4374
+ `.execute(db);
4375
+ await sql`CREATE INDEX idx_host_roster_reap
4376
+ ON public.host_roster (lifecycle_class, last_seen)`.execute(db);
4377
+ }
4378
+ async function down$3(db) {
4379
+ await sql`DROP TABLE IF EXISTS public.host_roster`.execute(db);
4380
+ }
4381
+ //#endregion
4382
+ //#region src/db/migrations/040_runsonall_pin.ts
4383
+ var _040_runsonall_pin_exports = /* @__PURE__ */ __exportAll({
4384
+ down: () => down$2,
4385
+ up: () => up$2
4386
+ });
4387
+ /**
4388
+ * Add the `runsOnAll` host fan-out columns:
4389
+ *
4390
+ * - `dispatch_queue.pinned_agent_id TEXT` — when set, the queued/dispatched job
4391
+ * targets exactly that agent (a host-fanout child). The dispatcher routes it
4392
+ * only to that agent; the queue drain never hands it to a different one.
4393
+ * - `execution_jobs.base_job_name` / `variant_kind` / `variant_label` — generic
4394
+ * fan-out columns (matrix + host uniform). `variant_kind` is `'matrix'` or
4395
+ * `'host'`; `variant_label` is the matrix suffix or the hostname. They make the
4396
+ * logical fan-out job first-class server-side so the dashboard groups on real
4397
+ * fields instead of string-parsing the job name. (`matrix_values` / `group_name`
4398
+ * already exist; the matrix path now also backfills `variant_kind='matrix'`.)
4399
+ *
4400
+ * Idempotent: re-running on a DB that already has any column is a no-op.
4401
+ */
4402
+ async function colExists$2(db, table, name) {
4403
+ return (await sql`
4404
+ SELECT EXISTS (
4405
+ SELECT 1 FROM information_schema.columns
4406
+ WHERE table_schema = 'public'
4407
+ AND table_name = ${table}
4408
+ AND column_name = ${name}
4409
+ ) AS exists
4410
+ `.execute(db)).rows[0]?.exists ?? false;
4411
+ }
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);
4417
+ await sql`
4418
+ CREATE INDEX IF NOT EXISTS idx_dispatch_queue_pinned_agent
4419
+ ON public.dispatch_queue (pinned_agent_id)
4420
+ WHERE pinned_agent_id IS NOT NULL
4421
+ `.execute(db);
4422
+ }
4423
+ async function down$2(db) {
4424
+ await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_pinned_agent`.execute(db);
4425
+ await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS pinned_agent_id`.execute(db);
4426
+ await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS variant_label`.execute(db);
4427
+ await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS variant_kind`.execute(db);
4428
+ await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS base_job_name`.execute(db);
4429
+ }
4430
+ //#endregion
4431
+ //#region src/db/migrations/041_wave_gated.ts
4432
+ var _041_wave_gated_exports = /* @__PURE__ */ __exportAll({
4433
+ down: () => down$1,
4434
+ up: () => up$1
4435
+ });
4436
+ /**
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.
4451
+ *
4452
+ * Idempotent: re-running on a DB that already has the columns is a no-op.
4453
+ */
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
+ 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);
4472
+ }
4473
+ 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);
4478
+ }
4479
+ //#endregion
4480
+ //#region src/db/migrations/042_dispatch_queue_patterns.ts
4481
+ var _042_dispatch_queue_patterns_exports = /* @__PURE__ */ __exportAll({
4482
+ down: () => down,
4483
+ up: () => up
4484
+ });
4485
+ /**
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.
4491
+ *
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.
4496
+ *
4497
+ * Idempotent: re-running on a DB that already has the columns is a no-op.
4498
+ */
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
+ 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);
4512
+ }
4513
+ 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);
4516
+ }
4517
+ //#endregion
4159
4518
  //#region src/db/migration-provider.ts
4160
4519
  function createMigrationProvider() {
4161
4520
  return { async getMigrations() {
@@ -4193,7 +4552,15 @@ function createMigrationProvider() {
4193
4552
  "031_dispatch_queue_ack_deadline": _031_dispatch_queue_ack_deadline_exports,
4194
4553
  "032_org_settings_dispatch_ack_timeout": _032_org_settings_dispatch_ack_timeout_exports,
4195
4554
  "033_org_settings_approval": _033_org_settings_approval_exports,
4196
- "034_held_runs_generalize": _034_held_runs_generalize_exports
4555
+ "034_held_runs_generalize": _034_held_runs_generalize_exports,
4556
+ "035_pending_workflow_contexts": _035_pending_workflow_contexts_exports,
4557
+ "036_attestations": _036_attestations_exports,
4558
+ "037_generic_sources_provider_type_local": _037_generic_sources_provider_type_local_exports,
4559
+ "038_remote_sources": _038_remote_sources_exports,
4560
+ "039_host_roster": _039_host_roster_exports,
4561
+ "040_runsonall_pin": _040_runsonall_pin_exports,
4562
+ "041_wave_gated": _041_wave_gated_exports,
4563
+ "042_dispatch_queue_patterns": _042_dispatch_queue_patterns_exports
4197
4564
  };
4198
4565
  } };
4199
4566
  }
@@ -4260,7 +4627,7 @@ async function runMigrations(opts) {
4260
4627
  * as `kici-admin peer create-token` — open a pool from KICI_DATABASE_URL /
4261
4628
  * DATABASE_URL / --database-url.
4262
4629
  */
4263
- function resolveDatabaseUrl$1(explicit) {
4630
+ function resolveDatabaseUrl$2(explicit) {
4264
4631
  const url = explicit ?? process.env.KICI_DATABASE_URL;
4265
4632
  if (!url) throw new Error("Database URL required. Pass --database-url or set KICI_DATABASE_URL.");
4266
4633
  return url;
@@ -4307,7 +4674,7 @@ function registerDbCommands(program, getClient) {
4307
4674
  });
4308
4675
  db.command("fresh").description("DROP + CREATE the orchestrator DB, run migrations, record content hash").option("--database-url <url>", "Target database URL (else KICI_DATABASE_URL / DATABASE_URL)").requiredOption("--confirm", "Explicit confirmation (destructive)").option("--yes", "Skip interactive confirmation (for scripted use)").action(async (opts) => {
4309
4676
  try {
4310
- const url = resolveDatabaseUrl$1(opts.databaseUrl);
4677
+ const url = resolveDatabaseUrl$2(opts.databaseUrl);
4311
4678
  const { dbName } = parseDatabaseUrl(url);
4312
4679
  if (!opts.yes) {
4313
4680
  if (!await confirmInteractive$1(`About to DROP + RECREATE database "${dbName}". Type "${dbName}" to confirm: `, dbName)) {
@@ -4338,7 +4705,7 @@ function registerDbCommands(program, getClient) {
4338
4705
  });
4339
4706
  db.command("ensure <name>").description("CREATE DATABASE IF NOT EXISTS (idempotent)").option("--database-url <url>", "Admin DB URL (else KICI_DATABASE_URL / DATABASE_URL)").option("--owner <role>", "DB owner role (default: URL user). Pass when the admin connection is privileged but the new DB should be owned by a separate non-privileged role.").option("--revoke-connect-public", "After ensure, REVOKE CONNECT ON DATABASE \"<name>\" FROM PUBLIC (recommended on shared clusters).").option("--grant-connect-role <role>", "After ensure (and any --revoke-connect-public), GRANT CONNECT ON DATABASE \"<name>\" TO \"<role>\". Repeatable.", (val, acc) => acc.concat([val]), []).action(async (name, opts) => {
4340
4707
  try {
4341
- const baseUrl = resolveDatabaseUrl$1(opts.databaseUrl);
4708
+ const baseUrl = resolveDatabaseUrl$2(opts.databaseUrl);
4342
4709
  const url = new URL(baseUrl);
4343
4710
  url.pathname = `/${name}`;
4344
4711
  const targetUrl = url.toString();
@@ -4357,7 +4724,7 @@ function registerDbCommands(program, getClient) {
4357
4724
  });
4358
4725
  db.command("create-role").description("CREATE / ALTER ROLE with LOGIN [+ CREATEDB] (idempotent)").option("--database-url <url>", "Admin DB URL (else KICI_DATABASE_URL / DATABASE_URL)").requiredOption("--user <name>", "Role name to create or update").requiredOption("--password <password>", "Role password (raw — quote as needed)").option("--createdb", "Grant CREATEDB to the new role", false).action(async (opts) => {
4359
4726
  try {
4360
- const { adminUrl } = parseDatabaseUrl(resolveDatabaseUrl$1(opts.databaseUrl));
4727
+ const { adminUrl } = parseDatabaseUrl(resolveDatabaseUrl$2(opts.databaseUrl));
4361
4728
  logInvocation(`create-role ${opts.user}`, adminUrl);
4362
4729
  const outcome = await createDbRole(adminUrl, {
4363
4730
  username: opts.user,
@@ -4372,7 +4739,7 @@ function registerDbCommands(program, getClient) {
4372
4739
  });
4373
4740
  db.command("create-readonly-user").description("Create a read-only role with SELECT on all tables + default privileges").option("--database-url <url>", "Target DB URL (must connect as owner)").requiredOption("--user <name>", "Read-only role name").requiredOption("--password <password>", "Role password").action(async (opts) => {
4374
4741
  try {
4375
- const url = resolveDatabaseUrl$1(opts.databaseUrl);
4742
+ const url = resolveDatabaseUrl$2(opts.databaseUrl);
4376
4743
  const { dbName } = parseDatabaseUrl(url);
4377
4744
  logInvocation(`create-readonly-user ${opts.user}`, url);
4378
4745
  const outcome = await createReadOnlyDbUser(url, {
@@ -4387,7 +4754,7 @@ function registerDbCommands(program, getClient) {
4387
4754
  });
4388
4755
  db.command("check-schema").description("Compare bundled migrations vs live schema. Exit 2 on drift.").option("--database-url <url>", "Target DB URL (else KICI_DATABASE_URL / DATABASE_URL)").option("--json", "Emit JSON instead of a human-readable line", false).action(async (opts) => {
4389
4756
  try {
4390
- const pool = createPool(resolveDatabaseUrl$1(opts.databaseUrl));
4757
+ const pool = createPool(resolveDatabaseUrl$2(opts.databaseUrl));
4391
4758
  try {
4392
4759
  const status = await isSchemaCurrent(pool, createMigrationProvider());
4393
4760
  if (opts.json) process.stdout.write(JSON.stringify(status) + "\n");
@@ -4404,7 +4771,7 @@ function registerDbCommands(program, getClient) {
4404
4771
  });
4405
4772
  db.command("collation-check").description("Compare pg_database.datcollversion against the running libc collation version. Exit 2 on drift.").option("--database-url <url>", "Target DB URL (else KICI_DATABASE_URL / DATABASE_URL)").option("--json", "Emit JSON instead of a human-readable line", false).action(async (opts) => {
4406
4773
  try {
4407
- const url = resolveDatabaseUrl$1(opts.databaseUrl);
4774
+ const url = resolveDatabaseUrl$2(opts.databaseUrl);
4408
4775
  const { dbName } = parseDatabaseUrl(url);
4409
4776
  const pool = createPool(url);
4410
4777
  try {
@@ -4434,7 +4801,7 @@ function registerDbCommands(program, getClient) {
4434
4801
  });
4435
4802
  db.command("reindex").description("REINDEX DATABASE CONCURRENTLY <db>. Rebuilds every index under the running libc collation rules. Non-blocking but takes minutes + ~2× temp disk.").option("--database-url <url>", "Target DB URL (else KICI_DATABASE_URL / DATABASE_URL)").requiredOption("--confirm", "Explicit confirmation (destructive — long-running)").requiredOption("--reason <text>", "Reason (recorded in stderr banner)").action(async (opts) => {
4436
4803
  try {
4437
- const url = resolveDatabaseUrl$1(opts.databaseUrl);
4804
+ const url = resolveDatabaseUrl$2(opts.databaseUrl);
4438
4805
  const { dbName } = parseDatabaseUrl(url);
4439
4806
  logInvocation(`reindex ${dbName} (${opts.reason})`, url);
4440
4807
  const pool = createPool(url);
@@ -4451,7 +4818,7 @@ function registerDbCommands(program, getClient) {
4451
4818
  });
4452
4819
  db.command("refresh-collation-version").description("ALTER DATABASE <db> REFRESH COLLATION VERSION. Metadata-only bump; pair with db reindex after a libc-base image rebuild.").option("--database-url <url>", "Target DB URL (else KICI_DATABASE_URL / DATABASE_URL)").requiredOption("--reason <text>", "Reason (recorded in stderr banner)").action(async (opts) => {
4453
4820
  try {
4454
- const url = resolveDatabaseUrl$1(opts.databaseUrl);
4821
+ const url = resolveDatabaseUrl$2(opts.databaseUrl);
4455
4822
  const { dbName } = parseDatabaseUrl(url);
4456
4823
  logInvocation(`refresh-collation-version ${dbName} (${opts.reason})`, url);
4457
4824
  const pool = createPool(url);
@@ -4707,6 +5074,119 @@ promisify(execFile);
4707
5074
  createLogger({ prefix: "universal-git:lock-file" });
4708
5075
  createLogger({ prefix: "universal-git:clone-token" });
4709
5076
  //#endregion
5077
+ //#region src/providers/local/local-source-config.ts
5078
+ /**
5079
+ * Configuration for a local filesystem (`file://`) source, stored as JSONB in
5080
+ * `generic_webhook_sources.git_config` and discriminated from universal-git
5081
+ * config by the row's `provider_type='local'`.
5082
+ *
5083
+ * A local source clones a git repository that already exists on the agent's
5084
+ * filesystem (host path for bare-metal scalers, image-bundled / rootfs /
5085
+ * bind-mounted path for container + Firecracker scalers). The orchestrator does
5086
+ * not verify in-agent reachability — that is the operator's responsibility.
5087
+ */
5088
+ const LocalSourceConfigSchema = z.object({
5089
+ /** Absolute path to the repo (or base dir of repos) on the agent filesystem. */
5090
+ repoBasePath: z.string().min(1).refine((p) => path.isAbsolute(p), { message: "repoBasePath must be an absolute path" }),
5091
+ /** Optional network clone base (git:// / http://) for remote agents that do
5092
+ * not share the orchestrator's filesystem. When unset, file:// is used. */
5093
+ cloneUrlBase: z.string().min(1).optional()
5094
+ }).strict();
5095
+ //#endregion
5096
+ //#region src/cli/commands/local-trigger.ts
5097
+ /**
5098
+ * Build and send the synthetic webhook that triggers a run against a local
5099
+ * filesystem (`file://`) source. The orchestrator's provider-agnostic generic
5100
+ * webhook route (`POST /webhook/:orgId/generic/:sourceId`) dispatches it through
5101
+ * the local provider's normalizer. Shared by the `source trigger-local` CLI
5102
+ * command and the generated `post-receive` hook.
5103
+ *
5104
+ * A local repo has no remote forge to send webhooks, so this is the operator's
5105
+ * way to drive a run: either by hand (`trigger-local`) or on every push via an
5106
+ * installed `post-receive` hook (`install-hook`).
5107
+ */
5108
+ /** Build the GitHub-shaped webhook request the local provider normalizer expects. */
5109
+ function buildLocalTriggerRequest(input) {
5110
+ const body = JSON.stringify({
5111
+ ref: input.ref,
5112
+ after: input.sha,
5113
+ repository: {
5114
+ full_name: input.repoFullName,
5115
+ default_branch: input.defaultBranch
5116
+ }
5117
+ });
5118
+ return {
5119
+ path: `/webhook/${input.orgId}/generic/${input.sourceId}`,
5120
+ headers: {
5121
+ "content-type": "application/json",
5122
+ "x-event-type": input.event,
5123
+ "x-delivery-id": randomUUID()
5124
+ },
5125
+ body
5126
+ };
5127
+ }
5128
+ /** Read HEAD ref + sha from a local git repo (used when the operator omits --ref/--sha). */
5129
+ function readRepoHead(repoPath) {
5130
+ const sha = execFileSync("git", [
5131
+ "-C",
5132
+ repoPath,
5133
+ "rev-parse",
5134
+ "HEAD"
5135
+ ], { encoding: "utf8" }).trim();
5136
+ return {
5137
+ ref: `refs/heads/${execFileSync("git", [
5138
+ "-C",
5139
+ repoPath,
5140
+ "rev-parse",
5141
+ "--abbrev-ref",
5142
+ "HEAD"
5143
+ ], { encoding: "utf8" }).trim()}`,
5144
+ sha
5145
+ };
5146
+ }
5147
+ /** POST the trigger request to the orchestrator base URL. Returns the HTTP status. */
5148
+ async function sendLocalTrigger(baseUrl, req) {
5149
+ return (await fetch(`${baseUrl.replace(/\/$/, "")}${req.path}`, {
5150
+ method: "POST",
5151
+ headers: req.headers,
5152
+ body: req.body
5153
+ })).status;
5154
+ }
5155
+ //#endregion
5156
+ //#region src/cli/commands/local-hook.ts
5157
+ /**
5158
+ * Render + install a git `post-receive` hook that triggers a KiCI run on every
5159
+ * push to a local filesystem (`file://`) source. The hook is a thin wrapper
5160
+ * around `kici-admin source trigger-local`; it carries no logic of its own.
5161
+ */
5162
+ /**
5163
+ * Render the post-receive hook script. For each pushed ref the hook calls
5164
+ * `kici-admin source trigger-local <id> --ref <ref> --sha <new> --base-url <url>`,
5165
+ * so a push to any branch dispatches a run for that ref.
5166
+ */
5167
+ function renderPostReceiveHook(opts) {
5168
+ return `#!/usr/bin/env bash
5169
+ # KiCI local-source post-receive hook (generated by 'kici-admin source install-hook').
5170
+ # Triggers a run for each pushed branch ref.
5171
+ set -euo pipefail
5172
+ while read -r _old _new ref; do
5173
+ kici-admin source trigger-local ${opts.sourceId} --ref "$ref" --sha "$_new" --base-url ${opts.baseUrl}
5174
+ done
5175
+ `;
5176
+ }
5177
+ /**
5178
+ * Write the rendered hook into the repo's `.git/hooks/post-receive` and make it
5179
+ * executable. Throws when the path is not a git repo (no `.git/hooks` dir).
5180
+ */
5181
+ function installPostReceiveHook(repoPath, script) {
5182
+ const hooksDir = path.join(repoPath, ".git", "hooks");
5183
+ if (!existsSync(hooksDir)) throw new Error(`Not a git repo (no .git/hooks): ${repoPath}`);
5184
+ const hookPath = path.join(hooksDir, "post-receive");
5185
+ writeFileSync(hookPath, script, { mode: 493 });
5186
+ chmodSync(hookPath, 493);
5187
+ return hookPath;
5188
+ }
5189
+ //#endregion
4710
5190
  //#region src/cli/commands/source.ts
4711
5191
  /**
4712
5192
  * Source management commands for kici-admin.
@@ -4728,6 +5208,24 @@ createLogger({ prefix: "universal-git:clone-token" });
4728
5208
  * --from-env <VAR_NAME> Read from environment variable
4729
5209
  * --stdin Read from stdin
4730
5210
  */
5211
+ /** Default orchestrator base URL for the webhook trigger route, matching the
5212
+ * kici-admin global `--url` default. */
5213
+ const DEFAULT_ORCH_URL = process.env.KICI_ADMIN_URL ?? "http://localhost:8080";
5214
+ /** Parse a generic source's git_config (string or object) into a LocalSourceConfig.
5215
+ * Returns null when the row is not a valid local source. */
5216
+ function parseLocalConfig(s) {
5217
+ if (s.provider_type !== "local" || !s.git_config) return null;
5218
+ const raw = typeof s.git_config === "string" ? safeParse(s.git_config) : s.git_config;
5219
+ const parsed = LocalSourceConfigSchema.safeParse(raw);
5220
+ return parsed.success ? parsed.data : null;
5221
+ }
5222
+ function safeParse(value) {
5223
+ try {
5224
+ return JSON.parse(value);
5225
+ } catch {
5226
+ return null;
5227
+ }
5228
+ }
4731
5229
  /**
4732
5230
  * Short reason shown in `(unavailable — <reason>)` when no webhook URL could be
4733
5231
  * resolved for an added source.
@@ -4809,7 +5307,11 @@ function formatGenericSource(s) {
4809
5307
  } catch {
4810
5308
  gc = null;
4811
5309
  }
4812
- if (gc) {
5310
+ if (gc && s.provider_type === "local") {
5311
+ console.log(` Local (file://): yes`);
5312
+ if (typeof gc.repoBasePath === "string") console.log(` Repo base path: ${gc.repoBasePath}`);
5313
+ if (typeof gc.cloneUrlBase === "string") console.log(` Clone URL base: ${gc.cloneUrlBase}`);
5314
+ } else if (gc) {
4813
5315
  console.log(` Universal-git: yes`);
4814
5316
  if (typeof gc.preset === "string") console.log(` Preset: ${gc.preset}`);
4815
5317
  if (typeof gc.gitUrlTemplate === "string") console.log(` Git URL template: ${gc.gitUrlTemplate}`);
@@ -4907,7 +5409,7 @@ function registerSourceCommands(program, getClient) {
4907
5409
  process.exit(1);
4908
5410
  }
4909
5411
  });
4910
- add.command("generic").description("Add a new generic webhook source").requiredOption("--org <orgId>", "Organization/customer ID").requiredOption("--name <name>", "Human-readable source name").option("--verification <method>", "Verification method: hmac_sha256, bearer_token, ip_allowlist, none").option("--secret <value>", "Verification secret (HMAC secret or bearer token, 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 (default: 300)", parseInt).option("--max-payload <bytes>", "Maximum payload size in bytes (default: 1048576)", 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 (default: 600)", parseInt).option("--preset <name>", "Universal-git preset: forgejo, gitea, gogs, gitlab-repo, github-repo, custom").option("--git-url-template <url>", "Clone URL template with {owner}/{name}/{repo}").option("--credential-ref <key>", "Secret key name (under __source__/<id> scope)").option("--credential-store <backend>", "Secret backend name (default: pg)").option("--credential-type <type>", "Credential type: pat, basic, ssh").option("--credential-user <user>", "Username for PAT/basic auth (default: x-access-token)").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). Required when --ssh-host-key-policy=pinned").option("--provider-type <type>", "Provider implementation: generic (default) or internal (for E2E/staging internal-webhook routing)").option("--json", "Emit raw JSON (the full source row) instead of formatted text").action(async (opts) => {
5412
+ add.command("generic").description("Add a new generic webhook source").requiredOption("--org <orgId>", "Organization/customer ID").requiredOption("--name <name>", "Human-readable source name").option("--verification <method>", "Verification method: hmac_sha256, bearer_token, ip_allowlist, none").option("--secret <value>", "Verification secret (HMAC secret or bearer token, 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 (default: 300)", parseInt).option("--max-payload <bytes>", "Maximum payload size in bytes (default: 1048576)", 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 (default: 600)", parseInt).option("--preset <name>", "Universal-git preset: forgejo, gitea, gogs, gitlab-repo, github-repo, custom").option("--git-url-template <url>", "Clone URL template with {owner}/{name}/{repo}").option("--credential-ref <key>", "Secret key name (under __source__/<id> scope)").option("--credential-store <backend>", "Secret backend name (default: pg)").option("--credential-type <type>", "Credential type: pat, basic, ssh").option("--credential-user <user>", "Username for PAT/basic auth (default: x-access-token)").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). Required when --ssh-host-key-policy=pinned").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 (opts) => {
4911
5413
  try {
4912
5414
  const secret = await resolveSecret({
4913
5415
  value: opts.secret,
@@ -4959,6 +5461,39 @@ function registerSourceCommands(program, getClient) {
4959
5461
  process.exit(1);
4960
5462
  }
4961
5463
  });
5464
+ add.command("local").description("Register a git repo present on the agent filesystem as a file:// source").requiredOption("--org <orgId>", "Organization/customer ID").requiredOption("--path <dir>", "Absolute path to the repo (or base dir of repos) on the agent filesystem").option("--name <name>", "Human-readable source name", "local").option("--clone-url-base <url>", "Optional git://|http:// base for remote agents that do not share the orchestrator filesystem (default: file://)").option("--json", "Emit raw JSON (the full source row) instead of formatted text").action(async (opts) => {
5465
+ try {
5466
+ if (!path.isAbsolute(opts.path)) {
5467
+ console.error(`Error: --path must be an absolute path: ${opts.path}`);
5468
+ process.exit(1);
5469
+ }
5470
+ const localConfig = { repoBasePath: opts.path };
5471
+ if (opts.cloneUrlBase) localConfig.cloneUrlBase = opts.cloneUrlBase;
5472
+ const s = (await getClient().createGenericSource({
5473
+ orgId: opts.org,
5474
+ name: opts.name,
5475
+ providerType: "local",
5476
+ verificationMethod: "none",
5477
+ localConfig
5478
+ })).source;
5479
+ if (opts.json) {
5480
+ console.log(JSON.stringify(s, null, 2));
5481
+ return;
5482
+ }
5483
+ console.log(`Local source created:`);
5484
+ console.log(` ID: ${s.id}`);
5485
+ console.log(` Name: ${s.name}`);
5486
+ console.log(` Routing key: ${s.routing_key}`);
5487
+ console.log(` Org: ${s.customer_id}`);
5488
+ console.log(` Repo path: ${opts.path}`);
5489
+ console.log("");
5490
+ console.log(`Trigger runs with: kici-admin source trigger-local ${s.id}`);
5491
+ console.log(`Or install a push hook: kici-admin source install-hook ${s.id}`);
5492
+ } catch (err) {
5493
+ console.error(`Error: ${toErrorMessage(err)}`);
5494
+ process.exit(1);
5495
+ }
5496
+ });
4962
5497
  src.command("list-presets").description("List built-in universal-git presets (forge shapes supported out of the box)").option("--format <format>", "Output format: table|json", "table").action((opts) => {
4963
5498
  const rows = Object.entries(UNIVERSAL_GIT_PRESETS).map(([name, def]) => ({
4964
5499
  preset: name,
@@ -5070,7 +5605,7 @@ function registerSourceCommands(program, getClient) {
5070
5605
  process.exit(1);
5071
5606
  }
5072
5607
  });
5073
- 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 internal (for E2E/staging internal-webhook routing)").option("--json", "Emit raw JSON (the full source row) instead of formatted text").action(async (id, opts) => {
5608
+ 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) => {
5074
5609
  try {
5075
5610
  const secret = await resolveSecret({
5076
5611
  value: opts.secret,
@@ -5117,7 +5652,32 @@ function registerSourceCommands(program, getClient) {
5117
5652
  process.exit(1);
5118
5653
  }
5119
5654
  });
5120
- src.command("remove <routingKey>").description("Remove a source (GitHub or generic with --generic flag)").option("--yes", "Skip confirmation").option("--generic", "Remove a generic source (routingKey is treated as source ID)").option("--hard", "Permanently delete a generic source (requires --generic)").action(async (routingKey, opts) => {
5655
+ src.command("update-local <id>").description("Update a local filesystem (file://) source").option("--name <name>", "New name").option("--path <dir>", "New absolute repo base path on the agent filesystem").option("--clone-url-base <url>", "New git://|http:// clone base (default: file://)").option("--json", "Emit raw JSON (the full source row) instead of formatted text").action(async (id, opts) => {
5656
+ try {
5657
+ const data = {};
5658
+ if (opts.name) data.name = opts.name;
5659
+ if (opts.path !== void 0) {
5660
+ if (!path.isAbsolute(opts.path)) {
5661
+ console.error(`Error: --path must be an absolute path: ${opts.path}`);
5662
+ process.exit(1);
5663
+ }
5664
+ const localConfig = { repoBasePath: opts.path };
5665
+ if (opts.cloneUrlBase) localConfig.cloneUrlBase = opts.cloneUrlBase;
5666
+ data.localConfig = localConfig;
5667
+ }
5668
+ if (Object.keys(data).length === 0) {
5669
+ console.error("Error: no fields to update. Provide --path and/or --name.");
5670
+ process.exit(1);
5671
+ }
5672
+ const result = await getClient().updateGenericSource(id, data);
5673
+ if (opts.json) console.log(JSON.stringify(result.source, null, 2));
5674
+ else console.log(`Local source updated: ${result.source.id} (${result.source.name})`);
5675
+ } catch (err) {
5676
+ console.error(`Error: ${toErrorMessage(err)}`);
5677
+ process.exit(1);
5678
+ }
5679
+ });
5680
+ src.command("remove <routingKey>").description("Remove a source (GitHub, or generic/local with --generic / --local)").option("--yes", "Skip confirmation").option("--generic", "Remove a generic source (routingKey is treated as source ID)").option("--local", "Remove a local (file://) source (routingKey is treated as source ID)").option("--hard", "Permanently delete a generic/local source (requires --generic or --local)").action(async (routingKey, opts) => {
5121
5681
  try {
5122
5682
  if (!opts.yes) {
5123
5683
  const rl = createInterface({
@@ -5133,12 +5693,13 @@ function registerSourceCommands(program, getClient) {
5133
5693
  return;
5134
5694
  }
5135
5695
  }
5136
- if (opts.generic) {
5696
+ if (opts.generic || opts.local) {
5137
5697
  const mode = (await getClient().deleteGenericSource(routingKey, opts.hard)).hard ? "permanently deleted" : "soft-deleted";
5138
- console.log(`Generic source ${mode}: ${routingKey}`);
5698
+ const kind = opts.local ? "Local" : "Generic";
5699
+ console.log(`${kind} source ${mode}: ${routingKey}`);
5139
5700
  } else {
5140
5701
  if (opts.hard) {
5141
- console.error("Error: --hard flag is only supported with --generic");
5702
+ console.error("Error: --hard flag is only supported with --generic / --local");
5142
5703
  process.exit(1);
5143
5704
  }
5144
5705
  await getClient().delete(`/api/v1/admin/sources/${encodeURIComponent(routingKey)}`);
@@ -5167,6 +5728,106 @@ function registerSourceCommands(program, getClient) {
5167
5728
  process.exit(1);
5168
5729
  }
5169
5730
  });
5731
+ src.command("trigger-local <id>").description("Trigger a run against a local source (reads HEAD when --ref/--sha omitted)").option("--event <event>", "push | pull_request", "push").option("--ref <ref>", "Git ref (default: repo HEAD branch)").option("--sha <sha>", "Commit SHA (default: repo HEAD)").option("--repo-full-name <name>", "owner/name identifier used in the payload", "local/repo").option("--base-url <url>", "Orchestrator base URL", DEFAULT_ORCH_URL).action(async (id, opts) => {
5732
+ try {
5733
+ const { source } = await getClient().getGenericSource(id);
5734
+ const cfg = parseLocalConfig(source);
5735
+ if (!cfg) {
5736
+ console.error(`Error: no local source with id ${id}`);
5737
+ process.exit(1);
5738
+ }
5739
+ const head = opts.ref && opts.sha ? {
5740
+ ref: opts.ref,
5741
+ sha: opts.sha
5742
+ } : readRepoHead(cfg.repoBasePath);
5743
+ const orgId = source.customer_id;
5744
+ const sourceId = source.name;
5745
+ const req = buildLocalTriggerRequest({
5746
+ orgId,
5747
+ sourceId,
5748
+ repoFullName: opts.repoFullName,
5749
+ event: opts.event === "pull_request" ? "pull_request" : "push",
5750
+ ref: head.ref,
5751
+ sha: head.sha,
5752
+ defaultBranch: head.ref.replace("refs/heads/", "")
5753
+ });
5754
+ const status = await sendLocalTrigger(opts.baseUrl, req);
5755
+ console.log(`Triggered local source ${id}: HTTP ${status}`);
5756
+ if (status >= 400) process.exit(1);
5757
+ } catch (err) {
5758
+ console.error(`Error: ${toErrorMessage(err)}`);
5759
+ process.exit(1);
5760
+ }
5761
+ });
5762
+ src.command("install-hook <id>").description("Install a post-receive hook in the local source repo that triggers runs on push").option("--repo <path>", "Repo path (default: the source repoBasePath)").option("--base-url <url>", "Orchestrator base URL", DEFAULT_ORCH_URL).action(async (id, opts) => {
5763
+ try {
5764
+ const { source } = await getClient().getGenericSource(id);
5765
+ const cfg = parseLocalConfig(source);
5766
+ if (!cfg) {
5767
+ console.error(`Error: no local source with id ${id}`);
5768
+ process.exit(1);
5769
+ }
5770
+ const repoPath = opts.repo ?? cfg.repoBasePath;
5771
+ const hookPath = installPostReceiveHook(repoPath, renderPostReceiveHook({
5772
+ sourceId: id,
5773
+ baseUrl: opts.baseUrl
5774
+ }));
5775
+ console.log(`Installed post-receive hook: ${hookPath}`);
5776
+ console.log(`Every push to ${repoPath} will now trigger a run for source ${id}.`);
5777
+ } catch (err) {
5778
+ console.error(`Error: ${toErrorMessage(err)}`);
5779
+ process.exit(1);
5780
+ }
5781
+ });
5782
+ }
5783
+ /** Read the auto-provisioned remote-source row for an org, if it exists. */
5784
+ async function getRemoteSource(db, orgId) {
5785
+ return db.selectFrom("remote_sources").selectAll().where("customer_id", "=", orgId).executeTakeFirst();
5786
+ }
5787
+ //#endregion
5788
+ //#region src/cli/commands/remote-source.ts
5789
+ init_client();
5790
+ function resolveDatabaseUrl$1(explicit) {
5791
+ const url = explicit ?? process.env.KICI_DATABASE_URL;
5792
+ if (!url) throw new Error("Database URL required. Pass --database-url or set KICI_DATABASE_URL.");
5793
+ return url;
5794
+ }
5795
+ function registerRemoteSourceCommands(program) {
5796
+ program.command("remote-source").description("Inspect the auto-provisioned remote-source org anchor").command("show <orgId>").description("Print the remote_sources anchor row for an org (routing key remote:<orgId>).").option("--database-url <url>", "Orchestrator DB URL (else KICI_DATABASE_URL)").option("--format <format>", "Output format: json|table", "table").action(async (orgId, opts) => {
5797
+ let url;
5798
+ try {
5799
+ url = resolveDatabaseUrl$1(opts.databaseUrl);
5800
+ } catch (err) {
5801
+ console.error(`Error: ${toErrorMessage(err)}`);
5802
+ process.exit(1);
5803
+ return;
5804
+ }
5805
+ const pool = createPool$1(url);
5806
+ const db = createDb$1(pool);
5807
+ try {
5808
+ const row = await getRemoteSource(db, orgId);
5809
+ if (!row) {
5810
+ console.error(`No remote_sources anchor found for org ${orgId}.`);
5811
+ process.exit(1);
5812
+ return;
5813
+ }
5814
+ if (opts.format === "json") {
5815
+ console.log(JSON.stringify(row, null, 2));
5816
+ return;
5817
+ }
5818
+ console.log(`customer_id: ${row.customer_id}`);
5819
+ console.log(`routing_key: ${row.routing_key}`);
5820
+ console.log(`cluster_id: ${row.cluster_id ?? "(none)"}`);
5821
+ console.log(`created_at: ${new Date(row.created_at).toISOString()}`);
5822
+ console.log(`updated_at: ${new Date(row.updated_at).toISOString()}`);
5823
+ } catch (err) {
5824
+ console.error(`Error: ${toErrorMessage(err)}`);
5825
+ process.exit(1);
5826
+ } finally {
5827
+ await db.destroy();
5828
+ await pool.end().catch(() => void 0);
5829
+ }
5830
+ });
5170
5831
  }
5171
5832
  //#endregion
5172
5833
  //#region src/cli/commands/workflow.ts
@@ -6168,7 +6829,7 @@ function registerDebugBundleCommand(program, getClient) {
6168
6829
  safeRequest(() => client.getText("/metrics"))
6169
6830
  ]);
6170
6831
  const output = fs$1.createWriteStream(outputPath);
6171
- const archive = archiver("zip", { zlib: { level: 6 } });
6832
+ const archive = new ZipArchive({ zlib: { level: 6 } });
6172
6833
  const finalized = new Promise((resolve, reject) => {
6173
6834
  output.on("close", resolve);
6174
6835
  archive.on("error", reject);
@@ -6495,8 +7156,18 @@ function isRoot() {
6495
7156
  *
6496
7157
  * Used by the instance index (`<kiciRoot>/instances.json`) which lives
6497
7158
  * outside any per-instance subdir, and as the base for {@link getConfigDir}.
7159
+ *
7160
+ * `KICI_CONFIG_ROOT` overrides the platform default for both privilege scopes.
7161
+ * It exists so a test harness (or a sandboxed run) can point the instance index
7162
+ * at an isolated directory instead of sharing the host's `~/.config/kici/` —
7163
+ * which otherwise lets concurrent installs clobber each other's index.
6498
7164
  */
6499
7165
  function kiciConfigRoot(isUserLevel) {
7166
+ const override = process.env.KICI_CONFIG_ROOT;
7167
+ if (override) {
7168
+ const sep = os.platform() === "win32" ? "\\" : "/";
7169
+ return override.endsWith(sep) ? override : override + sep;
7170
+ }
6500
7171
  const plat = os.platform();
6501
7172
  if (plat === "win32") {
6502
7173
  if (isUserLevel) return (process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local")) + "\\kici\\";
@@ -6644,6 +7315,37 @@ function readKiciVersion() {
6644
7315
  return "unknown";
6645
7316
  }
6646
7317
  }
7318
+ /**
7319
+ * Resolve the `@kici-dev/<component>` package version that a {@link LaunchSpec}
7320
+ * will actually execute, by locating the component's entry script among the
7321
+ * spec's tokens and reading the version from the package.json that owns it.
7322
+ *
7323
+ * Returns null when no resolvable entry script is present (e.g. an opaque
7324
+ * `--binary` install) or the owning package.json is missing / name-mismatched.
7325
+ * This is what lets the npm-source upgrade refuse to report a version it can't
7326
+ * stand behind.
7327
+ */
7328
+ function resolveVersionFromLaunchSpec(spec, component) {
7329
+ const pkgName = `@kici-dev/${component}`;
7330
+ const entryRe = new RegExp(`[/\\\\]@kici-dev[/\\\\]${component}[/\\\\]dist[/\\\\](?:server|standalone)\\.js$`);
7331
+ const entry = [spec.execPath, ...spec.args].find((a) => entryRe.test(a));
7332
+ if (!entry) return null;
7333
+ let dir = path.dirname(entry);
7334
+ for (let i = 0; i < 6; i++) {
7335
+ const candidate = path.join(dir, "package.json");
7336
+ if (fs.existsSync(candidate)) try {
7337
+ const pkg = JSON.parse(fs.readFileSync(candidate, "utf-8"));
7338
+ if (pkg.name === pkgName && typeof pkg.version === "string") return pkg.version;
7339
+ return null;
7340
+ } catch {
7341
+ return null;
7342
+ }
7343
+ const parent = path.dirname(dir);
7344
+ if (parent === dir) break;
7345
+ dir = parent;
7346
+ }
7347
+ return null;
7348
+ }
6647
7349
  //#endregion
6648
7350
  //#region src/cli/service/instance/index-file.ts
6649
7351
  /**
@@ -6721,27 +7423,54 @@ function removeIndexEntry(kiciRoot, key) {
6721
7423
  */
6722
7424
  /**
6723
7425
  * Reconcile <kiciRoot>/instances.json with the driver's native scan, then
6724
- * rewrite the index dropping entries whose units no longer exist. Returns
6725
- * the merged list filtered to the requested component + isUserLevel.
7426
+ * rewrite the index so it mirrors the scanned instances that have a known
7427
+ * instanceDir. Two self-heal directions happen here:
7428
+ *
7429
+ * - backward: drop index entries whose unit no longer exists.
7430
+ * - forward: adopt an instanceDir the driver recovered from the unit marker
7431
+ * when the index entry is missing or carries no dir — so a lost or emptied
7432
+ * index rebuilds itself from the init system on the next read.
7433
+ *
7434
+ * The init system is therefore the source of truth for the name→folder mapping
7435
+ * (via the X-KiCI-InstanceDir / KiCIInstanceDir / dev.kici.instance-dir / [KiCI-DIR]
7436
+ * markers), and the index is a pure cache. Returns the merged list filtered to
7437
+ * the requested component + isUserLevel.
6726
7438
  */
6727
7439
  async function listInstances(args) {
6728
7440
  const { component, isUserLevel, kiciRoot, manager } = args;
6729
7441
  const scanForComponent = (await manager.list(isUserLevel)).filter((s) => s.component === component);
6730
- const scanNames = new Set(scanForComponent.map((s) => s.name));
6731
7442
  const index = readIndex(kiciRoot);
6732
7443
  const relevantIndex = index.filter((e) => e.component === component && e.isUserLevel === isUserLevel);
6733
- const survivors = relevantIndex.filter((e) => scanNames.has(e.name));
6734
- if (survivors.length !== relevantIndex.length) writeIndex(kiciRoot, [...index.filter((e) => !(e.component === component && e.isUserLevel === isUserLevel)), ...survivors]);
6735
- const indexByName = new Map(survivors.map((e) => [e.name, e]));
6736
- return scanForComponent.map((s) => {
7444
+ const indexByName = new Map(relevantIndex.map((e) => [e.name, e]));
7445
+ const listed = scanForComponent.map((s) => {
6737
7446
  const idx = indexByName.get(s.name);
6738
7447
  return {
6739
7448
  ...s,
6740
7449
  component,
6741
- instanceDir: idx?.instanceDir,
7450
+ instanceDir: idx?.instanceDir ?? s.instanceDir,
6742
7451
  source: idx ? "index+scan" : "scan"
6743
7452
  };
6744
7453
  });
7454
+ const reconciled = [];
7455
+ for (const l of listed) {
7456
+ if (!l.instanceDir) continue;
7457
+ reconciled.push({
7458
+ component,
7459
+ name: l.name,
7460
+ platform: l.platform,
7461
+ isUserLevel,
7462
+ instanceDir: l.instanceDir
7463
+ });
7464
+ }
7465
+ if (!sameIndexSet(relevantIndex, reconciled)) writeIndex(kiciRoot, [...index.filter((e) => !(e.component === component && e.isUserLevel === isUserLevel)), ...reconciled]);
7466
+ return listed;
7467
+ }
7468
+ /** Order-insensitive equality of two index-entry sets (by their full identity). */
7469
+ function sameIndexSet(a, b) {
7470
+ if (a.length !== b.length) return false;
7471
+ const key = (e) => `${e.component}\0${e.name}\0${e.platform}\0${e.isUserLevel}\0${e.instanceDir}`;
7472
+ const setA = new Set(a.map(key));
7473
+ return b.every((e) => setA.has(key(e)));
6745
7474
  }
6746
7475
  /**
6747
7476
  * Resolve the target instance for the current lifecycle invocation.
@@ -6863,6 +7592,7 @@ var init_systemd = __esmMin((() => {
6863
7592
  lines.push("[Unit]");
6864
7593
  lines.push(`Description=${config.description}`);
6865
7594
  if (config.component) lines.push(`X-KiCI-Component=${config.component}`);
7595
+ if (config.instanceDir) lines.push(`X-KiCI-InstanceDir=${config.instanceDir}`);
6866
7596
  lines.push("After=network.target postgresql.service");
6867
7597
  lines.push("");
6868
7598
  lines.push("[Service]");
@@ -6999,6 +7729,22 @@ var init_systemd = __esmMin((() => {
6999
7729
  async isInstalled(config) {
7000
7730
  return fs.existsSync(this.unitFilePath(config));
7001
7731
  }
7732
+ async readLaunchSpec(config) {
7733
+ let content;
7734
+ try {
7735
+ content = fs.readFileSync(this.unitFilePath(config), "utf-8");
7736
+ } catch {
7737
+ return null;
7738
+ }
7739
+ const m = content.match(/^ExecStart=(.+?)\s*$/m);
7740
+ if (!m) return null;
7741
+ const tokens = m[1].trim().split(/\s+/).filter(Boolean);
7742
+ if (tokens.length === 0) return null;
7743
+ return {
7744
+ execPath: tokens[0],
7745
+ args: tokens.slice(1)
7746
+ };
7747
+ }
7002
7748
  async list(isUserLevel) {
7003
7749
  const unitDir = isUserLevel ? path.join(os.homedir(), ".config", "systemd", "user") : "/etc/systemd/system";
7004
7750
  if (!fs.existsSync(unitDir)) return [];
@@ -7006,13 +7752,16 @@ var init_systemd = __esmMin((() => {
7006
7752
  for (const entry of fs.readdirSync(unitDir)) {
7007
7753
  if (typeof entry !== "string") continue;
7008
7754
  if (!entry.startsWith("kici-") || !entry.endsWith(".service")) continue;
7009
- const m = fs.readFileSync(path.join(unitDir, entry), "utf-8").match(/^X-KiCI-Component=(orchestrator|agent)\s*$/m);
7755
+ const content = fs.readFileSync(path.join(unitDir, entry), "utf-8");
7756
+ const m = content.match(/^X-KiCI-Component=(orchestrator|agent)\s*$/m);
7010
7757
  if (!m) continue;
7758
+ const dirMatch = content.match(/^X-KiCI-InstanceDir=(.+?)\s*$/m);
7011
7759
  out.push({
7012
7760
  name: entry.replace(/\.service$/, ""),
7013
7761
  platform: "systemd",
7014
7762
  isUserLevel,
7015
- component: m[1]
7763
+ component: m[1],
7764
+ instanceDir: dirMatch?.[1]
7016
7765
  });
7017
7766
  }
7018
7767
  return out;
@@ -7071,6 +7820,10 @@ var init_launchd = __esmMin((() => {
7071
7820
  escapeXml(s) {
7072
7821
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
7073
7822
  }
7823
+ /** Inverse of {@link escapeXml} — decode the entities back to literals. */
7824
+ unescapeXml(s) {
7825
+ return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&amp;/g, "&");
7826
+ }
7074
7827
  /**
7075
7828
  * Generate a launchd plist XML string from a service config.
7076
7829
  * Visible for testing.
@@ -7090,6 +7843,10 @@ var init_launchd = __esmMin((() => {
7090
7843
  lines.push(" <key>KiCIComponent</key>");
7091
7844
  lines.push(` <string>${config.component}</string>`);
7092
7845
  }
7846
+ if (config.instanceDir) {
7847
+ lines.push(" <key>KiCIInstanceDir</key>");
7848
+ lines.push(` <string>${this.escapeXml(config.instanceDir)}</string>`);
7849
+ }
7093
7850
  lines.push(" <key>ProgramArguments</key>");
7094
7851
  lines.push(" <array>");
7095
7852
  lines.push(` <string>${this.escapeXml(config.executablePath)}</string>`);
@@ -7309,15 +8066,33 @@ var init_launchd = __esmMin((() => {
7309
8066
  }
7310
8067
  const match = content.match(/<key>KiCIComponent<\/key>\s*<string>(orchestrator|agent)<\/string>/);
7311
8068
  if (!match) continue;
8069
+ const dirMatch = content.match(/<key>KiCIInstanceDir<\/key>\s*<string>([^<]+)<\/string>/);
7312
8070
  out.push({
7313
8071
  name: entry.replace(/\.plist$/, ""),
7314
8072
  platform: "launchd",
7315
8073
  isUserLevel,
7316
- component: match[1]
8074
+ component: match[1],
8075
+ instanceDir: dirMatch ? this.unescapeXml(dirMatch[1]) : void 0
7317
8076
  });
7318
8077
  }
7319
8078
  return out;
7320
8079
  }
8080
+ async readLaunchSpec(config) {
8081
+ let content;
8082
+ try {
8083
+ content = fs.readFileSync(this.plistPath(config), "utf-8");
8084
+ } catch {
8085
+ return null;
8086
+ }
8087
+ const arr = content.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
8088
+ if (!arr) return null;
8089
+ const strings = [...arr[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((s) => this.unescapeXml(s[1]));
8090
+ if (strings.length === 0) return null;
8091
+ return {
8092
+ execPath: strings[0],
8093
+ args: strings.slice(1)
8094
+ };
8095
+ }
7321
8096
  };
7322
8097
  }));
7323
8098
  //#endregion
@@ -7452,61 +8227,61 @@ var init_registry = __esmMin((() => {
7452
8227
  rolldown: {
7453
8228
  "linux-x64": {
7454
8229
  name: "rolldown",
7455
- version: "1.0.0-rc.9",
8230
+ version: "1.1.1",
7456
8231
  platform: "linux",
7457
8232
  arch: "x64",
7458
- url: "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz",
7459
- sha256: "e97ccb470b8310892a5b5cfae640c728396c16317d1e7c8d305c738f63cfc13b",
8233
+ url: "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.1.tgz",
8234
+ sha256: "55bf455e4d6c5df19636c40b434718891a574214bbf09a50e2e7b21b9f640d56",
7460
8235
  extractPath: "package/",
7461
8236
  archiveType: "tar.gz"
7462
8237
  },
7463
8238
  "linux-arm64": {
7464
8239
  name: "rolldown",
7465
- version: "1.0.0-rc.9",
8240
+ version: "1.1.1",
7466
8241
  platform: "linux",
7467
8242
  arch: "arm64",
7468
- url: "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz",
7469
- sha256: "65b675cd2d065f9cac4acba9cab743269f5f21cfe2d34f18dc4ae5e5a80b01e8",
8243
+ url: "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.1.tgz",
8244
+ sha256: "af458cfd2ce237a5161bdfdcef960e710c394b966537e1d1374840fb7fc78308",
7470
8245
  extractPath: "package/",
7471
8246
  archiveType: "tar.gz"
7472
8247
  },
7473
8248
  "darwin-x64": {
7474
8249
  name: "rolldown",
7475
- version: "1.0.0-rc.9",
8250
+ version: "1.1.1",
7476
8251
  platform: "darwin",
7477
8252
  arch: "x64",
7478
- url: "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz",
7479
- sha256: "a7b282b2c2f809aed6ef8d90fe1412730e8d73de4a67997f3a91d9fca0568dd5",
8253
+ url: "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.1.tgz",
8254
+ sha256: "31bd12457aa41646dcee674d7fa73ec2c4625ef5d34d0ef6c6823cad811de3e2",
7480
8255
  extractPath: "package/",
7481
8256
  archiveType: "tar.gz"
7482
8257
  },
7483
8258
  "darwin-arm64": {
7484
8259
  name: "rolldown",
7485
- version: "1.0.0-rc.9",
8260
+ version: "1.1.1",
7486
8261
  platform: "darwin",
7487
8262
  arch: "arm64",
7488
- url: "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz",
7489
- sha256: "19464bb79c19817892722bfd172874d6853fa07c800deac965cc3c3e7e6e0739",
8263
+ url: "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.1.tgz",
8264
+ sha256: "8399e7a87640ffe485ba16289f6c3a7ab69b25d09b43e5896441372bcf6a02f9",
7490
8265
  extractPath: "package/",
7491
8266
  archiveType: "tar.gz"
7492
8267
  },
7493
8268
  "win32-x64": {
7494
8269
  name: "rolldown",
7495
- version: "1.0.0-rc.9",
8270
+ version: "1.1.1",
7496
8271
  platform: "win32",
7497
8272
  arch: "x64",
7498
- url: "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz",
7499
- sha256: "e359891b2508f9dde392367f70e0e8ad3511a595cf2a6d5ee33115eed8de8e86",
8273
+ url: "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.1.tgz",
8274
+ sha256: "2b2c34d530db0e2cf15cdbe328822d5d385b19ee3b5aba55771587027b9afa17",
7500
8275
  extractPath: "package/",
7501
8276
  archiveType: "tar.gz"
7502
8277
  },
7503
8278
  "win32-arm64": {
7504
8279
  name: "rolldown",
7505
- version: "1.0.0-rc.9",
8280
+ version: "1.1.1",
7506
8281
  platform: "win32",
7507
8282
  arch: "arm64",
7508
- url: "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz",
7509
- sha256: "e5473c970cdc559a392c8d4ffe10ef758712e7fd19e60f67bc1cb189b5f3260a",
8283
+ url: "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.1.tgz",
8284
+ sha256: "d7431cb75d0bed42dd8794b89c3d73ac7129a36465ac020519726f2a9dca1d7c",
7510
8285
  extractPath: "package/",
7511
8286
  archiveType: "tar.gz"
7512
8287
  }
@@ -7615,7 +8390,8 @@ var init_windows = __esmMin((() => {
7615
8390
  cmdParts.push("--", `"${config.executablePath}"`);
7616
8391
  for (const arg of config.args ?? []) cmdParts.push(`"${arg}"`);
7617
8392
  execSync(cmdParts.join(" "), { stdio: "pipe" });
7618
- const descText = config.component ? `[KiCI:${config.component}] ${config.description}` : config.description;
8393
+ const dirSuffix = config.component && config.instanceDir ? ` [KiCI-DIR:${config.instanceDir}]` : "";
8394
+ const descText = config.component ? `[KiCI:${config.component}] ${config.description}${dirSuffix}` : config.description;
7619
8395
  execSync(`sc.exe description ${config.name} "${descText.replace(/"/g, "\\\"")}"`, { stdio: "pipe" });
7620
8396
  execSync(`sc.exe config ${config.name} start= auto`, { stdio: "pipe" });
7621
8397
  const actions = config.restartPolicy.delays.map((d) => `restart/${d * 1e3}`).join("/");
@@ -7726,18 +8502,39 @@ var init_windows = __esmMin((() => {
7726
8502
  for (const row of rows) {
7727
8503
  if (!row || typeof row !== "object") continue;
7728
8504
  const r = row;
7729
- const match = (typeof r.Description === "string" ? r.Description : "").match(/^\[KiCI:(orchestrator|agent)\]/);
8505
+ const desc = typeof r.Description === "string" ? r.Description : "";
8506
+ const match = desc.match(/^\[KiCI:(orchestrator|agent)\]/);
7730
8507
  if (!match) continue;
7731
8508
  if (typeof r.Name !== "string") continue;
8509
+ const dirMatch = desc.match(/\[KiCI-DIR:([^\]]+)\]/);
7732
8510
  out.push({
7733
8511
  name: r.Name,
7734
8512
  platform: "windows",
7735
8513
  isUserLevel,
7736
- component: match[1]
8514
+ component: match[1],
8515
+ instanceDir: dirMatch?.[1]
7737
8516
  });
7738
8517
  }
7739
8518
  return out;
7740
8519
  }
8520
+ async readLaunchSpec(config) {
8521
+ let raw;
8522
+ try {
8523
+ raw = execSync(`sc.exe qc ${config.name}`, { stdio: "pipe" }).toString();
8524
+ } catch {
8525
+ return null;
8526
+ }
8527
+ const m = raw.match(/BINARY_PATH_NAME\s*:\s*(.+)/);
8528
+ if (!m) return null;
8529
+ const sep = m[1].indexOf(" -- ");
8530
+ if (sep === -1) return null;
8531
+ const tokens = [...m[1].slice(sep + 4).matchAll(/"([^"]*)"|(\S+)/g)].map((t) => t[1] ?? t[2]);
8532
+ if (tokens.length === 0 || !tokens[0]) return null;
8533
+ return {
8534
+ execPath: tokens[0],
8535
+ args: tokens.slice(1)
8536
+ };
8537
+ }
7741
8538
  };
7742
8539
  }));
7743
8540
  //#endregion
@@ -7878,6 +8675,7 @@ function generateComposeYaml(config) {
7878
8675
  if (config.component) {
7879
8676
  lines.push(" labels:");
7880
8677
  lines.push(` dev.kici.component: ${config.component}`);
8678
+ if (config.instanceDir) lines.push(` dev.kici.instance-dir: "${config.instanceDir}"`);
7881
8679
  }
7882
8680
  return lines.join("\n") + "\n";
7883
8681
  }
@@ -7970,19 +8768,25 @@ var init_compose = __esmMin((() => {
7970
8768
  }
7971
8769
  if (!row || typeof row !== "object") continue;
7972
8770
  const r = row;
7973
- const m = (typeof r.Labels === "string" ? r.Labels : "").match(/dev\.kici\.component=(orchestrator|agent)/);
8771
+ const labels = typeof r.Labels === "string" ? r.Labels : "";
8772
+ const m = labels.match(/dev\.kici\.component=(orchestrator|agent)/);
7974
8773
  if (!m) continue;
7975
8774
  const name = typeof r.Names === "string" ? r.Names : String(r.Names ?? "");
7976
8775
  if (!name) continue;
8776
+ const dirMatch = labels.match(/dev\.kici\.instance-dir=([^,]+)/);
7977
8777
  out.push({
7978
8778
  name,
7979
8779
  platform: "compose",
7980
8780
  isUserLevel,
7981
- component: m[1]
8781
+ component: m[1],
8782
+ instanceDir: dirMatch?.[1]
7982
8783
  });
7983
8784
  }
7984
8785
  return out;
7985
8786
  }
8787
+ async readLaunchSpec(_config) {
8788
+ return null;
8789
+ }
7986
8790
  };
7987
8791
  }));
7988
8792
  //#endregion
@@ -8104,7 +8908,8 @@ async function resolveUpgradeTarget(args) {
8104
8908
  maxRetries: 5,
8105
8909
  windowSeconds: 300
8106
8910
  },
8107
- component
8911
+ component,
8912
+ instanceDir: resolved.instanceDir
8108
8913
  },
8109
8914
  installBase: resolved.manifest.installBase,
8110
8915
  resolvedInstance: resolved
@@ -8243,6 +9048,51 @@ function updateSymlinkAtomic(installBase, component, version) {
8243
9048
  * 5. Update symlink (Unix) or service registration (Windows)
8244
9049
  * 6. Start service
8245
9050
  */
9051
+ /**
9052
+ * Resolve the target version for an npm-source upgrade (no --from/--url).
9053
+ *
9054
+ * The npm-source flow assumes the operator already ran
9055
+ * `npm install -g @kici-dev/<pkg>@<version>`, which overwrites the global
9056
+ * package in place. The running `kici-admin` binary is therefore the new
9057
+ * version, and `running` is its self-reported version. We default the target
9058
+ * to it, or validate an explicitly-passed --version matches — a mismatch means
9059
+ * the npm install did not actually update the global binary.
9060
+ */
9061
+ function resolveNpmSourceVersion(opts) {
9062
+ if (opts.running === "unknown" || opts.running.trim() === "") throw new Error("npm-source upgrade: could not determine the running package version. Pass --from <archive> / --url <url> + --version for an archive-based upgrade instead.");
9063
+ if (opts.requested && opts.requested !== opts.running) throw new Error(`npm-source upgrade: requested --version ${opts.requested} does not match the installed package version ${opts.running}. Run \`npm install -g @kici-dev/<pkg>@${opts.requested}\` first, then re-run the upgrade.`);
9064
+ return opts.running;
9065
+ }
9066
+ /**
9067
+ * Decide whether an npm-source upgrade may proceed, given the version the
9068
+ * invoking CLI is (`invoked`) and the version the installed unit will actually
9069
+ * launch (`launched`, or null when unresolvable — e.g. an opaque --binary
9070
+ * install). On success `manifestVersion` is the value to persist (null = leave
9071
+ * the manifest's kiciVersion unchanged because we couldn't verify it).
9072
+ */
9073
+ function verifyNpmSourceLaunch(opts) {
9074
+ const { component, invoked, launched, launchedPath, force } = opts;
9075
+ if (launched === null) {
9076
+ if (force) return {
9077
+ ok: true,
9078
+ version: invoked,
9079
+ manifestVersion: null
9080
+ };
9081
+ return {
9082
+ ok: false,
9083
+ reason: "npm-source upgrade aborted: could not determine the version the installed unit will launch (custom --binary install or unparseable launch target). Pass --force to restart without version verification (the manifest version is left unchanged)."
9084
+ };
9085
+ }
9086
+ if (launched !== invoked) return {
9087
+ ok: false,
9088
+ reason: `npm-source upgrade aborted: this kici-admin is version ${invoked}, but the installed unit will launch version ${launched}${launchedPath ? ` (via ${launchedPath})` : ""}. Your \`npm install -g\` updated a different install than the service is pinned to. Install ${invoked} under the unit's runtime (the node its ExecStart points at), or re-run \`kici-admin ${component} install\` to repoint the unit, then retry.`
9089
+ };
9090
+ return {
9091
+ ok: true,
9092
+ version: launched,
9093
+ manifestVersion: launched
9094
+ };
9095
+ }
8246
9096
  async function performVersionedUpgrade(component, opts) {
8247
9097
  try {
8248
9098
  const platform = detectPlatform(opts.platform);
@@ -8272,8 +9122,8 @@ async function performVersionedUpgrade(component, opts) {
8272
9122
  return;
8273
9123
  }
8274
9124
  if (!opts.from && !opts.url) {
8275
- console.error("Error: provide --from <archive-path> or --url <url> for the upgrade package");
8276
- process.exit(1);
9125
+ await performNpmSourceUpgrade(component, config, resolvedInstance, manager, opts);
9126
+ return;
8277
9127
  }
8278
9128
  if (!opts.version) {
8279
9129
  console.error("Error: --version is required to specify the target version");
@@ -8375,6 +9225,54 @@ async function performVersionedUpgrade(component, opts) {
8375
9225
  }
8376
9226
  }
8377
9227
  /**
9228
+ * npm-source upgrade (no --from/--url): the operator has already run
9229
+ * `npm install -g @kici-dev/<pkg>@<version>`, replacing the global package
9230
+ * in place. The systemd/launchd/Windows unit's ExecStart points at that global
9231
+ * package (see install.ts → import.meta.resolve), so a stop+start re-executes
9232
+ * the freshly-installed code. No archive, no versioned dir, no symlink.
9233
+ */
9234
+ async function performNpmSourceUpgrade(component, config, resolvedInstance, manager, opts) {
9235
+ const invoked = readKiciVersion();
9236
+ resolveNpmSourceVersion({
9237
+ requested: opts.version,
9238
+ running: invoked
9239
+ });
9240
+ if (!await manager.isInstalled(config)) {
9241
+ console.error(`Error: service "${config.name}" is not installed`);
9242
+ process.exit(1);
9243
+ }
9244
+ const spec = await manager.readLaunchSpec(config);
9245
+ const verdict = verifyNpmSourceLaunch({
9246
+ component,
9247
+ invoked,
9248
+ launched: spec ? resolveVersionFromLaunchSpec(spec, component) : null,
9249
+ launchedPath: spec?.execPath ?? null,
9250
+ force: opts.force ?? false
9251
+ });
9252
+ if (!verdict.ok) {
9253
+ console.error(`Error: ${verdict.reason}`);
9254
+ process.exit(1);
9255
+ }
9256
+ if (!opts.yes) {
9257
+ console.log(`This will restart "${config.name}" onto the npm-installed version ${verdict.version}.`);
9258
+ console.log(" The service will be stopped briefly during the restart.");
9259
+ console.log("");
9260
+ if (!await confirm$2("Proceed with upgrade?")) {
9261
+ console.log("Upgrade cancelled.");
9262
+ return;
9263
+ }
9264
+ }
9265
+ console.log("Restarting service onto the npm-installed package...");
9266
+ if ((await manager.status(config)).state === "running") await manager.stop(config);
9267
+ await manager.start(config);
9268
+ if (verdict.manifestVersion !== null) writeManifest(resolvedInstance.instanceDir, {
9269
+ ...resolvedInstance.manifest,
9270
+ kiciVersion: verdict.manifestVersion
9271
+ });
9272
+ else console.log("Note: manifest version left unchanged (verification was skipped via --force).");
9273
+ console.log(`Upgrade complete: service "${config.name}" is now running version ${verdict.version}.`);
9274
+ }
9275
+ /**
8378
9276
  * Handle --rollback: switch symlink to the previous version and restart.
8379
9277
  */
8380
9278
  async function handleRollback(component, platform, installBase, config, manager, opts) {
@@ -8722,7 +9620,7 @@ function registerOrchestratorInstall(orchestrator) {
8722
9620
  })).find((c) => c.name === serviceName);
8723
9621
  if (existing && existing.instanceDir !== instanceDir && !opts.force) {
8724
9622
  const at = existing.instanceDir ?? "(no manifest)";
8725
- console.error(`Error: an orchestrator instance "${serviceName}" is already installed at ${at}. Pass a different --name, a different --instance-dir, or --force to overwrite.`);
9623
+ console.error(`Error: an orchestrator instance "${serviceName}" is already installed at ${at}.\n\nUpgrading this service? Don't re-run install — installing again is for first-time\nsetup, not upgrades. For an npm-global install run \`npm install -g kici-admin@latest\`\nthen \`kici-admin orchestrator restart\`; for a versioned-directory install run\n\`kici-admin orchestrator upgrade\`.\n\nInstalling a second, separate instance? Pass a different --name or --instance-dir,\nor --force to take over this one.`);
8726
9624
  process.exit(1);
8727
9625
  }
8728
9626
  const configDir = getConfigDir(serviceName, userLevel);
@@ -8791,6 +9689,7 @@ function registerOrchestratorInstall(orchestrator) {
8791
9689
  isUserLevel: userLevel,
8792
9690
  user: opts.user,
8793
9691
  component: "orchestrator",
9692
+ instanceDir,
8794
9693
  restartPolicy: DEFAULT_RESTART_POLICY
8795
9694
  };
8796
9695
  await manager.install(config);
@@ -9297,6 +10196,7 @@ function registerAgentInstall(agent) {
9297
10196
  workingDirectory: configDir,
9298
10197
  isUserLevel: userLevel,
9299
10198
  component: "agent",
10199
+ instanceDir,
9300
10200
  restartPolicy: DEFAULT_RESTART_POLICY
9301
10201
  };
9302
10202
  await manager.install(config);
@@ -9730,6 +10630,7 @@ const TOKEN_PREFIX = "kici_join_v1";
9730
10630
  const DEFAULT_EXPIRY_MS = 36e5;
9731
10631
  const TOKEN_ALREADY_USED_MESSAGE = "Join token has already been used. Generate a new token with: kici admin create-join-token";
9732
10632
  var JoinTokenManager = class {
10633
+ deps;
9733
10634
  constructor(deps) {
9734
10635
  this.deps = deps;
9735
10636
  }
@@ -9859,6 +10760,7 @@ const DEFAULT_EXPIRY_DAYS = 90;
9859
10760
  * Used by the coordinator to manage credentials for all cluster members.
9860
10761
  */
9861
10762
  var PeerCredentialStore = class {
10763
+ db;
9862
10764
  constructor(db) {
9863
10765
  this.db = db;
9864
10766
  }
@@ -10087,6 +10989,211 @@ function registerPeerCommands(program, _getClient) {
10087
10989
  }
10088
10990
  });
10089
10991
  }
10992
+ /**
10993
+ * The ONE status-derivation function — used by the store and the kici-admin
10994
+ * host CLI. `ready` requires the host to be genuinely live (connected to some
10995
+ * instance AND heartbeat fresh — the freshness check catches a crashed
10996
+ * instance that never cleared `connected_instance_id`). It never returns
10997
+ * `ready` for a not-currently-live host: a not-live `static` reads
10998
+ * `unreachable` (the declared-but-absent alarm), a not-live `ephemeral` reads
10999
+ * `stale` (scaled down, awaiting reap).
11000
+ */
11001
+ function deriveHostStatus(row, nowMs, graceMs) {
11002
+ const ageMs = nowMs - new Date(row.last_seen).getTime();
11003
+ if (row.connected_instance_id !== null && ageMs <= graceMs) return "ready";
11004
+ if (row.lifecycle_class === "ephemeral") return "stale";
11005
+ return "unreachable";
11006
+ }
11007
+ /**
11008
+ * Durable, cluster-shared roster of every agent the cluster has ever enrolled.
11009
+ *
11010
+ * The in-memory `AgentRegistry` reconciles into this table on register
11011
+ * (`upsert`) / unregister (`markDisconnected`), with a coarse heartbeat
11012
+ * (`stampLastSeen`). All liveness writes are owner-guarded by `instanceId` so a
11013
+ * stale disconnect from an old instance can never clobber a row a different
11014
+ * instance now owns. The leader-only reaper deletes `ephemeral` rows past their
11015
+ * ttl via `reapEphemeralPastTtl`; `static` rows persist and read `unreachable`.
11016
+ */
11017
+ var HostRosterStore = class {
11018
+ db;
11019
+ constructor(db) {
11020
+ this.db = db;
11021
+ }
11022
+ /** Idempotent upsert on agent_id; stamps connected_instance_id + last_seen. */
11023
+ async upsert(input) {
11024
+ const labelsJson = JSON.stringify(input.labels);
11025
+ await this.db.insertInto("host_roster").values({
11026
+ agent_id: input.agentId,
11027
+ token_id: input.tokenId,
11028
+ lifecycle_class: input.lifecycleClass,
11029
+ labels: labelsJson,
11030
+ hostname: input.hostname,
11031
+ platform: input.platform,
11032
+ arch: input.arch,
11033
+ connected_instance_id: input.instanceId,
11034
+ last_seen: sql`now()`,
11035
+ updated_at: sql`now()`
11036
+ }).onConflict((oc) => oc.column("agent_id").doUpdateSet({
11037
+ token_id: input.tokenId,
11038
+ lifecycle_class: input.lifecycleClass,
11039
+ labels: labelsJson,
11040
+ hostname: input.hostname,
11041
+ platform: input.platform,
11042
+ arch: input.arch,
11043
+ connected_instance_id: input.instanceId,
11044
+ last_seen: sql`now()`,
11045
+ updated_at: sql`now()`
11046
+ })).execute();
11047
+ }
11048
+ /** Clear liveness on disconnect — but only if THIS instance still owns it. */
11049
+ async markDisconnected(agentId, instanceId) {
11050
+ await this.db.updateTable("host_roster").set({
11051
+ connected_instance_id: null,
11052
+ updated_at: sql`now()`
11053
+ }).where("agent_id", "=", agentId).where("connected_instance_id", "=", instanceId).execute();
11054
+ }
11055
+ /** Coarse heartbeat stamp — same owner-guard as markDisconnected. */
11056
+ async stampLastSeen(agentId, instanceId) {
11057
+ await this.db.updateTable("host_roster").set({ last_seen: sql`now()` }).where("agent_id", "=", agentId).where("connected_instance_id", "=", instanceId).execute();
11058
+ }
11059
+ /** Operator pre-declare of a static host before its agent dials in. */
11060
+ async declareStatic(input) {
11061
+ await this.db.insertInto("host_roster").values({
11062
+ agent_id: input.agentId,
11063
+ token_id: null,
11064
+ lifecycle_class: "static",
11065
+ labels: JSON.stringify(input.labels),
11066
+ hostname: input.hostname ?? null,
11067
+ connected_instance_id: null,
11068
+ last_seen: sql`now()`,
11069
+ updated_at: sql`now()`
11070
+ }).onConflict((oc) => oc.column("agent_id").doNothing()).execute();
11071
+ }
11072
+ async get(agentId) {
11073
+ return await this.db.selectFrom("host_roster").selectAll().where("agent_id", "=", agentId).executeTakeFirst() ?? null;
11074
+ }
11075
+ async listAll() {
11076
+ return this.db.selectFrom("host_roster").selectAll().orderBy("agent_id", "asc").execute();
11077
+ }
11078
+ /**
11079
+ * Resolve every roster host matching a `runsOnAll` predicate (OR-of-AND
11080
+ * include groups, minus exclude labels), tagged with its derived status. This
11081
+ * is the host-fanout resolver: it returns declared-but-absent static hosts
11082
+ * (status `unreachable`) so the caller can apply `onUnreachable` — the live
11083
+ * registry alone cannot name an expected-but-absent host.
11084
+ */
11085
+ async findMatching(include, exclude, graceMs) {
11086
+ const rows = await this.db.selectFrom("host_roster").selectAll().execute();
11087
+ const now = Date.now();
11088
+ const out = [];
11089
+ for (const row of rows) {
11090
+ const labels = JSON.parse(row.labels);
11091
+ const set = new Set(labels);
11092
+ if (exclude.some((e) => matcherSatisfiedBy(e, set))) continue;
11093
+ if (include.length && !include.some((grp) => grp.every((m) => matcherSatisfiedBy(m, set)))) continue;
11094
+ out.push({
11095
+ agentId: row.agent_id,
11096
+ host: row.hostname ?? row.agent_id,
11097
+ labels,
11098
+ lifecycleClass: row.lifecycle_class,
11099
+ connectedInstanceId: row.connected_instance_id,
11100
+ status: deriveHostStatus(row, now, graceMs),
11101
+ platform: row.platform,
11102
+ arch: row.arch
11103
+ });
11104
+ }
11105
+ out.sort((a, b) => a.agentId.localeCompare(b.agentId));
11106
+ return out;
11107
+ }
11108
+ /**
11109
+ * Count `static` (declared) hosts whose derived status is `unreachable` —
11110
+ * the "declared-but-absent" alarm population. Reuses the single-source
11111
+ * {@link deriveHostStatus} so the count never diverges from what
11112
+ * `kici-admin host list` shows. A not-currently-connected static host reads
11113
+ * `unreachable` regardless of grace (only the connected-but-stale case
11114
+ * depends on `graceMs`).
11115
+ */
11116
+ async countStaticUnreachable(graceMs) {
11117
+ const rows = await this.db.selectFrom("host_roster").selectAll().where("lifecycle_class", "=", "static").execute();
11118
+ const now = Date.now();
11119
+ return rows.filter((r) => deriveHostStatus(r, now, graceMs) === "unreachable").length;
11120
+ }
11121
+ /** Delete ephemeral rows whose last_seen is older than ttl. Returns count. */
11122
+ async reapEphemeralPastTtl(ttlMs) {
11123
+ const res = await this.db.deleteFrom("host_roster").where("lifecycle_class", "=", "ephemeral").where("last_seen", "<", sql`now() - (${ttlMs}::text || ' milliseconds')::interval`).executeTakeFirst();
11124
+ return Number(res.numDeletedRows ?? 0n);
11125
+ }
11126
+ };
11127
+ //#endregion
11128
+ //#region src/cli/commands/host.ts
11129
+ /**
11130
+ * The CLI reads the roster DB directly (no full orchestrator config load), so
11131
+ * it sources the grace window from the same env var the orchestrator config
11132
+ * defaults from. Default mirrors `config.ts` rosterGraceMs (300_000).
11133
+ */
11134
+ function rosterGraceMs() {
11135
+ const v = Number(process.env.KICI_ROSTER_GRACE_MS);
11136
+ return Number.isFinite(v) && v > 0 ? v : 3e5;
11137
+ }
11138
+ function formatHostTable(rows) {
11139
+ if (rows.length === 0) return "No hosts in the roster.";
11140
+ const header = "Agent ID | Class | Status | Instance | Last Seen | Labels";
11141
+ const sep = "-".repeat(57);
11142
+ const now = Date.now();
11143
+ const grace = rosterGraceMs();
11144
+ return [
11145
+ header,
11146
+ sep,
11147
+ ...rows.map((r) => {
11148
+ const status = deriveHostStatus(r, now, grace);
11149
+ const labels = JSON.parse(r.labels).join(",");
11150
+ return `${r.agent_id} | ${r.lifecycle_class} | ${status} | ${r.connected_instance_id ?? "-"} | ${new Date(r.last_seen).toISOString()} | ${labels}`;
11151
+ })
11152
+ ].join("\n");
11153
+ }
11154
+ function registerHostCommands(program) {
11155
+ const host = program.command("host").description("Inspect and declare the host roster");
11156
+ host.command("list").description("List all roster hosts").option("--json", "Output JSON").action(async (opts) => {
11157
+ try {
11158
+ const rows = await withDb((db) => new HostRosterStore(db).listAll());
11159
+ if (opts.json) {
11160
+ console.log(JSON.stringify(rows, null, 2));
11161
+ return;
11162
+ }
11163
+ console.log(formatHostTable(rows));
11164
+ } catch (err) {
11165
+ console.error(`Error: ${toErrorMessage(err)}`);
11166
+ process.exit(1);
11167
+ }
11168
+ });
11169
+ host.command("get").description("Show one roster host").requiredOption("--agent-id <id>", "Agent id").option("--json", "Output JSON").action(async (opts) => {
11170
+ try {
11171
+ const row = await withDb((db) => new HostRosterStore(db).get(opts.agentId));
11172
+ if (!row) {
11173
+ console.error(`No host found: ${opts.agentId}`);
11174
+ process.exit(1);
11175
+ }
11176
+ console.log(opts.json ? JSON.stringify(row, null, 2) : formatHostTable([row]));
11177
+ } catch (err) {
11178
+ console.error(`Error: ${toErrorMessage(err)}`);
11179
+ process.exit(1);
11180
+ }
11181
+ });
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) => {
11183
+ try {
11184
+ const labels = opts.labels ? String(opts.labels).split(",").map((s) => s.trim()).filter(Boolean) : [];
11185
+ await withDb((db) => new HostRosterStore(db).declareStatic({
11186
+ agentId: opts.agentId,
11187
+ labels,
11188
+ hostname: opts.hostname
11189
+ }));
11190
+ console.log(`Declared static host: ${opts.agentId}`);
11191
+ } catch (err) {
11192
+ console.error(`Error: ${toErrorMessage(err)}`);
11193
+ process.exit(1);
11194
+ }
11195
+ });
11196
+ }
10090
11197
  //#endregion
10091
11198
  //#region src/cli/commands/org-settings.ts
10092
11199
  function formatSettings(s, format) {
@@ -11448,6 +12555,8 @@ var init_execution_steps = __esmMin((() => {
11448
12555
  enabled: true
11449
12556
  };
11450
12557
  ExecutionStepsAdapter = class {
12558
+ kdb;
12559
+ instanceId;
11451
12560
  db = "orchestrator";
11452
12561
  table = "execution_steps";
11453
12562
  tenantColumn = "routing_key";
@@ -11603,6 +12712,8 @@ var init_execution_jobs = __esmMin((() => {
11603
12712
  enabled: true
11604
12713
  };
11605
12714
  ExecutionJobsAdapter = class {
12715
+ kdb;
12716
+ instanceId;
11606
12717
  db = "orchestrator";
11607
12718
  table = "execution_jobs";
11608
12719
  tenantColumn = "routing_key";
@@ -11771,6 +12882,8 @@ var init_execution_runs = __esmMin((() => {
11771
12882
  enabled: true
11772
12883
  };
11773
12884
  ExecutionRunsAdapter = class {
12885
+ kdb;
12886
+ instanceId;
11774
12887
  db = "orchestrator";
11775
12888
  table = "execution_runs";
11776
12889
  tenantColumn = "routing_key";
@@ -12002,6 +13115,8 @@ var init_secret_audit_log = __esmMin((() => {
12002
13115
  enabled: true
12003
13116
  };
12004
13117
  SecretAuditLogAdapter = class {
13118
+ kdb;
13119
+ instanceId;
12005
13120
  db = "orchestrator";
12006
13121
  table = "secret_audit_log";
12007
13122
  tenantColumn = "routing_key";
@@ -12255,6 +13370,8 @@ var init_access_log = __esmMin((() => {
12255
13370
  enabled: true
12256
13371
  };
12257
13372
  AccessLogAdapter = class {
13373
+ kdb;
13374
+ instanceId;
12258
13375
  db = "orchestrator";
12259
13376
  table = "access_log";
12260
13377
  tenantColumn = "org_id";
@@ -12510,6 +13627,8 @@ var init_event_log = __esmMin((() => {
12510
13627
  enabled: true
12511
13628
  };
12512
13629
  EventLogAdapter = class {
13630
+ kdb;
13631
+ instanceId;
12513
13632
  db = "orchestrator";
12514
13633
  table = "event_log";
12515
13634
  tenantColumn = "routing_key";
@@ -14590,6 +15709,7 @@ var FirecrackerApiError = class extends Error {
14590
15709
  * the `socketPath` option for HTTP-over-Unix-socket communication.
14591
15710
  */
14592
15711
  var FirecrackerApi = class {
15712
+ socketPath;
14593
15713
  constructor(socketPath) {
14594
15714
  this.socketPath = socketPath;
14595
15715
  }
@@ -16577,6 +17697,7 @@ async function writeConfigFile(path, config) {
16577
17697
  await writeFile(path, stringify(config), "utf-8");
16578
17698
  }
16579
17699
  var JoinClient = class {
17700
+ options;
16580
17701
  constructor(options) {
16581
17702
  this.options = options;
16582
17703
  if (!options.platformUrl && !options.peerUrl) throw new Error("Either --platform or --peer must be specified");
@@ -16748,6 +17869,7 @@ function buildProgram() {
16748
17869
  registerConfigCommands(program, getClient);
16749
17870
  registerDbCommands(program, getClient);
16750
17871
  registerSourceCommands(program, getClient);
17872
+ registerRemoteSourceCommands(program);
16751
17873
  registerWorkflowCommands(program, getClient);
16752
17874
  registerRunsCommands(program, getClient);
16753
17875
  registerEventLogCommands(program, getClient);
@@ -16761,6 +17883,7 @@ function buildProgram() {
16761
17883
  registerScalerCommands(program);
16762
17884
  registerAgentServiceCommands(program);
16763
17885
  registerPeerCommands(program, getClient);
17886
+ registerHostCommands(program);
16764
17887
  registerOrgSettingsCommands(program, getClient);
16765
17888
  registerClusterNameCommands(program, getClient);
16766
17889
  registerMaintenanceCommands(program, getClient);
@@ -16779,8 +17902,27 @@ function buildProgram() {
16779
17902
  function runCli(argv = process.argv) {
16780
17903
  buildProgram().parse(argv);
16781
17904
  }
16782
- if (resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) runCli();
17905
+ /**
17906
+ * Canonicalize a path through any symlinks, falling back to a plain `resolve`
17907
+ * when the target does not exist on disk. The entry-point guard compares the
17908
+ * invoked script path against this module's own path; both sides must be fully
17909
+ * canonicalized or a symlinked invocation path silently fails the match. The
17910
+ * canonical example is the light-package launcher running
17911
+ * `node /tmp/.../kici-admin.cjs` on macOS, where `/tmp` is a symlink to
17912
+ * `/private/tmp`: `process.argv[1]` keeps the `/tmp` form while the bundle's
17913
+ * `import.meta.url` resolves to the real `/private/tmp` path, so an
17914
+ * un-canonicalized comparison is always false and the CLI exits 0 without
17915
+ * running any command.
17916
+ */
17917
+ function canonicalize(p) {
17918
+ try {
17919
+ return realpathSync(resolve(p));
17920
+ } catch {
17921
+ return resolve(p);
17922
+ }
17923
+ }
17924
+ if (canonicalize(process.argv[1] ?? "") === canonicalize(fileURLToPath(import.meta.url))) runCli();
16783
17925
  //#endregion
16784
- export { buildProgram, runCli };
17926
+ export { buildProgram, canonicalize, runCli };
16785
17927
 
16786
17928
  //# sourceMappingURL=cli.js.map