@odla-ai/cli 0.25.3 → 0.25.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1156,6 +1156,37 @@ function redactSecrets(value) {
1156
1156
  for (const [pattern, replacement] of REPLACEMENTS) result = result.replace(pattern, replacement);
1157
1157
  return result;
1158
1158
  }
1159
+ function redactingOutput(output) {
1160
+ const redactArgs = (args) => args.map((value) => redactOutputValue(value, /* @__PURE__ */ new WeakMap()));
1161
+ return {
1162
+ log: (...args) => output.log(...redactArgs(args)),
1163
+ error: (...args) => output.error(...redactArgs(args))
1164
+ };
1165
+ }
1166
+ function redactOutputValue(value, seen) {
1167
+ if (typeof value === "string") return redactSecrets(value);
1168
+ if (value instanceof Error) {
1169
+ const redacted = new Error(redactSecrets(value.message));
1170
+ redacted.name = value.name;
1171
+ if (value.stack) redacted.stack = redactSecrets(value.stack);
1172
+ return redacted;
1173
+ }
1174
+ if (!value || typeof value !== "object") return value;
1175
+ const prior = seen.get(value);
1176
+ if (prior) return prior;
1177
+ if (Array.isArray(value)) {
1178
+ const next2 = [];
1179
+ seen.set(value, next2);
1180
+ for (const item of value) next2.push(redactOutputValue(item, seen));
1181
+ return next2;
1182
+ }
1183
+ const prototype = Object.getPrototypeOf(value);
1184
+ if (prototype !== Object.prototype && prototype !== null) return value;
1185
+ const next = {};
1186
+ seen.set(value, next);
1187
+ for (const [key, item] of Object.entries(value)) next[key] = redactOutputValue(item, seen);
1188
+ return next;
1189
+ }
1159
1190
  function looksSecret(value) {
1160
1191
  return redactSecrets(value) !== value || value.includes("-----BEGIN");
1161
1192
  }
