@kici-dev/orchestrator 0.1.13 → 0.1.15

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 (65) hide show
  1. package/README.md +13 -1
  2. package/dist/__test-helpers__/mock-db.d.ts +2 -0
  3. package/dist/agent/dispatcher.d.ts +110 -6
  4. package/dist/agent/registry.d.ts +14 -0
  5. package/dist/app.d.ts +11 -0
  6. package/dist/cache/agent-job-failed-error.d.ts +13 -0
  7. package/dist/cache/dispatch-cache-ref-tracker.d.ts +45 -0
  8. package/dist/cache/index.d.ts +2 -0
  9. package/dist/cache/user-cache.d.ts +116 -0
  10. package/dist/cancel/cancel-run.d.ts +56 -0
  11. package/dist/cli/commands/environment.d.ts +1 -0
  12. package/dist/cli/service/index.d.ts +1 -1
  13. package/dist/cli/service/instance/manifest.d.ts +9 -0
  14. package/dist/cli.js +601 -113
  15. package/dist/cluster/peer-registry.d.ts +6 -0
  16. package/dist/config/schema.d.ts +4 -0
  17. package/dist/config.d.ts +13 -0
  18. package/dist/dashboard/handler.d.ts +92 -1
  19. package/dist/db/migrations/025_init_failure.d.ts +16 -0
  20. package/dist/db/migrations/026_event_log_lockfile_corrupt.d.ts +11 -0
  21. package/dist/db/migrations/027_workflow_timeout.d.ts +20 -0
  22. package/dist/db/migrations/028_org_settings_user_cache.d.ts +4 -0
  23. package/dist/db/migrations/029_dispatch_queue_attempts.d.ts +16 -0
  24. package/dist/db/migrations/030_held_runs_env_set_null.d.ts +13 -0
  25. package/dist/db/migrations/031_dispatch_queue_ack_deadline.d.ts +19 -0
  26. package/dist/db/migrations/032_org_settings_dispatch_ack_timeout.d.ts +14 -0
  27. package/dist/db/types.d.ts +52 -2
  28. package/dist/diagnostics/checks/index.d.ts +2 -1
  29. package/dist/diagnostics/checks/scaler.d.ts +13 -0
  30. package/dist/diagnostics/types.d.ts +5 -2
  31. package/dist/environments/environment-store.d.ts +14 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +362 -40
  34. package/dist/lockfile-cache.d.ts +1 -1
  35. package/dist/metrics/prometheus.d.ts +8 -0
  36. package/dist/orchestrator-core.d.ts +4 -1
  37. package/dist/pipeline/dispatch-matched-workflow.d.ts +9 -0
  38. package/dist/pipeline/inline-eval.d.ts +17 -2
  39. package/dist/pipeline/process-webhook.d.ts +19 -0
  40. package/dist/pipeline/processor.d.ts +6 -1
  41. package/dist/pipeline/test-pipeline.d.ts +10 -0
  42. package/dist/providers/github/lock-file.d.ts +1 -1
  43. package/dist/providers/internal/lock-file-fetcher.d.ts +3 -2
  44. package/dist/queue/job-queue.d.ts +53 -1
  45. package/dist/reporting/execution-tracker.d.ts +80 -7
  46. package/dist/routes/admin-environments.d.ts +1 -0
  47. package/dist/scaler/bare-metal-backend.d.ts +1 -0
  48. package/dist/scaler/container-backend.d.ts +3 -2
  49. package/dist/scaler/failure-tracker.d.ts +46 -0
  50. package/dist/scaler/firecracker-backend.d.ts +17 -0
  51. package/dist/scaler/manager.d.ts +10 -0
  52. package/dist/scaler/nftables.d.ts +25 -3
  53. package/dist/scaler/types.d.ts +26 -1
  54. package/dist/server.js +4421 -1459
  55. package/dist/stale-detector/workflow-deadline-detector.d.ts +49 -0
  56. package/dist/standalone.js +17003 -14561
  57. package/dist/storage/filesystem.d.ts +12 -3
  58. package/dist/storage/s3.d.ts +17 -4
  59. package/dist/storage/types.d.ts +25 -5
  60. package/dist/worker/in-memory-job-queue.d.ts +40 -7
  61. package/dist/ws/agent-handler.d.ts +13 -0
  62. package/dist/ws/dashboard-env-handler.d.ts +1 -0
  63. package/dist/ws/platform-client.d.ts +10 -1
  64. package/package.json +13 -10
  65. package/sbom.spdx.json +91 -36
package/dist/cli.js CHANGED
@@ -1,10 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { fileURLToPath as __cjs_fileURLToPath } from "node:url";
3
- import { dirname as __cjs_dirname } from "node:path";
4
- __cjs_dirname(__cjs_fileURLToPath(import.meta.url));
5
2
  import { createRequire } from "node:module";
6
3
  import { Command } from "commander";
