@odla-ai/cli 0.25.22 → 0.26.1

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.
package/dist/bin.cjs CHANGED
@@ -182,10 +182,10 @@ function isManagedDevVar(line) {
182
182
  const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
183
183
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
184
184
  }
185
- function writePrivateText(path, text) {
185
+ function writePrivateText(path, text2) {
186
186
  (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
187
187
  const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
188
- (0, import_node_fs.writeFileSync)(temporary, text, { mode: 384 });
188
+ (0, import_node_fs.writeFileSync)(temporary, text2, { mode: 384 });
189
189
  (0, import_node_fs.chmodSync)(temporary, 384);
190
190
  (0, import_node_fs.renameSync)(temporary, path);
191
191
  }
@@ -492,7 +492,9 @@ function audienceBoundEnvToken(token, platform) {
492
492
  }
493
493
  var SCOPE_PURPOSE = {
494
494
  "platform:status:read": "read the platform fleet health and deployment snapshot",
495
+ "platform:chat:credential:write": "rotate the built-in Discussion responder credential",
495
496
  "app:config:read": "compare checked-in intent with an exact-id app Registry configuration",
497
+ "app:config:write": "apply or inspect one revision-bound configuration operation for an app you own",
496
498
  "platform:runbook:write": "add or edit odla's operational runbooks",
497
499
  "platform:ai:policy:write": "change System AI model routing",
498
500
  "platform:ai:policy:read": "read System AI model routing",
@@ -550,6 +552,14 @@ async function scopedToken(platform, scope, options, doFetch, out) {
550
552
  return token;
551
553
  }
552
554
 
555
+ // src/principal-presentation.ts
556
+ function unresolvedPrincipalLabel(credentialKind2, principalId) {
557
+ const kind = typeof credentialKind2 === "string" ? credentialKind2.trim() : "";
558
+ const id = typeof principalId === "string" ? principalId.trim() : "";
559
+ const audit = kind && id ? `${kind}:${id}` : kind || id;
560
+ return `Unknown principal${audit ? ` [${audit}]` : ""}`;
561
+ }
562
+
553
563
  // src/admin-ai-audit.ts
554
564
  function adminAiAuditQuery(filters) {
555
565
  if (filters.limit === void 0) return "";
@@ -579,17 +589,17 @@ async function readAdminAiAudit(request2) {
579
589
  String(event.changeKind ?? ""),
580
590
  String(event.purpose ?? event.provider ?? ""),
581
591
  route2,
582
- `${String(event.actorType ?? "")}:${String(event.actorId ?? "")}`
592
+ unresolvedPrincipalLabel(event.actorType, event.actorId)
583
593
  ].join(" "));
584
594
  }
585
595
  }
586
596
  async function responseBody(response2) {
587
- const text = await response2.text();
588
- if (!text) return {};
597
+ const text2 = await response2.text();
598
+ if (!text2) return {};
589
599
  try {
590
- return JSON.parse(text);
600
+ return JSON.parse(text2);
591
601
  } catch {
592
- return { message: text.slice(0, 300) };
602
+ return { message: text2.slice(0, 300) };
593
603
  }
594
604
  }
595
605
  function apiError(status, body) {
@@ -670,7 +680,7 @@ when app/env run actor purpose/role route / policy tokens cost status`);
670
680
  timestamp2(event.created_at),
671
681
  `${String(event.app_id ?? "")}/${String(event.env ?? "")}`,
672
682
  String(event.run_id ?? ""),
673
- `${String(event.actor_type ?? "")}:${String(event.actor_id ?? "")}`,
683
+ unresolvedPrincipalLabel(event.actor_type, event.actor_id),
674
684
  `${String(event.purpose ?? "")}/${String(event.role ?? "")}`,
675
685
  `${String(event.provider ?? "")}/${String(event.model ?? "")}@v${String(event.policy_version ?? "unknown")}`,
676
686
  String(input + output),
@@ -691,12 +701,12 @@ function timestamp2(value2) {
691
701
  return Number.isFinite(date.valueOf()) ? date.toISOString() : "";
692
702
  }
693
703
  async function responseBody2(res) {
694
- const text = await res.text();
695
- if (!text) return {};
704
+ const text2 = await res.text();
705
+ if (!text2) return {};
696
706
  try {
697
- return JSON.parse(text);
707
+ return JSON.parse(text2);
698
708
  } catch {
699
- return { message: text.slice(0, 300) };
709
+ return { message: text2.slice(0, 300) };
700
710
  }
701
711
  }
702
712
  function apiError2(action2, status, body) {
@@ -872,12 +882,12 @@ function catalogModels(body) {
872
882
  return body.catalog.models.filter((value2) => isRecord3(value2) && typeof value2.id === "string" && typeof value2.provider === "string");
873
883
  }
874
884
  async function responseBody3(res) {
875
- const text = await res.text();
876
- if (!text) return {};
885
+ const text2 = await res.text();
886
+ if (!text2) return {};
877
887
  try {
878
- return JSON.parse(text);
888
+ return JSON.parse(text2);
879
889
  } catch {
880
- return { message: text.slice(0, 300) };
890
+ return { message: text2.slice(0, 300) };
881
891
  }
882
892
  }
883
893
  function apiError3(action2, status, body) {
@@ -1048,6 +1058,7 @@ function unique(values) {
1048
1058
  var DEFAULT_PLATFORM = "https://odla.ai";
1049
1059
  var DEFAULT_ENVS = ["dev"];
1050
1060
  var DEFAULT_SERVICES = ["db", "ai"];
1061
+ var configImportSerial = 0;
1051
1062
  var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
1052
1063
  async function loadProjectConfig(configPath = "odla.config.mjs") {
1053
1064
  const resolved = (0, import_node_path4.resolve)(configPath);
@@ -1255,7 +1266,8 @@ function validId2(value2) {
1255
1266
  }
1256
1267
  async function loadConfigModule(path) {
1257
1268
  if (path.endsWith(".json")) return JSON.parse((0, import_node_fs4.readFileSync)(path, "utf8"));
1258
- const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?t=${Date.now()}`);
1269
+ const nonce = `${Date.now()}-${configImportSerial++}`;
1270
+ const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?reload=${nonce}`);
1259
1271
  const value2 = mod.default ?? mod.config;
1260
1272
  if (typeof value2 === "function") return await value2();
1261
1273
  return value2;
@@ -1719,8 +1731,8 @@ async function appImport(options) {
1719
1731
  const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
1720
1732
  const doFetch = options.fetch ?? fetch;
1721
1733
  const { tenant } = resolveTenant(cfg, options.env);
1722
- const text = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs8.readFileSync)(0, "utf8")))() : (0, import_node_fs8.readFileSync)(options.file, "utf8");
1723
- const { format, sources } = (0, import_import.parseImport)(text, options.ns);
1734
+ const text2 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs8.readFileSync)(0, "utf8")))() : (0, import_node_fs8.readFileSync)(options.file, "utf8");
1735
+ const { format, sources } = (0, import_import.parseImport)(text2, options.ns);
1724
1736
  if (format === "namespace-map" && options.ns) {
1725
1737
  throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
1726
1738
  }
@@ -1811,7 +1823,10 @@ function report(options, owners, headline) {
1811
1823
  if (headline) out.log(headline);
1812
1824
  out.log(`owners (${owners.length}):`);
1813
1825
  for (const o of owners) {
1814
- out.log(` ${o.primary ? "\u2605" : "\xB7"} ${o.email ?? o.ownerId}${o.primary ? " (primary)" : ""}`);
1826
+ const name = o.email?.trim() || "Unnamed member";
1827
+ out.log(
1828
+ ` ${o.primary ? "\u2605" : "\xB7"} ${name} [${o.ownerId}]${o.primary ? " (primary)" : ""}`
1829
+ );
1815
1830
  }
1816
1831
  }
1817
1832
  async function ownersList(options) {
@@ -2639,7 +2654,8 @@ var CAPABILITIES = {
2639
2654
  "save and explicitly select non-secret named operator contexts with isolated credential caches; resolve and explain platform, app, environment, and credential provenance without authenticating; then run PM, Discussions, o11y, runbook, and identity operations outside a project checkout",
2640
2655
  "read one versioned o11y status envelope spanning application RED, exact Worker versions and Cloudflare colos observed in traffic, current live-sync freshness/load, the protected commit-to-visible canary, collector ingest/scheduler trust, provider-owned runtime metrics, account-scoped Durable Object, D1, and R2 evidence under odla-db, and a bounded machine verdict",
2641
2656
  "read one canonical platform fleet snapshot over private service bindings, including release identities, probe latency, Cloudflare load/runtime freshness, explicit unknowns, and stable next actions through a read-only capability",
2642
- "compare one project's checked-in Registry intent with live owner-visible state and freeze a secret-free plan digest through an exact app:config:read capability",
2657
+ "compare one project's checked-in Registry intent with live owner-visible state, freeze a secret-free Registry-revision-bound plan through app:config:read, and conditionally apply checkpoint-free actions through exact app:config:write operation routes",
2658
+ "inspect or bounded-wait one exact durable config-operation journal entry and verify every terminal receipt digest before returning it to remote automation",
2643
2659
  "inspect durable agent wakeups as a versioned JSON envelope and explicitly requeue one dead-lettered job with a scoped environment credential",
2644
2660
  "run app-attributed hosted security discovery and independent validation without provider keys",
2645
2661
  "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
@@ -2686,10 +2702,31 @@ function printGroup(out, heading, items) {
2686
2702
  out.log("");
2687
2703
  }
2688
2704
 
2689
- // src/config-reconcile-command.ts
2705
+ // src/config-operation-command.ts
2690
2706
  var import_apps6 = require("@odla-ai/apps");
2691
2707
  var import_node_path7 = require("path");
2692
2708
 
2709
+ // src/version.ts
2710
+ var import_node_fs9 = require("fs");
2711
+ function cliVersion() {
2712
+ const pkg = JSON.parse((0, import_node_fs9.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
2713
+ return pkg.version ?? "unknown";
2714
+ }
2715
+
2716
+ // src/config-operation-error.ts
2717
+ var ConfigOperationCommandError = class extends Error {
2718
+ constructor(message2, code) {
2719
+ super(message2);
2720
+ this.code = code;
2721
+ this.name = "ConfigOperationCommandError";
2722
+ }
2723
+ code;
2724
+ };
2725
+
2726
+ // src/config-operation-validate.ts
2727
+ var import_apps3 = require("@odla-ai/apps");
2728
+ var import_node_fs10 = require("fs");
2729
+
2693
2730
  // src/config-reconcile-digest.ts
2694
2731
  var import_node_crypto = require("crypto");
2695
2732
  function canonicalJson(value2) {
@@ -2699,27 +2736,160 @@ function configDigest(value2) {
2699
2736
  return `sha256:${(0, import_node_crypto.createHash)("sha256").update(canonicalJson(value2)).digest("hex")}`;
2700
2737
  }
2701
2738
  function canonicalValue(value2) {
2739
+ if (value2 === null || typeof value2 === "string" || typeof value2 === "boolean") return value2;
2740
+ if (typeof value2 === "number") {
2741
+ if (!Number.isFinite(value2)) throw new TypeError("canonical JSON rejects non-finite numbers");
2742
+ return value2;
2743
+ }
2702
2744
  if (Array.isArray(value2)) return value2.map(canonicalValue);
2703
2745
  if (value2 && typeof value2 === "object") {
2746
+ const record11 = value2;
2704
2747
  return Object.fromEntries(
2705
- Object.entries(value2).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonicalValue(entry)])
2748
+ Object.keys(record11).filter((key) => record11[key] !== void 0).sort().map((key) => [key, canonicalValue(record11[key])])
2706
2749
  );
2707
2750
  }
2708
- return value2;
2751
+ throw new TypeError("canonical JSON rejects unsupported values");
2752
+ }
2753
+
2754
+ // src/config-operation-validate.ts
2755
+ var DIGEST = /^sha256:[0-9a-f]{64}$/;
2756
+ var REVISION = /^registry:[1-9][0-9]*$/;
2757
+ var OPERATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
2758
+ var ACTION_ID = /^action-[0-9a-f]{16}$/;
2759
+ var ENV = /^[a-z0-9]{2,12}$/;
2760
+ var SERVICE = /^[a-z][a-z0-9-]{0,39}$/;
2761
+ function readPlan(path) {
2762
+ let value2;
2763
+ try {
2764
+ const raw = (0, import_node_fs10.readFileSync)(path, "utf8");
2765
+ if (Buffer.byteLength(raw) > 128 * 1024) throw new Error("plan exceeds 128 KiB");
2766
+ value2 = JSON.parse(raw);
2767
+ } catch (error) {
2768
+ throw new ConfigOperationCommandError(
2769
+ `cannot read --plan ${path}: ${error instanceof Error ? error.message : String(error)}`,
2770
+ "invalid_plan"
2771
+ );
2772
+ }
2773
+ if (!record2(value2) || value2.schemaVersion !== "odla.config-plan/v2") invalidPlan("unsupported plan schema");
2774
+ if (!record2(value2.scope) || typeof value2.scope.appId !== "string" || typeof value2.scope.platformUrl !== "string") {
2775
+ invalidPlan("plan scope is invalid");
2776
+ }
2777
+ if (!DIGEST.test(String(value2.desiredRevision)) || !DIGEST.test(String(value2.observedRevision))) {
2778
+ invalidPlan("plan content revisions are invalid");
2779
+ }
2780
+ if (!REVISION.test(String(value2.registryRevision)) || !DIGEST.test(String(value2.planDigest))) {
2781
+ invalidPlan("plan Registry revision or digest is invalid");
2782
+ }
2783
+ if (!Array.isArray(value2.actions) || !value2.actions.length || value2.actions.length > 32) {
2784
+ invalidPlan("plan actions must contain 1-32 entries");
2785
+ }
2786
+ assertActions(value2.actions);
2787
+ const plan = value2;
2788
+ const digest = configDigest({
2789
+ schemaVersion: plan.schemaVersion,
2790
+ desiredRevision: plan.desiredRevision,
2791
+ observedRevision: plan.observedRevision,
2792
+ registryRevision: plan.registryRevision,
2793
+ actions: plan.actions
2794
+ });
2795
+ if (digest !== plan.planDigest) invalidPlan("plan digest does not bind these revisions and actions");
2796
+ return plan;
2797
+ }
2798
+ function assertPlanContext(plan, appId, platformUrl) {
2799
+ if (plan.scope.appId !== appId || plan.scope.platformUrl.replace(/\/$/, "") !== platformUrl.replace(/\/$/, "")) {
2800
+ throw new ConfigOperationCommandError("plan scope does not match the selected project config", "checkpoint_required");
2801
+ }
2802
+ }
2803
+ function verifyReceipt(receipt, appId, operationId) {
2804
+ if (receipt.appId !== appId || operationId && receipt.operationId !== operationId) {
2805
+ throw new ConfigOperationCommandError("Registry returned a receipt outside the requested scope", "invalid_receipt");
2806
+ }
2807
+ if (receipt.receiptDigest) {
2808
+ const { receiptDigest, ...fields } = receipt;
2809
+ if (configDigest(fields) !== receiptDigest) {
2810
+ throw new ConfigOperationCommandError("config operation receipt digest is invalid", "invalid_receipt");
2811
+ }
2812
+ } else if (receipt.state === "succeeded" || receipt.state === "conflict" || receipt.state === "failed" && !receipt.error?.retryable) {
2813
+ throw new ConfigOperationCommandError("terminal config operation receipt is not digest-authenticated", "invalid_receipt");
2814
+ }
2815
+ }
2816
+ function assertOperationId(value2) {
2817
+ if (!OPERATION_ID.test(value2)) {
2818
+ throw new ConfigOperationCommandError("operation id must be a UUID", "invalid_operation_id");
2819
+ }
2820
+ }
2821
+ function assertActions(actions) {
2822
+ const ids = /* @__PURE__ */ new Set();
2823
+ for (const action2 of actions) {
2824
+ if (!record2(action2)) invalidPlan("every plan action must be an object");
2825
+ const id = String(action2.id ?? "");
2826
+ if (!ACTION_ID.test(id) || ids.has(id)) invalidPlan("plan action ids must be unique frozen ids");
2827
+ ids.add(id);
2828
+ if (typeof action2.path !== "string" || typeof action2.reason !== "string" || !action2.reason || action2.reason.length > 500 || typeof action2.requiresApproval !== "boolean" || !["low", "medium", "high"].includes(String(action2.risk)) || !["provision", "command", "studio"].includes(String(action2.applySupport))) {
2829
+ invalidPlan("plan action metadata is invalid");
2830
+ }
2831
+ if (["rename_app", "enable_service", "configure_service", "set_link"].includes(String(action2.kind))) {
2832
+ assertConditionalAction(action2);
2833
+ }
2834
+ }
2835
+ }
2836
+ function assertConditionalAction(action2) {
2837
+ if (action2.kind === "rename_app") {
2838
+ if (action2.path !== "app.name" || action2.applySupport !== "command" || typeof action2.before !== "string" || typeof action2.after !== "string" || action2.after !== action2.after.trim() || !action2.after || action2.after.length > 80) invalidPlan("rename action is invalid");
2839
+ return;
2840
+ }
2841
+ if (!action2.env || !ENV.test(action2.env)) invalidPlan("conditional action env is invalid");
2842
+ if (action2.kind === "set_link") {
2843
+ if (action2.path !== `environments.${action2.env}.link` || action2.applySupport !== "provision" || !linkState(action2.before) || !linkState(action2.after)) invalidPlan("link action is invalid");
2844
+ return;
2845
+ }
2846
+ if (!action2.service || !SERVICE.test(action2.service) || !(0, import_apps3.appServiceDefinition)(action2.service)) {
2847
+ invalidPlan("service action names an unknown service");
2848
+ }
2849
+ const base = `environments.${action2.env}.services.${action2.service}`;
2850
+ if (action2.path !== (action2.kind === "configure_service" ? `${base}.config` : base)) {
2851
+ invalidPlan("service action path is invalid");
2852
+ }
2853
+ if (action2.applySupport !== "provision" || !record2(action2.after)) {
2854
+ invalidPlan("service action payload is invalid");
2855
+ }
2856
+ if (action2.kind === "enable_service") {
2857
+ if (action2.after.enabled !== true || action2.before !== null && !record2(action2.before)) {
2858
+ invalidPlan("service enable action is invalid");
2859
+ }
2860
+ } else if (!record2(action2.before)) {
2861
+ invalidPlan("service configure action is invalid");
2862
+ }
2863
+ }
2864
+ function linkState(value2) {
2865
+ if (value2 === null) return true;
2866
+ if (typeof value2 !== "string") return false;
2867
+ try {
2868
+ const url = new URL(value2.trim());
2869
+ return url.protocol === "http:" || url.protocol === "https:";
2870
+ } catch {
2871
+ return false;
2872
+ }
2873
+ }
2874
+ function invalidPlan(message2) {
2875
+ throw new ConfigOperationCommandError(message2, "invalid_plan");
2876
+ }
2877
+ function record2(value2) {
2878
+ return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
2709
2879
  }
2710
2880
 
2711
2881
  // src/config-reconcile-desired.ts
2712
- var import_apps4 = require("@odla-ai/apps");
2882
+ var import_apps5 = require("@odla-ai/apps");
2713
2883
 
2714
2884
  // src/provision-helpers.ts
2715
2885
  var import_ai = require("@odla-ai/ai");
2716
- var import_apps3 = require("@odla-ai/apps");
2886
+ var import_apps4 = require("@odla-ai/apps");
2717
2887
  function defaultSecretName(provider) {
2718
2888
  const names = import_ai.DEFAULT_SECRET_NAMES;
2719
2889
  return names[provider] ?? `${provider}_api_key`;
2720
2890
  }
2721
2891
  async function assertTenantAdminAccess(doFetch, cfg, env, token) {
2722
- const tenantId = (0, import_apps3.tenantIdFor)(cfg.app.id, env);
2892
+ const tenantId = (0, import_apps4.tenantIdFor)(cfg.app.id, env);
2723
2893
  const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/entitlements`, {
2724
2894
  headers: { authorization: `Bearer ${token}` }
2725
2895
  });