@@ -1585,6 +1616,7 @@ var CAPABILITIES = {
1585
1616
  "compose declared app-capability schema/rules, create guarded seeds when absent, and configure platform AI, auth, and deployment links",
1586
1617
  "apply Google Calendar booking config, then drive state-bound consent, status, discovery, and disconnect flows (bookings run live through the platform proxy)",
1587
1618
  "validate integration contracts offline and smoke-test a provisioned db environment plus anonymous capability routes",
1619
+ "read one versioned o11y status envelope spanning application RED, current live-sync freshness/load, the protected commit-to-visible canary, collector ingest/scheduler trust, provider-owned runtime metrics, and a bounded machine verdict",
1588
1620
  "run app-attributed hosted security discovery and independent validation without provider keys",
1589
1621
  "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
1590
1622
  "let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
@@ -1605,7 +1637,7 @@ var CAPABILITIES = {
1605
1637
  "approve GitHub App repository access separately from redacted-snippet disclosure"
1606
1638
  ],
1607
1639
  studio: [
1608
- "view telemetry and environment state",
1640
+ "view application telemetry, reconciled live-sync load/freshness, commit-to-visible canary health, provider-owned Worker runtime metrics, and environment state",
1609
1641
  "let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them",
1610
1642
  "review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens",
1611
1643
  "perform manual credential recovery \u2014 for the primary owner or any co-owner \u2014 when the CLI's local shown-once copy is unavailable",
@@ -2578,9 +2610,17 @@ async function smoke(options) {
2578
2610
  throw new Error(`local credentials are for app "${credentials.appId}", but config app is "${cfg.app.id}"`);
2579
2611
  }
2580
2612
  const entry = credentials.envs[env];
2581
- if (!entry?.tenantId || !entry.dbKey) {
2613
+ if (!entry?.tenantId) {
2614
+ throw new Error(`local credentials have no tenant for env "${env}". Run "odla-ai provision --write-dev-vars".`);
2615
+ }
2616
+ const hasDb = cfg.services.includes("db");
2617
+ const hasO11y = cfg.services.includes("o11y");
2618
+ if (hasDb && !entry.dbKey) {
2582
2619
  throw new Error(`local credentials have no db key for env "${env}". Run "odla-ai provision --write-dev-vars".`);
2583
2620
  }
2621
+ if (hasO11y && !entry.o11yToken) {
2622
+ throw new Error(`local credentials have no o11y token for env "${env}". Run "odla-ai provision --write-dev-vars".`);
2623
+ }
2584
2624
  const doFetch = options.fetch ?? fetch;
2585
2625
  out.log(`smoke: ${cfg.app.id}/${env}`);
2586
2626
  out.log(` tenant: ${entry.tenantId}`);
@@ -2593,6 +2633,7 @@ async function smoke(options) {
2593
2633
  }
2594
2634
  out.log(` ai: ${provider}`);
2595
2635
  }
2636
+ if (hasO11y) out.log(` o11y: credentials present`);
2596
2637
  if (cfg.services.includes("calendar")) {
2597
2638
  const token = await getDeveloperToken(
2598
2639
  cfg,
@@ -2611,26 +2652,30 @@ async function smoke(options) {
2611
2652
  out.log(` calendar: ${status.status}, bookable (booking \u2192 ${status.bookingCalendarId ?? "primary"})`);
2612
2653
  }
2613
2654
  const database = await resolveDatabaseConfig(cfg);
2614
- const expectedSchema = database.schema;
2615
- const expectedEntities = serializedEntities(expectedSchema);
2616
- const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/schema`, entry.dbKey);
2617
- const liveSchema = liveSchemaPayload.schema ?? liveSchemaPayload;
2618
- const liveEntities = serializedEntities(liveSchema);
2619
- if (expectedEntities.length) {
2620
- const missing = expectedEntities.filter((entity) => !liveEntities.includes(entity));
2621
- if (missing.length) throw new Error(`live schema is missing expected entities: ${missing.join(", ")}`);
2622
- }
2623
- out.log(` schema: ${liveEntities.length} entities`);
2624
- const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
2625
- if (aggregateEntity) {
2626
- const aggregate = await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
2627
- ns: aggregateEntity,
2628
- aggregate: { count: true }
2629
- });
2630
- const count = aggregate.aggregate?.count;
2631
- out.log(` aggregate: ${aggregateEntity}.count=${String(count ?? "ok")}`);
2655
+ if (hasDb) {
2656
+ const expectedSchema = database.schema;
2657
+ const expectedEntities = serializedEntities(expectedSchema);
2658
+ const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/schema`, entry.dbKey);
2659
+ const liveSchema = liveSchemaPayload.schema ?? liveSchemaPayload;
2660
+ const liveEntities = serializedEntities(liveSchema);
2661
+ if (expectedEntities.length) {
2662
+ const missing = expectedEntities.filter((entity) => !liveEntities.includes(entity));
2663
+ if (missing.length) throw new Error(`live schema is missing expected entities: ${missing.join(", ")}`);
2664
+ }
2665
+ out.log(` schema: ${liveEntities.length} entities`);
2666
+ const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
2667
+ if (aggregateEntity) {
2668
+ const aggregate = await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
2669
+ ns: aggregateEntity,
2670
+ aggregate: { count: true }
2671
+ });
2672
+ const count = aggregate.aggregate?.count;
2673
+ out.log(` aggregate: ${aggregateEntity}.count=${String(count ?? "ok")}`);
2674
+ } else {
2675
+ out.log(` aggregate: skipped (schema has no entities)`);
2676
+ }
2632
2677
  } else {
2633
- out.log(` aggregate: skipped (schema has no entities)`);
2678
+ out.log(` db: skipped (not enabled)`);
2634
2679
  }
2635
2680
  const probes = database.integrations.flatMap(
2636
2681
  (integration) => (integration.probes ?? []).map((probe) => ({ integration: integration.id, probe }))
@@ -2683,7 +2728,7 @@ function publicConfigUrl(platformUrl, appId, env) {
2683
2728
  }
2684
2729
  async function safeText3(res) {
2685
2730
  try {
2686
- return (await res.text()).slice(0, 500);
2731
+ return redactSecrets((await res.text()).slice(0, 500));
2687
2732
  } catch {
2688
2733
  return "";
2689
2734
  }
@@ -3406,8 +3451,8 @@ async function materializeGitTree(source, commitSha, options = {}) {
3406
3451
  const maxFiles = options.maxFiles ?? 2e4;
3407
3452
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
3408
3453
  const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
3409
- const entries = inventory.flatMap((record5) => {
3410
- const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record5);
3454
+ const entries = inventory.flatMap((record6) => {
3455
+ const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record6);
3411
3456
  return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
3412
3457
  });
3413
3458
  if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
@@ -3655,8 +3700,8 @@ function normalize(value) {
3655
3700
  if (Array.isArray(value)) return value.map(normalize);
3656
3701
  if (value instanceof Uint8Array) return { $bytes: [...value] };
3657
3702
  if (typeof value === "object") {
3658
- const record5 = value;
3659
- return Object.fromEntries(Object.keys(record5).filter((key) => record5[key] !== void 0).sort().map((key) => [key, normalize(record5[key])]));
3703
+ const record6 = value;
3704
+ return Object.fromEntries(Object.keys(record6).filter((key) => record6[key] !== void 0).sort().map((key) => [key, normalize(record6[key])]));
3660
3705
  }
3661
3706
  throw new CamelError("state_conflict", "Canonical JSON rejects unsupported values.");
3662
3707
  }
@@ -5901,8 +5946,8 @@ function cliVersion() {
5901
5946
  const pkg = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8"));
5902
5947
  return pkg.version ?? "unknown";
5903
5948
  }
5904
- function printHelp() {
5905
- console.log(`odla-ai
5949
+ function printHelp(output = console) {
5950
+ output.log(`odla-ai
5906
5951
 
5907
5952
  Start here:
5908
5953
  odla-ai runbook ask "<question>" The current procedure, from odla's own
@@ -5934,22 +5979,23 @@ Usage:
5934
5979
  odla-ai app owners add <email> [--email <odla-account>] [--json]
5935
5980
  odla-ai app owners remove <email> [--email <odla-account>] [--json]
5936
5981
  odla-ai pm <goal|task|decision|bug> list [--app <id>] [--status <s>] [--severity <s>] [--column <c>] [--q <text>] [--json]
5937
- odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--description <text>|--body <text>|--proof <text>]
5982
+ odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--description <text>|--body <text>|--proof <text>] [--mutation-id <id>]
5938
5983
  odla-ai pm <goal|task|decision|bug> get <id> [--json]
5939
- odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>]
5940
- odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>]
5941
- odla-ai pm <goal|task|decision|bug> comment <id> --body "..."
5984
+ odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>] [--mutation-id <id>]
5985
+ odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>] [--mutation-id <id>]
5986
+ odla-ai pm <goal|task|decision|bug> comment <id> --body "..." [--mutation-id <id>]
5942
5987
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
5943
5988
  odla-ai pm <goal|task|decision|bug> rm <id>
