@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/bin.cjs CHANGED
@@ -32,6 +32,7 @@ var import_node_process7 = __toESM(require("process"), 1);
32
32
 
33
33
  // src/token.ts
34
34
  var import_db = require("@odla-ai/db");
35
+ var import_node_crypto = require("crypto");
35
36
  var import_node_process4 = __toESM(require("process"), 1);
36
37
 
37
38
  // src/handshake-approval.ts
@@ -386,6 +387,7 @@ async function freshHandshake(ctx, waitMs) {
386
387
  endpoint: ctx.cfg.platformUrl,
387
388
  email: ctx.email,
388
389
  label: `${ctx.cfg.app.id} provisioner`,
390
+ agentHandle: projectAgentHandle(ctx.cfg.app.id),
389
391
  projectIds: [ctx.cfg.app.id],
390
392
  fetch: ctx.doFetch,
391
393
  waitMs,
@@ -421,6 +423,12 @@ async function freshHandshake(ctx, waitMs) {
421
423
  stopReminder?.();
422
424
  }
423
425
  }
426
+ function projectAgentHandle(appId) {
427
+ const candidate = /^[a-z]/.test(appId) ? appId : `app-${appId}`;
428
+ if (candidate.length <= 32) return candidate;
429
+ const digest = (0, import_node_crypto.createHash)("sha256").update(appId).digest("hex").slice(0, 8);
430
+ return `${candidate.slice(0, 23).replace(/-+$/, "")}-${digest}`;
431
+ }
424
432
  function stillPending(pending, email) {
425
433
  return new import_db.OdlaError(
426
434
  "handshake_pending",
@@ -993,6 +1001,32 @@ var import_node_path4 = require("path");
993
1001
  var import_node_url = require("url");
994
1002
  var import_apps = require("@odla-ai/apps");
995
1003
 
1004
+ // src/ai-config-validation.ts
1005
+ function validateAiConfig(cfg, path) {
1006
+ if (cfg.ai === void 0) return;
1007
+ if (!isRecord4(cfg.ai)) throw new Error(`${path}: ai must be an object`);
1008
+ assertOnly(cfg.ai, ["mode", "provider", "model", "keyEnv", "secretName"], `${path}: ai`);
1009
+ if (cfg.ai.mode !== void 0 && cfg.ai.mode !== "hosted" && cfg.ai.mode !== "byok") {
1010
+ throw new Error(`${path}: ai.mode must be hosted or byok`);
1011
+ }
1012
+ if (cfg.ai.mode === "byok" && !safeText(cfg.ai.provider, 100)) {
1013
+ throw new Error(`${path}: ai.provider is required when ai.mode is byok`);
1014
+ }
1015
+ if (cfg.ai.mode === "hosted" && (cfg.ai.provider || cfg.ai.keyEnv || cfg.ai.secretName)) {
1016
+ throw new Error(`${path}: hosted ai cannot configure a provider, keyEnv, or secretName`);
1017
+ }
1018
+ }
1019
+ function assertOnly(value2, allowed, label) {
1020
+ const extra = Object.keys(value2).find((key) => !allowed.includes(key));
1021
+ if (extra) throw new Error(`${label}.${extra} is not supported`);
1022
+ }
1023
+ function isRecord4(value2) {
1024
+ return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
1025
+ }
1026
+ function safeText(value2, max) {
1027
+ return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
1028
+ }
1029
+
996
1030
  // src/integration-validation.ts
997
1031
  function validateIntegrations(cfg, path, defaultServices) {
998
1032
  if (cfg.integrations === void 0) return;
@@ -1000,16 +1034,16 @@ function validateIntegrations(cfg, path, defaultServices) {
1000
1034
  const ids = /* @__PURE__ */ new Set();
1001
1035
  for (const [index, integration] of cfg.integrations.entries()) {
1002
1036
  const at = `${path}: integrations[${index}]`;
1003
- if (!isRecord4(integration)) throw new Error(`${at} must be an object`);
1037
+ if (!isRecord5(integration)) throw new Error(`${at} must be an object`);
1004
1038
  if (!validId(integration.id)) throw new Error(`${at}.id must be lowercase letters, numbers, and hyphens`);
1005
1039
  if (ids.has(integration.id)) throw new Error(`${path}: duplicate integration id "${integration.id}"`);
1006
1040
  ids.add(integration.id);
1007
- if (!safeText(integration.title, 200)) throw new Error(`${at}.title is required`);
1008
- if (!safeText(integration.npm, 200)) throw new Error(`${at}.npm is required`);
1009
- if (integration.schema !== void 0 && (!isRecord4(integration.schema) || !isRecord4(integration.schema.entities))) {
1041
+ if (!safeText2(integration.title, 200)) throw new Error(`${at}.title is required`);
1042
+ if (!safeText2(integration.npm, 200)) throw new Error(`${at}.npm is required`);
1043
+ if (integration.schema !== void 0 && (!isRecord5(integration.schema) || !isRecord5(integration.schema.entities))) {
1010
1044
  throw new Error(`${at}.schema must contain an entities object`);
1011
1045
  }
1012
- if (integration.rules !== void 0 && !isRecord4(integration.rules)) throw new Error(`${at}.rules must be an object`);
1046
+ if (integration.rules !== void 0 && !isRecord5(integration.rules)) throw new Error(`${at}.rules must be an object`);
1013
1047
  validateSeeds(integration, at);
1014
1048
  validateProbes(integration, at);
1015
1049
  }
@@ -1023,13 +1057,13 @@ function validateSeeds(integration, at) {
1023
1057
  const ids = /* @__PURE__ */ new Set();
1024
1058
  for (const [index, seed] of integration.seeds.entries()) {
1025
1059
  const sat = `${at}.seeds[${index}]`;
1026
- if (!isRecord4(seed) || !safeText(seed.id, 200) || !safeText(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
1060
+ if (!isRecord5(seed) || !safeText2(seed.id, 200) || !safeText2(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
1027
1061
  if (ids.has(seed.id)) throw new Error(`${at} has duplicate seed id "${seed.id}"`);
1028
1062
  ids.add(seed.id);
1029
- if (!isRecord4(seed.key) || !safeText(seed.key.attr, 200) || !safeText(seed.key.value, 2048)) {
1063
+ if (!isRecord5(seed.key) || !safeText2(seed.key.attr, 200) || !safeText2(seed.key.value, 2048)) {
1030
1064
  throw new Error(`${sat}.key requires string attr and value`);
1031
1065
  }
1032
- if (!isRecord4(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
1066
+ if (!isRecord5(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
1033
1067
  if (Object.hasOwn(seed.attrs, seed.key.attr) && seed.attrs[seed.key.attr] !== seed.key.value) {
1034
1068
  throw new Error(`${sat}.attrs.${seed.key.attr} conflicts with its natural key`);
1035
1069
  }
@@ -1040,16 +1074,16 @@ function validateProbes(integration, at) {
1040
1074
  if (!Array.isArray(integration.probes)) throw new Error(`${at}.probes must be an array`);
1041
1075
  for (const [index, probe] of integration.probes.entries()) {
1042
1076
  const pat = `${at}.probes[${index}]`;
1043
- if (!isRecord4(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
1077
+ if (!isRecord5(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
1044
1078
  if (!Number.isInteger(probe.expectedStatus) || probe.expectedStatus < 100 || probe.expectedStatus > 599) {
1045
1079
  throw new Error(`${pat}.expectedStatus must be an HTTP status`);
1046
1080
  }
1047
1081
  }
1048
1082
  }
1049
- function isRecord4(value2) {
1083
+ function isRecord5(value2) {
1050
1084
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
1051
1085
  }
1052
- function safeText(value2, max) {
1086
+ function safeText2(value2, max) {
1053
1087
  return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
1054
1088
  }
1055
1089
  function safeProbePath(value2) {
@@ -1179,6 +1213,7 @@ function validateRawConfig(raw, path) {
1179
1213
  if (cfg.services !== void 0 && (!Array.isArray(cfg.services) || cfg.services.some((service) => typeof service !== "string" || !service.trim()))) {
1180
1214
  throw new Error(`${path}: services must be an array of non-empty names`);
1181
1215
  }
1216
+ validateAiConfig(cfg, path);
1182
1217
  validateIntegrations(cfg, path, DEFAULT_SERVICES);
1183
1218
  }
1184
1219
  function validateCalendarConfig(cfg, envs, services, path) {
@@ -1187,11 +1222,11 @@ function validateCalendarConfig(cfg, envs, services, path) {
1187
1222
  if (enabled) throw new Error(`${path}: calendar.google is required when services includes "calendar"`);
1188
1223
  return;
1189
1224
  }
1190
- if (!isRecord5(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
1191
- assertOnly(cfg.calendar, ["google"], `${path}: calendar`);
1192
- if (!isRecord5(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
1225
+ if (!isRecord6(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
1226
+ assertOnly2(cfg.calendar, ["google"], `${path}: calendar`);
1227
+ if (!isRecord6(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
1193
1228
  const google = cfg.calendar.google;
1194
- assertOnly(
1229
+ assertOnly2(
1195
1230
  google,
1196
1231
  ["availabilityCalendars", "calendars", "bookingCalendar", "bookingPageUrl"],
1197
1232
  `${path}: calendar.google`
@@ -1201,7 +1236,7 @@ function validateCalendarConfig(cfg, envs, services, path) {
1201
1236
  throw new Error(`${path}: calendar.google requires exactly one of availabilityCalendars or calendars (legacy)`);
1202
1237
  }
1203
1238
  const availability = google[availabilityKey];
1204
- if (!isRecord5(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
1239
+ if (!isRecord6(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
1205
1240
  const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env));
1206
1241
  if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
1207
1242
  for (const env of envs) {
@@ -1212,22 +1247,22 @@ function validateCalendarConfig(cfg, envs, services, path) {
1212
1247
  if (ids.length > 10) {
1213
1248
  throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
1214
1249
  }
1215
- if (ids.some((id) => !safeText2(id, 1024))) {
1250
+ if (ids.some((id) => !safeText3(id, 1024))) {
1216
1251
  throw new Error(`${path}: calendar.google.${availabilityKey}.${env} contains an invalid calendar id`);
1217
1252
  }
1218
1253
  }
1219
1254
  if (google.bookingCalendar !== void 0) {
1220
- if (!isRecord5(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
1255
+ if (!isRecord6(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
1221
1256
  const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env));
1222
1257
  if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
1223
1258
  for (const [env, value2] of Object.entries(google.bookingCalendar)) {
1224
- if (!safeText2(value2, 1024)) {
1259
+ if (!safeText3(value2, 1024)) {
1225
1260
  throw new Error(`${path}: calendar.google.bookingCalendar.${env} must be a calendar id`);
1226
1261
  }
1227
1262
  }
1228
1263
  }
1229
1264
  if (google.bookingPageUrl !== void 0) {
1230
- if (!isRecord5(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
1265
+ if (!isRecord6(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
1231
1266
  const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env));
1232
1267
  if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingPageUrl.${unknownBookingEnv} is not in config envs`);
1233
1268
  for (const [env, value2] of Object.entries(google.bookingPageUrl)) {
@@ -1250,14 +1285,14 @@ function validateServices(services, path) {
1250
1285
  }
1251
1286
  }
1252
1287
  }
1253
- function assertOnly(value2, allowed, label) {
1288
+ function assertOnly2(value2, allowed, label) {
1254
1289
  const extra = Object.keys(value2).find((key) => !allowed.includes(key));
1255
1290
  if (extra) throw new Error(`${label}.${extra} is not supported`);
1256
1291
  }
1257
- function isRecord5(value2) {
1292
+ function isRecord6(value2) {
1258
1293
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
1259
1294
  }
1260
- function safeText2(value2, max) {
1295
+ function safeText3(value2, max) {
1261
1296
  return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
1262
1297
  }
1263
1298
  function safeHttpsUrl(value2) {
@@ -2633,8 +2668,8 @@ async function calendarCalendars(options) {
2633
2668
  async function calendarConnect(options) {
2634
2669
  const { cfg, ctx, out } = await lifecycleContext(options);
2635
2670
  productionConsent(ctx.env, options.yes, "connect calendar");
2636
- const page = calendarBookingPageUrl(cfg, ctx.env);
2637
- const applied = page === void 0 ? await readCalendarStatus(ctx) : await applyCalendarSettings(ctx, page);
2671
+ const page2 = calendarBookingPageUrl(cfg, ctx.env);
2672
+ const applied = page2 === void 0 ? await readCalendarStatus(ctx) : await applyCalendarSettings(ctx, page2);
2638
2673
  const connectOptions = connectionOptions(options, out);
2639
2674
  return await continueConnectedCalendar(ctx, applied, connectOptions) ?? connectWithContext(ctx, connectOptions);
2640
2675
  }
@@ -2883,12 +2918,12 @@ var import_apps3 = require("@odla-ai/apps");
2883
2918
  var import_node_fs10 = require("fs");
2884
2919
 
2885
2920
  // src/config-reconcile-digest.ts
2886
- var import_node_crypto = require("crypto");
2921
+ var import_node_crypto2 = require("crypto");
2887
2922
  function canonicalJson(value2) {
2888
2923
  return JSON.stringify(canonicalValue(value2));
2889
2924
  }
2890
2925
  function configDigest(value2) {
2891
- return `sha256:${(0, import_node_crypto.createHash)("sha256").update(canonicalJson(value2)).digest("hex")}`;
2926
+ return `sha256:${(0, import_node_crypto2.createHash)("sha256").update(canonicalJson(value2)).digest("hex")}`;
2892
2927
  }
2893
2928
  function canonicalValue(value2) {
2894
2929
  if (value2 === null || typeof value2 === "string" || typeof value2 === "boolean") return value2;
@@ -3054,7 +3089,7 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
3054
3089
  `${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`
3055
3090
  );
3056
3091
  }
3057
- throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText3(res)}`);
3092
+ throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
3058
3093
  }
3059
3094
  async function postJson(doFetch, url, bearer, body) {
3060
3095
  const res = await doFetch(url, {
@@ -3062,7 +3097,7 @@ async function postJson(doFetch, url, bearer, body) {
3062
3097
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
3063
3098
  body: JSON.stringify(body)
3064
3099
  });
3065
- if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText3(res)}`);
3100
+ if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText4(res)}`);
3066
3101
  }
3067
3102
  function normalizeClerkConfig(value2) {
3068
3103
  if (!value2) return null;
@@ -3075,7 +3110,7 @@ function normalizeClerkConfig(value2) {
3075
3110
  const publishableKey = envValue(cfg.publishableKey);
3076
3111
  return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
3077
3112
  }
3078
- async function safeText3(res) {
3113
+ async function safeText4(res) {
3079
3114
  try {
3080
3115
  return redactSecrets((await res.text()).slice(0, 500));
3081
3116
  } catch {
@@ -4057,18 +4092,18 @@ function assertSeedContracts(integrations, schema) {
4057
4092
  }
4058
4093
  }
4059
4094
  function isUniqueAttr(schema, ns, attr) {
4060
- if (!isRecord6(schema) || !isRecord6(schema.entities)) return false;
4095
+ if (!isRecord7(schema) || !isRecord7(schema.entities)) return false;
4061
4096
  const entity = schema.entities[ns];
4062
- if (!isRecord6(entity) || !isRecord6(entity.attrs)) return false;
4097
+ if (!isRecord7(entity) || !isRecord7(entity.attrs)) return false;
4063
4098
  const definition = entity.attrs[attr];
4064
- return isRecord6(definition) && definition.unique === true;
4099
+ return isRecord7(definition) && definition.unique === true;
4065
4100
  }
4066
4101
  function normalizeSchema(value2) {
4067
4102
  if (value2 === void 0 || value2 === null) return { entities: {}, links: {} };
4068
- if (!isRecord6(value2) || !isRecord6(value2.entities)) {
4103
+ if (!isRecord7(value2) || !isRecord7(value2.entities)) {
4069
4104
  throw new Error("db schema must be a serialized schema object with an entities map");
4070
4105
  }
4071
- if (value2.links !== void 0 && !isRecord6(value2.links)) throw new Error("db schema links must be an object");
4106
+ if (value2.links !== void 0 && !isRecord7(value2.links)) throw new Error("db schema links must be an object");
4072
4107
  return {
4073
4108
  entities: { ...value2.entities },
4074
4109
  links: { ...value2.links ?? {} }
@@ -4082,7 +4117,7 @@ function mergeMap(target, fragment, label) {
4082
4117
  target[name] = value2;
4083
4118
  }
4084
4119
  }
4085
- function isRecord6(value2) {
4120
+ function isRecord7(value2) {
4086
4121
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
4087
4122
  }
4088
4123
 
@@ -4101,7 +4136,7 @@ async function doctor(options) {
4101
4136
  out.log(`integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
4102
4137
  out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
4103
4138
  out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
4104
- out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
4139
+ out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
4105
4140
  if (cfg.services.includes("calendar")) {
4106
4141
  const calendar = cfg.envs.map((env) => {
4107
4142
  const resolved = calendarServiceConfig(cfg, env);
@@ -4122,7 +4157,9 @@ async function doctor(options) {
4122
4157
  }
4123
4158
  }
4124
4159
  warnings.push(...integrationWarnings(database.integrations, schema, rules));
4125
- if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
4160
+ if (cfg.services.includes("ai") && cfg.ai?.mode === "byok" && !cfg.ai.provider) {
4161
+ warnings.push("ai.mode is byok but ai.provider is not set");
4162
+ }
4126
4163
  if (cfg.auth?.clerk) {
4127
4164
  for (const [env, value2] of Object.entries(cfg.auth.clerk)) {
4128
4165
  if (typeof value2 === "string" && value2.startsWith("$") && !process.env[value2.slice(1)]) {
@@ -4202,7 +4239,7 @@ function initProject(options) {
4202
4239
  if (!services.includes(dependency)) throw new Error(`--services ${service} requires ${dependency}`);
4203
4240
  }
4204
4241
  }
4205
- const aiProvider = options.aiProvider ?? "anthropic";
4242
+ const aiProvider = options.aiProvider;
4206
4243
  (0, import_node_fs13.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
4207
4244
  (0, import_node_fs13.mkdirSync)((0, import_node_path12.resolve)(rootDir, "src/odla"), { recursive: true });
4208
4245
  (0, import_node_fs13.mkdirSync)((0, import_node_path12.resolve)(rootDir, ".odla"), { recursive: true });
@@ -4229,6 +4266,15 @@ function configTemplate(input) {
4229
4266
  },
4230
4267
  },
4231
4268
  ` : "";
4269
+ const ai = input.aiProvider ? ` ai: {
4270
+ mode: "byok",
4271
+ provider: process.env.ODLA_AI_PROVIDER ?? "${input.aiProvider}",
4272
+ // Optional: set this env var while running provision to store the provider
4273
+ // key in the app vault for each tenant.
4274
+ keyEnv: "${defaultKeyEnv(input.aiProvider)}",
4275
+ },` : ` // Hosted AI uses admin-approved models and central cost tracking.
4276
+ // Pass --ai-provider during init only when this app must use BYOK.
4277
+ ai: { mode: "hosted" },`;
4232
4278
  return `export default {
4233
4279
  platformUrl: process.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
4234
4280
  dbEndpoint: process.env.ODLA_ENDPOINT ?? process.env.ODLA_DB_ENDPOINT ?? "https://db.odla.ai",
@@ -4247,12 +4293,7 @@ function configTemplate(input) {
4247
4293
  // When rules is omitted, the CLI generates deny-all rules from schema.
4248
4294
  defaultRules: "deny",
4249
4295
  },
4250
- ai: {
4251
- provider: process.env.ODLA_AI_PROVIDER ?? "${input.aiProvider}",
4252
- // Optional: set this env var while running provision to store the provider
4253
- // key in the platform vault for each tenant.
4254
- keyEnv: "${defaultKeyEnv(input.aiProvider)}",
4255
- },
4296
+ ${ai}
4256
4297
  ${calendar}
4257
4298
  auth: {
4258
4299
  clerk: {
@@ -4461,9 +4502,10 @@ For work that creates an odla app or adds odla services, read and follow
4461
4502
  \`.agents/skills/odla-migrate/SKILL.md\`. For production telemetry triage, use
4462
4503
  \`.agents/skills/odla-o11y-debug/SKILL.md\`.
4463
4504
 
4464
- Track the work in odla's PM as you go \u2014 record decisions when you make them, file
4465
- bugs when you notice them, and move tasks when the work moves
4466
- (\`npx @odla-ai/cli pm task list\`). The conventions and the full command set are
4505
+ Track the work in odla's PM as you go. Before project-mutating work, run
4506
+ \`npx @odla-ai/cli pm next --app <appId>\`, confirm alignment to an open goal,
4507
+ and atomically claim a refined Ready task. Record decisions when you make them
4508
+ and file bugs when you notice them. The conventions and the full command set are
4467
4509
  in \`.agents/skills/odla/references/pm.md\`.
4468
4510
 
4469
4511
  The setup runbooks and their references are installed in this repository, pinned
@@ -4747,12 +4789,16 @@ async function smoke(options) {
4747
4789
  out.log(` tenant: ${entry.tenantId}`);
4748
4790
  const publicConfig = await getJson(doFetch, publicConfigUrl(cfg.platformUrl, cfg.app.id, env), void 0);
4749
4791
  out.log(` public-config: ok`);
4750
- if (cfg.ai?.provider) {
4792
+ if (cfg.services.includes("ai") && cfg.ai?.provider) {
4751
4793
  const provider = publicConfig.ai?.provider ?? null;
4752
4794
  if (provider !== cfg.ai.provider) {
4753
4795
  throw new Error(`ai provider mismatch: expected "${cfg.ai.provider}", public-config has "${provider ?? "none"}"`);
4754
4796
  }
4755
- out.log(` ai: ${provider}`);
4797
+ out.log(` ai: byok/${provider}`);
4798
+ } else if (cfg.services.includes("ai")) {
4799
+ const mode = publicConfig.ai?.mode;
4800
+ if (mode !== "hosted") throw new Error(`ai mode mismatch: expected "hosted", public-config has "${String(mode ?? "none")}"`);
4801
+ out.log(" ai: hosted");
4756
4802
  }
4757
4803
  if (hasO11y) out.log(` o11y: credentials present`);
4758
4804
  if (cfg.services.includes("calendar")) {
@@ -4830,7 +4876,7 @@ async function getJson(doFetch, url, bearer) {
4830
4876
  const res = await doFetch(url, {
4831
4877
  headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
4832
4878
  });
4833
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
4879
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
4834
4880
  return res.json();
4835
4881
  }
4836
4882
  async function postJson2(doFetch, url, bearer, body) {
@@ -4839,7 +4885,7 @@ async function postJson2(doFetch, url, bearer, body) {
4839
4885
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
4840
4886
  body: JSON.stringify(body)
4841
4887
  });
4842
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
4888
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
4843
4889
  return res.json();
4844
4890
  }
4845
4891
  function publicConfigUrl(platformUrl, appId, env) {
@@ -4847,7 +4893,7 @@ function publicConfigUrl(platformUrl, appId, env) {
4847
4893
  url.searchParams.set("env", env);
4848
4894
  return url.toString();
4849
4895
  }
4850
- async function safeText4(res) {
4896
+ async function safeText5(res) {
4851
4897
  try {
4852
4898
  return redactSecrets((await res.text()).slice(0, 500));
4853
4899
  } catch {
@@ -5687,7 +5733,7 @@ function dependenciesOf(values, influence = "data") {
5687
5733
  return [...unique3.values()];
5688
5734
  }
5689
5735
 
5690
- // ../camel/dist/chunk-S7EVNA2U.js
5736
+ // ../camel/dist/chunk-4DQ6BIHP.js
5691
5737
  var camelValueBrand = /* @__PURE__ */ Symbol("@odla-ai/camel/value");
5692
5738
  var authenticCamelValues = /* @__PURE__ */ new WeakSet();
5693
5739
  function isCamelValue(value2) {
@@ -5699,13 +5745,13 @@ function isSafe(value2) {
5699
5745
  function isUnsafe(value2) {
5700
5746
  return isCamelValue(value2) && value2.label.promptSafety === "unsafe";
5701
5747
  }
5702
- function createSafeInternal(value2, safeBasis, metadata2) {
5748
+ function createSafeInternal(value2, safeBasis, metadata2, extra) {
5703
5749
  return createValue(value2, {
5704
5750
  schemaVersion: 1,
5705
5751
  promptSafety: "safe",
5706
5752
  safeBasis,
5707
5753
  ...copyMetadata(metadata2)
5708
- });
5754
+ }, extra);
5709
5755
  }
5710
5756
  function createUnsafeInternal(value2, metadata2) {
5711
5757
  return createValue(value2, {
@@ -5714,8 +5760,8 @@ function createUnsafeInternal(value2, metadata2) {
5714
5760
  ...copyMetadata(metadata2)
5715
5761
  });
5716
5762
  }
5717
- function createValue(value2, label) {
5718
- const result = { value: value2, label: Object.freeze(label) };
5763
+ function createValue(value2, label, extra) {
5764
+ const result = { value: value2, label: Object.freeze(label), ...extra };
5719
5765
  Object.defineProperty(result, camelValueBrand, { value: label.promptSafety, enumerable: false });
5720
5766
  authenticCamelValues.add(result);
5721
5767
  return Object.freeze(result);
@@ -5976,7 +6022,7 @@ var import_path8 = require("path");
5976
6022
  var import_promises10 = require("fs/promises");
5977
6023
  var import_path9 = require("path");
5978
6024
 
5979
- // ../camel/dist/chunk-LAXU2AVK.js
6025
+ // ../camel/dist/chunk-4EIRFS3A.js
5980
6026
  function conversionPolicyDigest(policy) {
5981
6027
  return sha256Hex(canonicalJson2(policy));
5982
6028
  }
@@ -6064,10 +6110,18 @@ async function convert(source, policy, value2, counts) {
6064
6110
  const count = counts.get(countKey) ?? 0;
6065
6111
  if (count >= policy.maximumOutputsPerArtifact) throw new CamelError("limit_exceeded", "Conversion output count exceeds its per-source bound.");
6066
6112
  counts.set(countKey, count + 1);
6113
+ const conversionRecordId = await sha256Hex(canonicalJson2({ conversionId: policy.conversionId, digest: policy.digest, source: sourceKey, ordinal: count }));
6114
+ const datumId = await sha256Hex(canonicalJson2({ conversionRecordId, value: value2 }));
6067
6115
  return createSafeInternal(value2, "atomic_conversion", {
6068
6116
  readers: source.label.readers,
6069
6117
  provenance: [...source.label.provenance, { kind: "converter", id: policy.conversionId, digest: policy.digest }],
6070
6118
  dependencies: dependenciesOf([source])
6119
+ }, {
6120
+ datumId,
6121
+ kind: policy.output.kind,
6122
+ conversionId: policy.conversionId,
6123
+ conversionDigest: policy.digest,
6124
+ conversionRecordId
6071
6125
  });
6072
6126
  }
6073
6127
  function validatePolicyShape(policy) {
@@ -6132,7 +6186,7 @@ function missingPolicy() {
6132
6186
  throw new CamelError("conversion_rejected", "Conversion policy is not registered.");
6133
6187
  }
6134
6188
 
6135
- // ../camel/dist/chunk-P2WQIAG6.js
6189
+ // ../camel/dist/chunk-VEAUXH4F.js
6136
6190
  function createCamelIngress(constants2 = []) {
6137
6191
  const byId = /* @__PURE__ */ new Map();
6138
6192
  for (const item of constants2) {
@@ -8122,7 +8176,7 @@ async function defaultReadOrigin(cwd) {
8122
8176
 
8123
8177
  // src/code-local-source.ts
8124
8178
  var import_node_child_process5 = require("child_process");
8125
- var import_node_crypto2 = require("crypto");
8179
+ var import_node_crypto3 = require("crypto");
8126
8180
  var SOURCE_LIMITS2 = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
8127
8181
  async function prepareCodeLocalSource(cwd, repository, readHead = readGitHead) {
8128
8182
  const headCommitSha = await readHead(cwd);
@@ -8173,12 +8227,12 @@ async function readGitHead(cwd) {
8173
8227
  return value2;
8174
8228
  }
8175
8229
  function digestText(value2) {
8176
- return `sha256:${(0, import_node_crypto2.createHash)("sha256").update(value2).digest("hex")}`;
8230
+ return `sha256:${(0, import_node_crypto3.createHash)("sha256").update(value2).digest("hex")}`;
8177
8231
  }
8178
8232
 
8179
8233
  // src/code-images.ts
8180
8234
  var import_node_child_process6 = require("child_process");
8181
- var import_node_crypto3 = require("crypto");
8235
+ var import_node_crypto4 = require("crypto");
8182
8236
  var import_promises11 = require("fs/promises");
8183
8237
  var import_node_os3 = require("os");
8184
8238
  var import_node_path14 = require("path");
@@ -8264,7 +8318,7 @@ async function embeddedPiImageName() {
8264
8318
  const bundle = await (0, import_promises11.readFile)(embeddedPiAssetPath()).catch(() => {
8265
8319
  throw new Error("CLI-embedded Pi runtime is missing; reinstall this exact @odla-ai/cli version");
8266
8320
  });
8267
- return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto3.createHash)("sha256").update(bundle).digest("hex")}`;
8321
+ return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto4.createHash)("sha256").update(bundle).digest("hex")}`;
8268
8322
  }
8269
8323
  async function buildEmbeddedPiImage(engine, image, run) {
8270
8324
  const context = await (0, import_promises11.mkdtemp)((0, import_node_path14.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
@@ -8669,7 +8723,7 @@ Start here:
8669
8723
 
8670
8724
  Usage:
8671
8725
  odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
8672
- odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
8726
+ odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod] [--ai-provider <byok-provider>]
8673
8727
  odla-ai doctor [--config odla.config.mjs]
8674
8728
  odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
8675
8729
  odla-ai config apply --plan <plan.json> [--idempotency-key <key>] [--email <odla-account>] [--json]
@@ -8692,16 +8746,22 @@ Usage:
8692
8746
  odla-ai app owners remove <email> [--email <odla-account>] [--json]
8693
8747
  odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
8694
8748
  odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8695
- odla-ai pm task list [--app <id>] [--column <c>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8749
+ odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8696
8750
  odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8697
8751
  odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8698
8752
  odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
8699
- 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]
8753
+ 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]
8754
+ odla-ai pm next --app <id> [--json]
8755
+ 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]
8756
+ 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]
8757
+ odla-ai pm task claim <id> --expected-revision <n> [--mutation-id <id>] [--json]
8758
+ odla-ai pm task release <id> --expected-revision <n> [--mutation-id <id>] [--json]
8700
8759
  odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
8701
8760
  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]
8702
8761
  odla-ai pm <goal|task|decision|bug> get <id> [--json]
8762
+ odla-ai pm <goal|task|decision|bug> ref <id> [--json]
8703
8763
  odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
8704
- 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]
8764
+ 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]
8705
8765
  odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
8706
8766
  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]
8707
8767
  odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
@@ -9020,11 +9080,11 @@ async function discussList(ctx, parsed) {
9020
9080
  if (value2) query.set(param, value2);
9021
9081
  }
9022
9082
  const qs = query.toString();
9023
- const page = await request(ctx, "GET", `/topics${qs ? `?${qs}` : ""}`);
9024
- emit(ctx, page, () => {
9025
- ctx.out.log(`topics \u2014 ${page.topics.length} of ${page.total}`);
9083
+ const page2 = await request(ctx, "GET", `/topics${qs ? `?${qs}` : ""}`);
9084
+ emit(ctx, page2, () => {
9085
+ ctx.out.log(`topics \u2014 ${page2.topics.length} of ${page2.total}`);
9026
9086
  ctx.out.log("id state app replies subject");
9027
- for (const topic of page.topics) {
9087
+ for (const topic of page2.topics) {
9028
9088
  ctx.out.log(
9029
9089
  `${topic.id} ${state(topic)} ${topic.appId ?? ""} ${topic.replyCount} ${topic.subject}`
9030
9090
  );
@@ -9039,11 +9099,11 @@ async function discussRead(ctx, id, parsed) {
9039
9099
  limit: requestedLimit ?? "200",
9040
9100
  offset: requestedOffset ?? "0"
9041
9101
  });
9042
- const page = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
9102
+ const page2 = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
9043
9103
  emit(
9044
9104
  ctx,
9045
- page,
9046
- () => renderDiscussRead(ctx, page.topic, page.posts, page)
9105
+ page2,
9106
+ () => renderDiscussRead(ctx, page2.topic, page2.posts, page2)
9047
9107
  );
9048
9108
  return;
9049
9109
  }
@@ -9056,20 +9116,20 @@ async function discussRead(ctx, id, parsed) {
9056
9116
  let topic = null;
9057
9117
  let offset = 0;
9058
9118
  for (; ; ) {
9059
- const page = await request(
9119
+ const page2 = await request(
9060
9120
  ctx,
9061
9121
  "GET",
9062
9122
  `/topics/${encodeURIComponent(id)}?limit=200&offset=${offset}`
9063
9123
  );
9064
- topic = page.topic;
9065
- for (const post of page.posts) posts.set(post.id, post);
9066
- mergeDiscussPrincipals(projection, page);
9124
+ topic = page2.topic;
9125
+ for (const post of page2.posts) posts.set(post.id, post);
9126
+ mergeDiscussPrincipals(projection, page2);
9067
9127
  if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
9068
- if (!page.page?.hasMore) break;
9069
- if (page.page.nextOffset === null || page.page.nextOffset <= offset) {
9128
+ if (!page2.page?.hasMore) break;
9129
+ if (page2.page.nextOffset === null || page2.page.nextOffset <= offset) {
9070
9130
  throw new Error("discuss read failed: registry returned a non-advancing post page");
9071
9131
  }
9072
- offset = page.page.nextOffset;
9132
+ offset = page2.page.nextOffset;
9073
9133
  }
9074
9134
  const ordered = [...posts.values()].sort(
9075
9135
  (a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
@@ -9237,9 +9297,9 @@ async function discussWatch(ctx, topicId, parsed) {
9237
9297
  let firstSuccess = true;
9238
9298
  let consecutiveFailures = 0;
9239
9299
  for (; ; ) {
9240
- let page;
9300
+ let page2;
9241
9301
  try {
9242
- page = await getWatchPage(ctx, requestPath(topicId, app, cursor));
9302
+ page2 = await getWatchPage(ctx, requestPath(topicId, app, cursor));
9243
9303
  consecutiveFailures = 0;
9244
9304
  } catch (error) {
9245
9305
  if (!(error instanceof WatchRequestError) || !error.retryable) {
@@ -9275,25 +9335,25 @@ async function discussWatch(ctx, topicId, parsed) {
9275
9335
  await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
9276
9336
  continue;
9277
9337
  }
9278
- cursor = page.cursor;
9279
- const baseline = firstSuccess && page.events.length === 0;
9338
+ cursor = page2.cursor;
9339
+ const baseline = firstSuccess && page2.events.length === 0;
9280
9340
  if (baseline) {
9281
9341
  jsonl(ctx, parsed, {
9282
9342
  type: "checkpoint",
9283
- streamId: page.streamId,
9343
+ streamId: page2.streamId,
9284
9344
  cursor,
9285
- serverTime: page.serverTime
9345
+ serverTime: page2.serverTime
9286
9346
  });
9287
9347
  }
9288
9348
  firstSuccess = false;
9289
- const matching = page.events.filter((event) => {
9349
+ const matching = page2.events.filter((event) => {
9290
9350
  if (topicId && (event.type !== "message" || event.action !== "created")) return false;
9291
9351
  return (!by || event.actor.id === by) && (!self || event.actor.id !== self);
9292
9352
  });
9293
9353
  for (const event of matching) {
9294
9354
  jsonl(ctx, parsed, {
9295
9355
  type: "event",
9296
- streamId: page.streamId,
9356
+ streamId: page2.streamId,
9297
9357
  eventId: event.id,
9298
9358
  cursor: event.cursor,
9299
9359
  event
@@ -9302,9 +9362,9 @@ async function discussWatch(ctx, topicId, parsed) {
9302
9362
  if (matching.length > 0) {
9303
9363
  jsonl(ctx, parsed, {
9304
9364
  type: "checkpoint",
9305
- streamId: page.streamId,
9365
+ streamId: page2.streamId,
9306
9366
  cursor,
9307
- serverTime: page.serverTime
9367
+ serverTime: page2.serverTime
9308
9368
  });
9309
9369
  const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
9310
9370
  const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
@@ -9312,28 +9372,28 @@ async function discussWatch(ctx, topicId, parsed) {
9312
9372
  found: true,
9313
9373
  cursor,
9314
9374
  events: matching,
9315
- ...page.authors ? { authors: page.authors } : {},
9316
- ...page.principals ? { principals: page.principals } : {},
9375
+ ...page2.authors ? { authors: page2.authors } : {},
9376
+ ...page2.principals ? { principals: page2.principals } : {},
9317
9377
  ...posts && posts.length > 0 ? { posts } : {},
9318
9378
  ...topics && topics.length > 0 ? { topics } : {}
9319
9379
  });
9320
9380
  }
9321
- if (page.events.length > 0) {
9381
+ if (page2.events.length > 0) {
9322
9382
  jsonl(ctx, parsed, {
9323
9383
  type: "checkpoint",
9324
- streamId: page.streamId,
9384
+ streamId: page2.streamId,
9325
9385
  cursor,
9326
- serverTime: page.serverTime
9386
+ serverTime: page2.serverTime
9327
9387
  });
9328
9388
  } else if (!baseline) {
9329
9389
  jsonl(ctx, parsed, {
9330
9390
  type: "heartbeat",
9331
- streamId: page.streamId,
9391
+ streamId: page2.streamId,
9332
9392
  cursor,
9333
- serverTime: page.serverTime
9393
+ serverTime: page2.serverTime
9334
9394
  });
9335
9395
  }
9336
- if (page.hasMore) continue;
9396
+ if (page2.hasMore) continue;
9337
9397
  if (deadline !== void 0 && now() >= deadline) {
9338
9398
  return report2(ctx, parsed, { found: false, cursor });
9339
9399
  }
@@ -9452,7 +9512,13 @@ async function discussCommand(parsed, deps = {}) {
9452
9512
  }
9453
9513
  }
9454
9514
 
9455
- // src/pm-actions.ts
9515
+ // src/pm-action-core.ts
9516
+ var DONE = {
9517
+ goal: { status: "met", currentPct: 100 },
9518
+ task: { column: "done" },
9519
+ decision: { status: "accepted" },
9520
+ bug: { status: "fixed" }
9521
+ };
9456
9522
  var writeMutationId2 = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
9457
9523
  var FIELD_MAP = {
9458
9524
  title: { key: "title" },
@@ -9468,22 +9534,27 @@ var FIELD_MAP = {
9468
9534
  body: { key: "body" },
9469
9535
  target: { key: "targetPct", num: true },
9470
9536
  description: { key: "description" },
9471
- desc: { key: "description" }
9472
- };
9473
- var DONE = {
9474
- goal: { status: "met", currentPct: 100 },
9475
- task: { column: "done" },
9476
- decision: { status: "accepted" },
9477
- bug: { status: "fixed" }
9537
+ desc: { key: "description" },
9538
+ acceptance: { key: "acceptanceCriteria" },
9539
+ "alignment-decision": { key: "alignmentDecisionId" },
9540
+ execution: { key: "executionMode" },
9541
+ "expected-revision": { key: "expectedRevision", num: true }
9478
9542
  };
9479
9543
  async function pmRequest(ctx, method, path, body) {
9480
- const res = await ctx.doFetch(`${ctx.platformUrl}/registry/pm${path}`, {
9544
+ const response2 = await ctx.doFetch(`${ctx.platformUrl}/registry/pm${path}`, {
9481
9545
  method,
9482
- headers: { authorization: `Bearer ${ctx.token}`, "content-type": "application/json" },
9546
+ headers: {
9547
+ authorization: `Bearer ${ctx.token}`,
9548
+ "content-type": "application/json"
9549
+ },
9483
9550
  body: body === void 0 ? void 0 : JSON.stringify(body)
9484
9551
  });
9485
- const data = await res.json().catch(() => ({}));
9486
- if (!res.ok) throw new Error(`pm ${method} ${path} failed: ${data.error ?? `registry returned ${res.status}`}`);
9552
+ const data = await response2.json().catch(() => ({}));
9553
+ if (!response2.ok) {
9554
+ throw new Error(
9555
+ `pm ${method} ${path} failed: ${data.error ?? `registry returned ${response2.status}`}`
9556
+ );
9557
+ }
9487
9558
  return data;
9488
9559
  }
9489
9560
  function collectFields(parsed, allowClear) {
@@ -9506,20 +9577,32 @@ function collectEntityFields(entity, parsed, allowClear) {
9506
9577
  if (fields.description === void 0) fields.description = fields.body;
9507
9578
  delete fields.body;
9508
9579
  }
9580
+ if (entity === "task" && fields.column === "ready") fields.column = "todo";
9509
9581
  return fields;
9510
9582
  }
9511
- function statusCol(entity, r) {
9512
- if (entity === "bug") return `${r.status ?? ""}/${r.severity ?? ""}`;
9513
- if (entity === "task") return String(r.column ?? "");
9514
- return String(r.status ?? "");
9583
+ function statusCol(entity, record10) {
9584
+ if (entity === "bug") return `${record10.status ?? ""}/${record10.severity ?? ""}`;
9585
+ if (entity === "task") {
9586
+ const state2 = record10.column === "todo" ? "ready" : String(record10.column ?? "");
9587
+ return record10.revision ? `${state2}; r${record10.revision}` : state2;
9588
+ }
9589
+ return String(record10.status ?? "");
9515
9590
  }
9516
- function printRecord(ctx, entity, r) {
9517
- ctx.out.log(`${r.id} [${statusCol(entity, r)}] ${r.appId} ${r.title ?? ""}`);
9591
+ function referenceMarkup(entity, record10) {
9592
+ const label = (record10.title?.trim() || `${entity} ${record10.id}`).replaceAll("]", ")");
9593
+ return `@[${label}](pm:${entity}/${record10.id})`;
9594
+ }
9595
+ function printRecord(ctx, entity, record10) {
9596
+ ctx.out.log(
9597
+ `${record10.id} [${statusCol(entity, record10)}] ${record10.appId} ${record10.title ?? ""}`
9598
+ );
9518
9599
  }
9519
9600
  function emit2(ctx, value2, human) {
9520
9601
  if (ctx.json) ctx.out.log(JSON.stringify(value2, null, 2));
9521
9602
  else human();
9522
9603
  }
9604
+
9605
+ // src/pm-actions.ts
9523
9606
  async function pmList(ctx, entity, parsed) {
9524
9607
  const q = new URLSearchParams();
9525
9608
  const filters = {
@@ -9533,7 +9616,8 @@ async function pmList(ctx, entity, parsed) {
9533
9616
  const app = stringOpt(parsed.options.app) ?? ctx.appId;
9534
9617
  if (app) q.set("app", app);
9535
9618
  for (const [flag, param] of Object.entries(filters)) {
9536
- const v = stringOpt(parsed.options[flag]);
9619
+ const raw = stringOpt(parsed.options[flag]);
9620
+ const v = entity === "task" && flag === "column" && raw === "ready" ? "todo" : raw;
9537
9621
  if (v) q.set(param, v);
9538
9622
  }
9539
9623
  for (const opt of ["q", "limit", "offset"]) {
@@ -9541,11 +9625,11 @@ async function pmList(ctx, entity, parsed) {
9541
9625
  if (v) q.set(opt, v);
9542
9626
  }
9543
9627
  const qs = q.toString();
9544
- const page = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
9545
- emit2(ctx, page, () => {
9546
- ctx.out.log(`${entity} \u2014 ${page.records.length} of ${page.total}`);
9628
+ const page2 = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
9629
+ emit2(ctx, page2, () => {
9630
+ ctx.out.log(`${entity} \u2014 ${page2.records.length} of ${page2.total}`);
9547
9631
  ctx.out.log("id state app title");
9548
- for (const r of page.records) printRecord(ctx, entity, r);
9632
+ for (const r of page2.records) printRecord(ctx, entity, r);
9549
9633
  });
9550
9634
  }
9551
9635
  async function pmAdd(ctx, entity, parsed) {
@@ -9566,6 +9650,17 @@ async function pmGet(ctx, entity, id) {
9566
9650
  const { record: record10 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
9567
9651
  emit2(ctx, record10, () => printRecord(ctx, entity, record10));
9568
9652
  }
9653
+ async function pmReference(ctx, entity, id) {
9654
+ const { record: record10 } = await pmRequest(
9655
+ ctx,
9656
+ "GET",
9657
+ `/${entity}/${encodeURIComponent(id)}`
9658
+ );
9659
+ const markup = referenceMarkup(entity, record10);
9660
+ emit2(ctx, { kind: `pm:${entity}`, id: record10.id, label: record10.title ?? "", markup }, () => {
9661
+ ctx.out.log(markup);
9662
+ });
9663
+ }
9569
9664
  async function pmSet(ctx, entity, id, parsed) {
9570
9665
  const patch2 = collectEntityFields(entity, parsed, true);
9571
9666
  if (Object.keys(patch2).length === 0)
@@ -9586,6 +9681,36 @@ async function pmDone(ctx, entity, id, parsed) {
9586
9681
  });
9587
9682
  emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
9588
9683
  }
9684
+ async function pmTaskLifecycle(ctx, id, action2, parsed) {
9685
+ const rawRevision = stringOpt(parsed.options["expected-revision"]);
9686
+ const expectedRevision = Number(rawRevision);
9687
+ if (!rawRevision || !Number.isSafeInteger(expectedRevision) || expectedRevision < 1) {
9688
+ throw new Error(`pm task ${action2} needs --expected-revision <n>`);
9689
+ }
9690
+ const mutationId = writeMutationId2(parsed);
9691
+ const res = action2 === "ready" ? await pmRequest(
9692
+ ctx,
9693
+ "PATCH",
9694
+ `/task/${encodeURIComponent(id)}`,
9695
+ {
9696
+ patch: {
9697
+ ...collectEntityFields("task", parsed, true),
9698
+ column: "todo",
9699
+ expectedRevision
9700
+ },
9701
+ mutationId
9702
+ }
9703
+ ) : await pmRequest(
9704
+ ctx,
9705
+ "POST",
9706
+ `/task/${encodeURIComponent(id)}/${action2}`,
9707
+ { expectedRevision, mutationId }
9708
+ );
9709
+ emit2(ctx, res, () => {
9710
+ const state2 = res.record ? statusCol("task", res.record) : action2;
9711
+ ctx.out.log(`task ${id} \u2192 ${state2}`);
9712
+ });
9713
+ }
9589
9714
  async function allRecords(ctx, entity, appId) {
9590
9715
  const records = [];
9591
9716
  for (; ; ) {
@@ -9594,11 +9719,48 @@ async function allRecords(ctx, entity, appId) {
9594
9719
  limit: "100",
9595
9720
  offset: String(records.length)
9596
9721
  });
9597
- const page = await pmRequest(ctx, "GET", `/${entity}?${q}`);
9598
- records.push(...page.records);
9599
- if (records.length >= page.total || page.records.length === 0) return records;
9722
+ const page2 = await pmRequest(ctx, "GET", `/${entity}?${q}`);
9723
+ records.push(...page2.records);
9724
+ if (records.length >= page2.total || page2.records.length === 0) return records;
9600
9725
  }
9601
9726
  }
9727
+ async function pmNext(ctx, parsed) {
9728
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
9729
+ if (!appId) throw new Error("pm next needs --app <appId>");
9730
+ const [goals, tasks] = await Promise.all([
9731
+ allRecords(ctx, "goal", appId),
9732
+ allRecords(ctx, "task", appId)
9733
+ ]);
9734
+ const result = {
9735
+ appId,
9736
+ openGoals: goals.filter((record10) => record10.status === "open"),
9737
+ doing: tasks.filter((record10) => record10.column === "doing"),
9738
+ ready: tasks.filter((record10) => record10.column === "todo")
9739
+ };
9740
+ emit2(ctx, result, () => {
9741
+ ctx.out.log(`${appId}: goal-aligned work intake (read only)`);
9742
+ for (const [label, records] of [
9743
+ ["doing", result.doing],
9744
+ ["ready", result.ready],
9745
+ ["open goals", result.openGoals]
9746
+ ]) {
9747
+ ctx.out.log(`${label}:`);
9748
+ if (!records.length) ctx.out.log("- (none)");
9749
+ else for (const record10 of records) printRecord(
9750
+ ctx,
9751
+ label === "open goals" ? "goal" : "task",
9752
+ record10
9753
+ );
9754
+ }
9755
+ if (!result.openGoals.length) {
9756
+ ctx.out.log("next: discuss alignment with the user before creating or claiming project work");
9757
+ } else if (!result.ready.length) {
9758
+ ctx.out.log("next: refine a linked Backlog task and mark it Ready");
9759
+ } else {
9760
+ ctx.out.log("next: review a Ready task, then claim it with its revision");
9761
+ }
9762
+ });
9763
+ }
9602
9764
  async function pmHandoff(ctx, parsed) {
9603
9765
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
9604
9766
  if (!appId) throw new Error("pm handoff needs --app <appId>");
@@ -9638,6 +9800,12 @@ async function pmHandoff(ctx, parsed) {
9638
9800
  }
9639
9801
  });
9640
9802
  }
9803
+ async function pmRemove(ctx, entity, id) {
9804
+ await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id)}`);
9805
+ ctx.out.log(`deleted ${entity} ${id}`);
9806
+ }
9807
+
9808
+ // src/pm-comments.ts
9641
9809
  async function pmComment(ctx, entity, id, parsed) {
9642
9810
  const body = stringOpt(parsed.options.body);
9643
9811
  if (!body) throw new Error('pm comment needs --body "..."');
@@ -9648,19 +9816,218 @@ async function pmComment(ctx, entity, id, parsed) {
9648
9816
  ctx.out.log(`commented on ${entity} ${id}`);
9649
9817
  }
9650
9818
  async function pmComments(ctx, entity, id) {
9651
- const { messages } = await pmRequest(
9652
- ctx,
9653
- "GET",
9654
- `/${entity}/${encodeURIComponent(id)}/comments`
9655
- );
9819
+ const { messages } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}/comments`);
9656
9820
  emit2(ctx, messages, () => {
9657
9821
  if (messages.length === 0) ctx.out.log("(no comments)");
9658
- else for (const m of messages) ctx.out.log(`[${m.authorId ?? "?"}] ${m.body ?? ""}`);
9822
+ else for (const message2 of messages) {
9823
+ ctx.out.log(
9824
+ `[${message2.authorId ?? "?"}] ${message2.markup ?? message2.body ?? ""}`
9825
+ );
9826
+ }
9659
9827
  });
9660
9828
  }
9661
- async function pmRemove(ctx, entity, id) {
9662
- await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id)}`);
9663
- ctx.out.log(`deleted ${entity} ${id}`);
9829
+
9830
+ // src/pm-watch-types.ts
9831
+ var PmWatchCheckpointError = class extends Error {
9832
+ constructor(cursor, streamId) {
9833
+ super("PM work cursor requires a new checkpoint");
9834
+ this.cursor = cursor;
9835
+ this.streamId = streamId;
9836
+ this.name = "PmWatchCheckpointError";
9837
+ }
9838
+ cursor;
9839
+ streamId;
9840
+ code = "checkpoint_required";
9841
+ };
9842
+ var PmWatchRequestError = class extends Error {
9843
+ constructor(message2, retryable, status) {
9844
+ super(message2);
9845
+ this.retryable = retryable;
9846
+ this.status = status;
9847
+ this.name = "PmWatchRequestError";
9848
+ }
9849
+ retryable;
9850
+ status;
9851
+ };
9852
+
9853
+ // src/pm-watch.ts
9854
+ var DEFAULT_INTERVAL_MS2 = 15e3;
9855
+ var MAX_CONSECUTIVE_FAILURES2 = 5;
9856
+ var MAX_BACKOFF_MS2 = 3e4;
9857
+ function positiveNumber(parsed, flag, fallback) {
9858
+ const raw = stringOpt(parsed.options[flag]);
9859
+ const value2 = raw == null ? NaN : Number(raw);
9860
+ return Number.isFinite(value2) && value2 > 0 ? value2 : fallback;
9861
+ }
9862
+ function jsonl2(ctx, parsed, value2) {
9863
+ if (parsed.options.jsonl === true) ctx.out.log(JSON.stringify({ v: 1, ...value2 }));
9864
+ }
9865
+ async function page(ctx, appId, cursor) {
9866
+ const params = new URLSearchParams({ app: appId });
9867
+ if (cursor) params.set("cursor", cursor);
9868
+ let response2;
9869
+ try {
9870
+ response2 = await ctx.doFetch(`${ctx.platformUrl}/registry/pm/work/watch?${params}`, {
9871
+ headers: { authorization: `Bearer ${ctx.token}` }
9872
+ });
9873
+ } catch (error) {
9874
+ throw new PmWatchRequestError(
9875
+ `pm watch request failed: ${error instanceof Error ? error.message : String(error)}`,
9876
+ true
9877
+ );
9878
+ }
9879
+ const data = await response2.json().catch(() => ({}));
9880
+ if (response2.status === 409 && data.code === "checkpoint_required") {
9881
+ throw new PmWatchCheckpointError(data.cursor, data.streamId);
9882
+ }
9883
+ if (!response2.ok) {
9884
+ throw new PmWatchRequestError(
9885
+ `pm watch failed: ${data.error ?? `registry returned ${response2.status}`}`,
9886
+ response2.status === 429 || response2.status >= 500,
9887
+ response2.status
9888
+ );
9889
+ }
9890
+ return data;
9891
+ }
9892
+ function recordState(record10) {
9893
+ if (record10.column) return record10.column === "todo" ? "ready" : record10.column;
9894
+ return String(record10.status ?? "");
9895
+ }
9896
+ function eventRecord(event) {
9897
+ return event.payload.payload;
9898
+ }
9899
+ function eventLabel(event) {
9900
+ const record10 = eventRecord(event);
9901
+ if (record10) return String(record10.title ?? event.payload.entityId);
9902
+ const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
9903
+ return body || event.payload.entityId;
9904
+ }
9905
+ function report3(ctx, parsed, result) {
9906
+ if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
9907
+ else if (parsed.options.jsonl !== true && result.found) {
9908
+ for (const event of result.events ?? []) {
9909
+ const record10 = eventRecord(event);
9910
+ const state2 = record10 ? recordState(record10) : "comment";
9911
+ ctx.out.log(
9912
+ `${event.id} ${event.type} ${state2}${record10?.revision ? `; r${record10.revision}` : ""} ${eventLabel(event)}`
9913
+ );
9914
+ }
9915
+ }
9916
+ return result;
9917
+ }
9918
+ async function pmWatch(ctx, parsed) {
9919
+ if (ctx.json && parsed.options.jsonl === true) {
9920
+ throw new Error("--json and --jsonl cannot be combined");
9921
+ }
9922
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
9923
+ if (!appId) throw new Error("pm watch needs --app <appId>");
9924
+ const sleep = ctx.sleep ?? ((ms) => new Promise((resolve13) => setTimeout(resolve13, ms)));
9925
+ const now = ctx.now ?? Date.now;
9926
+ const intervalMs = (positiveNumber(parsed, "interval", DEFAULT_INTERVAL_MS2 / 1e3) ?? DEFAULT_INTERVAL_MS2 / 1e3) * 1e3;
9927
+ const timeoutSeconds = positiveNumber(parsed, "timeout");
9928
+ const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
9929
+ const entity = stringOpt(parsed.options.entity);
9930
+ const action2 = stringOpt(parsed.options.action);
9931
+ const wantedState = stringOpt(parsed.options.state)?.toLowerCase();
9932
+ const by = stringOpt(parsed.options.by);
9933
+ const self = stringOpt(parsed.options.self);
9934
+ let cursor = stringOpt(parsed.options.cursor);
9935
+ let firstSuccess = true;
9936
+ let consecutiveFailures = 0;
9937
+ for (; ; ) {
9938
+ let current;
9939
+ try {
9940
+ current = await page(ctx, appId, cursor);
9941
+ consecutiveFailures = 0;
9942
+ } catch (error) {
9943
+ if (error instanceof PmWatchCheckpointError) {
9944
+ jsonl2(ctx, parsed, {
9945
+ type: "status",
9946
+ state: "checkpoint_required",
9947
+ retryable: false,
9948
+ ...error.cursor ? { cursor: error.cursor } : {},
9949
+ ...error.streamId ? { streamId: error.streamId } : {}
9950
+ });
9951
+ throw error;
9952
+ }
9953
+ if (!(error instanceof PmWatchRequestError) || !error.retryable) throw error;
9954
+ consecutiveFailures++;
9955
+ jsonl2(ctx, parsed, {
9956
+ type: "status",
9957
+ state: "degraded",
9958
+ retryable: true,
9959
+ attempt: consecutiveFailures,
9960
+ ...cursor ? { cursor } : {},
9961
+ ...error.status ? { status: error.status } : {}
9962
+ });
9963
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
9964
+ if (deadline !== void 0 && now() >= deadline) {
9965
+ return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
9966
+ }
9967
+ const backoff = Math.min(
9968
+ MAX_BACKOFF_MS2,
9969
+ Math.min(intervalMs, 1e3) * 2 ** (consecutiveFailures - 1)
9970
+ );
9971
+ await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
9972
+ continue;
9973
+ }
9974
+ cursor = current.cursor;
9975
+ const baseline = firstSuccess && current.events.length === 0;
9976
+ if (baseline) {
9977
+ jsonl2(ctx, parsed, {
9978
+ type: "checkpoint",
9979
+ streamId: current.streamId,
9980
+ cursor,
9981
+ serverTime: current.serverTime
9982
+ });
9983
+ }
9984
+ firstSuccess = false;
9985
+ const matching = current.events.filter((event) => {
9986
+ const record10 = eventRecord(event);
9987
+ const state2 = record10 ? recordState(record10).toLowerCase() : "";
9988
+ 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);
9989
+ });
9990
+ for (const event of matching) {
9991
+ jsonl2(ctx, parsed, {
9992
+ type: "event",
9993
+ streamId: current.streamId,
9994
+ eventId: event.id,
9995
+ cursor: event.cursor,
9996
+ event
9997
+ });
9998
+ }
9999
+ if (matching.length > 0) {
10000
+ jsonl2(ctx, parsed, {
10001
+ type: "checkpoint",
10002
+ streamId: current.streamId,
10003
+ cursor,
10004
+ serverTime: current.serverTime
10005
+ });
10006
+ return report3(ctx, parsed, { found: true, cursor, events: matching });
10007
+ }
10008
+ if (current.events.length > 0) {
10009
+ jsonl2(ctx, parsed, {
10010
+ type: "checkpoint",
10011
+ streamId: current.streamId,
10012
+ cursor,
10013
+ serverTime: current.serverTime
10014
+ });
10015
+ } else if (!baseline) {
10016
+ jsonl2(ctx, parsed, {
10017
+ type: "heartbeat",
10018
+ streamId: current.streamId,
10019
+ cursor,
10020
+ serverTime: current.serverTime
10021
+ });
10022
+ }
10023
+ if (current.hasMore) continue;
10024
+ if (deadline !== void 0 && now() >= deadline) {
10025
+ return report3(ctx, parsed, { found: false, cursor });
10026
+ }
10027
+ await sleep(
10028
+ deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
10029
+ );
10030
+ }
9664
10031
  }
9665
10032
 
9666
10033
  // src/pm-command.ts
@@ -9681,7 +10048,11 @@ var ACTION_OPTIONS = {
9681
10048
  done: ["mutation-id"],
9682
10049
  comment: ["body", "mutation-id"],
9683
10050
  comments: [],
9684
- rm: []
10051
+ rm: [],
10052
+ ready: ["goal", "alignment-decision", "execution", "description", "desc", "body", "acceptance", "expected-revision", "mutation-id"],
10053
+ claim: ["expected-revision", "mutation-id"],
10054
+ release: ["expected-revision", "mutation-id"],
10055
+ ref: []
9685
10056
  };
9686
10057
  var ENTITY_OPTIONS = {
9687
10058
  goal: {
@@ -9692,8 +10063,8 @@ var ENTITY_OPTIONS = {
9692
10063
  },
9693
10064
  task: {
9694
10065
  list: ["column", "goal", "assignee"],
9695
- add: ["column", "goal", "assignee", "due", "description", "desc", "body"],
9696
- set: ["title", "column", "rank", "goal", "assignee", "due", "description", "desc", "body"],
10066
+ add: ["column", "goal", "alignment-decision", "execution", "assignee", "due", "description", "desc", "body", "acceptance"],
10067
+ set: ["title", "column", "rank", "goal", "alignment-decision", "execution", "assignee", "due", "description", "desc", "body", "acceptance", "expected-revision"],
9697
10068
  done: []
9698
10069
  },
9699
10070
  decision: {
@@ -9744,11 +10115,33 @@ async function buildContext2(parsed, deps) {
9744
10115
  json: parsed.options.json === true,
9745
10116
  // Preserve PM's existing cross-project default when a config is present;
9746
10117
  // only an explicit remote-agent environment or profile selects an app.
9747
- appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
10118
+ appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0,
10119
+ ...deps.sleep ? { sleep: deps.sleep } : {},
10120
+ ...deps.now ? { now: deps.now } : {}
9748
10121
  };
9749
10122
  }
9750
10123
  async function pmCommand(parsed, deps = {}) {
9751
10124
  const word = parsed.positionals[1] ?? "";
10125
+ if (word === "next") {
10126
+ assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
10127
+ return pmNext(await buildContext2(parsed, deps), parsed);
10128
+ }
10129
+ if (word === "watch") {
10130
+ assertArgs(parsed, [
10131
+ ...COMMON_OPTIONS,
10132
+ "app",
10133
+ "cursor",
10134
+ "interval",
10135
+ "timeout",
10136
+ "jsonl",
10137
+ "entity",
10138
+ "action",
10139
+ "state",
10140
+ "by",
10141
+ "self"
10142
+ ], 2);
10143
+ return pmWatch(await buildContext2(parsed, deps), parsed).then(() => void 0);
10144
+ }
9752
10145
  if (word === "handoff") {
9753
10146
  assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
9754
10147
  return pmHandoff(await buildContext2(parsed, deps), parsed);
@@ -9759,6 +10152,9 @@ async function pmCommand(parsed, deps = {}) {
9759
10152
  const action2 = canonicalAction(requestedAction);
9760
10153
  if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|comment|comments|rm.`);
9761
10154
  assertArgs(parsed, allowedOptions(entity, action2), 4);
10155
+ if ((action2 === "ready" || action2 === "claim" || action2 === "release") && entity !== "task") {
10156
+ throw new Error(`pm ${action2} is only valid for tasks`);
10157
+ }
9762
10158
  const ctx = await buildContext2(parsed, deps);
9763
10159
  const id = parsed.positionals[3];
9764
10160
  switch (action2) {
@@ -9778,6 +10174,12 @@ async function pmCommand(parsed, deps = {}) {
9778
10174
  return pmComments(ctx, entity, requireId2(id, action2));
9779
10175
  case "rm":
9780
10176
  return pmRemove(ctx, entity, requireId2(id, action2));
10177
+ case "ref":
10178
+ return pmReference(ctx, entity, requireId2(id, action2));
10179
+ case "ready":
10180
+ case "claim":
10181
+ case "release":
10182
+ return pmTaskLifecycle(ctx, requireId2(id, action2), action2, parsed);
9781
10183
  }
9782
10184
  }
9783
10185
 
@@ -10314,7 +10716,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
10314
10716
  const payload = await postJson3(doFetch, `${base}/query`, dbKey, {
10315
10717
  query: { [seed.ns]: { $: { where: { [seed.key.attr]: seed.key.value }, limit: 1 } } }
10316
10718
  });
10317
- const rows = isRecord7(payload) && isRecord7(payload.result) ? payload.result[seed.ns] : void 0;
10719
+ const rows = isRecord8(payload) && isRecord8(payload.result) ? payload.result[seed.ns] : void 0;
10318
10720
  if (!Array.isArray(rows)) {
10319
10721
  throw new Error(`${env}: integration ${integration.id} seed ${seed.id} query returned an invalid response`);
10320
10722
  }
@@ -10346,7 +10748,7 @@ async function postJson3(doFetch, url, bearer, body) {
10346
10748
  if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await responseText(res)}`);
10347
10749
  return res.json().catch(() => ({}));
10348
10750
  }
10349
- function isRecord7(value2) {
10751
+ function isRecord8(value2) {
10350
10752
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
10351
10753
  }
10352
10754
  async function responseText(res) {
@@ -10416,14 +10818,14 @@ async function mintDbKey(opts, tenantId) {
10416
10818
  appId: tenantId
10417
10819
  })
10418
10820
  });
10419
- if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText5(created)}`);
10821
+ if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText6(created)}`);
10420
10822
  res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
10421
10823
  method: "POST",
10422
10824
  headers,
10423
10825
  body: "{}"
10424
10826
  });
10425
10827
  }
10426
- if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
10828
+ if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText6(res)}`);
10427
10829
  const body = await res.json();
10428
10830
  if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
10429
10831
  return body.key;
@@ -10439,12 +10841,12 @@ async function issueO11yToken(opts) {
10439
10841
  `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`
10440
10842
  );
10441
10843
  }
10442
- if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText5(res)}`);
10844
+ if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText6(res)}`);
10443
10845
  const body = await res.json();
10444
10846
  if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
10445
10847
  return body.token;
10446
10848
  }
10447
- async function safeText5(res) {
10849
+ async function safeText6(res) {
10448
10850
  try {
10449
10851
  return redactSecrets((await res.text()).slice(0, 500));
10450
10852
  } catch {
@@ -10487,7 +10889,7 @@ async function provision(options) {
10487
10889
  const namespaces = Object.keys(integration.schema?.entities ?? {}).length;
10488
10890
  out.log(` integration.${integration.id}: ${namespaces} namespaces, ${integration.seeds?.length ?? 0} seeds, ${integration.probes?.length ?? 0} smoke probes`);
10489
10891
  }
10490
- out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
10892
+ out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "hosted" : "not enabled"}`);
10491
10893
  if (cfg.services.includes("calendar")) {
10492
10894
  for (const env of cfg.envs) {
10493
10895
  const calendar = calendarServiceConfig(cfg, env);
@@ -10538,11 +10940,11 @@ async function provision(options) {
10538
10940
  for (const service of serviceOrder) {
10539
10941
  if (service === "ai") {
10540
10942
  if (cfg.ai?.provider) {
10541
- await apps.setAi(cfg.app.id, env, { provider: cfg.ai.provider, ...cfg.ai.model ? { model: cfg.ai.model } : {} });
10542
- out.log(`${env}: ai configured (${cfg.ai.provider}${cfg.ai.model ? `/${cfg.ai.model}` : ""})`);
10943
+ await apps.setAi(cfg.app.id, env, { mode: "byok", provider: cfg.ai.provider, ...cfg.ai.model ? { model: cfg.ai.model } : {} });
10944
+ out.log(`${env}: ai configured (byok ${cfg.ai.provider}${cfg.ai.model ? `/${cfg.ai.model}` : ""})`);
10543
10945
  } else {
10544
- await apps.setService(cfg.app.id, "ai", true, { env });
10545
- out.log(`${env}: ai enabled`);
10946
+ await apps.setAi(cfg.app.id, env, { mode: "hosted", ...cfg.ai?.model ? { model: cfg.ai.model } : {} });
10947
+ out.log(`${env}: ai configured (hosted${cfg.ai?.model ? `/${cfg.ai.model}` : ""})`);
10546
10948
  }
10547
10949
  } else {
10548
10950
  const config = service === "calendar" ? calendarServiceConfig(cfg, env) : void 0;
@@ -10667,12 +11069,23 @@ var PM_ACTIONS = {
10667
11069
  done: {},
10668
11070
  comment: {},
10669
11071
  comments: {},
11072
+ ref: {},
10670
11073
  rm: {},
10671
11074
  delete: {}
10672
11075
  };
10673
- var PM_ENTITIES = Object.fromEntries(
10674
- ["goal", "conformance", "task", "kanban", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
10675
- );
11076
+ var PM_TASK_ACTIONS = {
11077
+ ...PM_ACTIONS,
11078
+ ready: {},
11079
+ claim: {},
11080
+ release: {}
11081
+ };
11082
+ var PM_ENTITIES = {
11083
+ ...Object.fromEntries(
11084
+ ["goal", "conformance", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
11085
+ ),
11086
+ task: PM_TASK_ACTIONS,
11087
+ kanban: PM_TASK_ACTIONS
11088
+ };
10676
11089
  var COMMAND_SURFACE = {
10677
11090
  agent: { jobs: {}, retry: {} },
10678
11091
  admin: {
@@ -10725,7 +11138,9 @@ var COMMAND_SURFACE = {
10725
11138
  },
10726
11139
  pm: {
10727
11140
  ...PM_ENTITIES,
10728
- handoff: {}
11141
+ handoff: {},
11142
+ next: {},
11143
+ watch: {}
10729
11144
  },
10730
11145
  provision: {},
10731
11146
  runbook: {
@@ -10889,12 +11304,12 @@ async function call(ctx, method, path, body) {
10889
11304
  throw new Error(message2);
10890
11305
  }
10891
11306
  async function bySlug(ctx, slug) {
10892
- const page = await call(
11307
+ const page2 = await call(
10893
11308
  ctx,
10894
11309
  "GET",
10895
11310
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
10896
11311
  );
10897
- const found = page.records[0];
11312
+ const found = page2.records[0];
10898
11313
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
10899
11314
  return found;
10900
11315
  }
@@ -10908,11 +11323,11 @@ async function runbookList(ctx, all, query) {
10908
11323
  const params = new URLSearchParams();
10909
11324
  if (!all) params.set("app", ctx.appId);
10910
11325
  if (query) params.set("q", query);
10911
- const page = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
10912
- if (ctx.json) return ctx.out.log(JSON.stringify(page, null, 2));
10913
- if (!page.records.length) return ctx.out.log("(no runbooks)");
11326
+ const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
11327
+ if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
11328
+ if (!page2.records.length) return ctx.out.log("(no runbooks)");
10914
11329
  ctx.out.log(["SLUG", "STATUS", "V", "SCOPE", "UPDATED", "TITLE"].join(" "));
10915
- for (const r of page.records)
11330
+ for (const r of page2.records)
10916
11331
  ctx.out.log(
10917
11332
  [r.slug, r.status, `v${r.version}`, r.appId === PLATFORM_SCOPE ? "platform" : r.appId, stamp(r.updatedAt), r.title].join(" ")
10918
11333
  );
@@ -10967,12 +11382,12 @@ async function runbookVisibility(ctx, slug, visibility) {
10967
11382
  }
10968
11383
  async function runbookHistory(ctx, slug) {
10969
11384
  const runbook = await bySlug(ctx, slug);
10970
- const page = await call(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
10971
- if (ctx.json) return ctx.out.log(JSON.stringify(page, null, 2));
11385
+ const page2 = await call(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
11386
+ if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
10972
11387
  ctx.out.log(`${slug} is at v${runbook.version}`);
10973
- if (!page.records.length) return ctx.out.log("(no earlier versions)");
11388
+ if (!page2.records.length) return ctx.out.log("(no earlier versions)");
10974
11389
  ctx.out.log(["V", "WHEN", "BY", "NOTE"].join(" "));
10975
- for (const r of page.records)
11390
+ for (const r of page2.records)
10976
11391
  ctx.out.log([`v${r.version}`, stamp(r.createdAt), r.ownerEmail ?? "", r.note ?? ""].join(" "));
10977
11392
  }
10978
11393
  async function runbookRevert(ctx, slug, version) {
@@ -11047,12 +11462,12 @@ ${counts.created} created, ${counts.updated} updated, ${counts.unchanged} unchan
11047
11462
  );
11048
11463
  }
11049
11464
  async function upsert(ctx, r, visibility) {
11050
- const page = await call(
11465
+ const page2 = await call(
11051
11466
  ctx,
11052
11467
  "GET",
11053
11468
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(r.slug)}&limit=1`
11054
11469
  );
11055
- const found = page.records[0];
11470
+ const found = page2.records[0];
11056
11471
  if (!found) {
11057
11472
  await call(ctx, "POST", "/runbook", {
11058
11473
  appId: ctx.appId,
@@ -11308,7 +11723,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
11308
11723
  return out;
11309
11724
  }
11310
11725
  var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
11311
- function report3(ctx, impacts) {
11726
+ function report4(ctx, impacts) {
11312
11727
  const covered = impacts.filter((i) => i.runbooks.length);
11313
11728
  ctx.out.log(
11314
11729
  `${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.`
@@ -11346,7 +11761,7 @@ async function runbookImpact(ctx, options, deps = {}) {
11346
11761
  }
11347
11762
  const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
11348
11763
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
11349
- report3(ctx, impacts);
11764
+ report4(ctx, impacts);
11350
11765
  }
11351
11766
 
11352
11767
  // src/runbook-lint.ts
@@ -11384,17 +11799,17 @@ function lintRunbook(runbook, installed) {
11384
11799
  async function runbookLint(ctx, all) {
11385
11800
  const params = new URLSearchParams();
11386
11801
  if (!all) params.set("app", ctx.appId);
11387
- const page = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
11802
+ const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
11388
11803
  const installed = { "@odla-ai/cli": cliVersion() };
11389
- const findings = page.records.flatMap((runbook) => lintRunbook(runbook, installed));
11390
- if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page.records.length, findings }, null, 2));
11391
- if (!page.records.length) return ctx.out.log("(no runbooks in scope)");
11804
+ const findings = page2.records.flatMap((runbook) => lintRunbook(runbook, installed));
11805
+ if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page2.records.length, findings }, null, 2));
11806
+ if (!page2.records.length) return ctx.out.log("(no runbooks in scope)");
11392
11807
  if (!findings.length) {
11393
11808
  return ctx.out.log(
11394
- `${page.records.length} runbook${page.records.length === 1 ? "" : "s"} checked; every command they name is real for @odla-ai/cli ${cliVersion()}.`
11809
+ `${page2.records.length} runbook${page2.records.length === 1 ? "" : "s"} checked; every command they name is real for @odla-ai/cli ${cliVersion()}.`
11395
11810
  );
11396
11811
  }
11397
- ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${page.records.length} runbooks:`);
11812
+ ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${page2.records.length} runbooks:`);
11398
11813
  for (const finding of findings) {
11399
11814
  const scope = finding.appId === PLATFORM_SCOPE ? "" : ` --app ${finding.appId}`;
11400
11815
  ctx.out.log("");
@@ -11457,12 +11872,12 @@ async function runbookAsk(ctx, question, all) {
11457
11872
  ctx.out.log(JSDOC_POINTER);
11458
11873
  }
11459
11874
  async function runbookComment(ctx, slug, body) {
11460
- const page = await call(
11875
+ const page2 = await call(
11461
11876
  ctx,
11462
11877
  "GET",
11463
11878
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
11464
11879
  );
11465
- const found = page.records[0];
11880
+ const found = page2.records[0];
11466
11881
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
11467
11882
  await call(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
11468
11883
  ctx.out.log(`commented on ${slug} (v${found.version})`);
@@ -11514,12 +11929,12 @@ var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
11514
11929
 
11515
11930
  // src/runbook-edit-flow.ts
11516
11931
  async function editRunbook(ctx, slug, deps = {}) {
11517
- const page = await call(
11932
+ const page2 = await call(
11518
11933
  ctx,
11519
11934
  "GET",
11520
11935
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
11521
11936
  );
11522
- const found = page.records[0];
11937
+ const found = page2.records[0];
11523
11938
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
11524
11939
  ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
11525
11940
  const body = await editText(found.body, slug, deps);
@@ -11940,31 +12355,31 @@ function printHostedJob(out, job, platform, appId) {
11940
12355
  url.searchParams.set("job", job.jobId);
11941
12356
  out.log(` Studio: ${url.toString()}`);
11942
12357
  }
11943
- function printHostedReport(out, report4) {
11944
- out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
11945
- 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}`);
11946
- out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
11947
- out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
11948
- out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
11949
- for (const finding of report4.findings) {
12358
+ function printHostedReport(out, report5) {
12359
+ out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
12360
+ 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}`);
12361
+ out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
12362
+ out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
12363
+ out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
12364
+ for (const finding of report5.findings) {
11950
12365
  const location = finding.locations[0];
11951
12366
  out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
11952
12367
  }
11953
- for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
12368
+ for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
11954
12369
  }
11955
- function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
12370
+ function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
11956
12371
  const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
11957
12372
  const candidateValue = parsed.options["fail-on-candidates"];
11958
12373
  const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
11959
12374
  const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
11960
- const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
11961
- const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
11962
- const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
12375
+ const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
12376
+ const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
12377
+ const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
11963
12378
  if (confirmed.length || leads.length || incomplete) {
11964
- throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
12379
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
11965
12380
  }
11966
12381
  if (emitSuccess) {
11967
- out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
12382
+ out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
11968
12383
  }
11969
12384
  }
11970
12385
  function printHostedSecurityPlanRoute(out, label, route2) {
@@ -12047,17 +12462,17 @@ async function runHostedSecurity(options) {
12047
12462
  allowNetwork: false
12048
12463
  }
12049
12464
  });
12050
- const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
12051
- await (0, import_node3.writeSecurityArtifacts)(output, report4);
12052
- const reportDigest = await (0, import_security.securityFingerprint)(report4);
12465
+ const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
12466
+ await (0, import_node3.writeSecurityArtifacts)(output, report5);
12467
+ const reportDigest = await (0, import_security.securityFingerprint)(report5);
12053
12468
  await hosted.complete({
12054
12469
  reportDigest,
12055
- coverageStatus: report4.coverageStatus,
12056
- confirmed: report4.metrics.confirmed,
12057
- candidates: report4.metrics.candidates
12470
+ coverageStatus: report5.coverageStatus,
12471
+ confirmed: report5.metrics.confirmed,
12472
+ candidates: report5.metrics.candidates
12058
12473
  }, { signal: options.signal });
12059
- printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
12060
- return Object.freeze({ report: report4, run: hosted.run, output });
12474
+ printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
12475
+ return Object.freeze({ report: report5, run: hosted.run, output });
12061
12476
  }
12062
12477
  function selectEnv(requested, declared, configPath, rootDir) {
12063
12478
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
@@ -12083,14 +12498,14 @@ function profileFor(name, maxHuntTasks) {
12083
12498
  if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
12084
12499
  return { ...profile, maxHuntTasks };
12085
12500
  }
12086
- function printSummary(out, appId, env, run, report4, output) {
12087
- const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
12501
+ function printSummary(out, appId, env, run, report5, output) {
12502
+ const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
12088
12503
  out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
12089
12504
  out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
12090
12505
  out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
12091
- 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}`);
12092
- if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
12093
- out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
12506
+ 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}`);
12507
+ if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
12508
+ out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
12094
12509
  out.log(` report: ${(0, import_node_path19.resolve)(output, "REPORT.md")}`);
12095
12510
  }
12096
12511
  function formatBudget(usage) {
@@ -12326,13 +12741,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
12326
12741
  }
12327
12742
  throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
12328
12743
  }
12329
- const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
12744
+ const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
12330
12745
  if (parsed.options.json === true) {
12331
- context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
12746
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
12332
12747
  } else {
12333
- printHostedReport(context.stdout, report4);
12748
+ printHostedReport(context.stdout, report5);
12334
12749
  }
12335
- enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
12750
+ enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
12336
12751
  }
12337
12752
  async function runLocalSecurityCommand(parsed, dependencies) {
12338
12753
  if (parsed.options.source === true) {
@@ -12399,13 +12814,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
12399
12814
  });
12400
12815
  enforceLocalGate(result.report, parsed);
12401
12816
  }
12402
- function enforceLocalGate(report4, parsed) {
12817
+ function enforceLocalGate(report5, parsed) {
12403
12818
  const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
12404
12819
  const candidateValue = parsed.options["fail-on-candidates"];
12405
12820
  const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
12406
- const confirmed = (0, import_security2.findingsAtOrAbove)(report4, failOn);
12407
- const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
12408
- const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
12821
+ const confirmed = (0, import_security2.findingsAtOrAbove)(report5, failOn);
12822
+ const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
12823
+ const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
12409
12824
  if (confirmed.length || leads.length || incomplete) {
12410
12825
  throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
12411
12826
  }
@@ -12444,9 +12859,9 @@ async function securityCommand(parsed, dependencies) {
12444
12859
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
12445
12860
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
12446
12861
  const context = await hostedSecurityContext(parsed, dependencies);
12447
- const report4 = await getHostedSecurityReport({ ...context, jobId });
12448
- if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
12449
- else printHostedReport(context.stdout, report4);
12862
+ const report5 = await getHostedSecurityReport({ ...context, jobId });
12863
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
12864
+ else printHostedReport(context.stdout, report5);
12450
12865
  return;
12451
12866
  }
12452
12867
  if (sub !== "run") {