@odla-ai/cli 0.27.3 → 0.27.4
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 +44 -7
- package/dist/bin.cjs +536 -158
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-JWEBGIBR.js → chunk-GENTB3VO.js} +539 -161
- package/dist/chunk-GENTB3VO.js.map +1 -0
- package/dist/index.cjs +536 -158
- package/dist/index.cjs.map +1 -1
- 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-migrate/references/project-state.md +8 -2
- package/dist/chunk-JWEBGIBR.js.map +0 -1
package/dist/bin.cjs
CHANGED
|
@@ -32,6 +32,7 @@ var import_node_process7 = __toESM(require("process"), 1);
|
|
|
32
32
|
|
|
33
33
|
// src/token.ts
|
|
34
34
|
var import_db = require("@odla-ai/db");
|
|
35
|
+
var import_node_crypto = require("crypto");
|
|
35
36
|
var import_node_process4 = __toESM(require("process"), 1);
|
|
36
37
|
|
|
37
38
|
// src/handshake-approval.ts
|
|
@@ -386,6 +387,7 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
386
387
|
endpoint: ctx.cfg.platformUrl,
|
|
387
388
|
email: ctx.email,
|
|
388
389
|
label: `${ctx.cfg.app.id} provisioner`,
|
|
390
|
+
agentHandle: projectAgentHandle(ctx.cfg.app.id),
|
|
389
391
|
projectIds: [ctx.cfg.app.id],
|
|
390
392
|
fetch: ctx.doFetch,
|
|
391
393
|
waitMs,
|
|
@@ -421,6 +423,12 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
421
423
|
stopReminder?.();
|
|
422
424
|
}
|
|
423
425
|
}
|
|
426
|
+
function projectAgentHandle(appId) {
|
|
427
|
+
const candidate = /^[a-z]/.test(appId) ? appId : `app-${appId}`;
|
|
428
|
+
if (candidate.length <= 32) return candidate;
|
|
429
|
+
const digest = (0, import_node_crypto.createHash)("sha256").update(appId).digest("hex").slice(0, 8);
|
|
430
|
+
return `${candidate.slice(0, 23).replace(/-+$/, "")}-${digest}`;
|
|
431
|
+
}
|
|
424
432
|
function stillPending(pending, email) {
|
|
425
433
|
return new import_db.OdlaError(
|
|
426
434
|
"handshake_pending",
|
|
@@ -2633,8 +2641,8 @@ async function calendarCalendars(options) {
|
|
|
2633
2641
|
async function calendarConnect(options) {
|
|
2634
2642
|
const { cfg, ctx, out } = await lifecycleContext(options);
|
|
2635
2643
|
productionConsent(ctx.env, options.yes, "connect calendar");
|
|
2636
|
-
const
|
|
2637
|
-
const applied =
|
|
2644
|
+
const page2 = calendarBookingPageUrl(cfg, ctx.env);
|
|
2645
|
+
const applied = page2 === void 0 ? await readCalendarStatus(ctx) : await applyCalendarSettings(ctx, page2);
|
|
2638
2646
|
const connectOptions = connectionOptions(options, out);
|
|
2639
2647
|
return await continueConnectedCalendar(ctx, applied, connectOptions) ?? connectWithContext(ctx, connectOptions);
|
|
2640
2648
|
}
|
|
@@ -2883,12 +2891,12 @@ var import_apps3 = require("@odla-ai/apps");
|
|
|
2883
2891
|
var import_node_fs10 = require("fs");
|
|
2884
2892
|
|
|
2885
2893
|
// src/config-reconcile-digest.ts
|
|
2886
|
-
var
|
|
2894
|
+
var import_node_crypto2 = require("crypto");
|
|
2887
2895
|
function canonicalJson(value2) {
|
|
2888
2896
|
return JSON.stringify(canonicalValue(value2));
|
|
2889
2897
|
}
|
|
2890
2898
|
function configDigest(value2) {
|
|
2891
|
-
return `sha256:${(0,
|
|
2899
|
+
return `sha256:${(0, import_node_crypto2.createHash)("sha256").update(canonicalJson(value2)).digest("hex")}`;
|
|
2892
2900
|
}
|
|
2893
2901
|
function canonicalValue(value2) {
|
|
2894
2902
|
if (value2 === null || typeof value2 === "string" || typeof value2 === "boolean") return value2;
|
|
@@ -4461,9 +4469,10 @@ For work that creates an odla app or adds odla services, read and follow
|
|
|
4461
4469
|
\`.agents/skills/odla-migrate/SKILL.md\`. For production telemetry triage, use
|
|
4462
4470
|
\`.agents/skills/odla-o11y-debug/SKILL.md\`.
|
|
4463
4471
|
|
|
4464
|
-
Track the work in odla's PM as you go
|
|
4465
|
-
|
|
4466
|
-
|
|
4472
|
+
Track the work in odla's PM as you go. Before project-mutating work, run
|
|
4473
|
+
\`npx @odla-ai/cli pm next --app <appId>\`, confirm alignment to an open goal,
|
|
4474
|
+
and atomically claim a refined Ready task. Record decisions when you make them
|
|
4475
|
+
and file bugs when you notice them. The conventions and the full command set are
|
|
4467
4476
|
in \`.agents/skills/odla/references/pm.md\`.
|
|
4468
4477
|
|
|
4469
4478
|
The setup runbooks and their references are installed in this repository, pinned
|
|
@@ -5687,7 +5696,7 @@ function dependenciesOf(values, influence = "data") {
|
|
|
5687
5696
|
return [...unique3.values()];
|
|
5688
5697
|
}
|
|
5689
5698
|
|
|
5690
|
-
// ../camel/dist/chunk-
|
|
5699
|
+
// ../camel/dist/chunk-4DQ6BIHP.js
|
|
5691
5700
|
var camelValueBrand = /* @__PURE__ */ Symbol("@odla-ai/camel/value");
|
|
5692
5701
|
var authenticCamelValues = /* @__PURE__ */ new WeakSet();
|
|
5693
5702
|
function isCamelValue(value2) {
|
|
@@ -5699,13 +5708,13 @@ function isSafe(value2) {
|
|
|
5699
5708
|
function isUnsafe(value2) {
|
|
5700
5709
|
return isCamelValue(value2) && value2.label.promptSafety === "unsafe";
|
|
5701
5710
|
}
|
|
5702
|
-
function createSafeInternal(value2, safeBasis, metadata2) {
|
|
5711
|
+
function createSafeInternal(value2, safeBasis, metadata2, extra) {
|
|
5703
5712
|
return createValue(value2, {
|
|
5704
5713
|
schemaVersion: 1,
|
|
5705
5714
|
promptSafety: "safe",
|
|
5706
5715
|
safeBasis,
|
|
5707
5716
|
...copyMetadata(metadata2)
|
|
5708
|
-
});
|
|
5717
|
+
}, extra);
|
|
5709
5718
|
}
|
|
5710
5719
|
function createUnsafeInternal(value2, metadata2) {
|
|
5711
5720
|
return createValue(value2, {
|
|
@@ -5714,8 +5723,8 @@ function createUnsafeInternal(value2, metadata2) {
|
|
|
5714
5723
|
...copyMetadata(metadata2)
|
|
5715
5724
|
});
|
|
5716
5725
|
}
|
|
5717
|
-
function createValue(value2, label) {
|
|
5718
|
-
const result = { value: value2, label: Object.freeze(label) };
|
|
5726
|
+
function createValue(value2, label, extra) {
|
|
5727
|
+
const result = { value: value2, label: Object.freeze(label), ...extra };
|
|
5719
5728
|
Object.defineProperty(result, camelValueBrand, { value: label.promptSafety, enumerable: false });
|
|
5720
5729
|
authenticCamelValues.add(result);
|
|
5721
5730
|
return Object.freeze(result);
|
|
@@ -5976,7 +5985,7 @@ var import_path8 = require("path");
|
|
|
5976
5985
|
var import_promises10 = require("fs/promises");
|
|
5977
5986
|
var import_path9 = require("path");
|
|
5978
5987
|
|
|
5979
|
-
// ../camel/dist/chunk-
|
|
5988
|
+
// ../camel/dist/chunk-4EIRFS3A.js
|
|
5980
5989
|
function conversionPolicyDigest(policy) {
|
|
5981
5990
|
return sha256Hex(canonicalJson2(policy));
|
|
5982
5991
|
}
|
|
@@ -6064,10 +6073,18 @@ async function convert(source, policy, value2, counts) {
|
|
|
6064
6073
|
const count = counts.get(countKey) ?? 0;
|
|
6065
6074
|
if (count >= policy.maximumOutputsPerArtifact) throw new CamelError("limit_exceeded", "Conversion output count exceeds its per-source bound.");
|
|
6066
6075
|
counts.set(countKey, count + 1);
|
|
6076
|
+
const conversionRecordId = await sha256Hex(canonicalJson2({ conversionId: policy.conversionId, digest: policy.digest, source: sourceKey, ordinal: count }));
|
|
6077
|
+
const datumId = await sha256Hex(canonicalJson2({ conversionRecordId, value: value2 }));
|
|
6067
6078
|
return createSafeInternal(value2, "atomic_conversion", {
|
|
6068
6079
|
readers: source.label.readers,
|
|
6069
6080
|
provenance: [...source.label.provenance, { kind: "converter", id: policy.conversionId, digest: policy.digest }],
|
|
6070
6081
|
dependencies: dependenciesOf([source])
|
|
6082
|
+
}, {
|
|
6083
|
+
datumId,
|
|
6084
|
+
kind: policy.output.kind,
|
|
6085
|
+
conversionId: policy.conversionId,
|
|
6086
|
+
conversionDigest: policy.digest,
|
|
6087
|
+
conversionRecordId
|
|
6071
6088
|
});
|
|
6072
6089
|
}
|
|
6073
6090
|
function validatePolicyShape(policy) {
|
|
@@ -6132,7 +6149,7 @@ function missingPolicy() {
|
|
|
6132
6149
|
throw new CamelError("conversion_rejected", "Conversion policy is not registered.");
|
|
6133
6150
|
}
|
|
6134
6151
|
|
|
6135
|
-
// ../camel/dist/chunk-
|
|
6152
|
+
// ../camel/dist/chunk-VEAUXH4F.js
|
|
6136
6153
|
function createCamelIngress(constants2 = []) {
|
|
6137
6154
|
const byId = /* @__PURE__ */ new Map();
|
|
6138
6155
|
for (const item of constants2) {
|
|
@@ -8122,7 +8139,7 @@ async function defaultReadOrigin(cwd) {
|
|
|
8122
8139
|
|
|
8123
8140
|
// src/code-local-source.ts
|
|
8124
8141
|
var import_node_child_process5 = require("child_process");
|
|
8125
|
-
var
|
|
8142
|
+
var import_node_crypto3 = require("crypto");
|
|
8126
8143
|
var SOURCE_LIMITS2 = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
|
|
8127
8144
|
async function prepareCodeLocalSource(cwd, repository, readHead = readGitHead) {
|
|
8128
8145
|
const headCommitSha = await readHead(cwd);
|
|
@@ -8173,12 +8190,12 @@ async function readGitHead(cwd) {
|
|
|
8173
8190
|
return value2;
|
|
8174
8191
|
}
|
|
8175
8192
|
function digestText(value2) {
|
|
8176
|
-
return `sha256:${(0,
|
|
8193
|
+
return `sha256:${(0, import_node_crypto3.createHash)("sha256").update(value2).digest("hex")}`;
|
|
8177
8194
|
}
|
|
8178
8195
|
|
|
8179
8196
|
// src/code-images.ts
|
|
8180
8197
|
var import_node_child_process6 = require("child_process");
|
|
8181
|
-
var
|
|
8198
|
+
var import_node_crypto4 = require("crypto");
|
|
8182
8199
|
var import_promises11 = require("fs/promises");
|
|
8183
8200
|
var import_node_os3 = require("os");
|
|
8184
8201
|
var import_node_path14 = require("path");
|
|
@@ -8264,7 +8281,7 @@ async function embeddedPiImageName() {
|
|
|
8264
8281
|
const bundle = await (0, import_promises11.readFile)(embeddedPiAssetPath()).catch(() => {
|
|
8265
8282
|
throw new Error("CLI-embedded Pi runtime is missing; reinstall this exact @odla-ai/cli version");
|
|
8266
8283
|
});
|
|
8267
|
-
return `odla-ai/pi-agent:embedded-sha256-${(0,
|
|
8284
|
+
return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto4.createHash)("sha256").update(bundle).digest("hex")}`;
|
|
8268
8285
|
}
|
|
8269
8286
|
async function buildEmbeddedPiImage(engine, image, run) {
|
|
8270
8287
|
const context = await (0, import_promises11.mkdtemp)((0, import_node_path14.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
|
|
@@ -8692,16 +8709,22 @@ Usage:
|
|
|
8692
8709
|
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
8693
8710
|
odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
|
|
8694
8711
|
odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8695
|
-
odla-ai pm task list [--app <id>] [--column <
|
|
8712
|
+
odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8696
8713
|
odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8697
8714
|
odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8698
8715
|
odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
|
|
8699
|
-
odla-ai pm task add --app <id> --title <t> [--column <
|
|
8716
|
+
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]
|
|
8717
|
+
odla-ai pm next --app <id> [--json]
|
|
8718
|
+
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]
|
|
8719
|
+
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]
|
|
8720
|
+
odla-ai pm task claim <id> --expected-revision <n> [--mutation-id <id>] [--json]
|
|
8721
|
+
odla-ai pm task release <id> --expected-revision <n> [--mutation-id <id>] [--json]
|
|
8700
8722
|
odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
|
|
8701
8723
|
odla-ai pm bug add --app <id> --title <t> (--description <text>|--body <text>) [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--mutation-id <id>] [--json]
|
|
8702
8724
|
odla-ai pm <goal|task|decision|bug> get <id> [--json]
|
|
8725
|
+
odla-ai pm <goal|task|decision|bug> ref <id> [--json]
|
|
8703
8726
|
odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
|
|
8704
|
-
odla-ai pm task set <id> [--title <t>|--column <
|
|
8727
|
+
odla-ai pm task set <id> [--title <t>|--column <backlog|ready|doing|review|done>|--rank <n>|--goal <id>|--no-goal|--alignment-decision <id>|--no-alignment-decision|--execution <human|agent|either>|--assignee <id>|--no-assignee|--description <text>|--body <text>|--acceptance <text>|--no-acceptance|--due <epoch-ms>|--no-due|--expected-revision <n>] [--mutation-id <id>] [--json]
|
|
8705
8728
|
odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
|
|
8706
8729
|
odla-ai pm bug set <id> [--title <t>|--status <s>|--severity <s>|--goal <id>|--no-goal|--assignee <id>|--no-assignee|--decision <id>|--no-decision|--description <text>|--body <text>] [--mutation-id <id>] [--json]
|
|
8707
8730
|
odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
|
|
@@ -9020,11 +9043,11 @@ async function discussList(ctx, parsed) {
|
|
|
9020
9043
|
if (value2) query.set(param, value2);
|
|
9021
9044
|
}
|
|
9022
9045
|
const qs = query.toString();
|
|
9023
|
-
const
|
|
9024
|
-
emit(ctx,
|
|
9025
|
-
ctx.out.log(`topics \u2014 ${
|
|
9046
|
+
const page2 = await request(ctx, "GET", `/topics${qs ? `?${qs}` : ""}`);
|
|
9047
|
+
emit(ctx, page2, () => {
|
|
9048
|
+
ctx.out.log(`topics \u2014 ${page2.topics.length} of ${page2.total}`);
|
|
9026
9049
|
ctx.out.log("id state app replies subject");
|
|
9027
|
-
for (const topic of
|
|
9050
|
+
for (const topic of page2.topics) {
|
|
9028
9051
|
ctx.out.log(
|
|
9029
9052
|
`${topic.id} ${state(topic)} ${topic.appId ?? ""} ${topic.replyCount} ${topic.subject}`
|
|
9030
9053
|
);
|
|
@@ -9039,11 +9062,11 @@ async function discussRead(ctx, id, parsed) {
|
|
|
9039
9062
|
limit: requestedLimit ?? "200",
|
|
9040
9063
|
offset: requestedOffset ?? "0"
|
|
9041
9064
|
});
|
|
9042
|
-
const
|
|
9065
|
+
const page2 = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
|
|
9043
9066
|
emit(
|
|
9044
9067
|
ctx,
|
|
9045
|
-
|
|
9046
|
-
() => renderDiscussRead(ctx,
|
|
9068
|
+
page2,
|
|
9069
|
+
() => renderDiscussRead(ctx, page2.topic, page2.posts, page2)
|
|
9047
9070
|
);
|
|
9048
9071
|
return;
|
|
9049
9072
|
}
|
|
@@ -9056,20 +9079,20 @@ async function discussRead(ctx, id, parsed) {
|
|
|
9056
9079
|
let topic = null;
|
|
9057
9080
|
let offset = 0;
|
|
9058
9081
|
for (; ; ) {
|
|
9059
|
-
const
|
|
9082
|
+
const page2 = await request(
|
|
9060
9083
|
ctx,
|
|
9061
9084
|
"GET",
|
|
9062
9085
|
`/topics/${encodeURIComponent(id)}?limit=200&offset=${offset}`
|
|
9063
9086
|
);
|
|
9064
|
-
topic =
|
|
9065
|
-
for (const post of
|
|
9066
|
-
mergeDiscussPrincipals(projection,
|
|
9087
|
+
topic = page2.topic;
|
|
9088
|
+
for (const post of page2.posts) posts.set(post.id, post);
|
|
9089
|
+
mergeDiscussPrincipals(projection, page2);
|
|
9067
9090
|
if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
|
|
9068
|
-
if (!
|
|
9069
|
-
if (
|
|
9091
|
+
if (!page2.page?.hasMore) break;
|
|
9092
|
+
if (page2.page.nextOffset === null || page2.page.nextOffset <= offset) {
|
|
9070
9093
|
throw new Error("discuss read failed: registry returned a non-advancing post page");
|
|
9071
9094
|
}
|
|
9072
|
-
offset =
|
|
9095
|
+
offset = page2.page.nextOffset;
|
|
9073
9096
|
}
|
|
9074
9097
|
const ordered = [...posts.values()].sort(
|
|
9075
9098
|
(a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
|
|
@@ -9237,9 +9260,9 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9237
9260
|
let firstSuccess = true;
|
|
9238
9261
|
let consecutiveFailures = 0;
|
|
9239
9262
|
for (; ; ) {
|
|
9240
|
-
let
|
|
9263
|
+
let page2;
|
|
9241
9264
|
try {
|
|
9242
|
-
|
|
9265
|
+
page2 = await getWatchPage(ctx, requestPath(topicId, app, cursor));
|
|
9243
9266
|
consecutiveFailures = 0;
|
|
9244
9267
|
} catch (error) {
|
|
9245
9268
|
if (!(error instanceof WatchRequestError) || !error.retryable) {
|
|
@@ -9275,25 +9298,25 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9275
9298
|
await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
|
|
9276
9299
|
continue;
|
|
9277
9300
|
}
|
|
9278
|
-
cursor =
|
|
9279
|
-
const baseline = firstSuccess &&
|
|
9301
|
+
cursor = page2.cursor;
|
|
9302
|
+
const baseline = firstSuccess && page2.events.length === 0;
|
|
9280
9303
|
if (baseline) {
|
|
9281
9304
|
jsonl(ctx, parsed, {
|
|
9282
9305
|
type: "checkpoint",
|
|
9283
|
-
streamId:
|
|
9306
|
+
streamId: page2.streamId,
|
|
9284
9307
|
cursor,
|
|
9285
|
-
serverTime:
|
|
9308
|
+
serverTime: page2.serverTime
|
|
9286
9309
|
});
|
|
9287
9310
|
}
|
|
9288
9311
|
firstSuccess = false;
|
|
9289
|
-
const matching =
|
|
9312
|
+
const matching = page2.events.filter((event) => {
|
|
9290
9313
|
if (topicId && (event.type !== "message" || event.action !== "created")) return false;
|
|
9291
9314
|
return (!by || event.actor.id === by) && (!self || event.actor.id !== self);
|
|
9292
9315
|
});
|
|
9293
9316
|
for (const event of matching) {
|
|
9294
9317
|
jsonl(ctx, parsed, {
|
|
9295
9318
|
type: "event",
|
|
9296
|
-
streamId:
|
|
9319
|
+
streamId: page2.streamId,
|
|
9297
9320
|
eventId: event.id,
|
|
9298
9321
|
cursor: event.cursor,
|
|
9299
9322
|
event
|
|
@@ -9302,9 +9325,9 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9302
9325
|
if (matching.length > 0) {
|
|
9303
9326
|
jsonl(ctx, parsed, {
|
|
9304
9327
|
type: "checkpoint",
|
|
9305
|
-
streamId:
|
|
9328
|
+
streamId: page2.streamId,
|
|
9306
9329
|
cursor,
|
|
9307
|
-
serverTime:
|
|
9330
|
+
serverTime: page2.serverTime
|
|
9308
9331
|
});
|
|
9309
9332
|
const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
|
|
9310
9333
|
const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
|
|
@@ -9312,28 +9335,28 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9312
9335
|
found: true,
|
|
9313
9336
|
cursor,
|
|
9314
9337
|
events: matching,
|
|
9315
|
-
...
|
|
9316
|
-
...
|
|
9338
|
+
...page2.authors ? { authors: page2.authors } : {},
|
|
9339
|
+
...page2.principals ? { principals: page2.principals } : {},
|
|
9317
9340
|
...posts && posts.length > 0 ? { posts } : {},
|
|
9318
9341
|
...topics && topics.length > 0 ? { topics } : {}
|
|
9319
9342
|
});
|
|
9320
9343
|
}
|
|
9321
|
-
if (
|
|
9344
|
+
if (page2.events.length > 0) {
|
|
9322
9345
|
jsonl(ctx, parsed, {
|
|
9323
9346
|
type: "checkpoint",
|
|
9324
|
-
streamId:
|
|
9347
|
+
streamId: page2.streamId,
|
|
9325
9348
|
cursor,
|
|
9326
|
-
serverTime:
|
|
9349
|
+
serverTime: page2.serverTime
|
|
9327
9350
|
});
|
|
9328
9351
|
} else if (!baseline) {
|
|
9329
9352
|
jsonl(ctx, parsed, {
|
|
9330
9353
|
type: "heartbeat",
|
|
9331
|
-
streamId:
|
|
9354
|
+
streamId: page2.streamId,
|
|
9332
9355
|
cursor,
|
|
9333
|
-
serverTime:
|
|
9356
|
+
serverTime: page2.serverTime
|
|
9334
9357
|
});
|
|
9335
9358
|
}
|
|
9336
|
-
if (
|
|
9359
|
+
if (page2.hasMore) continue;
|
|
9337
9360
|
if (deadline !== void 0 && now() >= deadline) {
|
|
9338
9361
|
return report2(ctx, parsed, { found: false, cursor });
|
|
9339
9362
|
}
|
|
@@ -9452,7 +9475,13 @@ async function discussCommand(parsed, deps = {}) {
|
|
|
9452
9475
|
}
|
|
9453
9476
|
}
|
|
9454
9477
|
|
|
9455
|
-
// src/pm-
|
|
9478
|
+
// src/pm-action-core.ts
|
|
9479
|
+
var DONE = {
|
|
9480
|
+
goal: { status: "met", currentPct: 100 },
|
|
9481
|
+
task: { column: "done" },
|
|
9482
|
+
decision: { status: "accepted" },
|
|
9483
|
+
bug: { status: "fixed" }
|
|
9484
|
+
};
|
|
9456
9485
|
var writeMutationId2 = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
|
|
9457
9486
|
var FIELD_MAP = {
|
|
9458
9487
|
title: { key: "title" },
|
|
@@ -9468,22 +9497,27 @@ var FIELD_MAP = {
|
|
|
9468
9497
|
body: { key: "body" },
|
|
9469
9498
|
target: { key: "targetPct", num: true },
|
|
9470
9499
|
description: { key: "description" },
|
|
9471
|
-
desc: { key: "description" }
|
|
9472
|
-
}
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
decision: { status: "accepted" },
|
|
9477
|
-
bug: { status: "fixed" }
|
|
9500
|
+
desc: { key: "description" },
|
|
9501
|
+
acceptance: { key: "acceptanceCriteria" },
|
|
9502
|
+
"alignment-decision": { key: "alignmentDecisionId" },
|
|
9503
|
+
execution: { key: "executionMode" },
|
|
9504
|
+
"expected-revision": { key: "expectedRevision", num: true }
|
|
9478
9505
|
};
|
|
9479
9506
|
async function pmRequest(ctx, method, path, body) {
|
|
9480
|
-
const
|
|
9507
|
+
const response2 = await ctx.doFetch(`${ctx.platformUrl}/registry/pm${path}`, {
|
|
9481
9508
|
method,
|
|
9482
|
-
headers: {
|
|
9509
|
+
headers: {
|
|
9510
|
+
authorization: `Bearer ${ctx.token}`,
|
|
9511
|
+
"content-type": "application/json"
|
|
9512
|
+
},
|
|
9483
9513
|
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
9484
9514
|
});
|
|
9485
|
-
const data = await
|
|
9486
|
-
if (!
|
|
9515
|
+
const data = await response2.json().catch(() => ({}));
|
|
9516
|
+
if (!response2.ok) {
|
|
9517
|
+
throw new Error(
|
|
9518
|
+
`pm ${method} ${path} failed: ${data.error ?? `registry returned ${response2.status}`}`
|
|
9519
|
+
);
|
|
9520
|
+
}
|
|
9487
9521
|
return data;
|
|
9488
9522
|
}
|
|
9489
9523
|
function collectFields(parsed, allowClear) {
|
|
@@ -9506,20 +9540,32 @@ function collectEntityFields(entity, parsed, allowClear) {
|
|
|
9506
9540
|
if (fields.description === void 0) fields.description = fields.body;
|
|
9507
9541
|
delete fields.body;
|
|
9508
9542
|
}
|
|
9543
|
+
if (entity === "task" && fields.column === "ready") fields.column = "todo";
|
|
9509
9544
|
return fields;
|
|
9510
9545
|
}
|
|
9511
|
-
function statusCol(entity,
|
|
9512
|
-
if (entity === "bug") return `${
|
|
9513
|
-
if (entity === "task")
|
|
9514
|
-
|
|
9546
|
+
function statusCol(entity, record10) {
|
|
9547
|
+
if (entity === "bug") return `${record10.status ?? ""}/${record10.severity ?? ""}`;
|
|
9548
|
+
if (entity === "task") {
|
|
9549
|
+
const state2 = record10.column === "todo" ? "ready" : String(record10.column ?? "");
|
|
9550
|
+
return record10.revision ? `${state2}; r${record10.revision}` : state2;
|
|
9551
|
+
}
|
|
9552
|
+
return String(record10.status ?? "");
|
|
9515
9553
|
}
|
|
9516
|
-
function
|
|
9517
|
-
|
|
9554
|
+
function referenceMarkup(entity, record10) {
|
|
9555
|
+
const label = (record10.title?.trim() || `${entity} ${record10.id}`).replaceAll("]", ")");
|
|
9556
|
+
return `@[${label}](pm:${entity}/${record10.id})`;
|
|
9557
|
+
}
|
|
9558
|
+
function printRecord(ctx, entity, record10) {
|
|
9559
|
+
ctx.out.log(
|
|
9560
|
+
`${record10.id} [${statusCol(entity, record10)}] ${record10.appId} ${record10.title ?? ""}`
|
|
9561
|
+
);
|
|
9518
9562
|
}
|
|
9519
9563
|
function emit2(ctx, value2, human) {
|
|
9520
9564
|
if (ctx.json) ctx.out.log(JSON.stringify(value2, null, 2));
|
|
9521
9565
|
else human();
|
|
9522
9566
|
}
|
|
9567
|
+
|
|
9568
|
+
// src/pm-actions.ts
|
|
9523
9569
|
async function pmList(ctx, entity, parsed) {
|
|
9524
9570
|
const q = new URLSearchParams();
|
|
9525
9571
|
const filters = {
|
|
@@ -9533,7 +9579,8 @@ async function pmList(ctx, entity, parsed) {
|
|
|
9533
9579
|
const app = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
9534
9580
|
if (app) q.set("app", app);
|
|
9535
9581
|
for (const [flag, param] of Object.entries(filters)) {
|
|
9536
|
-
const
|
|
9582
|
+
const raw = stringOpt(parsed.options[flag]);
|
|
9583
|
+
const v = entity === "task" && flag === "column" && raw === "ready" ? "todo" : raw;
|
|
9537
9584
|
if (v) q.set(param, v);
|
|
9538
9585
|
}
|
|
9539
9586
|
for (const opt of ["q", "limit", "offset"]) {
|
|
@@ -9541,11 +9588,11 @@ async function pmList(ctx, entity, parsed) {
|
|
|
9541
9588
|
if (v) q.set(opt, v);
|
|
9542
9589
|
}
|
|
9543
9590
|
const qs = q.toString();
|
|
9544
|
-
const
|
|
9545
|
-
emit2(ctx,
|
|
9546
|
-
ctx.out.log(`${entity} \u2014 ${
|
|
9591
|
+
const page2 = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
|
|
9592
|
+
emit2(ctx, page2, () => {
|
|
9593
|
+
ctx.out.log(`${entity} \u2014 ${page2.records.length} of ${page2.total}`);
|
|
9547
9594
|
ctx.out.log("id state app title");
|
|
9548
|
-
for (const r of
|
|
9595
|
+
for (const r of page2.records) printRecord(ctx, entity, r);
|
|
9549
9596
|
});
|
|
9550
9597
|
}
|
|
9551
9598
|
async function pmAdd(ctx, entity, parsed) {
|
|
@@ -9566,6 +9613,17 @@ async function pmGet(ctx, entity, id) {
|
|
|
9566
9613
|
const { record: record10 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
|
|
9567
9614
|
emit2(ctx, record10, () => printRecord(ctx, entity, record10));
|
|
9568
9615
|
}
|
|
9616
|
+
async function pmReference(ctx, entity, id) {
|
|
9617
|
+
const { record: record10 } = await pmRequest(
|
|
9618
|
+
ctx,
|
|
9619
|
+
"GET",
|
|
9620
|
+
`/${entity}/${encodeURIComponent(id)}`
|
|
9621
|
+
);
|
|
9622
|
+
const markup = referenceMarkup(entity, record10);
|
|
9623
|
+
emit2(ctx, { kind: `pm:${entity}`, id: record10.id, label: record10.title ?? "", markup }, () => {
|
|
9624
|
+
ctx.out.log(markup);
|
|
9625
|
+
});
|
|
9626
|
+
}
|
|
9569
9627
|
async function pmSet(ctx, entity, id, parsed) {
|
|
9570
9628
|
const patch2 = collectEntityFields(entity, parsed, true);
|
|
9571
9629
|
if (Object.keys(patch2).length === 0)
|
|
@@ -9586,6 +9644,36 @@ async function pmDone(ctx, entity, id, parsed) {
|
|
|
9586
9644
|
});
|
|
9587
9645
|
emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
|
|
9588
9646
|
}
|
|
9647
|
+
async function pmTaskLifecycle(ctx, id, action2, parsed) {
|
|
9648
|
+
const rawRevision = stringOpt(parsed.options["expected-revision"]);
|
|
9649
|
+
const expectedRevision = Number(rawRevision);
|
|
9650
|
+
if (!rawRevision || !Number.isSafeInteger(expectedRevision) || expectedRevision < 1) {
|
|
9651
|
+
throw new Error(`pm task ${action2} needs --expected-revision <n>`);
|
|
9652
|
+
}
|
|
9653
|
+
const mutationId = writeMutationId2(parsed);
|
|
9654
|
+
const res = action2 === "ready" ? await pmRequest(
|
|
9655
|
+
ctx,
|
|
9656
|
+
"PATCH",
|
|
9657
|
+
`/task/${encodeURIComponent(id)}`,
|
|
9658
|
+
{
|
|
9659
|
+
patch: {
|
|
9660
|
+
...collectEntityFields("task", parsed, true),
|
|
9661
|
+
column: "todo",
|
|
9662
|
+
expectedRevision
|
|
9663
|
+
},
|
|
9664
|
+
mutationId
|
|
9665
|
+
}
|
|
9666
|
+
) : await pmRequest(
|
|
9667
|
+
ctx,
|
|
9668
|
+
"POST",
|
|
9669
|
+
`/task/${encodeURIComponent(id)}/${action2}`,
|
|
9670
|
+
{ expectedRevision, mutationId }
|
|
9671
|
+
);
|
|
9672
|
+
emit2(ctx, res, () => {
|
|
9673
|
+
const state2 = res.record ? statusCol("task", res.record) : action2;
|
|
9674
|
+
ctx.out.log(`task ${id} \u2192 ${state2}`);
|
|
9675
|
+
});
|
|
9676
|
+
}
|
|
9589
9677
|
async function allRecords(ctx, entity, appId) {
|
|
9590
9678
|
const records = [];
|
|
9591
9679
|
for (; ; ) {
|
|
@@ -9594,11 +9682,48 @@ async function allRecords(ctx, entity, appId) {
|
|
|
9594
9682
|
limit: "100",
|
|
9595
9683
|
offset: String(records.length)
|
|
9596
9684
|
});
|
|
9597
|
-
const
|
|
9598
|
-
records.push(...
|
|
9599
|
-
if (records.length >=
|
|
9685
|
+
const page2 = await pmRequest(ctx, "GET", `/${entity}?${q}`);
|
|
9686
|
+
records.push(...page2.records);
|
|
9687
|
+
if (records.length >= page2.total || page2.records.length === 0) return records;
|
|
9600
9688
|
}
|
|
9601
9689
|
}
|
|
9690
|
+
async function pmNext(ctx, parsed) {
|
|
9691
|
+
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
9692
|
+
if (!appId) throw new Error("pm next needs --app <appId>");
|
|
9693
|
+
const [goals, tasks] = await Promise.all([
|
|
9694
|
+
allRecords(ctx, "goal", appId),
|
|
9695
|
+
allRecords(ctx, "task", appId)
|
|
9696
|
+
]);
|
|
9697
|
+
const result = {
|
|
9698
|
+
appId,
|
|
9699
|
+
openGoals: goals.filter((record10) => record10.status === "open"),
|
|
9700
|
+
doing: tasks.filter((record10) => record10.column === "doing"),
|
|
9701
|
+
ready: tasks.filter((record10) => record10.column === "todo")
|
|
9702
|
+
};
|
|
9703
|
+
emit2(ctx, result, () => {
|
|
9704
|
+
ctx.out.log(`${appId}: goal-aligned work intake (read only)`);
|
|
9705
|
+
for (const [label, records] of [
|
|
9706
|
+
["doing", result.doing],
|
|
9707
|
+
["ready", result.ready],
|
|
9708
|
+
["open goals", result.openGoals]
|
|
9709
|
+
]) {
|
|
9710
|
+
ctx.out.log(`${label}:`);
|
|
9711
|
+
if (!records.length) ctx.out.log("- (none)");
|
|
9712
|
+
else for (const record10 of records) printRecord(
|
|
9713
|
+
ctx,
|
|
9714
|
+
label === "open goals" ? "goal" : "task",
|
|
9715
|
+
record10
|
|
9716
|
+
);
|
|
9717
|
+
}
|
|
9718
|
+
if (!result.openGoals.length) {
|
|
9719
|
+
ctx.out.log("next: discuss alignment with the user before creating or claiming project work");
|
|
9720
|
+
} else if (!result.ready.length) {
|
|
9721
|
+
ctx.out.log("next: refine a linked Backlog task and mark it Ready");
|
|
9722
|
+
} else {
|
|
9723
|
+
ctx.out.log("next: review a Ready task, then claim it with its revision");
|
|
9724
|
+
}
|
|
9725
|
+
});
|
|
9726
|
+
}
|
|
9602
9727
|
async function pmHandoff(ctx, parsed) {
|
|
9603
9728
|
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
9604
9729
|
if (!appId) throw new Error("pm handoff needs --app <appId>");
|
|
@@ -9638,6 +9763,12 @@ async function pmHandoff(ctx, parsed) {
|
|
|
9638
9763
|
}
|
|
9639
9764
|
});
|
|
9640
9765
|
}
|
|
9766
|
+
async function pmRemove(ctx, entity, id) {
|
|
9767
|
+
await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id)}`);
|
|
9768
|
+
ctx.out.log(`deleted ${entity} ${id}`);
|
|
9769
|
+
}
|
|
9770
|
+
|
|
9771
|
+
// src/pm-comments.ts
|
|
9641
9772
|
async function pmComment(ctx, entity, id, parsed) {
|
|
9642
9773
|
const body = stringOpt(parsed.options.body);
|
|
9643
9774
|
if (!body) throw new Error('pm comment needs --body "..."');
|
|
@@ -9648,19 +9779,218 @@ async function pmComment(ctx, entity, id, parsed) {
|
|
|
9648
9779
|
ctx.out.log(`commented on ${entity} ${id}`);
|
|
9649
9780
|
}
|
|
9650
9781
|
async function pmComments(ctx, entity, id) {
|
|
9651
|
-
const { messages } = await pmRequest(
|
|
9652
|
-
ctx,
|
|
9653
|
-
"GET",
|
|
9654
|
-
`/${entity}/${encodeURIComponent(id)}/comments`
|
|
9655
|
-
);
|
|
9782
|
+
const { messages } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}/comments`);
|
|
9656
9783
|
emit2(ctx, messages, () => {
|
|
9657
9784
|
if (messages.length === 0) ctx.out.log("(no comments)");
|
|
9658
|
-
else for (const
|
|
9785
|
+
else for (const message2 of messages) {
|
|
9786
|
+
ctx.out.log(
|
|
9787
|
+
`[${message2.authorId ?? "?"}] ${message2.markup ?? message2.body ?? ""}`
|
|
9788
|
+
);
|
|
9789
|
+
}
|
|
9659
9790
|
});
|
|
9660
9791
|
}
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
9792
|
+
|
|
9793
|
+
// src/pm-watch-types.ts
|
|
9794
|
+
var PmWatchCheckpointError = class extends Error {
|
|
9795
|
+
constructor(cursor, streamId) {
|
|
9796
|
+
super("PM work cursor requires a new checkpoint");
|
|
9797
|
+
this.cursor = cursor;
|
|
9798
|
+
this.streamId = streamId;
|
|
9799
|
+
this.name = "PmWatchCheckpointError";
|
|
9800
|
+
}
|
|
9801
|
+
cursor;
|
|
9802
|
+
streamId;
|
|
9803
|
+
code = "checkpoint_required";
|
|
9804
|
+
};
|
|
9805
|
+
var PmWatchRequestError = class extends Error {
|
|
9806
|
+
constructor(message2, retryable, status) {
|
|
9807
|
+
super(message2);
|
|
9808
|
+
this.retryable = retryable;
|
|
9809
|
+
this.status = status;
|
|
9810
|
+
this.name = "PmWatchRequestError";
|
|
9811
|
+
}
|
|
9812
|
+
retryable;
|
|
9813
|
+
status;
|
|
9814
|
+
};
|
|
9815
|
+
|
|
9816
|
+
// src/pm-watch.ts
|
|
9817
|
+
var DEFAULT_INTERVAL_MS2 = 15e3;
|
|
9818
|
+
var MAX_CONSECUTIVE_FAILURES2 = 5;
|
|
9819
|
+
var MAX_BACKOFF_MS2 = 3e4;
|
|
9820
|
+
function positiveNumber(parsed, flag, fallback) {
|
|
9821
|
+
const raw = stringOpt(parsed.options[flag]);
|
|
9822
|
+
const value2 = raw == null ? NaN : Number(raw);
|
|
9823
|
+
return Number.isFinite(value2) && value2 > 0 ? value2 : fallback;
|
|
9824
|
+
}
|
|
9825
|
+
function jsonl2(ctx, parsed, value2) {
|
|
9826
|
+
if (parsed.options.jsonl === true) ctx.out.log(JSON.stringify({ v: 1, ...value2 }));
|
|
9827
|
+
}
|
|
9828
|
+
async function page(ctx, appId, cursor) {
|
|
9829
|
+
const params = new URLSearchParams({ app: appId });
|
|
9830
|
+
if (cursor) params.set("cursor", cursor);
|
|
9831
|
+
let response2;
|
|
9832
|
+
try {
|
|
9833
|
+
response2 = await ctx.doFetch(`${ctx.platformUrl}/registry/pm/work/watch?${params}`, {
|
|
9834
|
+
headers: { authorization: `Bearer ${ctx.token}` }
|
|
9835
|
+
});
|
|
9836
|
+
} catch (error) {
|
|
9837
|
+
throw new PmWatchRequestError(
|
|
9838
|
+
`pm watch request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
9839
|
+
true
|
|
9840
|
+
);
|
|
9841
|
+
}
|
|
9842
|
+
const data = await response2.json().catch(() => ({}));
|
|
9843
|
+
if (response2.status === 409 && data.code === "checkpoint_required") {
|
|
9844
|
+
throw new PmWatchCheckpointError(data.cursor, data.streamId);
|
|
9845
|
+
}
|
|
9846
|
+
if (!response2.ok) {
|
|
9847
|
+
throw new PmWatchRequestError(
|
|
9848
|
+
`pm watch failed: ${data.error ?? `registry returned ${response2.status}`}`,
|
|
9849
|
+
response2.status === 429 || response2.status >= 500,
|
|
9850
|
+
response2.status
|
|
9851
|
+
);
|
|
9852
|
+
}
|
|
9853
|
+
return data;
|
|
9854
|
+
}
|
|
9855
|
+
function recordState(record10) {
|
|
9856
|
+
if (record10.column) return record10.column === "todo" ? "ready" : record10.column;
|
|
9857
|
+
return String(record10.status ?? "");
|
|
9858
|
+
}
|
|
9859
|
+
function eventRecord(event) {
|
|
9860
|
+
return event.payload.payload;
|
|
9861
|
+
}
|
|
9862
|
+
function eventLabel(event) {
|
|
9863
|
+
const record10 = eventRecord(event);
|
|
9864
|
+
if (record10) return String(record10.title ?? event.payload.entityId);
|
|
9865
|
+
const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
|
|
9866
|
+
return body || event.payload.entityId;
|
|
9867
|
+
}
|
|
9868
|
+
function report3(ctx, parsed, result) {
|
|
9869
|
+
if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
|
|
9870
|
+
else if (parsed.options.jsonl !== true && result.found) {
|
|
9871
|
+
for (const event of result.events ?? []) {
|
|
9872
|
+
const record10 = eventRecord(event);
|
|
9873
|
+
const state2 = record10 ? recordState(record10) : "comment";
|
|
9874
|
+
ctx.out.log(
|
|
9875
|
+
`${event.id} ${event.type} ${state2}${record10?.revision ? `; r${record10.revision}` : ""} ${eventLabel(event)}`
|
|
9876
|
+
);
|
|
9877
|
+
}
|
|
9878
|
+
}
|
|
9879
|
+
return result;
|
|
9880
|
+
}
|
|
9881
|
+
async function pmWatch(ctx, parsed) {
|
|
9882
|
+
if (ctx.json && parsed.options.jsonl === true) {
|
|
9883
|
+
throw new Error("--json and --jsonl cannot be combined");
|
|
9884
|
+
}
|
|
9885
|
+
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
9886
|
+
if (!appId) throw new Error("pm watch needs --app <appId>");
|
|
9887
|
+
const sleep = ctx.sleep ?? ((ms) => new Promise((resolve13) => setTimeout(resolve13, ms)));
|
|
9888
|
+
const now = ctx.now ?? Date.now;
|
|
9889
|
+
const intervalMs = (positiveNumber(parsed, "interval", DEFAULT_INTERVAL_MS2 / 1e3) ?? DEFAULT_INTERVAL_MS2 / 1e3) * 1e3;
|
|
9890
|
+
const timeoutSeconds = positiveNumber(parsed, "timeout");
|
|
9891
|
+
const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
|
|
9892
|
+
const entity = stringOpt(parsed.options.entity);
|
|
9893
|
+
const action2 = stringOpt(parsed.options.action);
|
|
9894
|
+
const wantedState = stringOpt(parsed.options.state)?.toLowerCase();
|
|
9895
|
+
const by = stringOpt(parsed.options.by);
|
|
9896
|
+
const self = stringOpt(parsed.options.self);
|
|
9897
|
+
let cursor = stringOpt(parsed.options.cursor);
|
|
9898
|
+
let firstSuccess = true;
|
|
9899
|
+
let consecutiveFailures = 0;
|
|
9900
|
+
for (; ; ) {
|
|
9901
|
+
let current;
|
|
9902
|
+
try {
|
|
9903
|
+
current = await page(ctx, appId, cursor);
|
|
9904
|
+
consecutiveFailures = 0;
|
|
9905
|
+
} catch (error) {
|
|
9906
|
+
if (error instanceof PmWatchCheckpointError) {
|
|
9907
|
+
jsonl2(ctx, parsed, {
|
|
9908
|
+
type: "status",
|
|
9909
|
+
state: "checkpoint_required",
|
|
9910
|
+
retryable: false,
|
|
9911
|
+
...error.cursor ? { cursor: error.cursor } : {},
|
|
9912
|
+
...error.streamId ? { streamId: error.streamId } : {}
|
|
9913
|
+
});
|
|
9914
|
+
throw error;
|
|
9915
|
+
}
|
|
9916
|
+
if (!(error instanceof PmWatchRequestError) || !error.retryable) throw error;
|
|
9917
|
+
consecutiveFailures++;
|
|
9918
|
+
jsonl2(ctx, parsed, {
|
|
9919
|
+
type: "status",
|
|
9920
|
+
state: "degraded",
|
|
9921
|
+
retryable: true,
|
|
9922
|
+
attempt: consecutiveFailures,
|
|
9923
|
+
...cursor ? { cursor } : {},
|
|
9924
|
+
...error.status ? { status: error.status } : {}
|
|
9925
|
+
});
|
|
9926
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
|
|
9927
|
+
if (deadline !== void 0 && now() >= deadline) {
|
|
9928
|
+
return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
9929
|
+
}
|
|
9930
|
+
const backoff = Math.min(
|
|
9931
|
+
MAX_BACKOFF_MS2,
|
|
9932
|
+
Math.min(intervalMs, 1e3) * 2 ** (consecutiveFailures - 1)
|
|
9933
|
+
);
|
|
9934
|
+
await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
|
|
9935
|
+
continue;
|
|
9936
|
+
}
|
|
9937
|
+
cursor = current.cursor;
|
|
9938
|
+
const baseline = firstSuccess && current.events.length === 0;
|
|
9939
|
+
if (baseline) {
|
|
9940
|
+
jsonl2(ctx, parsed, {
|
|
9941
|
+
type: "checkpoint",
|
|
9942
|
+
streamId: current.streamId,
|
|
9943
|
+
cursor,
|
|
9944
|
+
serverTime: current.serverTime
|
|
9945
|
+
});
|
|
9946
|
+
}
|
|
9947
|
+
firstSuccess = false;
|
|
9948
|
+
const matching = current.events.filter((event) => {
|
|
9949
|
+
const record10 = eventRecord(event);
|
|
9950
|
+
const state2 = record10 ? recordState(record10).toLowerCase() : "";
|
|
9951
|
+
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);
|
|
9952
|
+
});
|
|
9953
|
+
for (const event of matching) {
|
|
9954
|
+
jsonl2(ctx, parsed, {
|
|
9955
|
+
type: "event",
|
|
9956
|
+
streamId: current.streamId,
|
|
9957
|
+
eventId: event.id,
|
|
9958
|
+
cursor: event.cursor,
|
|
9959
|
+
event
|
|
9960
|
+
});
|
|
9961
|
+
}
|
|
9962
|
+
if (matching.length > 0) {
|
|
9963
|
+
jsonl2(ctx, parsed, {
|
|
9964
|
+
type: "checkpoint",
|
|
9965
|
+
streamId: current.streamId,
|
|
9966
|
+
cursor,
|
|
9967
|
+
serverTime: current.serverTime
|
|
9968
|
+
});
|
|
9969
|
+
return report3(ctx, parsed, { found: true, cursor, events: matching });
|
|
9970
|
+
}
|
|
9971
|
+
if (current.events.length > 0) {
|
|
9972
|
+
jsonl2(ctx, parsed, {
|
|
9973
|
+
type: "checkpoint",
|
|
9974
|
+
streamId: current.streamId,
|
|
9975
|
+
cursor,
|
|
9976
|
+
serverTime: current.serverTime
|
|
9977
|
+
});
|
|
9978
|
+
} else if (!baseline) {
|
|
9979
|
+
jsonl2(ctx, parsed, {
|
|
9980
|
+
type: "heartbeat",
|
|
9981
|
+
streamId: current.streamId,
|
|
9982
|
+
cursor,
|
|
9983
|
+
serverTime: current.serverTime
|
|
9984
|
+
});
|
|
9985
|
+
}
|
|
9986
|
+
if (current.hasMore) continue;
|
|
9987
|
+
if (deadline !== void 0 && now() >= deadline) {
|
|
9988
|
+
return report3(ctx, parsed, { found: false, cursor });
|
|
9989
|
+
}
|
|
9990
|
+
await sleep(
|
|
9991
|
+
deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
|
|
9992
|
+
);
|
|
9993
|
+
}
|
|
9664
9994
|
}
|
|
9665
9995
|
|
|
9666
9996
|
// src/pm-command.ts
|
|
@@ -9681,7 +10011,11 @@ var ACTION_OPTIONS = {
|
|
|
9681
10011
|
done: ["mutation-id"],
|
|
9682
10012
|
comment: ["body", "mutation-id"],
|
|
9683
10013
|
comments: [],
|
|
9684
|
-
rm: []
|
|
10014
|
+
rm: [],
|
|
10015
|
+
ready: ["goal", "alignment-decision", "execution", "description", "desc", "body", "acceptance", "expected-revision", "mutation-id"],
|
|
10016
|
+
claim: ["expected-revision", "mutation-id"],
|
|
10017
|
+
release: ["expected-revision", "mutation-id"],
|
|
10018
|
+
ref: []
|
|
9685
10019
|
};
|
|
9686
10020
|
var ENTITY_OPTIONS = {
|
|
9687
10021
|
goal: {
|
|
@@ -9692,8 +10026,8 @@ var ENTITY_OPTIONS = {
|
|
|
9692
10026
|
},
|
|
9693
10027
|
task: {
|
|
9694
10028
|
list: ["column", "goal", "assignee"],
|
|
9695
|
-
add: ["column", "goal", "assignee", "due", "description", "desc", "body"],
|
|
9696
|
-
set: ["title", "column", "rank", "goal", "assignee", "due", "description", "desc", "body"],
|
|
10029
|
+
add: ["column", "goal", "alignment-decision", "execution", "assignee", "due", "description", "desc", "body", "acceptance"],
|
|
10030
|
+
set: ["title", "column", "rank", "goal", "alignment-decision", "execution", "assignee", "due", "description", "desc", "body", "acceptance", "expected-revision"],
|
|
9697
10031
|
done: []
|
|
9698
10032
|
},
|
|
9699
10033
|
decision: {
|
|
@@ -9744,11 +10078,33 @@ async function buildContext2(parsed, deps) {
|
|
|
9744
10078
|
json: parsed.options.json === true,
|
|
9745
10079
|
// Preserve PM's existing cross-project default when a config is present;
|
|
9746
10080
|
// only an explicit remote-agent environment or profile selects an app.
|
|
9747
|
-
appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
|
|
10081
|
+
appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0,
|
|
10082
|
+
...deps.sleep ? { sleep: deps.sleep } : {},
|
|
10083
|
+
...deps.now ? { now: deps.now } : {}
|
|
9748
10084
|
};
|
|
9749
10085
|
}
|
|
9750
10086
|
async function pmCommand(parsed, deps = {}) {
|
|
9751
10087
|
const word = parsed.positionals[1] ?? "";
|
|
10088
|
+
if (word === "next") {
|
|
10089
|
+
assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
|
|
10090
|
+
return pmNext(await buildContext2(parsed, deps), parsed);
|
|
10091
|
+
}
|
|
10092
|
+
if (word === "watch") {
|
|
10093
|
+
assertArgs(parsed, [
|
|
10094
|
+
...COMMON_OPTIONS,
|
|
10095
|
+
"app",
|
|
10096
|
+
"cursor",
|
|
10097
|
+
"interval",
|
|
10098
|
+
"timeout",
|
|
10099
|
+
"jsonl",
|
|
10100
|
+
"entity",
|
|
10101
|
+
"action",
|
|
10102
|
+
"state",
|
|
10103
|
+
"by",
|
|
10104
|
+
"self"
|
|
10105
|
+
], 2);
|
|
10106
|
+
return pmWatch(await buildContext2(parsed, deps), parsed).then(() => void 0);
|
|
10107
|
+
}
|
|
9752
10108
|
if (word === "handoff") {
|
|
9753
10109
|
assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
|
|
9754
10110
|
return pmHandoff(await buildContext2(parsed, deps), parsed);
|
|
@@ -9759,6 +10115,9 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
9759
10115
|
const action2 = canonicalAction(requestedAction);
|
|
9760
10116
|
if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|comment|comments|rm.`);
|
|
9761
10117
|
assertArgs(parsed, allowedOptions(entity, action2), 4);
|
|
10118
|
+
if ((action2 === "ready" || action2 === "claim" || action2 === "release") && entity !== "task") {
|
|
10119
|
+
throw new Error(`pm ${action2} is only valid for tasks`);
|
|
10120
|
+
}
|
|
9762
10121
|
const ctx = await buildContext2(parsed, deps);
|
|
9763
10122
|
const id = parsed.positionals[3];
|
|
9764
10123
|
switch (action2) {
|
|
@@ -9778,6 +10137,12 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
9778
10137
|
return pmComments(ctx, entity, requireId2(id, action2));
|
|
9779
10138
|
case "rm":
|
|
9780
10139
|
return pmRemove(ctx, entity, requireId2(id, action2));
|
|
10140
|
+
case "ref":
|
|
10141
|
+
return pmReference(ctx, entity, requireId2(id, action2));
|
|
10142
|
+
case "ready":
|
|
10143
|
+
case "claim":
|
|
10144
|
+
case "release":
|
|
10145
|
+
return pmTaskLifecycle(ctx, requireId2(id, action2), action2, parsed);
|
|
9781
10146
|
}
|
|
9782
10147
|
}
|
|
9783
10148
|
|
|
@@ -10667,12 +11032,23 @@ var PM_ACTIONS = {
|
|
|
10667
11032
|
done: {},
|
|
10668
11033
|
comment: {},
|
|
10669
11034
|
comments: {},
|
|
11035
|
+
ref: {},
|
|
10670
11036
|
rm: {},
|
|
10671
11037
|
delete: {}
|
|
10672
11038
|
};
|
|
10673
|
-
var
|
|
10674
|
-
|
|
10675
|
-
|
|
11039
|
+
var PM_TASK_ACTIONS = {
|
|
11040
|
+
...PM_ACTIONS,
|
|
11041
|
+
ready: {},
|
|
11042
|
+
claim: {},
|
|
11043
|
+
release: {}
|
|
11044
|
+
};
|
|
11045
|
+
var PM_ENTITIES = {
|
|
11046
|
+
...Object.fromEntries(
|
|
11047
|
+
["goal", "conformance", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
|
|
11048
|
+
),
|
|
11049
|
+
task: PM_TASK_ACTIONS,
|
|
11050
|
+
kanban: PM_TASK_ACTIONS
|
|
11051
|
+
};
|
|
10676
11052
|
var COMMAND_SURFACE = {
|
|
10677
11053
|
agent: { jobs: {}, retry: {} },
|
|
10678
11054
|
admin: {
|
|
@@ -10725,7 +11101,9 @@ var COMMAND_SURFACE = {
|
|
|
10725
11101
|
},
|
|
10726
11102
|
pm: {
|
|
10727
11103
|
...PM_ENTITIES,
|
|
10728
|
-
handoff: {}
|
|
11104
|
+
handoff: {},
|
|
11105
|
+
next: {},
|
|
11106
|
+
watch: {}
|
|
10729
11107
|
},
|
|
10730
11108
|
provision: {},
|
|
10731
11109
|
runbook: {
|
|
@@ -10889,12 +11267,12 @@ async function call(ctx, method, path, body) {
|
|
|
10889
11267
|
throw new Error(message2);
|
|
10890
11268
|
}
|
|
10891
11269
|
async function bySlug(ctx, slug) {
|
|
10892
|
-
const
|
|
11270
|
+
const page2 = await call(
|
|
10893
11271
|
ctx,
|
|
10894
11272
|
"GET",
|
|
10895
11273
|
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
|
|
10896
11274
|
);
|
|
10897
|
-
const found =
|
|
11275
|
+
const found = page2.records[0];
|
|
10898
11276
|
if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
|
|
10899
11277
|
return found;
|
|
10900
11278
|
}
|
|
@@ -10908,11 +11286,11 @@ async function runbookList(ctx, all, query) {
|
|
|
10908
11286
|
const params = new URLSearchParams();
|
|
10909
11287
|
if (!all) params.set("app", ctx.appId);
|
|
10910
11288
|
if (query) params.set("q", query);
|
|
10911
|
-
const
|
|
10912
|
-
if (ctx.json) return ctx.out.log(JSON.stringify(
|
|
10913
|
-
if (!
|
|
11289
|
+
const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
|
|
11290
|
+
if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
|
|
11291
|
+
if (!page2.records.length) return ctx.out.log("(no runbooks)");
|
|
10914
11292
|
ctx.out.log(["SLUG", "STATUS", "V", "SCOPE", "UPDATED", "TITLE"].join(" "));
|
|
10915
|
-
for (const r of
|
|
11293
|
+
for (const r of page2.records)
|
|
10916
11294
|
ctx.out.log(
|
|
10917
11295
|
[r.slug, r.status, `v${r.version}`, r.appId === PLATFORM_SCOPE ? "platform" : r.appId, stamp(r.updatedAt), r.title].join(" ")
|
|
10918
11296
|
);
|
|
@@ -10967,12 +11345,12 @@ async function runbookVisibility(ctx, slug, visibility) {
|
|
|
10967
11345
|
}
|
|
10968
11346
|
async function runbookHistory(ctx, slug) {
|
|
10969
11347
|
const runbook = await bySlug(ctx, slug);
|
|
10970
|
-
const
|
|
10971
|
-
if (ctx.json) return ctx.out.log(JSON.stringify(
|
|
11348
|
+
const page2 = await call(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
|
|
11349
|
+
if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
|
|
10972
11350
|
ctx.out.log(`${slug} is at v${runbook.version}`);
|
|
10973
|
-
if (!
|
|
11351
|
+
if (!page2.records.length) return ctx.out.log("(no earlier versions)");
|
|
10974
11352
|
ctx.out.log(["V", "WHEN", "BY", "NOTE"].join(" "));
|
|
10975
|
-
for (const r of
|
|
11353
|
+
for (const r of page2.records)
|
|
10976
11354
|
ctx.out.log([`v${r.version}`, stamp(r.createdAt), r.ownerEmail ?? "", r.note ?? ""].join(" "));
|
|
10977
11355
|
}
|
|
10978
11356
|
async function runbookRevert(ctx, slug, version) {
|
|
@@ -11047,12 +11425,12 @@ ${counts.created} created, ${counts.updated} updated, ${counts.unchanged} unchan
|
|
|
11047
11425
|
);
|
|
11048
11426
|
}
|
|
11049
11427
|
async function upsert(ctx, r, visibility) {
|
|
11050
|
-
const
|
|
11428
|
+
const page2 = await call(
|
|
11051
11429
|
ctx,
|
|
11052
11430
|
"GET",
|
|
11053
11431
|
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(r.slug)}&limit=1`
|
|
11054
11432
|
);
|
|
11055
|
-
const found =
|
|
11433
|
+
const found = page2.records[0];
|
|
11056
11434
|
if (!found) {
|
|
11057
11435
|
await call(ctx, "POST", "/runbook", {
|
|
11058
11436
|
appId: ctx.appId,
|
|
@@ -11308,7 +11686,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
11308
11686
|
return out;
|
|
11309
11687
|
}
|
|
11310
11688
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
11311
|
-
function
|
|
11689
|
+
function report4(ctx, impacts) {
|
|
11312
11690
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
11313
11691
|
ctx.out.log(
|
|
11314
11692
|
`${impacts.length} changed surface${impacts.length === 1 ? "" : "s"}; ${covered.length} covered by a runbook. Reread each one and fix any step this change made wrong.`
|
|
@@ -11346,7 +11724,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
11346
11724
|
}
|
|
11347
11725
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
11348
11726
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
11349
|
-
|
|
11727
|
+
report4(ctx, impacts);
|
|
11350
11728
|
}
|
|
11351
11729
|
|
|
11352
11730
|
// src/runbook-lint.ts
|
|
@@ -11384,17 +11762,17 @@ function lintRunbook(runbook, installed) {
|
|
|
11384
11762
|
async function runbookLint(ctx, all) {
|
|
11385
11763
|
const params = new URLSearchParams();
|
|
11386
11764
|
if (!all) params.set("app", ctx.appId);
|
|
11387
|
-
const
|
|
11765
|
+
const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
|
|
11388
11766
|
const installed = { "@odla-ai/cli": cliVersion() };
|
|
11389
|
-
const findings =
|
|
11390
|
-
if (ctx.json) return ctx.out.log(JSON.stringify({ checked:
|
|
11391
|
-
if (!
|
|
11767
|
+
const findings = page2.records.flatMap((runbook) => lintRunbook(runbook, installed));
|
|
11768
|
+
if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page2.records.length, findings }, null, 2));
|
|
11769
|
+
if (!page2.records.length) return ctx.out.log("(no runbooks in scope)");
|
|
11392
11770
|
if (!findings.length) {
|
|
11393
11771
|
return ctx.out.log(
|
|
11394
|
-
`${
|
|
11772
|
+
`${page2.records.length} runbook${page2.records.length === 1 ? "" : "s"} checked; every command they name is real for @odla-ai/cli ${cliVersion()}.`
|
|
11395
11773
|
);
|
|
11396
11774
|
}
|
|
11397
|
-
ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${
|
|
11775
|
+
ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${page2.records.length} runbooks:`);
|
|
11398
11776
|
for (const finding of findings) {
|
|
11399
11777
|
const scope = finding.appId === PLATFORM_SCOPE ? "" : ` --app ${finding.appId}`;
|
|
11400
11778
|
ctx.out.log("");
|
|
@@ -11457,12 +11835,12 @@ async function runbookAsk(ctx, question, all) {
|
|
|
11457
11835
|
ctx.out.log(JSDOC_POINTER);
|
|
11458
11836
|
}
|
|
11459
11837
|
async function runbookComment(ctx, slug, body) {
|
|
11460
|
-
const
|
|
11838
|
+
const page2 = await call(
|
|
11461
11839
|
ctx,
|
|
11462
11840
|
"GET",
|
|
11463
11841
|
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
|
|
11464
11842
|
);
|
|
11465
|
-
const found =
|
|
11843
|
+
const found = page2.records[0];
|
|
11466
11844
|
if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
|
|
11467
11845
|
await call(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
|
|
11468
11846
|
ctx.out.log(`commented on ${slug} (v${found.version})`);
|
|
@@ -11514,12 +11892,12 @@ var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
|
|
|
11514
11892
|
|
|
11515
11893
|
// src/runbook-edit-flow.ts
|
|
11516
11894
|
async function editRunbook(ctx, slug, deps = {}) {
|
|
11517
|
-
const
|
|
11895
|
+
const page2 = await call(
|
|
11518
11896
|
ctx,
|
|
11519
11897
|
"GET",
|
|
11520
11898
|
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
|
|
11521
11899
|
);
|
|
11522
|
-
const found =
|
|
11900
|
+
const found = page2.records[0];
|
|
11523
11901
|
if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
|
|
11524
11902
|
ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
|
|
11525
11903
|
const body = await editText(found.body, slug, deps);
|
|
@@ -11940,31 +12318,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
11940
12318
|
url.searchParams.set("job", job.jobId);
|
|
11941
12319
|
out.log(` Studio: ${url.toString()}`);
|
|
11942
12320
|
}
|
|
11943
|
-
function printHostedReport(out,
|
|
11944
|
-
out.log(`security report ${
|
|
11945
|
-
out.log(` coverage: ${
|
|
11946
|
-
out.log(` findings: confirmed=${
|
|
11947
|
-
out.log(` discovery: ${
|
|
11948
|
-
out.log(` validation: ${
|
|
11949
|
-
for (const finding of
|
|
12321
|
+
function printHostedReport(out, report5) {
|
|
12322
|
+
out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
|
|
12323
|
+
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}`);
|
|
12324
|
+
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
|
|
12325
|
+
out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
|
|
12326
|
+
out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
|
|
12327
|
+
for (const finding of report5.findings) {
|
|
11950
12328
|
const location = finding.locations[0];
|
|
11951
12329
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
11952
12330
|
}
|
|
11953
|
-
for (const limitation of
|
|
12331
|
+
for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
|
|
11954
12332
|
}
|
|
11955
|
-
function enforceHostedReportGate(
|
|
12333
|
+
function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
|
|
11956
12334
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
11957
12335
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
11958
12336
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
11959
12337
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
11960
|
-
const confirmed =
|
|
11961
|
-
const leads = failOnCandidates ?
|
|
11962
|
-
const incomplete =
|
|
12338
|
+
const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
12339
|
+
const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
12340
|
+
const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
11963
12341
|
if (confirmed.length || leads.length || incomplete) {
|
|
11964
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
12342
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
|
|
11965
12343
|
}
|
|
11966
12344
|
if (emitSuccess) {
|
|
11967
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
12345
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
|
|
11968
12346
|
}
|
|
11969
12347
|
}
|
|
11970
12348
|
function printHostedSecurityPlanRoute(out, label, route2) {
|
|
@@ -12047,17 +12425,17 @@ async function runHostedSecurity(options) {
|
|
|
12047
12425
|
allowNetwork: false
|
|
12048
12426
|
}
|
|
12049
12427
|
});
|
|
12050
|
-
const
|
|
12051
|
-
await (0, import_node3.writeSecurityArtifacts)(output,
|
|
12052
|
-
const reportDigest = await (0, import_security.securityFingerprint)(
|
|
12428
|
+
const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
12429
|
+
await (0, import_node3.writeSecurityArtifacts)(output, report5);
|
|
12430
|
+
const reportDigest = await (0, import_security.securityFingerprint)(report5);
|
|
12053
12431
|
await hosted.complete({
|
|
12054
12432
|
reportDigest,
|
|
12055
|
-
coverageStatus:
|
|
12056
|
-
confirmed:
|
|
12057
|
-
candidates:
|
|
12433
|
+
coverageStatus: report5.coverageStatus,
|
|
12434
|
+
confirmed: report5.metrics.confirmed,
|
|
12435
|
+
candidates: report5.metrics.candidates
|
|
12058
12436
|
}, { signal: options.signal });
|
|
12059
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
12060
|
-
return Object.freeze({ report:
|
|
12437
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
|
|
12438
|
+
return Object.freeze({ report: report5, run: hosted.run, output });
|
|
12061
12439
|
}
|
|
12062
12440
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
12063
12441
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -12083,14 +12461,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
12083
12461
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
12084
12462
|
return { ...profile, maxHuntTasks };
|
|
12085
12463
|
}
|
|
12086
|
-
function printSummary(out, appId, env, run,
|
|
12087
|
-
const complete =
|
|
12464
|
+
function printSummary(out, appId, env, run, report5, output) {
|
|
12465
|
+
const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
|
|
12088
12466
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
12089
12467
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
12090
12468
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
12091
|
-
out.log(` coverage: ${
|
|
12092
|
-
if (
|
|
12093
|
-
out.log(` findings: confirmed=${
|
|
12469
|
+
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}`);
|
|
12470
|
+
if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
|
|
12471
|
+
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
|
|
12094
12472
|
out.log(` report: ${(0, import_node_path19.resolve)(output, "REPORT.md")}`);
|
|
12095
12473
|
}
|
|
12096
12474
|
function formatBudget(usage) {
|
|
@@ -12326,13 +12704,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
12326
12704
|
}
|
|
12327
12705
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
12328
12706
|
}
|
|
12329
|
-
const
|
|
12707
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
12330
12708
|
if (parsed.options.json === true) {
|
|
12331
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
12709
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
|
|
12332
12710
|
} else {
|
|
12333
|
-
printHostedReport(context.stdout,
|
|
12711
|
+
printHostedReport(context.stdout, report5);
|
|
12334
12712
|
}
|
|
12335
|
-
enforceHostedReportGate(
|
|
12713
|
+
enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
|
|
12336
12714
|
}
|
|
12337
12715
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
12338
12716
|
if (parsed.options.source === true) {
|
|
@@ -12399,13 +12777,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
12399
12777
|
});
|
|
12400
12778
|
enforceLocalGate(result.report, parsed);
|
|
12401
12779
|
}
|
|
12402
|
-
function enforceLocalGate(
|
|
12780
|
+
function enforceLocalGate(report5, parsed) {
|
|
12403
12781
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
12404
12782
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
12405
12783
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
12406
|
-
const confirmed = (0, import_security2.findingsAtOrAbove)(
|
|
12407
|
-
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(
|
|
12408
|
-
const incomplete =
|
|
12784
|
+
const confirmed = (0, import_security2.findingsAtOrAbove)(report5, failOn);
|
|
12785
|
+
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
12786
|
+
const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
12409
12787
|
if (confirmed.length || leads.length || incomplete) {
|
|
12410
12788
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
12411
12789
|
}
|
|
@@ -12444,9 +12822,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
12444
12822
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
12445
12823
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
12446
12824
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
12447
|
-
const
|
|
12448
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
12449
|
-
else printHostedReport(context.stdout,
|
|
12825
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId });
|
|
12826
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
|
|
12827
|
+
else printHostedReport(context.stdout, report5);
|
|
12450
12828
|
return;
|
|
12451
12829
|
}
|
|
12452
12830
|
if (sub !== "run") {
|