5944
5989
  odla-ai pm handoff --app <id> [--json]
5945
5990
  odla-ai discuss groups [--json]
5946
5991
  odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
5947
5992
  odla-ai discuss read <topic> [--json]
5948
- odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"]
5949
- odla-ai discuss reply <topic> --body "..." [--markup "..."]
5950
- odla-ai discuss resolve <topic> [--reopen]
5993
+ odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"] [--mutation-id <id>]
5994
+ odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
5995
+ odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
5951
5996
  odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
5952
5997
  odla-ai discuss watch [<topic>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json]
5998
+ odla-ai o11y status [--app <id>] [--env prod] [--minutes 60] [--json]
5953
5999
  odla-ai whoami [--json]
5954
6000
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
5955
6001
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
@@ -6061,6 +6107,11 @@ Commands:
6061
6107
  (not 1) when it times out with nothing new, so a script can tell
6062
6108
  "no answer yet" from a failure and just rerun. Use "who" to look
6063
6109
  up the exact @[Label](kind/id) markup for a mention.
6110
+ o11y Read one stable status envelope for application RED, current
6111
+ live-sync load/freshness, the protected commit-to-visible
6112
+ canary, collector ingest/scheduler trust, Cloudflare-owned
6113
+ runtime metrics, and a machine verdict.
6114
+ --json keeps auth progress on stderr for unattended agents.
6064
6115
  provision Register services, compose integrations, persist credentials, optionally push secrets.
6065
6116
  smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
6066
6117
  skill Same installer; --agent accepts all, claude, codex, cursor,
@@ -6521,15 +6572,6 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
6521
6572
  }
6522
6573
  throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
6523
6574
  }
