@odla-ai/cli 0.25.12 → 0.25.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -138,7 +138,7 @@ import process2 from "process";
138
138
  async function openUrl(url, options = {}) {
139
139
  const command = openerFor(options.platform ?? process2.platform);
140
140
  const doSpawn = options.spawnImpl ?? spawn;
141
- await new Promise((resolve11, reject) => {
141
+ await new Promise((resolve12, reject) => {
142
142
  const child = doSpawn(command.cmd, [...command.args, url], {
143
143
  stdio: "ignore",
144
144
  detached: true
@@ -146,7 +146,7 @@ async function openUrl(url, options = {}) {
146
146
  child.once("error", reject);
147
147
  child.once("spawn", () => {
148
148
  child.unref();
149
- resolve11();
149
+ resolve12();
150
150
  });
151
151
  });
152
152
  }
@@ -1414,8 +1414,8 @@ function credential(value) {
1414
1414
  // src/calendar-poll.ts
1415
1415
  async function waitForCalendarPoll(milliseconds, signal) {
1416
1416
  if (signal?.aborted) throw signal.reason ?? new Error("calendar connection aborted");
1417
- await new Promise((resolve11, reject) => {
1418
- const timer = setTimeout(resolve11, milliseconds);
1417
+ await new Promise((resolve12, reject) => {
1418
+ const timer = setTimeout(resolve12, milliseconds);
1419
1419
  signal?.addEventListener("abort", () => {
1420
1420
  clearTimeout(timer);
1421
1421
  reject(signal.reason ?? new Error("calendar connection aborted"));
@@ -1616,7 +1616,8 @@ var CAPABILITIES = {
1616
1616
  "compose declared app-capability schema/rules, create guarded seeds when absent, and configure platform AI, auth, and deployment links",
1617
1617
  "apply Google Calendar booking config, then drive state-bound consent, status, discovery, and disconnect flows (bookings run live through the platform proxy)",
1618
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, exact Worker versions observed in traffic, current live-sync freshness/load, the protected commit-to-visible canary, collector ingest/scheduler trust, provider-owned runtime metrics, and a bounded machine verdict",
1619
+ "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",
1620
+ "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",
1620
1621
  "inspect durable agent wakeups as a versioned JSON envelope and explicitly requeue one dead-lettered job with a scoped environment credential",
1621
1622
  "run app-attributed hosted security discovery and independent validation without provider keys",
1622
1623
  "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
@@ -2822,8 +2823,8 @@ function hostedPollTimeout(value = 10 * 6e4) {
2822
2823
  }
2823
2824
  async function waitForHostedPoll(milliseconds, signal) {
2824
2825
  if (signal?.aborted) throw signal.reason ?? new DOMException("aborted", "AbortError");
2825
- await new Promise((resolve11, reject) => {
2826
- const timer = setTimeout(resolve11, milliseconds);
2826
+ await new Promise((resolve12, reject) => {
2827
+ const timer = setTimeout(resolve12, milliseconds);
2827
2828
  signal?.addEventListener("abort", () => {
2828
2829
  clearTimeout(timer);
2829
2830
  reject(signal.reason ?? new DOMException("aborted", "AbortError"));
@@ -4843,21 +4844,21 @@ async function verifyCodeCandidate(input) {
4843
4844
  const recipes = [];
4844
4845
  const logs = [];
4845
4846
  for (const recipe2 of policy.recipes) {
4846
- const clean = await stageWorkspace(staged.workspaceDir, limits);
4847
+ const clean4 = await stageWorkspace(staged.workspaceDir, limits);
4847
4848
  try {
4848
- if (await digestStagedWorkspace(clean.workspaceDir, limits) !== sourceDigest) {
4849
+ if (await digestStagedWorkspace(clean4.workspaceDir, limits) !== sourceDigest) {
4849
4850
  throw new TypeError("clean verifier source changed before execution");
4850
4851
  }
4851
4852
  const result = checkedResult(await input.recipeExecutor.run({
4852
- workspaceDir: clean.workspaceDir,
4853
+ workspaceDir: clean4.workspaceDir,
4853
4854
  recipe: recipe2,
4854
4855
  signal: input.signal
4855
4856
  }), recipe2.maxOutputBytes);
4856
- const artifacts = await inspectArtifacts(clean.workspaceDir, recipe2);
4857
+ const artifacts = await inspectArtifacts(clean4.workspaceDir, recipe2);
4857
4858
  recipes.push(recipeReceipt(recipe2, result, artifacts));
4858
4859
  logs.push({ recipeId: recipe2.id, ...boundedLogs(result, recipe2.maxOutputBytes) });
4859
4860
  } finally {
4860
- await clean.cleanup();
4861
+ await clean4.cleanup();
4861
4862
  }
4862
4863
  }
4863
4864
  const fields = {
@@ -5979,11 +5980,21 @@ Usage:
5979
5980
  odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
5980
5981
  odla-ai app owners add <email> [--email <odla-account>] [--json]
5981
5982
  odla-ai app owners remove <email> [--email <odla-account>] [--json]
5982
- odla-ai pm <goal|task|decision|bug> list [--app <id>] [--status <s>] [--severity <s>] [--column <c>] [--q <text>] [--json]
5983
- 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>]
5983
+ odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
5984
+ odla-ai pm task list [--app <id>] [--column <c>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
5985
+ odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
5986
+ odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
5987
+ odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
5988
+ odla-ai pm task add --app <id> --title <t> [--column <c>] [--goal <id>] [--assignee <id>] [--description <text>|--body <text>] [--due <epoch-ms>] [--mutation-id <id>] [--json]
5989
+ odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
5990
+ odla-ai pm bug add --app <id> --title <t> (--description <text>|--body <text>) [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--mutation-id <id>] [--json]
5984
5991
  odla-ai pm <goal|task|decision|bug> get <id> [--json]
5985
- odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>] [--mutation-id <id>]
5986
- odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>] [--mutation-id <id>]
5992
+ odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
5993
+ odla-ai pm task set <id> [--title <t>|--column <c>|--rank <n>|--goal <id>|--no-goal|--assignee <id>|--no-assignee|--description <text>|--body <text>|--due <epoch-ms>|--no-due] [--mutation-id <id>] [--json]
5994
+ odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
5995
+ odla-ai pm bug set <id> [--title <t>|--status <s>|--severity <s>|--goal <id>|--no-goal|--assignee <id>|--no-assignee|--decision <id>|--no-decision|--description <text>|--body <text>] [--mutation-id <id>] [--json]
5996
+ odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
5997
+ odla-ai pm bug done <id> [--decision <accepted-decision-id>] [--mutation-id <id>]
5987
5998
  odla-ai pm <goal|task|decision|bug> comment <id> --body "..." [--mutation-id <id>]
5988
5999
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
5989
6000
  odla-ai pm <goal|task|decision|bug> rm <id>
@@ -5998,8 +6009,12 @@ Usage:
5998
6009
  odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
5999
6010
  odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
6000
6011
  odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
6001
- odla-ai o11y status [--app <id>] [--env prod] [--minutes 60] [--json]
6002
- odla-ai whoami [--json]
6012
+ odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
6013
+ odla-ai context list [--json]
6014
+ odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
6015
+ odla-ai context remove <name> --yes [--json]
6016
+ odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
6017
+ odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
6003
6018
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
6004
6019
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
6005
6020
  odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
@@ -6064,6 +6079,11 @@ Commands:
6064
6079
  whoami Report who this terminal is authenticated as, and whether it holds
6065
6080
  platform admin. A handshake-minted device token is never admin,
6066
6081
  however the human who approved it is configured.
6082
+ context Explain selected config, platform, app, environment, and
6083
+ developer-token provenance without printing credentials or
6084
+ starting a device handshake. Operator work can run outside a
6085
+ project checkout with explicit flags or ODLA_* environment
6086
+ variables.
6067
6087
  setup Install offline odla runbooks for common coding-agent harnesses.
6068
6088
  init Create a generic odla.config.mjs plus starter schema/rules files.
6069
6089
  doctor Validate and summarize the project config without network calls.
@@ -6135,6 +6155,15 @@ Safety:
6135
6155
  Provision caches the approved developer token and service credentials under
6136
6156
  .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
6137
6157
  preflights Wrangler before any shown-once issuance or destructive rotation.
6158
+ Projectless PM, Discussions, o11y, runbook, and identity commands use
6159
+ --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
6160
+ ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
6161
+ select it explicitly with --context or ODLA_CONTEXT. Each named context gets
6162
+ isolated developer and scoped-token caches. Override their locations with
6163
+ ODLA_DEV_TOKEN_FILE and ODLA_ADMIN_TOKEN_FILE; ODLA_CONTEXT_FILE relocates
6164
+ the metadata file. Flags and specific ODLA_* scope variables beat a selected
6165
+ context, which beats project config. There is no ambient current context.
6166
+ "context show" reports only provenance and cache state and never authenticates.
6138
6167
  Provision opens the approval page in your browser automatically whenever the
6139
6168
  machine can show one, including agent-driven runs; only CI, SSH, and
6140
6169
  display-less hosts skip it. Use --open to force or --no-open to suppress.
@@ -6848,6 +6877,7 @@ var COMMAND_SURFACE = {
6848
6877
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
6849
6878
  capabilities: {},
6850
6879
  code: { connect: {} },
6880
+ context: { show: {}, list: {}, save: {}, remove: {} },
6851
6881
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
6852
6882
  discuss: {
6853
6883
  groups: {},
@@ -7254,8 +7284,8 @@ function parseArgv(argv) {
7254
7284
  }
7255
7285
  return { positionals, options };
7256
7286
  }
7257
- function assertArgs(parsed, allowedOptions, maxPositionals) {
7258
- const allowed = new Set(allowedOptions);
7287
+ function assertArgs(parsed, allowedOptions2, maxPositionals) {
7288
+ const allowed = new Set(allowedOptions2);
7259
7289
  for (const name of Object.keys(parsed.options)) {
7260
7290
  if (!allowed.has(name)) throw new Error(`unknown option "--${name}"; run "odla-ai help" for supported options`);
7261
7291
  }
@@ -8135,6 +8165,384 @@ async function codeCommand(parsed, dependencies) {
8135
8165
  });
8136
8166
  }
8137
8167
 
8168
+ // src/operator-credentials.ts
8169
+ import process9 from "process";
8170
+ function developerTokenStatus(context, parsed, now = Date.now()) {
8171
+ const cached = readJsonFile(context.cfg.local.tokenFile);
8172
+ const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
8173
+ const source = clean(
8174
+ stringOpt(parsed.options.token)
8175
+ ) ? "flag" : clean(process9.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
8176
+ return {
8177
+ source,
8178
+ cacheFile: context.cfg.local.tokenFile,
8179
+ cacheStatus
8180
+ };
8181
+ }
8182
+ function clean(value) {
8183
+ const normalized = value?.trim();
8184
+ return normalized || void 0;
8185
+ }
8186
+
8187
+ // src/operator-context.ts
8188
+ import { existsSync as existsSync10 } from "fs";
8189
+ import { join as join10, resolve as resolve11 } from "path";
8190
+ import process11 from "process";
8191
+
8192
+ // src/operator-profiles.ts
8193
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
8194
+ import { homedir as homedir2 } from "os";
8195
+ import { dirname as dirname7, join as join9, resolve as resolve10 } from "path";
8196
+ import process10 from "process";
8197
+ function operatorProfileFile() {
8198
+ return resolve10(
8199
+ clean2(process10.env.ODLA_CONTEXT_FILE) ?? join9(homedir2(), ".odla", "contexts.json")
8200
+ );
8201
+ }
8202
+ function resolveOperatorProfile(parsed) {
8203
+ const fromFlag = clean2(stringOpt(parsed.options.context));
8204
+ const fromEnvironment = clean2(process10.env.ODLA_CONTEXT);
8205
+ const name = fromFlag ?? fromEnvironment ?? null;
8206
+ const file = operatorProfileFile();
8207
+ if (!name) {
8208
+ return { name: null, source: "unresolved", file, value: null };
8209
+ }
8210
+ assertOperatorName(name, "context");
8211
+ const profiles = readOperatorProfiles(file);
8212
+ const value = Object.hasOwn(profiles, name) ? profiles[name] : void 0;
8213
+ if (!value) {
8214
+ throw new Error(
8215
+ `operator context "${name}" not found in ${file}; run "odla-ai context list" or save it first`
8216
+ );
8217
+ }
8218
+ return {
8219
+ name,
8220
+ source: fromFlag ? "flag" : "environment",
8221
+ file,
8222
+ value
8223
+ };
8224
+ }
8225
+ function listOperatorProfiles(file = operatorProfileFile()) {
8226
+ return Object.entries(readOperatorProfiles(file)).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({ name, ...profile }));
8227
+ }
8228
+ function saveOperatorProfile(name, profile, file = operatorProfileFile()) {
8229
+ assertOperatorName(name, "context");
8230
+ const normalized = validateProfile(profile, `operator context "${name}"`);
8231
+ const profiles = readOperatorProfiles(file);
8232
+ profiles[name] = normalized;
8233
+ writePrivateJson(file, { schemaVersion: 1, profiles });
8234
+ }
8235
+ function removeOperatorProfile(name, file = operatorProfileFile()) {
8236
+ assertOperatorName(name, "context");
8237
+ const profiles = readOperatorProfiles(file);
8238
+ if (!Object.hasOwn(profiles, name)) return false;
8239
+ delete profiles[name];
8240
+ writePrivateJson(file, { schemaVersion: 1, profiles });
8241
+ return true;
8242
+ }
8243
+ function operatorCredentialFiles(selection) {
8244
+ const base = selection.name ? join9(dirname7(selection.file), "profiles", selection.name) : join9(homedir2(), ".odla");
8245
+ return {
8246
+ developer: join9(base, "dev-token.json"),
8247
+ scoped: join9(base, "admin-token.local.json")
8248
+ };
8249
+ }
8250
+ function assertOperatorName(value, label) {
8251
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(value)) {
8252
+ throw new Error(
8253
+ `${label} must contain lowercase letters, numbers, and hyphens`
8254
+ );
8255
+ }
8256
+ }
8257
+ function readOperatorProfiles(file) {
8258
+ if (!existsSync9(file)) return emptyProfiles();
8259
+ let raw;
8260
+ try {
8261
+ raw = JSON.parse(readFileSync8(file, "utf8"));
8262
+ } catch {
8263
+ throw new Error(`operator context file ${file} is not valid JSON`);
8264
+ }
8265
+ if (!raw || typeof raw !== "object" || raw.schemaVersion !== 1 || !raw.profiles || typeof raw.profiles !== "object" || Array.isArray(raw.profiles)) {
8266
+ throw new Error(`operator context file ${file} has an invalid shape`);
8267
+ }
8268
+ const unknown = Object.keys(raw).filter(
8269
+ (key) => !["schemaVersion", "profiles"].includes(key)
8270
+ );
8271
+ if (unknown.length > 0) {
8272
+ throw new Error(
8273
+ `operator context file ${file} contains unsupported field(s): ${unknown.sort().join(", ")}`
8274
+ );
8275
+ }
8276
+ const profiles = emptyProfiles();
8277
+ for (const [name, value] of Object.entries(
8278
+ raw.profiles
8279
+ )) {
8280
+ assertOperatorName(name, "context");
8281
+ profiles[name] = validateProfile(value, `operator context "${name}"`);
8282
+ }
8283
+ return profiles;
8284
+ }
8285
+ function emptyProfiles() {
8286
+ return /* @__PURE__ */ Object.create(null);
8287
+ }
8288
+ function validateProfile(value, label) {
8289
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
8290
+ throw new Error(`${label} has an invalid shape`);
8291
+ }
8292
+ const unknown = Object.keys(value).filter(
8293
+ (key) => !["platform", "app", "environment"].includes(key)
8294
+ );
8295
+ if (unknown.length > 0) {
8296
+ throw new Error(
8297
+ `${label} contains unsupported field(s): ${unknown.sort().join(", ")}; contexts store scope metadata only, never credentials`
8298
+ );
8299
+ }
8300
+ const candidate = value;
8301
+ if (typeof candidate.platform !== "string") {
8302
+ throw new Error(`${label} needs an absolute platform URL`);
8303
+ }
8304
+ const platform = platformAudience(candidate.platform);
8305
+ const app = clean2(candidate.app);
8306
+ const environment2 = clean2(candidate.environment);
8307
+ if (app) assertOperatorName(app, "app");
8308
+ if (environment2) assertOperatorName(environment2, "environment");
8309
+ return {
8310
+ platform,
8311
+ ...app ? { app } : {},
8312
+ ...environment2 ? { environment: environment2 } : {}
8313
+ };
8314
+ }
8315
+ function clean2(value) {
8316
+ const normalized = value?.trim();
8317
+ return normalized || void 0;
8318
+ }
8319
+
8320
+ // src/operator-context.ts
8321
+ var DEFAULT_PLATFORM2 = "https://odla.ai";
8322
+ async function resolveOperatorContext(parsed, options = {}) {
8323
+ const profile = resolveOperatorProfile(parsed);
8324
+ const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
8325
+ const configPath = resolve11(configArgument);
8326
+ const explicitConfig = parsed.options.config !== void 0;
8327
+ const hasConfig = existsSync10(configPath);
8328
+ if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
8329
+ await loadProjectConfig(configArgument);
8330
+ }
8331
+ const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
8332
+ const platformFlag = clean3(stringOpt(parsed.options.platform));
8333
+ const platformEnvironment = clean3(process11.env.ODLA_PLATFORM_URL);
8334
+ const platformValue = platformAudience(
8335
+ platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
8336
+ );
8337
+ const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
8338
+ const appFlag = clean3(stringOpt(parsed.options.app));
8339
+ const appEnvironment = clean3(process11.env.ODLA_APP_ID);
8340
+ const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
8341
+ const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
8342
+ if (appValue) assertOperatorName(appValue, "app");
8343
+ if (options.requireApp && !appValue) {
8344
+ throw new Error(
8345
+ "app context is unresolved; pass --app <id>, set ODLA_APP_ID, select --context <name>, or run inside a project with odla.config.mjs"
8346
+ );
8347
+ }
8348
+ const envFlag = clean3(stringOpt(parsed.options.env));
8349
+ const envEnvironment = clean3(process11.env.ODLA_ENV);
8350
+ const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
8351
+ const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
8352
+ if (environmentValue) {
8353
+ assertOperatorName(environmentValue, "environment");
8354
+ }
8355
+ const rootDir = loaded?.rootDir ?? process11.cwd();
8356
+ const profileCredentials = operatorCredentialFiles(profile);
8357
+ const tokenFile = clean3(process11.env.ODLA_DEV_TOKEN_FILE) ? resolve11(process11.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
8358
+ const scopedTokenFile = clean3(process11.env.ODLA_ADMIN_TOKEN_FILE) ? resolve11(process11.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? join10(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
8359
+ const cfg = loaded ? {
8360
+ ...loaded,
8361
+ platformUrl: platformValue,
8362
+ app: appValue ? {
8363
+ id: appValue,
8364
+ name: appValue === loaded.app.id ? loaded.app.name : appValue
8365
+ } : loaded.app,
8366
+ local: { ...loaded.local, tokenFile }
8367
+ } : {
8368
+ configPath,
8369
+ rootDir,
8370
+ platformUrl: platformValue,
8371
+ dbEndpoint: platformValue,
8372
+ app: {
8373
+ id: appValue ?? "operator",
8374
+ name: appValue ?? "Operator"
8375
+ },
8376
+ envs: environmentValue ? [environmentValue] : [],
8377
+ services: [],
8378
+ local: {
8379
+ tokenFile,
8380
+ credentialsFile: join10(rootDir, ".odla", "credentials.local.json"),
8381
+ devVarsFile: join10(rootDir, ".dev.vars"),
8382
+ gitignore: true
8383
+ }
8384
+ };
8385
+ return {
8386
+ cfg,
8387
+ profile: {
8388
+ name: profile.name,
8389
+ source: profile.source,
8390
+ file: profile.file
8391
+ },
8392
+ config: {
8393
+ path: configPath,
8394
+ status: loaded ? "loaded" : "absent",
8395
+ explicit: explicitConfig
8396
+ },
8397
+ platform: { value: platformValue, source: platformSource },
8398
+ app: { value: appValue, source: appSource },
8399
+ environment: {
8400
+ value: environmentValue,
8401
+ source: environmentSource
8402
+ },
8403
+ credentials: {
8404
+ developerTokenFile: tokenFile,
8405
+ scopedTokenFile
8406
+ }
8407
+ };
8408
+ }
8409
+ function clean3(value) {
8410
+ const normalized = value?.trim();
8411
+ return normalized || void 0;
8412
+ }
8413
+
8414
+ // src/context-command.ts
8415
+ async function contextCommand(parsed, deps = {}) {
8416
+ assertArgs(
8417
+ parsed,
8418
+ [
8419
+ "config",
8420
+ "context",
8421
+ "platform",
8422
+ "app",
8423
+ "env",
8424
+ "token",
8425
+ "json",
8426
+ "yes"
8427
+ ],
8428
+ 3
8429
+ );
8430
+ const action = parsed.positionals[1];
8431
+ const out = deps.stdout ?? console;
8432
+ if (action === "list") {
8433
+ const profiles = listOperatorProfiles();
8434
+ if (parsed.options.json === true) {
8435
+ out.log(JSON.stringify({ schemaVersion: 1, profiles }, null, 2));
8436
+ return;
8437
+ }
8438
+ if (profiles.length === 0) {
8439
+ out.log("No named operator contexts.");
8440
+ return;
8441
+ }
8442
+ out.log("name platform app environment");
8443
+ for (const profile of profiles) {
8444
+ out.log(
8445
+ `${profile.name} ${profile.platform} ${profile.app ?? ""} ${profile.environment ?? ""}`
8446
+ );
8447
+ }
8448
+ return;
8449
+ }
8450
+ if (action === "save") {
8451
+ if (parsed.options.token !== void 0) {
8452
+ throw new Error(
8453
+ "context save does not accept --token; contexts store scope metadata only"
8454
+ );
8455
+ }
8456
+ const name = requireName(parsed);
8457
+ const context2 = await resolveOperatorContext(parsed, {
8458
+ allowMissingConfig: true
8459
+ });
8460
+ const profile = {
8461
+ platform: context2.platform.value,
8462
+ ...context2.app.value ? { app: context2.app.value } : {},
8463
+ ...context2.environment.value ? { environment: context2.environment.value } : {}
8464
+ };
8465
+ saveOperatorProfile(name, profile, context2.profile.file);
8466
+ if (parsed.options.json === true) {
8467
+ out.log(JSON.stringify({ schemaVersion: 1, name, ...profile }, null, 2));
8468
+ } else {
8469
+ out.log(`saved operator context "${name}" (${profile.platform})`);
8470
+ }
8471
+ return;
8472
+ }
8473
+ if (action === "remove") {
8474
+ const name = requireName(parsed);
8475
+ if (parsed.options.yes !== true) {
8476
+ throw new Error(`context remove "${name}" requires --yes`);
8477
+ }
8478
+ const removed = removeOperatorProfile(name);
8479
+ const result2 = {
8480
+ schemaVersion: 1,
8481
+ name,
8482
+ removed,
8483
+ credentialCachesRemoved: false,
8484
+ grantsRevoked: false
8485
+ };
8486
+ if (parsed.options.json === true) out.log(JSON.stringify(result2, null, 2));
8487
+ else {
8488
+ const status = removed ? `removed operator context "${name}"` : `operator context "${name}" was already absent`;
8489
+ out.log(`${status}; credential caches were not deleted or revoked`);
8490
+ }
8491
+ return;
8492
+ }
8493
+ if (action !== "show") {
8494
+ throw new Error(
8495
+ `unknown context action "${action ?? ""}". Try show|list|save|remove.`
8496
+ );
8497
+ }
8498
+ const context = await resolveOperatorContext(parsed, {
8499
+ allowMissingConfig: true,
8500
+ defaultEnvironment: "prod"
8501
+ });
8502
+ const token = developerTokenStatus(context, parsed);
8503
+ const result = {
8504
+ schemaVersion: 1,
8505
+ profile: context.profile,
8506
+ config: context.config,
8507
+ platform: context.platform,
8508
+ app: context.app,
8509
+ environment: context.environment,
8510
+ authentication: {
8511
+ developerToken: token,
8512
+ scopedTokenCacheFile: context.credentials.scopedTokenFile,
8513
+ handshakeStarted: false
8514
+ }
8515
+ };
8516
+ if (parsed.options.json === true) {
8517
+ out.log(JSON.stringify(result, null, 2));
8518
+ return;
8519
+ }
8520
+ out.log(`config: ${context.config.status} (${context.config.path})`);
8521
+ out.log(
8522
+ `context: ${context.profile.name ?? "(none)"} (${context.profile.source}; ${context.profile.file})`
8523
+ );
8524
+ out.log(
8525
+ `platform: ${context.platform.value} (${context.platform.source})`
8526
+ );
8527
+ out.log(
8528
+ `app: ${context.app.value ?? "(unresolved)"} (${context.app.source})`
8529
+ );
8530
+ out.log(
8531
+ `env: ${context.environment.value ?? "(unresolved)"} (${context.environment.source})`
8532
+ );
8533
+ out.log(
8534
+ `auth: ${token.source} (cache ${token.cacheStatus}: ${token.cacheFile})`
8535
+ );
8536
+ out.log("handshake: not started");
8537
+ }
8538
+ function requireName(parsed) {
8539
+ const name = parsed.positionals[2]?.trim();
8540
+ if (!name) {
8541
+ throw new Error(`context ${parsed.positionals[1]} needs a profile name`);
8542
+ }
8543
+ return name;
8544
+ }
8545
+
8138
8546
  // src/discuss-actions.ts
8139
8547
  var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
8140
8548
  async function request(ctx, method, path, body) {
@@ -8186,8 +8594,9 @@ async function discussGroups(ctx) {
8186
8594
  }
8187
8595
  async function discussList(ctx, parsed) {
8188
8596
  const query = new URLSearchParams();
8597
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
8598
+ if (app) query.set("app", app);
8189
8599
  for (const [flag, param] of [
8190
- ["app", "app"],
8191
8600
  ["q", "q"],
8192
8601
  ["state", "state"],
8193
8602
  ["limit", "limit"],
@@ -8263,7 +8672,7 @@ function renderRead(ctx, topic, posts) {
8263
8672
  }
8264
8673
  }
8265
8674
  async function discussPost(ctx, parsed) {
8266
- const appId = stringOpt(parsed.options.app);
8675
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
8267
8676
  if (!appId) throw new Error("discuss post needs --app <appId>");
8268
8677
  const subject = stringOpt(parsed.options.subject);
8269
8678
  if (!subject) throw new Error('discuss post needs --subject "\u2026"');
@@ -8295,7 +8704,7 @@ async function discussResolve(ctx, id, resolved, parsed) {
8295
8704
  }
8296
8705
  async function discussWho(ctx, parsed) {
8297
8706
  const query = new URLSearchParams({ q: stringOpt(parsed.options.q) ?? "" });
8298
- const app = stringOpt(parsed.options.app);
8707
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
8299
8708
  if (app) query.set("app", app);
8300
8709
  const kinds = stringOpt(parsed.options.kinds);
8301
8710
  if (kinds) query.set("kinds", kinds);
@@ -8399,14 +8808,14 @@ function jsonl(ctx, parsed, value) {
8399
8808
  }
8400
8809
  async function discussWatch(ctx, topicId, parsed) {
8401
8810
  if (ctx.json && parsed.options.jsonl === true) throw new Error("--json and --jsonl cannot be combined");
8402
- const sleep = ctx.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
8811
+ const sleep = ctx.sleep ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms)));
8403
8812
  const now = ctx.now ?? Date.now;
8404
8813
  const intervalMs = (numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) ?? DEFAULT_INTERVAL_MS / 1e3) * 1e3;
8405
8814
  const timeoutSeconds = numberOpt2(parsed, "timeout");
8406
8815
  const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
8407
8816
  const by = stringOpt(parsed.options.by);
8408
8817
  const self = stringOpt(parsed.options.self);
8409
- const app = stringOpt(parsed.options.app);
8818
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
8410
8819
  let cursor = stringOpt(parsed.options.cursor);
8411
8820
  let firstSuccess = true;
8412
8821
  let consecutiveFailures = 0;
@@ -8551,14 +8960,19 @@ var ALLOWED = [
8551
8960
  "timeout",
8552
8961
  "cursor",
8553
8962
  "jsonl",
8554
- "mutation-id"
8963
+ "mutation-id",
8964
+ "platform",
8965
+ "context"
8555
8966
  ];
8556
8967
  function requireId(id, action) {
8557
8968
  if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
8558
8969
  return id;
8559
8970
  }
8560
8971
  async function buildContext(parsed, deps) {
8561
- const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
8972
+ const context = await resolveOperatorContext(parsed, {
8973
+ allowMissingConfig: true
8974
+ });
8975
+ const { cfg } = context;
8562
8976
  const doFetch = deps.fetch ?? fetch;
8563
8977
  const out = deps.stdout ?? console;
8564
8978
  const token = await getDeveloperToken(
@@ -8572,7 +8986,16 @@ async function buildContext(parsed, deps) {
8572
8986
  doFetch,
8573
8987
  out
8574
8988
  );
8575
- return { platformUrl: cfg.platformUrl, token, doFetch, out, json: parsed.options.json === true };
8989
+ return {
8990
+ platformUrl: cfg.platformUrl,
8991
+ token,
8992
+ doFetch,
8993
+ out,
8994
+ json: parsed.options.json === true,
8995
+ // Preserve Discussions' existing cross-project default for config-backed
8996
+ // use while allowing remote agents to scope via ODLA_APP_ID or a profile.
8997
+ appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
8998
+ };
8576
8999
  }
8577
9000
  async function discussCommand(parsed, deps = {}) {
8578
9001
  assertArgs(parsed, ALLOWED, 3);
@@ -8684,7 +9107,7 @@ async function pmList(ctx, entity, parsed) {
8684
9107
  assignee: "assigneeId",
8685
9108
  decision: "decisionId"
8686
9109
  };
8687
- const app = stringOpt(parsed.options.app);
9110
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
8688
9111
  if (app) q.set("app", app);
8689
9112
  for (const [flag, param] of Object.entries(filters)) {
8690
9113
  const v = stringOpt(parsed.options[flag]);
@@ -8703,7 +9126,7 @@ async function pmList(ctx, entity, parsed) {
8703
9126
  });
8704
9127
  }
8705
9128
  async function pmAdd(ctx, entity, parsed) {
8706
- const appId = stringOpt(parsed.options.app);
9129
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
8707
9130
  if (!appId) throw new Error("pm add needs --app <appId>");
8708
9131
  const input = collectEntityFields(entity, parsed, false);
8709
9132
  if (!input.title) throw new Error("pm add needs --title <title>");
@@ -8754,7 +9177,7 @@ async function allRecords(ctx, entity, appId) {
8754
9177
  }
8755
9178
  }
8756
9179
  async function pmHandoff(ctx, parsed) {
8757
- const appId = stringOpt(parsed.options.app);
9180
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
8758
9181
  if (!appId) throw new Error("pm handoff needs --app <appId>");
8759
9182
  const [goals, tasks, bugs] = await Promise.all([
8760
9183
  allRecords(ctx, "goal", appId),
@@ -8826,37 +9249,62 @@ var ALIASES = {
8826
9249
  decision: "decision",
8827
9250
  bug: "bug"
8828
9251
  };
8829
- var ALLOWED2 = [
8830
- "config",
8831
- "token",
8832
- "email",
8833
- "json",
8834
- "app",
8835
- "title",
8836
- "status",
8837
- "severity",
8838
- "column",
8839
- "rank",
8840
- "goal",
8841
- "assignee",
8842
- "due",
8843
- "proof",
8844
- "body",
8845
- "target",
8846
- "description",
8847
- "desc",
8848
- "decision",
8849
- "limit",
8850
- "offset",
8851
- "q",
8852
- "mutation-id"
8853
- ];
9252
+ var COMMON_OPTIONS = ["config", "token", "email", "json", "platform", "context"];
9253
+ var ACTION_OPTIONS = {
9254
+ list: ["app", "q", "limit", "offset"],
9255
+ add: ["app", "title", "mutation-id"],
9256
+ get: [],
9257
+ set: ["mutation-id"],
9258
+ done: ["mutation-id"],
9259
+ comment: ["body", "mutation-id"],
9260
+ comments: [],
9261
+ rm: []
9262
+ };
9263
+ var ENTITY_OPTIONS = {
9264
+ goal: {
9265
+ list: ["status"],
9266
+ add: ["status", "proof", "target"],
9267
+ set: ["title", "status", "proof", "target"],
9268
+ done: []
9269
+ },
9270
+ task: {
9271
+ list: ["column", "goal", "assignee"],
9272
+ add: ["column", "goal", "assignee", "due", "description", "desc", "body"],
9273
+ set: ["title", "column", "rank", "goal", "assignee", "due", "description", "desc", "body"],
9274
+ done: []
9275
+ },
9276
+ decision: {
9277
+ list: ["status"],
9278
+ add: ["status", "body"],
9279
+ set: ["title", "status", "body"],
9280
+ done: []
9281
+ },
9282
+ bug: {
9283
+ list: ["status", "severity", "goal", "assignee", "decision"],
9284
+ add: ["status", "severity", "goal", "assignee", "decision", "description", "desc", "body"],
9285
+ set: ["title", "status", "severity", "goal", "assignee", "decision", "description", "desc", "body"],
9286
+ done: ["decision"]
9287
+ }
9288
+ };
9289
+ function canonicalAction(action) {
9290
+ if (action === "create") return "add";
9291
+ if (action === "update" || action === "status" || action === "move") return "set";
9292
+ if (action === "delete") return "rm";
9293
+ return action in ACTION_OPTIONS ? action : null;
9294
+ }
9295
+ function allowedOptions(entity, action) {
9296
+ const entityOptions = action === "list" || action === "add" || action === "set" || action === "done" ? ENTITY_OPTIONS[entity][action] : [];
9297
+ return [...COMMON_OPTIONS, ...ACTION_OPTIONS[action], ...entityOptions];
9298
+ }
8854
9299
  function requireId2(id, action) {
8855
9300
  if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
8856
9301
  return id;
8857
9302
  }
8858
9303
  async function buildContext2(parsed, deps) {
8859
- const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
9304
+ const context = await resolveOperatorContext(parsed, {
9305
+ allowMissingConfig: true
9306
+ });
9307
+ const { cfg } = context;
8860
9308
  const doFetch = deps.fetch ?? fetch;
8861
9309
  const out = deps.stdout ?? console;
8862
9310
  const token = await getDeveloperToken(
@@ -8865,32 +9313,39 @@ async function buildContext2(parsed, deps) {
8865
9313
  doFetch,
8866
9314
  out
8867
9315
  );
8868
- return { platformUrl: cfg.platformUrl, token, doFetch, out, json: parsed.options.json === true };
9316
+ return {
9317
+ platformUrl: cfg.platformUrl,
9318
+ token,
9319
+ doFetch,
9320
+ out,
9321
+ json: parsed.options.json === true,
9322
+ // Preserve PM's existing cross-project default when a config is present;
9323
+ // only an explicit remote-agent environment or profile selects an app.
9324
+ appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
9325
+ };
8869
9326
  }
8870
9327
  async function pmCommand(parsed, deps = {}) {
8871
9328
  const word = parsed.positionals[1] ?? "";
8872
9329
  if (word === "handoff") {
8873
- assertArgs(parsed, ALLOWED2, 2);
9330
+ assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
8874
9331
  return pmHandoff(await buildContext2(parsed, deps), parsed);
8875
9332
  }
8876
9333
  const entity = ALIASES[word];
8877
9334
  if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
8878
- const action = parsed.positionals[2] ?? "list";
8879
- assertArgs(parsed, ALLOWED2, 4);
9335
+ const requestedAction = parsed.positionals[2] ?? "list";
9336
+ const action = canonicalAction(requestedAction);
9337
+ if (!action) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|comment|comments|rm.`);
9338
+ assertArgs(parsed, allowedOptions(entity, action), 4);
8880
9339
  const ctx = await buildContext2(parsed, deps);
8881
9340
  const id = parsed.positionals[3];
8882
9341
  switch (action) {
8883
9342
  case "list":
8884
9343
  return pmList(ctx, entity, parsed);
8885
9344
  case "add":
8886
- case "create":
8887
9345
  return pmAdd(ctx, entity, parsed);
8888
9346
  case "get":
8889
9347
  return pmGet(ctx, entity, requireId2(id, action));
8890
9348
  case "set":
8891
- case "update":
8892
- case "status":
8893
- case "move":
8894
9349
  return pmSet(ctx, entity, requireId2(id, action), parsed);
8895
9350
  case "done":
8896
9351
  return pmDone(ctx, entity, requireId2(id, action), parsed);
@@ -8899,10 +9354,7 @@ async function pmCommand(parsed, deps = {}) {
8899
9354
  case "comments":
8900
9355
  return pmComments(ctx, entity, requireId2(id, action));
8901
9356
  case "rm":
8902
- case "delete":
8903
9357
  return pmRemove(ctx, entity, requireId2(id, action));
8904
- default:
8905
- throw new Error(`unknown pm action "${action}". Try list|add|get|set|done|comment|comments|rm.`);
8906
9358
  }
8907
9359
  }
8908
9360
 
@@ -8914,6 +9366,20 @@ function statusVerdict(reads) {
8914
9366
  sourceHttp(reasons, "canary", reads.canary);
8915
9367
  sourceHttp(reasons, "collector", reads.collector);
8916
9368
  sourceHttp(reasons, "provider", reads.provider);
9369
+ if (reads.providerCapacity) {
9370
+ sourceHttp(
9371
+ reasons,
9372
+ "provider",
9373
+ reads.providerCapacity,
9374
+ "provider_capacity_"
9375
+ );
9376
+ }
9377
+ sourceHttp(
9378
+ reasons,
9379
+ "provider",
9380
+ reads.providerReconciliation,
9381
+ "provider_reconciliation_"
9382
+ );
8917
9383
  sourceHttp(reasons, "provider", reads.providerHistory, "provider_history_");
8918
9384
  const liveStatus = String(reads.liveSync.body.status ?? "");
8919
9385
  if (liveStatus === "partial") {
@@ -8986,6 +9452,17 @@ function statusVerdict(reads) {
8986
9452
  severity: "degraded"
8987
9453
  });
8988
9454
  }
9455
+ const reconciliationStatus = String(
9456
+ reads.providerReconciliation.body.status ?? ""
9457
+ );
9458
+ if (reads.providerReconciliation.httpStatus >= 200 && reads.providerReconciliation.httpStatus < 300 && reconciliationStatus && reconciliationStatus !== "within_budget") {
9459
+ const reconciliationReason = reads.providerReconciliation.body.reason;
9460
+ reasons.push({
9461
+ source: "provider",
9462
+ code: reconciliationStatus === "outside_budget" ? typeof reconciliationReason === "string" && reconciliationReason ? reconciliationReason : "request_coverage_outside_budget" : `request_reconciliation_${reconciliationStatus}`,
9463
+ severity: "degraded"
9464
+ });
9465
+ }
8989
9466
  const providerHistoryStatus = String(reads.providerHistory.body.status ?? "");
8990
9467
  if (providerHistoryStatus && providerHistoryStatus !== "observed" && providerHistoryStatus !== "no_data") {
8991
9468
  reasons.push({
@@ -9056,8 +9533,19 @@ function printO11yStatus(status, out) {
9056
9533
  `collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
9057
9534
  );
9058
9535
  const providerMetrics = record6(status.provider.body.metrics) ? status.provider.body.metrics : {};
9536
+ const providerCapacity = record6(status.provider.body.capacity) ? status.provider.body.capacity : {};
9537
+ const workerMemory = record6(providerCapacity.memory) ? providerCapacity.memory : {};
9059
9538
  out.log(
9060
- `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors`
9539
+ `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`
9540
+ );
9541
+ for (const line of providerCapacityLines(status.providerCapacity)) {
9542
+ out.log(line);
9543
+ }
9544
+ const coverage = record6(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
9545
+ const coverageCounts = record6(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
9546
+ const coverageBudget = record6(status.providerReconciliation.body.budget) ? status.providerReconciliation.body.budget : {};
9547
+ out.log(
9548
+ `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`
9061
9549
  );
9062
9550
  const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
9063
9551
  const providerFreshness = record6(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
@@ -9068,6 +9556,27 @@ function printO11yStatus(status, out) {
9068
9556
  `verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
9069
9557
  );
9070
9558
  }
9559
+ function providerCapacityLines(read3) {
9560
+ const resources = record6(read3.body.resources) ? read3.body.resources : {};
9561
+ const durableObjects = record6(resources.durableObjects) ? resources.durableObjects : {};
9562
+ const periodic = record6(durableObjects.periodic) ? durableObjects.periodic : {};
9563
+ const storage = record6(durableObjects.sqliteStorage) ? durableObjects.sqliteStorage : {};
9564
+ const d1 = record6(resources.d1) ? resources.d1 : {};
9565
+ const d1Activity = record6(d1.activity) ? d1.activity : {};
9566
+ const d1Storage = record6(d1.storage) ? d1.storage : {};
9567
+ const d1Latency = record6(d1Activity.latency) ? d1Activity.latency : {};
9568
+ const r2 = record6(resources.r2) ? resources.r2 : {};
9569
+ const r2Operations = record6(r2.operations) ? r2.operations : {};
9570
+ const r2Storage = record6(r2.storage) ? r2.storage : {};
9571
+ const status = String(
9572
+ read3.body.status ?? read3.body.error ?? "unavailable"
9573
+ );
9574
+ return [
9575
+ `cloudflare-capacity ${read3.httpStatus} ${status} ${optionalBytes(storage.storedBytes)} SQLite DO storage ${optionalCount(periodic.activeWebsocketConnectionsMax, "active socket max")} ${optionalAgeSeconds(storage.ageSeconds)} storage evidence age`,
9576
+ `cloudflare-d1 ${read3.httpStatus} ${status} ${optionalBytes(d1Storage.databaseSizeBytes)} storage ${numeric3(d1Activity.readQueries) + numeric3(d1Activity.writeQueries)} queries ${numeric3(d1Activity.rowsRead)}/${numeric3(d1Activity.rowsWritten)} rows read/written ${optionalNumeric(d1Latency.worstDatabaseHourP99Ms)} worst database-hour p99 ${optionalAgeSeconds(d1Activity.ageSeconds)} activity evidence age`,
9577
+ `cloudflare-r2 ${read3.httpStatus} ${status} ${optionalBytes(r2Storage.storedBytes)} storage ${numeric3(r2Storage.objectCount)} objects ${numeric3(r2Operations.requests)} operations ${numeric3(r2Operations.userErrorRequests) + numeric3(r2Operations.internalErrorRequests)} provider errors ${optionalAgeSeconds(r2Operations.ageSeconds)} operation evidence age`
9578
+ ];
9579
+ }
9071
9580
  function liveSyncLine(read3) {
9072
9581
  const performance = record6(read3.body.performance) ? read3.body.performance : {};
9073
9582
  const commitToSend = record6(performance.commitToSend) ? performance.commitToSend : {};
@@ -9086,12 +9595,42 @@ function optionalAge(value) {
9086
9595
  if (typeof value !== "number" || !Number.isFinite(value)) return "unknown";
9087
9596
  return `${Math.max(0, Math.round(value / 1e3))}s`;
9088
9597
  }
9598
+ function optionalPercent(value) {
9599
+ return typeof value === "number" && Number.isFinite(value) ? `${Math.round(value * 100)}%` : "\u2014";
9600
+ }
9601
+ function optionalBytes(value) {
9602
+ if (typeof value !== "number" || !Number.isFinite(value)) return "\u2014";
9603
+ if (value >= 1024 * 1024 * 1024) {
9604
+ return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
9605
+ }
9606
+ if (value >= 1024 * 1024) {
9607
+ return `${(value / (1024 * 1024)).toFixed(1)} MiB`;
9608
+ }
9609
+ if (value >= 1024) return `${(value / 1024).toFixed(1)} KiB`;
9610
+ return `${Math.round(value)} B`;
9611
+ }
9612
+ function optionalCount(value, unit) {
9613
+ return typeof value === "number" && Number.isFinite(value) ? `${value} ${unit}` : `\u2014 ${unit}`;
9614
+ }
9615
+ function optionalAgeSeconds(value) {
9616
+ return typeof value === "number" && Number.isFinite(value) ? `${Math.max(0, Math.round(value))}s` : "unknown";
9617
+ }
9089
9618
 
9090
9619
  // src/o11y-command.ts
9091
9620
  async function o11yCommand(parsed, deps = {}) {
9092
9621
  assertArgs(
9093
9622
  parsed,
9094
- ["config", "token", "email", "json", "app", "env", "minutes"],
9623
+ [
9624
+ "config",
9625
+ "context",
9626
+ "platform",
9627
+ "token",
9628
+ "email",
9629
+ "json",
9630
+ "app",
9631
+ "env",
9632
+ "minutes"
9633
+ ],
9095
9634
  2
9096
9635
  );
9097
9636
  const action = parsed.positionals[1];
@@ -9103,9 +9642,12 @@ async function o11yCommand(parsed, deps = {}) {
9103
9642
  const minutes = statusMinutes(
9104
9643
  numberOpt(parsed.options.minutes, "--minutes") ?? 60
9105
9644
  );
9106
- const cfg = await loadProjectConfig(
9107
- stringOpt(parsed.options.config) ?? "odla.config.mjs"
9108
- );
9645
+ const context = await resolveOperatorContext(parsed, {
9646
+ allowMissingConfig: true,
9647
+ defaultEnvironment: "prod",
9648
+ requireApp: true
9649
+ });
9650
+ const cfg = context.cfg;
9109
9651
  const doFetch = deps.fetch ?? fetch;
9110
9652
  const out = deps.stdout ?? console;
9111
9653
  const token = await getDeveloperToken(
@@ -9119,8 +9661,8 @@ async function o11yCommand(parsed, deps = {}) {
9119
9661
  doFetch,
9120
9662
  out
9121
9663
  );
9122
- const appId = stringOpt(parsed.options.app)?.trim() || cfg.app.id;
9123
- const env = stringOpt(parsed.options.env)?.trim() || "prod";
9664
+ const appId = context.app.value;
9665
+ const env = context.environment.value;
9124
9666
  const base = `${cfg.platformUrl}/o11y/${encodeURIComponent(appId)}`;
9125
9667
  const query = new URLSearchParams({ env, minutes: String(minutes) });
9126
9668
  const headers = { authorization: `Bearer ${token}` };
@@ -9137,6 +9679,8 @@ async function o11yCommand(parsed, deps = {}) {
9137
9679
  canary,
9138
9680
  collector,
9139
9681
  provider,
9682
+ providerCapacity,
9683
+ providerReconciliation,
9140
9684
  providerHistory
9141
9685
  ] = await Promise.all([
9142
9686
  read2(`${base}/metrics/red?${query}`, headers, doFetch),
@@ -9153,6 +9697,19 @@ async function o11yCommand(parsed, deps = {}) {
9153
9697
  ),
9154
9698
  read2(`${base}/self-health?${query}`, headers, doFetch),
9155
9699
  read2(`${base}/provider/cloudflare?${query}`, headers, doFetch),
9700
+ appId === "odla-db" ? read2(`${base}/provider/cloudflare/capacity`, headers, doFetch) : Promise.resolve({
9701
+ httpStatus: 204,
9702
+ body: {
9703
+ status: "not_applicable",
9704
+ scope: "cloudflare_account",
9705
+ reason: "account-scoped Durable Object capacity is exposed only through odla-db"
9706
+ }
9707
+ }),
9708
+ read2(
9709
+ `${base}/provider/cloudflare/reconciliation?${query}`,
9710
+ headers,
9711
+ doFetch
9712
+ ),
9156
9713
  read2(`${base}/provider/cloudflare/history?${query}`, headers, doFetch)
9157
9714
  ]);
9158
9715
  const verdict = statusVerdict({
@@ -9161,10 +9718,12 @@ async function o11yCommand(parsed, deps = {}) {
9161
9718
  canary,
9162
9719
  collector,
9163
9720
  provider,
9721
+ providerCapacity,
9722
+ providerReconciliation,
9164
9723
  providerHistory
9165
9724
  });
9166
9725
  const status = {
9167
- schemaVersion: 6,
9726
+ schemaVersion: 9,
9168
9727
  scope: { appId, env, minutes },
9169
9728
  observedAt: (/* @__PURE__ */ new Date()).toISOString(),
9170
9729
  verdict,
@@ -9174,6 +9733,8 @@ async function o11yCommand(parsed, deps = {}) {
9174
9733
  canary,
9175
9734
  collector,
9176
9735
  provider,
9736
+ providerCapacity,
9737
+ providerReconciliation,
9177
9738
  providerHistory
9178
9739
  };
9179
9740
  if (parsed.options.json === true) {
@@ -9205,14 +9766,14 @@ async function read2(url, headers, doFetch) {
9205
9766
 
9206
9767
  // src/record.ts
9207
9768
  import { appendFileSync } from "fs";
9208
- import process9 from "process";
9769
+ import process12 from "process";
9209
9770
  function recordInvocation(parsed) {
9210
- const file = process9.env.ODLA_CLI_RECORD;
9771
+ const file = process12.env.ODLA_CLI_RECORD;
9211
9772
  if (!file) return;
9212
9773
  try {
9213
9774
  const entry = {
9214
9775
  path: invocationPath(parsed.positionals),
9215
- options: Object.keys(parsed.options).sort()
9776
+ options: Object.entries(parsed.options).map(([name, value]) => value === false ? `no-${name}` : name).sort()
9216
9777
  };
9217
9778
  if (!entry.path.length) return;
9218
9779
  appendFileSync(file, `${JSON.stringify(entry)}
@@ -9221,13 +9782,8 @@ function recordInvocation(parsed) {
9221
9782
  }
9222
9783
  }
9223
9784
 
9224
- // src/runbook-command.ts
9225
- import { existsSync as existsSync10 } from "fs";
9226
- import { join as join12, resolve as resolve10 } from "path";
9227
- import process11 from "process";
9228
-
9229
9785
  // src/runbook-actions.ts
9230
- import { readFileSync as readFileSync8 } from "fs";
9786
+ import { readFileSync as readFileSync9 } from "fs";
9231
9787
 
9232
9788
  // src/runbook-requires.ts
9233
9789
  var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
@@ -9312,7 +9868,7 @@ async function bySlug(ctx, slug) {
9312
9868
  function readBody(file, inline) {
9313
9869
  if (inline !== void 0) return inline;
9314
9870
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
9315
- return readFileSync8(file === "-" ? 0 : file, "utf8");
9871
+ return readFileSync9(file === "-" ? 0 : file, "utf8");
9316
9872
  }
9317
9873
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
9318
9874
  async function runbookList(ctx, all, query) {
@@ -9399,8 +9955,8 @@ async function runbookRemove(ctx, slug) {
9399
9955
  }
9400
9956
 
9401
9957
  // src/runbook-import.ts
9402
- import { readFileSync as readFileSync9, readdirSync as readdirSync2, statSync } from "fs";
9403
- import { basename as basename2, join as join9 } from "path";
9958
+ import { readFileSync as readFileSync10, readdirSync as readdirSync2, statSync } from "fs";
9959
+ import { basename as basename2, join as join11 } from "path";
9404
9960
  function parseRunbook(text, slug) {
9405
9961
  let rest = text;
9406
9962
  const meta = {};
@@ -9430,7 +9986,7 @@ function readRunbookDir(dir) {
9430
9986
  if (!files.length) throw new Error(`no .md files in ${dir}`);
9431
9987
  return files.map((file) => {
9432
9988
  const slug = basename2(file, ".md");
9433
- const parsed = parseRunbook(readFileSync9(join9(dir, file), "utf8"), slug);
9989
+ const parsed = parseRunbook(readFileSync10(join11(dir, file), "utf8"), slug);
9434
9990
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
9435
9991
  });
9436
9992
  }
@@ -9502,8 +10058,8 @@ async function upsert(ctx, r, visibility) {
9502
10058
 
9503
10059
  // src/runbook-impact.ts
9504
10060
  import { execFileSync as execFileSync2 } from "child_process";
9505
- import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
9506
- import { join as join10 } from "path";
10061
+ import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
10062
+ import { join as join12 } from "path";
9507
10063
 
9508
10064
  // src/runbook-impact-scan.ts
9509
10065
  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$]*)/;
@@ -9672,10 +10228,10 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
9672
10228
  }
9673
10229
  function manifestLabeller(root) {
9674
10230
  return (workspace) => {
9675
- const manifest = join10(root, workspace, "package.json");
9676
- if (!existsSync9(manifest)) return void 0;
10231
+ const manifest = join12(root, workspace, "package.json");
10232
+ if (!existsSync11(manifest)) return void 0;
9677
10233
  try {
9678
- const name = JSON.parse(readFileSync10(manifest, "utf8")).name;
10234
+ const name = JSON.parse(readFileSync11(manifest, "utf8")).name;
9679
10235
  return typeof name === "string" ? name : void 0;
9680
10236
  } catch {
9681
10237
  return void 0;
@@ -9742,7 +10298,7 @@ function report3(ctx, impacts) {
9742
10298
  async function runbookImpact(ctx, options, deps = {}) {
9743
10299
  const cwd = deps.cwd ?? process.cwd();
9744
10300
  const runGit = deps.runGit ?? gitRunner(cwd);
9745
- const read3 = deps.readRepoFile ?? ((path) => readFileSync10(join10(cwd, path), "utf8"));
10301
+ const read3 = deps.readRepoFile ?? ((path) => readFileSync11(join12(cwd, path), "utf8"));
9746
10302
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
9747
10303
  if (!surfaces.length) {
9748
10304
  return ctx.out.log(
@@ -9875,12 +10431,12 @@ async function runbookComment(ctx, slug, body) {
9875
10431
 
9876
10432
  // src/runbook-editor.ts
9877
10433
  import { spawnSync } from "child_process";
9878
- import { mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
10434
+ import { mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
9879
10435
  import { tmpdir as tmpdir4 } from "os";
9880
- import { join as join11 } from "path";
9881
- import process10 from "process";
10436
+ import { join as join13 } from "path";
10437
+ import process13 from "process";
9882
10438
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
9883
- function resolveEditor(env = process10.env) {
10439
+ function resolveEditor(env = process13.env) {
9884
10440
  for (const name of EDITOR_ENV) {
9885
10441
  const value = env[name];
9886
10442
  if (value && value.trim()) return value.trim();
@@ -9894,8 +10450,8 @@ function defaultRun(command, path) {
9894
10450
  return result.status ?? 0;
9895
10451
  }
9896
10452
  function editText(initial, slug, deps = {}) {
9897
- const env = deps.env ?? process10.env;
9898
- const interactive = deps.interactive ?? (() => Boolean(process10.stdin.isTTY));
10453
+ const env = deps.env ?? process13.env;
10454
+ const interactive = deps.interactive ?? (() => Boolean(process13.stdin.isTTY));
9899
10455
  const editor = resolveEditor(env);
9900
10456
  if (!editor)
9901
10457
  throw new Error(
@@ -9903,13 +10459,13 @@ function editText(initial, slug, deps = {}) {
9903
10459
  );
9904
10460
  if (!interactive())
9905
10461
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
9906
- const dir = mkdtempSync(join11(tmpdir4(), "odla-runbook-"));
9907
- const file = join11(dir, `${slug}.md`);
10462
+ const dir = mkdtempSync(join13(tmpdir4(), "odla-runbook-"));
10463
+ const file = join13(dir, `${slug}.md`);
9908
10464
  try {
9909
10465
  writeFileSync4(file, initial, { mode: 384 });
9910
10466
  const code = defaultRunOrInjected(deps)(editor, file);
9911
10467
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
9912
- const edited = readFileSync11(file, "utf8");
10468
+ const edited = readFileSync12(file, "utf8");
9913
10469
  return edited === initial ? null : edited;
9914
10470
  } finally {
9915
10471
  rmSync2(dir, { recursive: true, force: true });
@@ -9950,10 +10506,16 @@ function credentialKind(identity) {
9950
10506
  return identity.scopes.length ? "device token (scoped)" : "device token or session";
9951
10507
  }
9952
10508
  async function whoamiCommand(parsed, deps = {}) {
9953
- assertArgs(parsed, ["config", "token", "email", "json"], 1);
10509
+ assertArgs(
10510
+ parsed,
10511
+ ["config", "context", "platform", "token", "email", "json"],
10512
+ 1
10513
+ );
9954
10514
  const out = deps.stdout ?? console;
9955
10515
  const doFetch = deps.fetch ?? fetch;
9956
- const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
10516
+ const { cfg } = await resolveOperatorContext(parsed, {
10517
+ allowMissingConfig: true
10518
+ });
9957
10519
  const token = await getDeveloperToken(
9958
10520
  cfg,
9959
10521
  {
@@ -9984,7 +10546,7 @@ async function whoamiCommand(parsed, deps = {}) {
9984
10546
  }
9985
10547
 
9986
10548
  // src/runbook-command.ts
9987
- var ALLOWED3 = [
10549
+ var ALLOWED2 = [
9988
10550
  "config",
9989
10551
  "token",
9990
10552
  "email",
@@ -10002,34 +10564,24 @@ var ALLOWED3 = [
10002
10564
  "dry-run",
10003
10565
  "limit",
10004
10566
  "base",
10005
- "requires"
10567
+ "requires",
10568
+ "platform",
10569
+ "context"
10006
10570
  ];
10007
10571
  function requireSlug(slug, action) {
10008
10572
  if (!slug) throw new Error(`"runbook ${action}" needs a slug, e.g. "odla-ai runbook ${action} release"`);
10009
10573
  return slug;
10010
10574
  }
10011
10575
  var WRITES = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visibility", "revert", "rm", "import"]);
10012
- var CONFIG_FREE = /* @__PURE__ */ new Set(["list", "search", "ask", "get", "cat", "history", "impact", "lint"]);
10013
- async function loadOrDefaultConfig(configPath, action, appId) {
10014
- const projectScoped = appId !== void 0;
10015
- if (!projectScoped && CONFIG_FREE.has(action) && !existsSync10(resolve10(configPath))) {
10016
- const rootDir = process11.cwd();
10017
- return {
10018
- configPath,
10019
- platformUrl: process11.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
10020
- rootDir,
10021
- app: { id: "", name: "" },
10022
- local: { tokenFile: join12(rootDir, ".odla/dev-token.json") }
10023
- };
10024
- }
10025
- return loadProjectConfig(configPath);
10026
- }
10027
10576
  async function buildContext3(parsed, deps, action) {
10028
- const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
10029
- const cfg = await loadOrDefaultConfig(configPath, action, stringOpt(parsed.options.app));
10577
+ const appIdOption = stringOpt(parsed.options.app);
10578
+ const context = await resolveOperatorContext(parsed, {
10579
+ allowMissingConfig: true
10580
+ });
10581
+ const { cfg } = context;
10030
10582
  const doFetch = deps.fetch ?? fetch;
10031
10583
  const out = deps.stdout ?? console;
10032
- const appId = stringOpt(parsed.options.app) ?? PLATFORM_SCOPE;
10584
+ const appId = appIdOption ?? (context.app.source === "environment" || context.app.source === "profile" ? context.app.value : null) ?? PLATFORM_SCOPE;
10033
10585
  const dryRun = parsed.options["dry-run"] === true;
10034
10586
  if (action === "import" && dryRun) {
10035
10587
  return {
@@ -10050,8 +10602,10 @@ async function buildContext3(parsed, deps, action) {
10050
10602
  fetch: doFetch,
10051
10603
  stdout: out,
10052
10604
  openApprovalUrl: deps.openUrl,
10053
- // Anchor the grant cache to this project, not the shell's cwd.
10054
- rootDir: cfg.rootDir
10605
+ // A project, named context, or the global operator context owns the
10606
+ // exact-scope cache; it never follows an arbitrary shell directory.
10607
+ rootDir: cfg.rootDir,
10608
+ tokenFile: context.credentials.scopedTokenFile
10055
10609
  }) : await getDeveloperToken(
10056
10610
  cfg,
10057
10611
  {
@@ -10080,7 +10634,7 @@ async function buildContext3(parsed, deps, action) {
10080
10634
  }
10081
10635
  async function runbookCommand(parsed, deps = {}) {
10082
10636
  const action = parsed.positionals[1] ?? "list";
10083
- assertArgs(parsed, ALLOWED3, action === "ask" || action === "search" ? 64 : 4);
10637
+ assertArgs(parsed, ALLOWED2, action === "ask" || action === "search" ? 64 : 4);
10084
10638
  const ctx = await buildContext3(parsed, deps, action);
10085
10639
  const slug = parsed.positionals[2];
10086
10640
  switch (action) {
@@ -10634,6 +11188,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
10634
11188
  await whoamiCommand(parsed, runtime);
10635
11189
  return;
10636
11190
  }
11191
+ if (command === "context") {
11192
+ await contextCommand(parsed, runtime);
11193
+ return;
11194
+ }
10637
11195
  if (command === "runbook") {
10638
11196
  await runbookCommand(parsed, runtime);
10639
11197
  return;
@@ -10793,4 +11351,4 @@ export {
10793
11351
  exitCodeFor,
10794
11352
  runCli
10795
11353
  };
10796
- //# sourceMappingURL=chunk-MFC4TX6W.js.map
11354
+ //# sourceMappingURL=chunk-B6DISJPC.js.map