@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.
@@ -232,6 +232,7 @@ function handshakeUrl(platformUrl, userCode) {
232
232
 
233
233
  // src/token.ts
234
234
  import { collectToken, OdlaError, requestToken } from "@odla-ai/db";
235
+ import { createHash } from "crypto";
235
236
  import process5 from "process";
236
237
 
237
238
  // src/handshake-state.ts
@@ -360,6 +361,7 @@ async function freshHandshake(ctx, waitMs) {
360
361
  endpoint: ctx.cfg.platformUrl,
361
362
  email: ctx.email,
362
363
  label: `${ctx.cfg.app.id} provisioner`,
364
+ agentHandle: projectAgentHandle(ctx.cfg.app.id),
363
365
  projectIds: [ctx.cfg.app.id],
364
366
  fetch: ctx.doFetch,
365
367
  waitMs,
@@ -395,6 +397,12 @@ async function freshHandshake(ctx, waitMs) {
395
397
  stopReminder?.();
396
398
  }
397
399
  }
400
+ function projectAgentHandle(appId) {
401
+ const candidate = /^[a-z]/.test(appId) ? appId : `app-${appId}`;
402
+ if (candidate.length <= 32) return candidate;
403
+ const digest = createHash("sha256").update(appId).digest("hex").slice(0, 8);
404
+ return `${candidate.slice(0, 23).replace(/-+$/, "")}-${digest}`;
405
+ }
398
406
  function stillPending(pending, email) {
399
407
  return new OdlaError(
400
408
  "handshake_pending",
@@ -886,6 +894,32 @@ import { dirname as dirname3, isAbsolute as isAbsolute2, resolve as resolve2 } f
886
894
  import { pathToFileURL } from "url";
887
895
  import { appServiceDefinition, appServiceIds } from "@odla-ai/apps";
888
896
 
897
+ // src/ai-config-validation.ts
898
+ function validateAiConfig(cfg, path) {
899
+ if (cfg.ai === void 0) return;
900
+ if (!isRecord4(cfg.ai)) throw new Error(`${path}: ai must be an object`);
901
+ assertOnly(cfg.ai, ["mode", "provider", "model", "keyEnv", "secretName"], `${path}: ai`);
902
+ if (cfg.ai.mode !== void 0 && cfg.ai.mode !== "hosted" && cfg.ai.mode !== "byok") {
903
+ throw new Error(`${path}: ai.mode must be hosted or byok`);
904
+ }
905
+ if (cfg.ai.mode === "byok" && !safeText(cfg.ai.provider, 100)) {
906
+ throw new Error(`${path}: ai.provider is required when ai.mode is byok`);
907
+ }
908
+ if (cfg.ai.mode === "hosted" && (cfg.ai.provider || cfg.ai.keyEnv || cfg.ai.secretName)) {
909
+ throw new Error(`${path}: hosted ai cannot configure a provider, keyEnv, or secretName`);
910
+ }
911
+ }
912
+ function assertOnly(value2, allowed, label) {
913
+ const extra = Object.keys(value2).find((key) => !allowed.includes(key));
914
+ if (extra) throw new Error(`${label}.${extra} is not supported`);
915
+ }
916
+ function isRecord4(value2) {
917
+ return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
918
+ }
919
+ function safeText(value2, max) {
920
+ return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
921
+ }
922
+
889
923
  // src/integration-validation.ts
890
924
  function validateIntegrations(cfg, path, defaultServices) {
891
925
  if (cfg.integrations === void 0) return;
@@ -893,16 +927,16 @@ function validateIntegrations(cfg, path, defaultServices) {
893
927
  const ids = /* @__PURE__ */ new Set();
894
928
  for (const [index, integration] of cfg.integrations.entries()) {
895
929
  const at = `${path}: integrations[${index}]`;
896
- if (!isRecord4(integration)) throw new Error(`${at} must be an object`);
930
+ if (!isRecord5(integration)) throw new Error(`${at} must be an object`);
897
931
  if (!validId(integration.id)) throw new Error(`${at}.id must be lowercase letters, numbers, and hyphens`);
898
932
  if (ids.has(integration.id)) throw new Error(`${path}: duplicate integration id "${integration.id}"`);
899
933
  ids.add(integration.id);
900
- if (!safeText(integration.title, 200)) throw new Error(`${at}.title is required`);
901
- if (!safeText(integration.npm, 200)) throw new Error(`${at}.npm is required`);
902
- if (integration.schema !== void 0 && (!isRecord4(integration.schema) || !isRecord4(integration.schema.entities))) {
934
+ if (!safeText2(integration.title, 200)) throw new Error(`${at}.title is required`);
935
+ if (!safeText2(integration.npm, 200)) throw new Error(`${at}.npm is required`);
936
+ if (integration.schema !== void 0 && (!isRecord5(integration.schema) || !isRecord5(integration.schema.entities))) {
903
937
  throw new Error(`${at}.schema must contain an entities object`);
904
938
  }
905
- if (integration.rules !== void 0 && !isRecord4(integration.rules)) throw new Error(`${at}.rules must be an object`);
939
+ if (integration.rules !== void 0 && !isRecord5(integration.rules)) throw new Error(`${at}.rules must be an object`);
906
940
  validateSeeds(integration, at);
907
941
  validateProbes(integration, at);
908
942
  }
@@ -916,13 +950,13 @@ function validateSeeds(integration, at) {
916
950
  const ids = /* @__PURE__ */ new Set();
917
951
  for (const [index, seed] of integration.seeds.entries()) {
918
952
  const sat = `${at}.seeds[${index}]`;
919
- if (!isRecord4(seed) || !safeText(seed.id, 200) || !safeText(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
953
+ if (!isRecord5(seed) || !safeText2(seed.id, 200) || !safeText2(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
920
954
  if (ids.has(seed.id)) throw new Error(`${at} has duplicate seed id "${seed.id}"`);
921
955
  ids.add(seed.id);
922
- if (!isRecord4(seed.key) || !safeText(seed.key.attr, 200) || !safeText(seed.key.value, 2048)) {
956
+ if (!isRecord5(seed.key) || !safeText2(seed.key.attr, 200) || !safeText2(seed.key.value, 2048)) {
923
957
  throw new Error(`${sat}.key requires string attr and value`);
924
958
  }
925
- if (!isRecord4(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
959
+ if (!isRecord5(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
926
960
  if (Object.hasOwn(seed.attrs, seed.key.attr) && seed.attrs[seed.key.attr] !== seed.key.value) {
927
961
  throw new Error(`${sat}.attrs.${seed.key.attr} conflicts with its natural key`);
928
962
  }
@@ -933,16 +967,16 @@ function validateProbes(integration, at) {
933
967
  if (!Array.isArray(integration.probes)) throw new Error(`${at}.probes must be an array`);
934
968
  for (const [index, probe] of integration.probes.entries()) {
935
969
  const pat = `${at}.probes[${index}]`;
936
- if (!isRecord4(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
970
+ if (!isRecord5(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
937
971
  if (!Number.isInteger(probe.expectedStatus) || probe.expectedStatus < 100 || probe.expectedStatus > 599) {
938
972
  throw new Error(`${pat}.expectedStatus must be an HTTP status`);
939
973
  }
940
974
  }
941
975
  }
942
- function isRecord4(value2) {
976
+ function isRecord5(value2) {
943
977
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
944
978
  }
945
- function safeText(value2, max) {
979
+ function safeText2(value2, max) {
946
980
  return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
947
981
  }
948
982
  function safeProbePath(value2) {
@@ -1072,6 +1106,7 @@ function validateRawConfig(raw, path) {
1072
1106
  if (cfg.services !== void 0 && (!Array.isArray(cfg.services) || cfg.services.some((service) => typeof service !== "string" || !service.trim()))) {
1073
1107
  throw new Error(`${path}: services must be an array of non-empty names`);
1074
1108
  }
1109
+ validateAiConfig(cfg, path);
1075
1110
  validateIntegrations(cfg, path, DEFAULT_SERVICES);
1076
1111
  }
1077
1112
  function validateCalendarConfig(cfg, envs, services, path) {
@@ -1080,11 +1115,11 @@ function validateCalendarConfig(cfg, envs, services, path) {
1080
1115
  if (enabled) throw new Error(`${path}: calendar.google is required when services includes "calendar"`);
1081
1116
  return;
1082
1117
  }
1083
- if (!isRecord5(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
1084
- assertOnly(cfg.calendar, ["google"], `${path}: calendar`);
1085
- if (!isRecord5(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
1118
+ if (!isRecord6(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
1119
+ assertOnly2(cfg.calendar, ["google"], `${path}: calendar`);
1120
+ if (!isRecord6(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
1086
1121
  const google = cfg.calendar.google;
1087
- assertOnly(
1122
+ assertOnly2(
1088
1123
  google,
1089
1124
  ["availabilityCalendars", "calendars", "bookingCalendar", "bookingPageUrl"],
1090
1125
  `${path}: calendar.google`
@@ -1094,7 +1129,7 @@ function validateCalendarConfig(cfg, envs, services, path) {
1094
1129
  throw new Error(`${path}: calendar.google requires exactly one of availabilityCalendars or calendars (legacy)`);
1095
1130
  }
1096
1131
  const availability = google[availabilityKey];
1097
- if (!isRecord5(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
1132
+ if (!isRecord6(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
1098
1133
  const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env));
1099
1134
  if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
1100
1135
  for (const env of envs) {
@@ -1105,22 +1140,22 @@ function validateCalendarConfig(cfg, envs, services, path) {
1105
1140
  if (ids.length > 10) {
1106
1141
  throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
1107
1142
  }
1108
- if (ids.some((id) => !safeText2(id, 1024))) {
1143
+ if (ids.some((id) => !safeText3(id, 1024))) {
1109
1144
  throw new Error(`${path}: calendar.google.${availabilityKey}.${env} contains an invalid calendar id`);
1110
1145
  }
1111
1146
  }
1112
1147
  if (google.bookingCalendar !== void 0) {
1113
- if (!isRecord5(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
1148
+ if (!isRecord6(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
1114
1149
  const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env));
1115
1150
  if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
1116
1151
  for (const [env, value2] of Object.entries(google.bookingCalendar)) {
1117
- if (!safeText2(value2, 1024)) {
1152
+ if (!safeText3(value2, 1024)) {
1118
1153
  throw new Error(`${path}: calendar.google.bookingCalendar.${env} must be a calendar id`);
1119
1154
  }
1120
1155
  }
1121
1156
  }
1122
1157
  if (google.bookingPageUrl !== void 0) {
1123
- if (!isRecord5(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
1158
+ if (!isRecord6(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
1124
1159
  const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env));
1125
1160
  if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingPageUrl.${unknownBookingEnv} is not in config envs`);
1126
1161
  for (const [env, value2] of Object.entries(google.bookingPageUrl)) {
@@ -1143,14 +1178,14 @@ function validateServices(services, path) {
1143
1178
  }
1144
1179
  }
1145
1180
  }
1146
- function assertOnly(value2, allowed, label) {
1181
+ function assertOnly2(value2, allowed, label) {
1147
1182
  const extra = Object.keys(value2).find((key) => !allowed.includes(key));
1148
1183
  if (extra) throw new Error(`${label}.${extra} is not supported`);
1149
1184
  }
1150
- function isRecord5(value2) {
1185
+ function isRecord6(value2) {
1151
1186
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
1152
1187
  }
1153
- function safeText2(value2, max) {
1188
+ function safeText3(value2, max) {
1154
1189
  return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
1155
1190
  }
1156
1191
  function safeHttpsUrl(value2) {
@@ -1482,8 +1517,8 @@ async function calendarCalendars(options) {
1482
1517
  async function calendarConnect(options) {
1483
1518
  const { cfg, ctx, out } = await lifecycleContext(options);
1484
1519
  productionConsent(ctx.env, options.yes, "connect calendar");
1485
- const page = calendarBookingPageUrl(cfg, ctx.env);
1486
- const applied = page === void 0 ? await readCalendarStatus(ctx) : await applyCalendarSettings(ctx, page);
1520
+ const page2 = calendarBookingPageUrl(cfg, ctx.env);
1521
+ const applied = page2 === void 0 ? await readCalendarStatus(ctx) : await applyCalendarSettings(ctx, page2);
1487
1522
  const connectOptions = connectionOptions(options, out);
1488
1523
  return await continueConnectedCalendar(ctx, applied, connectOptions) ?? connectWithContext(ctx, connectOptions);
1489
1524
  }
@@ -1737,7 +1772,7 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
1737
1772
  `${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`
1738
1773
  );
1739
1774
  }
1740
- throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText3(res)}`);
1775
+ throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
1741
1776
  }
1742
1777
  async function postJson(doFetch, url, bearer, body) {
1743
1778
  const res = await doFetch(url, {
@@ -1745,7 +1780,7 @@ async function postJson(doFetch, url, bearer, body) {
1745
1780
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
1746
1781
  body: JSON.stringify(body)
1747
1782
  });
1748
- if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText3(res)}`);
1783
+ if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText4(res)}`);
1749
1784
  }
1750
1785
  function normalizeClerkConfig(value2) {
1751
1786
  if (!value2) return null;
@@ -1758,7 +1793,7 @@ function normalizeClerkConfig(value2) {
1758
1793
  const publishableKey = envValue(cfg.publishableKey);
1759
1794
  return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
1760
1795
  }
1761
- async function safeText3(res) {
1796
+ async function safeText4(res) {
1762
1797
  try {
1763
1798
  return redactSecrets((await res.text()).slice(0, 500));
1764
1799
  } catch {
@@ -1830,12 +1865,12 @@ import {
1830
1865
  import { readFileSync as readFileSync4 } from "fs";
1831
1866
 
1832
1867
  // src/config-reconcile-digest.ts
1833
- import { createHash } from "crypto";
1868
+ import { createHash as createHash2 } from "crypto";
1834
1869
  function canonicalJson(value2) {
1835
1870
  return JSON.stringify(canonicalValue(value2));
1836
1871
  }
1837
1872
  function configDigest(value2) {
1838
- return `sha256:${createHash("sha256").update(canonicalJson(value2)).digest("hex")}`;
1873
+ return `sha256:${createHash2("sha256").update(canonicalJson(value2)).digest("hex")}`;
1839
1874
  }
1840
1875
  function canonicalValue(value2) {
1841
1876
  if (value2 === null || typeof value2 === "string" || typeof value2 === "boolean") return value2;
@@ -2909,18 +2944,18 @@ function assertSeedContracts(integrations, schema) {
2909
2944
  }
2910
2945
  }
2911
2946
  function isUniqueAttr(schema, ns, attr) {
2912
- if (!isRecord6(schema) || !isRecord6(schema.entities)) return false;
2947
+ if (!isRecord7(schema) || !isRecord7(schema.entities)) return false;
2913
2948
  const entity = schema.entities[ns];
2914
- if (!isRecord6(entity) || !isRecord6(entity.attrs)) return false;
2949
+ if (!isRecord7(entity) || !isRecord7(entity.attrs)) return false;
2915
2950
  const definition = entity.attrs[attr];
2916
- return isRecord6(definition) && definition.unique === true;
2951
+ return isRecord7(definition) && definition.unique === true;
2917
2952
  }
2918
2953
  function normalizeSchema(value2) {
2919
2954
  if (value2 === void 0 || value2 === null) return { entities: {}, links: {} };
2920
- if (!isRecord6(value2) || !isRecord6(value2.entities)) {
2955
+ if (!isRecord7(value2) || !isRecord7(value2.entities)) {
2921
2956
  throw new Error("db schema must be a serialized schema object with an entities map");
2922
2957
  }
2923
- if (value2.links !== void 0 && !isRecord6(value2.links)) throw new Error("db schema links must be an object");
2958
+ if (value2.links !== void 0 && !isRecord7(value2.links)) throw new Error("db schema links must be an object");
2924
2959
  return {
2925
2960
  entities: { ...value2.entities },
2926
2961
  links: { ...value2.links ?? {} }
@@ -2934,7 +2969,7 @@ function mergeMap(target, fragment, label) {
2934
2969
  target[name] = value2;
2935
2970
  }
2936
2971
  }
2937
- function isRecord6(value2) {
2972
+ function isRecord7(value2) {
2938
2973
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
2939
2974
  }
2940
2975
 
@@ -2953,7 +2988,7 @@ async function doctor(options) {
2953
2988
  out.log(`integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
2954
2989
  out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
2955
2990
  out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
2956
- out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
2991
+ out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
2957
2992
  if (cfg.services.includes("calendar")) {
2958
2993
  const calendar = cfg.envs.map((env) => {
2959
2994
  const resolved = calendarServiceConfig(cfg, env);
@@ -2974,7 +3009,9 @@ async function doctor(options) {
2974
3009
  }
2975
3010
  }
2976
3011
  warnings.push(...integrationWarnings(database.integrations, schema, rules));
2977
- if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
3012
+ if (cfg.services.includes("ai") && cfg.ai?.mode === "byok" && !cfg.ai.provider) {
3013
+ warnings.push("ai.mode is byok but ai.provider is not set");
3014
+ }
2978
3015
  if (cfg.auth?.clerk) {
2979
3016
  for (const [env, value2] of Object.entries(cfg.auth.clerk)) {
2980
3017
  if (typeof value2 === "string" && value2.startsWith("$") && !process.env[value2.slice(1)]) {
@@ -3035,7 +3072,7 @@ function initProject(options) {
3035
3072
  if (!services.includes(dependency)) throw new Error(`--services ${service} requires ${dependency}`);
3036
3073
  }
3037
3074
  }
3038
- const aiProvider = options.aiProvider ?? "anthropic";
3075
+ const aiProvider = options.aiProvider;
3039
3076
  mkdirSync2(dirname4(configPath), { recursive: true });
3040
3077
  mkdirSync2(resolve4(rootDir, "src/odla"), { recursive: true });
3041
3078
  mkdirSync2(resolve4(rootDir, ".odla"), { recursive: true });
@@ -3062,6 +3099,15 @@ function configTemplate(input) {
3062
3099
  },
3063
3100
  },
3064
3101
  ` : "";
3102
+ const ai = input.aiProvider ? ` ai: {
3103
+ mode: "byok",
3104
+ provider: process.env.ODLA_AI_PROVIDER ?? "${input.aiProvider}",
3105
+ // Optional: set this env var while running provision to store the provider
3106
+ // key in the app vault for each tenant.
3107
+ keyEnv: "${defaultKeyEnv(input.aiProvider)}",
3108
+ },` : ` // Hosted AI uses admin-approved models and central cost tracking.
3109
+ // Pass --ai-provider during init only when this app must use BYOK.
3110
+ ai: { mode: "hosted" },`;
3065
3111
  return `export default {
3066
3112
  platformUrl: process.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
3067
3113
  dbEndpoint: process.env.ODLA_ENDPOINT ?? process.env.ODLA_DB_ENDPOINT ?? "https://db.odla.ai",
@@ -3080,12 +3126,7 @@ function configTemplate(input) {
3080
3126
  // When rules is omitted, the CLI generates deny-all rules from schema.
3081
3127
  defaultRules: "deny",
3082
3128
  },
3083
- ai: {
3084
- provider: process.env.ODLA_AI_PROVIDER ?? "${input.aiProvider}",
3085
- // Optional: set this env var while running provision to store the provider
3086
- // key in the platform vault for each tenant.
3087
- keyEnv: "${defaultKeyEnv(input.aiProvider)}",
3088
- },
3129
+ ${ai}
3089
3130
  ${calendar}
3090
3131
  auth: {
3091
3132
  clerk: {
@@ -3294,9 +3335,10 @@ For work that creates an odla app or adds odla services, read and follow
3294
3335
  \`.agents/skills/odla-migrate/SKILL.md\`. For production telemetry triage, use
3295
3336
  \`.agents/skills/odla-o11y-debug/SKILL.md\`.
3296
3337
 
3297
- Track the work in odla's PM as you go \u2014 record decisions when you make them, file
3298
- bugs when you notice them, and move tasks when the work moves
3299
- (\`npx @odla-ai/cli pm task list\`). The conventions and the full command set are
3338
+ Track the work in odla's PM as you go. Before project-mutating work, run
3339
+ \`npx @odla-ai/cli pm next --app <appId>\`, confirm alignment to an open goal,
3340
+ and atomically claim a refined Ready task. Record decisions when you make them
3341
+ and file bugs when you notice them. The conventions and the full command set are
3300
3342
  in \`.agents/skills/odla/references/pm.md\`.
3301
3343
 
3302
3344
  The setup runbooks and their references are installed in this repository, pinned
@@ -3580,12 +3622,16 @@ async function smoke(options) {
3580
3622
  out.log(` tenant: ${entry.tenantId}`);
3581
3623
  const publicConfig = await getJson(doFetch, publicConfigUrl(cfg.platformUrl, cfg.app.id, env), void 0);
3582
3624
  out.log(` public-config: ok`);
3583
- if (cfg.ai?.provider) {
3625
+ if (cfg.services.includes("ai") && cfg.ai?.provider) {
3584
3626
  const provider = publicConfig.ai?.provider ?? null;
3585
3627
  if (provider !== cfg.ai.provider) {
3586
3628
  throw new Error(`ai provider mismatch: expected "${cfg.ai.provider}", public-config has "${provider ?? "none"}"`);
3587
3629
  }
3588
- out.log(` ai: ${provider}`);
3630
+ out.log(` ai: byok/${provider}`);
3631
+ } else if (cfg.services.includes("ai")) {
3632
+ const mode = publicConfig.ai?.mode;
3633
+ if (mode !== "hosted") throw new Error(`ai mode mismatch: expected "hosted", public-config has "${String(mode ?? "none")}"`);
3634
+ out.log(" ai: hosted");
3589
3635
  }
3590
3636
  if (hasO11y) out.log(` o11y: credentials present`);
3591
3637
  if (cfg.services.includes("calendar")) {
@@ -3663,7 +3709,7 @@ async function getJson(doFetch, url, bearer) {
3663
3709
  const res = await doFetch(url, {
3664
3710
  headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
3665
3711
  });
3666
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
3712
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
3667
3713
  return res.json();
3668
3714
  }
3669
3715
  async function postJson2(doFetch, url, bearer, body) {
@@ -3672,7 +3718,7 @@ async function postJson2(doFetch, url, bearer, body) {
3672
3718
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
3673
3719
  body: JSON.stringify(body)
3674
3720
  });
3675
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
3721
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
3676
3722
  return res.json();
3677
3723
  }
3678
3724
  function publicConfigUrl(platformUrl, appId, env) {
@@ -3680,7 +3726,7 @@ function publicConfigUrl(platformUrl, appId, env) {
3680
3726
  url.searchParams.set("env", env);
3681
3727
  return url.toString();
3682
3728
  }
3683
- async function safeText4(res) {
3729
+ async function safeText5(res) {
3684
3730
  try {
3685
3731
  return redactSecrets((await res.text()).slice(0, 500));
3686
3732
  } catch {
@@ -3929,7 +3975,7 @@ var CODE_BUILD_RECIPES = Object.freeze([{
3929
3975
 
3930
3976
  // src/code-images.ts
3931
3977
  import { spawn as spawn3 } from "child_process";
3932
- import { createHash as createHash2 } from "crypto";
3978
+ import { createHash as createHash3 } from "crypto";
3933
3979
  import { copyFile, mkdtemp, readFile, rm, writeFile } from "fs/promises";
3934
3980
  import { tmpdir } from "os";
3935
3981
  import { join as join8 } from "path";
@@ -3988,7 +4034,7 @@ async function embeddedPiImageName() {
3988
4034
  const bundle = await readFile(embeddedPiAssetPath()).catch(() => {
3989
4035
  throw new Error("CLI-embedded Pi runtime is missing; reinstall this exact @odla-ai/cli version");
3990
4036
  });
3991
- return `odla-ai/pi-agent:embedded-sha256-${createHash2("sha256").update(bundle).digest("hex")}`;
4037
+ return `odla-ai/pi-agent:embedded-sha256-${createHash3("sha256").update(bundle).digest("hex")}`;
3992
4038
  }
3993
4039
  async function buildEmbeddedPiImage(engine, image, run) {
3994
4040
  const context = await mkdtemp(join8(tmpdir(), "odla-code-pi-"));
@@ -4611,7 +4657,7 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
4611
4657
  }
4612
4658
 
4613
4659
  // ../harness/dist/chunk-GMVZ4LZH.js
4614
- import { createHash as createHash3 } from "crypto";
4660
+ import { createHash as createHash4 } from "crypto";
4615
4661
  import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
4616
4662
  import { relative as relative4, resolve as resolve7 } from "path";
4617
4663
 
@@ -4675,7 +4721,7 @@ function dependenciesOf(values, influence = "data") {
4675
4721
  return [...unique3.values()];
4676
4722
  }
4677
4723
 
4678
- // ../camel/dist/chunk-S7EVNA2U.js
4724
+ // ../camel/dist/chunk-4DQ6BIHP.js
4679
4725
  var camelValueBrand = /* @__PURE__ */ Symbol("@odla-ai/camel/value");
4680
4726
  var authenticCamelValues = /* @__PURE__ */ new WeakSet();
4681
4727
  function isCamelValue(value2) {
@@ -4687,13 +4733,13 @@ function isSafe(value2) {
4687
4733
  function isUnsafe(value2) {
4688
4734
  return isCamelValue(value2) && value2.label.promptSafety === "unsafe";
4689
4735
  }
4690
- function createSafeInternal(value2, safeBasis, metadata2) {
4736
+ function createSafeInternal(value2, safeBasis, metadata2, extra) {
4691
4737
  return createValue(value2, {
4692
4738
  schemaVersion: 1,
4693
4739
  promptSafety: "safe",
4694
4740
  safeBasis,
4695
4741
  ...copyMetadata(metadata2)
4696
- });
4742
+ }, extra);
4697
4743
  }
4698
4744
  function createUnsafeInternal(value2, metadata2) {
4699
4745
  return createValue(value2, {
@@ -4702,8 +4748,8 @@ function createUnsafeInternal(value2, metadata2) {
4702
4748
  ...copyMetadata(metadata2)
4703
4749
  });
4704
4750
  }
4705
- function createValue(value2, label) {
4706
- const result = { value: value2, label: Object.freeze(label) };
4751
+ function createValue(value2, label, extra) {
4752
+ const result = { value: value2, label: Object.freeze(label), ...extra };
4707
4753
  Object.defineProperty(result, camelValueBrand, { value: label.promptSafety, enumerable: false });
4708
4754
  authenticCamelValues.add(result);
4709
4755
  return Object.freeze(result);
@@ -4964,7 +5010,7 @@ import { dirname as dirname6, join as join23, resolve as resolve32, sep as sep23
4964
5010
  import { readFile as readFile22, readdir as readdir22, stat as stat2 } from "fs/promises";
4965
5011
  import { relative as relative22, resolve as resolve42 } from "path";
4966
5012
 
4967
- // ../camel/dist/chunk-LAXU2AVK.js
5013
+ // ../camel/dist/chunk-4EIRFS3A.js
4968
5014
  function conversionPolicyDigest(policy) {
4969
5015
  return sha256Hex(canonicalJson2(policy));
4970
5016
  }
@@ -5052,10 +5098,18 @@ async function convert(source, policy, value2, counts) {
5052
5098
  const count = counts.get(countKey) ?? 0;
5053
5099
  if (count >= policy.maximumOutputsPerArtifact) throw new CamelError("limit_exceeded", "Conversion output count exceeds its per-source bound.");
5054
5100
  counts.set(countKey, count + 1);
5101
+ const conversionRecordId = await sha256Hex(canonicalJson2({ conversionId: policy.conversionId, digest: policy.digest, source: sourceKey, ordinal: count }));
5102
+ const datumId = await sha256Hex(canonicalJson2({ conversionRecordId, value: value2 }));
5055
5103
  return createSafeInternal(value2, "atomic_conversion", {
5056
5104
  readers: source.label.readers,
5057
5105
  provenance: [...source.label.provenance, { kind: "converter", id: policy.conversionId, digest: policy.digest }],
5058
5106
  dependencies: dependenciesOf([source])
5107
+ }, {
5108
+ datumId,
5109
+ kind: policy.output.kind,
5110
+ conversionId: policy.conversionId,
5111
+ conversionDigest: policy.digest,
5112
+ conversionRecordId
5059
5113
  });
5060
5114
  }
5061
5115
  function validatePolicyShape(policy) {
@@ -5120,7 +5174,7 @@ function missingPolicy() {
5120
5174
  throw new CamelError("conversion_rejected", "Conversion policy is not registered.");
5121
5175
  }
5122
5176
 
5123
- // ../camel/dist/chunk-P2WQIAG6.js
5177
+ // ../camel/dist/chunk-VEAUXH4F.js
5124
5178
  function createCamelIngress(constants2 = []) {
5125
5179
  const byId = /* @__PURE__ */ new Map();
5126
5180
  for (const item of constants2) {
@@ -5257,7 +5311,7 @@ async function digestStagedWorkspace(root, limits) {
5257
5311
  }
5258
5312
  };
5259
5313
  await walk(resolve7(root));
5260
- const hash = createHash3("sha256");
5314
+ const hash = createHash4("sha256");
5261
5315
  let bytes = 0;
5262
5316
  for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
5263
5317
  const content2 = await readFile2(file.target);
@@ -6896,7 +6950,7 @@ var CodePiRuntimeEngine = class {
6896
6950
 
6897
6951
  // src/code-local-source.ts
6898
6952
  import { execFile as execFile3 } from "child_process";
6899
- import { createHash as createHash4 } from "crypto";
6953
+ import { createHash as createHash5 } from "crypto";
6900
6954
  var SOURCE_LIMITS2 = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
6901
6955
  async function prepareCodeLocalSource(cwd, repository, readHead = readGitHead) {
6902
6956
  const headCommitSha = await readHead(cwd);
@@ -6947,7 +7001,7 @@ async function readGitHead(cwd) {
6947
7001
  return value2;
6948
7002
  }
6949
7003
  function digestText(value2) {
6950
- return `sha256:${createHash4("sha256").update(value2).digest("hex")}`;
7004
+ return `sha256:${createHash5("sha256").update(value2).digest("hex")}`;
6951
7005
  }
6952
7006
 
6953
7007
  // src/code-connect.ts
@@ -7143,7 +7197,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
7143
7197
  const payload = await postJson3(doFetch, `${base}/query`, dbKey, {
7144
7198
  query: { [seed.ns]: { $: { where: { [seed.key.attr]: seed.key.value }, limit: 1 } } }
7145
7199
  });
7146
- const rows = isRecord7(payload) && isRecord7(payload.result) ? payload.result[seed.ns] : void 0;
7200
+ const rows = isRecord8(payload) && isRecord8(payload.result) ? payload.result[seed.ns] : void 0;
7147
7201
  if (!Array.isArray(rows)) {
7148
7202
  throw new Error(`${env}: integration ${integration.id} seed ${seed.id} query returned an invalid response`);
7149
7203
  }
@@ -7175,7 +7229,7 @@ async function postJson3(doFetch, url, bearer, body) {
7175
7229
  if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await responseText(res)}`);
7176
7230
  return res.json().catch(() => ({}));
7177
7231
  }
7178
- function isRecord7(value2) {
7232
+ function isRecord8(value2) {
7179
7233
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
7180
7234
  }
7181
7235
  async function responseText(res) {
@@ -7245,14 +7299,14 @@ async function mintDbKey(opts, tenantId) {
7245
7299
  appId: tenantId
7246
7300
  })
7247
7301
  });
7248
- if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText5(created)}`);
7302
+ if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText6(created)}`);
7249
7303
  res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
7250
7304
  method: "POST",
7251
7305
  headers,
7252
7306
  body: "{}"
7253
7307
  });
7254
7308
  }
7255
- if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
7309
+ if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText6(res)}`);
7256
7310
  const body = await res.json();
7257
7311
  if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
7258
7312
  return body.key;
@@ -7268,12 +7322,12 @@ async function issueO11yToken(opts) {
7268
7322
  `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`
7269
7323
  );
7270
7324
  }
7271
- if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText5(res)}`);
7325
+ if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText6(res)}`);
7272
7326
  const body = await res.json();
7273
7327
  if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
7274
7328
  return body.token;
7275
7329
  }
7276
- async function safeText5(res) {
7330
+ async function safeText6(res) {
7277
7331
  try {
7278
7332
  return redactSecrets((await res.text()).slice(0, 500));
7279
7333
  } catch {
@@ -7316,7 +7370,7 @@ async function provision(options) {
7316
7370
  const namespaces = Object.keys(integration.schema?.entities ?? {}).length;
7317
7371
  out.log(` integration.${integration.id}: ${namespaces} namespaces, ${integration.seeds?.length ?? 0} seeds, ${integration.probes?.length ?? 0} smoke probes`);
7318
7372
  }
7319
- out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
7373
+ out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "hosted" : "not enabled"}`);
7320
7374
  if (cfg.services.includes("calendar")) {
7321
7375
  for (const env of cfg.envs) {
7322
7376
  const calendar = calendarServiceConfig(cfg, env);
@@ -7367,11 +7421,11 @@ async function provision(options) {
7367
7421
  for (const service of serviceOrder) {
7368
7422
  if (service === "ai") {
7369
7423
  if (cfg.ai?.provider) {
7370
- await apps.setAi(cfg.app.id, env, { provider: cfg.ai.provider, ...cfg.ai.model ? { model: cfg.ai.model } : {} });
7371
- out.log(`${env}: ai configured (${cfg.ai.provider}${cfg.ai.model ? `/${cfg.ai.model}` : ""})`);
7424
+ await apps.setAi(cfg.app.id, env, { mode: "byok", provider: cfg.ai.provider, ...cfg.ai.model ? { model: cfg.ai.model } : {} });
7425
+ out.log(`${env}: ai configured (byok ${cfg.ai.provider}${cfg.ai.model ? `/${cfg.ai.model}` : ""})`);
7372
7426
  } else {
7373
- await apps.setService(cfg.app.id, "ai", true, { env });
7374
- out.log(`${env}: ai enabled`);
7427
+ await apps.setAi(cfg.app.id, env, { mode: "hosted", ...cfg.ai?.model ? { model: cfg.ai.model } : {} });
7428
+ out.log(`${env}: ai configured (hosted${cfg.ai?.model ? `/${cfg.ai.model}` : ""})`);
7375
7429
  }
7376
7430
  } else {
7377
7431
  const config = service === "calendar" ? calendarServiceConfig(cfg, env) : void 0;
@@ -7492,12 +7546,23 @@ var PM_ACTIONS = {
7492
7546
  done: {},
7493
7547
  comment: {},
7494
7548
  comments: {},
7549
+ ref: {},
7495
7550
  rm: {},
7496
7551
  delete: {}
7497
7552
  };
7498
- var PM_ENTITIES = Object.fromEntries(
7499
- ["goal", "conformance", "task", "kanban", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
7500
- );
7553
+ var PM_TASK_ACTIONS = {
7554
+ ...PM_ACTIONS,
7555
+ ready: {},
7556
+ claim: {},
7557
+ release: {}
7558
+ };
7559
+ var PM_ENTITIES = {
7560
+ ...Object.fromEntries(
7561
+ ["goal", "conformance", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
7562
+ ),
7563
+ task: PM_TASK_ACTIONS,
7564
+ kanban: PM_TASK_ACTIONS
7565
+ };
7501
7566
  var COMMAND_SURFACE = {
7502
7567
  agent: { jobs: {}, retry: {} },
7503
7568
  admin: {
@@ -7550,7 +7615,9 @@ var COMMAND_SURFACE = {
7550
7615
  },
7551
7616
  pm: {
7552
7617
  ...PM_ENTITIES,
7553
- handoff: {}
7618
+ handoff: {},
7619
+ next: {},
7620
+ watch: {}
7554
7621
  },
7555
7622
  provision: {},
7556
7623
  runbook: {
@@ -7694,17 +7761,17 @@ async function runHostedSecurity(options) {
7694
7761
  allowNetwork: false
7695
7762
  }
7696
7763
  });
7697
- const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
7698
- await writeSecurityArtifacts(output, report4);
7699
- const reportDigest = await securityFingerprint(report4);
7764
+ const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
7765
+ await writeSecurityArtifacts(output, report5);
7766
+ const reportDigest = await securityFingerprint(report5);
7700
7767
  await hosted.complete({
7701
7768
  reportDigest,
7702
- coverageStatus: report4.coverageStatus,
7703
- confirmed: report4.metrics.confirmed,
7704
- candidates: report4.metrics.candidates
7769
+ coverageStatus: report5.coverageStatus,
7770
+ confirmed: report5.metrics.confirmed,
7771
+ candidates: report5.metrics.candidates
7705
7772
  }, { signal: options.signal });
7706
- printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
7707
- return Object.freeze({ report: report4, run: hosted.run, output });
7773
+ printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
7774
+ return Object.freeze({ report: report5, run: hosted.run, output });
7708
7775
  }
7709
7776
  function selectEnv(requested, declared, configPath, rootDir) {
7710
7777
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
@@ -7730,14 +7797,14 @@ function profileFor(name, maxHuntTasks) {
7730
7797
  if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
7731
7798
  return { ...profile, maxHuntTasks };
7732
7799
  }
7733
- function printSummary(out, appId, env, run, report4, output) {
7734
- const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
7800
+ function printSummary(out, appId, env, run, report5, output) {
7801
+ const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
7735
7802
  out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
7736
7803
  out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
7737
7804
  out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
7738
- 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}`);
7739
- if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
7740
- out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
7805
+ 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}`);
7806
+ if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
7807
+ out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
7741
7808
  out.log(` report: ${resolve9(output, "REPORT.md")}`);
7742
7809
  }
7743
7810
  function formatBudget(usage) {
@@ -9431,7 +9498,7 @@ Start here:
9431
9498
 
9432
9499
  Usage:
9433
9500
  odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
9434
- odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
9501
+ odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod] [--ai-provider <byok-provider>]
9435
9502
  odla-ai doctor [--config odla.config.mjs]
9436
9503
  odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
9437
9504
  odla-ai config apply --plan <plan.json> [--idempotency-key <key>] [--email <odla-account>] [--json]
@@ -9454,16 +9521,22 @@ Usage:
9454
9521
  odla-ai app owners remove <email> [--email <odla-account>] [--json]
9455
9522
  odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
9456
9523
  odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
9457
- odla-ai pm task list [--app <id>] [--column <c>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
9524
+ odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
9458
9525
  odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
9459
9526
  odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
9460
9527
  odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
9461
- 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]
9528
+ 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]
9529
+ odla-ai pm next --app <id> [--json]
9530
+ 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]
9531
+ 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]
9532
+ odla-ai pm task claim <id> --expected-revision <n> [--mutation-id <id>] [--json]
9533
+ odla-ai pm task release <id> --expected-revision <n> [--mutation-id <id>] [--json]
9462
9534
  odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
9463
9535
  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]
9464
9536
  odla-ai pm <goal|task|decision|bug> get <id> [--json]
9537
+ odla-ai pm <goal|task|decision|bug> ref <id> [--json]
9465
9538
  odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
9466
- 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]
9539
+ 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]
9467
9540
  odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
9468
9541
  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]
9469
9542
  odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
@@ -9782,11 +9855,11 @@ async function discussList(ctx, parsed) {
9782
9855
  if (value2) query.set(param, value2);
9783
9856
  }
9784
9857
  const qs = query.toString();
9785
- const page = await request(ctx, "GET", `/topics${qs ? `?${qs}` : ""}`);
9786
- emit(ctx, page, () => {
9787
- ctx.out.log(`topics \u2014 ${page.topics.length} of ${page.total}`);
9858
+ const page2 = await request(ctx, "GET", `/topics${qs ? `?${qs}` : ""}`);
9859
+ emit(ctx, page2, () => {
9860
+ ctx.out.log(`topics \u2014 ${page2.topics.length} of ${page2.total}`);
9788
9861
  ctx.out.log("id state app replies subject");
9789
- for (const topic of page.topics) {
9862
+ for (const topic of page2.topics) {
9790
9863
  ctx.out.log(
9791
9864
  `${topic.id} ${state(topic)} ${topic.appId ?? ""} ${topic.replyCount} ${topic.subject}`
9792
9865
  );
@@ -9801,11 +9874,11 @@ async function discussRead(ctx, id, parsed) {
9801
9874
  limit: requestedLimit ?? "200",
9802
9875
  offset: requestedOffset ?? "0"
9803
9876
  });
9804
- const page = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
9877
+ const page2 = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
9805
9878
  emit(
9806
9879
  ctx,
9807
- page,
9808
- () => renderDiscussRead(ctx, page.topic, page.posts, page)
9880
+ page2,
9881
+ () => renderDiscussRead(ctx, page2.topic, page2.posts, page2)
9809
9882
  );
9810
9883
  return;
9811
9884
  }
@@ -9818,20 +9891,20 @@ async function discussRead(ctx, id, parsed) {
9818
9891
  let topic = null;
9819
9892
  let offset = 0;
9820
9893
  for (; ; ) {
9821
- const page = await request(
9894
+ const page2 = await request(
9822
9895
  ctx,
9823
9896
  "GET",
9824
9897
  `/topics/${encodeURIComponent(id)}?limit=200&offset=${offset}`
9825
9898
  );
9826
- topic = page.topic;
9827
- for (const post of page.posts) posts.set(post.id, post);
9828
- mergeDiscussPrincipals(projection, page);
9899
+ topic = page2.topic;
9900
+ for (const post of page2.posts) posts.set(post.id, post);
9901
+ mergeDiscussPrincipals(projection, page2);
9829
9902
  if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
9830
- if (!page.page?.hasMore) break;
9831
- if (page.page.nextOffset === null || page.page.nextOffset <= offset) {
9903
+ if (!page2.page?.hasMore) break;
9904
+ if (page2.page.nextOffset === null || page2.page.nextOffset <= offset) {
9832
9905
  throw new Error("discuss read failed: registry returned a non-advancing post page");
9833
9906
  }
9834
- offset = page.page.nextOffset;
9907
+ offset = page2.page.nextOffset;
9835
9908
  }
9836
9909
  const ordered = [...posts.values()].sort(
9837
9910
  (a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
@@ -9999,9 +10072,9 @@ async function discussWatch(ctx, topicId, parsed) {
9999
10072
  let firstSuccess = true;
10000
10073
  let consecutiveFailures = 0;
10001
10074
  for (; ; ) {
10002
- let page;
10075
+ let page2;
10003
10076
  try {
10004
- page = await getWatchPage(ctx, requestPath(topicId, app, cursor));
10077
+ page2 = await getWatchPage(ctx, requestPath(topicId, app, cursor));
10005
10078
  consecutiveFailures = 0;
10006
10079
  } catch (error) {
10007
10080
  if (!(error instanceof WatchRequestError) || !error.retryable) {
@@ -10037,25 +10110,25 @@ async function discussWatch(ctx, topicId, parsed) {
10037
10110
  await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
10038
10111
  continue;
10039
10112
  }
10040
- cursor = page.cursor;
10041
- const baseline = firstSuccess && page.events.length === 0;
10113
+ cursor = page2.cursor;
10114
+ const baseline = firstSuccess && page2.events.length === 0;
10042
10115
  if (baseline) {
10043
10116
  jsonl(ctx, parsed, {
10044
10117
  type: "checkpoint",
10045
- streamId: page.streamId,
10118
+ streamId: page2.streamId,
10046
10119
  cursor,
10047
- serverTime: page.serverTime
10120
+ serverTime: page2.serverTime
10048
10121
  });
10049
10122
  }
10050
10123
  firstSuccess = false;
10051
- const matching = page.events.filter((event) => {
10124
+ const matching = page2.events.filter((event) => {
10052
10125
  if (topicId && (event.type !== "message" || event.action !== "created")) return false;
10053
10126
  return (!by || event.actor.id === by) && (!self || event.actor.id !== self);
10054
10127
  });
10055
10128
  for (const event of matching) {
10056
10129
  jsonl(ctx, parsed, {
10057
10130
  type: "event",
10058
- streamId: page.streamId,
10131
+ streamId: page2.streamId,
10059
10132
  eventId: event.id,
10060
10133
  cursor: event.cursor,
10061
10134
  event
@@ -10064,9 +10137,9 @@ async function discussWatch(ctx, topicId, parsed) {
10064
10137
  if (matching.length > 0) {
10065
10138
  jsonl(ctx, parsed, {
10066
10139
  type: "checkpoint",
10067
- streamId: page.streamId,
10140
+ streamId: page2.streamId,
10068
10141
  cursor,
10069
- serverTime: page.serverTime
10142
+ serverTime: page2.serverTime
10070
10143
  });
10071
10144
  const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
10072
10145
  const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
@@ -10074,28 +10147,28 @@ async function discussWatch(ctx, topicId, parsed) {
10074
10147
  found: true,
10075
10148
  cursor,
10076
10149
  events: matching,
10077
- ...page.authors ? { authors: page.authors } : {},
10078
- ...page.principals ? { principals: page.principals } : {},
10150
+ ...page2.authors ? { authors: page2.authors } : {},
10151
+ ...page2.principals ? { principals: page2.principals } : {},
10079
10152
  ...posts && posts.length > 0 ? { posts } : {},
10080
10153
  ...topics && topics.length > 0 ? { topics } : {}
10081
10154
  });
10082
10155
  }
10083
- if (page.events.length > 0) {
10156
+ if (page2.events.length > 0) {
10084
10157
  jsonl(ctx, parsed, {
10085
10158
  type: "checkpoint",
10086
- streamId: page.streamId,
10159
+ streamId: page2.streamId,
10087
10160
  cursor,
10088
- serverTime: page.serverTime
10161
+ serverTime: page2.serverTime
10089
10162
  });
10090
10163
  } else if (!baseline) {
10091
10164
  jsonl(ctx, parsed, {
10092
10165
  type: "heartbeat",
10093
- streamId: page.streamId,
10166
+ streamId: page2.streamId,
10094
10167
  cursor,
10095
- serverTime: page.serverTime
10168
+ serverTime: page2.serverTime
10096
10169
  });
10097
10170
  }
10098
- if (page.hasMore) continue;
10171
+ if (page2.hasMore) continue;
10099
10172
  if (deadline !== void 0 && now() >= deadline) {
10100
10173
  return report2(ctx, parsed, { found: false, cursor });
10101
10174
  }
@@ -10214,7 +10287,13 @@ async function discussCommand(parsed, deps = {}) {
10214
10287
  }
10215
10288
  }
10216
10289
 
10217
- // src/pm-actions.ts
10290
+ // src/pm-action-core.ts
10291
+ var DONE = {
10292
+ goal: { status: "met", currentPct: 100 },
10293
+ task: { column: "done" },
10294
+ decision: { status: "accepted" },
10295
+ bug: { status: "fixed" }
10296
+ };
10218
10297
  var writeMutationId2 = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
10219
10298
  var FIELD_MAP = {
10220
10299
  title: { key: "title" },
@@ -10230,22 +10309,27 @@ var FIELD_MAP = {
10230
10309
  body: { key: "body" },
10231
10310
  target: { key: "targetPct", num: true },
10232
10311
  description: { key: "description" },
10233
- desc: { key: "description" }
10234
- };
10235
- var DONE = {
10236
- goal: { status: "met", currentPct: 100 },
10237
- task: { column: "done" },
10238
- decision: { status: "accepted" },
10239
- bug: { status: "fixed" }
10312
+ desc: { key: "description" },
10313
+ acceptance: { key: "acceptanceCriteria" },
10314
+ "alignment-decision": { key: "alignmentDecisionId" },
10315
+ execution: { key: "executionMode" },
10316
+ "expected-revision": { key: "expectedRevision", num: true }
10240
10317
  };
10241
10318
  async function pmRequest(ctx, method, path, body) {
10242
- const res = await ctx.doFetch(`${ctx.platformUrl}/registry/pm${path}`, {
10319
+ const response2 = await ctx.doFetch(`${ctx.platformUrl}/registry/pm${path}`, {
10243
10320
  method,
10244
- headers: { authorization: `Bearer ${ctx.token}`, "content-type": "application/json" },
10321
+ headers: {
10322
+ authorization: `Bearer ${ctx.token}`,
10323
+ "content-type": "application/json"
10324
+ },
10245
10325
  body: body === void 0 ? void 0 : JSON.stringify(body)
10246
10326
  });
10247
- const data = await res.json().catch(() => ({}));
10248
- if (!res.ok) throw new Error(`pm ${method} ${path} failed: ${data.error ?? `registry returned ${res.status}`}`);
10327
+ const data = await response2.json().catch(() => ({}));
10328
+ if (!response2.ok) {
10329
+ throw new Error(
10330
+ `pm ${method} ${path} failed: ${data.error ?? `registry returned ${response2.status}`}`
10331
+ );
10332
+ }
10249
10333
  return data;
10250
10334
  }
10251
10335
  function collectFields(parsed, allowClear) {
@@ -10268,20 +10352,32 @@ function collectEntityFields(entity, parsed, allowClear) {
10268
10352
  if (fields.description === void 0) fields.description = fields.body;
10269
10353
  delete fields.body;
10270
10354
  }
10355
+ if (entity === "task" && fields.column === "ready") fields.column = "todo";
10271
10356
  return fields;
10272
10357
  }
10273
- function statusCol(entity, r) {
10274
- if (entity === "bug") return `${r.status ?? ""}/${r.severity ?? ""}`;
10275
- if (entity === "task") return String(r.column ?? "");
10276
- return String(r.status ?? "");
10358
+ function statusCol(entity, record10) {
10359
+ if (entity === "bug") return `${record10.status ?? ""}/${record10.severity ?? ""}`;
10360
+ if (entity === "task") {
10361
+ const state2 = record10.column === "todo" ? "ready" : String(record10.column ?? "");
10362
+ return record10.revision ? `${state2}; r${record10.revision}` : state2;
10363
+ }
10364
+ return String(record10.status ?? "");
10365
+ }
10366
+ function referenceMarkup(entity, record10) {
10367
+ const label = (record10.title?.trim() || `${entity} ${record10.id}`).replaceAll("]", ")");
10368
+ return `@[${label}](pm:${entity}/${record10.id})`;
10277
10369
  }
10278
- function printRecord(ctx, entity, r) {
10279
- ctx.out.log(`${r.id} [${statusCol(entity, r)}] ${r.appId} ${r.title ?? ""}`);
10370
+ function printRecord(ctx, entity, record10) {
10371
+ ctx.out.log(
10372
+ `${record10.id} [${statusCol(entity, record10)}] ${record10.appId} ${record10.title ?? ""}`
10373
+ );
10280
10374
  }
10281
10375
  function emit2(ctx, value2, human) {
10282
10376
  if (ctx.json) ctx.out.log(JSON.stringify(value2, null, 2));
10283
10377
  else human();
10284
10378
  }
10379
+
10380
+ // src/pm-actions.ts
10285
10381
  async function pmList(ctx, entity, parsed) {
10286
10382
  const q = new URLSearchParams();
10287
10383
  const filters = {
@@ -10295,7 +10391,8 @@ async function pmList(ctx, entity, parsed) {
10295
10391
  const app = stringOpt(parsed.options.app) ?? ctx.appId;
10296
10392
  if (app) q.set("app", app);
10297
10393
  for (const [flag, param] of Object.entries(filters)) {
10298
- const v = stringOpt(parsed.options[flag]);
10394
+ const raw = stringOpt(parsed.options[flag]);
10395
+ const v = entity === "task" && flag === "column" && raw === "ready" ? "todo" : raw;
10299
10396
  if (v) q.set(param, v);
10300
10397
  }
10301
10398
  for (const opt of ["q", "limit", "offset"]) {
@@ -10303,11 +10400,11 @@ async function pmList(ctx, entity, parsed) {
10303
10400
  if (v) q.set(opt, v);
10304
10401
  }
10305
10402
  const qs = q.toString();
10306
- const page = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
10307
- emit2(ctx, page, () => {
10308
- ctx.out.log(`${entity} \u2014 ${page.records.length} of ${page.total}`);
10403
+ const page2 = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
10404
+ emit2(ctx, page2, () => {
10405
+ ctx.out.log(`${entity} \u2014 ${page2.records.length} of ${page2.total}`);
10309
10406
  ctx.out.log("id state app title");
10310
- for (const r of page.records) printRecord(ctx, entity, r);
10407
+ for (const r of page2.records) printRecord(ctx, entity, r);
10311
10408
  });
10312
10409
  }
10313
10410
  async function pmAdd(ctx, entity, parsed) {
@@ -10328,6 +10425,17 @@ async function pmGet(ctx, entity, id) {
10328
10425
  const { record: record10 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
10329
10426
  emit2(ctx, record10, () => printRecord(ctx, entity, record10));
10330
10427
  }
10428
+ async function pmReference(ctx, entity, id) {
10429
+ const { record: record10 } = await pmRequest(
10430
+ ctx,
10431
+ "GET",
10432
+ `/${entity}/${encodeURIComponent(id)}`
10433
+ );
10434
+ const markup = referenceMarkup(entity, record10);
10435
+ emit2(ctx, { kind: `pm:${entity}`, id: record10.id, label: record10.title ?? "", markup }, () => {
10436
+ ctx.out.log(markup);
10437
+ });
10438
+ }
10331
10439
  async function pmSet(ctx, entity, id, parsed) {
10332
10440
  const patch2 = collectEntityFields(entity, parsed, true);
10333
10441
  if (Object.keys(patch2).length === 0)
@@ -10348,6 +10456,36 @@ async function pmDone(ctx, entity, id, parsed) {
10348
10456
  });
10349
10457
  emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
10350
10458
  }
10459
+ async function pmTaskLifecycle(ctx, id, action2, parsed) {
10460
+ const rawRevision = stringOpt(parsed.options["expected-revision"]);
10461
+ const expectedRevision = Number(rawRevision);
10462
+ if (!rawRevision || !Number.isSafeInteger(expectedRevision) || expectedRevision < 1) {
10463
+ throw new Error(`pm task ${action2} needs --expected-revision <n>`);
10464
+ }
10465
+ const mutationId = writeMutationId2(parsed);
10466
+ const res = action2 === "ready" ? await pmRequest(
10467
+ ctx,
10468
+ "PATCH",
10469
+ `/task/${encodeURIComponent(id)}`,
10470
+ {
10471
+ patch: {
10472
+ ...collectEntityFields("task", parsed, true),
10473
+ column: "todo",
10474
+ expectedRevision
10475
+ },
10476
+ mutationId
10477
+ }
10478
+ ) : await pmRequest(
10479
+ ctx,
10480
+ "POST",
10481
+ `/task/${encodeURIComponent(id)}/${action2}`,
10482
+ { expectedRevision, mutationId }
10483
+ );
10484
+ emit2(ctx, res, () => {
10485
+ const state2 = res.record ? statusCol("task", res.record) : action2;
10486
+ ctx.out.log(`task ${id} \u2192 ${state2}`);
10487
+ });
10488
+ }
10351
10489
  async function allRecords(ctx, entity, appId) {
10352
10490
  const records = [];
10353
10491
  for (; ; ) {
@@ -10356,11 +10494,48 @@ async function allRecords(ctx, entity, appId) {
10356
10494
  limit: "100",
10357
10495
  offset: String(records.length)
10358
10496
  });
10359
- const page = await pmRequest(ctx, "GET", `/${entity}?${q}`);
10360
- records.push(...page.records);
10361
- if (records.length >= page.total || page.records.length === 0) return records;
10497
+ const page2 = await pmRequest(ctx, "GET", `/${entity}?${q}`);
10498
+ records.push(...page2.records);
10499
+ if (records.length >= page2.total || page2.records.length === 0) return records;
10362
10500
  }
10363
10501
  }
10502
+ async function pmNext(ctx, parsed) {
10503
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
10504
+ if (!appId) throw new Error("pm next needs --app <appId>");
10505
+ const [goals, tasks] = await Promise.all([
10506
+ allRecords(ctx, "goal", appId),
10507
+ allRecords(ctx, "task", appId)
10508
+ ]);
10509
+ const result = {
10510
+ appId,
10511
+ openGoals: goals.filter((record10) => record10.status === "open"),
10512
+ doing: tasks.filter((record10) => record10.column === "doing"),
10513
+ ready: tasks.filter((record10) => record10.column === "todo")
10514
+ };
10515
+ emit2(ctx, result, () => {
10516
+ ctx.out.log(`${appId}: goal-aligned work intake (read only)`);
10517
+ for (const [label, records] of [
10518
+ ["doing", result.doing],
10519
+ ["ready", result.ready],
10520
+ ["open goals", result.openGoals]
10521
+ ]) {
10522
+ ctx.out.log(`${label}:`);
10523
+ if (!records.length) ctx.out.log("- (none)");
10524
+ else for (const record10 of records) printRecord(
10525
+ ctx,
10526
+ label === "open goals" ? "goal" : "task",
10527
+ record10
10528
+ );
10529
+ }
10530
+ if (!result.openGoals.length) {
10531
+ ctx.out.log("next: discuss alignment with the user before creating or claiming project work");
10532
+ } else if (!result.ready.length) {
10533
+ ctx.out.log("next: refine a linked Backlog task and mark it Ready");
10534
+ } else {
10535
+ ctx.out.log("next: review a Ready task, then claim it with its revision");
10536
+ }
10537
+ });
10538
+ }
10364
10539
  async function pmHandoff(ctx, parsed) {
10365
10540
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
10366
10541
  if (!appId) throw new Error("pm handoff needs --app <appId>");
@@ -10400,6 +10575,12 @@ async function pmHandoff(ctx, parsed) {
10400
10575
  }
10401
10576
  });
10402
10577
  }
10578
+ async function pmRemove(ctx, entity, id) {
10579
+ await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id)}`);
10580
+ ctx.out.log(`deleted ${entity} ${id}`);
10581
+ }
10582
+
10583
+ // src/pm-comments.ts
10403
10584
  async function pmComment(ctx, entity, id, parsed) {
10404
10585
  const body = stringOpt(parsed.options.body);
10405
10586
  if (!body) throw new Error('pm comment needs --body "..."');
@@ -10410,19 +10591,218 @@ async function pmComment(ctx, entity, id, parsed) {
10410
10591
  ctx.out.log(`commented on ${entity} ${id}`);
10411
10592
  }
10412
10593
  async function pmComments(ctx, entity, id) {
10413
- const { messages } = await pmRequest(
10414
- ctx,
10415
- "GET",
10416
- `/${entity}/${encodeURIComponent(id)}/comments`
10417
- );
10594
+ const { messages } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}/comments`);
10418
10595
  emit2(ctx, messages, () => {
10419
10596
  if (messages.length === 0) ctx.out.log("(no comments)");
10420
- else for (const m of messages) ctx.out.log(`[${m.authorId ?? "?"}] ${m.body ?? ""}`);
10597
+ else for (const message2 of messages) {
10598
+ ctx.out.log(
10599
+ `[${message2.authorId ?? "?"}] ${message2.markup ?? message2.body ?? ""}`
10600
+ );
10601
+ }
10421
10602
  });
10422
10603
  }
10423
- async function pmRemove(ctx, entity, id) {
10424
- await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id)}`);
10425
- ctx.out.log(`deleted ${entity} ${id}`);
10604
+
10605
+ // src/pm-watch-types.ts
10606
+ var PmWatchCheckpointError = class extends Error {
10607
+ constructor(cursor, streamId) {
10608
+ super("PM work cursor requires a new checkpoint");
10609
+ this.cursor = cursor;
10610
+ this.streamId = streamId;
10611
+ this.name = "PmWatchCheckpointError";
10612
+ }
10613
+ cursor;
10614
+ streamId;
10615
+ code = "checkpoint_required";
10616
+ };
10617
+ var PmWatchRequestError = class extends Error {
10618
+ constructor(message2, retryable, status) {
10619
+ super(message2);
10620
+ this.retryable = retryable;
10621
+ this.status = status;
10622
+ this.name = "PmWatchRequestError";
10623
+ }
10624
+ retryable;
10625
+ status;
10626
+ };
10627
+
10628
+ // src/pm-watch.ts
10629
+ var DEFAULT_INTERVAL_MS2 = 15e3;
10630
+ var MAX_CONSECUTIVE_FAILURES2 = 5;
10631
+ var MAX_BACKOFF_MS2 = 3e4;
10632
+ function positiveNumber(parsed, flag, fallback) {
10633
+ const raw = stringOpt(parsed.options[flag]);
10634
+ const value2 = raw == null ? NaN : Number(raw);
10635
+ return Number.isFinite(value2) && value2 > 0 ? value2 : fallback;
10636
+ }
10637
+ function jsonl2(ctx, parsed, value2) {
10638
+ if (parsed.options.jsonl === true) ctx.out.log(JSON.stringify({ v: 1, ...value2 }));
10639
+ }
10640
+ async function page(ctx, appId, cursor) {
10641
+ const params = new URLSearchParams({ app: appId });
10642
+ if (cursor) params.set("cursor", cursor);
10643
+ let response2;
10644
+ try {
10645
+ response2 = await ctx.doFetch(`${ctx.platformUrl}/registry/pm/work/watch?${params}`, {
10646
+ headers: { authorization: `Bearer ${ctx.token}` }
10647
+ });
10648
+ } catch (error) {
10649
+ throw new PmWatchRequestError(
10650
+ `pm watch request failed: ${error instanceof Error ? error.message : String(error)}`,
10651
+ true
10652
+ );
10653
+ }
10654
+ const data = await response2.json().catch(() => ({}));
10655
+ if (response2.status === 409 && data.code === "checkpoint_required") {
10656
+ throw new PmWatchCheckpointError(data.cursor, data.streamId);
10657
+ }
10658
+ if (!response2.ok) {
10659
+ throw new PmWatchRequestError(
10660
+ `pm watch failed: ${data.error ?? `registry returned ${response2.status}`}`,
10661
+ response2.status === 429 || response2.status >= 500,
10662
+ response2.status
10663
+ );
10664
+ }
10665
+ return data;
10666
+ }
10667
+ function recordState(record10) {
10668
+ if (record10.column) return record10.column === "todo" ? "ready" : record10.column;
10669
+ return String(record10.status ?? "");
10670
+ }
10671
+ function eventRecord(event) {
10672
+ return event.payload.payload;
10673
+ }
10674
+ function eventLabel(event) {
10675
+ const record10 = eventRecord(event);
10676
+ if (record10) return String(record10.title ?? event.payload.entityId);
10677
+ const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
10678
+ return body || event.payload.entityId;
10679
+ }
10680
+ function report3(ctx, parsed, result) {
10681
+ if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
10682
+ else if (parsed.options.jsonl !== true && result.found) {
10683
+ for (const event of result.events ?? []) {
10684
+ const record10 = eventRecord(event);
10685
+ const state2 = record10 ? recordState(record10) : "comment";
10686
+ ctx.out.log(
10687
+ `${event.id} ${event.type} ${state2}${record10?.revision ? `; r${record10.revision}` : ""} ${eventLabel(event)}`
10688
+ );
10689
+ }
10690
+ }
10691
+ return result;
10692
+ }
10693
+ async function pmWatch(ctx, parsed) {
10694
+ if (ctx.json && parsed.options.jsonl === true) {
10695
+ throw new Error("--json and --jsonl cannot be combined");
10696
+ }
10697
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
10698
+ if (!appId) throw new Error("pm watch needs --app <appId>");
10699
+ const sleep = ctx.sleep ?? ((ms) => new Promise((resolve13) => setTimeout(resolve13, ms)));
10700
+ const now = ctx.now ?? Date.now;
10701
+ const intervalMs = (positiveNumber(parsed, "interval", DEFAULT_INTERVAL_MS2 / 1e3) ?? DEFAULT_INTERVAL_MS2 / 1e3) * 1e3;
10702
+ const timeoutSeconds = positiveNumber(parsed, "timeout");
10703
+ const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
10704
+ const entity = stringOpt(parsed.options.entity);
10705
+ const action2 = stringOpt(parsed.options.action);
10706
+ const wantedState = stringOpt(parsed.options.state)?.toLowerCase();
10707
+ const by = stringOpt(parsed.options.by);
10708
+ const self = stringOpt(parsed.options.self);
10709
+ let cursor = stringOpt(parsed.options.cursor);
10710
+ let firstSuccess = true;
10711
+ let consecutiveFailures = 0;
10712
+ for (; ; ) {
10713
+ let current;
10714
+ try {
10715
+ current = await page(ctx, appId, cursor);
10716
+ consecutiveFailures = 0;
10717
+ } catch (error) {
10718
+ if (error instanceof PmWatchCheckpointError) {
10719
+ jsonl2(ctx, parsed, {
10720
+ type: "status",
10721
+ state: "checkpoint_required",
10722
+ retryable: false,
10723
+ ...error.cursor ? { cursor: error.cursor } : {},
10724
+ ...error.streamId ? { streamId: error.streamId } : {}
10725
+ });
10726
+ throw error;
10727
+ }
10728
+ if (!(error instanceof PmWatchRequestError) || !error.retryable) throw error;
10729
+ consecutiveFailures++;
10730
+ jsonl2(ctx, parsed, {
10731
+ type: "status",
10732
+ state: "degraded",
10733
+ retryable: true,
10734
+ attempt: consecutiveFailures,
10735
+ ...cursor ? { cursor } : {},
10736
+ ...error.status ? { status: error.status } : {}
10737
+ });
10738
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
10739
+ if (deadline !== void 0 && now() >= deadline) {
10740
+ return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
10741
+ }
10742
+ const backoff = Math.min(
10743
+ MAX_BACKOFF_MS2,
10744
+ Math.min(intervalMs, 1e3) * 2 ** (consecutiveFailures - 1)
10745
+ );
10746
+ await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
10747
+ continue;
10748
+ }
10749
+ cursor = current.cursor;
10750
+ const baseline = firstSuccess && current.events.length === 0;
10751
+ if (baseline) {
10752
+ jsonl2(ctx, parsed, {
10753
+ type: "checkpoint",
10754
+ streamId: current.streamId,
10755
+ cursor,
10756
+ serverTime: current.serverTime
10757
+ });
10758
+ }
10759
+ firstSuccess = false;
10760
+ const matching = current.events.filter((event) => {
10761
+ const record10 = eventRecord(event);
10762
+ const state2 = record10 ? recordState(record10).toLowerCase() : "";
10763
+ 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);
10764
+ });
10765
+ for (const event of matching) {
10766
+ jsonl2(ctx, parsed, {
10767
+ type: "event",
10768
+ streamId: current.streamId,
10769
+ eventId: event.id,
10770
+ cursor: event.cursor,
10771
+ event
10772
+ });
10773
+ }
10774
+ if (matching.length > 0) {
10775
+ jsonl2(ctx, parsed, {
10776
+ type: "checkpoint",
10777
+ streamId: current.streamId,
10778
+ cursor,
10779
+ serverTime: current.serverTime
10780
+ });
10781
+ return report3(ctx, parsed, { found: true, cursor, events: matching });
10782
+ }
10783
+ if (current.events.length > 0) {
10784
+ jsonl2(ctx, parsed, {
10785
+ type: "checkpoint",
10786
+ streamId: current.streamId,
10787
+ cursor,
10788
+ serverTime: current.serverTime
10789
+ });
10790
+ } else if (!baseline) {
10791
+ jsonl2(ctx, parsed, {
10792
+ type: "heartbeat",
10793
+ streamId: current.streamId,
10794
+ cursor,
10795
+ serverTime: current.serverTime
10796
+ });
10797
+ }
10798
+ if (current.hasMore) continue;
10799
+ if (deadline !== void 0 && now() >= deadline) {
10800
+ return report3(ctx, parsed, { found: false, cursor });
10801
+ }
10802
+ await sleep(
10803
+ deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
10804
+ );
10805
+ }
10426
10806
  }
10427
10807
 
10428
10808
  // src/pm-command.ts
@@ -10443,7 +10823,11 @@ var ACTION_OPTIONS = {
10443
10823
  done: ["mutation-id"],
10444
10824
  comment: ["body", "mutation-id"],
10445
10825
  comments: [],
10446
- rm: []
10826
+ rm: [],
10827
+ ready: ["goal", "alignment-decision", "execution", "description", "desc", "body", "acceptance", "expected-revision", "mutation-id"],
10828
+ claim: ["expected-revision", "mutation-id"],
10829
+ release: ["expected-revision", "mutation-id"],
10830
+ ref: []
10447
10831
  };
10448
10832
  var ENTITY_OPTIONS = {
10449
10833
  goal: {
@@ -10454,8 +10838,8 @@ var ENTITY_OPTIONS = {
10454
10838
  },
10455
10839
  task: {
10456
10840
  list: ["column", "goal", "assignee"],
10457
- add: ["column", "goal", "assignee", "due", "description", "desc", "body"],
10458
- set: ["title", "column", "rank", "goal", "assignee", "due", "description", "desc", "body"],
10841
+ add: ["column", "goal", "alignment-decision", "execution", "assignee", "due", "description", "desc", "body", "acceptance"],
10842
+ set: ["title", "column", "rank", "goal", "alignment-decision", "execution", "assignee", "due", "description", "desc", "body", "acceptance", "expected-revision"],
10459
10843
  done: []
10460
10844
  },
10461
10845
  decision: {
@@ -10506,11 +10890,33 @@ async function buildContext2(parsed, deps) {
10506
10890
  json: parsed.options.json === true,
10507
10891
  // Preserve PM's existing cross-project default when a config is present;
10508
10892
  // only an explicit remote-agent environment or profile selects an app.
10509
- appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
10893
+ appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0,
10894
+ ...deps.sleep ? { sleep: deps.sleep } : {},
10895
+ ...deps.now ? { now: deps.now } : {}
10510
10896
  };
10511
10897
  }
10512
10898
  async function pmCommand(parsed, deps = {}) {
10513
10899
  const word = parsed.positionals[1] ?? "";
10900
+ if (word === "next") {
10901
+ assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
10902
+ return pmNext(await buildContext2(parsed, deps), parsed);
10903
+ }
10904
+ if (word === "watch") {
10905
+ assertArgs(parsed, [
10906
+ ...COMMON_OPTIONS,
10907
+ "app",
10908
+ "cursor",
10909
+ "interval",
10910
+ "timeout",
10911
+ "jsonl",
10912
+ "entity",
10913
+ "action",
10914
+ "state",
10915
+ "by",
10916
+ "self"
10917
+ ], 2);
10918
+ return pmWatch(await buildContext2(parsed, deps), parsed).then(() => void 0);
10919
+ }
10514
10920
  if (word === "handoff") {
10515
10921
  assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
10516
10922
  return pmHandoff(await buildContext2(parsed, deps), parsed);
@@ -10521,6 +10927,9 @@ async function pmCommand(parsed, deps = {}) {
10521
10927
  const action2 = canonicalAction(requestedAction);
10522
10928
  if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|comment|comments|rm.`);
10523
10929
  assertArgs(parsed, allowedOptions(entity, action2), 4);
10930
+ if ((action2 === "ready" || action2 === "claim" || action2 === "release") && entity !== "task") {
10931
+ throw new Error(`pm ${action2} is only valid for tasks`);
10932
+ }
10524
10933
  const ctx = await buildContext2(parsed, deps);
10525
10934
  const id = parsed.positionals[3];
10526
10935
  switch (action2) {
@@ -10540,6 +10949,12 @@ async function pmCommand(parsed, deps = {}) {
10540
10949
  return pmComments(ctx, entity, requireId2(id, action2));
10541
10950
  case "rm":
10542
10951
  return pmRemove(ctx, entity, requireId2(id, action2));
10952
+ case "ref":
10953
+ return pmReference(ctx, entity, requireId2(id, action2));
10954
+ case "ready":
10955
+ case "claim":
10956
+ case "release":
10957
+ return pmTaskLifecycle(ctx, requireId2(id, action2), action2, parsed);
10543
10958
  }
10544
10959
  }
10545
10960
 
@@ -11154,12 +11569,12 @@ async function call(ctx, method, path, body) {
11154
11569
  throw new Error(message2);
11155
11570
  }
11156
11571
  async function bySlug(ctx, slug) {
11157
- const page = await call(
11572
+ const page2 = await call(
11158
11573
  ctx,
11159
11574
  "GET",
11160
11575
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
11161
11576
  );
11162
- const found = page.records[0];
11577
+ const found = page2.records[0];
11163
11578
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
11164
11579
  return found;
11165
11580
  }
@@ -11173,11 +11588,11 @@ async function runbookList(ctx, all, query) {
11173
11588
  const params = new URLSearchParams();
11174
11589
  if (!all) params.set("app", ctx.appId);
11175
11590
  if (query) params.set("q", query);
11176
- const page = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
11177
- if (ctx.json) return ctx.out.log(JSON.stringify(page, null, 2));
11178
- if (!page.records.length) return ctx.out.log("(no runbooks)");
11591
+ const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
11592
+ if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
11593
+ if (!page2.records.length) return ctx.out.log("(no runbooks)");
11179
11594
  ctx.out.log(["SLUG", "STATUS", "V", "SCOPE", "UPDATED", "TITLE"].join(" "));
11180
- for (const r of page.records)
11595
+ for (const r of page2.records)
11181
11596
  ctx.out.log(
11182
11597
  [r.slug, r.status, `v${r.version}`, r.appId === PLATFORM_SCOPE ? "platform" : r.appId, stamp(r.updatedAt), r.title].join(" ")
11183
11598
  );
@@ -11232,12 +11647,12 @@ async function runbookVisibility(ctx, slug, visibility) {
11232
11647
  }
11233
11648
  async function runbookHistory(ctx, slug) {
11234
11649
  const runbook = await bySlug(ctx, slug);
11235
- const page = await call(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
11236
- if (ctx.json) return ctx.out.log(JSON.stringify(page, null, 2));
11650
+ const page2 = await call(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
11651
+ if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
11237
11652
  ctx.out.log(`${slug} is at v${runbook.version}`);
11238
- if (!page.records.length) return ctx.out.log("(no earlier versions)");
11653
+ if (!page2.records.length) return ctx.out.log("(no earlier versions)");
11239
11654
  ctx.out.log(["V", "WHEN", "BY", "NOTE"].join(" "));
11240
- for (const r of page.records)
11655
+ for (const r of page2.records)
11241
11656
  ctx.out.log([`v${r.version}`, stamp(r.createdAt), r.ownerEmail ?? "", r.note ?? ""].join(" "));
11242
11657
  }
11243
11658
  async function runbookRevert(ctx, slug, version) {
@@ -11312,12 +11727,12 @@ ${counts.created} created, ${counts.updated} updated, ${counts.unchanged} unchan
11312
11727
  );
11313
11728
  }
11314
11729
  async function upsert(ctx, r, visibility) {
11315
- const page = await call(
11730
+ const page2 = await call(
11316
11731
  ctx,
11317
11732
  "GET",
11318
11733
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(r.slug)}&limit=1`
11319
11734
  );
11320
- const found = page.records[0];
11735
+ const found = page2.records[0];
11321
11736
  if (!found) {
11322
11737
  await call(ctx, "POST", "/runbook", {
11323
11738
  appId: ctx.appId,
@@ -11573,7 +11988,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
11573
11988
  return out;
11574
11989
  }
11575
11990
  var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
11576
- function report3(ctx, impacts) {
11991
+ function report4(ctx, impacts) {
11577
11992
  const covered = impacts.filter((i) => i.runbooks.length);
11578
11993
  ctx.out.log(
11579
11994
  `${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.`
@@ -11611,7 +12026,7 @@ async function runbookImpact(ctx, options, deps = {}) {
11611
12026
  }
11612
12027
  const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
11613
12028
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
11614
- report3(ctx, impacts);
12029
+ report4(ctx, impacts);
11615
12030
  }
11616
12031
 
11617
12032
  // src/runbook-lint.ts
@@ -11649,17 +12064,17 @@ function lintRunbook(runbook, installed) {
11649
12064
  async function runbookLint(ctx, all) {
11650
12065
  const params = new URLSearchParams();
11651
12066
  if (!all) params.set("app", ctx.appId);
11652
- const page = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
12067
+ const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
11653
12068
  const installed = { "@odla-ai/cli": cliVersion() };
11654
- const findings = page.records.flatMap((runbook) => lintRunbook(runbook, installed));
11655
- if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page.records.length, findings }, null, 2));
11656
- if (!page.records.length) return ctx.out.log("(no runbooks in scope)");
12069
+ const findings = page2.records.flatMap((runbook) => lintRunbook(runbook, installed));
12070
+ if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page2.records.length, findings }, null, 2));
12071
+ if (!page2.records.length) return ctx.out.log("(no runbooks in scope)");
11657
12072
  if (!findings.length) {
11658
12073
  return ctx.out.log(
11659
- `${page.records.length} runbook${page.records.length === 1 ? "" : "s"} checked; every command they name is real for @odla-ai/cli ${cliVersion()}.`
12074
+ `${page2.records.length} runbook${page2.records.length === 1 ? "" : "s"} checked; every command they name is real for @odla-ai/cli ${cliVersion()}.`
11660
12075
  );
11661
12076
  }
11662
- ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${page.records.length} runbooks:`);
12077
+ ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${page2.records.length} runbooks:`);
11663
12078
  for (const finding of findings) {
11664
12079
  const scope = finding.appId === PLATFORM_SCOPE ? "" : ` --app ${finding.appId}`;
11665
12080
  ctx.out.log("");
@@ -11722,12 +12137,12 @@ async function runbookAsk(ctx, question, all) {
11722
12137
  ctx.out.log(JSDOC_POINTER);
11723
12138
  }
11724
12139
  async function runbookComment(ctx, slug, body) {
11725
- const page = await call(
12140
+ const page2 = await call(
11726
12141
  ctx,
11727
12142
  "GET",
11728
12143
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
11729
12144
  );
11730
- const found = page.records[0];
12145
+ const found = page2.records[0];
11731
12146
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
11732
12147
  await call(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
11733
12148
  ctx.out.log(`commented on ${slug} (v${found.version})`);
@@ -11779,12 +12194,12 @@ var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
11779
12194
 
11780
12195
  // src/runbook-edit-flow.ts
11781
12196
  async function editRunbook(ctx, slug, deps = {}) {
11782
- const page = await call(
12197
+ const page2 = await call(
11783
12198
  ctx,
11784
12199
  "GET",
11785
12200
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
11786
12201
  );
11787
- const found = page.records[0];
12202
+ const found = page2.records[0];
11788
12203
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
11789
12204
  ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
11790
12205
  const body = await editText(found.body, slug, deps);
@@ -12205,31 +12620,31 @@ function printHostedJob(out, job, platform, appId) {
12205
12620
  url.searchParams.set("job", job.jobId);
12206
12621
  out.log(` Studio: ${url.toString()}`);
12207
12622
  }
12208
- function printHostedReport(out, report4) {
12209
- out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
12210
- 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}`);
12211
- out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
12212
- out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
12213
- out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
12214
- for (const finding of report4.findings) {
12623
+ function printHostedReport(out, report5) {
12624
+ out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
12625
+ 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}`);
12626
+ out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
12627
+ out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
12628
+ out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
12629
+ for (const finding of report5.findings) {
12215
12630
  const location = finding.locations[0];
12216
12631
  out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
12217
12632
  }
12218
- for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
12633
+ for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
12219
12634
  }
12220
- function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
12635
+ function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
12221
12636
  const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
12222
12637
  const candidateValue = parsed.options["fail-on-candidates"];
12223
12638
  const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
12224
12639
  const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
12225
- const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
12226
- const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
12227
- const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
12640
+ const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
12641
+ const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
12642
+ const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
12228
12643
  if (confirmed.length || leads.length || incomplete) {
12229
- throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
12644
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
12230
12645
  }
12231
12646
  if (emitSuccess) {
12232
- out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
12647
+ out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
12233
12648
  }
12234
12649
  }
12235
12650
  function printHostedSecurityPlanRoute(out, label, route2) {
@@ -12333,13 +12748,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
12333
12748
  }
12334
12749
  throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
12335
12750
  }
12336
- const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
12751
+ const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
12337
12752
  if (parsed.options.json === true) {
12338
- context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
12753
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
12339
12754
  } else {
12340
- printHostedReport(context.stdout, report4);
12755
+ printHostedReport(context.stdout, report5);
12341
12756
  }
12342
- enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
12757
+ enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
12343
12758
  }
12344
12759
  async function runLocalSecurityCommand(parsed, dependencies) {
12345
12760
  if (parsed.options.source === true) {
@@ -12406,13 +12821,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
12406
12821
  });
12407
12822
  enforceLocalGate(result.report, parsed);
12408
12823
  }
12409
- function enforceLocalGate(report4, parsed) {
12824
+ function enforceLocalGate(report5, parsed) {
12410
12825
  const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
12411
12826
  const candidateValue = parsed.options["fail-on-candidates"];
12412
12827
  const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
12413
- const confirmed = findingsAtOrAbove(report4, failOn);
12414
- const leads = failOnCandidates ? findingsAtOrAbove(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
12415
- const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
12828
+ const confirmed = findingsAtOrAbove(report5, failOn);
12829
+ const leads = failOnCandidates ? findingsAtOrAbove(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
12830
+ const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
12416
12831
  if (confirmed.length || leads.length || incomplete) {
12417
12832
  throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
12418
12833
  }
@@ -12451,9 +12866,9 @@ async function securityCommand(parsed, dependencies) {
12451
12866
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
12452
12867
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
12453
12868
  const context = await hostedSecurityContext(parsed, dependencies);
12454
- const report4 = await getHostedSecurityReport({ ...context, jobId });
12455
- if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
12456
- else printHostedReport(context.stdout, report4);
12869
+ const report5 = await getHostedSecurityReport({ ...context, jobId });
12870
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
12871
+ else printHostedReport(context.stdout, report5);
12457
12872
  return;
12458
12873
  }
12459
12874
  if (sub !== "run") {
@@ -12746,4 +13161,4 @@ export {
12746
13161
  exitCodeFor,
12747
13162
  runCli
12748
13163
  };
12749
- //# sourceMappingURL=chunk-JWEBGIBR.js.map
13164
+ //# sourceMappingURL=chunk-DQOJ4S6H.js.map