6524
- async function readRegistryApp(cfg, token, doFetch) {
6525
- const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}`, {
6526
- headers: { authorization: `Bearer ${token}` }
6527
- });
6528
- if (res.status === 404) return null;
6529
- if (!res.ok) return null;
6530
- const json = await res.json();
6531
- return json.app ?? null;
6532
- }
6533
6575
  async function postJson3(doFetch, url, bearer, body) {
6534
6576
  const res = await doFetch(url, {
6535
6577
  method: "POST",
@@ -6628,7 +6670,7 @@ async function provision(options) {
6628
6670
  const doFetch = options.fetch ?? fetch;
6629
6671
  const token = await getDeveloperToken(cfg, options, doFetch, out);
6630
6672
  const apps = createAppsClient({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
6631
- const existing = await readRegistryApp(cfg, token, doFetch);
6673
+ const existing = await apps.resolveApp(cfg.app.id);
6632
6674
  if (existing) {
6633
6675
  out.log(`app: ${cfg.app.id} already exists`);
6634
6676
  } else {
@@ -6815,6 +6857,7 @@ var COMMAND_SURFACE = {
6815
6857
  doctor: {},
6816
6858
  help: {},
6817
6859
  init: {},
6860
+ o11y: { status: {} },
6818
6861
  pm: {
6819
6862
  ...PM_ENTITIES,
6820
6863
  handoff: {}
@@ -8009,6 +8052,7 @@ async function codeCommand(parsed, dependencies) {
8009
8052
  }
8010
8053
 
8011
8054
  // src/discuss-actions.ts
8055
+ var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
8012
8056
  async function request(ctx, method, path, body) {
8013
8057
  const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
8014
8058
  method,
@@ -8105,7 +8149,8 @@ async function discussPost(ctx, parsed) {
8105
8149
  const created = await request(ctx, "POST", "/topics", {
8106
8150
  appId,
8107
8151
  subject,
8108
- ...content(parsed)
8152
+ ...content(parsed),
8153
+ mutationId: writeMutationId(parsed)
8109
8154
  });
8110
8155
  emit(ctx, created, () => ctx.out.log(`opened topic ${created.id}`));
8111
8156
  }
@@ -8114,16 +8159,16 @@ async function discussReply(ctx, id, parsed) {
8114
8159
  ctx,
8115
8160
  "POST",
8116
8161
  `/topics/${encodeURIComponent(id)}/replies`,
8117
- content(parsed)
8162
+ { ...content(parsed), mutationId: writeMutationId(parsed) }
8118
8163
  );
8119
8164
  emit(ctx, created, () => ctx.out.log(`replied ${created.id}`));
8120
8165
  }
8121
- async function discussResolve(ctx, id, resolved) {
8166
+ async function discussResolve(ctx, id, resolved, parsed) {
8122
8167
  const result = await request(
8123
8168
  ctx,
8124
8169
  "PATCH",
8125
8170
  `/topics/${encodeURIComponent(id)}`,
8126
- { resolved }
8171
+ { resolved, mutationId: writeMutationId(parsed) }
8127
8172
  );
8128
8173
  emit(ctx, result, () => ctx.out.log(`${resolved ? "resolved" : "reopened"} ${result.id}`));
8129
8174
  }
@@ -8236,7 +8281,8 @@ var ALLOWED = [
8236
8281
  "by",
8237
8282
  "self",
8238
8283
  "interval",
8239
- "timeout"
8284
+ "timeout",
8285
+ "mutation-id"
8240
8286
  ];
8241
8287
  function requireId(id, action) {
8242
8288
  if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
@@ -8278,7 +8324,7 @@ async function discussCommand(parsed, deps = {}) {
8278
8324
  case "reply":
8279
8325
  return discussReply(ctx, requireId(id, "reply"), parsed);
8280
8326
  case "resolve":
8281
- return discussResolve(ctx, requireId(id, "resolve"), parsed.options.reopen !== true);
8327
+ return discussResolve(ctx, requireId(id, "resolve"), parsed.options.reopen !== true, parsed);
8282
8328
  case "who":
8283
8329
  return discussWho(ctx, parsed);
8284
8330
  case "watch": {
@@ -8292,6 +8338,7 @@ async function discussCommand(parsed, deps = {}) {
8292
8338
  }
8293
8339
 
8294
8340
  // src/pm-actions.ts
8341
+ var writeMutationId2 = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
8295
8342
  var FIELD_MAP = {
8296
8343
  title: { key: "title" },
8297
8344
  status: { key: "status" },
@@ -8393,25 +8440,35 @@ async function pmAdd(ctx, entity, parsed) {
8393
8440
  if (!input.title) throw new Error("pm add needs --title <title>");
8394
8441
  if (entity === "bug" && !input.description)
8395
8442
  throw new Error("pm bug add needs --description <context> (or --body <context>)");
8396
- const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
8443
+ const res = await pmRequest(ctx, "POST", `/${entity}`, {
8444
+ appId,
8445
+ input,
8446
+ mutationId: writeMutationId2(parsed)
8447
+ });
8397
8448
  emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
8398
8449
  }
8399
8450
  async function pmGet(ctx, entity, id) {
8400
- const { record: record5 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
8401
- emit2(ctx, record5, () => printRecord(ctx, entity, record5));
8451
+ const { record: record6 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
8452
+ emit2(ctx, record6, () => printRecord(ctx, entity, record6));
8402
8453
  }
8403
8454
  async function pmSet(ctx, entity, id, parsed) {
8404
8455
  const patch2 = collectEntityFields(entity, parsed, true);
8405
8456
  if (Object.keys(patch2).length === 0)
8406
8457
  throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
8407
- const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
8458
+ const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, {
8459
+ patch: patch2,
8460
+ mutationId: writeMutationId2(parsed)
8461
+ });
8408
8462
  emit2(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
8409
8463
  }
8410
8464
  async function pmDone(ctx, entity, id, parsed) {
8411
8465
  const decisionId = stringOpt(parsed.options.decision);
8412
8466
  if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
8413
8467
  const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
8414
- const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
8468
+ const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, {
8469
+ patch: patch2,
8470
+ mutationId: writeMutationId2(parsed)
8471
+ });
8415
8472
  emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
8416
8473
  }
8417
8474
  async function allRecords(ctx, entity, appId) {
@@ -8437,9 +8494,9 @@ async function pmHandoff(ctx, parsed) {
8437
8494
  ]);
8438
8495
  const handoff = {
8439
8496
  appId,
8440
- unmetGoals: goals.filter((record5) => record5.status !== "met"),
8441
- activeTasks: tasks.filter((record5) => record5.column !== "done"),
8442
- openBugs: bugs.filter((record5) => record5.status !== "fixed" && record5.status !== "wontfix")
8497
+ unmetGoals: goals.filter((record6) => record6.status !== "met"),
8498
+ activeTasks: tasks.filter((record6) => record6.column !== "done"),
8499
+ openBugs: bugs.filter((record6) => record6.status !== "fixed" && record6.status !== "wontfix")
8443
8500
  };
8444
8501
  const result = {
8445
8502
  ...handoff,
@@ -8458,10 +8515,10 @@ async function pmHandoff(ctx, parsed) {
8458
8515
  ]) {
8459
8516
  ctx.out.log(`${label}:`);
8460
8517
  if (!records.length) ctx.out.log("- (none)");
8461
- else for (const record5 of records) printRecord(
8518
+ else for (const record6 of records) printRecord(
8462
8519
  ctx,
8463
8520
  label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
8464
- record5
8521
+ record6
8465
8522
  );
8466
8523
  }
8467
8524
  });
@@ -8469,7 +8526,10 @@ async function pmHandoff(ctx, parsed) {
8469
8526
  async function pmComment(ctx, entity, id, parsed) {
8470
8527
  const body = stringOpt(parsed.options.body);
8471
8528
  if (!body) throw new Error('pm comment needs --body "..."');
8472
- await pmRequest(ctx, "POST", `/${entity}/${encodeURIComponent(id)}/comments`, { body });
8529
+ await pmRequest(ctx, "POST", `/${entity}/${encodeURIComponent(id)}/comments`, {
8530
+ body,
8531
+ mutationId: writeMutationId2(parsed)
8532
+ });
8473
8533
  ctx.out.log(`commented on ${entity} ${id}`);
8474
8534
  }
8475
8535
  async function pmComments(ctx, entity, id) {
@@ -8519,7 +8579,8 @@ var ALLOWED2 = [
8519
8579
  "decision",
8520
8580
  "limit",
8521
8581
  "offset",
8522
- "q"
8582
+ "q",
8583
+ "mutation-id"
8523
8584
  ];
8524
8585
  function requireId2(id, action) {
8525
8586
  if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
@@ -8576,6 +8637,234 @@ async function pmCommand(parsed, deps = {}) {
8576
8637
  }
8577
8638
  }
8578
8639
 
8640
+ // src/o11y-verdict.ts
8641
+ function statusVerdict(reads) {
8642
+ const reasons = [];
8643
+ sourceHttp(reasons, "application", reads.application);
8644
+ sourceHttp(reasons, "liveSync", reads.liveSync);
8645
+ sourceHttp(reasons, "canary", reads.canary);
8646
+ sourceHttp(reasons, "collector", reads.collector);
8647
+ sourceHttp(reasons, "provider", reads.provider);
8648
+ const liveStatus = String(reads.liveSync.body.status ?? "");
8649
+ if (liveStatus === "partial") {
8650
+ reasons.push({
8651
+ source: "liveSync",
8652
+ code: "partial_reconciliation",
8653
+ severity: "degraded"
8654
+ });
8655
+ } else if (liveStatus === "stale" || liveStatus === "unavailable") {
8656
+ reasons.push({
8657
+ source: "liveSync",
8658
+ code: `reconciliation_${liveStatus}`,
8659
+ severity: "degraded"
8660
+ });
8661
+ }
8662
+ const canaryStatus = String(reads.canary.body.status ?? "");
8663
+ if (canaryStatus === "failing") {
8664
+ reasons.push({
8665
+ source: "canary",
8666
+ code: String(reads.canary.body.errorCode ?? "journey_failed"),
8667
+ severity: "unhealthy"
8668
+ });
8669
+ } else if (canaryStatus === "stale") {
8670
+ reasons.push({
8671
+ source: "canary",
8672
+ code: "journey_stale",
8673
+ severity: "unhealthy"
8674
+ });
8675
+ } else if (canaryStatus === "pending" || canaryStatus === "not_configured") {
8676
+ reasons.push({
8677
+ source: "canary",
8678
+ code: `journey_${canaryStatus}`,
8679
+ severity: "degraded"
8680
+ });
8681
+ }
8682
+ const collectorStatus = String(reads.collector.body.status ?? "");
8683
+ if (collectorStatus === "unhealthy") {
8684
+ reasons.push({
8685
+ source: "collector",
8686
+ code: collectorReason(reads.collector, "collector_unhealthy"),
8687
+ severity: "unhealthy"
8688
+ });
8689
+ } else if (collectorStatus === "degraded") {
8690
+ reasons.push({
8691
+ source: "collector",
8692
+ code: collectorReason(reads.collector, "collector_degraded"),
8693
+ severity: "degraded"
8694
+ });
8695
+ }
8696
+ const providerStatus = String(reads.provider.body.status ?? "");
8697
+ if (providerStatus && providerStatus !== "observed") {
8698
+ reasons.push({
8699
+ source: "provider",
8700
+ code: `provider_${providerStatus}`,
8701
+ severity: "degraded"
8702
+ });
8703
+ }
8704
+ return {
8705
+ status: reasons.some((reason) => reason.severity === "unhealthy") ? "unhealthy" : reasons.length > 0 ? "degraded" : "healthy",
8706
+ reasons
8707
+ };
8708
+ }
8709
+ function collectorReason(read3, fallback) {
8710
+ const reasons = read3.body.reasons;
8711
+ if (!Array.isArray(reasons)) return fallback;
8712
+ const first = reasons.find(
8713
+ (reason) => Boolean(reason) && typeof reason === "object" && !Array.isArray(reason)
8714
+ );
8715
+ return typeof first?.code === "string" && first.code ? first.code : fallback;
8716
+ }
8717
+ function sourceHttp(reasons, source, read3) {
8718
+ if (read3.httpStatus >= 200 && read3.httpStatus < 300) return;
8719
+ reasons.push({
8720
+ source,
8721
+ code: `http_${read3.httpStatus}`,
8722
+ severity: read3.httpStatus >= 500 && read3.httpStatus !== 501 ? "unhealthy" : "degraded"
8723
+ });
8724
+ }
8725
+
8726
+ // src/o11y-command.ts
8727
+ async function o11yCommand(parsed, deps = {}) {
8728
+ assertArgs(
8729
+ parsed,
8730
+ ["config", "token", "email", "json", "app", "env", "minutes"],
8731
+ 2
8732
+ );
8733
+ const action = parsed.positionals[1];
8734
+ if (action !== "status") {
8735
+ throw new Error(
8736
+ `unknown o11y action "${action ?? ""}". Try "odla-ai o11y status --json".`
8737
+ );
8738
+ }
8739
+ const minutes = statusMinutes(
8740
+ numberOpt(parsed.options.minutes, "--minutes") ?? 60
8741
+ );
8742
+ const cfg = await loadProjectConfig(
8743
+ stringOpt(parsed.options.config) ?? "odla.config.mjs"
8744
+ );
8745
+ const doFetch = deps.fetch ?? fetch;
8746
+ const out = deps.stdout ?? console;
8747
+ const token = await getDeveloperToken(
8748
+ cfg,
8749
+ {
8750
+ configPath: cfg.configPath,
8751
+ token: stringOpt(parsed.options.token),
8752
+ email: stringOpt(parsed.options.email),
8753
+ open: false
8754
+ },
8755
+ doFetch,
8756
+ out
8757
+ );
8758
+ const appId = stringOpt(parsed.options.app)?.trim() || cfg.app.id;
8759
+ const env = stringOpt(parsed.options.env)?.trim() || "prod";
8760
+ const base = `${cfg.platformUrl}/o11y/${encodeURIComponent(appId)}`;
8761
+ const query = new URLSearchParams({ env, minutes: String(minutes) });
8762
+ const headers = { authorization: `Bearer ${token}` };
8763
+ const [application, liveSync, canary, collector, provider] = await Promise.all([
8764
+ read2(`${base}/metrics/red?${query}`, headers, doFetch),
8765
+ read2(
8766
+ `${base}/live-sync/health?${new URLSearchParams({ env })}`,
8767
+ headers,
8768
+ doFetch
8769
+ ),
8770
+ read2(
8771
+ `${base}/live-sync/canary?${new URLSearchParams({ env })}`,
8772
+ headers,
8773
+ doFetch
8774
+ ),
8775
+ read2(`${base}/self-health?${query}`, headers, doFetch),
8776
+ read2(`${base}/provider/cloudflare?${query}`, headers, doFetch)
8777
+ ]);
8778
+ const verdict = statusVerdict({
8779
+ application,
8780
+ liveSync,
8781
+ canary,
8782
+ collector,
8783
+ provider
8784
+ });
8785
+ const status = {
8786
+ schemaVersion: 3,
8787
+ scope: { appId, env, minutes },
8788
+ observedAt: (/* @__PURE__ */ new Date()).toISOString(),
8789
+ verdict,
8790
+ application,
8791
+ liveSync,
8792
+ canary,
8793
+ collector,
8794
+ provider
8795
+ };
8796
+ if (parsed.options.json === true) {
8797
+ out.log(JSON.stringify(status, null, 2));
8798
+ return;
8799
+ }
8800
+ printStatus2(status, out);
8801
+ }
8802
+ function statusMinutes(value) {
8803
+ if (!Number.isSafeInteger(value) || value < 1 || value > 7 * 24 * 60) {
8804
+ throw new Error("--minutes must be an integer from 1 to 10080");
8805
+ }
8806
+ return value;
8807
+ }
8808
+ async function read2(url, headers, doFetch) {
8809
+ const response2 = await doFetch(url, { headers });
8810
+ const text = await response2.text();
8811
+ let body = {};
8812
+ if (text) {
8813
+ try {
8814
+ const value = JSON.parse(text);
8815
+ body = value && typeof value === "object" && !Array.isArray(value) ? value : { value };
8816
+ } catch {
8817
+ body = { message: text.slice(0, 300) };
8818
+ }
8819
+ }
8820
+ return { httpStatus: response2.status, body };
8821
+ }
8822
+ function printStatus2(status, out) {
8823
+ out.log(
8824
+ `o11y status ${status.scope.appId}/${status.scope.env} (${status.scope.minutes}m)`
8825
+ );
8826
+ const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record5) : [];
8827
+ const requests = routes.reduce(
8828
+ (total, row) => total + numeric2(row.requests),
8829
+ 0
8830
+ );
8831
+ const errors = routes.reduce(
8832
+ (total, row) => total + numeric2(row.errors),
8833
+ 0
8834
+ );
8835
+ out.log(
8836
+ `application ${status.application.httpStatus} ${requests} requests ${errors} errors`
8837
+ );
8838
+ out.log(
8839
+ `live-sync ${status.liveSync.httpStatus} ${String(status.liveSync.body.status ?? status.liveSync.body.error ?? "unavailable")} ${numeric2(status.liveSync.body.activeConnections)} active`
8840
+ );
8841
+ const canaryDurations = record5(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
8842
+ out.log(
8843
+ `canary ${status.canary.httpStatus} ${String(status.canary.body.status ?? status.canary.body.error ?? "unavailable")} ${optionalNumeric(canaryDurations.publishToVisibleMs)} publish-to-visible`
8844
+ );
8845
+ const collectorIngest = record5(status.collector.body.ingest) ? status.collector.body.ingest : {};
8846
+ const collectorStorage = record5(collectorIngest.storage) ? collectorIngest.storage : {};
8847
+ out.log(
8848
+ `collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric2(collectorStorage.affectedPoints)} affected points`
8849
+ );
8850
+ const providerMetrics = record5(status.provider.body.metrics) ? status.provider.body.metrics : {};
8851
+ out.log(
8852
+ `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric2(providerMetrics.requests)} invocations ${numeric2(providerMetrics.errors)} runtime errors`
8853
+ );
8854
+ out.log(
8855
+ `verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
8856
+ );
8857
+ }
8858
+ function record5(value) {
8859
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
8860
+ }
8861
+ function numeric2(value) {
8862
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
8863
+ }
8864
+ function optionalNumeric(value) {
8865
+ return typeof value === "number" && Number.isFinite(value) ? `${value} ms` : "\u2014";
8866
+ }
8867
+
8579
8868
  // src/record.ts