@@ -2761,7 +2931,7 @@ async function safeText3(res) {
2761
2931
  // src/config-reconcile-desired.ts
2762
2932
  function desiredRegistryState(cfg) {
2763
2933
  const environments = {};
2764
- const services = (0, import_apps4.orderAppServices)(cfg.services);
2934
+ const services = (0, import_apps5.orderAppServices)(cfg.services);
2765
2935
  for (const env of cfg.envs) {
2766
2936
  const desiredServices = {};
2767
2937
  for (const service of services) {
@@ -2801,8 +2971,208 @@ function managedServiceConfig(cfg, env, service) {
2801
2971
  return {};
2802
2972
  }
2803
2973
 
2974
+ // src/config-reconcile-support.ts
2975
+ var CONDITIONAL_KINDS = /* @__PURE__ */ new Set([
2976
+ "rename_app",
2977
+ "enable_service",
2978
+ "configure_service",
2979
+ "set_link"
2980
+ ]);
2981
+ function configApplySupport(reconciliation) {
2982
+ if (!reconciliation.observedRevision || !reconciliation.registryRevision) {
2983
+ return {
2984
+ supported: false,
2985
+ checkpointRequired: false,
2986
+ reason: "the app must already exist in a revision-aware Registry"
2987
+ };
2988
+ }
2989
+ if (!reconciliation.actions.length) {
2990
+ return {
2991
+ supported: false,
2992
+ checkpointRequired: false,
2993
+ reason: "there are no managed changes to apply"
2994
+ };
2995
+ }
2996
+ const blocked = reconciliation.actions.filter(
2997
+ (action2) => !CONDITIONAL_KINDS.has(action2.kind) || action2.risk !== "low" || action2.requiresApproval || action2.applySupport === "studio" || action2.env === "prod" || action2.env === "production"
2998
+ );
2999
+ if (blocked.length) {
3000
+ return {
3001
+ supported: false,
3002
+ checkpointRequired: blocked.some(
3003
+ (action2) => action2.risk === "high" || action2.requiresApproval || action2.applySupport === "studio" || action2.env === "prod" || action2.env === "production"
3004
+ ),
3005
+ reason: `${blocked.length} action${blocked.length === 1 ? "" : "s"} remain outside conditional apply`
3006
+ };
3007
+ }
3008
+ return {
3009
+ supported: true,
3010
+ checkpointRequired: false,
3011
+ reason: "all actions are low-risk, checkpoint-free Registry changes"
3012
+ };
3013
+ }
3014
+
3015
+ // src/config-operation-command.ts
3016
+ var IDEMPOTENCY_KEY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$/;
3017
+ var DEFAULT_WAIT_SECONDS = 60;
3018
+ var DEFAULT_INTERVAL_SECONDS = 2;
3019
+ async function configApply(options) {
3020
+ const plan = readPlan(options.planPath);
3021
+ const cfg = await loadProjectConfig(options.configPath);
3022
+ assertPlanContext(plan, cfg.app.id, cfg.platformUrl);
3023
+ const support = configApplySupport(plan);
3024
+ if (!support.supported) {
3025
+ throw new ConfigOperationCommandError(
3026
+ support.reason,
3027
+ support.checkpointRequired ? "checkpoint_required" : "invalid_plan"
3028
+ );
3029
+ }
3030
+ if (configDigest(desiredRegistryState(cfg)) !== plan.desiredRevision) {
3031
+ throw new ConfigOperationCommandError(
3032
+ "project config changed after this plan was frozen; generate and review a fresh plan",
3033
+ "checkpoint_required"
3034
+ );
3035
+ }
3036
+ const idempotencyKey = options.idempotencyKey ?? `cli:${plan.planDigest.slice(7)}`;
3037
+ if (!IDEMPOTENCY_KEY.test(idempotencyKey)) {
3038
+ throw new ConfigOperationCommandError("--idempotency-key is not a safe 1-120 character key", "invalid_plan");
3039
+ }
3040
+ const client = await operationClient(cfg, options, "apply");
3041
+ const request2 = {
3042
+ schemaVersion: "odla.config-operation-request/v1",
3043
+ expectedRevision: plan.registryRevision,
3044
+ desiredRevision: plan.desiredRevision,
3045
+ observedRevision: plan.observedRevision,
3046
+ planDigest: plan.planDigest,
3047
+ idempotencyKey,
3048
+ source: { kind: "cli", version: cliVersion() },
3049
+ actions: plan.actions
3050
+ };
3051
+ let receipt;
3052
+ try {
3053
+ receipt = await client.applyConfigOperation(cfg.app.id, request2);
3054
+ } catch (error) {
3055
+ const retained = retainedReceipt(error);
3056
+ if (retained) {
3057
+ verifyReceipt(retained, cfg.app.id);
3058
+ printReceipt(retained, options);
3059
+ throw failureForReceipt(retained);
3060
+ }
3061
+ throw normalizeRequestError(error);
3062
+ }
3063
+ verifyReceipt(receipt, cfg.app.id);
3064
+ printReceipt(receipt, options);
3065
+ assertApplyCompleted(receipt);
3066
+ return receipt;
3067
+ }
3068
+ async function configOperationGet(options) {
3069
+ assertOperationId(options.operationId);
3070
+ const cfg = await loadProjectConfig(options.configPath);
3071
+ const client = await operationClient(cfg, options, "read");
3072
+ const receipt = await client.getConfigOperation(cfg.app.id, options.operationId).catch((error) => {
3073
+ throw normalizeRequestError(error);
3074
+ });
3075
+ if (!receipt) {
3076
+ throw new ConfigOperationCommandError("config operation not found for this app", "operation_not_found");
3077
+ }
3078
+ verifyReceipt(receipt, cfg.app.id, options.operationId);
3079
+ printReceipt(receipt, options);
3080
+ return receipt;
3081
+ }
3082
+ async function configOperationWait(options) {
3083
+ assertOperationId(options.operationId);
3084
+ const cfg = await loadProjectConfig(options.configPath);
3085
+ const client = await operationClient(cfg, options, "wait");
3086
+ const wait2 = options.pollWait ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms)));
3087
+ const now = () => (options.now?.() ?? /* @__PURE__ */ new Date()).getTime();
3088
+ const deadline = now() + (options.timeoutSeconds ?? DEFAULT_WAIT_SECONDS) * 1e3;
3089
+ const interval = (options.intervalSeconds ?? DEFAULT_INTERVAL_SECONDS) * 1e3;
3090
+ let receipt = null;
3091
+ for (; ; ) {
3092
+ receipt = await client.getConfigOperation(cfg.app.id, options.operationId).catch((error) => {
3093
+ throw normalizeRequestError(error);
3094
+ });
3095
+ if (!receipt) {
3096
+ throw new ConfigOperationCommandError("config operation not found for this app", "operation_not_found");
3097
+ }
3098
+ verifyReceipt(receipt, cfg.app.id, options.operationId);
3099
+ if (receipt.state !== "running") break;
3100
+ if (now() >= deadline) {
3101
+ printReceipt(receipt, options);
3102
+ throw new ConfigOperationCommandError("config operation is still running", "operation_pending");
3103
+ }
3104
+ await wait2(Math.min(interval, Math.max(0, deadline - now())));
3105
+ }
3106
+ printReceipt(receipt, options);
3107
+ if (receipt.state !== "succeeded") throw failureForReceipt(receipt);
3108
+ return receipt;
3109
+ }
3110
+ async function operationClient(cfg, options, purpose) {
3111
+ const doFetch = options.fetch ?? fetch;
3112
+ const out = options.stdout ?? console;
3113
+ const token = await resolveAdminPlatformToken({
3114
+ platform: cfg.platformUrl,
3115
+ scope: "app:config:write",
3116
+ token: options.token,
3117
+ tokenFile: (0, import_node_path7.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3118
+ rootDir: cfg.rootDir,
3119
+ email: options.email,
3120
+ open: options.open,
3121
+ fetch: doFetch,
3122
+ stdout: out,
3123
+ openApprovalUrl: options.openApprovalUrl,
3124
+ label: `odla CLI (${cfg.app.id} config operation ${purpose})`
3125
+ });
3126
+ return (0, import_apps6.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
3127
+ }
3128
+ function printReceipt(receipt, options) {
3129
+ const out = options.stdout ?? console;
3130
+ if (options.json) out.log(JSON.stringify(receipt, null, 2));
3131
+ else {
3132
+ out.log(`config operation ${receipt.operationId}: ${receipt.state}`);
3133
+ out.log(`revision: ${receipt.expectedRevision} \u2192 ${receipt.currentRevision}`);
3134
+ for (const step of receipt.progress) out.log(` ${step.state} ${step.actionId} attempts=${step.attempts}`);
3135
+ out.log(`receipt: ${receipt.receiptDigest ?? "pending"}`);
3136
+ out.log(`studio: ${receipt.studioUrl}`);
3137
+ }
3138
+ }
3139
+ function assertApplyCompleted(receipt) {
3140
+ if (receipt.state === "succeeded") return;
3141
+ throw receipt.state === "running" ? new ConfigOperationCommandError("config operation is still running", "operation_pending") : failureForReceipt(receipt);
3142
+ }
3143
+ function failureForReceipt(receipt) {
3144
+ if (receipt.state === "conflict") return new ConfigOperationCommandError(
3145
+ receipt.error?.message ?? "config operation conflicted with current Registry state",
3146
+ "checkpoint_required"
3147
+ );
3148
+ if (receipt.error?.retryable) return new ConfigOperationCommandError(receipt.error.message, "remote_unavailable");
3149
+ return new ConfigOperationCommandError(receipt.error?.message ?? `config operation ${receipt.state}`, "config_operation_failed");
3150
+ }
3151
+ function retainedReceipt(error) {
3152
+ if (!(error instanceof import_apps6.AppsError) || !record3(error.details)) return null;
3153
+ return record3(error.details.operation) ? error.details.operation : null;
3154
+ }
3155
+ function normalizeRequestError(error) {
3156
+ if (!(error instanceof import_apps6.AppsError)) return error instanceof Error ? error : new Error(String(error));
3157
+ if (error.status === 401 || error.status === 403) {
3158
+ return new ConfigOperationCommandError(error.message, "auth_failed");
3159
+ }
3160
+ if (error.status === 409) return new ConfigOperationCommandError(error.message, "checkpoint_required");
3161
+ if (error.status === 429 || error.status >= 500) {
3162
+ return new ConfigOperationCommandError(error.message, "remote_unavailable");
3163
+ }
3164
+ return new ConfigOperationCommandError(error.message, error.code || "config_operation_failed");
3165
+ }
3166
+ function record3(value2) {
3167
+ return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
3168
+ }
3169
+
3170
+ // src/config-reconcile-command.ts
3171
+ var import_apps8 = require("@odla-ai/apps");
3172
+ var import_node_path8 = require("path");
3173
+
2804
3174
  // src/config-reconcile.ts
2805
- var import_apps5 = require("@odla-ai/apps");
3175
+ var import_apps7 = require("@odla-ai/apps");
2806
3176
 
2807
3177
  // src/config-reconcile-values.ts
2808
3178
  function difference(path, desired, observed, status, reason, desiredSource, observedSource, env, service) {
@@ -2903,6 +3273,7 @@ function reconcileConfig(input) {
2903
3273
  sources: { desired: desiredSource, observed: observedSource },
2904
3274
  desiredRevision,
2905
3275
  observedRevision,
3276
+ registryRevision: input.observed?.configRevision ?? null,
2906
3277
  status: different ? "different" : unmanaged ? "unmanaged" : "in_sync",
2907
3278
  summary: {
2908
3279
  differences: different,
@@ -2957,7 +3328,7 @@ function compareEnvironments(desired, observed, desiredSource, observedSource, a
2957
3328
  function compareServices(env, desired, observed, desiredSource, observedSource, add) {
2958
3329
  const wanted = desired.environments[env]?.services ?? {};
2959
3330
  const live = observed?.environments[env] ?? {};
2960
- const knownOrder = (0, import_apps5.orderAppServices)((0, import_apps5.appServiceIds)());
3331
+ const knownOrder = (0, import_apps7.orderAppServices)((0, import_apps7.appServiceIds)());
2961
3332
  const services = [.../* @__PURE__ */ new Set([...knownOrder, ...Object.keys(wanted), ...Object.keys(live)])];
2962
3333
  for (const service of services) {
2963
3334
  const next = wanted[service];
@@ -3068,20 +3439,19 @@ async function configDiff(options) {
3068
3439
  }
3069
3440
  async function configPlan(options) {
3070
3441
  const reconciliation = await inspectConfig(options);
3442
+ const apply = configApplySupport(reconciliation);
3071
3443
  const planDigest = configDigest({
3072
- schemaVersion: "odla.config-plan/v1",
3444
+ schemaVersion: "odla.config-plan/v2",
3073
3445
  desiredRevision: reconciliation.desiredRevision,
3074
3446
  observedRevision: reconciliation.observedRevision,
3447
+ registryRevision: reconciliation.registryRevision,
3075
3448
  actions: reconciliation.actions
3076
3449
  });
3077
3450
  const document2 = {
3078
- schemaVersion: "odla.config-plan/v1",
3451
+ schemaVersion: "odla.config-plan/v2",
3079
3452
  ...reconciliation,
3080
3453
  planDigest,
3081
- apply: {
3082
- supported: false,
3083
- reason: "conditional, resumable config apply is not available yet; use the exact reviewed commands below"
3084
- },
3454
+ apply,
3085
3455
  nextActions: planNextActions(reconciliation, options.configPath)
3086
3456
  };
3087
3457
  printPlan2(document2, options);
@@ -3095,7 +3465,7 @@ async function inspectConfig(options) {
3095
3465
  platform: cfg.platformUrl,
3096
3466
  scope: "app:config:read",
3097
3467
  token: options.token,
3098
- tokenFile: (0, import_node_path7.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3468
+ tokenFile: (0, import_node_path8.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3099
3469
  rootDir: cfg.rootDir,
3100
3470
  email: options.email,
3101
3471
  open: options.open,
@@ -3104,7 +3474,7 @@ async function inspectConfig(options) {
3104
3474
  openApprovalUrl: options.openApprovalUrl,
3105
3475
  label: `odla CLI (${cfg.app.id} config read)`
3106
3476
  });
3107
- const client = (0, import_apps6.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
3477
+ const client = (0, import_apps8.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
3108
3478
  const observed = await client.resolveApp(cfg.app.id);
3109
3479
  return reconcileConfig({
3110
3480
  desired: desiredRegistryState(cfg),
@@ -3140,7 +3510,7 @@ function printPlan2(document2, options) {
3140
3510
  }
3141
3511
  if (!document2.actions.length) out.log(" no managed changes");
3142
3512
  out.log(`plan digest: ${document2.planDigest}`);
3143
- out.log(`apply: unsupported \u2014 ${document2.apply.reason}`);
3513
+ out.log(`apply: ${document2.apply.supported ? "supported" : "blocked"} \u2014 ${document2.apply.reason}`);
3144
3514
  printNext(out, document2.nextActions);
3145
3515
  }
3146
3516
  function printHeader(out, kind, document2) {
@@ -3148,6 +3518,7 @@ function printHeader(out, kind, document2) {
3148
3518
  out.log(`platform: ${document2.scope.platformUrl}`);
3149
3519
  out.log(`desired: ${document2.desiredRevision}`);
3150
3520
  out.log(`observed: ${document2.observedRevision ?? "absent"}`);
3521
+ out.log(`registry: ${document2.registryRevision ?? "absent"}`);
3151
3522
  out.log(
3152
3523
  `summary: ${document2.summary.differences} different, ${document2.summary.unmanaged} unmanaged, ${document2.summary.actions} planned actions`
3153
3524
  );
@@ -3180,6 +3551,13 @@ function diffNextActions(reconciliation, configPath) {
3180
3551
  }
3181
3552
  function planNextActions(reconciliation, configPath) {
3182
3553
  const next = [];
3554
+ if (configApplySupport(reconciliation).supported) {
3555
+ next.push({
3556
+ code: "apply_frozen_plan",
3557
+ command: "odla-ai config apply --plan <saved-plan.json> --json",
3558
+ description: "Save this JSON document, then conditionally apply these exact revision-bound actions."
3559
+ });
3560
+ }
3183
3561
  if (reconciliation.actions.some((action2) => action2.applySupport === "provision")) {
3184
3562
  next.push({
3185
3563
  code: "review_provision",
@@ -3193,35 +3571,39 @@ function planNextActions(reconciliation, configPath) {
3193
3571
  }
3194
3572
  }
3195
3573
  if (reconciliation.actions.some((action2) => action2.applySupport === "studio")) {
3196
- const { appId, platformUrl } = reconciliation.scope;
3197
3574
  next.push({
3198
3575
  code: "review_destructive",
3199
- command: `${platformUrl}/studio/apps/${encodeURIComponent(appId)}/settings`,
3576
+ command: studioSettingsUrl(reconciliation),
3200
3577
  description: "Review service disablement in the owning Studio scope; this plan will not apply it."
3201
3578
  });
3202
3579
  }
3203
3580
  if (!reconciliation.actions.length && reconciliation.summary.unmanaged) {
3204
3581
  next.push({
3205
3582
  code: "declare_or_accept_runtime_state",
3206
- command: `${reconciliation.scope.platformUrl}/studio/apps/${encodeURIComponent(reconciliation.scope.appId)}/settings`,
3583
+ command: studioSettingsUrl(reconciliation),
3207
3584
  description: "Declare the live value in project config or keep it as an explicit runtime-managed setting."
3208
3585
  });
3209
3586
  }
3210
3587
  return next;
3211
3588
  }
3589
+ function studioSettingsUrl(reconciliation) {
3590
+ const environment2 = reconciliation.actions.map((action2) => action2.env).concat(reconciliation.scope.environments).find((candidate) => candidate === "dev" || candidate === "prod") ?? "dev";
3591
+ const origin = reconciliation.scope.platformUrl.replace(/\/$/, "");
3592
+ return `${origin}${(0, import_apps8.studioAppSettingsPath)(reconciliation.scope.appId, environment2, "environment")}`;
3593
+ }
3212
3594
  function quoteArg2(value2) {
3213
3595
  return `'${value2.replace(/'/g, `'\\''`)}'`;
3214
3596
  }
3215
3597
 
3216
3598
  // src/doctor-checks.ts
3217
3599
  var import_node_child_process3 = require("child_process");
3218
- var import_node_fs10 = require("fs");
3219
- var import_node_path9 = require("path");
3600
+ var import_node_fs12 = require("fs");
3601
+ var import_node_path10 = require("path");
3220
3602
 
3221
3603
  // src/wrangler.ts
3222
3604
  var import_node_child_process2 = require("child_process");
3223
- var import_node_fs9 = require("fs");
3224
- var import_node_path8 = require("path");
3605
+ var import_node_fs11 = require("fs");
3606
+ var import_node_path9 = require("path");
3225
3607
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
3226
3608
  const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
3227
3609
  let stdout = "";
@@ -3235,28 +3617,28 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) =>
3235
3617
  var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
3236
3618
  function findWranglerConfig(rootDir) {
3237
3619
  for (const name of WRANGLER_CONFIG_FILES) {
3238
- const path = (0, import_node_path8.join)(rootDir, name);
3239
- if ((0, import_node_fs9.existsSync)(path)) return path;
3620
+ const path = (0, import_node_path9.join)(rootDir, name);
3621
+ if ((0, import_node_fs11.existsSync)(path)) return path;
3240
3622
  }
3241
3623
  return null;
3242
3624
  }
3243
3625
  function readWranglerConfig(path) {
3244
3626
  if (path.endsWith(".toml")) return null;
3245
3627
  try {
3246
- return JSON.parse(stripJsonComments((0, import_node_fs9.readFileSync)(path, "utf8")));
3628
+ return JSON.parse(stripJsonComments((0, import_node_fs11.readFileSync)(path, "utf8")));
3247
3629
  } catch {
3248
3630
  return null;
3249
3631
  }
3250
3632
  }
3251
- function stripJsonComments(text) {
3633
+ function stripJsonComments(text2) {
3252
3634
  let result = "";
3253
3635
  let inString = false;
3254
- for (let i = 0; i < text.length; i++) {
3255
- const ch = text[i];
3636
+ for (let i = 0; i < text2.length; i++) {
3637
+ const ch = text2[i];
3256
3638
  if (inString) {
3257
3639
  result += ch;
3258
3640
  if (ch === "\\") {
3259
- result += text[i + 1] ?? "";
3641
+ result += text2[i + 1] ?? "";
3260
3642
  i++;
3261
3643
  } else if (ch === '"') {
3262
3644
  inString = false;
@@ -3268,14 +3650,14 @@ function stripJsonComments(text) {
3268
3650
  result += ch;
3269
3651
  continue;
3270
3652
  }
3271
- if (ch === "/" && text[i + 1] === "/") {
3272
- while (i < text.length && text[i] !== "\n") i++;
3653
+ if (ch === "/" && text2[i + 1] === "/") {
3654
+ while (i < text2.length && text2[i] !== "\n") i++;
3273
3655
  result += "\n";
3274
3656
  continue;
3275
3657
  }
3276
- if (ch === "/" && text[i + 1] === "*") {
3658
+ if (ch === "/" && text2[i + 1] === "*") {
3277
3659
  i += 2;
3278
- while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
3660
+ while (i < text2.length && !(text2[i] === "*" && text2[i + 1] === "/")) i++;
3279
3661
  i++;
3280
3662
  continue;
3281
3663
  }
@@ -3292,7 +3674,14 @@ async function wranglerLoggedIn(run, cwd) {
3292
3674
  }
3293
3675
  }
3294
3676
  function wranglerPutSecret(run, opts) {
3295
- const args = ["wrangler", "secret", "put", opts.name, ...opts.env ? ["--env", opts.env] : []];
3677
+ const args = [
3678
+ "wrangler",
3679
+ "secret",
3680
+ "put",
3681
+ opts.name,
3682
+ ...opts.env ? ["--env", opts.env] : [],
3683
+ ...opts.configPath ? ["--config", opts.configPath] : []
3684
+ ];
3296
3685
  return run("npx", args, { input: opts.value, cwd: opts.cwd });
3297
3686
  }
3298
3687
 
@@ -3341,10 +3730,10 @@ function wranglerWarnings(rootDir) {
3341
3730
  for (const { label, block } of blocks) {
3342
3731
  const assets = block.assets;
3343
3732
  if (assets?.directory) {
3344
- const dir = (0, import_node_path9.resolve)(rootDir, assets.directory);
3345
- if (dir === (0, import_node_path9.resolve)(rootDir)) {
3733
+ const dir = (0, import_node_path10.resolve)(rootDir, assets.directory);
3734
+ if (dir === (0, import_node_path10.resolve)(rootDir)) {
3346
3735
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
3347
- } else if ((0, import_node_fs10.existsSync)((0, import_node_path9.join)(dir, "node_modules"))) {
3736
+ } else if ((0, import_node_fs12.existsSync)((0, import_node_path10.join)(dir, "node_modules"))) {
3348
3737
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
3349
3738
  }
3350
3739
  }
@@ -3379,13 +3768,13 @@ function o11yProjectWarnings(rootDir) {
3379
3768
  warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
3380
3769
  return warnings;
3381
3770
  }
3382
- const main = typeof config.main === "string" ? (0, import_node_path9.resolve)(rootDir, config.main) : null;
3383
- if (!main || !(0, import_node_fs10.existsSync)(main)) {
3771
+ const main = typeof config.main === "string" ? (0, import_node_path10.resolve)(rootDir, config.main) : null;
3772
+ if (!main || !(0, import_node_fs12.existsSync)(main)) {
3384
3773
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
3385
3774
  } else {
3386
3775
  let source = "";
3387
3776
  try {
3388
- source = (0, import_node_fs10.readFileSync)(main, "utf8");
3777
+ source = (0, import_node_fs12.readFileSync)(main, "utf8");
3389
3778
  } catch {
3390
3779
  }
3391
3780
  if (!/\bwithObservability\b/.test(source)) {
@@ -3409,7 +3798,7 @@ function calendarProjectWarnings(rootDir) {
3409
3798
  }
3410
3799
  function readPackageJson(rootDir) {
3411
3800
  try {
3412
- return JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path9.join)(rootDir, "package.json"), "utf8"));
3801
+ return JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path10.join)(rootDir, "package.json"), "utf8"));
3413
3802
  } catch {
3414
3803
  return null;
3415
3804
  }
@@ -3636,14 +4025,14 @@ function harnessOption(value2, flag) {
3636
4025
  }
3637
4026
 
3638
4027
  // src/init.ts
3639
- var import_node_fs11 = require("fs");
3640
- var import_node_path10 = require("path");
3641
- var import_apps7 = require("@odla-ai/apps");
4028
+ var import_node_fs13 = require("fs");
4029
+ var import_node_path11 = require("path");
4030
+ var import_apps9 = require("@odla-ai/apps");
3642
4031
  function initProject(options) {
3643
4032
  const out = options.stdout ?? console;
3644
- const rootDir = (0, import_node_path10.resolve)(options.rootDir ?? process.cwd());
3645
- const configPath = (0, import_node_path10.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
3646
- if ((0, import_node_fs11.existsSync)(configPath) && !options.force) {
4033
+ const rootDir = (0, import_node_path11.resolve)(options.rootDir ?? process.cwd());
4034
+ const configPath = (0, import_node_path11.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
4035
+ if ((0, import_node_fs13.existsSync)(configPath) && !options.force) {
3647
4036
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
3648
4037
  }
3649
4038
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -3652,27 +4041,27 @@ function initProject(options) {
3652
4041
  const envs = options.envs?.length ? options.envs : ["dev"];
3653
4042
  const services = options.services?.length ? options.services : ["db", "ai"];
3654
4043
  for (const service of services) {
3655
- const definition = (0, import_apps7.appServiceDefinition)(service);
3656
- if (!definition) throw new Error(`--services contains unknown service "${service}" (known: ${(0, import_apps7.appServiceIds)().join(", ")})`);
4044
+ const definition = (0, import_apps9.appServiceDefinition)(service);
4045
+ if (!definition) throw new Error(`--services contains unknown service "${service}" (known: ${(0, import_apps9.appServiceIds)().join(", ")})`);
3657
4046
  for (const dependency of definition.requires) {
3658
4047
  if (!services.includes(dependency)) throw new Error(`--services ${service} requires ${dependency}`);
3659
4048
  }
3660
4049
  }
3661
4050
  const aiProvider = options.aiProvider ?? "anthropic";
3662
- (0, import_node_fs11.mkdirSync)((0, import_node_path10.dirname)(configPath), { recursive: true });
3663
- (0, import_node_fs11.mkdirSync)((0, import_node_path10.resolve)(rootDir, "src/odla"), { recursive: true });
3664
- (0, import_node_fs11.mkdirSync)((0, import_node_path10.resolve)(rootDir, ".odla"), { recursive: true });
3665
- (0, import_node_fs11.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
3666
- writeIfMissing((0, import_node_path10.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
3667
- writeIfMissing((0, import_node_path10.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
4051
+ (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(configPath), { recursive: true });
4052
+ (0, import_node_fs13.mkdirSync)((0, import_node_path11.resolve)(rootDir, "src/odla"), { recursive: true });
4053
+ (0, import_node_fs13.mkdirSync)((0, import_node_path11.resolve)(rootDir, ".odla"), { recursive: true });
4054
+ (0, import_node_fs13.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
4055
+ writeIfMissing((0, import_node_path11.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
4056
+ writeIfMissing((0, import_node_path11.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
3668
4057
  ensureGitignore(rootDir);
3669
4058
  out.log(`created ${relativeDisplay(configPath, rootDir)}`);
3670
4059
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
3671
4060
  out.log("updated .gitignore for local odla credentials");
3672
4061
  }
3673
- function writeIfMissing(path, text) {
3674
- if ((0, import_node_fs11.existsSync)(path)) return;
3675
- (0, import_node_fs11.writeFileSync)(path, text);
4062
+ function writeIfMissing(path, text2) {
4063
+ if ((0, import_node_fs13.existsSync)(path)) return;
4064
+ (0, import_node_fs13.writeFileSync)(path, text2);
3676
4065
  }
3677
4066
  function configTemplate(input) {
3678
4067
  const calendar = input.services.includes("calendar") ? ` calendar: {
@@ -3847,7 +4236,7 @@ function assertWranglerConfig(cfg) {
3847
4236
 
3848
4237
  // src/secrets-set.ts
3849
4238
  var import_ai2 = require("@odla-ai/ai");
3850
- var import_apps8 = require("@odla-ai/apps");
4239
+ var import_apps10 = require("@odla-ai/apps");
3851
4240
  var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
3852
4241
  async function secretsSet(options) {
3853
4242
  const name = (options.name ?? "").trim();
@@ -3880,13 +4269,13 @@ async function secretsSetClerkKey(options) {
3880
4269
  body: JSON.stringify({ value: value2 })
3881
4270
  });
3882
4271
  if (!res.ok) {
3883
- const text = scrubValue((await res.text().catch(() => "")).slice(0, 300), value2);
3884
- throw new Error(`store Clerk secret key failed (${res.status}): ${text || "request failed"}`);
4272
+ const text2 = scrubValue((await res.text().catch(() => "")).slice(0, 300), value2);
4273
+ throw new Error(`store Clerk secret key failed (${res.status}): ${text2 || "request failed"}`);
3885
4274
  }
3886
4275
  out.log(`Clerk secret key stored for ${tenantId} ($clerk_secret, reserved + write-only; the value was never echoed)`);
3887
4276
  }
3888
- function scrubValue(text, value2) {
3889
- return redactSecrets(text).split(value2).join("[value redacted]");
4277
+ function scrubValue(text2, value2) {
4278
+ return redactSecrets(text2).split(value2).join("[value redacted]");
3890
4279
  }
3891
4280
  async function resolveVaultWrite(options) {
3892
4281
  const out = options.stdout ?? console;
@@ -3899,13 +4288,13 @@ async function resolveVaultWrite(options) {
3899
4288
  throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
3900
4289
  }
3901
4290
  const value2 = await secretInputValue(options, "secret");
3902
- return { cfg, tenantId: (0, import_apps8.tenantIdFor)(cfg.app.id, options.env), value: value2, doFetch, out };
4291
+ return { cfg, tenantId: (0, import_apps10.tenantIdFor)(cfg.app.id, options.env), value: value2, doFetch, out };
3903
4292
  }
3904
4293
 
3905
4294
  // src/skill.ts
3906
- var import_node_fs12 = require("fs");
4295
+ var import_node_fs14 = require("fs");
3907
4296
  var import_node_os2 = require("os");
3908
- var import_node_path11 = require("path");
4297
+ var import_node_path12 = require("path");
3909
4298
  var import_node_url2 = require("url");
3910
4299
 
3911
4300
  // src/skill-adapters.ts
@@ -3984,8 +4373,8 @@ function installSkill(options = {}) {
3984
4373
  const files = listFiles(sourceDir);
3985
4374
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
3986
4375
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
3987
- const root = (0, import_node_path11.resolve)(options.dir ?? process.cwd());
3988
- const home = (0, import_node_path11.resolve)(options.homeDir ?? (0, import_node_os2.homedir)());
4376
+ const root = (0, import_node_path12.resolve)(options.dir ?? process.cwd());
4377
+ const home = (0, import_node_path12.resolve)(options.homeDir ?? (0, import_node_os2.homedir)());
3989
4378
  const plans = /* @__PURE__ */ new Map();
3990
4379
  const targets = /* @__PURE__ */ new Map();
3991
4380
  const rememberTarget = (harness, target) => {
@@ -3999,48 +4388,48 @@ function installSkill(options = {}) {
3999
4388
  plans.set(target, { target, content: content2, boundary, managedMerge });
4000
4389
  };
4001
4390
  const planSkillTree = (targetDir2, boundary = root) => {
4002
- for (const rel of files) plan((0, import_node_path11.join)(targetDir2, rel), (0, import_node_fs12.readFileSync)((0, import_node_path11.join)(sourceDir, rel), "utf8"), false, boundary);
4391
+ for (const rel of files) plan((0, import_node_path12.join)(targetDir2, rel), (0, import_node_fs14.readFileSync)((0, import_node_path12.join)(sourceDir, rel), "utf8"), false, boundary);
4003
4392
  };
4004
4393
  let targetDir;
4005
4394
  if (options.global) {
4006
- const claudeRoot = (0, import_node_path11.join)(home, ".claude", "skills");
4007
- const codexRoot = (0, import_node_path11.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path11.join)(home, ".codex"), "skills");
4395
+ const claudeRoot = (0, import_node_path12.join)(home, ".claude", "skills");
4396
+ const codexRoot = (0, import_node_path12.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path12.join)(home, ".codex"), "skills");
4008
4397
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
4009
4398
  for (const harness of harnesses) {
4010
4399
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
4011
- planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path11.dirname)((0, import_node_path11.dirname)(codexRoot)));
4400
+ planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path12.dirname)((0, import_node_path12.dirname)(codexRoot)));
4012
4401
  rememberTarget(harness, skillRoot);
4013
4402
  }
4014
4403
  } else {
4015
- const sharedRoot = (0, import_node_path11.join)(root, ".agents", "skills");
4404
+ const sharedRoot = (0, import_node_path12.join)(root, ".agents", "skills");
4016
4405
  planSkillTree(sharedRoot);
4017
- const claudeRoot = (0, import_node_path11.join)(root, ".claude", "skills");
4406
+ const claudeRoot = (0, import_node_path12.join)(root, ".claude", "skills");
4018
4407
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
4019
4408
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
4020
4409
  if (harnesses.includes("claude")) {
4021
4410
  for (const skill of skillNames(files)) {
4022
- const canonical = (0, import_node_fs12.readFileSync)((0, import_node_path11.join)(sourceDir, skill, "SKILL.md"), "utf8");
4023
- plan((0, import_node_path11.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
4411
+ const canonical = (0, import_node_fs14.readFileSync)((0, import_node_path12.join)(sourceDir, skill, "SKILL.md"), "utf8");
4412
+ plan((0, import_node_path12.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
4024
4413
  }
4025
4414
  rememberTarget("claude", claudeRoot);
4026
4415
  }
4027
4416
  if (harnesses.includes("cursor")) {
4028
- const cursorRule = (0, import_node_path11.join)(root, ".cursor", "rules", "odla.mdc");
4417
+ const cursorRule = (0, import_node_path12.join)(root, ".cursor", "rules", "odla.mdc");
4029
4418
  plan(cursorRule, CURSOR_RULE);
4030
4419
  rememberTarget("cursor", cursorRule);
4031
4420
  }
4032
4421
  if (harnesses.includes("agents")) {
4033
- const agentsFile = (0, import_node_path11.join)(root, "AGENTS.md");
4422
+ const agentsFile = (0, import_node_path12.join)(root, "AGENTS.md");
4034
4423
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
4035
4424
  rememberTarget("agents", agentsFile);
4036
4425
  }
4037
4426
  if (harnesses.includes("copilot")) {
4038
- const copilotFile = (0, import_node_path11.join)(root, ".github", "copilot-instructions.md");
4427
+ const copilotFile = (0, import_node_path12.join)(root, ".github", "copilot-instructions.md");
4039
4428
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
4040
4429
  rememberTarget("copilot", copilotFile);
4041
4430
  }
4042
4431
  if (harnesses.includes("gemini")) {
4043
- const geminiFile = (0, import_node_path11.join)(root, "GEMINI.md");
4432
+ const geminiFile = (0, import_node_path12.join)(root, "GEMINI.md");
4044
4433
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
4045
4434
  rememberTarget("gemini", geminiFile);
4046
4435
  }
@@ -4054,11 +4443,11 @@ function installSkill(options = {}) {
4054
4443
  conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
4055
4444
  continue;
4056
4445
  }
4057
- if (!(0, import_node_fs12.existsSync)(file.target)) {
4446
+ if (!(0, import_node_fs14.existsSync)(file.target)) {
4058
4447
  writtenPaths.add(file.target);
4059
4448
  continue;
4060
4449
  }
4061
- const current = (0, import_node_fs12.readFileSync)(file.target, "utf8");
4450
+ const current = (0, import_node_fs14.readFileSync)(file.target, "utf8");
4062
4451
  if (current === file.content) {
4063
4452
  unchangedPaths.add(file.target);
4064
4453
  } else if (file.managedMerge || options.force) {
@@ -4075,9 +4464,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
4075
4464
  );
4076
4465
  }
4077
4466
  for (const file of plans.values()) {
4078
- if (!(0, import_node_fs12.existsSync)(file.target) || (0, import_node_fs12.readFileSync)(file.target, "utf8") !== file.content) {
4079
- (0, import_node_fs12.mkdirSync)((0, import_node_path11.dirname)(file.target), { recursive: true });
4080
- (0, import_node_fs12.writeFileSync)(file.target, file.content);
4467
+ if (!(0, import_node_fs14.existsSync)(file.target) || (0, import_node_fs14.readFileSync)(file.target, "utf8") !== file.content) {
4468
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(file.target), { recursive: true });
4469
+ (0, import_node_fs14.writeFileSync)(file.target, file.content);
4081
4470
  }
4082
4471
  }
4083
4472
  const skills = skillNames(files);
@@ -4096,7 +4485,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
4096
4485
  };
4097
4486
  }
4098
4487
  function pathsUnder(root, paths) {
4099
- return [...paths].map((path) => (0, import_node_path11.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path11.sep}`) && !(0, import_node_path11.isAbsolute)(path)).sort();
4488
+ return [...paths].map((path) => (0, import_node_path12.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path12.sep}`) && !(0, import_node_path12.isAbsolute)(path)).sort();
4100
4489
  }
4101
4490
  function normalizeHarnesses(values, global) {
4102
4491
  const requested = values?.length ? values : ["claude"];
@@ -4118,9 +4507,9 @@ function normalizeHarnesses(values, global) {
4118
4507
  function managedFileContent(path, block, force, boundary) {
4119
4508
  const symlink = symlinkedComponent(boundary, path);
4120
4509
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
4121
- if (!(0, import_node_fs12.existsSync)(path)) return `${block}
4510
+ if (!(0, import_node_fs14.existsSync)(path)) return `${block}
4122
4511
  `;
4123
- const current = (0, import_node_fs12.readFileSync)(path, "utf8");
4512
+ const current = (0, import_node_fs14.readFileSync)(path, "utf8");
4124
4513
  const start = "<!-- odla-ai agent setup:start -->";
4125
4514
  const end = "<!-- odla-ai agent setup:end -->";
4126
4515
  const startAt = current.indexOf(start);
@@ -4141,15 +4530,15 @@ function managedFileContent(path, block, force, boundary) {
4141
4530
  return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
4142
4531
  }
4143
4532
  function symlinkedComponent(boundary, target) {
4144
- const rel = (0, import_node_path11.relative)(boundary, target);
4145
- if (rel === ".." || rel.startsWith(`..${import_node_path11.sep}`) || (0, import_node_path11.isAbsolute)(rel)) {
4533
+ const rel = (0, import_node_path12.relative)(boundary, target);
4534
+ if (rel === ".." || rel.startsWith(`..${import_node_path12.sep}`) || (0, import_node_path12.isAbsolute)(rel)) {
4146
4535
  throw new Error(`agent setup target escapes its install root: ${target}`);
4147
4536
  }
4148
4537
  let current = boundary;
4149
- for (const part of rel.split(import_node_path11.sep).filter(Boolean)) {
4150
- current = (0, import_node_path11.join)(current, part);
4538
+ for (const part of rel.split(import_node_path12.sep).filter(Boolean)) {
4539
+ current = (0, import_node_path12.join)(current, part);
4151
4540
  try {
4152
- if ((0, import_node_fs12.lstatSync)(current).isSymbolicLink()) return current;
4541
+ if ((0, import_node_fs14.lstatSync)(current).isSymbolicLink()) return current;
4153
4542
  } catch (error) {
4154
4543
  if (error.code !== "ENOENT") throw error;
4155
4544
  }
@@ -4160,13 +4549,13 @@ function skillNames(files) {
4160
4549
  return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
4161
4550
  }
4162
4551
  function listFiles(dir) {
4163
- if (!(0, import_node_fs12.existsSync)(dir)) return [];
4552
+ if (!(0, import_node_fs14.existsSync)(dir)) return [];
4164
4553
  const results = [];
4165
4554
  const walk = (current) => {
4166
- for (const entry of (0, import_node_fs12.readdirSync)(current, { withFileTypes: true })) {
4167
- const path = (0, import_node_path11.join)(current, entry.name);
4555
+ for (const entry of (0, import_node_fs14.readdirSync)(current, { withFileTypes: true })) {
4556
+ const path = (0, import_node_path12.join)(current, entry.name);
4168
4557
  if (entry.isDirectory()) walk(path);
4169
- else results.push((0, import_node_path11.relative)(dir, path));
4558
+ else results.push((0, import_node_path12.relative)(dir, path));
4170
4559
  }
4171
4560
  };
4172
4561
  walk(dir);
@@ -4361,10 +4750,14 @@ async function secretsCommand(parsed, deps) {
4361
4750
  async function projectCommand(command, parsed, deps) {
4362
4751
  if (command === "config") {
4363
4752
  const sub = parsed.positionals[1];
4364
- if (sub !== "diff" && sub !== "plan") {
4753
+ if (sub !== "diff" && sub !== "plan" && sub !== "apply") {
4365
4754
  throw new Error(`unknown config subcommand "${sub ?? ""}". Try "odla-ai config diff --json".`);
4366
4755
  }
4367
- assertArgs(parsed, ["config", "token", "email", "open", "json"], 2);
4756
+ assertArgs(
4757
+ parsed,
4758
+ sub === "apply" ? ["config", "plan", "idempotency-key", "token", "email", "open", "json"] : ["config", "token", "email", "open", "json"],
4759
+ 2
4760
+ );
4368
4761
  const options = {
4369
4762
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
4370
4763
  token: stringOpt(parsed.options.token),
@@ -4376,7 +4769,42 @@ async function projectCommand(command, parsed, deps) {
4376
4769
  stdout: deps.stdout
4377
4770
  };
4378
4771
  if (sub === "diff") await configDiff(options);
4379
- else await configPlan(options);
4772
+ else if (sub === "plan") await configPlan(options);
4773
+ else await configApply({
4774
+ ...options,
4775
+ planPath: requiredString(parsed.options.plan, "--plan"),
4776
+ idempotencyKey: stringOpt(parsed.options["idempotency-key"])
4777
+ });
4778
+ return true;
4779
+ }
4780
+ if (command === "operations") {
4781
+ const sub = parsed.positionals[1];
4782
+ if (sub !== "get" && sub !== "wait") {
4783
+ throw new Error(`unknown operations subcommand "${sub ?? ""}". Try "odla-ai operations get <operation-id> --json".`);
4784
+ }
4785
+ assertArgs(
4786
+ parsed,
4787
+ sub === "wait" ? ["config", "token", "email", "open", "json", "interval", "timeout"] : ["config", "token", "email", "open", "json"],
4788
+ 3
4789
+ );
4790
+ const operationId = parsed.positionals[2];
4791
+ if (!operationId) throw new Error(`operation id is required`);
4792
+ const options = {
4793
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
4794
+ operationId,
4795
+ token: stringOpt(parsed.options.token),
4796
+ email: stringOpt(parsed.options.email),
4797
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
4798
+ json: parsed.options.json === true,
4799
+ fetch: deps.fetch,
4800
+ openApprovalUrl: deps.openUrl,
4801
+ stdout: deps.stdout,
4802
+ pollWait: deps.pollWait,
4803
+ intervalSeconds: numberOpt(parsed.options.interval, "--interval"),
4804
+ timeoutSeconds: numberOpt(parsed.options.timeout, "--timeout")
4805
+ };
4806
+ if (sub === "get") await configOperationGet(options);
4807
+ else await configOperationWait(options);
4380
4808
  return true;
4381
4809
  }
4382
4810
  if (command === "init") {
@@ -4437,9 +4865,9 @@ async function projectCommand(command, parsed, deps) {
4437
4865
  }
4438
4866
 
4439
4867
  // src/code-connect.ts
4440
- var import_node_fs14 = require("fs");
4868
+ var import_node_fs15 = require("fs");
4441
4869
  var import_node_os4 = require("os");
4442
- var import_node_path13 = require("path");
4870
+ var import_node_path14 = require("path");
4443
4871
 
4444
4872
  // ../harness/dist/chunk-QTUEF2HZ.js
4445
4873
  var HARNESS_PROTOCOL_VERSION = 1;
@@ -4449,7 +4877,7 @@ var CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
4449
4877
  var HarnessProtocolError = class extends Error {
4450
4878
  name = "HarnessProtocolError";
4451
4879
  };
4452
- function record2(value2) {
4880
+ function record4(value2) {
4453
4881
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
4454
4882
  }
4455
4883
  function boundedText(value2, label, max) {
@@ -4466,7 +4894,7 @@ function parseAgentOutput(line) {
4466
4894
  } catch {
4467
4895
  throw new HarnessProtocolError("agent emitted invalid JSON");
4468
4896
  }
4469
- const message2 = record2(value2);
4897
+ const message2 = record4(value2);
4470
4898
  if (!message2 || message2.protocolVersion !== HARNESS_PROTOCOL_VERSION) {
4471
4899
  throw new HarnessProtocolError(`agent protocolVersion must be ${HARNESS_PROTOCOL_VERSION}`);
4472
4900
  }
@@ -4479,7 +4907,7 @@ function parseAgentOutput(line) {
4479
4907
  };
4480
4908
  }
4481
4909
  if (message2.type === "inference.request") {
4482
- const call2 = record2(message2.call);
4910
+ const call2 = record4(message2.call);
4483
4911
  if (!call2 || !Array.isArray(call2.messages) || !Number.isSafeInteger(call2.maxTokens)) {
4484
4912
  throw new HarnessProtocolError("inference.request.call requires messages and maxTokens");
4485
4913
  }
@@ -4491,7 +4919,7 @@ function parseAgentOutput(line) {
4491
4919
  };
4492
4920
  }
4493
4921
  if (message2.type === "tool.request") {
4494
- const input = record2(message2.input);
4922
+ const input = record4(message2.input);
4495
4923
  const tool = String(message2.tool);
4496
4924
  if (!input || !["sandbox.read", "sandbox.apply_patch", "sandbox.run_recipe"].includes(tool)) {
4497
4925
  throw new HarnessProtocolError("tool.request requires a registered tool and object input");
@@ -4675,8 +5103,8 @@ async function runContainerAttempt(options) {
4675
5103
  let stopped = false;
4676
5104
  let exited = false;
4677
5105
  child.stderr.setEncoding("utf8");
4678
- child.stderr.on("data", (text) => {
4679
- if (stderr.length < 64 * 1024) stderr += text.slice(0, 64 * 1024 - stderr.length);
5106
+ child.stderr.on("data", (text2) => {
5107
+ if (stderr.length < 64 * 1024) stderr += text2.slice(0, 64 * 1024 - stderr.length);
4680
5108
  });
4681
5109
  const stop = (reason) => {
4682
5110
  if (stopped || exited) return;
@@ -4834,8 +5262,8 @@ async function materializeGitTree(source, commitSha, options = {}) {
4834
5262
  const maxFiles = options.maxFiles ?? 2e4;
4835
5263
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
4836
5264
  const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
4837
- const entries = inventory.flatMap((record8) => {
4838
- const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record8);
5265
+ const entries = inventory.flatMap((record11) => {
5266
+ const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record11);
4839
5267
  return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
4840
5268
  });
4841
5269
  if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
@@ -5083,8 +5511,8 @@ function normalize(value2) {
5083
5511
  if (Array.isArray(value2)) return value2.map(normalize);
5084
5512
  if (value2 instanceof Uint8Array) return { $bytes: [...value2] };
5085
5513
  if (typeof value2 === "object") {
5086
- const record8 = value2;
5087
- return Object.fromEntries(Object.keys(record8).filter((key) => record8[key] !== void 0).sort().map((key) => [key, normalize(record8[key])]));
5514
+ const record11 = value2;
5515
+ return Object.fromEntries(Object.keys(record11).filter((key) => record11[key] !== void 0).sort().map((key) => [key, normalize(record11[key])]));
5088
5516
  }
5089
5517
  throw new CamelError("state_conflict", "Canonical JSON rejects unsupported values.");
5090
5518
  }
@@ -5163,7 +5591,7 @@ function normalizeReaders(readers) {
5163
5591
  }
5164
5592
 
5165
5593
  // ../camel/dist/code.js
5166
- var DIGEST = /^sha256:[0-9a-f]{64}$/;
5594
+ var DIGEST2 = /^sha256:[0-9a-f]{64}$/;
5167
5595
  var SHA = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
5168
5596
  var ID = /^[A-Za-z0-9._:-]{1,160}$/;
5169
5597
  async function digestCodeVerificationReceipt(fields) {
@@ -5198,18 +5626,18 @@ async function digestCodeVerificationReceipt(fields) {
5198
5626
  return `sha256:${await sha256Hex(canonicalJson2(canonical))}`;
5199
5627
  }
5200
5628
  function validate(fields) {
5201
- if (fields.schemaVersion !== 1 || typeof fields.verificationId !== "string" || !ID.test(fields.verificationId) || typeof fields.trustedBaseCommitSha !== "string" || !SHA.test(fields.trustedBaseCommitSha) || !DIGEST.test(fields.trustedBaseDigest) || !DIGEST.test(fields.patchDigest) || !DIGEST.test(fields.candidateDigest) || !DIGEST.test(fields.sourceDigest) || !DIGEST.test(fields.policyDigest) || !DIGEST.test(fields.changedTestSetDigest) || !Number.isSafeInteger(fields.changedTestCount) || fields.changedTestCount < 0 || fields.changedTestCount > 1e4 || fields.changedTestsRequireReview !== fields.changedTestCount > 0 || fields.recipes.length < 1 || fields.recipes.length > 64) {
5629
+ if (fields.schemaVersion !== 1 || typeof fields.verificationId !== "string" || !ID.test(fields.verificationId) || typeof fields.trustedBaseCommitSha !== "string" || !SHA.test(fields.trustedBaseCommitSha) || !DIGEST2.test(fields.trustedBaseDigest) || !DIGEST2.test(fields.patchDigest) || !DIGEST2.test(fields.candidateDigest) || !DIGEST2.test(fields.sourceDigest) || !DIGEST2.test(fields.policyDigest) || !DIGEST2.test(fields.changedTestSetDigest) || !Number.isSafeInteger(fields.changedTestCount) || fields.changedTestCount < 0 || fields.changedTestCount > 1e4 || fields.changedTestsRequireReview !== fields.changedTestCount > 0 || fields.recipes.length < 1 || fields.recipes.length > 64) {
5202
5630
  throw new CamelError("state_conflict", "Code verification receipt is malformed or outside its bounds.");
5203
5631
  }
5204
5632
  const ids = /* @__PURE__ */ new Set();
5205
5633
  for (const recipe2 of fields.recipes) {
5206
- if (typeof recipe2.recipeId !== "string" || !ID.test(recipe2.recipeId) || ids.has(recipe2.recipeId) || typeof recipe2.recipeDigest !== "string" || !DIGEST.test(recipe2.recipeDigest) || !["passed", "failed", "timed_out", "output_limited"].includes(recipe2.status) || !Number.isSafeInteger(recipe2.exitCode) || recipe2.exitCode < 0 || recipe2.exitCode > 255 || !Number.isSafeInteger(recipe2.durationMs) || recipe2.durationMs < 0 || recipe2.durationMs > 30 * 6e4) {
5634
+ if (typeof recipe2.recipeId !== "string" || !ID.test(recipe2.recipeId) || ids.has(recipe2.recipeId) || typeof recipe2.recipeDigest !== "string" || !DIGEST2.test(recipe2.recipeDigest) || !["passed", "failed", "timed_out", "output_limited"].includes(recipe2.status) || !Number.isSafeInteger(recipe2.exitCode) || recipe2.exitCode < 0 || recipe2.exitCode > 255 || !Number.isSafeInteger(recipe2.durationMs) || recipe2.durationMs < 0 || recipe2.durationMs > 30 * 6e4) {
5207
5635
  throw new CamelError("state_conflict", "Code verification recipe receipt is malformed or outside its bounds.");
5208
5636
  }
5209
5637
  const artifactIds = /* @__PURE__ */ new Set();
5210
5638
  if (recipe2.artifacts.length > 64) throw new CamelError("state_conflict", "Code verification has too many artifacts.");
5211
5639
  for (const artifact of recipe2.artifacts) {
5212
- if (typeof artifact.artifactId !== "string" || !ID.test(artifact.artifactId) || artifactIds.has(artifact.artifactId) || !["verified", "missing", "invalid", "too_large"].includes(artifact.status) || artifact.bytes !== null && (!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 0) || artifact.digest !== null && !DIGEST.test(artifact.digest) || artifact.status === "verified" !== (artifact.bytes !== null && artifact.digest !== null) || ["missing", "invalid"].includes(artifact.status) && (artifact.bytes !== null || artifact.digest !== null) || artifact.status === "too_large" && (artifact.bytes === null || artifact.digest !== null)) {
5640
+ if (typeof artifact.artifactId !== "string" || !ID.test(artifact.artifactId) || artifactIds.has(artifact.artifactId) || !["verified", "missing", "invalid", "too_large"].includes(artifact.status) || artifact.bytes !== null && (!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 0) || artifact.digest !== null && !DIGEST2.test(artifact.digest) || artifact.status === "verified" !== (artifact.bytes !== null && artifact.digest !== null) || ["missing", "invalid"].includes(artifact.status) && (artifact.bytes !== null || artifact.digest !== null) || artifact.status === "too_large" && (artifact.bytes === null || artifact.digest !== null)) {
5213
5641
  throw new CamelError("state_conflict", "Code verification artifact receipt is malformed.");
5214
5642
  }
5215
5643
  artifactIds.add(artifact.artifactId);
@@ -5225,7 +5653,7 @@ function validate(fields) {
5225
5653
  }
5226
5654
  }
5227
5655
  var SHA2 = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
5228
- var DIGEST2 = /^sha256:[0-9a-f]{64}$/;
5656
+ var DIGEST22 = /^sha256:[0-9a-f]{64}$/;
5229
5657
  var ID2 = /^[A-Za-z0-9._:-]{1,180}$/;
5230
5658
  var MAX_PATCH_BYTES = 256 * 1024;
5231
5659
  var MAX_STATE_BYTES = 64 * 1024;
@@ -5270,14 +5698,14 @@ function normalizeState(value2) {
5270
5698
  ]);
5271
5699
  const planCursor = state2.planCursor;
5272
5700
  const conversations = strings(state2.conversationRefs, "conversationRefs", ID2, 256, false);
5273
- const approvals = strings(state2.unresolvedApprovals, "unresolvedApprovals", DIGEST2, 256, true);
5274
- if (planCursor !== null && (typeof planCursor !== "string" || !ID2.test(planCursor)) || typeof state2.planningInputDigest !== "string" || !DIGEST2.test(state2.planningInputDigest) || typeof state2.buildPolicyDigest !== "string" || !DIGEST2.test(state2.buildPolicyDigest) || state2.dependencyLayerDigest !== null && (typeof state2.dependencyLayerDigest !== "string" || !DIGEST2.test(state2.dependencyLayerDigest)) || state2.verificationDigest !== null && (typeof state2.verificationDigest !== "string" || !DIGEST2.test(state2.verificationDigest)) || state2.reviewDigest !== null && (typeof state2.reviewDigest !== "string" || !DIGEST2.test(state2.reviewDigest)) || !["candidate_untrusted", "verified", "reviewed"].includes(String(state2.trustStatus)) || !Array.isArray(state2.completedEffects) || state2.completedEffects.length > 256) {
5701
+ const approvals = strings(state2.unresolvedApprovals, "unresolvedApprovals", DIGEST22, 256, true);
5702
+ if (planCursor !== null && (typeof planCursor !== "string" || !ID2.test(planCursor)) || typeof state2.planningInputDigest !== "string" || !DIGEST22.test(state2.planningInputDigest) || typeof state2.buildPolicyDigest !== "string" || !DIGEST22.test(state2.buildPolicyDigest) || state2.dependencyLayerDigest !== null && (typeof state2.dependencyLayerDigest !== "string" || !DIGEST22.test(state2.dependencyLayerDigest)) || state2.verificationDigest !== null && (typeof state2.verificationDigest !== "string" || !DIGEST22.test(state2.verificationDigest)) || state2.reviewDigest !== null && (typeof state2.reviewDigest !== "string" || !DIGEST22.test(state2.reviewDigest)) || !["candidate_untrusted", "verified", "reviewed"].includes(String(state2.trustStatus)) || !Array.isArray(state2.completedEffects) || state2.completedEffects.length > 256) {
5275
5703
  throw invalid2("Portable checkpoint state is malformed or outside its bounds.");
5276
5704
  }
5277
5705
  const effects = state2.completedEffects.map((item) => {
5278
5706
  const effect = object(item, "completed effect");
5279
5707
  exact2(effect, ["effectId", "actionDigest", "receiptDigest"]);
5280
- if (typeof effect.effectId !== "string" || !ID2.test(effect.effectId) || typeof effect.actionDigest !== "string" || !DIGEST2.test(effect.actionDigest) || typeof effect.receiptDigest !== "string" || !DIGEST2.test(effect.receiptDigest)) {
5708
+ if (typeof effect.effectId !== "string" || !ID2.test(effect.effectId) || typeof effect.actionDigest !== "string" || !DIGEST22.test(effect.actionDigest) || typeof effect.receiptDigest !== "string" || !DIGEST22.test(effect.receiptDigest)) {
5281
5709
  throw invalid2("Portable checkpoint effect receipt is malformed.");
5282
5710
  }
5283
5711
  return {
@@ -5508,8 +5936,8 @@ function boundedInteger(value2, spec) {
5508
5936
  }
5509
5937
  function boundedNumber(value2, spec) {
5510
5938
  if (spec.kind !== "finite_number" || typeof value2 !== "number" || !Number.isFinite(value2) || value2 < spec.minimum || value2 > spec.maximum) throw new CamelError("conversion_rejected", "Finite-number conversion rejected the structured value.");
5511
- const text = String(value2);
5512
- if (/e/i.test(text) || (text.split(".")[1]?.length ?? 0) > spec.maximumDecimalPlaces) throw new CamelError("conversion_rejected", "Finite-number conversion rejected a non-canonical decimal.");
5939
+ const text2 = String(value2);
5940
+ if (/e/i.test(text2) || (text2.split(".")[1]?.length ?? 0) > spec.maximumDecimalPlaces) throw new CamelError("conversion_rejected", "Finite-number conversion rejected a non-canonical decimal.");
5513
5941
  return value2;
5514
5942
  }
5515
5943
  function enumMember(value2, spec) {
@@ -5665,8 +6093,8 @@ function validateUnsafeSelector(path, value2, tool) {
5665
6093
  return void 0;
5666
6094
  }
5667
6095
  function looksLikeDestination(value2) {
5668
- const text = value2.trim();
5669
- return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text);
6096
+ const text2 = value2.trim();
6097
+ return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text2) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text2);
5670
6098
  }
5671
6099
 
5672
6100
  // ../harness/dist/chunk-GMVZ4LZH.js
@@ -5807,7 +6235,7 @@ function createCodeRuntimeControlClient(options) {
5807
6235
  }
5808
6236
  const value2 = await response2.json().catch(() => null);
5809
6237
  if (!response2.ok) {
5810
- const problem = record3(record3(value2)?.error);
6238
+ const problem = record5(record5(value2)?.error);
5811
6239
  throw new CodeRuntimeControlError(
5812
6240
  typeof problem?.message === "string" ? problem.message : `Code runtime request failed (${response2.status})`,
5813
6241
  response2.status,
@@ -5829,12 +6257,12 @@ function createCodeRuntimeControlClient(options) {
5829
6257
  await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
5830
6258
  ),
5831
6259
  infer: async (sessionId, inference) => {
5832
- const value2 = record3(await call2(
6260
+ const value2 = record5(await call2(
5833
6261
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/inference`,
5834
6262
  inference,
5835
6263
  modelRequestTimeoutMs
5836
6264
  ));
5837
- if (!value2 || value2.requestId !== inference.requestId || !record3(value2.response) || !record3(value2.receipt)) {
6265
+ if (!value2 || value2.requestId !== inference.requestId || !record5(value2.response) || !record5(value2.receipt)) {
5838
6266
  throw new CodeRuntimeControlError("invalid Code inference response", 502, "invalid_response");
5839
6267
  }
5840
6268
  return value2;
@@ -5892,12 +6320,12 @@ function validateHeartbeat(version, capabilities) {
5892
6320
  }
5893
6321
  }
5894
6322
  function parseSnapshot(value2) {
5895
- const root = record3(value2);
5896
- const host = record3(root?.host);
6323
+ const root = record5(value2);
6324
+ const host = record5(root?.host);
5897
6325
  if (!host || typeof host.hostId !== "string" || typeof host.runtimeVersion !== "string" || !Number.isSafeInteger(host.lastSeenAt) || host.revokedAt !== null || !Array.isArray(root?.bindings) || root.bindings.length > 1024 || !Array.isArray(root?.commands) || root.commands.length > 64) throw invalid("heartbeat");
5898
6326
  const bindingIds = /* @__PURE__ */ new Set();
5899
6327
  const bindings = root.bindings.map((item) => {
5900
- const binding = record3(item);
6328
+ const binding = record5(item);
5901
6329
  if (!binding || typeof binding.bindingId !== "string" || typeof binding.appId !== "string" || binding.env !== "dev" && binding.env !== "prod" || typeof binding.offerId !== "string" || binding.hostId !== host.hostId || !Number.isSafeInteger(binding.generation) || Number(binding.generation) < 1 || binding.revokedAt !== null || bindingIds.has(binding.bindingId)) {
5902
6330
  throw invalid("binding");
5903
6331
  }
@@ -5907,10 +6335,10 @@ function parseSnapshot(value2) {
5907
6335
  const commandIds = /* @__PURE__ */ new Set();
5908
6336
  const commandSequences = /* @__PURE__ */ new Set();
5909
6337
  const commands = root.commands.map((item) => {
5910
- const command = record3(item);
6338
+ const command = record5(item);
5911
6339
  const binding = bindings.find((candidate) => candidate.bindingId === command?.bindingId);
5912
6340
  const sequenceKey = `${String(command?.instanceId)}:${String(command?.sequence)}`;
5913
- if (!command || typeof command.commandId !== "string" || !/^ccmd_[0-9a-f]{32}$/.test(command.commandId) || typeof command.instanceId !== "string" || typeof command.sessionId !== "string" || !/^csess_[0-9a-f]{32}$/.test(command.sessionId) || typeof command.appId !== "string" || command.env !== "dev" && command.env !== "prod" || command.hostId !== host.hostId || !binding || binding.appId !== command.appId || binding.generation !== command.bindingGeneration || !Number.isSafeInteger(command.sequence) || Number(command.sequence) < 1 || commandIds.has(command.commandId) || commandSequences.has(sequenceKey) || !["start", "prompt", "checkpoint_stop", "resume"].includes(String(command.kind)) || !record3(command.payload) || !Number.isSafeInteger(command.createdAt)) throw invalid("command");
6341
+ if (!command || typeof command.commandId !== "string" || !/^ccmd_[0-9a-f]{32}$/.test(command.commandId) || typeof command.instanceId !== "string" || typeof command.sessionId !== "string" || !/^csess_[0-9a-f]{32}$/.test(command.sessionId) || typeof command.appId !== "string" || command.env !== "dev" && command.env !== "prod" || command.hostId !== host.hostId || !binding || binding.appId !== command.appId || binding.generation !== command.bindingGeneration || !Number.isSafeInteger(command.sequence) || Number(command.sequence) < 1 || commandIds.has(command.commandId) || commandSequences.has(sequenceKey) || !["start", "prompt", "checkpoint_stop", "resume"].includes(String(command.kind)) || !record5(command.payload) || !Number.isSafeInteger(command.createdAt)) throw invalid("command");
5914
6342
  commandIds.add(command.commandId);
5915
6343
  commandSequences.add(sequenceKey);
5916
6344
  return command;
@@ -5918,10 +6346,10 @@ function parseSnapshot(value2) {
5918
6346
  return { host, bindings, commands };
5919
6347
  }
5920
6348
  async function parseSource(value2) {
5921
- const snapshot = record3(record3(value2)?.snapshot);
6349
+ const snapshot = record5(record5(value2)?.snapshot);
5922
6350
  if (!snapshot || typeof snapshot.repository !== "string" || typeof snapshot.commitSha !== "string" || typeof snapshot.treeDigest !== "string" || !Array.isArray(snapshot.files)) throw invalid("source");
5923
6351
  const files = snapshot.files.map((value22) => {
5924
- const file = record3(value22);
6352
+ const file = record5(value22);
5925
6353
  if (!file || typeof file.path !== "string" || typeof file.content !== "string") throw invalid("source file");
5926
6354
  return { path: file.path, content: file.content };
5927
6355
  });
@@ -5930,11 +6358,11 @@ async function parseSource(value2) {
5930
6358
  const aliases = /* @__PURE__ */ new Set();
5931
6359
  const references = [];
5932
6360
  for (const item of referencesValue) {
5933
- const reference = record3(item);
6361
+ const reference = record5(item);
5934
6362
  if (!reference || typeof reference.alias !== "string" || !/^[a-z][a-z0-9-]{0,39}$/.test(reference.alias) || aliases.has(reference.alias) || reference.alias === "primary" || typeof reference.repository !== "string" || typeof reference.commitSha !== "string" || typeof reference.treeDigest !== "string" || !Array.isArray(reference.files)) throw invalid("reference source");
5935
6363
  aliases.add(reference.alias);
5936
6364
  const referenceFiles = reference.files.map((entry) => {
5937
- const file = record3(entry);
6365
+ const file = record5(entry);
5938
6366
  if (!file || typeof file.path !== "string" || typeof file.content !== "string") throw invalid("reference source file");
5939
6367
  return { path: file.path, content: file.content };
5940
6368
  });
@@ -5949,18 +6377,18 @@ async function parseSource(value2) {
5949
6377
  return { ...source, treeDigest: digest, ...references.length ? { references } : {} };
5950
6378
  }
5951
6379
  function parseReview(value2) {
5952
- const review = record3(record3(value2)?.review);
6380
+ const review = record5(record5(value2)?.review);
5953
6381
  if (!review || !["approved", "rejected"].includes(String(review.verdict)) || typeof review.reviewDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(review.reviewDigest) || typeof review.provider !== "string" || !review.provider || typeof review.model !== "string" || !review.model || !Number.isSafeInteger(review.policyVersion) || Number(review.policyVersion) < 1) throw invalid("review");
5954
6382
  return review;
5955
6383
  }
5956
6384
  function parseCandidate(value2) {
5957
- const candidate = record3(record3(value2)?.candidate);
6385
+ const candidate = record5(record5(value2)?.candidate);
5958
6386
  if (!candidate || typeof candidate.candidateId !== "string" || !/^ccand_[0-9a-f]{32}$/.test(candidate.candidateId) || !["submitted", "approved", "published", "failed"].includes(String(candidate.status))) {
5959
6387
  throw invalid("candidate");
5960
6388
  }
5961
6389
  return { candidateId: candidate.candidateId, status: candidate.status };
5962
6390
  }
5963
- var record3 = (value2) => value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
6391
+ var record5 = (value2) => value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
5964
6392
  var invalid = (part) => new CodeRuntimeControlError(`invalid Code runtime ${part} response`, 502, "invalid_response");
5965
6393
  var RESERVED = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
5966
6394
  var SECRET = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
@@ -6035,8 +6463,8 @@ function gitApply(cwd, patch2, check) {
6035
6463
  });
6036
6464
  let stderr = "";
6037
6465
  child.stderr.setEncoding("utf8");
6038
- child.stderr.on("data", (text) => {
6039
- if (stderr.length < 4e3) stderr += text.slice(0, 4e3);
6466
+ child.stderr.on("data", (text2) => {
6467
+ if (stderr.length < 4e3) stderr += text2.slice(0, 4e3);
6040
6468
  });
6041
6469
  child.once("error", reject);
6042
6470
  child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(`patch did not apply: ${stderr.trim().slice(0, 500)}`)));
@@ -7207,10 +7635,10 @@ var CodePiRuntimeEngine = class {
7207
7635
  task: lease.task,
7208
7636
  limits: this.options.limits,
7209
7637
  signal: active.abort.signal,
7210
- onStderr: (text) => this.#event(command, {
7638
+ onStderr: (text2) => this.#event(command, {
7211
7639
  type: "message",
7212
7640
  actor: "system",
7213
- body: text.slice(0, 4e3)
7641
+ body: text2.slice(0, 4e3)
7214
7642
  }, active.conversationRefs),
7215
7643
  onMessage: async (output) => {
7216
7644
  if (output.type === "inference.request") {
@@ -7323,13 +7751,6 @@ var CodePiRuntimeEngine = class {
7323
7751
  }
7324
7752
  };
7325
7753
 
7326
- // src/version.ts
7327
- var import_node_fs13 = require("fs");
7328
- function cliVersion() {
7329
- const pkg = JSON.parse((0, import_node_fs13.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
7330
- return pkg.version ?? "unknown";
7331
- }
7332
-
7333
7754
  // src/security-hosted-github.ts
7334
7755
  var import_node_child_process4 = require("child_process");
7335
7756
  var import_node_util2 = require("util");
@@ -7605,7 +8026,7 @@ var import_node_child_process6 = require("child_process");
7605
8026
  var import_node_crypto3 = require("crypto");
7606
8027
  var import_promises10 = require("fs/promises");
7607
8028
  var import_node_os3 = require("os");
7608
- var import_node_path12 = require("path");
8029
+ var import_node_path13 = require("path");
7609
8030
  var import_node_url3 = require("url");
7610
8031
 
7611
8032
  // src/code-runtime-config.ts
@@ -7691,10 +8112,10 @@ async function embeddedPiImageName() {
7691
8112
  return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto3.createHash)("sha256").update(bundle).digest("hex")}`;
7692
8113
  }
7693
8114
  async function buildEmbeddedPiImage(engine, image, run) {
7694
- const context = await (0, import_promises10.mkdtemp)((0, import_node_path12.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
8115
+ const context = await (0, import_promises10.mkdtemp)((0, import_node_path13.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
7695
8116
  try {
7696
- await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0, import_node_path12.join)(context, "pi-agent.js"));
7697
- await (0, import_promises10.writeFile)((0, import_node_path12.join)(context, "Dockerfile"), [
8117
+ await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0, import_node_path13.join)(context, "pi-agent.js"));
8118
+ await (0, import_promises10.writeFile)((0, import_node_path13.join)(context, "Dockerfile"), [
7698
8119
  `FROM ${CODE_NODE_IMAGE}`,
7699
8120
  "COPY pi-agent.js /opt/odla/pi-agent.js",
7700
8121
  "WORKDIR /workspace",
@@ -7710,8 +8131,8 @@ async function buildEmbeddedPiImage(engine, image, run) {
7710
8131
  // src/code-connect.ts
7711
8132
  async function codeConnect(options) {
7712
8133
  const cwd = options.cwd ?? process.cwd();
7713
- const configPath = (0, import_node_path13.resolve)(cwd, options.configPath);
7714
- const cfg = (0, import_node_fs14.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
8134
+ const configPath = (0, import_node_path14.resolve)(cwd, options.configPath);
8135
+ const cfg = (0, import_node_fs15.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
7715
8136
  const requestedAppId = options.appId?.trim();
7716
8137
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
7717
8138
  throw new Error("--app-id must be a valid odla app id");
@@ -7869,20 +8290,20 @@ async function runCodeRuntime(input) {
7869
8290
  }
7870
8291
  }
7871
8292
  function parseConnection(value2, appId, appEnv) {
7872
- const root = record4(value2);
7873
- const host = record4(root?.host);
7874
- const offer = record4(root?.offer);
7875
- const binding = record4(root?.binding);
8293
+ const root = record6(value2);
8294
+ const host = record6(root?.host);
8295
+ const offer = record6(root?.offer);
8296
+ const binding = record6(root?.binding);
7876
8297
  if (!root || typeof root.token !== "string" || !/^odla_code_host_[0-9a-f]{64}$/.test(root.token) || typeof root.resumed !== "boolean" || !host || !/^chost_[0-9a-f]{32}$/.test(String(host.hostId)) || typeof host.name !== "string" || !offer || !Number.isSafeInteger(offer.slots) || !binding || typeof binding.appId !== "string" || !binding.appId || appId && binding.appId !== appId || binding.env !== appEnv || !Number.isSafeInteger(binding.generation)) {
7877
8298
  throw new Error("connect Code host returned an invalid response");
7878
8299
  }
7879
8300
  return root;
7880
8301
  }
7881
8302
  function apiFailure(action2, status, value2) {
7882
- const message2 = record4(record4(value2)?.error)?.message;
8303
+ const message2 = record6(record6(value2)?.error)?.message;
7883
8304
  return `${action2} failed (${status})${typeof message2 === "string" ? `: ${message2}` : ""}`;
7884
8305
  }
7885
- function record4(value2) {
8306
+ function record6(value2) {
7886
8307
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
7887
8308
  }
7888
8309
 
@@ -8097,8 +8518,10 @@ Usage:
8097
8518
  odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
8098
8519
  odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
8099
8520
  odla-ai doctor [--config odla.config.mjs]
8100
- odla-ai config diff [--config odla.config.mjs] [--email <odla-account>] [--json]
8101
- odla-ai config plan [--config odla.config.mjs] [--email <odla-account>] [--json]
8521
+ odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
8522
+ odla-ai config apply --plan <plan.json> [--idempotency-key <key>] [--email <odla-account>] [--json]
8523
+ odla-ai operations get <operation-id> [--json]
8524
+ odla-ai operations wait <operation-id> [--interval <seconds>] [--timeout <seconds>] [--json]
8102
8525
  odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
8103
8526
  odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
8104
8527
  odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
@@ -8149,6 +8572,7 @@ Usage:
8149
8572
  odla-ai context remove <name> --yes [--json]
8150
8573
  odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
8151
8574
  odla-ai platform status [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
8575
+ odla-ai platform chat-credentials rotate [--context <name>] [--email <odla-account>] [--wrangler-config <path>] [--expected-version <id>] [--json] --yes
8152
8576
  odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
8153
8577
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
8154
8578
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
@@ -8212,9 +8636,9 @@ Commands:
8212
8636
  does and what it guarantees is JSDoc, rendered per package at
8213
8637
  https://odla.ai/docs and shipped in the installed .d.ts. Answering
8214
8638
  a question usually needs both.
8215
- whoami Report who this terminal is authenticated as, and whether it holds
8216
- platform admin. A handshake-minted device token is never admin,
8217
- however the human who approved it is configured.
8639
+ whoami Report the actual principal name, handle and kind, its manager,
8640
+ credential kind, accountable owner, and platform-admin status.
8641
+ A manager relationship never supplies authority by itself.
8218
8642
  context Explain selected config, platform, app, environment, and
8219
8643
  developer-token provenance without printing credentials or
8220
8644
  starting a device handshake. Operator work can run outside a
@@ -8223,8 +8647,8 @@ Commands:
8223
8647
  setup Install offline odla runbooks for common coding-agent harnesses.
8224
8648
  init Create a generic odla.config.mjs plus starter schema/rules files.
8225
8649
  doctor Validate and summarize the project config without network calls.
8226
- config Read-only diff and revision-bound plan for checked-in Registry
8227
- intent; runtime-owned fields and excluded coverage stay explicit.
8650
+ config Diff Registry intent, freeze a CAS-bound plan, and conditionally apply its safe actions.
8651
+ operations Inspect or wait on one exact, durable config-operation receipt.
8228
8652
  calendar Inspect, connect, or disconnect the live Google booking connection.
8229
8653
  app Archive (suspend, data retained), restore, export, import, or
8230
8654
  manage the co-owners of the app. Archiving takes every
@@ -8334,6 +8758,58 @@ Safety:
8334
8758
  `);
8335
8759
  }
8336
8760
 
8761
+ // src/discuss-principals.ts
8762
+ function mergeDiscussPrincipals(target, source) {
8763
+ Object.assign(target.authors, source.authors ?? {});
8764
+ Object.assign(target.principals, source.principals ?? {});
8765
+ }
8766
+ var withAt = (handle) => handle.startsWith("@") ? handle : `@${handle}`;
8767
+ function discussPrincipalLabel(principalId, authorKind, projection) {
8768
+ const profile = projection.principals?.[principalId];
8769
+ if (profile) {
8770
+ const identity = `${profile.displayName} (${withAt(profile.handle)})`;
8771
+ if (profile.kind === "agent") {
8772
+ return profile.managerDisplayName ? `${identity} \u2014 agent managed by ${profile.managerDisplayName}` : `${identity} \u2014 agent`;
8773
+ }
8774
+ return profile.kind === "service" ? `${identity} \u2014 service` : identity;
8775
+ }
8776
+ const legacyName = projection.authors?.[principalId]?.trim();
8777
+ if (legacyName) {
8778
+ return authorKind === "bot" ? `${legacyName} \u2014 agent` : legacyName;
8779
+ }
8780
+ return authorKind === "bot" ? "Unnamed agent" : "Unnamed member";
8781
+ }
8782
+
8783
+ // src/discuss-read-render.ts
8784
+ function discussBodyWithRefs(post) {
8785
+ if (!post.refs || post.refs.length === 0) return post.body;
8786
+ let out = "";
8787
+ let cursor = 0;
8788
+ for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
8789
+ if (ref.start < cursor || ref.end > post.body.length) continue;
8790
+ out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
8791
+ cursor = ref.end;
8792
+ }
8793
+ return out + post.body.slice(cursor);
8794
+ }
8795
+ function renderDiscussRead(ctx, topic, posts, projection) {
8796
+ const status = topic.resolved ? "resolved" : "open";
8797
+ ctx.out.log(`${topic.subject} [${status}] ${topic.appId ?? ""}`);
8798
+ for (const post of posts) {
8799
+ const who = discussPrincipalLabel(
8800
+ post.authorId,
8801
+ post.authorKind,
8802
+ projection
8803
+ );
8804
+ ctx.out.log(`
8805
+ \u2014 ${who}`);
8806
+ ctx.out.log(discussBodyWithRefs(post));
8807
+ for (const file of post.attachments ?? []) {
8808
+ ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
8809
+ }
8810
+ }
8811
+ }
8812
+
8337
8813
  // src/discuss-actions.ts
8338
8814
  var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
8339
8815
  async function request(ctx, method, path, body) {
@@ -8352,17 +8828,6 @@ function emit(ctx, value2, human) {
8352
8828
  else human();
8353
8829
  }
8354
8830
  var state = (topic) => topic.resolved ? "resolved" : "open";
8355
- function bodyWithRefs(post) {
8356
- if (!post.refs || post.refs.length === 0) return post.body;
8357
- let out = "";
8358
- let cursor = 0;
8359
- for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
8360
- if (ref.start < cursor || ref.end > post.body.length) continue;
8361
- out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
8362
- cursor = ref.end;
8363
- }
8364
- return out + post.body.slice(cursor);
8365
- }
8366
8831
  function content(parsed) {
8367
8832
  const markup = stringOpt(parsed.options.markup);
8368
8833
  if (markup) return { markup };
@@ -8417,11 +8882,19 @@ async function discussRead(ctx, id, parsed) {
8417
8882
  offset: requestedOffset ?? "0"
8418
8883
  });
8419
8884
  const page = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
8420
- emit(ctx, page, () => renderRead(ctx, page.topic, page.posts));
8885
+ emit(
8886
+ ctx,
8887
+ page,
8888
+ () => renderDiscussRead(ctx, page.topic, page.posts, page)
8889
+ );
8421
8890
  return;
8422
8891
  }
8423
8892
  for (let scan = 0; scan < 3; scan++) {
8424
8893
  const posts = /* @__PURE__ */ new Map();
8894
+ const projection = {
8895
+ authors: {},
8896
+ principals: {}
8897
+ };
8425
8898
  let topic = null;
8426
8899
  let offset = 0;
8427
8900
  for (; ; ) {
@@ -8432,6 +8905,7 @@ async function discussRead(ctx, id, parsed) {
8432
8905
  );
8433
8906
  topic = page.topic;
8434
8907
  for (const post of page.posts) posts.set(post.id, post);
8908
+ mergeDiscussPrincipals(projection, page);
8435
8909
  if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
8436
8910
  if (!page.page?.hasMore) break;
8437
8911
  if (page.page.nextOffset === null || page.page.nextOffset <= offset) {
@@ -8444,24 +8918,18 @@ async function discussRead(ctx, id, parsed) {
8444
8918
  );
8445
8919
  const expected = Number.isInteger(topic.replyCount) ? topic.replyCount + 1 : ordered.length;
8446
8920
  if (ordered.length === expected) {
8447
- const result = { topic, posts: ordered };
8448
- emit(ctx, result, () => renderRead(ctx, result.topic, result.posts));
8921
+ const result = { topic, posts: ordered, ...projection };
8922
+ emit(
8923
+ ctx,
8924
+ result,
8925
+ () => renderDiscussRead(ctx, result.topic, result.posts, result)
8926
+ );
8449
8927
  return;
8450
8928
  }
8451
8929
  if (offset === 0) throw new Error("discuss read failed: registry did not provide forward post pages");
8452
8930
  }
8453
8931
  throw new Error("discuss read failed: conversation changed during every complete-read attempt");
8454
8932
  }
8455
- function renderRead(ctx, topic, posts) {
8456
- ctx.out.log(`${topic.subject} [${state(topic)}] ${topic.appId ?? ""}`);
8457
- for (const post of posts) {
8458
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
8459
- ctx.out.log(`
8460
- \u2014 ${who}`);
8461
- ctx.out.log(bodyWithRefs(post));
8462
- for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
8463
- }
8464
- }
8465
8933
  async function discussPost(ctx, parsed) {
8466
8934
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
8467
8935
  if (!appId) throw new Error("discuss post needs --app <appId>");
@@ -8686,6 +9154,8 @@ async function discussWatch(ctx, topicId, parsed) {
8686
9154
  found: true,
8687
9155
  cursor,
8688
9156
  events: matching,
9157
+ ...page.authors ? { authors: page.authors } : {},
9158
+ ...page.principals ? { principals: page.principals } : {},
8689
9159
  ...posts && posts.length > 0 ? { posts } : {},
8690
9160
  ...topics && topics.length > 0 ? { topics } : {}
8691
9161
  });
@@ -8718,7 +9188,11 @@ function report2(ctx, parsed, result) {
8718
9188
  ctx.out.log(JSON.stringify(result, null, 2));
8719
9189
  } else if (parsed.options.jsonl !== true && result.found) {
8720
9190
  for (const post of result.posts ?? []) {
8721
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
9191
+ const who = discussPrincipalLabel(
9192
+ post.authorId,
9193
+ post.authorKind,
9194
+ result
9195
+ );
8722
9196
  ctx.out.log(`\u2014 ${who}
8723
9197
  ${post.body}`);
8724
9198
  }
@@ -8863,8 +9337,8 @@ function collectFields(parsed, allowClear) {
8863
9337
  if (allowClear) out[spec.key] = null;
8864
9338
  continue;
8865
9339
  }
8866
- const text = stringOpt(value2);
8867
- out[spec.key] = spec.num ? Number(text) : text;
9340
+ const text2 = stringOpt(value2);
9341
+ out[spec.key] = spec.num ? Number(text2) : text2;
8868
9342
  }
8869
9343
  return out;
8870
9344
  }
@@ -8931,8 +9405,8 @@ async function pmAdd(ctx, entity, parsed) {
8931
9405
  emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
8932
9406
  }
8933
9407
  async function pmGet(ctx, entity, id) {
8934
- const { record: record8 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
8935
- emit2(ctx, record8, () => printRecord(ctx, entity, record8));
9408
+ const { record: record11 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
9409
+ emit2(ctx, record11, () => printRecord(ctx, entity, record11));
8936
9410
  }
8937
9411
  async function pmSet(ctx, entity, id, parsed) {
8938
9412
  const patch2 = collectEntityFields(entity, parsed, true);
@@ -8977,9 +9451,9 @@ async function pmHandoff(ctx, parsed) {
8977
9451
  ]);
8978
9452
  const handoff = {
8979
9453
  appId,
8980
- unmetGoals: goals.filter((record8) => record8.status !== "met"),
8981
- activeTasks: tasks.filter((record8) => record8.column !== "done"),
8982
- openBugs: bugs.filter((record8) => record8.status !== "fixed" && record8.status !== "wontfix")
9454
+ unmetGoals: goals.filter((record11) => record11.status !== "met"),
9455
+ activeTasks: tasks.filter((record11) => record11.column !== "done"),
9456
+ openBugs: bugs.filter((record11) => record11.status !== "fixed" && record11.status !== "wontfix")
8983
9457
  };
8984
9458
  const result = {
8985
9459
  ...handoff,
@@ -8998,10 +9472,10 @@ async function pmHandoff(ctx, parsed) {
8998
9472
  ]) {
8999
9473
  ctx.out.log(`${label}:`);
9000
9474
  if (!records.length) ctx.out.log("- (none)");
9001
- else for (const record8 of records) printRecord(
9475
+ else for (const record11 of records) printRecord(
9002
9476
  ctx,
9003
9477
  label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
9004
- record8
9478
+ record11
9005
9479
  );
9006
9480
  }
9007
9481
  });
@@ -9192,19 +9666,180 @@ function age(input) {
9192
9666
  return `${Math.round(input / 6e4)}m`;
9193
9667
  }
9194
9668
 
9669
+ // src/platform-chat-credential-command.ts
9670
+ var import_node_process10 = __toESM(require("process"), 1);
9671
+ async function rotatePlatformChatCredential(parsed, deps) {
9672
+ assertArgs(
9673
+ parsed,
9674
+ [
9675
+ "config",
9676
+ "context",
9677
+ "platform",
9678
+ "token",
9679
+ "email",
9680
+ "open",
9681
+ "json",
9682
+ "yes",
9683
+ "wrangler-config",
9684
+ "expected-version"
9685
+ ],
9686
+ 3
9687
+ );
9688
+ if (parsed.options.yes !== true) {
9689
+ throw new Error(
9690
+ "platform chat-credentials rotate changes the production chat secret; pass --yes"
9691
+ );
9692
+ }
9693
+ const context = await resolveOperatorContext(parsed, {
9694
+ allowMissingConfig: true
9695
+ });
9696
+ const platform = context.platform.value;
9697
+ const doFetch = deps.fetch ?? fetch;
9698
+ const out = deps.stdout ?? console;
9699
+ const run = deps.runner ?? defaultRunner;
9700
+ const cwd = import_node_process10.default.cwd();
9701
+ const wranglerConfig = stringOpt(parsed.options["wrangler-config"]) ?? "packages/chat-agent/wrangler.jsonc";
9702
+ if (!await wranglerLoggedIn(run, cwd)) {
9703
+ throw new Error(
9704
+ 'Wrangler is not authenticated; run "npx wrangler login" and retry'
9705
+ );
9706
+ }
9707
+ const priorHealth = await doFetch(`${platform}/health/services/chat`);
9708
+ const priorVersion = priorHealth.headers.get("x-odla-worker-version-id");
9709
+ await priorHealth.body?.cancel().catch(() => {
9710
+ });
9711
+ if (!priorVersion) {
9712
+ throw new Error(
9713
+ "chat health did not identify its current Worker version; refusing to rotate"
9714
+ );
9715
+ }
9716
+ const expectedVersion = stringOpt(parsed.options["expected-version"]);
9717
+ if (expectedVersion && priorVersion !== expectedVersion) {
9718
+ throw new Error(
9719
+ `chat health answered from version ${priorVersion}; expected ${expectedVersion}`
9720
+ );
9721
+ }
9722
+ const token = await resolveAdminPlatformToken({
9723
+ platform,
9724
+ scope: "platform:chat:credential:write",
9725
+ token: stringOpt(parsed.options.token),
9726
+ tokenFile: context.credentials.scopedTokenFile,
9727
+ email: stringOpt(parsed.options.email),
9728
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
9729
+ fetch: doFetch,
9730
+ stdout: out,
9731
+ openApprovalUrl: deps.openUrl,
9732
+ label: "odla CLI (rotate built-in Discussion responder)"
9733
+ });
9734
+ const mintedResponse = await doFetch(
9735
+ `${platform}/registry/platform/chat-credentials/rotate`,
9736
+ {
9737
+ method: "POST",
9738
+ headers: { authorization: `Bearer ${token}` }
9739
+ }
9740
+ );
9741
+ const minted = await mintedResponse.json().catch(() => null);
9742
+ if (!mintedResponse.ok) {
9743
+ throw new Error(
9744
+ `mint Discussion credential failed (HTTP ${mintedResponse.status}): ${apiMessage(minted)}`
9745
+ );
9746
+ }
9747
+ if (!isPlatformChatCredential(minted)) {
9748
+ throw new Error(
9749
+ "platform returned an invalid odla.platform-chat-credential/v1 envelope"
9750
+ );
9751
+ }
9752
+ const put = await wranglerPutSecret(run, {
9753
+ name: "ODLA_BOT_TOKENS",
9754
+ value: JSON.stringify({
9755
+ [minted.appId]: { [minted.principalId]: minted.key }
9756
+ }),
9757
+ configPath: wranglerConfig,
9758
+ cwd
9759
+ });
9760
+ if (put.code !== 0) {
9761
+ throw new Error(
9762
+ "Wrangler could not replace ODLA_BOT_TOKENS; the new scoped key was not activated"
9763
+ );
9764
+ }
9765
+ const version = await waitForRotatedChatHealth({
9766
+ fetch: doFetch,
9767
+ platform,
9768
+ priorVersion,
9769
+ wait: deps.pollWait
9770
+ });
9771
+ const result = {
9772
+ schemaVersion: "odla.platform-chat-credential-rotation/v1",
9773
+ appId: minted.appId,
9774
+ appIncarnation: minted.appIncarnation,
9775
+ principalId: minted.principalId,
9776
+ health: { ok: true, service: "odla-chat-agent", version }
9777
+ };
9778
+ if (parsed.options.json === true) {
9779
+ out.log(JSON.stringify(result, null, 2));
9780
+ } else {
9781
+ out.log(
9782
+ `rotated ${result.appId} ${result.principalId} ${result.health.version}`
9783
+ );
9784
+ }
9785
+ }
9786
+ async function waitForRotatedChatHealth(opts) {
9787
+ const wait2 = opts.wait ?? ((milliseconds) => new Promise((resolve12) => setTimeout(resolve12, milliseconds)));
9788
+ let lastStatus = 0;
9789
+ let lastVersion = null;
9790
+ let lastError = "private_service_unready";
9791
+ for (let attempt = 0; attempt < 60; attempt++) {
9792
+ const response2 = await opts.fetch(
9793
+ `${opts.platform}/health/services/chat`
9794
+ );
9795
+ const body = await response2.json().catch(() => null);
9796
+ const version = response2.headers.get("x-odla-worker-version-id");
9797
+ if (response2.ok && record7(body) && body.ok === true && body.service === "odla-chat-agent" && version && version !== opts.priorVersion) {
9798
+ return version;
9799
+ }
9800
+ lastStatus = response2.status;
9801
+ lastVersion = version;
9802
+ lastError = record7(body) && typeof body.error === "string" ? body.error : "private_service_unready";
9803
+ if (attempt < 59) await wait2(5e3);
9804
+ }
9805
+ throw new Error(
9806
+ `chat health did not converge on the rotated Worker deployment (HTTP ${lastStatus}, version ${lastVersion ?? "unknown"}, ${lastError})`
9807
+ );
9808
+ }
9809
+ function isPlatformChatCredential(value2) {
9810
+ return record7(value2) && value2.schemaVersion === "odla.platform-chat-credential/v1" && value2.appId === "odla-pm" && typeof value2.appIncarnation === "string" && value2.appIncarnation.length > 0 && value2.principalId === "agent_odla" && typeof value2.key === "string" && value2.key.startsWith("odla_sk_");
9811
+ }
9812
+ function apiMessage(value2) {
9813
+ if (!record7(value2)) return "request failed";
9814
+ const error = record7(value2.error) ? value2.error : value2;
9815
+ return typeof error.message === "string" ? error.message : typeof error.code === "string" ? error.code : "request failed";
9816
+ }
9817
+ function record7(value2) {
9818
+ return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
9819
+ }
9820
+
9195
9821
  // src/platform-command.ts
9196
9822
  async function platformCommand(parsed, deps = {}) {
9823
+ const action2 = parsed.positionals[1];
9824
+ if (action2 === "status") {
9825
+ return platformStatus(parsed, deps);
9826
+ }
9827
+ if (action2 === "chat-credentials" && parsed.positionals[2] === "rotate") {
9828
+ return rotatePlatformChatCredential(parsed, deps);
9829
+ }
9830
+ throw new Error(
9831
+ `unknown platform action "${[
9832
+ action2,
9833
+ parsed.positionals[2]
9834
+ ].filter(Boolean).join(" ")}". Try "odla-ai platform status --json".`
9835
+ );
9836
+ }
9837
+ async function platformStatus(parsed, deps) {
9197
9838
  assertArgs(
9198
9839
  parsed,
9199
9840
  ["config", "context", "platform", "token", "email", "open", "json"],
9200
9841
  2
9201
9842
  );
9202
- const action2 = parsed.positionals[1];
9203
- if (action2 !== "status") {
9204
- throw new Error(
9205
- `unknown platform action "${action2 ?? ""}". Try "odla-ai platform status --json".`
9206
- );
9207
- }
9208
9843
  const context = await resolveOperatorContext(parsed, {
9209
9844
  allowMissingConfig: true
9210
9845
  });
@@ -9229,7 +9864,7 @@ async function platformCommand(parsed, deps = {}) {
9229
9864
  const body = await response2.json().catch(() => null);
9230
9865
  if (!response2.ok) {
9231
9866
  throw new Error(
9232
- `read platform status failed (HTTP ${response2.status}): ${apiMessage(body)}`
9867
+ `read platform status failed (HTTP ${response2.status}): ${apiMessage2(body)}`
9233
9868
  );
9234
9869
  }
9235
9870
  if (!isPlatformStatus(body)) {
@@ -9242,17 +9877,17 @@ async function platformCommand(parsed, deps = {}) {
9242
9877
  }
9243
9878
  }
9244
9879
  function isPlatformStatus(value2) {
9245
- if (!record5(value2) || value2.schemaVersion !== "odla.platform-status/v1") return false;
9246
- if (!record5(value2.verdict) || !Array.isArray(value2.verdict.reasons)) return false;
9247
- if (!record5(value2.catalog) || !record5(value2.summary)) return false;
9880
+ if (!record8(value2) || value2.schemaVersion !== "odla.platform-status/v1") return false;
9881
+ if (!record8(value2.verdict) || !Array.isArray(value2.verdict.reasons)) return false;
9882
+ if (!record8(value2.catalog) || !record8(value2.summary)) return false;
9248
9883
  return Array.isArray(value2.services) && Array.isArray(value2.nextActions);
9249
9884
  }
9250
- function apiMessage(value2) {
9251
- if (!record5(value2)) return "request failed";
9252
- const error = record5(value2.error) ? value2.error : value2;
9885
+ function apiMessage2(value2) {
9886
+ if (!record8(value2)) return "request failed";
9887
+ const error = record8(value2.error) ? value2.error : value2;
9253
9888
  return typeof error.message === "string" ? error.message : typeof error.code === "string" ? error.code : "request failed";
9254
9889
  }
9255
- function record5(value2) {
9890
+ function record8(value2) {
9256
9891
  return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
9257
9892
  }
9258
9893
 
@@ -9293,7 +9928,7 @@ function statusVerdict(reads) {
9293
9928
  severity: "degraded"
9294
9929
  });
9295
9930
  }
9296
- const performance = record6(reads.liveSync.body.performance) ? reads.liveSync.body.performance : null;
9931
+ const performance = record9(reads.liveSync.body.performance) ? reads.liveSync.body.performance : null;
9297
9932
  if (performance?.status === "unavailable") {
9298
9933
  reasons.push({
9299
9934
  source: "liveSync",
@@ -9374,7 +10009,7 @@ function statusVerdict(reads) {
9374
10009
  reasons
9375
10010
  };
9376
10011
  }
9377
- function record6(value2) {
10012
+ function record9(value2) {
9378
10013
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
9379
10014
  }
9380
10015
  function numeric2(value2) {
@@ -9402,7 +10037,7 @@ function printO11yStatus(status, out) {
9402
10037
  out.log(
9403
10038
  `o11y status ${status.scope.appId}/${status.scope.env} (${status.scope.minutes}m)`
9404
10039
  );
9405
- const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record7) : [];
10040
+ const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record10) : [];
9406
10041
  const requests = routes.reduce(
9407
10042
  (total, row) => total + numeric3(row.requests),
9408
10043
  0
@@ -9414,39 +10049,39 @@ function printO11yStatus(status, out) {
9414
10049
  out.log(
9415
10050
  `application ${status.application.httpStatus} ${requests} requests ${errors} errors`
9416
10051
  );
9417
- const versions = Array.isArray(status.applicationVersions.body.rows) ? status.applicationVersions.body.rows.filter(record7) : [];
10052
+ const versions = Array.isArray(status.applicationVersions.body.rows) ? status.applicationVersions.body.rows.filter(record10) : [];
9418
10053
  out.log(
9419
10054
  `application-versions ${status.applicationVersions.httpStatus} ${versions.length ? versions.slice(0, 5).map(
9420
10055
  (row) => `${String(row.value || "(unattributed)")}:${numeric3(row.requests)}`
9421
10056
  ).join(", ") : "none observed"}`
9422
10057
  );
9423
10058
  out.log(liveSyncLine(status.liveSync));
9424
- const canaryDurations = record7(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
10059
+ const canaryDurations = record10(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
9425
10060
  out.log(
9426
10061
  `canary ${status.canary.httpStatus} ${String(status.canary.body.status ?? status.canary.body.error ?? "unavailable")} ${optionalNumeric(canaryDurations.publishToVisibleMs)} publish-to-visible`
9427
10062
  );
9428
- const collectorIngest = record7(status.collector.body.ingest) ? status.collector.body.ingest : {};
9429
- const collectorStorage = record7(collectorIngest.storage) ? collectorIngest.storage : {};
10063
+ const collectorIngest = record10(status.collector.body.ingest) ? status.collector.body.ingest : {};
10064
+ const collectorStorage = record10(collectorIngest.storage) ? collectorIngest.storage : {};
9430
10065
  out.log(
9431
10066
  `collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
9432
10067
  );
9433
- const providerMetrics = record7(status.provider.body.metrics) ? status.provider.body.metrics : {};
9434
- const providerCapacity = record7(status.provider.body.capacity) ? status.provider.body.capacity : {};
9435
- const workerMemory = record7(providerCapacity.memory) ? providerCapacity.memory : {};
10068
+ const providerMetrics = record10(status.provider.body.metrics) ? status.provider.body.metrics : {};
10069
+ const providerCapacity = record10(status.provider.body.capacity) ? status.provider.body.capacity : {};
10070
+ const workerMemory = record10(providerCapacity.memory) ? providerCapacity.memory : {};
9436
10071
  out.log(
9437
10072
  `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors ${optionalBytes(workerMemory.headroomBytes)} isolate memory headroom`
9438
10073
  );
9439
10074
  for (const line of providerCapacityLines(status.providerCapacity)) {
9440
10075
  out.log(line);
9441
10076
  }
9442
- const coverage = record7(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
9443
- const coverageCounts = record7(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
9444
- const coverageBudget = record7(status.providerReconciliation.body.budget) ? status.providerReconciliation.body.budget : {};
10077
+ const coverage = record10(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
10078
+ const coverageCounts = record10(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
10079
+ const coverageBudget = record10(status.providerReconciliation.body.budget) ? status.providerReconciliation.body.budget : {};
9445
10080
  out.log(
9446
10081
  `request-coverage ${status.providerReconciliation.httpStatus} ${String(status.providerReconciliation.body.status ?? status.providerReconciliation.body.error ?? "unavailable")} ${optionalPercent(coverage.applicationCoverage)} application/provider ${numeric3(coverageCounts.applicationRequests)}/${numeric3(coverageCounts.providerRequests)} requests \xB1${optionalPercent(coverageBudget.maxRelativeError)} budget`
9447
10082
  );
9448
10083
  const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
9449
- const providerFreshness = record7(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
10084
+ const providerFreshness = record10(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
9450
10085
  out.log(
9451
10086
  `cloudflare-history ${status.providerHistory.httpStatus} ${String(status.providerHistory.body.status ?? status.providerHistory.body.error ?? "unavailable")} ${providerPoints} snapshots ${optionalAge(providerFreshness.ageMs)} old`
9452
10087
  );
@@ -9455,17 +10090,17 @@ function printO11yStatus(status, out) {
9455
10090
  );
9456
10091
  }
9457
10092
  function providerCapacityLines(read3) {
9458
- const resources = record7(read3.body.resources) ? read3.body.resources : {};
9459
- const durableObjects = record7(resources.durableObjects) ? resources.durableObjects : {};
9460
- const periodic = record7(durableObjects.periodic) ? durableObjects.periodic : {};
9461
- const storage = record7(durableObjects.sqliteStorage) ? durableObjects.sqliteStorage : {};
9462
- const d1 = record7(resources.d1) ? resources.d1 : {};
9463
- const d1Activity = record7(d1.activity) ? d1.activity : {};
9464
- const d1Storage = record7(d1.storage) ? d1.storage : {};
9465
- const d1Latency = record7(d1Activity.latency) ? d1Activity.latency : {};
9466
- const r2 = record7(resources.r2) ? resources.r2 : {};
9467
- const r2Operations = record7(r2.operations) ? r2.operations : {};
9468
- const r2Storage = record7(r2.storage) ? r2.storage : {};
10093
+ const resources = record10(read3.body.resources) ? read3.body.resources : {};
10094
+ const durableObjects = record10(resources.durableObjects) ? resources.durableObjects : {};
10095
+ const periodic = record10(durableObjects.periodic) ? durableObjects.periodic : {};
10096
+ const storage = record10(durableObjects.sqliteStorage) ? durableObjects.sqliteStorage : {};
10097
+ const d1 = record10(resources.d1) ? resources.d1 : {};
10098
+ const d1Activity = record10(d1.activity) ? d1.activity : {};
10099
+ const d1Storage = record10(d1.storage) ? d1.storage : {};
10100
+ const d1Latency = record10(d1Activity.latency) ? d1Activity.latency : {};
10101
+ const r2 = record10(resources.r2) ? resources.r2 : {};
10102
+ const r2Operations = record10(r2.operations) ? r2.operations : {};
10103
+ const r2Storage = record10(r2.storage) ? r2.storage : {};
9469
10104
  const status = String(
9470
10105
  read3.body.status ?? read3.body.error ?? "unavailable"
9471
10106
  );
@@ -9476,11 +10111,11 @@ function providerCapacityLines(read3) {
9476
10111
  ];
9477
10112
  }
9478
10113
  function liveSyncLine(read3) {
9479
- const performance = record7(read3.body.performance) ? read3.body.performance : {};
9480
- const commitToSend = record7(performance.commitToSend) ? performance.commitToSend : {};
10114
+ const performance = record10(read3.body.performance) ? read3.body.performance : {};
10115
+ const commitToSend = record10(performance.commitToSend) ? performance.commitToSend : {};
9481
10116
  return `live-sync ${read3.httpStatus} ${String(read3.body.status ?? read3.body.error ?? "unavailable")} ${numeric3(read3.body.activeConnections)} active ${optionalNumeric(commitToSend.p95)} commit-to-send p95 ${numeric3(performance.sendFailures)} send failures`;
9482
10117
  }
9483
- function record7(value2) {
10118
+ function record10(value2) {
9484
10119
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
9485
10120
  }
9486
10121
  function numeric3(value2) {
@@ -9649,23 +10284,23 @@ function statusMinutes(value2) {
9649
10284
  }
9650
10285
  async function read2(url, headers, doFetch) {
9651
10286
  const response2 = await doFetch(url, { headers });
9652
- const text = await response2.text();
10287
+ const text2 = await response2.text();
9653
10288
  let body = {};
9654
- if (text) {
10289
+ if (text2) {
9655
10290
  try {
9656
- const value2 = JSON.parse(text);
10291
+ const value2 = JSON.parse(text2);
9657
10292
  body = value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : { value: value2 };
9658
10293
  } catch {
9659
- body = { message: text.slice(0, 300) };
10294
+ body = { message: text2.slice(0, 300) };
9660
10295
  }
9661
10296
  }
9662
10297
  return { httpStatus: response2.status, body };
9663
10298
  }
9664
10299
 
9665
10300
  // src/provision.ts
9666
- var import_apps10 = require("@odla-ai/apps");
10301
+ var import_apps12 = require("@odla-ai/apps");
9667
10302
  var import_ai3 = require("@odla-ai/ai");
9668
- var import_node_process10 = __toESM(require("process"), 1);
10303
+ var import_node_process11 = __toESM(require("process"), 1);
9669
10304
 
9670
10305
  // src/integration-provision.ts
9671
10306
  var import_db3 = require("@odla-ai/db");
@@ -9720,9 +10355,9 @@ async function responseText(res) {
9720
10355
  }
9721
10356
 
9722
10357
  // src/provision-credentials.ts
9723
- var import_apps9 = require("@odla-ai/apps");
10358
+ var import_apps11 = require("@odla-ai/apps");
9724
10359
  async function provisionEnvCredentials(opts) {
9725
- const tenantId = (0, import_apps9.tenantIdFor)(opts.cfg.app.id, opts.env);
10360
+ const tenantId = (0, import_apps11.tenantIdFor)(opts.cfg.app.id, opts.env);
9726
10361
  const prior = opts.credentials?.envs[opts.env];
9727
10362
  let credentials = opts.credentials;
9728
10363
  let dbKey = opts.cfg.services.includes("db") && !opts.rotateDb ? prior?.dbKey : void 0;
@@ -9884,7 +10519,7 @@ async function provision(options) {
9884
10519
  }
9885
10520
  const doFetch = options.fetch ?? fetch;
9886
10521
  const token = await getDeveloperToken(cfg, options, doFetch, out);
9887
- const apps = (0, import_apps10.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
10522
+ const apps = (0, import_apps12.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
9888
10523
  const existing = await apps.resolveApp(cfg.app.id);
9889
10524
  if (existing) {
9890
10525
  out.log(`app: ${cfg.app.id} already exists`);
@@ -9895,7 +10530,7 @@ async function provision(options) {
9895
10530
  for (const env of cfg.envs) {
9896
10531
  await assertTenantAdminAccess(doFetch, cfg, env, token);
9897
10532
  }
9898
- const serviceOrder = (0, import_apps10.orderAppServices)(cfg.services);
10533
+ const serviceOrder = (0, import_apps12.orderAppServices)(cfg.services);
9899
10534
  for (const env of cfg.envs) {
9900
10535
  for (const service of serviceOrder) {
9901
10536
  if (service === "ai") {
@@ -9928,7 +10563,7 @@ async function provision(options) {
9928
10563
  }
9929
10564
  }
9930
10565
  for (const env of cfg.envs) {
9931
- const tenantId = (0, import_apps10.tenantIdFor)(cfg.app.id, env);
10566
+ const tenantId = (0, import_apps12.tenantIdFor)(cfg.app.id, env);
9932
10567
  credentials = await provisionEnvCredentials({
9933
10568
  cfg,
9934
10569
  env,
@@ -9972,7 +10607,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
9972
10607
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
9973
10608
  }
9974
10609
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
9975
- const key = import_node_process10.default.env[cfg.ai.keyEnv];
10610
+ const key = import_node_process11.default.env[cfg.ai.keyEnv];
9976
10611
  if (key) {
9977
10612
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
9978
10613
  await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -10013,8 +10648,8 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10013
10648
  }
10014
10649
 
10015
10650
  // src/record.ts
10016
- var import_node_fs15 = require("fs");
10017
- var import_node_process11 = __toESM(require("process"), 1);
10651
+ var import_node_fs16 = require("fs");
10652
+ var import_node_process12 = __toESM(require("process"), 1);
10018
10653
 
10019
10654
  // src/surface.ts
10020
10655
  var PM_ACTIONS = {
@@ -10062,7 +10697,7 @@ var COMMAND_SURFACE = {
10062
10697
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
10063
10698
  capabilities: {},
10064
10699
  code: { connect: {} },
10065
- config: { diff: {}, plan: {} },
10700
+ config: { diff: {}, plan: {}, apply: {} },
10066
10701
  context: { show: {}, list: {}, save: {}, remove: {} },
10067
10702
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
10068
10703
  discuss: {
@@ -10080,7 +10715,11 @@ var COMMAND_SURFACE = {
10080
10715
  help: {},
10081
10716
  init: {},
10082
10717
  o11y: { status: {} },
10083
- platform: { status: {} },
10718
+ operations: { get: {}, wait: {} },
10719
+ platform: {
10720
+ status: {},
10721
+ "chat-credentials": { rotate: {} }
10722
+ },
10084
10723
  pm: {
10085
10724
  ...PM_ENTITIES,
10086
10725
  handoff: {}
@@ -10159,7 +10798,7 @@ function invocationPath(words2) {
10159
10798
 
10160
10799
  // src/record.ts
10161
10800
  function recordInvocation(parsed) {
10162
- const file = import_node_process11.default.env.ODLA_CLI_RECORD;
10801
+ const file = import_node_process12.default.env.ODLA_CLI_RECORD;
10163
10802
  if (!file) return;
10164
10803
  try {
10165
10804
  const entry = {
@@ -10167,14 +10806,14 @@ function recordInvocation(parsed) {
10167
10806
  options: Object.entries(parsed.options).map(([name, value2]) => value2 === false ? `no-${name}` : name).sort()
10168
10807
  };
10169
10808
  if (!entry.path.length) return;
10170
- (0, import_node_fs15.appendFileSync)(file, `${JSON.stringify(entry)}
10809
+ (0, import_node_fs16.appendFileSync)(file, `${JSON.stringify(entry)}
10171
10810
  `);
10172
10811
  } catch {
10173
10812
  }
10174
10813
  }
10175
10814
 
10176
10815
  // src/runbook-actions.ts
10177
- var import_node_fs16 = require("fs");
10816
+ var import_node_fs17 = require("fs");
10178
10817
 
10179
10818
  // src/runbook-requires.ts
10180
10819
  var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
@@ -10259,7 +10898,7 @@ async function bySlug(ctx, slug) {
10259
10898
  function readBody(file, inline) {
10260
10899
  if (inline !== void 0) return inline;
10261
10900
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
10262
- return (0, import_node_fs16.readFileSync)(file === "-" ? 0 : file, "utf8");
10901
+ return (0, import_node_fs17.readFileSync)(file === "-" ? 0 : file, "utf8");
10263
10902
  }
10264
10903
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
10265
10904
  async function runbookList(ctx, all, query) {
@@ -10289,12 +10928,17 @@ async function runbookNew(ctx, slug, title, body, summary, requires) {
10289
10928
  });
10290
10929
  ctx.out.log(ctx.json ? JSON.stringify(created, null, 2) : `created ${slug} (${created.id})`);
10291
10930
  }
10292
- async function runbookEdit(ctx, slug, body, note, requires) {
10931
+ async function runbookEdit(ctx, slug, body, note, requires, expectedVersion) {
10293
10932
  const runbook = await bySlug(ctx, slug);
10294
10933
  const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
10295
10934
  // An empty --requires clears the declaration; omitting the flag leaves
10296
10935
  // whatever is there, so an ordinary body edit never drops it.
10297
- patch: { body, ...note ? { note } : {}, ...requires === void 0 ? {} : { requires: requires || null } }
10936
+ patch: {
10937
+ body,
10938
+ expectedVersion: expectedVersion ?? runbook.version,
10939
+ ...note ? { note } : {},
10940
+ ...requires === void 0 ? {} : { requires: requires || null }
10941
+ }
10298
10942
  });
10299
10943
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
10300
10944
  ctx.out.log(`${slug} \u2192 v${result.record?.version ?? runbook.version + 1}`);
@@ -10346,14 +10990,14 @@ async function runbookRemove(ctx, slug) {
10346
10990
  }
10347
10991
 
10348
10992
  // src/runbook-import.ts
10349
- var import_node_fs17 = require("fs");
10350
- var import_node_path14 = require("path");
10351
- function parseRunbook(text, slug) {
10352
- let rest = text;
10993
+ var import_node_fs18 = require("fs");
10994
+ var import_node_path15 = require("path");
10995
+ function parseRunbook(text2, slug) {
10996
+ let rest = text2;
10353
10997
  const meta = {};
10354
- const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
10998
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text2);
10355
10999
  if (fm) {
10356
- rest = text.slice(fm[0].length);
11000
+ rest = text2.slice(fm[0].length);
10357
11001
  for (const line of fm[1].split(/\r?\n/)) {
10358
11002
  const pair = /^(\w+)\s*:\s*(.+)$/.exec(line.trim());
10359
11003
  if (!pair) continue;
@@ -10372,12 +11016,12 @@ function parseRunbook(text, slug) {
10372
11016
  };
10373
11017
  }
10374
11018
  function readRunbookDir(dir) {
10375
- if (!(0, import_node_fs17.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
10376
- const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
11019
+ if (!(0, import_node_fs18.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
11020
+ const files = (0, import_node_fs18.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
10377
11021
  if (!files.length) throw new Error(`no .md files in ${dir}`);
10378
11022
  return files.map((file) => {
10379
- const slug = (0, import_node_path14.basename)(file, ".md");
10380
- const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path14.join)(dir, file), "utf8"), slug);
11023
+ const slug = (0, import_node_path15.basename)(file, ".md");
11024
+ const parsed = parseRunbook((0, import_node_fs18.readFileSync)((0, import_node_path15.join)(dir, file), "utf8"), slug);
10381
11025
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
10382
11026
  });
10383
11027
  }
@@ -10434,6 +11078,7 @@ async function upsert(ctx, r, visibility) {
10434
11078
  patch: {
10435
11079
  title: r.title,
10436
11080
  body: r.body,
11081
+ expectedVersion: found.version,
10437
11082
  ...r.summary ? { summary: r.summary } : {},
10438
11083
  ...r.tags ? { tags: r.tags } : {},
10439
11084
  note: `imported from ${r.file}`
@@ -10449,8 +11094,8 @@ async function upsert(ctx, r, visibility) {
10449
11094
 
10450
11095
  // src/runbook-impact.ts
10451
11096
  var import_node_child_process7 = require("child_process");
10452
- var import_node_fs18 = require("fs");
10453
- var import_node_path15 = require("path");
11097
+ var import_node_fs19 = require("fs");
11098
+ var import_node_path16 = require("path");
10454
11099
 
10455
11100
  // src/runbook-impact-scan.ts
10456
11101
  var DECL = /^[+-]\s*export\s+(?:declare\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/;
@@ -10619,10 +11264,10 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
10619
11264
  }
10620
11265
  function manifestLabeller(root) {
10621
11266
  return (workspace) => {
10622
- const manifest = (0, import_node_path15.join)(root, workspace, "package.json");
10623
- if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
11267
+ const manifest = (0, import_node_path16.join)(root, workspace, "package.json");
11268
+ if (!(0, import_node_fs19.existsSync)(manifest)) return void 0;
10624
11269
  try {
10625
- const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
11270
+ const name = JSON.parse((0, import_node_fs19.readFileSync)(manifest, "utf8")).name;
10626
11271
  return typeof name === "string" ? name : void 0;
10627
11272
  } catch {
10628
11273
  return void 0;
@@ -10689,7 +11334,7 @@ function report3(ctx, impacts) {
10689
11334
  async function runbookImpact(ctx, options, deps = {}) {
10690
11335
  const cwd = deps.cwd ?? process.cwd();
10691
11336
  const runGit = deps.runGit ?? gitRunner(cwd);
10692
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path15.join)(cwd, path), "utf8"));
11337
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs19.readFileSync)((0, import_node_path16.join)(cwd, path), "utf8"));
10693
11338
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
10694
11339
  if (!surfaces.length) {
10695
11340
  return ctx.out.log(
@@ -10822,12 +11467,12 @@ async function runbookComment(ctx, slug, body) {
10822
11467
 
10823
11468
  // src/runbook-editor.ts
10824
11469
  var import_node_child_process8 = require("child_process");
10825
- var import_node_fs19 = require("fs");
11470
+ var import_node_fs20 = require("fs");
10826
11471
  var import_node_os5 = require("os");
10827
- var import_node_path16 = require("path");
10828
- var import_node_process12 = __toESM(require("process"), 1);
11472
+ var import_node_path17 = require("path");
11473
+ var import_node_process13 = __toESM(require("process"), 1);
10829
11474
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
10830
- function resolveEditor(env = import_node_process12.default.env) {
11475
+ function resolveEditor(env = import_node_process13.default.env) {
10831
11476
  for (const name of EDITOR_ENV) {
10832
11477
  const value2 = env[name];
10833
11478
  if (value2 && value2.trim()) return value2.trim();
@@ -10841,8 +11486,8 @@ function defaultRun(command, path) {
10841
11486
  return result.status ?? 0;
10842
11487
  }
10843
11488
  function editText(initial, slug, deps = {}) {
10844
- const env = deps.env ?? import_node_process12.default.env;
10845
- const interactive = deps.interactive ?? (() => Boolean(import_node_process12.default.stdin.isTTY));
11489
+ const env = deps.env ?? import_node_process13.default.env;
11490
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process13.default.stdin.isTTY));
10846
11491
  const editor = resolveEditor(env);
10847
11492
  if (!editor)
10848
11493
  throw new Error(
@@ -10850,16 +11495,16 @@ function editText(initial, slug, deps = {}) {
10850
11495
  );
10851
11496
  if (!interactive())
10852
11497
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
10853
- const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path16.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
10854
- const file = (0, import_node_path16.join)(dir, `${slug}.md`);
11498
+ const dir = (0, import_node_fs20.mkdtempSync)((0, import_node_path17.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
11499
+ const file = (0, import_node_path17.join)(dir, `${slug}.md`);
10855
11500
  try {
10856
- (0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
11501
+ (0, import_node_fs20.writeFileSync)(file, initial, { mode: 384 });
10857
11502
  const code = defaultRunOrInjected(deps)(editor, file);
10858
11503
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
10859
- const edited = (0, import_node_fs19.readFileSync)(file, "utf8");
11504
+ const edited = (0, import_node_fs20.readFileSync)(file, "utf8");
10860
11505
  return edited === initial ? null : edited;
10861
11506
  } finally {
10862
- (0, import_node_fs19.rmSync)(dir, { recursive: true, force: true });
11507
+ (0, import_node_fs20.rmSync)(dir, { recursive: true, force: true });
10863
11508
  }
10864
11509
  }
10865
11510
  var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
@@ -10874,27 +11519,90 @@ async function editRunbook(ctx, slug, deps = {}) {
10874
11519
  const found = page.records[0];
10875
11520
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
10876
11521
  ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
10877
- return editText(found.body, slug, deps);
11522
+ const body = await editText(found.body, slug, deps);
11523
+ return body === null ? null : { body, expectedVersion: found.version };
10878
11524
  }
10879
11525
 
10880
11526
  // src/whoami-command.ts
11527
+ var text = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
11528
+ function principalKind(value2, machine) {
11529
+ return value2 === "human" || value2 === "agent" || value2 === "service" ? value2 : machine ? "service" : "human";
11530
+ }
11531
+ function credentialKind(value2, machine, scopes) {
11532
+ if (value2 === "machine" || value2 === "device" || value2 === "clerk") return value2;
11533
+ if (machine) return "machine";
11534
+ if (scopes.length) return "device";
11535
+ return "unknown";
11536
+ }
11537
+ function managerOf(value2) {
11538
+ if (!value2 || typeof value2 !== "object") return null;
11539
+ const row = value2;
11540
+ const principalId = text(row.principalId);
11541
+ if (!principalId) return null;
11542
+ return {
11543
+ principalId,
11544
+ displayName: text(row.displayName) ?? "Unnamed member",
11545
+ handle: text(row.handle) ?? ""
11546
+ };
11547
+ }
11548
+ function unnamedPrincipal(kind) {
11549
+ if (kind === "agent") return "Unnamed agent";
11550
+ if (kind === "service") return "Unnamed service";
11551
+ return "Unnamed member";
11552
+ }
10881
11553
  async function fetchIdentity(platformUrl, token, doFetch) {
10882
11554
  const res = await doFetch(`${platformUrl.replace(/\/$/, "")}/registry/me`, {
10883
11555
  headers: { authorization: `Bearer ${token}` }
10884
11556
  });
10885
11557
  if (!res.ok) throw new Error(`could not resolve identity (HTTP ${res.status})`);
10886
11558
  const body = await res.json();
11559
+ const developerId = text(body.developerId) ?? "";
11560
+ const machine = body.machine === true;
11561
+ const scopes = Array.isArray(body.scopes) ? body.scopes.map(String) : [];
11562
+ const principalId = text(body.principalId) ?? developerId;
11563
+ const email = text(body.email);
11564
+ const kind = principalKind(body.principalKind, machine);
11565
+ const displayName = text(body.displayName) ?? email ?? unnamedPrincipal(kind);
11566
+ const handle = text(body.handle) ?? "";
11567
+ const credential2 = body.credential && typeof body.credential === "object" ? body.credential : {};
10887
11568
  return {
10888
- developerId: String(body.developerId ?? ""),
10889
- email: body.email ?? null,
11569
+ developerId,
11570
+ principalId,
11571
+ principalKind: kind,
11572
+ displayName,
11573
+ handle,
11574
+ manager: managerOf(body.manager),
11575
+ credential: {
11576
+ id: text(credential2.id),
11577
+ kind: credentialKind(credential2.kind, machine, scopes)
11578
+ },
11579
+ email,
10890
11580
  admin: body.admin === true,
10891
- machine: body.machine === true,
10892
- scopes: Array.isArray(body.scopes) ? body.scopes.map(String) : []
11581
+ machine,
11582
+ scopes
10893
11583
  };
10894
11584
  }
10895
- function credentialKind(identity) {
10896
- if (identity.machine) return "machine (platform admin secret)";
10897
- return identity.scopes.length ? "device token (scoped)" : "device token or session";
11585
+ function credentialLabel(identity) {
11586
+ if (identity.credential.kind === "machine") return "machine (platform admin secret)";
11587
+ if (identity.credential.kind === "device")
11588
+ return identity.scopes.length ? "device (scoped)" : "device";
11589
+ if (identity.credential.kind === "clerk") return "clerk";
11590
+ return "unknown (legacy server)";
11591
+ }
11592
+ function namedPrincipal(identity) {
11593
+ return identity.handle && identity.handle !== identity.displayName ? `${identity.displayName} (@${identity.handle})` : identity.displayName;
11594
+ }
11595
+ function namedManager(manager) {
11596
+ return manager.handle && manager.handle !== manager.displayName ? `${manager.displayName} (@${manager.handle})` : manager.displayName;
11597
+ }
11598
+ function accountableOwner(identity) {
11599
+ if (identity.principalKind === "agent" && identity.manager) {
11600
+ return namedManager(identity.manager);
11601
+ }
11602
+ if (identity.principalKind === "human" && identity.principalId === identity.developerId) {
11603
+ return namedPrincipal(identity);
11604
+ }
11605
+ return identity.email ?? "Unnamed member";
10898
11606
  }
10899
11607
  async function whoamiCommand(parsed, deps = {}) {
10900
11608
  assertArgs(
@@ -10925,9 +11633,19 @@ async function whoamiCommand(parsed, deps = {}) {
10925
11633
  return;
10926
11634
  }
10927
11635
  out.log(`platform: ${cfg.platformUrl}`);
10928
- out.log(`developer: ${identity.developerId}`);
11636
+ out.log(`principal: ${namedPrincipal(identity)}`);
11637
+ out.log(`principal id: ${identity.principalId}`);
11638
+ out.log(`kind: ${identity.principalKind}`);
11639
+ if (identity.principalKind === "agent")
11640
+ out.log(
11641
+ `manager: ${identity.manager ? namedManager(identity.manager) : "(unknown)"}`
11642
+ );
11643
+ out.log(`owner: ${accountableOwner(identity)}`);
11644
+ out.log(`owner id: ${identity.developerId}`);
10929
11645
  out.log(`email: ${identity.email ?? "(none)"}`);
10930
- out.log(`credential: ${credentialKind(identity)}`);
11646
+ out.log(`credential: ${credentialLabel(identity)}`);
11647
+ if (identity.credential.id)
11648
+ out.log(`credential id: ${identity.credential.id}`);
10931
11649
  out.log(`admin: ${identity.admin ? "yes" : "no"}`);
10932
11650
  if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
10933
11651
  if (!identity.admin) {
@@ -11076,14 +11794,16 @@ async function runbookCommand(parsed, deps = {}) {
11076
11794
  const name = requireSlug(slug, "edit");
11077
11795
  const file = stringOpt(parsed.options.file);
11078
11796
  const inline = stringOpt(parsed.options.body);
11079
- const body = file === void 0 && inline === void 0 ? await editRunbook(ctx, name) : readBody(file, inline);
11080
- if (body === null) return ctx.out.log(`${name} unchanged; nothing written`);
11797
+ const edited = file === void 0 && inline === void 0 ? await editRunbook(ctx, name) : readBody(file, inline);
11798
+ if (edited === null) return ctx.out.log(`${name} unchanged; nothing written`);
11799
+ const body = typeof edited === "string" ? edited : edited.body;
11081
11800
  return runbookEdit(
11082
11801
  ctx,
11083
11802
  name,
11084
11803
  body,
11085
11804
  stringOpt(parsed.options.note),
11086
- parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? ""
11805
+ parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? "",
11806
+ typeof edited === "string" ? void 0 : edited.expectedVersion
11087
11807
  );
11088
11808
  }
11089
11809
  case "import": {
@@ -11271,7 +11991,7 @@ function hostedSeverity(value2, flag) {
11271
11991
  var import_security2 = require("@odla-ai/security");
11272
11992
 
11273
11993
  // src/security.ts
11274
- var import_node_path17 = require("path");
11994
+ var import_node_path18 = require("path");
11275
11995
  var import_security = require("@odla-ai/security");
11276
11996
  var import_node3 = require("@odla-ai/security/node");
11277
11997
  async function runHostedSecurity(options) {
@@ -11283,9 +12003,9 @@ async function runHostedSecurity(options) {
11283
12003
  const appId = selfAudit ? "odla-ai" : cfg.app.id;
11284
12004
  const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
11285
12005
  const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
11286
- const target = (0, import_node_path17.resolve)(options.target ?? cfg?.rootDir ?? ".");
11287
- const output = (0, import_node_path17.resolve)(options.out ?? (0, import_node_path17.resolve)(target, ".odla/security/hosted"));
11288
- const outputRelative = (0, import_node_path17.relative)(target, output).split(import_node_path17.sep).join("/");
12006
+ const target = (0, import_node_path18.resolve)(options.target ?? cfg?.rootDir ?? ".");
12007
+ const output = (0, import_node_path18.resolve)(options.out ?? (0, import_node_path18.resolve)(target, ".odla/security/hosted"));
12008
+ const outputRelative = (0, import_node_path18.relative)(target, output).split(import_node_path18.sep).join("/");
11289
12009
  if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
11290
12010
  const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
11291
12011
  const tokenRequest = {
@@ -11297,7 +12017,7 @@ async function runHostedSecurity(options) {
11297
12017
  };
11298
12018
  const token = await injectedToken(options, tokenRequest);
11299
12019
  const snapshot = await (0, import_node3.snapshotDirectory)(target, {
11300
- exclude: !outputRelative.startsWith("../") && !(0, import_node_path17.isAbsolute)(outputRelative) ? [outputRelative] : []
12020
+ exclude: !outputRelative.startsWith("../") && !(0, import_node_path18.isAbsolute)(outputRelative) ? [outputRelative] : []
11301
12021
  });
11302
12022
  const hosted = await (0, import_security.createPlatformSecurityReasoners)({
11303
12023
  platform,
@@ -11315,7 +12035,7 @@ async function runHostedSecurity(options) {
11315
12035
  });
11316
12036
  const harness = (0, import_security.createSecurityHarness)({
11317
12037
  profile,
11318
- store: new import_node3.FileRunStore((0, import_node_path17.resolve)(output, "state")),
12038
+ store: new import_node3.FileRunStore((0, import_node_path18.resolve)(output, "state")),
11319
12039
  discoveryReasoner: hosted.discoveryReasoner,
11320
12040
  validationReasoner: hosted.validationReasoner,
11321
12041
  policy: {
@@ -11339,7 +12059,7 @@ async function runHostedSecurity(options) {
11339
12059
  function selectEnv(requested, declared, configPath, rootDir) {
11340
12060
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
11341
12061
  if (!env || !declared.includes(env)) {
11342
- const shown = (0, import_node_path17.relative)(rootDir, configPath) || configPath;
12062
+ const shown = (0, import_node_path18.relative)(rootDir, configPath) || configPath;
11343
12063
  throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
11344
12064
  }
11345
12065
  return env;
@@ -11368,7 +12088,7 @@ function printSummary(out, appId, env, run, report4, output) {
11368
12088
  out.log(` coverage: ${report4.coverageStatus} ${complete}/${report4.coverage.length} blocked=${report4.metrics.blockedCells} shallow=${report4.metrics.shallowCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
11369
12089
  if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
11370
12090
  out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
11371
- out.log(` report: ${(0, import_node_path17.resolve)(output, "REPORT.md")}`);
12091
+ out.log(` report: ${(0, import_node_path18.resolve)(output, "REPORT.md")}`);
11372
12092
  }
11373
12093
  function formatBudget(usage) {
11374
12094
  return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
@@ -11808,11 +12528,11 @@ async function securityStatus(parsed, dependencies) {
11808
12528
  // src/cli.ts
11809
12529
  function exitCodeFor(err) {
11810
12530
  const code = err?.code;
11811
- if (code === "handshake_pending" || code === "watch_timeout") return 75;
12531
+ if (code === "handshake_pending" || code === "watch_timeout" || code === "operation_pending") return 75;
11812
12532
  if (code === "checkpoint_required") return 3;
11813
12533
  if (code === "remote_unavailable") return 6;
11814
12534
  if (code === "auth_failed") return 5;
11815
- if (code === "invalid_cursor") return 2;
12535
+ if (code === "invalid_cursor" || code === "invalid_plan" || code === "invalid_operation_id") return 2;
11816
12536
  return 1;
11817
12537
  }
11818
12538
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {