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