8580
8869
  import { appendFileSync } from "fs";
8581
8870
  import process9 from "process";
@@ -9002,7 +9291,7 @@ var SOURCE2 = /\.(ts|tsx|js|jsx|mts|cts)$/;
9002
9291
  function gitRunner(cwd) {
9003
9292
  return (args) => execFileSync2("git", args, { cwd, encoding: "utf8", maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
9004
9293
  }
9005
- function collectDiff(runGit, base, read2) {
9294
+ function collectDiff(runGit, base, read3) {
9006
9295
  let merged = "";
9007
9296
  try {
9008
9297
  merged += runGit(["diff", "--unified=3", `${base}...HEAD`]);
@@ -9015,9 +9304,9 @@ function collectDiff(runGit, base, read2) {
9015
9304
  merged += runGit(["diff", "--unified=3", "HEAD"]);
9016
9305
  } catch {
9017
9306
  }
9018
- return merged + untrackedDiff(runGit, read2);
9307
+ return merged + untrackedDiff(runGit, read3);
9019
9308
  }
9020
- function untrackedDiff(runGit, read2) {
9309
+ function untrackedDiff(runGit, read3) {
9021
9310
  let listed = "";
9022
9311
  try {
9023
9312
  listed = runGit(["ls-files", "--others", "--exclude-standard"]);
@@ -9033,7 +9322,7 @@ function untrackedDiff(runGit, read2) {
9033
9322
  if (!SOURCE2.test(path)) continue;
9034
9323
  let body;
9035
9324
  try {
9036
- body = read2(path);
9325
+ body = read3(path);
9037
9326
  } catch {
9038
9327
  continue;
9039
9328
  }
@@ -9115,8 +9404,8 @@ function report3(ctx, impacts) {
9115
9404
  async function runbookImpact(ctx, options, deps = {}) {
9116
9405
  const cwd = deps.cwd ?? process.cwd();
9117
9406
  const runGit = deps.runGit ?? gitRunner(cwd);
9118
- const read2 = deps.readRepoFile ?? ((path) => readFileSync10(join10(cwd, path), "utf8"));
9119
- const surfaces = changedSurfaces(collectDiff(runGit, options.base, read2), manifestLabeller(cwd));
9407
+ const read3 = deps.readRepoFile ?? ((path) => readFileSync10(join10(cwd, path), "utf8"));
9408
+ const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
9120
9409
  if (!surfaces.length) {
9121
9410
  return ctx.out.log(
9122
9411
  ctx.json ? JSON.stringify({ base: options.base, impacts: [] }, null, 2) : `no changes against ${options.base}`
@@ -9981,33 +10270,37 @@ function exitCodeFor(err) {
9981
10270
  return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
9982
10271
  }
9983
10272
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
10273
+ const runtime = {
10274
+ ...dependencies,
10275
+ stdout: redactingOutput(dependencies.stdout ?? console)
10276
+ };
9984
10277
  const parsed = parseArgv(argv);
9985
10278
  recordInvocation(parsed);
9986
10279
  const command = parsed.positionals[0] ?? "help";
9987
10280
  if (command === "version" || command === "-v" || parsed.options.version === true) {
9988
10281
  assertArgs(parsed, ["version"], 1);
9989
- console.log(cliVersion());
10282
+ runtime.stdout.log(cliVersion());
9990
10283
  return;
9991
10284
  }
9992
10285
  if (command === "help" || command === "--help" || command === "-h") {
9993
10286
  assertArgs(parsed, ["help"], 1);
9994
- printHelp();
10287
+ printHelp(runtime.stdout);
9995
10288
  return;
9996
10289
  }
9997
10290
  if (command === "whoami") {
9998
- await whoamiCommand(parsed, dependencies);
10291
+ await whoamiCommand(parsed, runtime);
9999
10292
  return;
10000
10293
  }
10001
10294
  if (command === "runbook") {
10002
- await runbookCommand(parsed, dependencies);
10295
+ await runbookCommand(parsed, runtime);
10003
10296
  return;
10004
10297
  }
10005
10298
  if (command === "calendar") {
10006
- await calendarCommand(parsed, dependencies);
10299
+ await calendarCommand(parsed, runtime);
10007
10300
  return;
10008
10301
  }
10009
10302
  if (command === "code") {
10010
- await codeCommand(parsed, dependencies);
10303
+ await codeCommand(parsed, runtime);
10011
10304
  return;
10012
10305
  }
10013
10306
  if (command === "admin") {
@@ -10015,26 +10308,30 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
10015
10308
  return;
10016
10309
  }
10017
10310
  if (command === "app") {
10018
- await appCommand(parsed, dependencies);
10311
+ await appCommand(parsed, runtime);
10019
10312
  return;
10020
10313
  }
10021
10314
  if (command === "security") {
10022
- await securityCommand(parsed, dependencies);
10315
+ await securityCommand(parsed, runtime);
10023
10316
  return;
10024
10317
  }
10025
10318
  if (command === "pm") {
10026
- await pmCommand(parsed, dependencies);
10319
+ await pmCommand(parsed, runtime);
10027
10320
  return;
10028
10321
  }
10029
10322
  if (command === "discuss") {
10030
- await discussCommand(parsed, dependencies);
10323
+ await discussCommand(parsed, runtime);
10324
+ return;
10325
+ }
10326
+ if (command === "o11y") {
10327
+ await o11yCommand(parsed, runtime);
10031
10328
  return;
10032
10329
  }
10033
10330
  if (command === "provision") {
10034
- await provisionCommand(parsed, dependencies);
10331
+ await provisionCommand(parsed, runtime);
10035
10332
  return;
10036
10333
  }
10037
- if (await projectCommand(command, parsed, dependencies)) return;
10334
+ if (await projectCommand(command, parsed, runtime)) return;
10038
10335
  throw new Error(`unknown command "${command}". Run "odla-ai help".`);
10039
10336
  }
10040
10337
  async function provisionCommand(parsed, dependencies) {
@@ -10149,4 +10446,4 @@ export {
10149
10446
  exitCodeFor,
10150
10447
  runCli
10151
10448
  };
10152
- //# sourceMappingURL=chunk-MX6FSJUN.js.map
10449
+ //# sourceMappingURL=chunk-MWLZO2EZ.js.map