@odla-ai/cli 0.27.3 → 0.27.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -100,6 +100,7 @@ var import_node_process7 = __toESM(require("process"), 1);
100
100
 
101
101
  // src/token.ts
102
102
  var import_db = require("@odla-ai/db");
103
+ var import_node_crypto = require("crypto");
103
104
  var import_node_process4 = __toESM(require("process"), 1);
104
105
 
105
106
  // src/handshake-approval.ts
@@ -454,6 +455,7 @@ async function freshHandshake(ctx, waitMs) {
454
455
  endpoint: ctx.cfg.platformUrl,
455
456
  email: ctx.email,
456
457
  label: `${ctx.cfg.app.id} provisioner`,
458
+ agentHandle: projectAgentHandle(ctx.cfg.app.id),
457
459
  projectIds: [ctx.cfg.app.id],
458
460
  fetch: ctx.doFetch,
459
461
  waitMs,
@@ -489,6 +491,12 @@ async function freshHandshake(ctx, waitMs) {
489
491
  stopReminder?.();
490
492
  }
491
493
  }
494
+ function projectAgentHandle(appId) {
495
+ const candidate = /^[a-z]/.test(appId) ? appId : `app-${appId}`;
496
+ if (candidate.length <= 32) return candidate;
497
+ const digest = (0, import_node_crypto.createHash)("sha256").update(appId).digest("hex").slice(0, 8);
498
+ return `${candidate.slice(0, 23).replace(/-+$/, "")}-${digest}`;
499
+ }
492
500
  function stillPending(pending, email) {
493
501
  return new import_db.OdlaError(
494
502
  "handshake_pending",
@@ -1061,6 +1069,32 @@ var import_node_path4 = require("path");
1061
1069
  var import_node_url = require("url");
1062
1070
  var import_apps = require("@odla-ai/apps");
1063
1071
 
1072
+ // src/ai-config-validation.ts
1073
+ function validateAiConfig(cfg, path) {
1074
+ if (cfg.ai === void 0) return;
1075
+ if (!isRecord4(cfg.ai)) throw new Error(`${path}: ai must be an object`);
1076
+ assertOnly(cfg.ai, ["mode", "provider", "model", "keyEnv", "secretName"], `${path}: ai`);
1077
+ if (cfg.ai.mode !== void 0 && cfg.ai.mode !== "hosted" && cfg.ai.mode !== "byok") {
1078
+ throw new Error(`${path}: ai.mode must be hosted or byok`);
1079
+ }
1080
+ if (cfg.ai.mode === "byok" && !safeText(cfg.ai.provider, 100)) {
1081
+ throw new Error(`${path}: ai.provider is required when ai.mode is byok`);
1082
+ }
1083
+ if (cfg.ai.mode === "hosted" && (cfg.ai.provider || cfg.ai.keyEnv || cfg.ai.secretName)) {
1084
+ throw new Error(`${path}: hosted ai cannot configure a provider, keyEnv, or secretName`);
1085
+ }
1086
+ }
1087
+ function assertOnly(value2, allowed, label) {
1088
+ const extra = Object.keys(value2).find((key) => !allowed.includes(key));
1089
+ if (extra) throw new Error(`${label}.${extra} is not supported`);
1090
+ }
1091
+ function isRecord4(value2) {
1092
+ return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
1093
+ }
1094
+ function safeText(value2, max) {
1095
+ return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
1096
+ }
1097
+
1064
1098
  // src/integration-validation.ts
1065
1099
  function validateIntegrations(cfg, path, defaultServices) {
1066
1100
  if (cfg.integrations === void 0) return;
@@ -1068,16 +1102,16 @@ function validateIntegrations(cfg, path, defaultServices) {
1068
1102
  const ids = /* @__PURE__ */ new Set();
1069
1103
  for (const [index, integration] of cfg.integrations.entries()) {
1070
1104
  const at = `${path}: integrations[${index}]`;
1071
- if (!isRecord4(integration)) throw new Error(`${at} must be an object`);
1105
+ if (!isRecord5(integration)) throw new Error(`${at} must be an object`);
1072
1106
  if (!validId(integration.id)) throw new Error(`${at}.id must be lowercase letters, numbers, and hyphens`);
1073
1107
  if (ids.has(integration.id)) throw new Error(`${path}: duplicate integration id "${integration.id}"`);
1074
1108
  ids.add(integration.id);
1075
- if (!safeText(integration.title, 200)) throw new Error(`${at}.title is required`);
1076
- if (!safeText(integration.npm, 200)) throw new Error(`${at}.npm is required`);
1077
- if (integration.schema !== void 0 && (!isRecord4(integration.schema) || !isRecord4(integration.schema.entities))) {
1109
+ if (!safeText2(integration.title, 200)) throw new Error(`${at}.title is required`);
1110
+ if (!safeText2(integration.npm, 200)) throw new Error(`${at}.npm is required`);
1111
+ if (integration.schema !== void 0 && (!isRecord5(integration.schema) || !isRecord5(integration.schema.entities))) {
1078
1112
  throw new Error(`${at}.schema must contain an entities object`);
1079
1113
  }
1080
- if (integration.rules !== void 0 && !isRecord4(integration.rules)) throw new Error(`${at}.rules must be an object`);
1114
+ if (integration.rules !== void 0 && !isRecord5(integration.rules)) throw new Error(`${at}.rules must be an object`);
1081
1115
  validateSeeds(integration, at);
1082
1116
  validateProbes(integration, at);
1083
1117
  }
@@ -1091,13 +1125,13 @@ function validateSeeds(integration, at) {
1091
1125
  const ids = /* @__PURE__ */ new Set();
1092
1126
  for (const [index, seed] of integration.seeds.entries()) {
1093
1127
  const sat = `${at}.seeds[${index}]`;
1094
- if (!isRecord4(seed) || !safeText(seed.id, 200) || !safeText(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
1128
+ if (!isRecord5(seed) || !safeText2(seed.id, 200) || !safeText2(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
1095
1129
  if (ids.has(seed.id)) throw new Error(`${at} has duplicate seed id "${seed.id}"`);
1096
1130
  ids.add(seed.id);
1097
- if (!isRecord4(seed.key) || !safeText(seed.key.attr, 200) || !safeText(seed.key.value, 2048)) {
1131
+ if (!isRecord5(seed.key) || !safeText2(seed.key.attr, 200) || !safeText2(seed.key.value, 2048)) {
1098
1132
  throw new Error(`${sat}.key requires string attr and value`);
1099
1133
  }
1100
- if (!isRecord4(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
1134
+ if (!isRecord5(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
1101
1135
  if (Object.hasOwn(seed.attrs, seed.key.attr) && seed.attrs[seed.key.attr] !== seed.key.value) {
1102
1136
  throw new Error(`${sat}.attrs.${seed.key.attr} conflicts with its natural key`);
1103
1137
  }
@@ -1108,16 +1142,16 @@ function validateProbes(integration, at) {
1108
1142
  if (!Array.isArray(integration.probes)) throw new Error(`${at}.probes must be an array`);
1109
1143
  for (const [index, probe] of integration.probes.entries()) {
1110
1144
  const pat = `${at}.probes[${index}]`;
1111
- if (!isRecord4(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
1145
+ if (!isRecord5(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
1112
1146
  if (!Number.isInteger(probe.expectedStatus) || probe.expectedStatus < 100 || probe.expectedStatus > 599) {
1113
1147
  throw new Error(`${pat}.expectedStatus must be an HTTP status`);
1114
1148
  }
1115
1149
  }
1116
1150
  }
1117
- function isRecord4(value2) {
1151
+ function isRecord5(value2) {
1118
1152
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
1119
1153
  }
1120
- function safeText(value2, max) {
1154
+ function safeText2(value2, max) {
1121
1155
  return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
1122
1156
  }
1123
1157
  function safeProbePath(value2) {
@@ -1247,6 +1281,7 @@ function validateRawConfig(raw, path) {
1247
1281
  if (cfg.services !== void 0 && (!Array.isArray(cfg.services) || cfg.services.some((service) => typeof service !== "string" || !service.trim()))) {
1248
1282
  throw new Error(`${path}: services must be an array of non-empty names`);
1249
1283
  }
1284
+ validateAiConfig(cfg, path);
1250
1285
  validateIntegrations(cfg, path, DEFAULT_SERVICES);
1251
1286
  }
1252
1287
  function validateCalendarConfig(cfg, envs, services, path) {
@@ -1255,11 +1290,11 @@ function validateCalendarConfig(cfg, envs, services, path) {
1255
1290
  if (enabled) throw new Error(`${path}: calendar.google is required when services includes "calendar"`);
1256
1291
  return;
1257
1292
  }
1258
- if (!isRecord5(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
1259
- assertOnly(cfg.calendar, ["google"], `${path}: calendar`);
1260
- if (!isRecord5(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
1293
+ if (!isRecord6(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
1294
+ assertOnly2(cfg.calendar, ["google"], `${path}: calendar`);
1295
+ if (!isRecord6(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
1261
1296
  const google = cfg.calendar.google;
1262
- assertOnly(
1297
+ assertOnly2(
1263
1298
  google,
1264
1299
  ["availabilityCalendars", "calendars", "bookingCalendar", "bookingPageUrl"],
1265
1300
  `${path}: calendar.google`
@@ -1269,7 +1304,7 @@ function validateCalendarConfig(cfg, envs, services, path) {
1269
1304
  throw new Error(`${path}: calendar.google requires exactly one of availabilityCalendars or calendars (legacy)`);
1270
1305
  }
1271
1306
  const availability = google[availabilityKey];
1272
- if (!isRecord5(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
1307
+ if (!isRecord6(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
1273
1308
  const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env));
1274
1309
  if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
1275
1310
  for (const env of envs) {
@@ -1280,22 +1315,22 @@ function validateCalendarConfig(cfg, envs, services, path) {
1280
1315
  if (ids.length > 10) {
1281
1316
  throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
1282
1317
  }
1283
- if (ids.some((id) => !safeText2(id, 1024))) {
1318
+ if (ids.some((id) => !safeText3(id, 1024))) {
1284
1319
  throw new Error(`${path}: calendar.google.${availabilityKey}.${env} contains an invalid calendar id`);
1285
1320
  }
1286
1321
  }
1287
1322
  if (google.bookingCalendar !== void 0) {
1288
- if (!isRecord5(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
1323
+ if (!isRecord6(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
1289
1324
  const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env));
1290
1325
  if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
1291
1326
  for (const [env, value2] of Object.entries(google.bookingCalendar)) {
1292
- if (!safeText2(value2, 1024)) {
1327
+ if (!safeText3(value2, 1024)) {
1293
1328
  throw new Error(`${path}: calendar.google.bookingCalendar.${env} must be a calendar id`);
1294
1329
  }
1295
1330
  }
1296
1331
  }
1297
1332
  if (google.bookingPageUrl !== void 0) {
1298
- if (!isRecord5(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
1333
+ if (!isRecord6(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
1299
1334
  const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env));
1300
1335
  if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingPageUrl.${unknownBookingEnv} is not in config envs`);
1301
1336
  for (const [env, value2] of Object.entries(google.bookingPageUrl)) {
@@ -1318,14 +1353,14 @@ function validateServices(services, path) {
1318
1353
  }
1319
1354
  }
1320
1355
  }
1321
- function assertOnly(value2, allowed, label) {
1356
+ function assertOnly2(value2, allowed, label) {
1322
1357
  const extra = Object.keys(value2).find((key) => !allowed.includes(key));
1323
1358
  if (extra) throw new Error(`${label}.${extra} is not supported`);
1324
1359
  }
1325
- function isRecord5(value2) {
1360
+ function isRecord6(value2) {
1326
1361
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
1327
1362
  }
1328
- function safeText2(value2, max) {
1363
+ function safeText3(value2, max) {
1329
1364
  return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
1330
1365
  }
1331
1366
  function safeHttpsUrl(value2) {
@@ -2701,8 +2736,8 @@ async function calendarCalendars(options) {
2701
2736
  async function calendarConnect(options) {
2702
2737
  const { cfg, ctx, out } = await lifecycleContext(options);
2703
2738
  productionConsent(ctx.env, options.yes, "connect calendar");
2704
- const page = calendarBookingPageUrl(cfg, ctx.env);
2705
- const applied = page === void 0 ? await readCalendarStatus(ctx) : await applyCalendarSettings(ctx, page);
2739
+ const page2 = calendarBookingPageUrl(cfg, ctx.env);
2740
+ const applied = page2 === void 0 ? await readCalendarStatus(ctx) : await applyCalendarSettings(ctx, page2);
2706
2741
  const connectOptions = connectionOptions(options, out);
2707
2742
  return await continueConnectedCalendar(ctx, applied, connectOptions) ?? connectWithContext(ctx, connectOptions);
2708
2743
  }
@@ -2951,12 +2986,12 @@ var import_apps3 = require("@odla-ai/apps");
2951
2986
  var import_node_fs10 = require("fs");
2952
2987
 
2953
2988
  // src/config-reconcile-digest.ts
2954
- var import_node_crypto = require("crypto");
2989
+ var import_node_crypto2 = require("crypto");
2955
2990
  function canonicalJson(value2) {
2956
2991
  return JSON.stringify(canonicalValue(value2));
2957
2992
  }
2958
2993
  function configDigest(value2) {
2959
- return `sha256:${(0, import_node_crypto.createHash)("sha256").update(canonicalJson(value2)).digest("hex")}`;
2994
+ return `sha256:${(0, import_node_crypto2.createHash)("sha256").update(canonicalJson(value2)).digest("hex")}`;
2960
2995
  }
2961
2996
  function canonicalValue(value2) {
2962
2997
  if (value2 === null || typeof value2 === "string" || typeof value2 === "boolean") return value2;
@@ -3122,7 +3157,7 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
3122
3157
  `${env}: you are not an owner of "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; ask an existing owner to run "odla-ai app owners add <your-email>", then re-run provision`
3123
3158
  );
3124
3159
  }
3125
- throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText3(res)}`);
3160
+ throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
3126
3161
  }
3127
3162
  async function postJson(doFetch, url, bearer, body) {
3128
3163
  const res = await doFetch(url, {
@@ -3130,7 +3165,7 @@ async function postJson(doFetch, url, bearer, body) {
3130
3165
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
3131
3166
  body: JSON.stringify(body)
3132
3167
  });
3133
- if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText3(res)}`);
3168
+ if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText4(res)}`);
3134
3169
  }
3135
3170
  function normalizeClerkConfig(value2) {
3136
3171
  if (!value2) return null;
@@ -3143,7 +3178,7 @@ function normalizeClerkConfig(value2) {
3143
3178
  const publishableKey = envValue(cfg.publishableKey);
3144
3179
  return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
3145
3180
  }
3146
- async function safeText3(res) {
3181
+ async function safeText4(res) {
3147
3182
  try {
3148
3183
  return redactSecrets((await res.text()).slice(0, 500));
3149
3184
  } catch {
@@ -4125,18 +4160,18 @@ function assertSeedContracts(integrations, schema) {
4125
4160
  }
4126
4161
  }
4127
4162
  function isUniqueAttr(schema, ns, attr) {
4128
- if (!isRecord6(schema) || !isRecord6(schema.entities)) return false;
4163
+ if (!isRecord7(schema) || !isRecord7(schema.entities)) return false;
4129
4164
  const entity = schema.entities[ns];
4130
- if (!isRecord6(entity) || !isRecord6(entity.attrs)) return false;
4165
+ if (!isRecord7(entity) || !isRecord7(entity.attrs)) return false;
4131
4166
  const definition = entity.attrs[attr];
4132
- return isRecord6(definition) && definition.unique === true;
4167
+ return isRecord7(definition) && definition.unique === true;
4133
4168
  }
4134
4169
  function normalizeSchema(value2) {
4135
4170
  if (value2 === void 0 || value2 === null) return { entities: {}, links: {} };
4136
- if (!isRecord6(value2) || !isRecord6(value2.entities)) {
4171
+ if (!isRecord7(value2) || !isRecord7(value2.entities)) {
4137
4172
  throw new Error("db schema must be a serialized schema object with an entities map");
4138
4173
  }
4139
- if (value2.links !== void 0 && !isRecord6(value2.links)) throw new Error("db schema links must be an object");
4174
+ if (value2.links !== void 0 && !isRecord7(value2.links)) throw new Error("db schema links must be an object");
4140
4175
  return {
4141
4176
  entities: { ...value2.entities },
4142
4177
  links: { ...value2.links ?? {} }
@@ -4150,7 +4185,7 @@ function mergeMap(target, fragment, label) {
4150
4185
  target[name] = value2;
4151
4186
  }
4152
4187
  }
4153
- function isRecord6(value2) {
4188
+ function isRecord7(value2) {
4154
4189
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
4155
4190
  }
4156
4191
 
@@ -4169,7 +4204,7 @@ async function doctor(options) {
4169
4204
  out.log(`integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
4170
4205
  out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
4171
4206
  out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
4172
- out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
4207
+ out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
4173
4208
  if (cfg.services.includes("calendar")) {
4174
4209
  const calendar = cfg.envs.map((env) => {
4175
4210
  const resolved = calendarServiceConfig(cfg, env);
@@ -4190,7 +4225,9 @@ async function doctor(options) {
4190
4225
  }
4191
4226
  }
4192
4227
  warnings.push(...integrationWarnings(database.integrations, schema, rules));
4193
- if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
4228
+ if (cfg.services.includes("ai") && cfg.ai?.mode === "byok" && !cfg.ai.provider) {
4229
+ warnings.push("ai.mode is byok but ai.provider is not set");
4230
+ }
4194
4231
  if (cfg.auth?.clerk) {
4195
4232
  for (const [env, value2] of Object.entries(cfg.auth.clerk)) {
4196
4233
  if (typeof value2 === "string" && value2.startsWith("$") && !process.env[value2.slice(1)]) {
@@ -4270,7 +4307,7 @@ function initProject(options) {
4270
4307
  if (!services.includes(dependency)) throw new Error(`--services ${service} requires ${dependency}`);
4271
4308
  }
4272
4309
  }
4273
- const aiProvider = options.aiProvider ?? "anthropic";
4310
+ const aiProvider = options.aiProvider;
4274
4311
  (0, import_node_fs13.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
4275
4312
  (0, import_node_fs13.mkdirSync)((0, import_node_path12.resolve)(rootDir, "src/odla"), { recursive: true });
4276
4313
  (0, import_node_fs13.mkdirSync)((0, import_node_path12.resolve)(rootDir, ".odla"), { recursive: true });
@@ -4297,6 +4334,15 @@ function configTemplate(input) {
4297
4334
  },
4298
4335
  },
4299
4336
  ` : "";
4337
+ const ai = input.aiProvider ? ` ai: {
4338
+ mode: "byok",
4339
+ provider: process.env.ODLA_AI_PROVIDER ?? "${input.aiProvider}",
4340
+ // Optional: set this env var while running provision to store the provider
4341
+ // key in the app vault for each tenant.
4342
+ keyEnv: "${defaultKeyEnv(input.aiProvider)}",
4343
+ },` : ` // Hosted AI uses admin-approved models and central cost tracking.
4344
+ // Pass --ai-provider during init only when this app must use BYOK.
4345
+ ai: { mode: "hosted" },`;
4300
4346
  return `export default {
4301
4347
  platformUrl: process.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
4302
4348
  dbEndpoint: process.env.ODLA_ENDPOINT ?? process.env.ODLA_DB_ENDPOINT ?? "https://db.odla.ai",
@@ -4315,12 +4361,7 @@ function configTemplate(input) {
4315
4361
  // When rules is omitted, the CLI generates deny-all rules from schema.
4316
4362
  defaultRules: "deny",
4317
4363
  },
4318
- ai: {
4319
- provider: process.env.ODLA_AI_PROVIDER ?? "${input.aiProvider}",
4320
- // Optional: set this env var while running provision to store the provider
4321
- // key in the platform vault for each tenant.
4322
- keyEnv: "${defaultKeyEnv(input.aiProvider)}",
4323
- },
4364
+ ${ai}
4324
4365
  ${calendar}
4325
4366
  auth: {
4326
4367
  clerk: {
@@ -4529,9 +4570,10 @@ For work that creates an odla app or adds odla services, read and follow
4529
4570
  \`.agents/skills/odla-migrate/SKILL.md\`. For production telemetry triage, use
4530
4571
  \`.agents/skills/odla-o11y-debug/SKILL.md\`.
4531
4572
 
4532
- Track the work in odla's PM as you go \u2014 record decisions when you make them, file
4533
- bugs when you notice them, and move tasks when the work moves
4534
- (\`npx @odla-ai/cli pm task list\`). The conventions and the full command set are
4573
+ Track the work in odla's PM as you go. Before project-mutating work, run
4574
+ \`npx @odla-ai/cli pm next --app <appId>\`, confirm alignment to an open goal,
4575
+ and atomically claim a refined Ready task. Record decisions when you make them
4576
+ and file bugs when you notice them. The conventions and the full command set are
4535
4577
  in \`.agents/skills/odla/references/pm.md\`.
4536
4578
 
4537
4579
  The setup runbooks and their references are installed in this repository, pinned
@@ -4815,12 +4857,16 @@ async function smoke(options) {
4815
4857
  out.log(` tenant: ${entry.tenantId}`);
4816
4858
  const publicConfig = await getJson(doFetch, publicConfigUrl(cfg.platformUrl, cfg.app.id, env), void 0);
4817
4859
  out.log(` public-config: ok`);
4818
- if (cfg.ai?.provider) {
4860
+ if (cfg.services.includes("ai") && cfg.ai?.provider) {
4819
4861
  const provider = publicConfig.ai?.provider ?? null;
4820
4862
  if (provider !== cfg.ai.provider) {
4821
4863
  throw new Error(`ai provider mismatch: expected "${cfg.ai.provider}", public-config has "${provider ?? "none"}"`);
4822
4864
  }
4823
- out.log(` ai: ${provider}`);
4865
+ out.log(` ai: byok/${provider}`);
4866
+ } else if (cfg.services.includes("ai")) {
4867
+ const mode = publicConfig.ai?.mode;
4868
+ if (mode !== "hosted") throw new Error(`ai mode mismatch: expected "hosted", public-config has "${String(mode ?? "none")}"`);
4869
+ out.log(" ai: hosted");
4824
4870
  }
4825
4871
  if (hasO11y) out.log(` o11y: credentials present`);
4826
4872
  if (cfg.services.includes("calendar")) {
@@ -4898,7 +4944,7 @@ async function getJson(doFetch, url, bearer) {
4898
4944
  const res = await doFetch(url, {
4899
4945
  headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
4900
4946
  });
4901
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
4947
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
4902
4948
  return res.json();
4903
4949
  }
4904
4950
  async function postJson2(doFetch, url, bearer, body) {
@@ -4907,7 +4953,7 @@ async function postJson2(doFetch, url, bearer, body) {
4907
4953
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
4908
4954
  body: JSON.stringify(body)
4909
4955
  });
4910
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
4956
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
4911
4957
  return res.json();
4912
4958
  }
4913
4959
  function publicConfigUrl(platformUrl, appId, env) {
@@ -4915,7 +4961,7 @@ function publicConfigUrl(platformUrl, appId, env) {
4915
4961
  url.searchParams.set("env", env);
4916
4962
  return url.toString();
4917
4963
  }
4918
- async function safeText4(res) {
4964
+ async function safeText5(res) {
4919
4965
  try {
4920
4966
  return redactSecrets((await res.text()).slice(0, 500));
4921
4967
  } catch {
@@ -5755,7 +5801,7 @@ function dependenciesOf(values, influence = "data") {
5755
5801
  return [...unique3.values()];
5756
5802
  }
5757
5803
 
5758
- // ../camel/dist/chunk-S7EVNA2U.js
5804
+ // ../camel/dist/chunk-4DQ6BIHP.js
5759
5805
  var camelValueBrand = /* @__PURE__ */ Symbol("@odla-ai/camel/value");
5760
5806
  var authenticCamelValues = /* @__PURE__ */ new WeakSet();
5761
5807
  function isCamelValue(value2) {
@@ -5767,13 +5813,13 @@ function isSafe(value2) {
5767
5813
  function isUnsafe(value2) {
5768
5814
  return isCamelValue(value2) && value2.label.promptSafety === "unsafe";
5769
5815
  }
5770
- function createSafeInternal(value2, safeBasis, metadata2) {
5816
+ function createSafeInternal(value2, safeBasis, metadata2, extra) {
5771
5817
  return createValue(value2, {
5772
5818
  schemaVersion: 1,
5773
5819
  promptSafety: "safe",
5774
5820
  safeBasis,
5775
5821
  ...copyMetadata(metadata2)
5776
- });
5822
+ }, extra);
5777
5823
  }
5778
5824
  function createUnsafeInternal(value2, metadata2) {
5779
5825
  return createValue(value2, {
@@ -5782,8 +5828,8 @@ function createUnsafeInternal(value2, metadata2) {
5782
5828
  ...copyMetadata(metadata2)
5783
5829
  });
5784
5830
  }
5785
- function createValue(value2, label) {
5786
- const result = { value: value2, label: Object.freeze(label) };
5831
+ function createValue(value2, label, extra) {
5832
+ const result = { value: value2, label: Object.freeze(label), ...extra };
5787
5833
  Object.defineProperty(result, camelValueBrand, { value: label.promptSafety, enumerable: false });
5788
5834
  authenticCamelValues.add(result);
5789
5835
  return Object.freeze(result);
@@ -6044,7 +6090,7 @@ var import_path8 = require("path");
6044
6090
  var import_promises10 = require("fs/promises");
6045
6091
  var import_path9 = require("path");
6046
6092
 
6047
- // ../camel/dist/chunk-LAXU2AVK.js
6093
+ // ../camel/dist/chunk-4EIRFS3A.js
6048
6094
  function conversionPolicyDigest(policy) {
6049
6095
  return sha256Hex(canonicalJson2(policy));
6050
6096
  }
@@ -6132,10 +6178,18 @@ async function convert(source, policy, value2, counts) {
6132
6178
  const count = counts.get(countKey) ?? 0;
6133
6179
  if (count >= policy.maximumOutputsPerArtifact) throw new CamelError("limit_exceeded", "Conversion output count exceeds its per-source bound.");
6134
6180
  counts.set(countKey, count + 1);
6181
+ const conversionRecordId = await sha256Hex(canonicalJson2({ conversionId: policy.conversionId, digest: policy.digest, source: sourceKey, ordinal: count }));
6182
+ const datumId = await sha256Hex(canonicalJson2({ conversionRecordId, value: value2 }));
6135
6183
  return createSafeInternal(value2, "atomic_conversion", {
6136
6184
  readers: source.label.readers,
6137
6185
  provenance: [...source.label.provenance, { kind: "converter", id: policy.conversionId, digest: policy.digest }],
6138
6186
  dependencies: dependenciesOf([source])
6187
+ }, {
6188
+ datumId,
6189
+ kind: policy.output.kind,
6190
+ conversionId: policy.conversionId,
6191
+ conversionDigest: policy.digest,
6192
+ conversionRecordId
6139
6193
  });
6140
6194
  }
6141
6195
  function validatePolicyShape(policy) {
@@ -6200,7 +6254,7 @@ function missingPolicy() {
6200
6254
  throw new CamelError("conversion_rejected", "Conversion policy is not registered.");
6201
6255
  }
6202
6256
 
6203
- // ../camel/dist/chunk-P2WQIAG6.js
6257
+ // ../camel/dist/chunk-VEAUXH4F.js
6204
6258
  function createCamelIngress(constants2 = []) {
6205
6259
  const byId = /* @__PURE__ */ new Map();
6206
6260
  for (const item of constants2) {
@@ -8190,7 +8244,7 @@ async function defaultReadOrigin(cwd) {
8190
8244
 
8191
8245
  // src/code-local-source.ts
8192
8246
  var import_node_child_process5 = require("child_process");
8193
- var import_node_crypto2 = require("crypto");
8247
+ var import_node_crypto3 = require("crypto");
8194
8248
  var SOURCE_LIMITS2 = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
8195
8249
  async function prepareCodeLocalSource(cwd, repository, readHead = readGitHead) {
8196
8250
  const headCommitSha = await readHead(cwd);
@@ -8241,12 +8295,12 @@ async function readGitHead(cwd) {
8241
8295
  return value2;
8242
8296
  }
8243
8297
  function digestText(value2) {
8244
- return `sha256:${(0, import_node_crypto2.createHash)("sha256").update(value2).digest("hex")}`;
8298
+ return `sha256:${(0, import_node_crypto3.createHash)("sha256").update(value2).digest("hex")}`;
8245
8299
  }
8246
8300
 
8247
8301
  // src/code-images.ts
8248
8302
  var import_node_child_process6 = require("child_process");
8249
- var import_node_crypto3 = require("crypto");
8303
+ var import_node_crypto4 = require("crypto");
8250
8304
  var import_promises11 = require("fs/promises");
8251
8305
  var import_node_os3 = require("os");
8252
8306
  var import_node_path14 = require("path");
@@ -8332,7 +8386,7 @@ async function embeddedPiImageName() {
8332
8386
  const bundle = await (0, import_promises11.readFile)(embeddedPiAssetPath()).catch(() => {
8333
8387
  throw new Error("CLI-embedded Pi runtime is missing; reinstall this exact @odla-ai/cli version");
8334
8388
  });
8335
- return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto3.createHash)("sha256").update(bundle).digest("hex")}`;
8389
+ return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto4.createHash)("sha256").update(bundle).digest("hex")}`;
8336
8390
  }
8337
8391
  async function buildEmbeddedPiImage(engine, image, run) {
8338
8392
  const context = await (0, import_promises11.mkdtemp)((0, import_node_path14.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
@@ -8737,7 +8791,7 @@ Start here:
8737
8791
 
8738
8792
  Usage:
8739
8793
  odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
8740
- odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
8794
+ odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod] [--ai-provider <byok-provider>]
8741
8795
  odla-ai doctor [--config odla.config.mjs]
8742
8796
  odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
8743
8797
  odla-ai config apply --plan <plan.json> [--idempotency-key <key>] [--email <odla-account>] [--json]
@@ -8760,16 +8814,22 @@ Usage:
8760
8814
  odla-ai app owners remove <email> [--email <odla-account>] [--json]
8761
8815
  odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
8762
8816
  odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8763
- odla-ai pm task list [--app <id>] [--column <c>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8817
+ odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8764
8818
  odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8765
8819
  odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8766
8820
  odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
8767
- 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]
8821
+ odla-ai pm task add --app <id> --title <t> [--column <backlog|ready|doing|review|done>] [--goal <id>|--alignment-decision <id>] [--assignee <id>] [--description <text>|--body <text>] [--acceptance <text>] [--execution <human|agent|either>] [--due <epoch-ms>] [--mutation-id <id>] [--json]
8822
+ odla-ai pm next --app <id> [--json]
8823
+ odla-ai pm watch --app <id> [--cursor <cursor>] [--entity goal|task|decision|bug] [--action created|updated|deleted|comment.created|comment.updated] [--state <state>] [--by <principalId>] [--self <principalId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
8824
+ odla-ai pm task ready <id> --expected-revision <n> [--goal <id>|--alignment-decision <id>] [--description <text>|--body <text>] [--acceptance <text>] [--execution <human|agent|either>] [--mutation-id <id>] [--json]
8825
+ odla-ai pm task claim <id> --expected-revision <n> [--mutation-id <id>] [--json]
8826
+ odla-ai pm task release <id> --expected-revision <n> [--mutation-id <id>] [--json]
8768
8827
  odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
8769
8828
  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]
8770
8829
  odla-ai pm <goal|task|decision|bug> get <id> [--json]
8830
+ odla-ai pm <goal|task|decision|bug> ref <id> [--json]
8771
8831
  odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
8772
- 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]
8832
+ odla-ai pm task set <id> [--title <t>|--column <backlog|ready|doing|review|done>|--rank <n>|--goal <id>|--no-goal|--alignment-decision <id>|--no-alignment-decision|--execution <human|agent|either>|--assignee <id>|--no-assignee|--description <text>|--body <text>|--acceptance <text>|--no-acceptance|--due <epoch-ms>|--no-due|--expected-revision <n>] [--mutation-id <id>] [--json]
8773
8833
  odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
8774
8834
  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]
8775
8835
  odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
@@ -9088,11 +9148,11 @@ async function discussList(ctx, parsed) {
9088
9148
  if (value2) query.set(param, value2);
9089
9149
  }
9090
9150
  const qs = query.toString();
9091
- const page = await request(ctx, "GET", `/topics${qs ? `?${qs}` : ""}`);
9092
- emit(ctx, page, () => {
9093
- ctx.out.log(`topics \u2014 ${page.topics.length} of ${page.total}`);
9151
+ const page2 = await request(ctx, "GET", `/topics${qs ? `?${qs}` : ""}`);
9152
+ emit(ctx, page2, () => {
9153
+ ctx.out.log(`topics \u2014 ${page2.topics.length} of ${page2.total}`);
9094
9154
  ctx.out.log("id state app replies subject");
9095
- for (const topic of page.topics) {
9155
+ for (const topic of page2.topics) {
9096
9156
  ctx.out.log(
9097
9157
  `${topic.id} ${state(topic)} ${topic.appId ?? ""} ${topic.replyCount} ${topic.subject}`
9098
9158
  );
@@ -9107,11 +9167,11 @@ async function discussRead(ctx, id, parsed) {
9107
9167
  limit: requestedLimit ?? "200",
9108
9168
  offset: requestedOffset ?? "0"
9109
9169
  });
9110
- const page = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
9170
+ const page2 = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
9111
9171
  emit(
9112
9172
  ctx,
9113
- page,
9114
- () => renderDiscussRead(ctx, page.topic, page.posts, page)
9173
+ page2,
9174
+ () => renderDiscussRead(ctx, page2.topic, page2.posts, page2)
9115
9175
  );
9116
9176
  return;
9117
9177
  }
@@ -9124,20 +9184,20 @@ async function discussRead(ctx, id, parsed) {
9124
9184
  let topic = null;
9125
9185
  let offset = 0;
9126
9186
  for (; ; ) {
9127
- const page = await request(
9187
+ const page2 = await request(
9128
9188
  ctx,
9129
9189
  "GET",
9130
9190
  `/topics/${encodeURIComponent(id)}?limit=200&offset=${offset}`
9131
9191
  );
9132
- topic = page.topic;
9133
- for (const post of page.posts) posts.set(post.id, post);
9134
- mergeDiscussPrincipals(projection, page);
9192
+ topic = page2.topic;
9193
+ for (const post of page2.posts) posts.set(post.id, post);
9194
+ mergeDiscussPrincipals(projection, page2);
9135
9195
  if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
9136
- if (!page.page?.hasMore) break;
9137
- if (page.page.nextOffset === null || page.page.nextOffset <= offset) {
9196
+ if (!page2.page?.hasMore) break;
9197
+ if (page2.page.nextOffset === null || page2.page.nextOffset <= offset) {
9138
9198
  throw new Error("discuss read failed: registry returned a non-advancing post page");
9139
9199
  }
9140
- offset = page.page.nextOffset;
9200
+ offset = page2.page.nextOffset;
9141
9201
  }
9142
9202
  const ordered = [...posts.values()].sort(
9143
9203
  (a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
@@ -9305,9 +9365,9 @@ async function discussWatch(ctx, topicId, parsed) {
9305
9365
  let firstSuccess = true;
9306
9366
  let consecutiveFailures = 0;
9307
9367
  for (; ; ) {
9308
- let page;
9368
+ let page2;
9309
9369
  try {
9310
- page = await getWatchPage(ctx, requestPath(topicId, app, cursor));
9370
+ page2 = await getWatchPage(ctx, requestPath(topicId, app, cursor));
9311
9371
  consecutiveFailures = 0;
9312
9372
  } catch (error) {
9313
9373
  if (!(error instanceof WatchRequestError) || !error.retryable) {
@@ -9343,25 +9403,25 @@ async function discussWatch(ctx, topicId, parsed) {
9343
9403
  await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
9344
9404
  continue;
9345
9405
  }
9346
- cursor = page.cursor;
9347
- const baseline = firstSuccess && page.events.length === 0;
9406
+ cursor = page2.cursor;
9407
+ const baseline = firstSuccess && page2.events.length === 0;
9348
9408
  if (baseline) {
9349
9409
  jsonl(ctx, parsed, {
9350
9410
  type: "checkpoint",
9351
- streamId: page.streamId,
9411
+ streamId: page2.streamId,
9352
9412
  cursor,
9353
- serverTime: page.serverTime
9413
+ serverTime: page2.serverTime
9354
9414
  });
9355
9415
  }
9356
9416
  firstSuccess = false;
9357
- const matching = page.events.filter((event) => {
9417
+ const matching = page2.events.filter((event) => {
9358
9418
  if (topicId && (event.type !== "message" || event.action !== "created")) return false;
9359
9419
  return (!by || event.actor.id === by) && (!self || event.actor.id !== self);
9360
9420
  });
9361
9421
  for (const event of matching) {
9362
9422
  jsonl(ctx, parsed, {
9363
9423
  type: "event",
9364
- streamId: page.streamId,
9424
+ streamId: page2.streamId,
9365
9425
  eventId: event.id,
9366
9426
  cursor: event.cursor,
9367
9427
  event
@@ -9370,9 +9430,9 @@ async function discussWatch(ctx, topicId, parsed) {
9370
9430
  if (matching.length > 0) {
9371
9431
  jsonl(ctx, parsed, {
9372
9432
  type: "checkpoint",
9373
- streamId: page.streamId,
9433
+ streamId: page2.streamId,
9374
9434
  cursor,
9375
- serverTime: page.serverTime
9435
+ serverTime: page2.serverTime
9376
9436
  });
9377
9437
  const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
9378
9438
  const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
@@ -9380,28 +9440,28 @@ async function discussWatch(ctx, topicId, parsed) {
9380
9440
  found: true,
9381
9441
  cursor,
9382
9442
  events: matching,
9383
- ...page.authors ? { authors: page.authors } : {},
9384
- ...page.principals ? { principals: page.principals } : {},
9443
+ ...page2.authors ? { authors: page2.authors } : {},
9444
+ ...page2.principals ? { principals: page2.principals } : {},
9385
9445
  ...posts && posts.length > 0 ? { posts } : {},
9386
9446
  ...topics && topics.length > 0 ? { topics } : {}
9387
9447
  });
9388
9448
  }
9389
- if (page.events.length > 0) {
9449
+ if (page2.events.length > 0) {
9390
9450
  jsonl(ctx, parsed, {
9391
9451
  type: "checkpoint",
9392
- streamId: page.streamId,
9452
+ streamId: page2.streamId,
9393
9453
  cursor,
9394
- serverTime: page.serverTime
9454
+ serverTime: page2.serverTime
9395
9455
  });
9396
9456
  } else if (!baseline) {
9397
9457
  jsonl(ctx, parsed, {
9398
9458
  type: "heartbeat",
9399
- streamId: page.streamId,
9459
+ streamId: page2.streamId,
9400
9460
  cursor,
9401
- serverTime: page.serverTime
9461
+ serverTime: page2.serverTime
9402
9462
  });
9403
9463
  }
9404
- if (page.hasMore) continue;
9464
+ if (page2.hasMore) continue;
9405
9465
  if (deadline !== void 0 && now() >= deadline) {
9406
9466
  return report2(ctx, parsed, { found: false, cursor });
9407
9467
  }
@@ -9520,7 +9580,13 @@ async function discussCommand(parsed, deps = {}) {
9520
9580
  }
9521
9581
  }
9522
9582
 
9523
- // src/pm-actions.ts
9583
+ // src/pm-action-core.ts
9584
+ var DONE = {
9585
+ goal: { status: "met", currentPct: 100 },
9586
+ task: { column: "done" },
9587
+ decision: { status: "accepted" },
9588
+ bug: { status: "fixed" }
9589
+ };
9524
9590
  var writeMutationId2 = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
9525
9591
  var FIELD_MAP = {
9526
9592
  title: { key: "title" },
@@ -9536,22 +9602,27 @@ var FIELD_MAP = {
9536
9602
  body: { key: "body" },
9537
9603
  target: { key: "targetPct", num: true },
9538
9604
  description: { key: "description" },
9539
- desc: { key: "description" }
9540
- };
9541
- var DONE = {
9542
- goal: { status: "met", currentPct: 100 },
9543
- task: { column: "done" },
9544
- decision: { status: "accepted" },
9545
- bug: { status: "fixed" }
9605
+ desc: { key: "description" },
9606
+ acceptance: { key: "acceptanceCriteria" },
9607
+ "alignment-decision": { key: "alignmentDecisionId" },
9608
+ execution: { key: "executionMode" },
9609
+ "expected-revision": { key: "expectedRevision", num: true }
9546
9610
  };
9547
9611
  async function pmRequest(ctx, method, path, body) {
9548
- const res = await ctx.doFetch(`${ctx.platformUrl}/registry/pm${path}`, {
9612
+ const response2 = await ctx.doFetch(`${ctx.platformUrl}/registry/pm${path}`, {
9549
9613
  method,
9550
- headers: { authorization: `Bearer ${ctx.token}`, "content-type": "application/json" },
9614
+ headers: {
9615
+ authorization: `Bearer ${ctx.token}`,
9616
+ "content-type": "application/json"
9617
+ },
9551
9618
  body: body === void 0 ? void 0 : JSON.stringify(body)
9552
9619
  });
9553
- const data = await res.json().catch(() => ({}));
9554
- if (!res.ok) throw new Error(`pm ${method} ${path} failed: ${data.error ?? `registry returned ${res.status}`}`);
9620
+ const data = await response2.json().catch(() => ({}));
9621
+ if (!response2.ok) {
9622
+ throw new Error(
9623
+ `pm ${method} ${path} failed: ${data.error ?? `registry returned ${response2.status}`}`
9624
+ );
9625
+ }
9555
9626
  return data;
9556
9627
  }
9557
9628
  function collectFields(parsed, allowClear) {
@@ -9574,20 +9645,32 @@ function collectEntityFields(entity, parsed, allowClear) {
9574
9645
  if (fields.description === void 0) fields.description = fields.body;
9575
9646
  delete fields.body;
9576
9647
  }
9648
+ if (entity === "task" && fields.column === "ready") fields.column = "todo";
9577
9649
  return fields;
9578
9650
  }
9579
- function statusCol(entity, r) {
9580
- if (entity === "bug") return `${r.status ?? ""}/${r.severity ?? ""}`;
9581
- if (entity === "task") return String(r.column ?? "");
9582
- return String(r.status ?? "");
9651
+ function statusCol(entity, record10) {
9652
+ if (entity === "bug") return `${record10.status ?? ""}/${record10.severity ?? ""}`;
9653
+ if (entity === "task") {
9654
+ const state2 = record10.column === "todo" ? "ready" : String(record10.column ?? "");
9655
+ return record10.revision ? `${state2}; r${record10.revision}` : state2;
9656
+ }
9657
+ return String(record10.status ?? "");
9583
9658
  }
9584
- function printRecord(ctx, entity, r) {
9585
- ctx.out.log(`${r.id} [${statusCol(entity, r)}] ${r.appId} ${r.title ?? ""}`);
9659
+ function referenceMarkup(entity, record10) {
9660
+ const label = (record10.title?.trim() || `${entity} ${record10.id}`).replaceAll("]", ")");
9661
+ return `@[${label}](pm:${entity}/${record10.id})`;
9662
+ }
9663
+ function printRecord(ctx, entity, record10) {
9664
+ ctx.out.log(
9665
+ `${record10.id} [${statusCol(entity, record10)}] ${record10.appId} ${record10.title ?? ""}`
9666
+ );
9586
9667
  }
9587
9668
  function emit2(ctx, value2, human) {
9588
9669
  if (ctx.json) ctx.out.log(JSON.stringify(value2, null, 2));
9589
9670
  else human();
9590
9671
  }
9672
+
9673
+ // src/pm-actions.ts
9591
9674
  async function pmList(ctx, entity, parsed) {
9592
9675
  const q = new URLSearchParams();
9593
9676
  const filters = {
@@ -9601,7 +9684,8 @@ async function pmList(ctx, entity, parsed) {
9601
9684
  const app = stringOpt(parsed.options.app) ?? ctx.appId;
9602
9685
  if (app) q.set("app", app);
9603
9686
  for (const [flag, param] of Object.entries(filters)) {
9604
- const v = stringOpt(parsed.options[flag]);
9687
+ const raw = stringOpt(parsed.options[flag]);
9688
+ const v = entity === "task" && flag === "column" && raw === "ready" ? "todo" : raw;
9605
9689
  if (v) q.set(param, v);
9606
9690
  }
9607
9691
  for (const opt of ["q", "limit", "offset"]) {
@@ -9609,11 +9693,11 @@ async function pmList(ctx, entity, parsed) {
9609
9693
  if (v) q.set(opt, v);
9610
9694
  }
9611
9695
  const qs = q.toString();
9612
- const page = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
9613
- emit2(ctx, page, () => {
9614
- ctx.out.log(`${entity} \u2014 ${page.records.length} of ${page.total}`);
9696
+ const page2 = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
9697
+ emit2(ctx, page2, () => {
9698
+ ctx.out.log(`${entity} \u2014 ${page2.records.length} of ${page2.total}`);
9615
9699
  ctx.out.log("id state app title");
9616
- for (const r of page.records) printRecord(ctx, entity, r);
9700
+ for (const r of page2.records) printRecord(ctx, entity, r);
9617
9701
  });
9618
9702
  }
9619
9703
  async function pmAdd(ctx, entity, parsed) {
@@ -9634,6 +9718,17 @@ async function pmGet(ctx, entity, id) {
9634
9718
  const { record: record10 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
9635
9719
  emit2(ctx, record10, () => printRecord(ctx, entity, record10));
9636
9720
  }
9721
+ async function pmReference(ctx, entity, id) {
9722
+ const { record: record10 } = await pmRequest(
9723
+ ctx,
9724
+ "GET",
9725
+ `/${entity}/${encodeURIComponent(id)}`
9726
+ );
9727
+ const markup = referenceMarkup(entity, record10);
9728
+ emit2(ctx, { kind: `pm:${entity}`, id: record10.id, label: record10.title ?? "", markup }, () => {
9729
+ ctx.out.log(markup);
9730
+ });
9731
+ }
9637
9732
  async function pmSet(ctx, entity, id, parsed) {
9638
9733
  const patch2 = collectEntityFields(entity, parsed, true);
9639
9734
  if (Object.keys(patch2).length === 0)
@@ -9654,6 +9749,36 @@ async function pmDone(ctx, entity, id, parsed) {
9654
9749
  });
9655
9750
  emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
9656
9751
  }
9752
+ async function pmTaskLifecycle(ctx, id, action2, parsed) {
9753
+ const rawRevision = stringOpt(parsed.options["expected-revision"]);
9754
+ const expectedRevision = Number(rawRevision);
9755
+ if (!rawRevision || !Number.isSafeInteger(expectedRevision) || expectedRevision < 1) {
9756
+ throw new Error(`pm task ${action2} needs --expected-revision <n>`);
9757
+ }
9758
+ const mutationId = writeMutationId2(parsed);
9759
+ const res = action2 === "ready" ? await pmRequest(
9760
+ ctx,
9761
+ "PATCH",
9762
+ `/task/${encodeURIComponent(id)}`,
9763
+ {
9764
+ patch: {
9765
+ ...collectEntityFields("task", parsed, true),
9766
+ column: "todo",
9767
+ expectedRevision
9768
+ },
9769
+ mutationId
9770
+ }
9771
+ ) : await pmRequest(
9772
+ ctx,
9773
+ "POST",
9774
+ `/task/${encodeURIComponent(id)}/${action2}`,
9775
+ { expectedRevision, mutationId }
9776
+ );
9777
+ emit2(ctx, res, () => {
9778
+ const state2 = res.record ? statusCol("task", res.record) : action2;
9779
+ ctx.out.log(`task ${id} \u2192 ${state2}`);
9780
+ });
9781
+ }
9657
9782
  async function allRecords(ctx, entity, appId) {
9658
9783
  const records = [];
9659
9784
  for (; ; ) {
@@ -9662,11 +9787,48 @@ async function allRecords(ctx, entity, appId) {
9662
9787
  limit: "100",
9663
9788
  offset: String(records.length)
9664
9789
  });
9665
- const page = await pmRequest(ctx, "GET", `/${entity}?${q}`);
9666
- records.push(...page.records);
9667
- if (records.length >= page.total || page.records.length === 0) return records;
9790
+ const page2 = await pmRequest(ctx, "GET", `/${entity}?${q}`);
9791
+ records.push(...page2.records);
9792
+ if (records.length >= page2.total || page2.records.length === 0) return records;
9668
9793
  }
9669
9794
  }
9795
+ async function pmNext(ctx, parsed) {
9796
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
9797
+ if (!appId) throw new Error("pm next needs --app <appId>");
9798
+ const [goals, tasks] = await Promise.all([
9799
+ allRecords(ctx, "goal", appId),
9800
+ allRecords(ctx, "task", appId)
9801
+ ]);
9802
+ const result = {
9803
+ appId,
9804
+ openGoals: goals.filter((record10) => record10.status === "open"),
9805
+ doing: tasks.filter((record10) => record10.column === "doing"),
9806
+ ready: tasks.filter((record10) => record10.column === "todo")
9807
+ };
9808
+ emit2(ctx, result, () => {
9809
+ ctx.out.log(`${appId}: goal-aligned work intake (read only)`);
9810
+ for (const [label, records] of [
9811
+ ["doing", result.doing],
9812
+ ["ready", result.ready],
9813
+ ["open goals", result.openGoals]
9814
+ ]) {
9815
+ ctx.out.log(`${label}:`);
9816
+ if (!records.length) ctx.out.log("- (none)");
9817
+ else for (const record10 of records) printRecord(
9818
+ ctx,
9819
+ label === "open goals" ? "goal" : "task",
9820
+ record10
9821
+ );
9822
+ }
9823
+ if (!result.openGoals.length) {
9824
+ ctx.out.log("next: discuss alignment with the user before creating or claiming project work");
9825
+ } else if (!result.ready.length) {
9826
+ ctx.out.log("next: refine a linked Backlog task and mark it Ready");
9827
+ } else {
9828
+ ctx.out.log("next: review a Ready task, then claim it with its revision");
9829
+ }
9830
+ });
9831
+ }
9670
9832
  async function pmHandoff(ctx, parsed) {
9671
9833
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
9672
9834
  if (!appId) throw new Error("pm handoff needs --app <appId>");
@@ -9706,6 +9868,12 @@ async function pmHandoff(ctx, parsed) {
9706
9868
  }
9707
9869
  });
9708
9870
  }
9871
+ async function pmRemove(ctx, entity, id) {
9872
+ await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id)}`);
9873
+ ctx.out.log(`deleted ${entity} ${id}`);
9874
+ }
9875
+
9876
+ // src/pm-comments.ts
9709
9877
  async function pmComment(ctx, entity, id, parsed) {
9710
9878
  const body = stringOpt(parsed.options.body);
9711
9879
  if (!body) throw new Error('pm comment needs --body "..."');
@@ -9716,19 +9884,218 @@ async function pmComment(ctx, entity, id, parsed) {
9716
9884
  ctx.out.log(`commented on ${entity} ${id}`);
9717
9885
  }
9718
9886
  async function pmComments(ctx, entity, id) {
9719
- const { messages } = await pmRequest(
9720
- ctx,
9721
- "GET",
9722
- `/${entity}/${encodeURIComponent(id)}/comments`
9723
- );
9887
+ const { messages } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}/comments`);
9724
9888
  emit2(ctx, messages, () => {
9725
9889
  if (messages.length === 0) ctx.out.log("(no comments)");
9726
- else for (const m of messages) ctx.out.log(`[${m.authorId ?? "?"}] ${m.body ?? ""}`);
9890
+ else for (const message2 of messages) {
9891
+ ctx.out.log(
9892
+ `[${message2.authorId ?? "?"}] ${message2.markup ?? message2.body ?? ""}`
9893
+ );
9894
+ }
9727
9895
  });
9728
9896
  }
9729
- async function pmRemove(ctx, entity, id) {
9730
- await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id)}`);
9731
- ctx.out.log(`deleted ${entity} ${id}`);
9897
+
9898
+ // src/pm-watch-types.ts
9899
+ var PmWatchCheckpointError = class extends Error {
9900
+ constructor(cursor, streamId) {
9901
+ super("PM work cursor requires a new checkpoint");
9902
+ this.cursor = cursor;
9903
+ this.streamId = streamId;
9904
+ this.name = "PmWatchCheckpointError";
9905
+ }
9906
+ cursor;
9907
+ streamId;
9908
+ code = "checkpoint_required";
9909
+ };
9910
+ var PmWatchRequestError = class extends Error {
9911
+ constructor(message2, retryable, status) {
9912
+ super(message2);
9913
+ this.retryable = retryable;
9914
+ this.status = status;
9915
+ this.name = "PmWatchRequestError";
9916
+ }
9917
+ retryable;
9918
+ status;
9919
+ };
9920
+
9921
+ // src/pm-watch.ts
9922
+ var DEFAULT_INTERVAL_MS2 = 15e3;
9923
+ var MAX_CONSECUTIVE_FAILURES2 = 5;
9924
+ var MAX_BACKOFF_MS2 = 3e4;
9925
+ function positiveNumber(parsed, flag, fallback) {
9926
+ const raw = stringOpt(parsed.options[flag]);
9927
+ const value2 = raw == null ? NaN : Number(raw);
9928
+ return Number.isFinite(value2) && value2 > 0 ? value2 : fallback;
9929
+ }
9930
+ function jsonl2(ctx, parsed, value2) {
9931
+ if (parsed.options.jsonl === true) ctx.out.log(JSON.stringify({ v: 1, ...value2 }));
9932
+ }
9933
+ async function page(ctx, appId, cursor) {
9934
+ const params = new URLSearchParams({ app: appId });
9935
+ if (cursor) params.set("cursor", cursor);
9936
+ let response2;
9937
+ try {
9938
+ response2 = await ctx.doFetch(`${ctx.platformUrl}/registry/pm/work/watch?${params}`, {
9939
+ headers: { authorization: `Bearer ${ctx.token}` }
9940
+ });
9941
+ } catch (error) {
9942
+ throw new PmWatchRequestError(
9943
+ `pm watch request failed: ${error instanceof Error ? error.message : String(error)}`,
9944
+ true
9945
+ );
9946
+ }
9947
+ const data = await response2.json().catch(() => ({}));
9948
+ if (response2.status === 409 && data.code === "checkpoint_required") {
9949
+ throw new PmWatchCheckpointError(data.cursor, data.streamId);
9950
+ }
9951
+ if (!response2.ok) {
9952
+ throw new PmWatchRequestError(
9953
+ `pm watch failed: ${data.error ?? `registry returned ${response2.status}`}`,
9954
+ response2.status === 429 || response2.status >= 500,
9955
+ response2.status
9956
+ );
9957
+ }
9958
+ return data;
9959
+ }
9960
+ function recordState(record10) {
9961
+ if (record10.column) return record10.column === "todo" ? "ready" : record10.column;
9962
+ return String(record10.status ?? "");
9963
+ }
9964
+ function eventRecord(event) {
9965
+ return event.payload.payload;
9966
+ }
9967
+ function eventLabel(event) {
9968
+ const record10 = eventRecord(event);
9969
+ if (record10) return String(record10.title ?? event.payload.entityId);
9970
+ const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
9971
+ return body || event.payload.entityId;
9972
+ }
9973
+ function report3(ctx, parsed, result) {
9974
+ if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
9975
+ else if (parsed.options.jsonl !== true && result.found) {
9976
+ for (const event of result.events ?? []) {
9977
+ const record10 = eventRecord(event);
9978
+ const state2 = record10 ? recordState(record10) : "comment";
9979
+ ctx.out.log(
9980
+ `${event.id} ${event.type} ${state2}${record10?.revision ? `; r${record10.revision}` : ""} ${eventLabel(event)}`
9981
+ );
9982
+ }
9983
+ }
9984
+ return result;
9985
+ }
9986
+ async function pmWatch(ctx, parsed) {
9987
+ if (ctx.json && parsed.options.jsonl === true) {
9988
+ throw new Error("--json and --jsonl cannot be combined");
9989
+ }
9990
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
9991
+ if (!appId) throw new Error("pm watch needs --app <appId>");
9992
+ const sleep = ctx.sleep ?? ((ms) => new Promise((resolve13) => setTimeout(resolve13, ms)));
9993
+ const now = ctx.now ?? Date.now;
9994
+ const intervalMs = (positiveNumber(parsed, "interval", DEFAULT_INTERVAL_MS2 / 1e3) ?? DEFAULT_INTERVAL_MS2 / 1e3) * 1e3;
9995
+ const timeoutSeconds = positiveNumber(parsed, "timeout");
9996
+ const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
9997
+ const entity = stringOpt(parsed.options.entity);
9998
+ const action2 = stringOpt(parsed.options.action);
9999
+ const wantedState = stringOpt(parsed.options.state)?.toLowerCase();
10000
+ const by = stringOpt(parsed.options.by);
10001
+ const self = stringOpt(parsed.options.self);
10002
+ let cursor = stringOpt(parsed.options.cursor);
10003
+ let firstSuccess = true;
10004
+ let consecutiveFailures = 0;
10005
+ for (; ; ) {
10006
+ let current;
10007
+ try {
10008
+ current = await page(ctx, appId, cursor);
10009
+ consecutiveFailures = 0;
10010
+ } catch (error) {
10011
+ if (error instanceof PmWatchCheckpointError) {
10012
+ jsonl2(ctx, parsed, {
10013
+ type: "status",
10014
+ state: "checkpoint_required",
10015
+ retryable: false,
10016
+ ...error.cursor ? { cursor: error.cursor } : {},
10017
+ ...error.streamId ? { streamId: error.streamId } : {}
10018
+ });
10019
+ throw error;
10020
+ }
10021
+ if (!(error instanceof PmWatchRequestError) || !error.retryable) throw error;
10022
+ consecutiveFailures++;
10023
+ jsonl2(ctx, parsed, {
10024
+ type: "status",
10025
+ state: "degraded",
10026
+ retryable: true,
10027
+ attempt: consecutiveFailures,
10028
+ ...cursor ? { cursor } : {},
10029
+ ...error.status ? { status: error.status } : {}
10030
+ });
10031
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
10032
+ if (deadline !== void 0 && now() >= deadline) {
10033
+ return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
10034
+ }
10035
+ const backoff = Math.min(
10036
+ MAX_BACKOFF_MS2,
10037
+ Math.min(intervalMs, 1e3) * 2 ** (consecutiveFailures - 1)
10038
+ );
10039
+ await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
10040
+ continue;
10041
+ }
10042
+ cursor = current.cursor;
10043
+ const baseline = firstSuccess && current.events.length === 0;
10044
+ if (baseline) {
10045
+ jsonl2(ctx, parsed, {
10046
+ type: "checkpoint",
10047
+ streamId: current.streamId,
10048
+ cursor,
10049
+ serverTime: current.serverTime
10050
+ });
10051
+ }
10052
+ firstSuccess = false;
10053
+ const matching = current.events.filter((event) => {
10054
+ const record10 = eventRecord(event);
10055
+ const state2 = record10 ? recordState(record10).toLowerCase() : "";
10056
+ return (!entity || event.payload.entityKind === entity) && (!action2 || event.payload.action === action2) && (!wantedState || state2 === wantedState || wantedState === "todo" && state2 === "ready") && (!by || event.actor.id === by) && (!self || event.actor.id !== self);
10057
+ });
10058
+ for (const event of matching) {
10059
+ jsonl2(ctx, parsed, {
10060
+ type: "event",
10061
+ streamId: current.streamId,
10062
+ eventId: event.id,
10063
+ cursor: event.cursor,
10064
+ event
10065
+ });
10066
+ }
10067
+ if (matching.length > 0) {
10068
+ jsonl2(ctx, parsed, {
10069
+ type: "checkpoint",
10070
+ streamId: current.streamId,
10071
+ cursor,
10072
+ serverTime: current.serverTime
10073
+ });
10074
+ return report3(ctx, parsed, { found: true, cursor, events: matching });
10075
+ }
10076
+ if (current.events.length > 0) {
10077
+ jsonl2(ctx, parsed, {
10078
+ type: "checkpoint",
10079
+ streamId: current.streamId,
10080
+ cursor,
10081
+ serverTime: current.serverTime
10082
+ });
10083
+ } else if (!baseline) {
10084
+ jsonl2(ctx, parsed, {
10085
+ type: "heartbeat",
10086
+ streamId: current.streamId,
10087
+ cursor,
10088
+ serverTime: current.serverTime
10089
+ });
10090
+ }
10091
+ if (current.hasMore) continue;
10092
+ if (deadline !== void 0 && now() >= deadline) {
10093
+ return report3(ctx, parsed, { found: false, cursor });
10094
+ }
10095
+ await sleep(
10096
+ deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
10097
+ );
10098
+ }
9732
10099
  }
9733
10100
 
9734
10101
  // src/pm-command.ts
@@ -9749,7 +10116,11 @@ var ACTION_OPTIONS = {
9749
10116
  done: ["mutation-id"],
9750
10117
  comment: ["body", "mutation-id"],
9751
10118
  comments: [],
9752
- rm: []
10119
+ rm: [],
10120
+ ready: ["goal", "alignment-decision", "execution", "description", "desc", "body", "acceptance", "expected-revision", "mutation-id"],
10121
+ claim: ["expected-revision", "mutation-id"],
10122
+ release: ["expected-revision", "mutation-id"],
10123
+ ref: []
9753
10124
  };
9754
10125
  var ENTITY_OPTIONS = {
9755
10126
  goal: {
@@ -9760,8 +10131,8 @@ var ENTITY_OPTIONS = {
9760
10131
  },
9761
10132
  task: {
9762
10133
  list: ["column", "goal", "assignee"],
9763
- add: ["column", "goal", "assignee", "due", "description", "desc", "body"],
9764
- set: ["title", "column", "rank", "goal", "assignee", "due", "description", "desc", "body"],
10134
+ add: ["column", "goal", "alignment-decision", "execution", "assignee", "due", "description", "desc", "body", "acceptance"],
10135
+ set: ["title", "column", "rank", "goal", "alignment-decision", "execution", "assignee", "due", "description", "desc", "body", "acceptance", "expected-revision"],
9765
10136
  done: []
9766
10137
  },
9767
10138
  decision: {
@@ -9812,11 +10183,33 @@ async function buildContext2(parsed, deps) {
9812
10183
  json: parsed.options.json === true,
9813
10184
  // Preserve PM's existing cross-project default when a config is present;
9814
10185
  // only an explicit remote-agent environment or profile selects an app.
9815
- appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
10186
+ appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0,
10187
+ ...deps.sleep ? { sleep: deps.sleep } : {},
10188
+ ...deps.now ? { now: deps.now } : {}
9816
10189
  };
9817
10190
  }
9818
10191
  async function pmCommand(parsed, deps = {}) {
9819
10192
  const word = parsed.positionals[1] ?? "";
10193
+ if (word === "next") {
10194
+ assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
10195
+ return pmNext(await buildContext2(parsed, deps), parsed);
10196
+ }
10197
+ if (word === "watch") {
10198
+ assertArgs(parsed, [
10199
+ ...COMMON_OPTIONS,
10200
+ "app",
10201
+ "cursor",
10202
+ "interval",
10203
+ "timeout",
10204
+ "jsonl",
10205
+ "entity",
10206
+ "action",
10207
+ "state",
10208
+ "by",
10209
+ "self"
10210
+ ], 2);
10211
+ return pmWatch(await buildContext2(parsed, deps), parsed).then(() => void 0);
10212
+ }
9820
10213
  if (word === "handoff") {
9821
10214
  assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
9822
10215
  return pmHandoff(await buildContext2(parsed, deps), parsed);
@@ -9827,6 +10220,9 @@ async function pmCommand(parsed, deps = {}) {
9827
10220
  const action2 = canonicalAction(requestedAction);
9828
10221
  if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|comment|comments|rm.`);
9829
10222
  assertArgs(parsed, allowedOptions(entity, action2), 4);
10223
+ if ((action2 === "ready" || action2 === "claim" || action2 === "release") && entity !== "task") {
10224
+ throw new Error(`pm ${action2} is only valid for tasks`);
10225
+ }
9830
10226
  const ctx = await buildContext2(parsed, deps);
9831
10227
  const id = parsed.positionals[3];
9832
10228
  switch (action2) {
@@ -9846,6 +10242,12 @@ async function pmCommand(parsed, deps = {}) {
9846
10242
  return pmComments(ctx, entity, requireId2(id, action2));
9847
10243
  case "rm":
9848
10244
  return pmRemove(ctx, entity, requireId2(id, action2));
10245
+ case "ref":
10246
+ return pmReference(ctx, entity, requireId2(id, action2));
10247
+ case "ready":
10248
+ case "claim":
10249
+ case "release":
10250
+ return pmTaskLifecycle(ctx, requireId2(id, action2), action2, parsed);
9849
10251
  }
9850
10252
  }
9851
10253
 
@@ -10382,7 +10784,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
10382
10784
  const payload = await postJson3(doFetch, `${base}/query`, dbKey, {
10383
10785
  query: { [seed.ns]: { $: { where: { [seed.key.attr]: seed.key.value }, limit: 1 } } }
10384
10786
  });
10385
- const rows = isRecord7(payload) && isRecord7(payload.result) ? payload.result[seed.ns] : void 0;
10787
+ const rows = isRecord8(payload) && isRecord8(payload.result) ? payload.result[seed.ns] : void 0;
10386
10788
  if (!Array.isArray(rows)) {
10387
10789
  throw new Error(`${env}: integration ${integration.id} seed ${seed.id} query returned an invalid response`);
10388
10790
  }
@@ -10414,7 +10816,7 @@ async function postJson3(doFetch, url, bearer, body) {
10414
10816
  if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await responseText(res)}`);
10415
10817
  return res.json().catch(() => ({}));
10416
10818
  }
10417
- function isRecord7(value2) {
10819
+ function isRecord8(value2) {
10418
10820
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
10419
10821
  }
10420
10822
  async function responseText(res) {
@@ -10484,14 +10886,14 @@ async function mintDbKey(opts, tenantId) {
10484
10886
  appId: tenantId
10485
10887
  })
10486
10888
  });
10487
- if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText5(created)}`);
10889
+ if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText6(created)}`);
10488
10890
  res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
10489
10891
  method: "POST",
10490
10892
  headers,
10491
10893
  body: "{}"
10492
10894
  });
10493
10895
  }
10494
- if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
10896
+ if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText6(res)}`);
10495
10897
  const body = await res.json();
10496
10898
  if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
10497
10899
  return body.key;
@@ -10507,12 +10909,12 @@ async function issueO11yToken(opts) {
10507
10909
  `o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --rotate-o11y-token --push-secrets" to replace it explicitly`
10508
10910
  );
10509
10911
  }
10510
- if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText5(res)}`);
10912
+ if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText6(res)}`);
10511
10913
  const body = await res.json();
10512
10914
  if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
10513
10915
  return body.token;
10514
10916
  }
10515
- async function safeText5(res) {
10917
+ async function safeText6(res) {
10516
10918
  try {
10517
10919
  return redactSecrets((await res.text()).slice(0, 500));
10518
10920
  } catch {
@@ -10555,7 +10957,7 @@ async function provision(options) {
10555
10957
  const namespaces = Object.keys(integration.schema?.entities ?? {}).length;
10556
10958
  out.log(` integration.${integration.id}: ${namespaces} namespaces, ${integration.seeds?.length ?? 0} seeds, ${integration.probes?.length ?? 0} smoke probes`);
10557
10959
  }
10558
- out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
10960
+ out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "hosted" : "not enabled"}`);
10559
10961
  if (cfg.services.includes("calendar")) {
10560
10962
  for (const env of cfg.envs) {
10561
10963
  const calendar = calendarServiceConfig(cfg, env);
@@ -10606,11 +11008,11 @@ async function provision(options) {
10606
11008
  for (const service of serviceOrder) {
10607
11009
  if (service === "ai") {
10608
11010
  if (cfg.ai?.provider) {
10609
- await apps.setAi(cfg.app.id, env, { provider: cfg.ai.provider, ...cfg.ai.model ? { model: cfg.ai.model } : {} });
10610
- out.log(`${env}: ai configured (${cfg.ai.provider}${cfg.ai.model ? `/${cfg.ai.model}` : ""})`);
11011
+ await apps.setAi(cfg.app.id, env, { mode: "byok", provider: cfg.ai.provider, ...cfg.ai.model ? { model: cfg.ai.model } : {} });
11012
+ out.log(`${env}: ai configured (byok ${cfg.ai.provider}${cfg.ai.model ? `/${cfg.ai.model}` : ""})`);
10611
11013
  } else {
10612
- await apps.setService(cfg.app.id, "ai", true, { env });
10613
- out.log(`${env}: ai enabled`);
11014
+ await apps.setAi(cfg.app.id, env, { mode: "hosted", ...cfg.ai?.model ? { model: cfg.ai.model } : {} });
11015
+ out.log(`${env}: ai configured (hosted${cfg.ai?.model ? `/${cfg.ai.model}` : ""})`);
10614
11016
  }
10615
11017
  } else {
10616
11018
  const config = service === "calendar" ? calendarServiceConfig(cfg, env) : void 0;
@@ -10735,12 +11137,23 @@ var PM_ACTIONS = {
10735
11137
  done: {},
10736
11138
  comment: {},
10737
11139
  comments: {},
11140
+ ref: {},
10738
11141
  rm: {},
10739
11142
  delete: {}
10740
11143
  };
10741
- var PM_ENTITIES = Object.fromEntries(
10742
- ["goal", "conformance", "task", "kanban", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
10743
- );
11144
+ var PM_TASK_ACTIONS = {
11145
+ ...PM_ACTIONS,
11146
+ ready: {},
11147
+ claim: {},
11148
+ release: {}
11149
+ };
11150
+ var PM_ENTITIES = {
11151
+ ...Object.fromEntries(
11152
+ ["goal", "conformance", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
11153
+ ),
11154
+ task: PM_TASK_ACTIONS,
11155
+ kanban: PM_TASK_ACTIONS
11156
+ };
10744
11157
  var COMMAND_SURFACE = {
10745
11158
  agent: { jobs: {}, retry: {} },
10746
11159
  admin: {
@@ -10793,7 +11206,9 @@ var COMMAND_SURFACE = {
10793
11206
  },
10794
11207
  pm: {
10795
11208
  ...PM_ENTITIES,
10796
- handoff: {}
11209
+ handoff: {},
11210
+ next: {},
11211
+ watch: {}
10797
11212
  },
10798
11213
  provision: {},
10799
11214
  runbook: {
@@ -10966,12 +11381,12 @@ async function call(ctx, method, path, body) {
10966
11381
  throw new Error(message2);
10967
11382
  }
10968
11383
  async function bySlug(ctx, slug) {
10969
- const page = await call(
11384
+ const page2 = await call(
10970
11385
  ctx,
10971
11386
  "GET",
10972
11387
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
10973
11388
  );
10974
- const found = page.records[0];
11389
+ const found = page2.records[0];
10975
11390
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
10976
11391
  return found;
10977
11392
  }
@@ -10985,11 +11400,11 @@ async function runbookList(ctx, all, query) {
10985
11400
  const params = new URLSearchParams();
10986
11401
  if (!all) params.set("app", ctx.appId);
10987
11402
  if (query) params.set("q", query);
10988
- const page = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
10989
- if (ctx.json) return ctx.out.log(JSON.stringify(page, null, 2));
10990
- if (!page.records.length) return ctx.out.log("(no runbooks)");
11403
+ const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
11404
+ if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
11405
+ if (!page2.records.length) return ctx.out.log("(no runbooks)");
10991
11406
  ctx.out.log(["SLUG", "STATUS", "V", "SCOPE", "UPDATED", "TITLE"].join(" "));
10992
- for (const r of page.records)
11407
+ for (const r of page2.records)
10993
11408
  ctx.out.log(
10994
11409
  [r.slug, r.status, `v${r.version}`, r.appId === PLATFORM_SCOPE ? "platform" : r.appId, stamp(r.updatedAt), r.title].join(" ")
10995
11410
  );
@@ -11044,12 +11459,12 @@ async function runbookVisibility(ctx, slug, visibility) {
11044
11459
  }
11045
11460
  async function runbookHistory(ctx, slug) {
11046
11461
  const runbook = await bySlug(ctx, slug);
11047
- const page = await call(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
11048
- if (ctx.json) return ctx.out.log(JSON.stringify(page, null, 2));
11462
+ const page2 = await call(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
11463
+ if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
11049
11464
  ctx.out.log(`${slug} is at v${runbook.version}`);
11050
- if (!page.records.length) return ctx.out.log("(no earlier versions)");
11465
+ if (!page2.records.length) return ctx.out.log("(no earlier versions)");
11051
11466
  ctx.out.log(["V", "WHEN", "BY", "NOTE"].join(" "));
11052
- for (const r of page.records)
11467
+ for (const r of page2.records)
11053
11468
  ctx.out.log([`v${r.version}`, stamp(r.createdAt), r.ownerEmail ?? "", r.note ?? ""].join(" "));
11054
11469
  }
11055
11470
  async function runbookRevert(ctx, slug, version) {
@@ -11124,12 +11539,12 @@ ${counts.created} created, ${counts.updated} updated, ${counts.unchanged} unchan
11124
11539
  );
11125
11540
  }
11126
11541
  async function upsert(ctx, r, visibility) {
11127
- const page = await call(
11542
+ const page2 = await call(
11128
11543
  ctx,
11129
11544
  "GET",
11130
11545
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(r.slug)}&limit=1`
11131
11546
  );
11132
- const found = page.records[0];
11547
+ const found = page2.records[0];
11133
11548
  if (!found) {
11134
11549
  await call(ctx, "POST", "/runbook", {
11135
11550
  appId: ctx.appId,
@@ -11385,7 +11800,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
11385
11800
  return out;
11386
11801
  }
11387
11802
  var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
11388
- function report3(ctx, impacts) {
11803
+ function report4(ctx, impacts) {
11389
11804
  const covered = impacts.filter((i) => i.runbooks.length);
11390
11805
  ctx.out.log(
11391
11806
  `${impacts.length} changed surface${impacts.length === 1 ? "" : "s"}; ${covered.length} covered by a runbook. Reread each one and fix any step this change made wrong.`
@@ -11423,7 +11838,7 @@ async function runbookImpact(ctx, options, deps = {}) {
11423
11838
  }
11424
11839
  const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
11425
11840
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
11426
- report3(ctx, impacts);
11841
+ report4(ctx, impacts);
11427
11842
  }
11428
11843
 
11429
11844
  // src/runbook-lint.ts
@@ -11461,17 +11876,17 @@ function lintRunbook(runbook, installed) {
11461
11876
  async function runbookLint(ctx, all) {
11462
11877
  const params = new URLSearchParams();
11463
11878
  if (!all) params.set("app", ctx.appId);
11464
- const page = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
11879
+ const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
11465
11880
  const installed = { "@odla-ai/cli": cliVersion() };
11466
- const findings = page.records.flatMap((runbook) => lintRunbook(runbook, installed));
11467
- if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page.records.length, findings }, null, 2));
11468
- if (!page.records.length) return ctx.out.log("(no runbooks in scope)");
11881
+ const findings = page2.records.flatMap((runbook) => lintRunbook(runbook, installed));
11882
+ if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page2.records.length, findings }, null, 2));
11883
+ if (!page2.records.length) return ctx.out.log("(no runbooks in scope)");
11469
11884
  if (!findings.length) {
11470
11885
  return ctx.out.log(
11471
- `${page.records.length} runbook${page.records.length === 1 ? "" : "s"} checked; every command they name is real for @odla-ai/cli ${cliVersion()}.`
11886
+ `${page2.records.length} runbook${page2.records.length === 1 ? "" : "s"} checked; every command they name is real for @odla-ai/cli ${cliVersion()}.`
11472
11887
  );
11473
11888
  }
11474
- ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${page.records.length} runbooks:`);
11889
+ ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${page2.records.length} runbooks:`);
11475
11890
  for (const finding of findings) {
11476
11891
  const scope = finding.appId === PLATFORM_SCOPE ? "" : ` --app ${finding.appId}`;
11477
11892
  ctx.out.log("");
@@ -11534,12 +11949,12 @@ async function runbookAsk(ctx, question, all) {
11534
11949
  ctx.out.log(JSDOC_POINTER);
11535
11950
  }
11536
11951
  async function runbookComment(ctx, slug, body) {
11537
- const page = await call(
11952
+ const page2 = await call(
11538
11953
  ctx,
11539
11954
  "GET",
11540
11955
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
11541
11956
  );
11542
- const found = page.records[0];
11957
+ const found = page2.records[0];
11543
11958
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
11544
11959
  await call(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
11545
11960
  ctx.out.log(`commented on ${slug} (v${found.version})`);
@@ -11591,12 +12006,12 @@ var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
11591
12006
 
11592
12007
  // src/runbook-edit-flow.ts
11593
12008
  async function editRunbook(ctx, slug, deps = {}) {
11594
- const page = await call(
12009
+ const page2 = await call(
11595
12010
  ctx,
11596
12011
  "GET",
11597
12012
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
11598
12013
  );
11599
- const found = page.records[0];
12014
+ const found = page2.records[0];
11600
12015
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
11601
12016
  ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
11602
12017
  const body = await editText(found.body, slug, deps);
@@ -12017,31 +12432,31 @@ function printHostedJob(out, job, platform, appId) {
12017
12432
  url.searchParams.set("job", job.jobId);
12018
12433
  out.log(` Studio: ${url.toString()}`);
12019
12434
  }
12020
- function printHostedReport(out, report4) {
12021
- out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
12022
- out.log(` coverage: ${report4.coverageStatus} cells=${report4.metrics.coverageCells} shallow=${report4.metrics.shallowCells} blocked=${report4.metrics.blockedCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
12023
- out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
12024
- out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
12025
- out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
12026
- for (const finding of report4.findings) {
12435
+ function printHostedReport(out, report5) {
12436
+ out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
12437
+ out.log(` coverage: ${report5.coverageStatus} cells=${report5.metrics.coverageCells} shallow=${report5.metrics.shallowCells} blocked=${report5.metrics.blockedCells} unscheduled=${report5.metrics.unscheduledCells} budget_exhausted=${report5.metrics.budgetExhaustedCells}`);
12438
+ out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
12439
+ out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
12440
+ out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
12441
+ for (const finding of report5.findings) {
12027
12442
  const location = finding.locations[0];
12028
12443
  out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
12029
12444
  }
12030
- for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
12445
+ for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
12031
12446
  }
12032
- function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
12447
+ function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
12033
12448
  const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
12034
12449
  const candidateValue = parsed.options["fail-on-candidates"];
12035
12450
  const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
12036
12451
  const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
12037
- const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
12038
- const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
12039
- const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
12452
+ const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
12453
+ const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
12454
+ const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
12040
12455
  if (confirmed.length || leads.length || incomplete) {
12041
- throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
12456
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
12042
12457
  }
12043
12458
  if (emitSuccess) {
12044
- out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
12459
+ out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
12045
12460
  }
12046
12461
  }
12047
12462
  function printHostedSecurityPlanRoute(out, label, route2) {
@@ -12124,17 +12539,17 @@ async function runHostedSecurity(options) {
12124
12539
  allowNetwork: false
12125
12540
  }
12126
12541
  });
12127
- const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
12128
- await (0, import_node3.writeSecurityArtifacts)(output, report4);
12129
- const reportDigest = await (0, import_security.securityFingerprint)(report4);
12542
+ const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
12543
+ await (0, import_node3.writeSecurityArtifacts)(output, report5);
12544
+ const reportDigest = await (0, import_security.securityFingerprint)(report5);
12130
12545
  await hosted.complete({
12131
12546
  reportDigest,
12132
- coverageStatus: report4.coverageStatus,
12133
- confirmed: report4.metrics.confirmed,
12134
- candidates: report4.metrics.candidates
12547
+ coverageStatus: report5.coverageStatus,
12548
+ confirmed: report5.metrics.confirmed,
12549
+ candidates: report5.metrics.candidates
12135
12550
  }, { signal: options.signal });
12136
- printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
12137
- return Object.freeze({ report: report4, run: hosted.run, output });
12551
+ printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
12552
+ return Object.freeze({ report: report5, run: hosted.run, output });
12138
12553
  }
12139
12554
  function selectEnv(requested, declared, configPath, rootDir) {
12140
12555
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
@@ -12160,14 +12575,14 @@ function profileFor(name, maxHuntTasks) {
12160
12575
  if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
12161
12576
  return { ...profile, maxHuntTasks };
12162
12577
  }
12163
- function printSummary(out, appId, env, run, report4, output) {
12164
- const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
12578
+ function printSummary(out, appId, env, run, report5, output) {
12579
+ const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
12165
12580
  out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
12166
12581
  out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
12167
12582
  out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
12168
- out.log(` coverage: ${report4.coverageStatus} ${complete}/${report4.coverage.length} blocked=${report4.metrics.blockedCells} shallow=${report4.metrics.shallowCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
12169
- if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
12170
- out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
12583
+ out.log(` coverage: ${report5.coverageStatus} ${complete}/${report5.coverage.length} blocked=${report5.metrics.blockedCells} shallow=${report5.metrics.shallowCells} unscheduled=${report5.metrics.unscheduledCells} budget_exhausted=${report5.metrics.budgetExhaustedCells}`);
12584
+ if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
12585
+ out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
12171
12586
  out.log(` report: ${(0, import_node_path19.resolve)(output, "REPORT.md")}`);
12172
12587
  }
12173
12588
  function formatBudget(usage) {
@@ -12414,13 +12829,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
12414
12829
  }
12415
12830
  throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
12416
12831
  }
12417
- const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
12832
+ const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
12418
12833
  if (parsed.options.json === true) {
12419
- context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
12834
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
12420
12835
  } else {
12421
- printHostedReport(context.stdout, report4);
12836
+ printHostedReport(context.stdout, report5);
12422
12837
  }
12423
- enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
12838
+ enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
12424
12839
  }
12425
12840
  async function runLocalSecurityCommand(parsed, dependencies) {
12426
12841
  if (parsed.options.source === true) {
@@ -12487,13 +12902,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
12487
12902
  });
12488
12903
  enforceLocalGate(result.report, parsed);
12489
12904
  }
12490
- function enforceLocalGate(report4, parsed) {
12905
+ function enforceLocalGate(report5, parsed) {
12491
12906
  const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
12492
12907
  const candidateValue = parsed.options["fail-on-candidates"];
12493
12908
  const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
12494
- const confirmed = (0, import_security2.findingsAtOrAbove)(report4, failOn);
12495
- const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
12496
- const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
12909
+ const confirmed = (0, import_security2.findingsAtOrAbove)(report5, failOn);
12910
+ const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
12911
+ const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
12497
12912
  if (confirmed.length || leads.length || incomplete) {
12498
12913
  throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
12499
12914
  }
@@ -12532,9 +12947,9 @@ async function securityCommand(parsed, dependencies) {
12532
12947
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
12533
12948
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
12534
12949
  const context = await hostedSecurityContext(parsed, dependencies);
12535
- const report4 = await getHostedSecurityReport({ ...context, jobId });
12536
- if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
12537
- else printHostedReport(context.stdout, report4);
12950
+ const report5 = await getHostedSecurityReport({ ...context, jobId });
12951
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
12952
+ else printHostedReport(context.stdout, report5);
12538
12953
  return;
12539
12954
  }
12540
12955
  if (sub !== "run") {