@odla-ai/cli 0.27.2 → 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 -3
- package/REQUIREMENTS.md +4 -0
- 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/index.cjs
CHANGED
|
@@ -100,6 +100,7 @@ var import_node_process7 = __toESM(require("process"), 1);
|
|
|
100
100
|
|
|
101
101
|
// src/token.ts
|
|
102
102
|
var import_db = require("@odla-ai/db");
|
|
103
|
+
var import_node_crypto = require("crypto");
|
|
103
104
|
var import_node_process4 = __toESM(require("process"), 1);
|
|
104
105
|
|
|
105
106
|
// src/handshake-approval.ts
|
|
@@ -454,6 +455,7 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
454
455
|
endpoint: ctx.cfg.platformUrl,
|
|
455
456
|
email: ctx.email,
|
|
456
457
|
label: `${ctx.cfg.app.id} provisioner`,
|
|
458
|
+
agentHandle: projectAgentHandle(ctx.cfg.app.id),
|
|
457
459
|
projectIds: [ctx.cfg.app.id],
|
|
458
460
|
fetch: ctx.doFetch,
|
|
459
461
|
waitMs,
|
|
@@ -489,6 +491,12 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
489
491
|
stopReminder?.();
|
|
490
492
|
}
|
|
491
493
|
}
|
|
494
|
+
function projectAgentHandle(appId) {
|
|
495
|
+
const candidate = /^[a-z]/.test(appId) ? appId : `app-${appId}`;
|
|
496
|
+
if (candidate.length <= 32) return candidate;
|
|
497
|
+
const digest = (0, import_node_crypto.createHash)("sha256").update(appId).digest("hex").slice(0, 8);
|
|
498
|
+
return `${candidate.slice(0, 23).replace(/-+$/, "")}-${digest}`;
|
|
499
|
+
}
|
|
492
500
|
function stillPending(pending, email) {
|
|
493
501
|
return new import_db.OdlaError(
|
|
494
502
|
"handshake_pending",
|
|
@@ -2701,8 +2709,8 @@ async function calendarCalendars(options) {
|
|
|
2701
2709
|
async function calendarConnect(options) {
|
|
2702
2710
|
const { cfg, ctx, out } = await lifecycleContext(options);
|
|
2703
2711
|
productionConsent(ctx.env, options.yes, "connect calendar");
|
|
2704
|
-
const
|
|
2705
|
-
const applied =
|
|
2712
|
+
const page2 = calendarBookingPageUrl(cfg, ctx.env);
|
|
2713
|
+
const applied = page2 === void 0 ? await readCalendarStatus(ctx) : await applyCalendarSettings(ctx, page2);
|
|
2706
2714
|
const connectOptions = connectionOptions(options, out);
|
|
2707
2715
|
return await continueConnectedCalendar(ctx, applied, connectOptions) ?? connectWithContext(ctx, connectOptions);
|
|
2708
2716
|
}
|
|
@@ -2951,12 +2959,12 @@ var import_apps3 = require("@odla-ai/apps");
|
|
|
2951
2959
|
var import_node_fs10 = require("fs");
|
|
2952
2960
|
|
|
2953
2961
|
// src/config-reconcile-digest.ts
|
|
2954
|
-
var
|
|
2962
|
+
var import_node_crypto2 = require("crypto");
|
|
2955
2963
|
function canonicalJson(value2) {
|
|
2956
2964
|
return JSON.stringify(canonicalValue(value2));
|
|
2957
2965
|
}
|
|
2958
2966
|
function configDigest(value2) {
|
|
2959
|
-
return `sha256:${(0,
|
|
2967
|
+
return `sha256:${(0, import_node_crypto2.createHash)("sha256").update(canonicalJson(value2)).digest("hex")}`;
|
|
2960
2968
|
}
|
|
2961
2969
|
function canonicalValue(value2) {
|
|
2962
2970
|
if (value2 === null || typeof value2 === "string" || typeof value2 === "boolean") return value2;
|
|
@@ -4529,9 +4537,10 @@ For work that creates an odla app or adds odla services, read and follow
|
|
|
4529
4537
|
\`.agents/skills/odla-migrate/SKILL.md\`. For production telemetry triage, use
|
|
4530
4538
|
\`.agents/skills/odla-o11y-debug/SKILL.md\`.
|
|
4531
4539
|
|
|
4532
|
-
Track the work in odla's PM as you go
|
|
4533
|
-
|
|
4534
|
-
|
|
4540
|
+
Track the work in odla's PM as you go. Before project-mutating work, run
|
|
4541
|
+
\`npx @odla-ai/cli pm next --app <appId>\`, confirm alignment to an open goal,
|
|
4542
|
+
and atomically claim a refined Ready task. Record decisions when you make them
|
|
4543
|
+
and file bugs when you notice them. The conventions and the full command set are
|
|
4535
4544
|
in \`.agents/skills/odla/references/pm.md\`.
|
|
4536
4545
|
|
|
4537
4546
|
The setup runbooks and their references are installed in this repository, pinned
|
|
@@ -5755,7 +5764,7 @@ function dependenciesOf(values, influence = "data") {
|
|
|
5755
5764
|
return [...unique3.values()];
|
|
5756
5765
|
}
|
|
5757
5766
|
|
|
5758
|
-
// ../camel/dist/chunk-
|
|
5767
|
+
// ../camel/dist/chunk-4DQ6BIHP.js
|
|
5759
5768
|
var camelValueBrand = /* @__PURE__ */ Symbol("@odla-ai/camel/value");
|
|
5760
5769
|
var authenticCamelValues = /* @__PURE__ */ new WeakSet();
|
|
5761
5770
|
function isCamelValue(value2) {
|
|
@@ -5767,13 +5776,13 @@ function isSafe(value2) {
|
|
|
5767
5776
|
function isUnsafe(value2) {
|
|
5768
5777
|
return isCamelValue(value2) && value2.label.promptSafety === "unsafe";
|
|
5769
5778
|
}
|
|
5770
|
-
function createSafeInternal(value2, safeBasis, metadata2) {
|
|
5779
|
+
function createSafeInternal(value2, safeBasis, metadata2, extra) {
|
|
5771
5780
|
return createValue(value2, {
|
|
5772
5781
|
schemaVersion: 1,
|
|
5773
5782
|
promptSafety: "safe",
|
|
5774
5783
|
safeBasis,
|
|
5775
5784
|
...copyMetadata(metadata2)
|
|
5776
|
-
});
|
|
5785
|
+
}, extra);
|
|
5777
5786
|
}
|
|
5778
5787
|
function createUnsafeInternal(value2, metadata2) {
|
|
5779
5788
|
return createValue(value2, {
|
|
@@ -5782,8 +5791,8 @@ function createUnsafeInternal(value2, metadata2) {
|
|
|
5782
5791
|
...copyMetadata(metadata2)
|
|
5783
5792
|
});
|
|
5784
5793
|
}
|
|
5785
|
-
function createValue(value2, label) {
|
|
5786
|
-
const result = { value: value2, label: Object.freeze(label) };
|
|
5794
|
+
function createValue(value2, label, extra) {
|
|
5795
|
+
const result = { value: value2, label: Object.freeze(label), ...extra };
|
|
5787
5796
|
Object.defineProperty(result, camelValueBrand, { value: label.promptSafety, enumerable: false });
|
|
5788
5797
|
authenticCamelValues.add(result);
|
|
5789
5798
|
return Object.freeze(result);
|
|
@@ -6044,7 +6053,7 @@ var import_path8 = require("path");
|
|
|
6044
6053
|
var import_promises10 = require("fs/promises");
|
|
6045
6054
|
var import_path9 = require("path");
|
|
6046
6055
|
|
|
6047
|
-
// ../camel/dist/chunk-
|
|
6056
|
+
// ../camel/dist/chunk-4EIRFS3A.js
|
|
6048
6057
|
function conversionPolicyDigest(policy) {
|
|
6049
6058
|
return sha256Hex(canonicalJson2(policy));
|
|
6050
6059
|
}
|
|
@@ -6132,10 +6141,18 @@ async function convert(source, policy, value2, counts) {
|
|
|
6132
6141
|
const count = counts.get(countKey) ?? 0;
|
|
6133
6142
|
if (count >= policy.maximumOutputsPerArtifact) throw new CamelError("limit_exceeded", "Conversion output count exceeds its per-source bound.");
|
|
6134
6143
|
counts.set(countKey, count + 1);
|
|
6144
|
+
const conversionRecordId = await sha256Hex(canonicalJson2({ conversionId: policy.conversionId, digest: policy.digest, source: sourceKey, ordinal: count }));
|
|
6145
|
+
const datumId = await sha256Hex(canonicalJson2({ conversionRecordId, value: value2 }));
|
|
6135
6146
|
return createSafeInternal(value2, "atomic_conversion", {
|
|
6136
6147
|
readers: source.label.readers,
|
|
6137
6148
|
provenance: [...source.label.provenance, { kind: "converter", id: policy.conversionId, digest: policy.digest }],
|
|
6138
6149
|
dependencies: dependenciesOf([source])
|
|
6150
|
+
}, {
|
|
6151
|
+
datumId,
|
|
6152
|
+
kind: policy.output.kind,
|
|
6153
|
+
conversionId: policy.conversionId,
|
|
6154
|
+
conversionDigest: policy.digest,
|
|
6155
|
+
conversionRecordId
|
|
6139
6156
|
});
|
|
6140
6157
|
}
|
|
6141
6158
|
function validatePolicyShape(policy) {
|
|
@@ -6200,7 +6217,7 @@ function missingPolicy() {
|
|
|
6200
6217
|
throw new CamelError("conversion_rejected", "Conversion policy is not registered.");
|
|
6201
6218
|
}
|
|
6202
6219
|
|
|
6203
|
-
// ../camel/dist/chunk-
|
|
6220
|
+
// ../camel/dist/chunk-VEAUXH4F.js
|
|
6204
6221
|
function createCamelIngress(constants2 = []) {
|
|
6205
6222
|
const byId = /* @__PURE__ */ new Map();
|
|
6206
6223
|
for (const item of constants2) {
|
|
@@ -8190,7 +8207,7 @@ async function defaultReadOrigin(cwd) {
|
|
|
8190
8207
|
|
|
8191
8208
|
// src/code-local-source.ts
|
|
8192
8209
|
var import_node_child_process5 = require("child_process");
|
|
8193
|
-
var
|
|
8210
|
+
var import_node_crypto3 = require("crypto");
|
|
8194
8211
|
var SOURCE_LIMITS2 = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
|
|
8195
8212
|
async function prepareCodeLocalSource(cwd, repository, readHead = readGitHead) {
|
|
8196
8213
|
const headCommitSha = await readHead(cwd);
|
|
@@ -8241,12 +8258,12 @@ async function readGitHead(cwd) {
|
|
|
8241
8258
|
return value2;
|
|
8242
8259
|
}
|
|
8243
8260
|
function digestText(value2) {
|
|
8244
|
-
return `sha256:${(0,
|
|
8261
|
+
return `sha256:${(0, import_node_crypto3.createHash)("sha256").update(value2).digest("hex")}`;
|
|
8245
8262
|
}
|
|
8246
8263
|
|
|
8247
8264
|
// src/code-images.ts
|
|
8248
8265
|
var import_node_child_process6 = require("child_process");
|
|
8249
|
-
var
|
|
8266
|
+
var import_node_crypto4 = require("crypto");
|
|
8250
8267
|
var import_promises11 = require("fs/promises");
|
|
8251
8268
|
var import_node_os3 = require("os");
|
|
8252
8269
|
var import_node_path14 = require("path");
|
|
@@ -8332,7 +8349,7 @@ async function embeddedPiImageName() {
|
|
|
8332
8349
|
const bundle = await (0, import_promises11.readFile)(embeddedPiAssetPath()).catch(() => {
|
|
8333
8350
|
throw new Error("CLI-embedded Pi runtime is missing; reinstall this exact @odla-ai/cli version");
|
|
8334
8351
|
});
|
|
8335
|
-
return `odla-ai/pi-agent:embedded-sha256-${(0,
|
|
8352
|
+
return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto4.createHash)("sha256").update(bundle).digest("hex")}`;
|
|
8336
8353
|
}
|
|
8337
8354
|
async function buildEmbeddedPiImage(engine, image, run) {
|
|
8338
8355
|
const context = await (0, import_promises11.mkdtemp)((0, import_node_path14.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
|
|
@@ -8760,16 +8777,22 @@ Usage:
|
|
|
8760
8777
|
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
8761
8778
|
odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
|
|
8762
8779
|
odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8763
|
-
odla-ai pm task list [--app <id>] [--column <
|
|
8780
|
+
odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8764
8781
|
odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8765
8782
|
odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8766
8783
|
odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
|
|
8767
|
-
odla-ai pm task add --app <id> --title <t> [--column <
|
|
8784
|
+
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]
|
|
8785
|
+
odla-ai pm next --app <id> [--json]
|
|
8786
|
+
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]
|
|
8787
|
+
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]
|
|
8788
|
+
odla-ai pm task claim <id> --expected-revision <n> [--mutation-id <id>] [--json]
|
|
8789
|
+
odla-ai pm task release <id> --expected-revision <n> [--mutation-id <id>] [--json]
|
|
8768
8790
|
odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
|
|
8769
8791
|
odla-ai pm bug add --app <id> --title <t> (--description <text>|--body <text>) [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--mutation-id <id>] [--json]
|
|
8770
8792
|
odla-ai pm <goal|task|decision|bug> get <id> [--json]
|
|
8793
|
+
odla-ai pm <goal|task|decision|bug> ref <id> [--json]
|
|
8771
8794
|
odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
|
|
8772
|
-
odla-ai pm task set <id> [--title <t>|--column <
|
|
8795
|
+
odla-ai pm task set <id> [--title <t>|--column <backlog|ready|doing|review|done>|--rank <n>|--goal <id>|--no-goal|--alignment-decision <id>|--no-alignment-decision|--execution <human|agent|either>|--assignee <id>|--no-assignee|--description <text>|--body <text>|--acceptance <text>|--no-acceptance|--due <epoch-ms>|--no-due|--expected-revision <n>] [--mutation-id <id>] [--json]
|
|
8773
8796
|
odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
|
|
8774
8797
|
odla-ai pm bug set <id> [--title <t>|--status <s>|--severity <s>|--goal <id>|--no-goal|--assignee <id>|--no-assignee|--decision <id>|--no-decision|--description <text>|--body <text>] [--mutation-id <id>] [--json]
|
|
8775
8798
|
odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
|
|
@@ -9088,11 +9111,11 @@ async function discussList(ctx, parsed) {
|
|
|
9088
9111
|
if (value2) query.set(param, value2);
|
|
9089
9112
|
}
|
|
9090
9113
|
const qs = query.toString();
|
|
9091
|
-
const
|
|
9092
|
-
emit(ctx,
|
|
9093
|
-
ctx.out.log(`topics \u2014 ${
|
|
9114
|
+
const page2 = await request(ctx, "GET", `/topics${qs ? `?${qs}` : ""}`);
|
|
9115
|
+
emit(ctx, page2, () => {
|
|
9116
|
+
ctx.out.log(`topics \u2014 ${page2.topics.length} of ${page2.total}`);
|
|
9094
9117
|
ctx.out.log("id state app replies subject");
|
|
9095
|
-
for (const topic of
|
|
9118
|
+
for (const topic of page2.topics) {
|
|
9096
9119
|
ctx.out.log(
|
|
9097
9120
|
`${topic.id} ${state(topic)} ${topic.appId ?? ""} ${topic.replyCount} ${topic.subject}`
|
|
9098
9121
|
);
|
|
@@ -9107,11 +9130,11 @@ async function discussRead(ctx, id, parsed) {
|
|
|
9107
9130
|
limit: requestedLimit ?? "200",
|
|
9108
9131
|
offset: requestedOffset ?? "0"
|
|
9109
9132
|
});
|
|
9110
|
-
const
|
|
9133
|
+
const page2 = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
|
|
9111
9134
|
emit(
|
|
9112
9135
|
ctx,
|
|
9113
|
-
|
|
9114
|
-
() => renderDiscussRead(ctx,
|
|
9136
|
+
page2,
|
|
9137
|
+
() => renderDiscussRead(ctx, page2.topic, page2.posts, page2)
|
|
9115
9138
|
);
|
|
9116
9139
|
return;
|
|
9117
9140
|
}
|
|
@@ -9124,20 +9147,20 @@ async function discussRead(ctx, id, parsed) {
|
|
|
9124
9147
|
let topic = null;
|
|
9125
9148
|
let offset = 0;
|
|
9126
9149
|
for (; ; ) {
|
|
9127
|
-
const
|
|
9150
|
+
const page2 = await request(
|
|
9128
9151
|
ctx,
|
|
9129
9152
|
"GET",
|
|
9130
9153
|
`/topics/${encodeURIComponent(id)}?limit=200&offset=${offset}`
|
|
9131
9154
|
);
|
|
9132
|
-
topic =
|
|
9133
|
-
for (const post of
|
|
9134
|
-
mergeDiscussPrincipals(projection,
|
|
9155
|
+
topic = page2.topic;
|
|
9156
|
+
for (const post of page2.posts) posts.set(post.id, post);
|
|
9157
|
+
mergeDiscussPrincipals(projection, page2);
|
|
9135
9158
|
if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
|
|
9136
|
-
if (!
|
|
9137
|
-
if (
|
|
9159
|
+
if (!page2.page?.hasMore) break;
|
|
9160
|
+
if (page2.page.nextOffset === null || page2.page.nextOffset <= offset) {
|
|
9138
9161
|
throw new Error("discuss read failed: registry returned a non-advancing post page");
|
|
9139
9162
|
}
|
|
9140
|
-
offset =
|
|
9163
|
+
offset = page2.page.nextOffset;
|
|
9141
9164
|
}
|
|
9142
9165
|
const ordered = [...posts.values()].sort(
|
|
9143
9166
|
(a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
|
|
@@ -9305,9 +9328,9 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9305
9328
|
let firstSuccess = true;
|
|
9306
9329
|
let consecutiveFailures = 0;
|
|
9307
9330
|
for (; ; ) {
|
|
9308
|
-
let
|
|
9331
|
+
let page2;
|
|
9309
9332
|
try {
|
|
9310
|
-
|
|
9333
|
+
page2 = await getWatchPage(ctx, requestPath(topicId, app, cursor));
|
|
9311
9334
|
consecutiveFailures = 0;
|
|
9312
9335
|
} catch (error) {
|
|
9313
9336
|
if (!(error instanceof WatchRequestError) || !error.retryable) {
|
|
@@ -9343,25 +9366,25 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9343
9366
|
await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
|
|
9344
9367
|
continue;
|
|
9345
9368
|
}
|
|
9346
|
-
cursor =
|
|
9347
|
-
const baseline = firstSuccess &&
|
|
9369
|
+
cursor = page2.cursor;
|
|
9370
|
+
const baseline = firstSuccess && page2.events.length === 0;
|
|
9348
9371
|
if (baseline) {
|
|
9349
9372
|
jsonl(ctx, parsed, {
|
|
9350
9373
|
type: "checkpoint",
|
|
9351
|
-
streamId:
|
|
9374
|
+
streamId: page2.streamId,
|
|
9352
9375
|
cursor,
|
|
9353
|
-
serverTime:
|
|
9376
|
+
serverTime: page2.serverTime
|
|
9354
9377
|
});
|
|
9355
9378
|
}
|
|
9356
9379
|
firstSuccess = false;
|
|
9357
|
-
const matching =
|
|
9380
|
+
const matching = page2.events.filter((event) => {
|
|
9358
9381
|
if (topicId && (event.type !== "message" || event.action !== "created")) return false;
|
|
9359
9382
|
return (!by || event.actor.id === by) && (!self || event.actor.id !== self);
|
|
9360
9383
|
});
|
|
9361
9384
|
for (const event of matching) {
|
|
9362
9385
|
jsonl(ctx, parsed, {
|
|
9363
9386
|
type: "event",
|
|
9364
|
-
streamId:
|
|
9387
|
+
streamId: page2.streamId,
|
|
9365
9388
|
eventId: event.id,
|
|
9366
9389
|
cursor: event.cursor,
|
|
9367
9390
|
event
|
|
@@ -9370,9 +9393,9 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9370
9393
|
if (matching.length > 0) {
|
|
9371
9394
|
jsonl(ctx, parsed, {
|
|
9372
9395
|
type: "checkpoint",
|
|
9373
|
-
streamId:
|
|
9396
|
+
streamId: page2.streamId,
|
|
9374
9397
|
cursor,
|
|
9375
|
-
serverTime:
|
|
9398
|
+
serverTime: page2.serverTime
|
|
9376
9399
|
});
|
|
9377
9400
|
const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
|
|
9378
9401
|
const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
|
|
@@ -9380,28 +9403,28 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9380
9403
|
found: true,
|
|
9381
9404
|
cursor,
|
|
9382
9405
|
events: matching,
|
|
9383
|
-
...
|
|
9384
|
-
...
|
|
9406
|
+
...page2.authors ? { authors: page2.authors } : {},
|
|
9407
|
+
...page2.principals ? { principals: page2.principals } : {},
|
|
9385
9408
|
...posts && posts.length > 0 ? { posts } : {},
|
|
9386
9409
|
...topics && topics.length > 0 ? { topics } : {}
|
|
9387
9410
|
});
|
|
9388
9411
|
}
|
|
9389
|
-
if (
|
|
9412
|
+
if (page2.events.length > 0) {
|
|
9390
9413
|
jsonl(ctx, parsed, {
|
|
9391
9414
|
type: "checkpoint",
|
|
9392
|
-
streamId:
|
|
9415
|
+
streamId: page2.streamId,
|
|
9393
9416
|
cursor,
|
|
9394
|
-
serverTime:
|
|
9417
|
+
serverTime: page2.serverTime
|
|
9395
9418
|
});
|
|
9396
9419
|
} else if (!baseline) {
|
|
9397
9420
|
jsonl(ctx, parsed, {
|
|
9398
9421
|
type: "heartbeat",
|
|
9399
|
-
streamId:
|
|
9422
|
+
streamId: page2.streamId,
|
|
9400
9423
|
cursor,
|
|
9401
|
-
serverTime:
|
|
9424
|
+
serverTime: page2.serverTime
|
|
9402
9425
|
});
|
|
9403
9426
|
}
|
|
9404
|
-
if (
|
|
9427
|
+
if (page2.hasMore) continue;
|
|
9405
9428
|
if (deadline !== void 0 && now() >= deadline) {
|
|
9406
9429
|
return report2(ctx, parsed, { found: false, cursor });
|
|
9407
9430
|
}
|
|
@@ -9520,7 +9543,13 @@ async function discussCommand(parsed, deps = {}) {
|
|
|
9520
9543
|
}
|
|
9521
9544
|
}
|
|
9522
9545
|
|
|
9523
|
-
// src/pm-
|
|
9546
|
+
// src/pm-action-core.ts
|
|
9547
|
+
var DONE = {
|
|
9548
|
+
goal: { status: "met", currentPct: 100 },
|
|
9549
|
+
task: { column: "done" },
|
|
9550
|
+
decision: { status: "accepted" },
|
|
9551
|
+
bug: { status: "fixed" }
|
|
9552
|
+
};
|
|
9524
9553
|
var writeMutationId2 = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
|
|
9525
9554
|
var FIELD_MAP = {
|
|
9526
9555
|
title: { key: "title" },
|
|
@@ -9536,22 +9565,27 @@ var FIELD_MAP = {
|
|
|
9536
9565
|
body: { key: "body" },
|
|
9537
9566
|
target: { key: "targetPct", num: true },
|
|
9538
9567
|
description: { key: "description" },
|
|
9539
|
-
desc: { key: "description" }
|
|
9540
|
-
}
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
decision: { status: "accepted" },
|
|
9545
|
-
bug: { status: "fixed" }
|
|
9568
|
+
desc: { key: "description" },
|
|
9569
|
+
acceptance: { key: "acceptanceCriteria" },
|
|
9570
|
+
"alignment-decision": { key: "alignmentDecisionId" },
|
|
9571
|
+
execution: { key: "executionMode" },
|
|
9572
|
+
"expected-revision": { key: "expectedRevision", num: true }
|
|
9546
9573
|
};
|
|
9547
9574
|
async function pmRequest(ctx, method, path, body) {
|
|
9548
|
-
const
|
|
9575
|
+
const response2 = await ctx.doFetch(`${ctx.platformUrl}/registry/pm${path}`, {
|
|
9549
9576
|
method,
|
|
9550
|
-
headers: {
|
|
9577
|
+
headers: {
|
|
9578
|
+
authorization: `Bearer ${ctx.token}`,
|
|
9579
|
+
"content-type": "application/json"
|
|
9580
|
+
},
|
|
9551
9581
|
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
9552
9582
|
});
|
|
9553
|
-
const data = await
|
|
9554
|
-
if (!
|
|
9583
|
+
const data = await response2.json().catch(() => ({}));
|
|
9584
|
+
if (!response2.ok) {
|
|
9585
|
+
throw new Error(
|
|
9586
|
+
`pm ${method} ${path} failed: ${data.error ?? `registry returned ${response2.status}`}`
|
|
9587
|
+
);
|
|
9588
|
+
}
|
|
9555
9589
|
return data;
|
|
9556
9590
|
}
|
|
9557
9591
|
function collectFields(parsed, allowClear) {
|
|
@@ -9574,20 +9608,32 @@ function collectEntityFields(entity, parsed, allowClear) {
|
|
|
9574
9608
|
if (fields.description === void 0) fields.description = fields.body;
|
|
9575
9609
|
delete fields.body;
|
|
9576
9610
|
}
|
|
9611
|
+
if (entity === "task" && fields.column === "ready") fields.column = "todo";
|
|
9577
9612
|
return fields;
|
|
9578
9613
|
}
|
|
9579
|
-
function statusCol(entity,
|
|
9580
|
-
if (entity === "bug") return `${
|
|
9581
|
-
if (entity === "task")
|
|
9582
|
-
|
|
9614
|
+
function statusCol(entity, record10) {
|
|
9615
|
+
if (entity === "bug") return `${record10.status ?? ""}/${record10.severity ?? ""}`;
|
|
9616
|
+
if (entity === "task") {
|
|
9617
|
+
const state2 = record10.column === "todo" ? "ready" : String(record10.column ?? "");
|
|
9618
|
+
return record10.revision ? `${state2}; r${record10.revision}` : state2;
|
|
9619
|
+
}
|
|
9620
|
+
return String(record10.status ?? "");
|
|
9583
9621
|
}
|
|
9584
|
-
function
|
|
9585
|
-
|
|
9622
|
+
function referenceMarkup(entity, record10) {
|
|
9623
|
+
const label = (record10.title?.trim() || `${entity} ${record10.id}`).replaceAll("]", ")");
|
|
9624
|
+
return `@[${label}](pm:${entity}/${record10.id})`;
|
|
9625
|
+
}
|
|
9626
|
+
function printRecord(ctx, entity, record10) {
|
|
9627
|
+
ctx.out.log(
|
|
9628
|
+
`${record10.id} [${statusCol(entity, record10)}] ${record10.appId} ${record10.title ?? ""}`
|
|
9629
|
+
);
|
|
9586
9630
|
}
|
|
9587
9631
|
function emit2(ctx, value2, human) {
|
|
9588
9632
|
if (ctx.json) ctx.out.log(JSON.stringify(value2, null, 2));
|
|
9589
9633
|
else human();
|
|
9590
9634
|
}
|
|
9635
|
+
|
|
9636
|
+
// src/pm-actions.ts
|
|
9591
9637
|
async function pmList(ctx, entity, parsed) {
|
|
9592
9638
|
const q = new URLSearchParams();
|
|
9593
9639
|
const filters = {
|
|
@@ -9601,7 +9647,8 @@ async function pmList(ctx, entity, parsed) {
|
|
|
9601
9647
|
const app = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
9602
9648
|
if (app) q.set("app", app);
|
|
9603
9649
|
for (const [flag, param] of Object.entries(filters)) {
|
|
9604
|
-
const
|
|
9650
|
+
const raw = stringOpt(parsed.options[flag]);
|
|
9651
|
+
const v = entity === "task" && flag === "column" && raw === "ready" ? "todo" : raw;
|
|
9605
9652
|
if (v) q.set(param, v);
|
|
9606
9653
|
}
|
|
9607
9654
|
for (const opt of ["q", "limit", "offset"]) {
|
|
@@ -9609,11 +9656,11 @@ async function pmList(ctx, entity, parsed) {
|
|
|
9609
9656
|
if (v) q.set(opt, v);
|
|
9610
9657
|
}
|
|
9611
9658
|
const qs = q.toString();
|
|
9612
|
-
const
|
|
9613
|
-
emit2(ctx,
|
|
9614
|
-
ctx.out.log(`${entity} \u2014 ${
|
|
9659
|
+
const page2 = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
|
|
9660
|
+
emit2(ctx, page2, () => {
|
|
9661
|
+
ctx.out.log(`${entity} \u2014 ${page2.records.length} of ${page2.total}`);
|
|
9615
9662
|
ctx.out.log("id state app title");
|
|
9616
|
-
for (const r of
|
|
9663
|
+
for (const r of page2.records) printRecord(ctx, entity, r);
|
|
9617
9664
|
});
|
|
9618
9665
|
}
|
|
9619
9666
|
async function pmAdd(ctx, entity, parsed) {
|
|
@@ -9634,6 +9681,17 @@ async function pmGet(ctx, entity, id) {
|
|
|
9634
9681
|
const { record: record10 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
|
|
9635
9682
|
emit2(ctx, record10, () => printRecord(ctx, entity, record10));
|
|
9636
9683
|
}
|
|
9684
|
+
async function pmReference(ctx, entity, id) {
|
|
9685
|
+
const { record: record10 } = await pmRequest(
|
|
9686
|
+
ctx,
|
|
9687
|
+
"GET",
|
|
9688
|
+
`/${entity}/${encodeURIComponent(id)}`
|
|
9689
|
+
);
|
|
9690
|
+
const markup = referenceMarkup(entity, record10);
|
|
9691
|
+
emit2(ctx, { kind: `pm:${entity}`, id: record10.id, label: record10.title ?? "", markup }, () => {
|
|
9692
|
+
ctx.out.log(markup);
|
|
9693
|
+
});
|
|
9694
|
+
}
|
|
9637
9695
|
async function pmSet(ctx, entity, id, parsed) {
|
|
9638
9696
|
const patch2 = collectEntityFields(entity, parsed, true);
|
|
9639
9697
|
if (Object.keys(patch2).length === 0)
|
|
@@ -9654,6 +9712,36 @@ async function pmDone(ctx, entity, id, parsed) {
|
|
|
9654
9712
|
});
|
|
9655
9713
|
emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
|
|
9656
9714
|
}
|
|
9715
|
+
async function pmTaskLifecycle(ctx, id, action2, parsed) {
|
|
9716
|
+
const rawRevision = stringOpt(parsed.options["expected-revision"]);
|
|
9717
|
+
const expectedRevision = Number(rawRevision);
|
|
9718
|
+
if (!rawRevision || !Number.isSafeInteger(expectedRevision) || expectedRevision < 1) {
|
|
9719
|
+
throw new Error(`pm task ${action2} needs --expected-revision <n>`);
|
|
9720
|
+
}
|
|
9721
|
+
const mutationId = writeMutationId2(parsed);
|
|
9722
|
+
const res = action2 === "ready" ? await pmRequest(
|
|
9723
|
+
ctx,
|
|
9724
|
+
"PATCH",
|
|
9725
|
+
`/task/${encodeURIComponent(id)}`,
|
|
9726
|
+
{
|
|
9727
|
+
patch: {
|
|
9728
|
+
...collectEntityFields("task", parsed, true),
|
|
9729
|
+
column: "todo",
|
|
9730
|
+
expectedRevision
|
|
9731
|
+
},
|
|
9732
|
+
mutationId
|
|
9733
|
+
}
|
|
9734
|
+
) : await pmRequest(
|
|
9735
|
+
ctx,
|
|
9736
|
+
"POST",
|
|
9737
|
+
`/task/${encodeURIComponent(id)}/${action2}`,
|
|
9738
|
+
{ expectedRevision, mutationId }
|
|
9739
|
+
);
|
|
9740
|
+
emit2(ctx, res, () => {
|
|
9741
|
+
const state2 = res.record ? statusCol("task", res.record) : action2;
|
|
9742
|
+
ctx.out.log(`task ${id} \u2192 ${state2}`);
|
|
9743
|
+
});
|
|
9744
|
+
}
|
|
9657
9745
|
async function allRecords(ctx, entity, appId) {
|
|
9658
9746
|
const records = [];
|
|
9659
9747
|
for (; ; ) {
|
|
@@ -9662,11 +9750,48 @@ async function allRecords(ctx, entity, appId) {
|
|
|
9662
9750
|
limit: "100",
|
|
9663
9751
|
offset: String(records.length)
|
|
9664
9752
|
});
|
|
9665
|
-
const
|
|
9666
|
-
records.push(...
|
|
9667
|
-
if (records.length >=
|
|
9753
|
+
const page2 = await pmRequest(ctx, "GET", `/${entity}?${q}`);
|
|
9754
|
+
records.push(...page2.records);
|
|
9755
|
+
if (records.length >= page2.total || page2.records.length === 0) return records;
|
|
9668
9756
|
}
|
|
9669
9757
|
}
|
|
9758
|
+
async function pmNext(ctx, parsed) {
|
|
9759
|
+
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
9760
|
+
if (!appId) throw new Error("pm next needs --app <appId>");
|
|
9761
|
+
const [goals, tasks] = await Promise.all([
|
|
9762
|
+
allRecords(ctx, "goal", appId),
|
|
9763
|
+
allRecords(ctx, "task", appId)
|
|
9764
|
+
]);
|
|
9765
|
+
const result = {
|
|
9766
|
+
appId,
|
|
9767
|
+
openGoals: goals.filter((record10) => record10.status === "open"),
|
|
9768
|
+
doing: tasks.filter((record10) => record10.column === "doing"),
|
|
9769
|
+
ready: tasks.filter((record10) => record10.column === "todo")
|
|
9770
|
+
};
|
|
9771
|
+
emit2(ctx, result, () => {
|
|
9772
|
+
ctx.out.log(`${appId}: goal-aligned work intake (read only)`);
|
|
9773
|
+
for (const [label, records] of [
|
|
9774
|
+
["doing", result.doing],
|
|
9775
|
+
["ready", result.ready],
|
|
9776
|
+
["open goals", result.openGoals]
|
|
9777
|
+
]) {
|
|
9778
|
+
ctx.out.log(`${label}:`);
|
|
9779
|
+
if (!records.length) ctx.out.log("- (none)");
|
|
9780
|
+
else for (const record10 of records) printRecord(
|
|
9781
|
+
ctx,
|
|
9782
|
+
label === "open goals" ? "goal" : "task",
|
|
9783
|
+
record10
|
|
9784
|
+
);
|
|
9785
|
+
}
|
|
9786
|
+
if (!result.openGoals.length) {
|
|
9787
|
+
ctx.out.log("next: discuss alignment with the user before creating or claiming project work");
|
|
9788
|
+
} else if (!result.ready.length) {
|
|
9789
|
+
ctx.out.log("next: refine a linked Backlog task and mark it Ready");
|
|
9790
|
+
} else {
|
|
9791
|
+
ctx.out.log("next: review a Ready task, then claim it with its revision");
|
|
9792
|
+
}
|
|
9793
|
+
});
|
|
9794
|
+
}
|
|
9670
9795
|
async function pmHandoff(ctx, parsed) {
|
|
9671
9796
|
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
9672
9797
|
if (!appId) throw new Error("pm handoff needs --app <appId>");
|
|
@@ -9706,6 +9831,12 @@ async function pmHandoff(ctx, parsed) {
|
|
|
9706
9831
|
}
|
|
9707
9832
|
});
|
|
9708
9833
|
}
|
|
9834
|
+
async function pmRemove(ctx, entity, id) {
|
|
9835
|
+
await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id)}`);
|
|
9836
|
+
ctx.out.log(`deleted ${entity} ${id}`);
|
|
9837
|
+
}
|
|
9838
|
+
|
|
9839
|
+
// src/pm-comments.ts
|
|
9709
9840
|
async function pmComment(ctx, entity, id, parsed) {
|
|
9710
9841
|
const body = stringOpt(parsed.options.body);
|
|
9711
9842
|
if (!body) throw new Error('pm comment needs --body "..."');
|
|
@@ -9716,19 +9847,218 @@ async function pmComment(ctx, entity, id, parsed) {
|
|
|
9716
9847
|
ctx.out.log(`commented on ${entity} ${id}`);
|
|
9717
9848
|
}
|
|
9718
9849
|
async function pmComments(ctx, entity, id) {
|
|
9719
|
-
const { messages } = await pmRequest(
|
|
9720
|
-
ctx,
|
|
9721
|
-
"GET",
|
|
9722
|
-
`/${entity}/${encodeURIComponent(id)}/comments`
|
|
9723
|
-
);
|
|
9850
|
+
const { messages } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}/comments`);
|
|
9724
9851
|
emit2(ctx, messages, () => {
|
|
9725
9852
|
if (messages.length === 0) ctx.out.log("(no comments)");
|
|
9726
|
-
else for (const
|
|
9853
|
+
else for (const message2 of messages) {
|
|
9854
|
+
ctx.out.log(
|
|
9855
|
+
`[${message2.authorId ?? "?"}] ${message2.markup ?? message2.body ?? ""}`
|
|
9856
|
+
);
|
|
9857
|
+
}
|
|
9727
9858
|
});
|
|
9728
9859
|
}
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9860
|
+
|
|
9861
|
+
// src/pm-watch-types.ts
|
|
9862
|
+
var PmWatchCheckpointError = class extends Error {
|
|
9863
|
+
constructor(cursor, streamId) {
|
|
9864
|
+
super("PM work cursor requires a new checkpoint");
|
|
9865
|
+
this.cursor = cursor;
|
|
9866
|
+
this.streamId = streamId;
|
|
9867
|
+
this.name = "PmWatchCheckpointError";
|
|
9868
|
+
}
|
|
9869
|
+
cursor;
|
|
9870
|
+
streamId;
|
|
9871
|
+
code = "checkpoint_required";
|
|
9872
|
+
};
|
|
9873
|
+
var PmWatchRequestError = class extends Error {
|
|
9874
|
+
constructor(message2, retryable, status) {
|
|
9875
|
+
super(message2);
|
|
9876
|
+
this.retryable = retryable;
|
|
9877
|
+
this.status = status;
|
|
9878
|
+
this.name = "PmWatchRequestError";
|
|
9879
|
+
}
|
|
9880
|
+
retryable;
|
|
9881
|
+
status;
|
|
9882
|
+
};
|
|
9883
|
+
|
|
9884
|
+
// src/pm-watch.ts
|
|
9885
|
+
var DEFAULT_INTERVAL_MS2 = 15e3;
|
|
9886
|
+
var MAX_CONSECUTIVE_FAILURES2 = 5;
|
|
9887
|
+
var MAX_BACKOFF_MS2 = 3e4;
|
|
9888
|
+
function positiveNumber(parsed, flag, fallback) {
|
|
9889
|
+
const raw = stringOpt(parsed.options[flag]);
|
|
9890
|
+
const value2 = raw == null ? NaN : Number(raw);
|
|
9891
|
+
return Number.isFinite(value2) && value2 > 0 ? value2 : fallback;
|
|
9892
|
+
}
|
|
9893
|
+
function jsonl2(ctx, parsed, value2) {
|
|
9894
|
+
if (parsed.options.jsonl === true) ctx.out.log(JSON.stringify({ v: 1, ...value2 }));
|
|
9895
|
+
}
|
|
9896
|
+
async function page(ctx, appId, cursor) {
|
|
9897
|
+
const params = new URLSearchParams({ app: appId });
|
|
9898
|
+
if (cursor) params.set("cursor", cursor);
|
|
9899
|
+
let response2;
|
|
9900
|
+
try {
|
|
9901
|
+
response2 = await ctx.doFetch(`${ctx.platformUrl}/registry/pm/work/watch?${params}`, {
|
|
9902
|
+
headers: { authorization: `Bearer ${ctx.token}` }
|
|
9903
|
+
});
|
|
9904
|
+
} catch (error) {
|
|
9905
|
+
throw new PmWatchRequestError(
|
|
9906
|
+
`pm watch request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
9907
|
+
true
|
|
9908
|
+
);
|
|
9909
|
+
}
|
|
9910
|
+
const data = await response2.json().catch(() => ({}));
|
|
9911
|
+
if (response2.status === 409 && data.code === "checkpoint_required") {
|
|
9912
|
+
throw new PmWatchCheckpointError(data.cursor, data.streamId);
|
|
9913
|
+
}
|
|
9914
|
+
if (!response2.ok) {
|
|
9915
|
+
throw new PmWatchRequestError(
|
|
9916
|
+
`pm watch failed: ${data.error ?? `registry returned ${response2.status}`}`,
|
|
9917
|
+
response2.status === 429 || response2.status >= 500,
|
|
9918
|
+
response2.status
|
|
9919
|
+
);
|
|
9920
|
+
}
|
|
9921
|
+
return data;
|
|
9922
|
+
}
|
|
9923
|
+
function recordState(record10) {
|
|
9924
|
+
if (record10.column) return record10.column === "todo" ? "ready" : record10.column;
|
|
9925
|
+
return String(record10.status ?? "");
|
|
9926
|
+
}
|
|
9927
|
+
function eventRecord(event) {
|
|
9928
|
+
return event.payload.payload;
|
|
9929
|
+
}
|
|
9930
|
+
function eventLabel(event) {
|
|
9931
|
+
const record10 = eventRecord(event);
|
|
9932
|
+
if (record10) return String(record10.title ?? event.payload.entityId);
|
|
9933
|
+
const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
|
|
9934
|
+
return body || event.payload.entityId;
|
|
9935
|
+
}
|
|
9936
|
+
function report3(ctx, parsed, result) {
|
|
9937
|
+
if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
|
|
9938
|
+
else if (parsed.options.jsonl !== true && result.found) {
|
|
9939
|
+
for (const event of result.events ?? []) {
|
|
9940
|
+
const record10 = eventRecord(event);
|
|
9941
|
+
const state2 = record10 ? recordState(record10) : "comment";
|
|
9942
|
+
ctx.out.log(
|
|
9943
|
+
`${event.id} ${event.type} ${state2}${record10?.revision ? `; r${record10.revision}` : ""} ${eventLabel(event)}`
|
|
9944
|
+
);
|
|
9945
|
+
}
|
|
9946
|
+
}
|
|
9947
|
+
return result;
|
|
9948
|
+
}
|
|
9949
|
+
async function pmWatch(ctx, parsed) {
|
|
9950
|
+
if (ctx.json && parsed.options.jsonl === true) {
|
|
9951
|
+
throw new Error("--json and --jsonl cannot be combined");
|
|
9952
|
+
}
|
|
9953
|
+
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
9954
|
+
if (!appId) throw new Error("pm watch needs --app <appId>");
|
|
9955
|
+
const sleep = ctx.sleep ?? ((ms) => new Promise((resolve13) => setTimeout(resolve13, ms)));
|
|
9956
|
+
const now = ctx.now ?? Date.now;
|
|
9957
|
+
const intervalMs = (positiveNumber(parsed, "interval", DEFAULT_INTERVAL_MS2 / 1e3) ?? DEFAULT_INTERVAL_MS2 / 1e3) * 1e3;
|
|
9958
|
+
const timeoutSeconds = positiveNumber(parsed, "timeout");
|
|
9959
|
+
const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
|
|
9960
|
+
const entity = stringOpt(parsed.options.entity);
|
|
9961
|
+
const action2 = stringOpt(parsed.options.action);
|
|
9962
|
+
const wantedState = stringOpt(parsed.options.state)?.toLowerCase();
|
|
9963
|
+
const by = stringOpt(parsed.options.by);
|
|
9964
|
+
const self = stringOpt(parsed.options.self);
|
|
9965
|
+
let cursor = stringOpt(parsed.options.cursor);
|
|
9966
|
+
let firstSuccess = true;
|
|
9967
|
+
let consecutiveFailures = 0;
|
|
9968
|
+
for (; ; ) {
|
|
9969
|
+
let current;
|
|
9970
|
+
try {
|
|
9971
|
+
current = await page(ctx, appId, cursor);
|
|
9972
|
+
consecutiveFailures = 0;
|
|
9973
|
+
} catch (error) {
|
|
9974
|
+
if (error instanceof PmWatchCheckpointError) {
|
|
9975
|
+
jsonl2(ctx, parsed, {
|
|
9976
|
+
type: "status",
|
|
9977
|
+
state: "checkpoint_required",
|
|
9978
|
+
retryable: false,
|
|
9979
|
+
...error.cursor ? { cursor: error.cursor } : {},
|
|
9980
|
+
...error.streamId ? { streamId: error.streamId } : {}
|
|
9981
|
+
});
|
|
9982
|
+
throw error;
|
|
9983
|
+
}
|
|
9984
|
+
if (!(error instanceof PmWatchRequestError) || !error.retryable) throw error;
|
|
9985
|
+
consecutiveFailures++;
|
|
9986
|
+
jsonl2(ctx, parsed, {
|
|
9987
|
+
type: "status",
|
|
9988
|
+
state: "degraded",
|
|
9989
|
+
retryable: true,
|
|
9990
|
+
attempt: consecutiveFailures,
|
|
9991
|
+
...cursor ? { cursor } : {},
|
|
9992
|
+
...error.status ? { status: error.status } : {}
|
|
9993
|
+
});
|
|
9994
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
|
|
9995
|
+
if (deadline !== void 0 && now() >= deadline) {
|
|
9996
|
+
return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
9997
|
+
}
|
|
9998
|
+
const backoff = Math.min(
|
|
9999
|
+
MAX_BACKOFF_MS2,
|
|
10000
|
+
Math.min(intervalMs, 1e3) * 2 ** (consecutiveFailures - 1)
|
|
10001
|
+
);
|
|
10002
|
+
await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
|
|
10003
|
+
continue;
|
|
10004
|
+
}
|
|
10005
|
+
cursor = current.cursor;
|
|
10006
|
+
const baseline = firstSuccess && current.events.length === 0;
|
|
10007
|
+
if (baseline) {
|
|
10008
|
+
jsonl2(ctx, parsed, {
|
|
10009
|
+
type: "checkpoint",
|
|
10010
|
+
streamId: current.streamId,
|
|
10011
|
+
cursor,
|
|
10012
|
+
serverTime: current.serverTime
|
|
10013
|
+
});
|
|
10014
|
+
}
|
|
10015
|
+
firstSuccess = false;
|
|
10016
|
+
const matching = current.events.filter((event) => {
|
|
10017
|
+
const record10 = eventRecord(event);
|
|
10018
|
+
const state2 = record10 ? recordState(record10).toLowerCase() : "";
|
|
10019
|
+
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);
|
|
10020
|
+
});
|
|
10021
|
+
for (const event of matching) {
|
|
10022
|
+
jsonl2(ctx, parsed, {
|
|
10023
|
+
type: "event",
|
|
10024
|
+
streamId: current.streamId,
|
|
10025
|
+
eventId: event.id,
|
|
10026
|
+
cursor: event.cursor,
|
|
10027
|
+
event
|
|
10028
|
+
});
|
|
10029
|
+
}
|
|
10030
|
+
if (matching.length > 0) {
|
|
10031
|
+
jsonl2(ctx, parsed, {
|
|
10032
|
+
type: "checkpoint",
|
|
10033
|
+
streamId: current.streamId,
|
|
10034
|
+
cursor,
|
|
10035
|
+
serverTime: current.serverTime
|
|
10036
|
+
});
|
|
10037
|
+
return report3(ctx, parsed, { found: true, cursor, events: matching });
|
|
10038
|
+
}
|
|
10039
|
+
if (current.events.length > 0) {
|
|
10040
|
+
jsonl2(ctx, parsed, {
|
|
10041
|
+
type: "checkpoint",
|
|
10042
|
+
streamId: current.streamId,
|
|
10043
|
+
cursor,
|
|
10044
|
+
serverTime: current.serverTime
|
|
10045
|
+
});
|
|
10046
|
+
} else if (!baseline) {
|
|
10047
|
+
jsonl2(ctx, parsed, {
|
|
10048
|
+
type: "heartbeat",
|
|
10049
|
+
streamId: current.streamId,
|
|
10050
|
+
cursor,
|
|
10051
|
+
serverTime: current.serverTime
|
|
10052
|
+
});
|
|
10053
|
+
}
|
|
10054
|
+
if (current.hasMore) continue;
|
|
10055
|
+
if (deadline !== void 0 && now() >= deadline) {
|
|
10056
|
+
return report3(ctx, parsed, { found: false, cursor });
|
|
10057
|
+
}
|
|
10058
|
+
await sleep(
|
|
10059
|
+
deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
|
|
10060
|
+
);
|
|
10061
|
+
}
|
|
9732
10062
|
}
|
|
9733
10063
|
|
|
9734
10064
|
// src/pm-command.ts
|
|
@@ -9749,7 +10079,11 @@ var ACTION_OPTIONS = {
|
|
|
9749
10079
|
done: ["mutation-id"],
|
|
9750
10080
|
comment: ["body", "mutation-id"],
|
|
9751
10081
|
comments: [],
|
|
9752
|
-
rm: []
|
|
10082
|
+
rm: [],
|
|
10083
|
+
ready: ["goal", "alignment-decision", "execution", "description", "desc", "body", "acceptance", "expected-revision", "mutation-id"],
|
|
10084
|
+
claim: ["expected-revision", "mutation-id"],
|
|
10085
|
+
release: ["expected-revision", "mutation-id"],
|
|
10086
|
+
ref: []
|
|
9753
10087
|
};
|
|
9754
10088
|
var ENTITY_OPTIONS = {
|
|
9755
10089
|
goal: {
|
|
@@ -9760,8 +10094,8 @@ var ENTITY_OPTIONS = {
|
|
|
9760
10094
|
},
|
|
9761
10095
|
task: {
|
|
9762
10096
|
list: ["column", "goal", "assignee"],
|
|
9763
|
-
add: ["column", "goal", "assignee", "due", "description", "desc", "body"],
|
|
9764
|
-
set: ["title", "column", "rank", "goal", "assignee", "due", "description", "desc", "body"],
|
|
10097
|
+
add: ["column", "goal", "alignment-decision", "execution", "assignee", "due", "description", "desc", "body", "acceptance"],
|
|
10098
|
+
set: ["title", "column", "rank", "goal", "alignment-decision", "execution", "assignee", "due", "description", "desc", "body", "acceptance", "expected-revision"],
|
|
9765
10099
|
done: []
|
|
9766
10100
|
},
|
|
9767
10101
|
decision: {
|
|
@@ -9812,11 +10146,33 @@ async function buildContext2(parsed, deps) {
|
|
|
9812
10146
|
json: parsed.options.json === true,
|
|
9813
10147
|
// Preserve PM's existing cross-project default when a config is present;
|
|
9814
10148
|
// only an explicit remote-agent environment or profile selects an app.
|
|
9815
|
-
appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
|
|
10149
|
+
appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0,
|
|
10150
|
+
...deps.sleep ? { sleep: deps.sleep } : {},
|
|
10151
|
+
...deps.now ? { now: deps.now } : {}
|
|
9816
10152
|
};
|
|
9817
10153
|
}
|
|
9818
10154
|
async function pmCommand(parsed, deps = {}) {
|
|
9819
10155
|
const word = parsed.positionals[1] ?? "";
|
|
10156
|
+
if (word === "next") {
|
|
10157
|
+
assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
|
|
10158
|
+
return pmNext(await buildContext2(parsed, deps), parsed);
|
|
10159
|
+
}
|
|
10160
|
+
if (word === "watch") {
|
|
10161
|
+
assertArgs(parsed, [
|
|
10162
|
+
...COMMON_OPTIONS,
|
|
10163
|
+
"app",
|
|
10164
|
+
"cursor",
|
|
10165
|
+
"interval",
|
|
10166
|
+
"timeout",
|
|
10167
|
+
"jsonl",
|
|
10168
|
+
"entity",
|
|
10169
|
+
"action",
|
|
10170
|
+
"state",
|
|
10171
|
+
"by",
|
|
10172
|
+
"self"
|
|
10173
|
+
], 2);
|
|
10174
|
+
return pmWatch(await buildContext2(parsed, deps), parsed).then(() => void 0);
|
|
10175
|
+
}
|
|
9820
10176
|
if (word === "handoff") {
|
|
9821
10177
|
assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
|
|
9822
10178
|
return pmHandoff(await buildContext2(parsed, deps), parsed);
|
|
@@ -9827,6 +10183,9 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
9827
10183
|
const action2 = canonicalAction(requestedAction);
|
|
9828
10184
|
if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|comment|comments|rm.`);
|
|
9829
10185
|
assertArgs(parsed, allowedOptions(entity, action2), 4);
|
|
10186
|
+
if ((action2 === "ready" || action2 === "claim" || action2 === "release") && entity !== "task") {
|
|
10187
|
+
throw new Error(`pm ${action2} is only valid for tasks`);
|
|
10188
|
+
}
|
|
9830
10189
|
const ctx = await buildContext2(parsed, deps);
|
|
9831
10190
|
const id = parsed.positionals[3];
|
|
9832
10191
|
switch (action2) {
|
|
@@ -9846,6 +10205,12 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
9846
10205
|
return pmComments(ctx, entity, requireId2(id, action2));
|
|
9847
10206
|
case "rm":
|
|
9848
10207
|
return pmRemove(ctx, entity, requireId2(id, action2));
|
|
10208
|
+
case "ref":
|
|
10209
|
+
return pmReference(ctx, entity, requireId2(id, action2));
|
|
10210
|
+
case "ready":
|
|
10211
|
+
case "claim":
|
|
10212
|
+
case "release":
|
|
10213
|
+
return pmTaskLifecycle(ctx, requireId2(id, action2), action2, parsed);
|
|
9849
10214
|
}
|
|
9850
10215
|
}
|
|
9851
10216
|
|
|
@@ -10735,12 +11100,23 @@ var PM_ACTIONS = {
|
|
|
10735
11100
|
done: {},
|
|
10736
11101
|
comment: {},
|
|
10737
11102
|
comments: {},
|
|
11103
|
+
ref: {},
|
|
10738
11104
|
rm: {},
|
|
10739
11105
|
delete: {}
|
|
10740
11106
|
};
|
|
10741
|
-
var
|
|
10742
|
-
|
|
10743
|
-
|
|
11107
|
+
var PM_TASK_ACTIONS = {
|
|
11108
|
+
...PM_ACTIONS,
|
|
11109
|
+
ready: {},
|
|
11110
|
+
claim: {},
|
|
11111
|
+
release: {}
|
|
11112
|
+
};
|
|
11113
|
+
var PM_ENTITIES = {
|
|
11114
|
+
...Object.fromEntries(
|
|
11115
|
+
["goal", "conformance", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
|
|
11116
|
+
),
|
|
11117
|
+
task: PM_TASK_ACTIONS,
|
|
11118
|
+
kanban: PM_TASK_ACTIONS
|
|
11119
|
+
};
|
|
10744
11120
|
var COMMAND_SURFACE = {
|
|
10745
11121
|
agent: { jobs: {}, retry: {} },
|
|
10746
11122
|
admin: {
|
|
@@ -10793,7 +11169,9 @@ var COMMAND_SURFACE = {
|
|
|
10793
11169
|
},
|
|
10794
11170
|
pm: {
|
|
10795
11171
|
...PM_ENTITIES,
|
|
10796
|
-
handoff: {}
|
|
11172
|
+
handoff: {},
|
|
11173
|
+
next: {},
|
|
11174
|
+
watch: {}
|
|
10797
11175
|
},
|
|
10798
11176
|
provision: {},
|
|
10799
11177
|
runbook: {
|
|
@@ -10966,12 +11344,12 @@ async function call(ctx, method, path, body) {
|
|
|
10966
11344
|
throw new Error(message2);
|
|
10967
11345
|
}
|
|
10968
11346
|
async function bySlug(ctx, slug) {
|
|
10969
|
-
const
|
|
11347
|
+
const page2 = await call(
|
|
10970
11348
|
ctx,
|
|
10971
11349
|
"GET",
|
|
10972
11350
|
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
|
|
10973
11351
|
);
|
|
10974
|
-
const found =
|
|
11352
|
+
const found = page2.records[0];
|
|
10975
11353
|
if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
|
|
10976
11354
|
return found;
|
|
10977
11355
|
}
|
|
@@ -10985,11 +11363,11 @@ async function runbookList(ctx, all, query) {
|
|
|
10985
11363
|
const params = new URLSearchParams();
|
|
10986
11364
|
if (!all) params.set("app", ctx.appId);
|
|
10987
11365
|
if (query) params.set("q", query);
|
|
10988
|
-
const
|
|
10989
|
-
if (ctx.json) return ctx.out.log(JSON.stringify(
|
|
10990
|
-
if (!
|
|
11366
|
+
const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
|
|
11367
|
+
if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
|
|
11368
|
+
if (!page2.records.length) return ctx.out.log("(no runbooks)");
|
|
10991
11369
|
ctx.out.log(["SLUG", "STATUS", "V", "SCOPE", "UPDATED", "TITLE"].join(" "));
|
|
10992
|
-
for (const r of
|
|
11370
|
+
for (const r of page2.records)
|
|
10993
11371
|
ctx.out.log(
|
|
10994
11372
|
[r.slug, r.status, `v${r.version}`, r.appId === PLATFORM_SCOPE ? "platform" : r.appId, stamp(r.updatedAt), r.title].join(" ")
|
|
10995
11373
|
);
|
|
@@ -11044,12 +11422,12 @@ async function runbookVisibility(ctx, slug, visibility) {
|
|
|
11044
11422
|
}
|
|
11045
11423
|
async function runbookHistory(ctx, slug) {
|
|
11046
11424
|
const runbook = await bySlug(ctx, slug);
|
|
11047
|
-
const
|
|
11048
|
-
if (ctx.json) return ctx.out.log(JSON.stringify(
|
|
11425
|
+
const page2 = await call(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
|
|
11426
|
+
if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
|
|
11049
11427
|
ctx.out.log(`${slug} is at v${runbook.version}`);
|
|
11050
|
-
if (!
|
|
11428
|
+
if (!page2.records.length) return ctx.out.log("(no earlier versions)");
|
|
11051
11429
|
ctx.out.log(["V", "WHEN", "BY", "NOTE"].join(" "));
|
|
11052
|
-
for (const r of
|
|
11430
|
+
for (const r of page2.records)
|
|
11053
11431
|
ctx.out.log([`v${r.version}`, stamp(r.createdAt), r.ownerEmail ?? "", r.note ?? ""].join(" "));
|
|
11054
11432
|
}
|
|
11055
11433
|
async function runbookRevert(ctx, slug, version) {
|
|
@@ -11124,12 +11502,12 @@ ${counts.created} created, ${counts.updated} updated, ${counts.unchanged} unchan
|
|
|
11124
11502
|
);
|
|
11125
11503
|
}
|
|
11126
11504
|
async function upsert(ctx, r, visibility) {
|
|
11127
|
-
const
|
|
11505
|
+
const page2 = await call(
|
|
11128
11506
|
ctx,
|
|
11129
11507
|
"GET",
|
|
11130
11508
|
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(r.slug)}&limit=1`
|
|
11131
11509
|
);
|
|
11132
|
-
const found =
|
|
11510
|
+
const found = page2.records[0];
|
|
11133
11511
|
if (!found) {
|
|
11134
11512
|
await call(ctx, "POST", "/runbook", {
|
|
11135
11513
|
appId: ctx.appId,
|
|
@@ -11385,7 +11763,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
11385
11763
|
return out;
|
|
11386
11764
|
}
|
|
11387
11765
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
11388
|
-
function
|
|
11766
|
+
function report4(ctx, impacts) {
|
|
11389
11767
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
11390
11768
|
ctx.out.log(
|
|
11391
11769
|
`${impacts.length} changed surface${impacts.length === 1 ? "" : "s"}; ${covered.length} covered by a runbook. Reread each one and fix any step this change made wrong.`
|
|
@@ -11423,7 +11801,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
11423
11801
|
}
|
|
11424
11802
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
11425
11803
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
11426
|
-
|
|
11804
|
+
report4(ctx, impacts);
|
|
11427
11805
|
}
|
|
11428
11806
|
|
|
11429
11807
|
// src/runbook-lint.ts
|
|
@@ -11461,17 +11839,17 @@ function lintRunbook(runbook, installed) {
|
|
|
11461
11839
|
async function runbookLint(ctx, all) {
|
|
11462
11840
|
const params = new URLSearchParams();
|
|
11463
11841
|
if (!all) params.set("app", ctx.appId);
|
|
11464
|
-
const
|
|
11842
|
+
const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
|
|
11465
11843
|
const installed = { "@odla-ai/cli": cliVersion() };
|
|
11466
|
-
const findings =
|
|
11467
|
-
if (ctx.json) return ctx.out.log(JSON.stringify({ checked:
|
|
11468
|
-
if (!
|
|
11844
|
+
const findings = page2.records.flatMap((runbook) => lintRunbook(runbook, installed));
|
|
11845
|
+
if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page2.records.length, findings }, null, 2));
|
|
11846
|
+
if (!page2.records.length) return ctx.out.log("(no runbooks in scope)");
|
|
11469
11847
|
if (!findings.length) {
|
|
11470
11848
|
return ctx.out.log(
|
|
11471
|
-
`${
|
|
11849
|
+
`${page2.records.length} runbook${page2.records.length === 1 ? "" : "s"} checked; every command they name is real for @odla-ai/cli ${cliVersion()}.`
|
|
11472
11850
|
);
|
|
11473
11851
|
}
|
|
11474
|
-
ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${
|
|
11852
|
+
ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${page2.records.length} runbooks:`);
|
|
11475
11853
|
for (const finding of findings) {
|
|
11476
11854
|
const scope = finding.appId === PLATFORM_SCOPE ? "" : ` --app ${finding.appId}`;
|
|
11477
11855
|
ctx.out.log("");
|
|
@@ -11534,12 +11912,12 @@ async function runbookAsk(ctx, question, all) {
|
|
|
11534
11912
|
ctx.out.log(JSDOC_POINTER);
|
|
11535
11913
|
}
|
|
11536
11914
|
async function runbookComment(ctx, slug, body) {
|
|
11537
|
-
const
|
|
11915
|
+
const page2 = await call(
|
|
11538
11916
|
ctx,
|
|
11539
11917
|
"GET",
|
|
11540
11918
|
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
|
|
11541
11919
|
);
|
|
11542
|
-
const found =
|
|
11920
|
+
const found = page2.records[0];
|
|
11543
11921
|
if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
|
|
11544
11922
|
await call(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
|
|
11545
11923
|
ctx.out.log(`commented on ${slug} (v${found.version})`);
|
|
@@ -11591,12 +11969,12 @@ var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
|
|
|
11591
11969
|
|
|
11592
11970
|
// src/runbook-edit-flow.ts
|
|
11593
11971
|
async function editRunbook(ctx, slug, deps = {}) {
|
|
11594
|
-
const
|
|
11972
|
+
const page2 = await call(
|
|
11595
11973
|
ctx,
|
|
11596
11974
|
"GET",
|
|
11597
11975
|
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
|
|
11598
11976
|
);
|
|
11599
|
-
const found =
|
|
11977
|
+
const found = page2.records[0];
|
|
11600
11978
|
if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
|
|
11601
11979
|
ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
|
|
11602
11980
|
const body = await editText(found.body, slug, deps);
|
|
@@ -12017,31 +12395,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
12017
12395
|
url.searchParams.set("job", job.jobId);
|
|
12018
12396
|
out.log(` Studio: ${url.toString()}`);
|
|
12019
12397
|
}
|
|
12020
|
-
function printHostedReport(out,
|
|
12021
|
-
out.log(`security report ${
|
|
12022
|
-
out.log(` coverage: ${
|
|
12023
|
-
out.log(` findings: confirmed=${
|
|
12024
|
-
out.log(` discovery: ${
|
|
12025
|
-
out.log(` validation: ${
|
|
12026
|
-
for (const finding of
|
|
12398
|
+
function printHostedReport(out, report5) {
|
|
12399
|
+
out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
|
|
12400
|
+
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}`);
|
|
12401
|
+
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
|
|
12402
|
+
out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
|
|
12403
|
+
out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
|
|
12404
|
+
for (const finding of report5.findings) {
|
|
12027
12405
|
const location = finding.locations[0];
|
|
12028
12406
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
12029
12407
|
}
|
|
12030
|
-
for (const limitation of
|
|
12408
|
+
for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
|
|
12031
12409
|
}
|
|
12032
|
-
function enforceHostedReportGate(
|
|
12410
|
+
function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
|
|
12033
12411
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
12034
12412
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
12035
12413
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
12036
12414
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
12037
|
-
const confirmed =
|
|
12038
|
-
const leads = failOnCandidates ?
|
|
12039
|
-
const incomplete =
|
|
12415
|
+
const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
12416
|
+
const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
12417
|
+
const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
12040
12418
|
if (confirmed.length || leads.length || incomplete) {
|
|
12041
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
12419
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
|
|
12042
12420
|
}
|
|
12043
12421
|
if (emitSuccess) {
|
|
12044
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
12422
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
|
|
12045
12423
|
}
|
|
12046
12424
|
}
|
|
12047
12425
|
function printHostedSecurityPlanRoute(out, label, route2) {
|
|
@@ -12124,17 +12502,17 @@ async function runHostedSecurity(options) {
|
|
|
12124
12502
|
allowNetwork: false
|
|
12125
12503
|
}
|
|
12126
12504
|
});
|
|
12127
|
-
const
|
|
12128
|
-
await (0, import_node3.writeSecurityArtifacts)(output,
|
|
12129
|
-
const reportDigest = await (0, import_security.securityFingerprint)(
|
|
12505
|
+
const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
12506
|
+
await (0, import_node3.writeSecurityArtifacts)(output, report5);
|
|
12507
|
+
const reportDigest = await (0, import_security.securityFingerprint)(report5);
|
|
12130
12508
|
await hosted.complete({
|
|
12131
12509
|
reportDigest,
|
|
12132
|
-
coverageStatus:
|
|
12133
|
-
confirmed:
|
|
12134
|
-
candidates:
|
|
12510
|
+
coverageStatus: report5.coverageStatus,
|
|
12511
|
+
confirmed: report5.metrics.confirmed,
|
|
12512
|
+
candidates: report5.metrics.candidates
|
|
12135
12513
|
}, { signal: options.signal });
|
|
12136
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
12137
|
-
return Object.freeze({ report:
|
|
12514
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
|
|
12515
|
+
return Object.freeze({ report: report5, run: hosted.run, output });
|
|
12138
12516
|
}
|
|
12139
12517
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
12140
12518
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -12160,14 +12538,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
12160
12538
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
12161
12539
|
return { ...profile, maxHuntTasks };
|
|
12162
12540
|
}
|
|
12163
|
-
function printSummary(out, appId, env, run,
|
|
12164
|
-
const complete =
|
|
12541
|
+
function printSummary(out, appId, env, run, report5, output) {
|
|
12542
|
+
const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
|
|
12165
12543
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
12166
12544
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
12167
12545
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
12168
|
-
out.log(` coverage: ${
|
|
12169
|
-
if (
|
|
12170
|
-
out.log(` findings: confirmed=${
|
|
12546
|
+
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}`);
|
|
12547
|
+
if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
|
|
12548
|
+
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
|
|
12171
12549
|
out.log(` report: ${(0, import_node_path19.resolve)(output, "REPORT.md")}`);
|
|
12172
12550
|
}
|
|
12173
12551
|
function formatBudget(usage) {
|
|
@@ -12414,13 +12792,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
12414
12792
|
}
|
|
12415
12793
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
12416
12794
|
}
|
|
12417
|
-
const
|
|
12795
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
12418
12796
|
if (parsed.options.json === true) {
|
|
12419
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
12797
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
|
|
12420
12798
|
} else {
|
|
12421
|
-
printHostedReport(context.stdout,
|
|
12799
|
+
printHostedReport(context.stdout, report5);
|
|
12422
12800
|
}
|
|
12423
|
-
enforceHostedReportGate(
|
|
12801
|
+
enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
|
|
12424
12802
|
}
|
|
12425
12803
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
12426
12804
|
if (parsed.options.source === true) {
|
|
@@ -12487,13 +12865,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
12487
12865
|
});
|
|
12488
12866
|
enforceLocalGate(result.report, parsed);
|
|
12489
12867
|
}
|
|
12490
|
-
function enforceLocalGate(
|
|
12868
|
+
function enforceLocalGate(report5, parsed) {
|
|
12491
12869
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
12492
12870
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
12493
12871
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
12494
|
-
const confirmed = (0, import_security2.findingsAtOrAbove)(
|
|
12495
|
-
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(
|
|
12496
|
-
const incomplete =
|
|
12872
|
+
const confirmed = (0, import_security2.findingsAtOrAbove)(report5, failOn);
|
|
12873
|
+
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
12874
|
+
const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
12497
12875
|
if (confirmed.length || leads.length || incomplete) {
|
|
12498
12876
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
12499
12877
|
}
|
|
@@ -12532,9 +12910,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
12532
12910
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
12533
12911
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
12534
12912
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
12535
|
-
const
|
|
12536
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
12537
|
-
else printHostedReport(context.stdout,
|
|
12913
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId });
|
|
12914
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
|
|
12915
|
+
else printHostedReport(context.stdout, report5);
|
|
12538
12916
|
return;
|
|
12539
12917
|
}
|
|
12540
12918
|
if (sub !== "run") {
|