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