7
- import { BaseColdStore, ChunkLru, chunkObjectKey, clearDispatchQueueDirect, computeChunkId, computeMigrationsHash, createDb, createDbRole, createEnvironmentTemplateDirect, createLogger, createPool, createReadOnlyDbUser, decodeChunk, 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";
4
+ import { BaseColdStore, ChunkLru, 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";
8
5
  import * as fs$1 from "node:fs";
9
6
  import fs, { createWriteStream, promises, readFileSync } from "node:fs";
10
7
  import crypto, { createDecipheriv, createHash, hkdfSync, randomBytes, randomUUID } from "node:crypto";
@@ -989,6 +986,8 @@ var init_schema = __esmMin((() => {
989
986
  cacheTtlDays: z.coerce.number().optional(),
990
987
  cacheBuildTimeoutMs: z.coerce.number().optional(),
991
988
  cacheMaxTarballBytes: z.coerce.number().optional(),
989
+ userCacheQuotaBytes: z.coerce.number().optional(),
990
+ userCacheTtlMs: z.coerce.number().optional(),
992
991
  eventRouter: z.object({
993
992
  maxChainDepth: z.coerce.number().optional(),
994
993
  rateLimitPerWorkflowPerMinute: z.coerce.number().optional(),
@@ -1063,6 +1062,8 @@ var init_schema = __esmMin((() => {
1063
1062
  cacheTtlDays: z.coerce.number().default(30),
1064
1063
  cacheBuildTimeoutMs: z.coerce.number().default(6e5),
1065
1064
  cacheMaxTarballBytes: z.coerce.number().default(524288e3),
1065
+ userCacheQuotaBytes: z.coerce.number().default(5 * 1024 * 1024 * 1024),
1066
+ userCacheTtlMs: z.coerce.number().default(10080 * 60 * 1e3),
1066
1067
  eventRouterMaxChainDepth: z.coerce.number().default(10),
1067
1068
  eventRouterRateLimitPerWorkflowPerMinute: z.coerce.number().default(100),
1068
1069
  eventRouterEventTtlSeconds: z.coerce.number().default(604800),
@@ -1517,8 +1518,8 @@ var init_client = __esmMin((() => {
1517
1518
  //#region src/db/migrations/001_initial.ts
1518
1519
  init_client();
1519
1520
  var _001_initial_exports = /* @__PURE__ */ __exportAll({
1520
- down: () => down$23,
1521
- up: () => up$23
1521
+ down: () => down$31,
1522
+ up: () => up$31
1522
1523
  });
1523
1524
  /**
1524
1525
  * Squashed initial migration -- creates the complete Orchestrator database schema.
@@ -2210,7 +2211,7 @@ const DDL_STATEMENTS = [
2210
2211
  `ALTER TABLE ONLY public.held_runs
2211
2212
  ADD CONSTRAINT held_runs_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES public.environments(id);`
2212
2213
  ];
2213
- async function up$23(db) {
2214
+ async function up$31(db) {
2214
2215
  for (const stmt of DDL_STATEMENTS) await sql.raw(stmt).execute(db);
2215
2216
  await sql`
2216
2217
  INSERT INTO cluster_meta (key, value)
@@ -2232,7 +2233,7 @@ async function up$23(db) {
2232
2233
  * Rollback drops everything created above. Uses CASCADE on table drops to cut
2233
2234
  * through the FK graph without relying on exact topological order.
2234
2235
  */
2235
- async function down$23(db) {
2236
+ async function down$31(db) {
2236
2237
  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);
2237
2238
  for (const table of [
2238
2239
  "workflow_registrations",
@@ -2279,8 +2280,8 @@ async function down$23(db) {
2279
2280
  //#endregion
2280
2281
  //#region src/db/migrations/002_config_versions_key_version.ts
2281
2282
  var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
2282
- down: () => down$22,
2283
- up: () => up$22
2283
+ down: () => down$30,
2284
+ up: () => up$30
2284
2285
  });
2285
2286
  /**
2286
2287
  * Add key_version column to config_versions so that sensitive-field encryption
@@ -2294,13 +2295,13 @@ var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
2294
2295
  * No index is needed: rotation does a full-table scan; reads are by
2295
2296
  * `version` primary key and never filter on `key_version`.
2296
2297
  */
2297
- async function up$22(db) {
2298
+ async function up$30(db) {
2298
2299
  await sql`
2299
2300
  ALTER TABLE public.config_versions
2300
2301
  ADD COLUMN key_version integer NOT NULL DEFAULT 1
2301
2302
  `.execute(db);
2302
2303
  }
2303
- async function down$22(db) {
2304
+ async function down$30(db) {
2304
2305
  await sql`
2305
2306
  ALTER TABLE public.config_versions
2306
2307
  DROP COLUMN key_version
@@ -2309,8 +2310,8 @@ async function down$22(db) {
2309
2310
  //#endregion
2310
2311
  //#region src/db/migrations/003_access_log.ts
2311
2312
  var _003_access_log_exports = /* @__PURE__ */ __exportAll({
2312
- down: () => down$21,
2313
- up: () => up$21
2313
+ down: () => down$29,
2314
+ up: () => up$29
2314
2315
  });
2315
2316
  /**
2316
2317
  * Access log: one row per read or orchestrator-admin mutation attributable
@@ -2334,7 +2335,7 @@ var _003_access_log_exports = /* @__PURE__ */ __exportAll({
2334
2335
  * Retention is TTL-based via expires_at; packages/orchestrator/src/queue/
2335
2336
  * cleanup.ts picks up the prune pass.
2336
2337
  */
2337
- async function up$21(db) {
2338
+ async function up$29(db) {
2338
2339
  await sql`
2339
2340
  CREATE TABLE public.access_log (
2340
2341
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -2372,28 +2373,28 @@ async function up$21(db) {
2372
2373
  ON public.access_log (actor_type, actor_id, created_at DESC)
2373
2374
  `.execute(db);
2374
2375
  }
2375
- async function down$21(db) {
2376
+ async function down$29(db) {
2376
2377
  await sql`DROP TABLE IF EXISTS public.access_log`.execute(db);
2377
2378
  }
2378
2379
  //#endregion
2379
2380
  //#region src/db/migrations/004_rename_bundle_to_source.ts
2380
2381
  var _004_rename_bundle_to_source_exports = /* @__PURE__ */ __exportAll({
2381
- down: () => down$20,
2382
- up: () => up$20
2382
+ down: () => down$28,
2383
+ up: () => up$28
2383
2384
  });
2384
- async function up$20(db) {
2385
+ async function up$28(db) {
2385
2386
  await db.schema.alterTable("dispatch_queue").renameColumn("bundle_url", "source_tar_url").execute();
2386
2387
  await db.schema.alterTable("dispatch_queue").renameColumn("bundle_hash", "source_tar_hash").execute();
2387
2388
  }
2388
- async function down$20(db) {
2389
+ async function down$28(db) {
2389
2390
  await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_url", "bundle_url").execute();
2390
2391
  await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_hash", "bundle_hash").execute();
2391
2392
  }
2392
2393
  //#endregion
2393
2394
  //#region src/db/migrations/005_cold_store_chunk_counter.ts
2394
2395
  var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
2395
- down: () => down$19,
2396
- up: () => up$19
2396
+ down: () => down$27,
2397
+ up: () => up$27
2397
2398
  });
2398
2399
  /**
2399
2400
  * Cold-store chunk counter table.
@@ -2410,7 +2411,7 @@ var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
2410
2411
  *
2411
2412
  * sections 5 and 8.
2412
2413
  */
2413
- async function up$19(db) {
2414
+ async function up$27(db) {
2414
2415
  await sql`
2415
2416
  CREATE TABLE public.cold_store_chunk_counts (
2416
2417
  db TEXT NOT NULL,
@@ -2428,14 +2429,14 @@ async function up$19(db) {
2428
2429
  ON public.cold_store_chunk_counts (db, table_name)
2429
2430
  `.execute(db);
2430
2431
  }
2431
- async function down$19(db) {
2432
+ async function down$27(db) {
2432
2433
  await sql`DROP TABLE IF EXISTS public.cold_store_chunk_counts`.execute(db);
2433
2434
  }
2434
2435
  //#endregion
2435
2436
  //#region src/db/migrations/006_runs_jobs_steps_archived_at.ts
2436
2437
  var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
2437
- down: () => down$18,
2438
- up: () => up$18
2438
+ down: () => down$26,
2439
+ up: () => up$26
2439
2440
  });
2440
2441
  /**
2441
2442
  * `execution_runs` / `execution_jobs` / `execution_steps` cold-store
@@ -2471,7 +2472,7 @@ var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
2471
2472
  * - `idx_execution_jobs_routing_key_created (routing_key, created_at)`
2472
2473
  * - `idx_execution_steps_routing_key_created (routing_key, created_at)`
2473
2474
  */
2474
- async function up$18(db) {
2475
+ async function up$26(db) {
2475
2476
  await sql`
2476
2477
  ALTER TABLE public.execution_runs
2477
2478
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2516,7 +2517,7 @@ async function up$18(db) {
2516
2517
  ON public.execution_steps (routing_key, created_at)
2517
2518
  `.execute(db);
2518
2519
  }
2519
- async function down$18(db) {
2520
+ async function down$26(db) {
2520
2521
  await sql`DROP INDEX IF EXISTS public.idx_execution_steps_routing_key_created`.execute(db);
2521
2522
  await sql`
2522
2523
  ALTER TABLE public.execution_steps
@@ -2541,8 +2542,8 @@ async function down$18(db) {
2541
2542
  //#endregion
2542
2543
  //#region src/db/migrations/007_audit_logs_archived_at.ts
2543
2544
  var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
2544
- down: () => down$17,
2545
- up: () => up$17
2545
+ down: () => down$25,
2546
+ up: () => up$25
2546
2547
  });
2547
2548
  /**
2548
2549
  * `secret_audit_log` and `access_log` cold-store schema additions, plus
@@ -2582,7 +2583,7 @@ var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
2582
2583
  * `down()` would not have meaningful retention bounds. Acceptable for
2583
2584
  * staging.
2584
2585
  */
2585
- async function up$17(db) {
2586
+ async function up$25(db) {
2586
2587
  await sql`
2587
2588
  ALTER TABLE public.secret_audit_log
2588
2589
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2603,7 +2604,7 @@ async function up$17(db) {
2603
2604
  DROP COLUMN IF EXISTS expires_at
2604
2605
  `.execute(db);
2605
2606
  }
2606
- async function down$17(db) {
2607
+ async function down$25(db) {
2607
2608
  await sql`
2608
2609
  ALTER TABLE public.access_log
2609
2610
  ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '90 days')
@@ -2631,8 +2632,8 @@ async function down$17(db) {
2631
2632
  //#endregion
2632
2633
  //#region src/db/migrations/008_event_log_archived_at.ts
2633
2634
  var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
2634
- down: () => down$16,
2635
- up: () => up$16
2635
+ down: () => down$24,
2636
+ up: () => up$24
2636
2637
  });
2637
2638
  /**
2638
2639
  * `event_log` cold-store schema additions plus removal of the
@@ -2670,7 +2671,7 @@ var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
2670
2671
  * — best effort; rows inserted between `up()` and a hypothetical
2671
2672
  * `down()` would not have meaningful retention bounds.
2672
2673
  */
2673
- async function up$16(db) {
2674
+ async function up$24(db) {
2674
2675
  await sql`
2675
2676
  ALTER TABLE public.event_log
2676
2677
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2686,7 +2687,7 @@ async function up$16(db) {
2686
2687
  DROP COLUMN IF EXISTS expires_at
2687
2688
  `.execute(db);
2688
2689
  }
2689
- async function down$16(db) {
2690
+ async function down$24(db) {
2690
2691
  await sql`
2691
2692
  ALTER TABLE public.event_log
2692
2693
  ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '30 days')
@@ -2705,8 +2706,8 @@ async function down$16(db) {
2705
2706
  //#endregion
2706
2707
  //#region src/db/migrations/009_access_log_trigram.ts
2707
2708
  var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
2708
- down: () => down$15,
2709
- up: () => up$15
2709
+ down: () => down$23,
2710
+ up: () => up$23
2710
2711
  });
2711
2712
  /**
2712
2713
  * Trigram (pg_trgm) index on access_log.error_message for the federated
@@ -2719,7 +2720,7 @@ var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
2719
2720
  * EXISTS` are both safe to re-run. No CONCURRENTLY because Kysely runs
2720
2721
  * migrations inside a transaction; the lock is brief on a sampled table.
2721
2722
  */
2722
- async function up$15(db) {
2723
+ async function up$23(db) {
2723
2724
  await sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`.execute(db);
2724
2725
  await sql`
2725
2726
  CREATE INDEX IF NOT EXISTS access_log_error_message_trgm_idx
@@ -2728,14 +2729,14 @@ async function up$15(db) {
2728
2729
  WHERE error_message IS NOT NULL
2729
2730
  `.execute(db);
2730
2731
  }
2731
- async function down$15(db) {
2732
+ async function down$23(db) {
2732
2733
  await sql`DROP INDEX IF EXISTS public.access_log_error_message_trgm_idx`.execute(db);
2733
2734
  }
2734
2735
  //#endregion
2735
2736
  //#region src/db/migrations/010_cold_store_chunks.ts
2736
2737
  var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
2737
- down: () => down$14,
2738
- up: () => up$14
2738
+ down: () => down$22,
2739
+ up: () => up$22
2739
2740
  });
2740
2741
  /**
2741
2742
  * Cold-store chunk index — Phase 2 (cold-store purge).
@@ -2767,7 +2768,7 @@ var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
2767
2768
  * forever. Adapters that don't opt into per-bucket archival via
2768
2769
  * `coldTtlDays` don't insert here either.
2769
2770
  */
2770
- async function up$14(db) {
2771
+ async function up$22(db) {
2771
2772
  await sql`
2772
2773
  CREATE TABLE public.cold_store_chunks (
2773
2774
  db TEXT NOT NULL,
@@ -2794,14 +2795,14 @@ async function up$14(db) {
2794
2795
  ON public.cold_store_chunks (db, table_name, tenant_id, archived_at DESC)
2795
2796
  `.execute(db);
2796
2797
  }
2797
- async function down$14(db) {
2798
+ async function down$22(db) {
2798
2799
  await sql`DROP TABLE IF EXISTS public.cold_store_chunks`.execute(db);
2799
2800
  }
2800
2801
  //#endregion
2801
2802
  //#region src/db/migrations/011_drop_source_secrets_notify.ts
2802
2803
  var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
2803
- down: () => down$13,
2804
- up: () => up$13
2804
+ down: () => down$21,
2805
+ up: () => up$21
2805
2806
  });
2806
2807
  /**
2807
2808
  * Drop the `source_secrets_change_trigger` and the
@@ -2820,11 +2821,11 @@ var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
2820
2821
  * in `001_initial.ts`. They wake up no consumer until the `WebhookSecretManager`
2821
2822
  * is restored, so this migration is safe to roll back.
2822
2823
  */
2823
- async function up$13(db) {
2824
+ async function up$21(db) {
2824
2825
  await sql`DROP TRIGGER IF EXISTS source_secrets_change_trigger ON public.scoped_secrets`.execute(db);
2825
2826
  await sql`DROP FUNCTION IF EXISTS public.notify_source_secrets_change() CASCADE`.execute(db);
2826
2827
  }
2827
- async function down$13(db) {
2828
+ async function down$21(db) {
2828
2829
  await sql`
2829
2830
  CREATE OR REPLACE FUNCTION public.notify_source_secrets_change() RETURNS trigger
2830
2831
  LANGUAGE plpgsql
@@ -2863,8 +2864,8 @@ async function down$13(db) {
2863
2864
  //#endregion
2864
2865
  //#region src/db/migrations/012_peer_credentials_active_uniq.ts
2865
2866
  var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
2866
- down: () => down$12,
2867
- up: () => up$12
2867
+ down: () => down$20,
2868
+ up: () => up$20
2868
2869
  });
2869
2870
  /**
2870
2871
  * Add a partial unique index on `peer_credentials (instance_id) WHERE
@@ -2890,7 +2891,7 @@ var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
2890
2891
  * `down()` only drops the index; it does NOT undo the dedupe (there's no
2891
2892
  * safe way to recreate revoked rows, and the dedupe is monotonic).
2892
2893
  */
2893
- async function up$12(db) {
2894
+ async function up$20(db) {
2894
2895
  await sql`
2895
2896
  UPDATE public.peer_credentials
2896
2897
  SET revoked_at = NOW()
@@ -2908,14 +2909,14 @@ async function up$12(db) {
2908
2909
  WHERE revoked_at IS NULL
2909
2910
  `.execute(db);
2910
2911
  }
2911
- async function down$12(db) {
2912
+ async function down$20(db) {
2912
2913
  await sql`DROP INDEX IF EXISTS public.peer_credentials_active_uniq`.execute(db);
2913
2914
  }
2914
2915
  //#endregion
2915
2916
  //#region src/db/migrations/013_execution_log_bytes.ts
2916
2917
  var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
2917
- down: () => down$11,
2918
- up: () => up$11
2918
+ down: () => down$19,
2919
+ up: () => up$19
2919
2920
  });
2920
2921
  /**
2921
2922
  * Add `log_bytes BIGINT NOT NULL DEFAULT 0` columns to `execution_runs` and
@@ -2934,7 +2935,7 @@ var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
2934
2935
  *
2935
2936
  * Idempotent (`ADD COLUMN IF NOT EXISTS`).
2936
2937
  */
2937
- async function up$11(db) {
2938
+ async function up$19(db) {
2938
2939
  await sql`
2939
2940
  ALTER TABLE public.execution_runs
2940
2941
  ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
@@ -2944,15 +2945,15 @@ async function up$11(db) {
2944
2945
  ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
2945
2946
  `.execute(db);
2946
2947
  }
2947
- async function down$11(db) {
2948
+ async function down$19(db) {
2948
2949
  await sql`ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS log_bytes`.execute(db);
2949
2950
  await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS log_bytes`.execute(db);
2950
2951
  }
2951
2952
  //#endregion
2952
2953
  //#region src/db/migrations/014_kici_events_lease_retry.ts
2953
2954
  var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
2954
- down: () => down$10,
2955
- up: () => up$10
2955
+ down: () => down$18,
2956
+ up: () => up$18
2956
2957
  });
2957
2958
  /**
2958
2959
  * Add lease + retry + DLQ columns to `kici_events` so the EventRouter can
@@ -2986,7 +2987,7 @@ var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
2986
2987
  *
2987
2988
  * Idempotent (`ADD COLUMN IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS`).
2988
2989
  */
2989
- async function up$10(db) {
2990
+ async function up$18(db) {
2990
2991
  await sql`
2991
2992
  ALTER TABLE public.kici_events
2992
2993
  ADD COLUMN IF NOT EXISTS claimed_at TIMESTAMPTZ,
@@ -3017,7 +3018,7 @@ async function up$10(db) {
3017
3018
  WHERE dlq_at IS NOT NULL
3018
3019
  `.execute(db);
3019
3020
  }
3020
- async function down$10(db) {
3021
+ async function down$18(db) {
3021
3022
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_dlq`.execute(db);
3022
3023
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_lease_expired`.execute(db);
3023
3024
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_retry_due`.execute(db);
@@ -3035,8 +3036,8 @@ async function down$10(db) {
3035
3036
  //#endregion
3036
3037
  //#region src/db/migrations/015_org_settings_customer_scoped.ts
3037
3038
  var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
3038
- down: () => down$9,
3039
- up: () => up$9
3039
+ down: () => down$17,
3040
+ up: () => up$17
3040
3041
  });
3041
3042
  /**
3042
3043
  * Org-scope `org_settings` and qualify each glob entry by source.
@@ -3064,7 +3065,7 @@ var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
3064
3065
  * Idempotent: a re-run on an already-migrated DB sees `customer_id` exists
3065
3066
  * and the list columns are already jsonb, so it is a no-op.
3066
3067
  */
3067
- async function up$9(db) {
3068
+ async function up$17(db) {
3068
3069
  if ((await sql`
3069
3070
  SELECT EXISTS (
3070
3071
  SELECT 1 FROM information_schema.columns
@@ -3189,7 +3190,7 @@ async function up$9(db) {
3189
3190
  await sql`DROP TABLE _org_settings_merged`.execute(db);
3190
3191
  await sql`DROP TABLE _org_settings_stage`.execute(db);
3191
3192
  }
3192
- async function down$9(db) {
3193
+ async function down$17(db) {
3193
3194
  if (!(await sql`
3194
3195
  SELECT EXISTS (
3195
3196
  SELECT 1 FROM information_schema.columns
@@ -3214,8 +3215,8 @@ async function down$9(db) {
3214
3215
  //#endregion
3215
3216
  //#region src/db/migrations/016_org_settings_allow_http_npm.ts
3216
3217
  var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
3217
- down: () => down$8,
3218
- up: () => up$8
3218
+ down: () => down$16,
3219
+ up: () => up$16
3219
3220
  });
3220
3221
  /**
3221
3222
  * Add `org_settings.allow_http_npm_registries boolean NOT NULL DEFAULT false`.
@@ -3228,7 +3229,7 @@ var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
3228
3229
  *
3229
3230
  * Idempotent: a re-run on a DB that already has the column is a no-op.
3230
3231
  */
3231
- async function up$8(db) {
3232
+ async function up$16(db) {
3232
3233
  if ((await sql`
3233
3234
  SELECT EXISTS (
3234
3235
  SELECT 1 FROM information_schema.columns
@@ -3242,7 +3243,7 @@ async function up$8(db) {
3242
3243
  ADD COLUMN allow_http_npm_registries boolean NOT NULL DEFAULT false
3243
3244
  `.execute(db);
3244
3245
  }
3245
- async function down$8(db) {
3246
+ async function down$16(db) {
3246
3247
  await sql`
3247
3248
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS allow_http_npm_registries
3248
3249
  `.execute(db);
@@ -3250,8 +3251,8 @@ async function down$8(db) {
3250
3251
  //#endregion
3251
3252
  //#region src/db/migrations/017_org_id_widen.ts
3252
3253
  var _017_org_id_widen_exports = /* @__PURE__ */ __exportAll({
3253
- down: () => down$7,
3254
- up: () => up$7
3254
+ down: () => down$15,
3255
+ up: () => up$15
3255
3256
  });
3256
3257
  /**
3257
3258
  * Widen every orchestrator-side `org_id varchar(12)` column to
@@ -3291,17 +3292,17 @@ const ORG_ID_TABLES$1 = [
3291
3292
  "held_runs",
3292
3293
  "scoped_secrets"
3293
3294
  ];
3294
- async function up$7(db) {
3295
+ async function up$15(db) {
3295
3296
  for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(16)`).execute(db);
3296
3297
  }
3297
- async function down$7(db) {
3298
+ async function down$15(db) {
3298
3299
  for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(12)`).execute(db);
3299
3300
  }
3300
3301
  //#endregion
3301
3302
  //#region src/db/migrations/018_org_id_prefix_backfill.ts
3302
3303
  var _018_org_id_prefix_backfill_exports = /* @__PURE__ */ __exportAll({
3303
- down: () => down$6,
3304
- up: () => up$6
3304
+ down: () => down$14,
3305
+ up: () => up$14
3305
3306
  });
3306
3307
  /**
3307
3308
  * Prefix every orchestrator-side tenant string with `org_` to align
@@ -3346,19 +3347,19 @@ const CUSTOMER_ID_TABLES = [
3346
3347
  "workflow_registrations",
3347
3348
  "org_settings"
3348
3349
  ];
3349
- async function up$6(db) {
3350
+ async function up$14(db) {
3350
3351
  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);
3351
3352
  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);
3352
3353
  }
3353
- async function down$6(db) {
3354
+ async function down$14(db) {
3354
3355
  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);
3355
3356
  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);
3356
3357
  }
3357
3358
  //#endregion
3358
3359
  //#region src/db/migrations/019_generic_sources_change_notify.ts
3359
3360
  var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
3360
- down: () => down$5,
3361
- up: () => up$5
3361
+ down: () => down$13,
3362
+ up: () => up$13
3362
3363
  });
3363
3364
  /**
3364
3365
  * Add a Postgres trigger on `generic_webhook_sources` that emits
@@ -3381,7 +3382,7 @@ var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
3381
3382
  * (`notify_sources_change()` + `sources_change_trigger`, defined in
3382
3383
  * `001_initial.ts`).
3383
3384
  */
3384
- async function up$5(db) {
3385
+ async function up$13(db) {
3385
3386
  await sql`
3386
3387
  CREATE FUNCTION public.notify_generic_sources_change() RETURNS trigger
3387
3388
  LANGUAGE plpgsql
@@ -3402,15 +3403,15 @@ async function up$5(db) {
3402
3403
  FOR EACH ROW EXECUTE FUNCTION public.notify_generic_sources_change()
3403
3404
  `.execute(db);
3404
3405
  }
3405
- async function down$5(db) {
3406
+ async function down$13(db) {
3406
3407
  await sql`DROP TRIGGER IF EXISTS generic_sources_change_trigger ON public.generic_webhook_sources`.execute(db);
3407
3408
  await sql`DROP FUNCTION IF EXISTS public.notify_generic_sources_change()`.execute(db);
3408
3409
  }
3409
3410
  //#endregion
3410
3411
  //#region src/db/migrations/020_org_settings_dashboard_write_policy.ts
3411
3412
  var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportAll({
3412
- down: () => down$4,
3413
- up: () => up$4
3413
+ down: () => down$12,
3414
+ up: () => up$12
3414
3415
  });
3415
3416
  /**
3416
3417
  * Add `org_settings.dashboard_write_policy jsonb NOT NULL DEFAULT '{}'`.
@@ -3425,7 +3426,7 @@ var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportA
3425
3426
  *
3426
3427
  * Idempotent: a re-run on a DB that already has the column is a no-op.
3427
3428
  */
3428
- async function up$4(db) {
3429
+ async function up$12(db) {
3429
3430
  if ((await sql`
3430
3431
  SELECT EXISTS (
3431
3432
  SELECT 1 FROM information_schema.columns
@@ -3439,7 +3440,7 @@ async function up$4(db) {
3439
3440
  ADD COLUMN dashboard_write_policy jsonb NOT NULL DEFAULT '{}'::jsonb
3440
3441
  `.execute(db);
3441
3442
  }
3442
- async function down$4(db) {
3443
+ async function down$12(db) {
3443
3444
  await sql`
3444
3445
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS dashboard_write_policy
3445
3446
  `.execute(db);
@@ -3447,8 +3448,8 @@ async function down$4(db) {
3447
3448
  //#endregion
3448
3449
  //#region src/db/migrations/021_check_run_tracking.ts
3449
3450
  var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
3450
- down: () => down$3,
3451
- up: () => up$3
3451
+ down: () => down$11,
3452
+ up: () => up$11
3452
3453
  });
3453
3454
  /**
3454
3455
  * Add `check_run_tracking` table for HA-safe check-run state persistence.
@@ -3472,7 +3473,7 @@ var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
3472
3473
  *
3473
3474
  * Idempotent: a re-run on a DB that already has the table is a no-op.
3474
3475
  */
3475
- async function up$3(db) {
3476
+ async function up$11(db) {
3476
3477
  if ((await sql`
3477
3478
  SELECT EXISTS (
3478
3479
  SELECT 1 FROM information_schema.tables
@@ -3503,14 +3504,14 @@ async function up$3(db) {
3503
3504
  WHERE run_id IS NOT NULL
3504
3505
  `.execute(db);
3505
3506
  }
3506
- async function down$3(db) {
3507
+ async function down$11(db) {
3507
3508
  await sql`DROP TABLE IF EXISTS public.check_run_tracking`.execute(db);
3508
3509
  }
3509
3510
  //#endregion
3510
3511
  //#region src/db/migrations/022_scaler_manager_state.ts
3511
3512
  var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
3512
- down: () => down$2,
3513
- up: () => up$2
3513
+ down: () => down$10,
3514
+ up: () => up$10
3514
3515
  });
3515
3516
  /**
3516
3517
  * Add three tables persisting `ScalerManager` per-coord state:
@@ -3537,7 +3538,7 @@ var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
3537
3538
  * Idempotent: a re-run on a DB that already has any of these tables
3538
3539
  * leaves the existing one alone.
3539
3540
  */
3540
- async function up$2(db) {
3541
+ async function up$10(db) {
3541
3542
  const tableExists = async (name) => {
3542
3543
  return (await sql`
3543
3544
  SELECT EXISTS (
@@ -3587,7 +3588,7 @@ async function up$2(db) {
3587
3588
  `.execute(db);
3588
3589
  }
3589
3590
  }
3590
- async function down$2(db) {
3591
+ async function down$10(db) {
3591
3592
  await sql`DROP TABLE IF EXISTS public.scaler_reservations`.execute(db);
3592
3593
  await sql`DROP TABLE IF EXISTS public.scaler_agent_jobs`.execute(db);
3593
3594
  await sql`DROP TABLE IF EXISTS public.scaler_spawning_agents`.execute(db);
@@ -3595,8 +3596,8 @@ async function down$2(db) {
3595
3596
  //#endregion
3596
3597
  //#region src/db/migrations/023_dispatch_queue_recovery_deadline.ts
3597
3598
  var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll({
3598
- down: () => down$1,
3599
- up: () => up$1
3599
+ down: () => down$9,
3600
+ up: () => up$9
3600
3601
  });
3601
3602
  /**
3602
3603
  * Add `dispatch_queue.recovery_deadline TIMESTAMPTZ` and
@@ -3620,7 +3621,7 @@ var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll(
3620
3621
  * Idempotent: re-running on a DB that already has either column is a
3621
3622
  * no-op.
3622
3623
  */
3623
- async function up$1(db) {
3624
+ async function up$9(db) {
3624
3625
  const colExists = async (name) => {
3625
3626
  return (await sql`
3626
3627
  SELECT EXISTS (
@@ -3645,7 +3646,7 @@ async function up$1(db) {
3645
3646
  WHERE recovery_deadline IS NOT NULL
3646
3647
  `.execute(db);
3647
3648
  }
3648
- async function down$1(db) {
3649
+ async function down$9(db) {
3649
3650
  await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_recovery_deadline`.execute(db);
3650
3651
  await sql`
3651
3652
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS recovery_agent_id
@@ -3657,8 +3658,8 @@ async function down$1(db) {
3657
3658
  //#endregion
3658
3659
  //#region src/db/migrations/024_dispatch_queue_provisioning_error.ts
3659
3660
  var _024_dispatch_queue_provisioning_error_exports = /* @__PURE__ */ __exportAll({
3660
- down: () => down,
3661
- up: () => up
3661
+ down: () => down$8,
3662
+ up: () => up$8
3662
3663
  });
3663
3664
  /**
3664
3665
  * Add `dispatch_queue.last_provisioning_error TEXT` recording the most
@@ -3674,7 +3675,7 @@ var _024_dispatch_queue_provisioning_error_exports = /* @__PURE__ */ __exportAll
3674
3675
  *
3675
3676
  * Idempotent: re-running on a DB that already has the column is a no-op.
3676
3677
  */
3677
- async function up(db) {
3678
+ async function up$8(db) {
3678
3679
  const colExists = async (name) => {
3679
3680
  return (await sql`
3680
3681
  SELECT EXISTS (
@@ -3690,12 +3691,326 @@ async function up(db) {
3690
3691
  ADD COLUMN last_provisioning_error TEXT
3691
3692
  `.execute(db);
3692
3693
  }
3693
- async function down(db) {
3694
+ async function down$8(db) {
3694
3695
  await sql`
3695
3696
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS last_provisioning_error
3696
3697
  `.execute(db);
3697
3698
  }
3698
3699
  //#endregion
3700
+ //#region src/db/migrations/025_init_failure.ts
3701
+ var _025_init_failure_exports = /* @__PURE__ */ __exportAll({
3702
+ down: () => down$7,
3703
+ up: () => up$7
3704
+ });
3705
+ /**
3706
+ * Add `init_failure jsonb` columns to `execution_runs` and `execution_jobs`.
3707
+ *
3708
+ * Presence of this column on a row means the run/job never executed a step
3709
+ * because of an init-phase failure; absence (NULL) means a normal run.
3710
+ * Shape on the wire is `InitFailure` from `@kici-dev/engine`. The dashboard
3711
+ * reads this column directly so it can render the right banner without
3712
+ * round-tripping to the orchestrator (which may be offline).
3713
+ *
3714
+ * Idempotent: re-running on a DB that already has either column is a no-op
3715
+ * for that column.
3716
+ */
3717
+ async function up$7(db) {
3718
+ const colExists = async (table, name) => {
3719
+ return (await sql`
3720
+ SELECT EXISTS (
3721
+ SELECT 1 FROM information_schema.columns
3722
+ WHERE table_schema = 'public'
3723
+ AND table_name = ${table}
3724
+ AND column_name = ${name}
3725
+ ) AS exists
3726
+ `.execute(db)).rows[0]?.exists ?? false;
3727
+ };
3728
+ if (!await colExists("execution_runs", "init_failure")) await sql`
3729
+ ALTER TABLE public.execution_runs
3730
+ ADD COLUMN init_failure JSONB DEFAULT NULL
3731
+ `.execute(db);
3732
+ if (!await colExists("execution_jobs", "init_failure")) await sql`
3733
+ ALTER TABLE public.execution_jobs
3734
+ ADD COLUMN init_failure JSONB DEFAULT NULL
3735
+ `.execute(db);
3736
+ }
3737
+ async function down$7(db) {
3738
+ await sql`
3739
+ ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS init_failure
3740
+ `.execute(db);
3741
+ await sql`
3742
+ ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS init_failure
3743
+ `.execute(db);
3744
+ }
3745
+ //#endregion
3746
+ //#region src/db/migrations/026_event_log_lockfile_corrupt.ts
3747
+ var _026_event_log_lockfile_corrupt_exports = /* @__PURE__ */ __exportAll({
3748
+ down: () => down$6,
3749
+ up: () => up$6
3750
+ });
3751
+ /**
3752
+ * Extend the event_log.status CHECK constraint with 'lockfile_corrupt' so the
3753
+ * orchestrator can log a delivery whose lock file was present but unparseable.
3754
+ * Orchestrator-only: the Platform event_log uses a separate, narrower status set.
3755
+ *
3756
+ * Idempotent: the DROP ... IF EXISTS / re-ADD pair re-runs cleanly.
3757
+ */
3758
+ async function up$6(db) {
3759
+ await sql`ALTER TABLE event_log DROP CONSTRAINT IF EXISTS event_log_status_check`.execute(db);
3760
+ await sql`
3761
+ ALTER TABLE event_log ADD CONSTRAINT event_log_status_check
3762
+ CHECK ((status = ANY (ARRAY[
3763
+ 'received'::text, 'processed'::text, 'duplicate'::text,
3764
+ 'lockfile_missing'::text, 'lockfile_corrupt'::text, 'failed'::text
3765
+ ])))
3766
+ `.execute(db);
3767
+ }
3768
+ async function down$6(db) {
3769
+ await sql`ALTER TABLE event_log DROP CONSTRAINT IF EXISTS event_log_status_check`.execute(db);
3770
+ await sql`
3771
+ ALTER TABLE event_log ADD CONSTRAINT event_log_status_check
3772
+ CHECK ((status = ANY (ARRAY[
3773
+ 'received'::text, 'processed'::text, 'duplicate'::text,
3774
+ 'lockfile_missing'::text, 'failed'::text
3775
+ ])))
3776
+ `.execute(db);
3777
+ }
3778
+ //#endregion
3779
+ //#region src/db/migrations/027_workflow_timeout.ts
3780
+ var _027_workflow_timeout_exports = /* @__PURE__ */ __exportAll({
3781
+ down: () => down$5,
3782
+ up: () => up$5
3783
+ });
3784
+ /**
3785
+ * Add `workflow_timeout_ms integer` to `execution_runs`.
3786
+ *
3787
+ * Persists the workflow-level wall-clock timeout (in milliseconds) read from
3788
+ * the lock workflow at run creation. NULL means no workflow-level cap is
3789
+ * configured. The WorkflowDeadlineDetector reads this column to find runs
3790
+ * whose `started_at + workflow_timeout_ms` has passed and cancels them with
3791
+ * the distinct workflow-timeout reason.
3792
+ *
3793
+ * Typed INTEGER to match the existing `*_ms` columns (`duration_ms`,
3794
+ * `concurrency_timeout_ms`), which keeps the pg read representation a plain
3795
+ * number rather than the BIGINT-as-string shape. INTEGER caps the timeout at
3796
+ * ~24.8 days, far beyond any sane workflow wall-clock budget.
3797
+ *
3798
+ * Idempotent: re-running on a DB that already has the column is a no-op.
3799
+ */
3800
+ async function up$5(db) {
3801
+ await sql`
3802
+ ALTER TABLE public.execution_runs
3803
+ ADD COLUMN IF NOT EXISTS workflow_timeout_ms INTEGER DEFAULT NULL
3804
+ `.execute(db);
3805
+ }
3806
+ async function down$5(db) {
3807
+ await sql`
3808
+ ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS workflow_timeout_ms
3809
+ `.execute(db);
3810
+ }
3811
+ //#endregion
3812
+ //#region src/db/migrations/028_org_settings_user_cache.ts
3813
+ var _028_org_settings_user_cache_exports = /* @__PURE__ */ __exportAll({
3814
+ down: () => down$4,
3815
+ up: () => up$4
3816
+ });
3817
+ /**
3818
+ * Add `org_settings.user_cache_quota_bytes bigint` and
3819
+ * `org_settings.user_cache_ttl_ms bigint`, both NULLABLE.
3820
+ *
3821
+ * These make the user-facing cache per-org byte quota and per-entry TTL
3822
+ * cluster-configurable (runtime-editable per `customer_id` via the admin
3823
+ * route + `kici-admin org-settings user-cache` CLI). A NULL value means
3824
+ * "use the cluster-wide default" — the `KICI_USER_CACHE_QUOTA_BYTES` /
3825
+ * `KICI_USER_CACHE_TTL_MS` env vars (5 GiB / 7 days) resolved in config.ts.
3826
+ *
3827
+ * BIGINT because a quota in bytes (5 GiB = 5_368_709_120) and a TTL in ms
3828
+ * (7 days = 604_800_000) both exceed the safe range only at extreme values,
3829
+ * but a quota can legitimately reach tens of GiB; BIGINT keeps headroom and
3830
+ * matches the sibling `log_bytes` bigint columns.
3831
+ *
3832
+ * Idempotent: a re-run on a DB that already has either column skips it.
3833
+ */
3834
+ async function columnExists(db, column) {
3835
+ return (await sql`
3836
+ SELECT EXISTS (
3837
+ SELECT 1 FROM information_schema.columns
3838
+ WHERE table_schema = 'public'
3839
+ AND table_name = 'org_settings'
3840
+ AND column_name = ${column}
3841
+ ) AS exists
3842
+ `.execute(db)).rows[0]?.exists ?? false;
3843
+ }
3844
+ async function up$4(db) {
3845
+ if (!await columnExists(db, "user_cache_quota_bytes")) await sql`
3846
+ ALTER TABLE public.org_settings
3847
+ ADD COLUMN user_cache_quota_bytes bigint
3848
+ `.execute(db);
3849
+ if (!await columnExists(db, "user_cache_ttl_ms")) await sql`
3850
+ ALTER TABLE public.org_settings
3851
+ ADD COLUMN user_cache_ttl_ms bigint
3852
+ `.execute(db);
3853
+ }
3854
+ async function down$4(db) {
3855
+ await sql`
3856
+ ALTER TABLE public.org_settings DROP COLUMN IF EXISTS user_cache_quota_bytes
3857
+ `.execute(db);
3858
+ await sql`
3859
+ ALTER TABLE public.org_settings DROP COLUMN IF EXISTS user_cache_ttl_ms
3860
+ `.execute(db);
3861
+ }
3862
+ //#endregion
3863
+ //#region src/db/migrations/029_dispatch_queue_attempts.ts
3864
+ var _029_dispatch_queue_attempts_exports = /* @__PURE__ */ __exportAll({
3865
+ down: () => down$3,
3866
+ up: () => up$3
3867
+ });
3868
+ /**
3869
+ * Add `dispatch_queue.dispatch_attempts INT NOT NULL DEFAULT 0`.
3870
+ *
3871
+ * Counts how many times a job has been returned to `pending` for
3872
+ * re-dispatch after a failed delivery attempt (agent sent job.reject, or
3873
+ * a scaler-managed agent disconnected before the job started). The
3874
+ * dispatcher fails the job permanently once the counter reaches
3875
+ * MAX_DISPATCH_ATTEMPTS, bounding requeue loops; `expires_at` remains the
3876
+ * time-based backstop.
3877
+ *
3878
+ * Idempotent: re-running on a DB that already has the column is a no-op.
3879
+ */
3880
+ async function up$3(db) {
3881
+ if (!((await sql`
3882
+ SELECT EXISTS (
3883
+ SELECT 1 FROM information_schema.columns
3884
+ WHERE table_schema = 'public'
3885
+ AND table_name = 'dispatch_queue'
3886
+ AND column_name = 'dispatch_attempts'
3887
+ ) AS exists
3888
+ `.execute(db)).rows[0]?.exists ?? false)) await sql`
3889
+ ALTER TABLE public.dispatch_queue
3890
+ ADD COLUMN dispatch_attempts INT NOT NULL DEFAULT 0
3891
+ `.execute(db);
3892
+ }
3893
+ async function down$3(db) {
3894
+ await sql`
3895
+ ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS dispatch_attempts
3896
+ `.execute(db);
3897
+ }
3898
+ //#endregion
3899
+ //#region src/db/migrations/030_held_runs_env_set_null.ts
3900
+ var _030_held_runs_env_set_null_exports = /* @__PURE__ */ __exportAll({
3901
+ down: () => down$2,
3902
+ up: () => up$2
3903
+ });
3904
+ /**
3905
+ * held_runs.environment_id becomes nullable with ON DELETE SET NULL so
3906
+ * terminal held-run history survives environment deletion (a null
3907
+ * environment_id means the environment was since deleted). Pending held
3908
+ * runs still block deletion — enforced in EnvironmentStore.delete.
3909
+ *
3910
+ * Idempotent: dropping the NOT NULL and the constraint are both no-ops on a
3911
+ * re-run, and the constraint is re-created with the SET NULL action.
3912
+ */
3913
+ async function up$2(db) {
3914
+ await sql`ALTER TABLE public.held_runs ALTER COLUMN environment_id DROP NOT NULL`.execute(db);
3915
+ await sql`ALTER TABLE public.held_runs DROP CONSTRAINT IF EXISTS held_runs_environment_id_fkey`.execute(db);
3916
+ await sql`ALTER TABLE public.held_runs
3917
+ ADD CONSTRAINT held_runs_environment_id_fkey
3918
+ FOREIGN KEY (environment_id) REFERENCES public.environments(id) ON DELETE SET NULL`.execute(db);
3919
+ }
3920
+ async function down$2(db) {
3921
+ await sql`ALTER TABLE public.held_runs DROP CONSTRAINT IF EXISTS held_runs_environment_id_fkey`.execute(db);
3922
+ await sql`ALTER TABLE public.held_runs
3923
+ ADD CONSTRAINT held_runs_environment_id_fkey
3924
+ FOREIGN KEY (environment_id) REFERENCES public.environments(id)`.execute(db);
3925
+ await sql`ALTER TABLE public.held_runs ALTER COLUMN environment_id SET NOT NULL`.execute(db);
3926
+ }
3927
+ //#endregion
3928
+ //#region src/db/migrations/031_dispatch_queue_ack_deadline.ts
3929
+ var _031_dispatch_queue_ack_deadline_exports = /* @__PURE__ */ __exportAll({
3930
+ down: () => down$1,
3931
+ up: () => up$1
3932
+ });
3933
+ /**
3934
+ * Add `dispatch_queue.ack_deadline TIMESTAMPTZ` and
3935
+ * `dispatch_queue.ack_agent_id TEXT` persisting the per-dispatch
3936
+ * acknowledgment deadline for HA-safe lost-dispatch detection.
3937
+ *
3938
+ * The dispatcher stamps both when a job.dispatch is sent and clears them
3939
+ * when the agent answers (job.ack / job.reject / job.status running). A
3940
+ * `dispatched` row past its deadline is requeued — by the owning coord's
3941
+ * in-memory timer, or by the leader-gated sweep
3942
+ * (`Dispatcher.sweepExpiredAckDeadlines`) when the owning coord crashed.
3943
+ * On coord boot, `Dispatcher.recoverState()` re-arms timers from the
3944
+ * persisted deadlines.
3945
+ *
3946
+ * Idempotent: re-running on a DB that already has either column is a no-op.
3947
+ */
3948
+ async function up$1(db) {
3949
+ const colExists = async (name) => {
3950
+ return (await sql`
3951
+ SELECT EXISTS (
3952
+ SELECT 1 FROM information_schema.columns
3953
+ WHERE table_schema = 'public'
3954
+ AND table_name = 'dispatch_queue'
3955
+ AND column_name = ${name}
3956
+ ) AS exists
3957
+ `.execute(db)).rows[0]?.exists ?? false;
3958
+ };
3959
+ if (!await colExists("ack_deadline")) await sql`
3960
+ ALTER TABLE public.dispatch_queue
3961
+ ADD COLUMN ack_deadline TIMESTAMPTZ
3962
+ `.execute(db);
3963
+ if (!await colExists("ack_agent_id")) await sql`
3964
+ ALTER TABLE public.dispatch_queue
3965
+ ADD COLUMN ack_agent_id TEXT
3966
+ `.execute(db);
3967
+ await sql`
3968
+ CREATE INDEX IF NOT EXISTS idx_dispatch_queue_ack_deadline
3969
+ ON public.dispatch_queue (ack_deadline)
3970
+ WHERE ack_deadline IS NOT NULL
3971
+ `.execute(db);
3972
+ }
3973
+ async function down$1(db) {
3974
+ await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_ack_deadline`.execute(db);
3975
+ await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS ack_agent_id`.execute(db);
3976
+ await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS ack_deadline`.execute(db);
3977
+ }
3978
+ //#endregion
3979
+ //#region src/db/migrations/032_org_settings_dispatch_ack_timeout.ts
3980
+ var _032_org_settings_dispatch_ack_timeout_exports = /* @__PURE__ */ __exportAll({
3981
+ down: () => down,
3982
+ up: () => up
3983
+ });
3984
+ /**
3985
+ * Add `org_settings.dispatch_ack_timeout_ms BIGINT` (nullable).
3986
+ *
3987
+ * Per-org override of the dispatch-acknowledgment deadline. NULL falls
3988
+ * back to the cluster-wide default (`KICI_DISPATCH_ACK_TIMEOUT_MS`,
3989
+ * default 10s). Operators raise it on high-latency networks via
3990
+ * `kici-admin org-settings`.
3991
+ *
3992
+ * Idempotent: a re-run on a DB that already has the column is a no-op.
3993
+ */
3994
+ async function up(db) {
3995
+ if ((await sql`
3996
+ SELECT EXISTS (
3997
+ SELECT 1 FROM information_schema.columns
3998
+ WHERE table_schema = 'public'
3999
+ AND table_name = 'org_settings'
4000
+ AND column_name = 'dispatch_ack_timeout_ms'
4001
+ ) AS exists
4002
+ `.execute(db)).rows[0]?.exists) return;
4003
+ await sql`
4004
+ ALTER TABLE public.org_settings
4005
+ ADD COLUMN dispatch_ack_timeout_ms BIGINT
4006
+ `.execute(db);
4007
+ }
4008
+ async function down(db) {
4009
+ await sql`
4010
+ ALTER TABLE public.org_settings DROP COLUMN IF EXISTS dispatch_ack_timeout_ms
4011
+ `.execute(db);
4012
+ }
4013
+ //#endregion
3699
4014
  //#region src/db/migration-provider.ts
3700
4015
  function createMigrationProvider() {
3701
4016
  return { async getMigrations() {
@@ -3723,7 +4038,15 @@ function createMigrationProvider() {
3723
4038
  "021_check_run_tracking": _021_check_run_tracking_exports,
3724
4039
  "022_scaler_manager_state": _022_scaler_manager_state_exports,
3725
4040
  "023_dispatch_queue_recovery_deadline": _023_dispatch_queue_recovery_deadline_exports,
3726
- "024_dispatch_queue_provisioning_error": _024_dispatch_queue_provisioning_error_exports
4041
+ "024_dispatch_queue_provisioning_error": _024_dispatch_queue_provisioning_error_exports,
4042
+ "025_init_failure": _025_init_failure_exports,
4043
+ "026_event_log_lockfile_corrupt": _026_event_log_lockfile_corrupt_exports,
4044
+ "027_workflow_timeout": _027_workflow_timeout_exports,
4045
+ "028_org_settings_user_cache": _028_org_settings_user_cache_exports,
4046
+ "029_dispatch_queue_attempts": _029_dispatch_queue_attempts_exports,
4047
+ "030_held_runs_env_set_null": _030_held_runs_env_set_null_exports,
4048
+ "031_dispatch_queue_ack_deadline": _031_dispatch_queue_ack_deadline_exports,
4049
+ "032_org_settings_dispatch_ack_timeout": _032_org_settings_dispatch_ack_timeout_exports
3727
4050
  };
3728
4051
  } };
3729
4052
  }
@@ -3762,6 +4085,8 @@ async function runMigrations(opts) {
3762
4085
  const applied = (results ?? []).filter((r) => r.status === "Success");
3763
4086
  if (applied.length === 0) logger$5.info("Database schema is up to date");
3764
4087
  else logger$5.info(`Applied ${applied.length} migration(s)`);
4088
+ const hash = await computeMigrationsHash(createMigrationProvider());
4089
+ await storeMigrationContentHash(opts.pool, hash);
3765
4090
  return results ?? [];
3766
4091
  } finally {
3767
4092
  await client.query("SELECT pg_advisory_unlock($1)", [ADVISORY_LOCK_KEY]);
@@ -3854,7 +4179,6 @@ function registerDbCommands(program, getClient) {
3854
4179
  pool
3855
4180
  })).filter((r) => r.status === "Success").length;
3856
4181
  const hash = await computeMigrationsHash(createMigrationProvider());
3857
- await storeMigrationContentHash(pool, hash);
3858
4182
  console.log(`db fresh: ${dbName} — applied ${applied} migration(s), content hash ${hash.slice(0, 12)}...`);
3859
4183
  } finally {
3860
4184
  await kdb.destroy();
@@ -3865,7 +4189,7 @@ function registerDbCommands(program, getClient) {
3865
4189
  process.exit(1);
3866
4190
  }
3867
4191
  });
3868
- 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).").action(async (name, opts) => {
4192
+ 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) => {
3869
4193
  try {
3870
4194
  const baseUrl = resolveDatabaseUrl$1(opts.databaseUrl);
3871
4195
  const url = new URL(baseUrl);
@@ -3874,9 +4198,10 @@ function registerDbCommands(program, getClient) {
3874
4198
  logInvocation(`ensure ${name}`, targetUrl);
3875
4199
  const outcome = await ensureDatabase(targetUrl, {
3876
4200
  owner: opts.owner,
3877
- revokeConnectFromPublic: !!opts.revokeConnectPublic
4201
+ revokeConnectFromPublic: !!opts.revokeConnectPublic,
4202
+ grantConnectToRoles: opts.grantConnectRole
3878
4203
  });
3879
- const suffix = (opts.owner ? ` (owner=${opts.owner})` : "") + (opts.revokeConnectPublic ? " [revoked CONNECT from PUBLIC]" : "");
4204
+ const suffix = (opts.owner ? ` (owner=${opts.owner})` : "") + (opts.revokeConnectPublic ? " [revoked CONNECT from PUBLIC]" : "") + (opts.grantConnectRole.length ? ` [granted CONNECT to: ${opts.grantConnectRole.join(", ")}]` : "");
3880
4205
  console.log(`db ensure: ${name} — ${outcome}${suffix}`);
3881
4206
  } catch (err) {
3882
4207
  console.error(`Error: ${toErrorMessage(err)}`);
@@ -6236,6 +6561,23 @@ function writeManifest(instanceDir, manifest) {
6236
6561
  fs.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
6237
6562
  return file;
6238
6563
  }
6564
+ /**
6565
+ * Read the running kici-admin's version from the orchestrator package.json.
6566
+ *
6567
+ * `process.env.npm_package_version` is only populated under `npm run` and is
6568
+ * undefined when kici-admin runs as a globally-installed binary, which is the
6569
+ * actual install path. Reading from the package.json on disk is the only
6570
+ * reliable source.
6571
+ */
6572
+ function readKiciVersion() {
6573
+ try {
6574
+ const pkgUrl = new URL("../../../../package.json", import.meta.url);
6575
+ const pkg = JSON.parse(fs.readFileSync(fileURLToPath(pkgUrl), "utf-8"));
6576
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
6577
+ } catch {
6578
+ return "unknown";
6579
+ }
6580
+ }
6239
6581
  //#endregion
6240
6582
  //#region src/cli/service/instance/index-file.ts
6241
6583
  /**
@@ -7105,11 +7447,11 @@ var init_registry = __esmMin((() => {
7105
7447
  },
7106
7448
  shawl: { "win32-x64": {
7107
7449
  name: "shawl",
7108
- version: "1.8.0",
7450
+ version: "1.9.0",
7109
7451
  platform: "win32",
7110
7452
  arch: "x64",
7111
- url: "https://github.com/mtkennerly/shawl/releases/download/v1.8.0/shawl-v1.8.0-win64.zip",
7112
- sha256: "ec9bd229a1bcf800509cee731c6a0324d925ca328598085dc743ab200b781678",
7453
+ url: "https://github.com/mtkennerly/shawl/releases/download/v1.9.0/shawl-v1.9.0-win64.zip",
7454
+ sha256: "f883c5d09c9beae2efaeabd8513e7d3f57cd1d0864cec3df4f4a7b6ee904351c",
7113
7455
  extractPath: "shawl.exe",
7114
7456
  archiveType: "zip"
7115
7457
  } }
@@ -8221,7 +8563,18 @@ function startDevPostgres(containerName) {
8221
8563
  if (err instanceof Error && err.message.includes("already exists")) throw err;
8222
8564
  }
8223
8565
  console.log(`Starting dev PostgreSQL container "${containerName}" on port ${port}...`);
8224
- execSync(`${runtime} run -d --name ${containerName} -p ${port}:5432 -e POSTGRES_PASSWORD=${password} -e POSTGRES_DB=kici postgres:18-trixie`, { stdio: "inherit" });
8566
+ const tmpEnvFile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "kici-dev-pg-")), "postgres.env");
8567
+ try {
8568
+ fs.writeFileSync(tmpEnvFile, `POSTGRES_PASSWORD=${password}\n`, { mode: 384 });
8569
+ execSync(`${runtime} run -d --name ${containerName} -p ${port}:5432 --env-file ${tmpEnvFile} -e POSTGRES_DB=kici postgres:18-trixie`, { stdio: "inherit" });
8570
+ } finally {
8571
+ try {
8572
+ fs.rmSync(path.dirname(tmpEnvFile), {
8573
+ recursive: true,
8574
+ force: true
8575
+ });
8576
+ } catch {}
8577
+ }
8225
8578
  return `postgresql://postgres:${password}@localhost:${port}/kici`;
8226
8579
  }
8227
8580
  function registerOrchestratorInstall(orchestrator) {
@@ -8331,7 +8684,7 @@ function registerOrchestratorInstall(orchestrator) {
8331
8684
  logDir,
8332
8685
  installBase: getInstallBase(platform, serviceName),
8333
8686
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8334
- kiciVersion: process.env.npm_package_version ?? "unknown"
8687
+ kiciVersion: readKiciVersion()
8335
8688
  });
8336
8689
  try {
8337
8690
  appendIndexEntry(kiciRoot, {
@@ -8837,7 +9190,7 @@ function registerAgentInstall(agent) {
8837
9190
  logDir,
8838
9191
  installBase: getInstallBase(platform, serviceName),
8839
9192
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8840
- kiciVersion: process.env.npm_package_version ?? "unknown"
9193
+ kiciVersion: readKiciVersion()
8841
9194
  });
8842
9195
  try {
8843
9196
  appendIndexEntry(kiciRoot, {
@@ -9625,6 +9978,9 @@ function formatSettings(s, format) {
9625
9978
  lines.push(`Denied source repos: ${s.deniedRepos === null ? "(none)" : formatList(s.deniedRepos)}`);
9626
9979
  lines.push(`Elevated authors: ${s.elevatedRepos === null ? "(none)" : formatList(s.elevatedRepos)}`);
9627
9980
  lines.push(`Allow http registries: ${s.allowHttpNpmRegistries}`);
9981
+ lines.push(`User-cache quota: ${s.userCacheQuotaBytes === null ? "(cluster default)" : `${s.userCacheQuotaBytes} bytes`}`);
9982
+ lines.push(`User-cache TTL: ${s.userCacheTtlMs === null ? "(cluster default)" : `${s.userCacheTtlMs} ms`}`);
9983
+ lines.push(`Dispatch ack timeout: ${s.dispatchAckTimeoutMs === null ? "(cluster default)" : `${s.dispatchAckTimeoutMs} ms`}`);
9628
9984
  if (s.createdAt) lines.push(`Created at: ${s.createdAt}`);
9629
9985
  if (s.updatedAt) lines.push(`Updated at: ${s.updatedAt}`);
9630
9986
  return lines.join("\n");
@@ -9699,6 +10055,117 @@ function registerOrgSettingsCommands(program, getClient) {
9699
10055
  process.exit(1);
9700
10056
  }
9701
10057
  });
10058
+ registerUserCacheCommands(orgSettings, getClient);
10059
+ registerDispatchAckCommands(orgSettings, getClient);
10060
+ }
10061
+ /**
10062
+ * `kici-admin org-settings dispatch-ack <show|set|reset>`.
10063
+ *
10064
+ * The per-org dispatch-acknowledgment deadline (ms). A null (unset) value
10065
+ * means the cluster-wide default applies (`KICI_DISPATCH_ACK_TIMEOUT_MS`,
10066
+ * default 10s). Operators raise it on high-latency networks.
10067
+ */
10068
+ function registerDispatchAckCommands(orgSettings, getClient) {
10069
+ const da = orgSettings.command("dispatch-ack").description("Manage the per-org dispatch-acknowledgment deadline (null = cluster default)");
10070
+ da.command("show").description("Print the current per-org dispatch-acknowledgment deadline").option("--customer-id <id>", "Customer / org id (alias: --org)").option("--org <id>", "Alias for --customer-id").option("--format <format>", "Output format: json|table", "table").action(async (opts) => {
10071
+ const customerId = resolveCustomerId(opts);
10072
+ try {
10073
+ const settings = await fetchSettings(getClient(), customerId);
10074
+ console.log(formatSettings(settings, opts.format));
10075
+ } catch (err) {
10076
+ console.error(`Error: ${toErrorMessage(err)}`);
10077
+ process.exit(1);
10078
+ }
10079
+ });
10080
+ da.command("set <value>").description("Set the per-org dispatch-acknowledgment deadline (integer milliseconds, >= 1000)").option("--customer-id <id>", "Customer / org id (alias: --org)").option("--org <id>", "Alias for --customer-id").option("--format <format>", "Output format: json|table", "table").action(async (value, opts) => {
10081
+ const n = Number(value);
10082
+ if (!Number.isInteger(n) || n < 1e3) {
10083
+ console.error("Error: value must be an integer >= 1000 (milliseconds)");
10084
+ process.exit(1);
10085
+ }
10086
+ const customerId = resolveCustomerId(opts);
10087
+ try {
10088
+ const updated = await patchSettings(getClient(), {
10089
+ customerId,
10090
+ dispatchAckTimeoutMs: n
10091
+ });
10092
+ console.log(formatSettings(updated, opts.format));
10093
+ } catch (err) {
10094
+ console.error(`Error: ${toErrorMessage(err)}`);
10095
+ process.exit(1);
10096
+ }
10097
+ });
10098
+ da.command("reset").description("Clear the per-org dispatch-ack deadline override (fall back to the cluster default)").option("--customer-id <id>", "Customer / org id (alias: --org)").option("--org <id>", "Alias for --customer-id").option("--format <format>", "Output format: json|table", "table").action(async (opts) => {
10099
+ const customerId = resolveCustomerId(opts);
10100
+ try {
10101
+ const updated = await patchSettings(getClient(), {
10102
+ customerId,
10103
+ dispatchAckTimeoutMs: null
10104
+ });
10105
+ console.log(formatSettings(updated, opts.format));
10106
+ } catch (err) {
10107
+ console.error(`Error: ${toErrorMessage(err)}`);
10108
+ process.exit(1);
10109
+ }
10110
+ });
10111
+ }
10112
+ /**
10113
+ * `kici-admin org-settings user-cache <show|set-quota|set-ttl|reset-quota|reset-ttl>`.
10114
+ *
10115
+ * The per-org byte quota and per-entry TTL for the user-facing cache. A null
10116
+ * (unset) value means the cluster-wide default applies (the
10117
+ * `KICI_USER_CACHE_QUOTA_BYTES` / `KICI_USER_CACHE_TTL_MS` env vars).
10118
+ */
10119
+ function registerUserCacheCommands(orgSettings, getClient) {
10120
+ const uc = orgSettings.command("user-cache").description("Manage per-org user-facing cache quota + entry TTL (null = cluster default)");
10121
+ uc.command("show").description("Print the current per-org user-cache quota + TTL settings").option("--customer-id <id>", "Customer / org id (alias: --org)").option("--org <id>", "Alias for --customer-id").option("--format <format>", "Output format: json|table", "table").action(async (opts) => {
10122
+ const customerId = resolveCustomerId(opts);
10123
+ try {
10124
+ const settings = await fetchSettings(getClient(), customerId);
10125
+ console.log(formatSettings(settings, opts.format));
10126
+ } catch (err) {
10127
+ console.error(`Error: ${toErrorMessage(err)}`);
10128
+ process.exit(1);
10129
+ }
10130
+ });
10131
+ registerUserCacheSetter(uc, getClient, "quota");
10132
+ registerUserCacheSetter(uc, getClient, "ttl");
10133
+ }
10134
+ /** Register `set-<knob>` and `reset-<knob>` for the byte-quota / ms-TTL knobs. */
10135
+ function registerUserCacheSetter(uc, getClient, knob) {
10136
+ const field = knob === "quota" ? "userCacheQuotaBytes" : "userCacheTtlMs";
10137
+ const unit = knob === "quota" ? "bytes" : "milliseconds";
10138
+ uc.command(`set-${knob} <value>`).description(`Set the per-org user-cache ${knob} (positive integer ${unit})`).option("--customer-id <id>", "Customer / org id (alias: --org)").option("--org <id>", "Alias for --customer-id").option("--format <format>", "Output format: json|table", "table").action(async (value, opts) => {
10139
+ const n = Number(value);
10140
+ if (!Number.isInteger(n) || n <= 0) {
10141
+ console.error(`Error: value must be a positive integer (${unit})`);
10142
+ process.exit(1);
10143
+ }
10144
+ const customerId = resolveCustomerId(opts);
10145
+ try {
10146
+ const updated = await patchSettings(getClient(), {
10147
+ customerId,
10148
+ [field]: n
10149
+ });
10150
+ console.log(formatSettings(updated, opts.format));
10151
+ } catch (err) {
10152
+ console.error(`Error: ${toErrorMessage(err)}`);
10153
+ process.exit(1);
10154
+ }
10155
+ });
10156
+ uc.command(`reset-${knob}`).description(`Clear the per-org user-cache ${knob} override (fall back to the cluster default)`).option("--customer-id <id>", "Customer / org id (alias: --org)").option("--org <id>", "Alias for --customer-id").option("--format <format>", "Output format: json|table", "table").action(async (opts) => {
10157
+ const customerId = resolveCustomerId(opts);
10158
+ try {
10159
+ const updated = await patchSettings(getClient(), {
10160
+ customerId,
10161
+ [field]: null
10162
+ });
10163
+ console.log(formatSettings(updated, opts.format));
10164
+ } catch (err) {
10165
+ console.error(`Error: ${toErrorMessage(err)}`);
10166
+ process.exit(1);
10167
+ }
10168
+ });
9702
10169
  }
9703
10170
  /** Register `<prefix>-add` and `<prefix>-remove` commands bound to a list field. */
9704
10171
  function registerListMutators(gw, getClient, prefix, field) {
@@ -10120,8 +10587,10 @@ function printShowEnvironment(res) {
10120
10587
  }
10121
10588
  function registerEnvironmentCommands(program, getClient) {
10122
10589
  const env = program.command("environment").description("Environment management (dual-mode)");
10123
- env.command("create").description("Upsert an environment (idempotent by org+name)").requiredOption("--org <id>", "Org ID").requiredOption("--name <name>", "Environment name").option("--type <t>", "Environment type (fixed|glob|template)", "fixed").option("--enabled <bool>", "Enabled flag (true|false)", "true").option("--branch-restrictions <json>", "JSON array of allowed branches (e.g. '[\"main\"]')").option("--required-reviewers <csv>", "CSV of required reviewer user IDs (or empty to clear)").option("--wait-timer <seconds>", "Wait timer before release (seconds)").option("--hold-expiry <seconds>", "Hold expiry TTL (seconds)").option("--minimum-trust <level>", "Minimum trust (known|trusted)").option("--database-url <url>", "Use direct DB access instead of HTTP (offline mode)").option("--json", "Emit JSON output").action(async (opts) => {
10590
+ env.command("create").description("Upsert an environment (idempotent by org+name)").requiredOption("--org <id>", "Org ID").requiredOption("--name <name>", "Environment name").option("--type <t>", "Environment type (fixed|glob|template)", "fixed").option("--glob-pattern <pattern>", "Glob pattern matched against declared environment names (required with --type glob)").option("--enabled <bool>", "Enabled flag (true|false)", "true").option("--branch-restrictions <json>", "JSON array of allowed branches (e.g. '[\"main\"]')").option("--required-reviewers <csv>", "CSV of required reviewer user IDs (or empty to clear)").option("--wait-timer <seconds>", "Wait timer before release (seconds)").option("--hold-expiry <seconds>", "Hold expiry TTL (seconds)").option("--minimum-trust <level>", "Minimum trust (known|trusted)").option("--database-url <url>", "Use direct DB access instead of HTTP (offline mode)").option("--json", "Emit JSON output").action(async (opts) => {
10124
10591
  try {
10592
+ if (opts.type === "glob" && !opts.globPattern) throw new Error("--type glob requires --glob-pattern <pattern>");
10593
+ if (opts.type !== "glob" && opts.globPattern !== void 0) throw new Error("--glob-pattern requires --type glob");
10125
10594
  const branchRestrictions = parseJsonOption(opts.branchRestrictions, "--branch-restrictions");
10126
10595
  const requiredReviewers = parseCsvOption(opts.requiredReviewers);
10127
10596
  const waitTimerSeconds = parseIntOption$3(opts.waitTimer, "--wait-timer");
@@ -10132,6 +10601,7 @@ function registerEnvironmentCommands(program, getClient) {
10132
10601
  name: opts.name,
10133
10602
  type: opts.type,
10134
10603
  enabled,
10604
+ globPattern: opts.globPattern,
10135
10605
  branchRestrictions,
10136
10606
  requiredReviewers,
10137
10607
  waitTimerSeconds,
@@ -10166,7 +10636,7 @@ function registerEnvironmentCommands(program, getClient) {
10166
10636
  process.exit(1);
10167
10637
  }
10168
10638
  });
10169
- env.command("set-policy").description("Update policy fields on an environment (only provided fields change)").requiredOption("--org <id>", "Org ID").requiredOption("--env <name>", "Environment name").option("--branch-restrictions <json>", "JSON array of allowed branches").option("--required-reviewers <csv>", "CSV of required reviewer user IDs (empty to clear)").option("--wait-timer <seconds>", "Wait timer before release (seconds)").option("--hold-expiry <seconds>", "Hold expiry TTL (seconds)").option("--minimum-trust <level>", "Minimum trust (known|trusted, or \"null\" to clear)").option("--enabled <bool>", "Enabled flag (true|false)").option("--database-url <url>", "Use direct DB access instead of HTTP (offline mode)").option("--json", "Emit JSON output").action(async (opts) => {
10639
+ env.command("set-policy").description("Update policy fields on an environment (only provided fields change)").requiredOption("--org <id>", "Org ID").requiredOption("--env <name>", "Environment name").option("--branch-restrictions <json>", "JSON array of allowed branches").option("--required-reviewers <csv>", "CSV of required reviewer user IDs (empty to clear)").option("--wait-timer <seconds>", "Wait timer before release (seconds)").option("--hold-expiry <seconds>", "Hold expiry TTL (seconds)").option("--minimum-trust <level>", "Minimum trust (known|trusted, or \"null\" to clear)").option("--enabled <bool>", "Enabled flag (true|false)").option("--allow-local-execution <bool>", "Allow CLI/test runs to resolve this env (true|false)").option("--database-url <url>", "Use direct DB access instead of HTTP (offline mode)").option("--json", "Emit JSON output").action(async (opts) => {
10170
10640
  try {
10171
10641
  const payload = {
10172
10642
  orgId: opts.org,
@@ -10178,6 +10648,7 @@ function registerEnvironmentCommands(program, getClient) {
10178
10648
  if (opts.holdExpiry !== void 0) payload.holdExpirySeconds = parseIntOption$3(opts.holdExpiry, "--hold-expiry");
10179
10649
  if (opts.minimumTrust !== void 0) payload.minimumTrust = opts.minimumTrust === "null" ? null : opts.minimumTrust;
10180
10650
  if (opts.enabled !== void 0) payload.enabled = opts.enabled === "false" ? false : true;
10651
+ if (opts.allowLocalExecution !== void 0) payload.allowLocalExecution = opts.allowLocalExecution === "false" ? false : true;
10181
10652
  const dbUrl = resolveDirectDbUrl$4(opts.databaseUrl);
10182
10653
  if (dbUrl) await setEnvironmentPolicyDirect(dbUrl, payload);
10183
10654
  else await getClient().patch(`/api/v1/admin/environments/${encodeURIComponent(opts.env)}/policy`, payload);
@@ -10213,6 +10684,23 @@ function registerEnvironmentCommands(program, getClient) {
10213
10684
  process.exit(1);
10214
10685
  }
10215
10686
  });
10687
+ env.command("delete").description("Delete an environment (cascades bindings, variables, overrides; held-run history survives; pending held runs block with a clear error, resolved holds do not)").requiredOption("--org <id>", "Org ID").requiredOption("--name <name>", "Environment name").option("--database-url <url>", "Use direct DB access instead of HTTP (offline mode)").option("--json", "Emit JSON output").action(async (opts) => {
10688
+ try {
10689
+ const dbUrl = resolveDirectDbUrl$4(opts.databaseUrl);
10690
+ let deleted;
10691
+ if (dbUrl) ({deleted} = await deleteEnvironmentDirect(dbUrl, {
10692
+ orgId: opts.org,
10693
+ name: opts.name
10694
+ }));
10695
+ else deleted = (await getClient().delete(`/api/v1/admin/environments/${encodeURIComponent(opts.name)}?orgId=${encodeURIComponent(opts.org)}`)).deleted;
10696
+ if (!deleted) throw new Error(`environment not found (org=${opts.org}, name=${opts.name})`);
10697
+ if (opts.json) console.log(JSON.stringify({ deleted: true }));
10698
+ else console.log(`environment delete: deleted=true${dbUrl ? " (direct)" : ""}`);
10699
+ } catch (err) {
10700
+ console.error(`Error: ${toErrorMessage(err)}`);
10701
+ process.exit(1);
10702
+ }
10703
+ });
10216
10704
  env.command("create-template").description("Create or update an environment template + its seed variables").requiredOption("--org <id>", "Org ID").requiredOption("--template <name>", "Template name").option("--type <t>", "Environment type (defaults to \"template\")", "template").option("--branch-restrictions <json>", "JSON array of allowed branches").option("--required-reviewers <csv>", "CSV of required reviewer user IDs").option("--wait-timer <seconds>", "Wait timer (seconds)").option("--hold-expiry <seconds>", "Hold expiry TTL (seconds)").option("--minimum-trust <level>", "Minimum trust (known|trusted)").option("--variables <json>", "JSON object of env variables to seed (e.g. '{\"K\":\"V\"}')").option("--database-url <url>", "Use direct DB access instead of HTTP (offline mode)").option("--json", "Emit JSON output").action(async (opts) => {
10217
10705
  try {
10218
10706
  const branchRestrictions = parseJsonOption(opts.branchRestrictions, "--branch-restrictions");