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