@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
|
@@ -232,6 +232,7 @@ function handshakeUrl(platformUrl, userCode) {
|
|
|
232
232
|
|
|
233
233
|
// src/token.ts
|
|
234
234
|
import { collectToken, OdlaError, requestToken } from "@odla-ai/db";
|
|
235
|
+
import { createHash } from "crypto";
|
|
235
236
|
import process5 from "process";
|
|
236
237
|
|
|
237
238
|
// src/handshake-state.ts
|
|
@@ -360,6 +361,7 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
360
361
|
endpoint: ctx.cfg.platformUrl,
|
|
361
362
|
email: ctx.email,
|
|
362
363
|
label: `${ctx.cfg.app.id} provisioner`,
|
|
364
|
+
agentHandle: projectAgentHandle(ctx.cfg.app.id),
|
|
363
365
|
projectIds: [ctx.cfg.app.id],
|
|
364
366
|
fetch: ctx.doFetch,
|
|
365
367
|
waitMs,
|
|
@@ -395,6 +397,12 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
395
397
|
stopReminder?.();
|
|
396
398
|
}
|
|
397
399
|
}
|
|
400
|
+
function projectAgentHandle(appId) {
|
|
401
|
+
const candidate = /^[a-z]/.test(appId) ? appId : `app-${appId}`;
|
|
402
|
+
if (candidate.length <= 32) return candidate;
|
|
403
|
+
const digest = createHash("sha256").update(appId).digest("hex").slice(0, 8);
|
|
404
|
+
return `${candidate.slice(0, 23).replace(/-+$/, "")}-${digest}`;
|
|
405
|
+
}
|
|
398
406
|
function stillPending(pending, email) {
|
|
399
407
|
return new OdlaError(
|
|
400
408
|
"handshake_pending",
|
|
@@ -1482,8 +1490,8 @@ async function calendarCalendars(options) {
|
|
|
1482
1490
|
async function calendarConnect(options) {
|
|
1483
1491
|
const { cfg, ctx, out } = await lifecycleContext(options);
|
|
1484
1492
|
productionConsent(ctx.env, options.yes, "connect calendar");
|
|
1485
|
-
const
|
|
1486
|
-
const applied =
|
|
1493
|
+
const page2 = calendarBookingPageUrl(cfg, ctx.env);
|
|
1494
|
+
const applied = page2 === void 0 ? await readCalendarStatus(ctx) : await applyCalendarSettings(ctx, page2);
|
|
1487
1495
|
const connectOptions = connectionOptions(options, out);
|
|
1488
1496
|
return await continueConnectedCalendar(ctx, applied, connectOptions) ?? connectWithContext(ctx, connectOptions);
|
|
1489
1497
|
}
|
|
@@ -1830,12 +1838,12 @@ import {
|
|
|
1830
1838
|
import { readFileSync as readFileSync4 } from "fs";
|
|
1831
1839
|
|
|
1832
1840
|
// src/config-reconcile-digest.ts
|
|
1833
|
-
import { createHash } from "crypto";
|
|
1841
|
+
import { createHash as createHash2 } from "crypto";
|
|
1834
1842
|
function canonicalJson(value2) {
|
|
1835
1843
|
return JSON.stringify(canonicalValue(value2));
|
|
1836
1844
|
}
|
|
1837
1845
|
function configDigest(value2) {
|
|
1838
|
-
return `sha256:${
|
|
1846
|
+
return `sha256:${createHash2("sha256").update(canonicalJson(value2)).digest("hex")}`;
|
|
1839
1847
|
}
|
|
1840
1848
|
function canonicalValue(value2) {
|
|
1841
1849
|
if (value2 === null || typeof value2 === "string" || typeof value2 === "boolean") return value2;
|
|
@@ -3294,9 +3302,10 @@ For work that creates an odla app or adds odla services, read and follow
|
|
|
3294
3302
|
\`.agents/skills/odla-migrate/SKILL.md\`. For production telemetry triage, use
|
|
3295
3303
|
\`.agents/skills/odla-o11y-debug/SKILL.md\`.
|
|
3296
3304
|
|
|
3297
|
-
Track the work in odla's PM as you go
|
|
3298
|
-
|
|
3299
|
-
|
|
3305
|
+
Track the work in odla's PM as you go. Before project-mutating work, run
|
|
3306
|
+
\`npx @odla-ai/cli pm next --app <appId>\`, confirm alignment to an open goal,
|
|
3307
|
+
and atomically claim a refined Ready task. Record decisions when you make them
|
|
3308
|
+
and file bugs when you notice them. The conventions and the full command set are
|
|
3300
3309
|
in \`.agents/skills/odla/references/pm.md\`.
|
|
3301
3310
|
|
|
3302
3311
|
The setup runbooks and their references are installed in this repository, pinned
|
|
@@ -3929,7 +3938,7 @@ var CODE_BUILD_RECIPES = Object.freeze([{
|
|
|
3929
3938
|
|
|
3930
3939
|
// src/code-images.ts
|
|
3931
3940
|
import { spawn as spawn3 } from "child_process";
|
|
3932
|
-
import { createHash as
|
|
3941
|
+
import { createHash as createHash3 } from "crypto";
|
|
3933
3942
|
import { copyFile, mkdtemp, readFile, rm, writeFile } from "fs/promises";
|
|
3934
3943
|
import { tmpdir } from "os";
|
|
3935
3944
|
import { join as join8 } from "path";
|
|
@@ -3988,7 +3997,7 @@ async function embeddedPiImageName() {
|
|
|
3988
3997
|
const bundle = await readFile(embeddedPiAssetPath()).catch(() => {
|
|
3989
3998
|
throw new Error("CLI-embedded Pi runtime is missing; reinstall this exact @odla-ai/cli version");
|
|
3990
3999
|
});
|
|
3991
|
-
return `odla-ai/pi-agent:embedded-sha256-${
|
|
4000
|
+
return `odla-ai/pi-agent:embedded-sha256-${createHash3("sha256").update(bundle).digest("hex")}`;
|
|
3992
4001
|
}
|
|
3993
4002
|
async function buildEmbeddedPiImage(engine, image, run) {
|
|
3994
4003
|
const context = await mkdtemp(join8(tmpdir(), "odla-code-pi-"));
|
|
@@ -4611,7 +4620,7 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
|
|
|
4611
4620
|
}
|
|
4612
4621
|
|
|
4613
4622
|
// ../harness/dist/chunk-GMVZ4LZH.js
|
|
4614
|
-
import { createHash as
|
|
4623
|
+
import { createHash as createHash4 } from "crypto";
|
|
4615
4624
|
import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
|
|
4616
4625
|
import { relative as relative4, resolve as resolve7 } from "path";
|
|
4617
4626
|
|
|
@@ -4675,7 +4684,7 @@ function dependenciesOf(values, influence = "data") {
|
|
|
4675
4684
|
return [...unique3.values()];
|
|
4676
4685
|
}
|
|
4677
4686
|
|
|
4678
|
-
// ../camel/dist/chunk-
|
|
4687
|
+
// ../camel/dist/chunk-4DQ6BIHP.js
|
|
4679
4688
|
var camelValueBrand = /* @__PURE__ */ Symbol("@odla-ai/camel/value");
|
|
4680
4689
|
var authenticCamelValues = /* @__PURE__ */ new WeakSet();
|
|
4681
4690
|
function isCamelValue(value2) {
|
|
@@ -4687,13 +4696,13 @@ function isSafe(value2) {
|
|
|
4687
4696
|
function isUnsafe(value2) {
|
|
4688
4697
|
return isCamelValue(value2) && value2.label.promptSafety === "unsafe";
|
|
4689
4698
|
}
|
|
4690
|
-
function createSafeInternal(value2, safeBasis, metadata2) {
|
|
4699
|
+
function createSafeInternal(value2, safeBasis, metadata2, extra) {
|
|
4691
4700
|
return createValue(value2, {
|
|
4692
4701
|
schemaVersion: 1,
|
|
4693
4702
|
promptSafety: "safe",
|
|
4694
4703
|
safeBasis,
|
|
4695
4704
|
...copyMetadata(metadata2)
|
|
4696
|
-
});
|
|
4705
|
+
}, extra);
|
|
4697
4706
|
}
|
|
4698
4707
|
function createUnsafeInternal(value2, metadata2) {
|
|
4699
4708
|
return createValue(value2, {
|
|
@@ -4702,8 +4711,8 @@ function createUnsafeInternal(value2, metadata2) {
|
|
|
4702
4711
|
...copyMetadata(metadata2)
|
|
4703
4712
|
});
|
|
4704
4713
|
}
|
|
4705
|
-
function createValue(value2, label) {
|
|
4706
|
-
const result = { value: value2, label: Object.freeze(label) };
|
|
4714
|
+
function createValue(value2, label, extra) {
|
|
4715
|
+
const result = { value: value2, label: Object.freeze(label), ...extra };
|
|
4707
4716
|
Object.defineProperty(result, camelValueBrand, { value: label.promptSafety, enumerable: false });
|
|
4708
4717
|
authenticCamelValues.add(result);
|
|
4709
4718
|
return Object.freeze(result);
|
|
@@ -4964,7 +4973,7 @@ import { dirname as dirname6, join as join23, resolve as resolve32, sep as sep23
|
|
|
4964
4973
|
import { readFile as readFile22, readdir as readdir22, stat as stat2 } from "fs/promises";
|
|
4965
4974
|
import { relative as relative22, resolve as resolve42 } from "path";
|
|
4966
4975
|
|
|
4967
|
-
// ../camel/dist/chunk-
|
|
4976
|
+
// ../camel/dist/chunk-4EIRFS3A.js
|
|
4968
4977
|
function conversionPolicyDigest(policy) {
|
|
4969
4978
|
return sha256Hex(canonicalJson2(policy));
|
|
4970
4979
|
}
|
|
@@ -5052,10 +5061,18 @@ async function convert(source, policy, value2, counts) {
|
|
|
5052
5061
|
const count = counts.get(countKey) ?? 0;
|
|
5053
5062
|
if (count >= policy.maximumOutputsPerArtifact) throw new CamelError("limit_exceeded", "Conversion output count exceeds its per-source bound.");
|
|
5054
5063
|
counts.set(countKey, count + 1);
|
|
5064
|
+
const conversionRecordId = await sha256Hex(canonicalJson2({ conversionId: policy.conversionId, digest: policy.digest, source: sourceKey, ordinal: count }));
|
|
5065
|
+
const datumId = await sha256Hex(canonicalJson2({ conversionRecordId, value: value2 }));
|
|
5055
5066
|
return createSafeInternal(value2, "atomic_conversion", {
|
|
5056
5067
|
readers: source.label.readers,
|
|
5057
5068
|
provenance: [...source.label.provenance, { kind: "converter", id: policy.conversionId, digest: policy.digest }],
|
|
5058
5069
|
dependencies: dependenciesOf([source])
|
|
5070
|
+
}, {
|
|
5071
|
+
datumId,
|
|
5072
|
+
kind: policy.output.kind,
|
|
5073
|
+
conversionId: policy.conversionId,
|
|
5074
|
+
conversionDigest: policy.digest,
|
|
5075
|
+
conversionRecordId
|
|
5059
5076
|
});
|
|
5060
5077
|
}
|
|
5061
5078
|
function validatePolicyShape(policy) {
|
|
@@ -5120,7 +5137,7 @@ function missingPolicy() {
|
|
|
5120
5137
|
throw new CamelError("conversion_rejected", "Conversion policy is not registered.");
|
|
5121
5138
|
}
|
|
5122
5139
|
|
|
5123
|
-
// ../camel/dist/chunk-
|
|
5140
|
+
// ../camel/dist/chunk-VEAUXH4F.js
|
|
5124
5141
|
function createCamelIngress(constants2 = []) {
|
|
5125
5142
|
const byId = /* @__PURE__ */ new Map();
|
|
5126
5143
|
for (const item of constants2) {
|
|
@@ -5257,7 +5274,7 @@ async function digestStagedWorkspace(root, limits) {
|
|
|
5257
5274
|
}
|
|
5258
5275
|
};
|
|
5259
5276
|
await walk(resolve7(root));
|
|
5260
|
-
const hash =
|
|
5277
|
+
const hash = createHash4("sha256");
|
|
5261
5278
|
let bytes = 0;
|
|
5262
5279
|
for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
5263
5280
|
const content2 = await readFile2(file.target);
|
|
@@ -6896,7 +6913,7 @@ var CodePiRuntimeEngine = class {
|
|
|
6896
6913
|
|
|
6897
6914
|
// src/code-local-source.ts
|
|
6898
6915
|
import { execFile as execFile3 } from "child_process";
|
|
6899
|
-
import { createHash as
|
|
6916
|
+
import { createHash as createHash5 } from "crypto";
|
|
6900
6917
|
var SOURCE_LIMITS2 = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
|
|
6901
6918
|
async function prepareCodeLocalSource(cwd, repository, readHead = readGitHead) {
|
|
6902
6919
|
const headCommitSha = await readHead(cwd);
|
|
@@ -6947,7 +6964,7 @@ async function readGitHead(cwd) {
|
|
|
6947
6964
|
return value2;
|
|
6948
6965
|
}
|
|
6949
6966
|
function digestText(value2) {
|
|
6950
|
-
return `sha256:${
|
|
6967
|
+
return `sha256:${createHash5("sha256").update(value2).digest("hex")}`;
|
|
6951
6968
|
}
|
|
6952
6969
|
|
|
6953
6970
|
// src/code-connect.ts
|
|
@@ -7492,12 +7509,23 @@ var PM_ACTIONS = {
|
|
|
7492
7509
|
done: {},
|
|
7493
7510
|
comment: {},
|
|
7494
7511
|
comments: {},
|
|
7512
|
+
ref: {},
|
|
7495
7513
|
rm: {},
|
|
7496
7514
|
delete: {}
|
|
7497
7515
|
};
|
|
7498
|
-
var
|
|
7499
|
-
|
|
7500
|
-
|
|
7516
|
+
var PM_TASK_ACTIONS = {
|
|
7517
|
+
...PM_ACTIONS,
|
|
7518
|
+
ready: {},
|
|
7519
|
+
claim: {},
|
|
7520
|
+
release: {}
|
|
7521
|
+
};
|
|
7522
|
+
var PM_ENTITIES = {
|
|
7523
|
+
...Object.fromEntries(
|
|
7524
|
+
["goal", "conformance", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
|
|
7525
|
+
),
|
|
7526
|
+
task: PM_TASK_ACTIONS,
|
|
7527
|
+
kanban: PM_TASK_ACTIONS
|
|
7528
|
+
};
|
|
7501
7529
|
var COMMAND_SURFACE = {
|
|
7502
7530
|
agent: { jobs: {}, retry: {} },
|
|
7503
7531
|
admin: {
|
|
@@ -7550,7 +7578,9 @@ var COMMAND_SURFACE = {
|
|
|
7550
7578
|
},
|
|
7551
7579
|
pm: {
|
|
7552
7580
|
...PM_ENTITIES,
|
|
7553
|
-
handoff: {}
|
|
7581
|
+
handoff: {},
|
|
7582
|
+
next: {},
|
|
7583
|
+
watch: {}
|
|
7554
7584
|
},
|
|
7555
7585
|
provision: {},
|
|
7556
7586
|
runbook: {
|
|
@@ -7694,17 +7724,17 @@ async function runHostedSecurity(options) {
|
|
|
7694
7724
|
allowNetwork: false
|
|
7695
7725
|
}
|
|
7696
7726
|
});
|
|
7697
|
-
const
|
|
7698
|
-
await writeSecurityArtifacts(output,
|
|
7699
|
-
const reportDigest = await securityFingerprint(
|
|
7727
|
+
const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
7728
|
+
await writeSecurityArtifacts(output, report5);
|
|
7729
|
+
const reportDigest = await securityFingerprint(report5);
|
|
7700
7730
|
await hosted.complete({
|
|
7701
7731
|
reportDigest,
|
|
7702
|
-
coverageStatus:
|
|
7703
|
-
confirmed:
|
|
7704
|
-
candidates:
|
|
7732
|
+
coverageStatus: report5.coverageStatus,
|
|
7733
|
+
confirmed: report5.metrics.confirmed,
|
|
7734
|
+
candidates: report5.metrics.candidates
|
|
7705
7735
|
}, { signal: options.signal });
|
|
7706
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
7707
|
-
return Object.freeze({ report:
|
|
7736
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
|
|
7737
|
+
return Object.freeze({ report: report5, run: hosted.run, output });
|
|
7708
7738
|
}
|
|
7709
7739
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
7710
7740
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -7730,14 +7760,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
7730
7760
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
7731
7761
|
return { ...profile, maxHuntTasks };
|
|
7732
7762
|
}
|
|
7733
|
-
function printSummary(out, appId, env, run,
|
|
7734
|
-
const complete =
|
|
7763
|
+
function printSummary(out, appId, env, run, report5, output) {
|
|
7764
|
+
const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
|
|
7735
7765
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
7736
7766
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
7737
7767
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
7738
|
-
out.log(` coverage: ${
|
|
7739
|
-
if (
|
|
7740
|
-
out.log(` findings: confirmed=${
|
|
7768
|
+
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}`);
|
|
7769
|
+
if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
|
|
7770
|
+
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
|
|
7741
7771
|
out.log(` report: ${resolve9(output, "REPORT.md")}`);
|
|
7742
7772
|
}
|
|
7743
7773
|
function formatBudget(usage) {
|
|
@@ -9454,16 +9484,22 @@ Usage:
|
|
|
9454
9484
|
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
9455
9485
|
odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
|
|
9456
9486
|
odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
9457
|
-
odla-ai pm task list [--app <id>] [--column <
|
|
9487
|
+
odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
9458
9488
|
odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
9459
9489
|
odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
9460
9490
|
odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
|
|
9461
|
-
odla-ai pm task add --app <id> --title <t> [--column <
|
|
9491
|
+
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]
|
|
9492
|
+
odla-ai pm next --app <id> [--json]
|
|
9493
|
+
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]
|
|
9494
|
+
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]
|
|
9495
|
+
odla-ai pm task claim <id> --expected-revision <n> [--mutation-id <id>] [--json]
|
|
9496
|
+
odla-ai pm task release <id> --expected-revision <n> [--mutation-id <id>] [--json]
|
|
9462
9497
|
odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
|
|
9463
9498
|
odla-ai pm bug add --app <id> --title <t> (--description <text>|--body <text>) [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--mutation-id <id>] [--json]
|
|
9464
9499
|
odla-ai pm <goal|task|decision|bug> get <id> [--json]
|
|
9500
|
+
odla-ai pm <goal|task|decision|bug> ref <id> [--json]
|
|
9465
9501
|
odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
|
|
9466
|
-
odla-ai pm task set <id> [--title <t>|--column <
|
|
9502
|
+
odla-ai pm task set <id> [--title <t>|--column <backlog|ready|doing|review|done>|--rank <n>|--goal <id>|--no-goal|--alignment-decision <id>|--no-alignment-decision|--execution <human|agent|either>|--assignee <id>|--no-assignee|--description <text>|--body <text>|--acceptance <text>|--no-acceptance|--due <epoch-ms>|--no-due|--expected-revision <n>] [--mutation-id <id>] [--json]
|
|
9467
9503
|
odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
|
|
9468
9504
|
odla-ai pm bug set <id> [--title <t>|--status <s>|--severity <s>|--goal <id>|--no-goal|--assignee <id>|--no-assignee|--decision <id>|--no-decision|--description <text>|--body <text>] [--mutation-id <id>] [--json]
|
|
9469
9505
|
odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
|
|
@@ -9782,11 +9818,11 @@ async function discussList(ctx, parsed) {
|
|
|
9782
9818
|
if (value2) query.set(param, value2);
|
|
9783
9819
|
}
|
|
9784
9820
|
const qs = query.toString();
|
|
9785
|
-
const
|
|
9786
|
-
emit(ctx,
|
|
9787
|
-
ctx.out.log(`topics \u2014 ${
|
|
9821
|
+
const page2 = await request(ctx, "GET", `/topics${qs ? `?${qs}` : ""}`);
|
|
9822
|
+
emit(ctx, page2, () => {
|
|
9823
|
+
ctx.out.log(`topics \u2014 ${page2.topics.length} of ${page2.total}`);
|
|
9788
9824
|
ctx.out.log("id state app replies subject");
|
|
9789
|
-
for (const topic of
|
|
9825
|
+
for (const topic of page2.topics) {
|
|
9790
9826
|
ctx.out.log(
|
|
9791
9827
|
`${topic.id} ${state(topic)} ${topic.appId ?? ""} ${topic.replyCount} ${topic.subject}`
|
|
9792
9828
|
);
|
|
@@ -9801,11 +9837,11 @@ async function discussRead(ctx, id, parsed) {
|
|
|
9801
9837
|
limit: requestedLimit ?? "200",
|
|
9802
9838
|
offset: requestedOffset ?? "0"
|
|
9803
9839
|
});
|
|
9804
|
-
const
|
|
9840
|
+
const page2 = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
|
|
9805
9841
|
emit(
|
|
9806
9842
|
ctx,
|
|
9807
|
-
|
|
9808
|
-
() => renderDiscussRead(ctx,
|
|
9843
|
+
page2,
|
|
9844
|
+
() => renderDiscussRead(ctx, page2.topic, page2.posts, page2)
|
|
9809
9845
|
);
|
|
9810
9846
|
return;
|
|
9811
9847
|
}
|
|
@@ -9818,20 +9854,20 @@ async function discussRead(ctx, id, parsed) {
|
|
|
9818
9854
|
let topic = null;
|
|
9819
9855
|
let offset = 0;
|
|
9820
9856
|
for (; ; ) {
|
|
9821
|
-
const
|
|
9857
|
+
const page2 = await request(
|
|
9822
9858
|
ctx,
|
|
9823
9859
|
"GET",
|
|
9824
9860
|
`/topics/${encodeURIComponent(id)}?limit=200&offset=${offset}`
|
|
9825
9861
|
);
|
|
9826
|
-
topic =
|
|
9827
|
-
for (const post of
|
|
9828
|
-
mergeDiscussPrincipals(projection,
|
|
9862
|
+
topic = page2.topic;
|
|
9863
|
+
for (const post of page2.posts) posts.set(post.id, post);
|
|
9864
|
+
mergeDiscussPrincipals(projection, page2);
|
|
9829
9865
|
if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
|
|
9830
|
-
if (!
|
|
9831
|
-
if (
|
|
9866
|
+
if (!page2.page?.hasMore) break;
|
|
9867
|
+
if (page2.page.nextOffset === null || page2.page.nextOffset <= offset) {
|
|
9832
9868
|
throw new Error("discuss read failed: registry returned a non-advancing post page");
|
|
9833
9869
|
}
|
|
9834
|
-
offset =
|
|
9870
|
+
offset = page2.page.nextOffset;
|
|
9835
9871
|
}
|
|
9836
9872
|
const ordered = [...posts.values()].sort(
|
|
9837
9873
|
(a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
|
|
@@ -9999,9 +10035,9 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9999
10035
|
let firstSuccess = true;
|
|
10000
10036
|
let consecutiveFailures = 0;
|
|
10001
10037
|
for (; ; ) {
|
|
10002
|
-
let
|
|
10038
|
+
let page2;
|
|
10003
10039
|
try {
|
|
10004
|
-
|
|
10040
|
+
page2 = await getWatchPage(ctx, requestPath(topicId, app, cursor));
|
|
10005
10041
|
consecutiveFailures = 0;
|
|
10006
10042
|
} catch (error) {
|
|
10007
10043
|
if (!(error instanceof WatchRequestError) || !error.retryable) {
|
|
@@ -10037,25 +10073,25 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10037
10073
|
await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
|
|
10038
10074
|
continue;
|
|
10039
10075
|
}
|
|
10040
|
-
cursor =
|
|
10041
|
-
const baseline = firstSuccess &&
|
|
10076
|
+
cursor = page2.cursor;
|
|
10077
|
+
const baseline = firstSuccess && page2.events.length === 0;
|
|
10042
10078
|
if (baseline) {
|
|
10043
10079
|
jsonl(ctx, parsed, {
|
|
10044
10080
|
type: "checkpoint",
|
|
10045
|
-
streamId:
|
|
10081
|
+
streamId: page2.streamId,
|
|
10046
10082
|
cursor,
|
|
10047
|
-
serverTime:
|
|
10083
|
+
serverTime: page2.serverTime
|
|
10048
10084
|
});
|
|
10049
10085
|
}
|
|
10050
10086
|
firstSuccess = false;
|
|
10051
|
-
const matching =
|
|
10087
|
+
const matching = page2.events.filter((event) => {
|
|
10052
10088
|
if (topicId && (event.type !== "message" || event.action !== "created")) return false;
|
|
10053
10089
|
return (!by || event.actor.id === by) && (!self || event.actor.id !== self);
|
|
10054
10090
|
});
|
|
10055
10091
|
for (const event of matching) {
|
|
10056
10092
|
jsonl(ctx, parsed, {
|
|
10057
10093
|
type: "event",
|
|
10058
|
-
streamId:
|
|
10094
|
+
streamId: page2.streamId,
|
|
10059
10095
|
eventId: event.id,
|
|
10060
10096
|
cursor: event.cursor,
|
|
10061
10097
|
event
|
|
@@ -10064,9 +10100,9 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10064
10100
|
if (matching.length > 0) {
|
|
10065
10101
|
jsonl(ctx, parsed, {
|
|
10066
10102
|
type: "checkpoint",
|
|
10067
|
-
streamId:
|
|
10103
|
+
streamId: page2.streamId,
|
|
10068
10104
|
cursor,
|
|
10069
|
-
serverTime:
|
|
10105
|
+
serverTime: page2.serverTime
|
|
10070
10106
|
});
|
|
10071
10107
|
const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
|
|
10072
10108
|
const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
|
|
@@ -10074,28 +10110,28 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10074
10110
|
found: true,
|
|
10075
10111
|
cursor,
|
|
10076
10112
|
events: matching,
|
|
10077
|
-
...
|
|
10078
|
-
...
|
|
10113
|
+
...page2.authors ? { authors: page2.authors } : {},
|
|
10114
|
+
...page2.principals ? { principals: page2.principals } : {},
|
|
10079
10115
|
...posts && posts.length > 0 ? { posts } : {},
|
|
10080
10116
|
...topics && topics.length > 0 ? { topics } : {}
|
|
10081
10117
|
});
|
|
10082
10118
|
}
|
|
10083
|
-
if (
|
|
10119
|
+
if (page2.events.length > 0) {
|
|
10084
10120
|
jsonl(ctx, parsed, {
|
|
10085
10121
|
type: "checkpoint",
|
|
10086
|
-
streamId:
|
|
10122
|
+
streamId: page2.streamId,
|
|
10087
10123
|
cursor,
|
|
10088
|
-
serverTime:
|
|
10124
|
+
serverTime: page2.serverTime
|
|
10089
10125
|
});
|
|
10090
10126
|
} else if (!baseline) {
|
|
10091
10127
|
jsonl(ctx, parsed, {
|
|
10092
10128
|
type: "heartbeat",
|
|
10093
|
-
streamId:
|
|
10129
|
+
streamId: page2.streamId,
|
|
10094
10130
|
cursor,
|
|
10095
|
-
serverTime:
|
|
10131
|
+
serverTime: page2.serverTime
|
|
10096
10132
|
});
|
|
10097
10133
|
}
|
|
10098
|
-
if (
|
|
10134
|
+
if (page2.hasMore) continue;
|
|
10099
10135
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10100
10136
|
return report2(ctx, parsed, { found: false, cursor });
|
|
10101
10137
|
}
|
|
@@ -10214,7 +10250,13 @@ async function discussCommand(parsed, deps = {}) {
|
|
|
10214
10250
|
}
|
|
10215
10251
|
}
|
|
10216
10252
|
|
|
10217
|
-
// src/pm-
|
|
10253
|
+
// src/pm-action-core.ts
|
|
10254
|
+
var DONE = {
|
|
10255
|
+
goal: { status: "met", currentPct: 100 },
|
|
10256
|
+
task: { column: "done" },
|
|
10257
|
+
decision: { status: "accepted" },
|
|
10258
|
+
bug: { status: "fixed" }
|
|
10259
|
+
};
|
|
10218
10260
|
var writeMutationId2 = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
|
|
10219
10261
|
var FIELD_MAP = {
|
|
10220
10262
|
title: { key: "title" },
|
|
@@ -10230,22 +10272,27 @@ var FIELD_MAP = {
|
|
|
10230
10272
|
body: { key: "body" },
|
|
10231
10273
|
target: { key: "targetPct", num: true },
|
|
10232
10274
|
description: { key: "description" },
|
|
10233
|
-
desc: { key: "description" }
|
|
10234
|
-
}
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
decision: { status: "accepted" },
|
|
10239
|
-
bug: { status: "fixed" }
|
|
10275
|
+
desc: { key: "description" },
|
|
10276
|
+
acceptance: { key: "acceptanceCriteria" },
|
|
10277
|
+
"alignment-decision": { key: "alignmentDecisionId" },
|
|
10278
|
+
execution: { key: "executionMode" },
|
|
10279
|
+
"expected-revision": { key: "expectedRevision", num: true }
|
|
10240
10280
|
};
|
|
10241
10281
|
async function pmRequest(ctx, method, path, body) {
|
|
10242
|
-
const
|
|
10282
|
+
const response2 = await ctx.doFetch(`${ctx.platformUrl}/registry/pm${path}`, {
|
|
10243
10283
|
method,
|
|
10244
|
-
headers: {
|
|
10284
|
+
headers: {
|
|
10285
|
+
authorization: `Bearer ${ctx.token}`,
|
|
10286
|
+
"content-type": "application/json"
|
|
10287
|
+
},
|
|
10245
10288
|
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
10246
10289
|
});
|
|
10247
|
-
const data = await
|
|
10248
|
-
if (!
|
|
10290
|
+
const data = await response2.json().catch(() => ({}));
|
|
10291
|
+
if (!response2.ok) {
|
|
10292
|
+
throw new Error(
|
|
10293
|
+
`pm ${method} ${path} failed: ${data.error ?? `registry returned ${response2.status}`}`
|
|
10294
|
+
);
|
|
10295
|
+
}
|
|
10249
10296
|
return data;
|
|
10250
10297
|
}
|
|
10251
10298
|
function collectFields(parsed, allowClear) {
|
|
@@ -10268,20 +10315,32 @@ function collectEntityFields(entity, parsed, allowClear) {
|
|
|
10268
10315
|
if (fields.description === void 0) fields.description = fields.body;
|
|
10269
10316
|
delete fields.body;
|
|
10270
10317
|
}
|
|
10318
|
+
if (entity === "task" && fields.column === "ready") fields.column = "todo";
|
|
10271
10319
|
return fields;
|
|
10272
10320
|
}
|
|
10273
|
-
function statusCol(entity,
|
|
10274
|
-
if (entity === "bug") return `${
|
|
10275
|
-
if (entity === "task")
|
|
10276
|
-
|
|
10321
|
+
function statusCol(entity, record10) {
|
|
10322
|
+
if (entity === "bug") return `${record10.status ?? ""}/${record10.severity ?? ""}`;
|
|
10323
|
+
if (entity === "task") {
|
|
10324
|
+
const state2 = record10.column === "todo" ? "ready" : String(record10.column ?? "");
|
|
10325
|
+
return record10.revision ? `${state2}; r${record10.revision}` : state2;
|
|
10326
|
+
}
|
|
10327
|
+
return String(record10.status ?? "");
|
|
10328
|
+
}
|
|
10329
|
+
function referenceMarkup(entity, record10) {
|
|
10330
|
+
const label = (record10.title?.trim() || `${entity} ${record10.id}`).replaceAll("]", ")");
|
|
10331
|
+
return `@[${label}](pm:${entity}/${record10.id})`;
|
|
10277
10332
|
}
|
|
10278
|
-
function printRecord(ctx, entity,
|
|
10279
|
-
ctx.out.log(
|
|
10333
|
+
function printRecord(ctx, entity, record10) {
|
|
10334
|
+
ctx.out.log(
|
|
10335
|
+
`${record10.id} [${statusCol(entity, record10)}] ${record10.appId} ${record10.title ?? ""}`
|
|
10336
|
+
);
|
|
10280
10337
|
}
|
|
10281
10338
|
function emit2(ctx, value2, human) {
|
|
10282
10339
|
if (ctx.json) ctx.out.log(JSON.stringify(value2, null, 2));
|
|
10283
10340
|
else human();
|
|
10284
10341
|
}
|
|
10342
|
+
|
|
10343
|
+
// src/pm-actions.ts
|
|
10285
10344
|
async function pmList(ctx, entity, parsed) {
|
|
10286
10345
|
const q = new URLSearchParams();
|
|
10287
10346
|
const filters = {
|
|
@@ -10295,7 +10354,8 @@ async function pmList(ctx, entity, parsed) {
|
|
|
10295
10354
|
const app = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
10296
10355
|
if (app) q.set("app", app);
|
|
10297
10356
|
for (const [flag, param] of Object.entries(filters)) {
|
|
10298
|
-
const
|
|
10357
|
+
const raw = stringOpt(parsed.options[flag]);
|
|
10358
|
+
const v = entity === "task" && flag === "column" && raw === "ready" ? "todo" : raw;
|
|
10299
10359
|
if (v) q.set(param, v);
|
|
10300
10360
|
}
|
|
10301
10361
|
for (const opt of ["q", "limit", "offset"]) {
|
|
@@ -10303,11 +10363,11 @@ async function pmList(ctx, entity, parsed) {
|
|
|
10303
10363
|
if (v) q.set(opt, v);
|
|
10304
10364
|
}
|
|
10305
10365
|
const qs = q.toString();
|
|
10306
|
-
const
|
|
10307
|
-
emit2(ctx,
|
|
10308
|
-
ctx.out.log(`${entity} \u2014 ${
|
|
10366
|
+
const page2 = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
|
|
10367
|
+
emit2(ctx, page2, () => {
|
|
10368
|
+
ctx.out.log(`${entity} \u2014 ${page2.records.length} of ${page2.total}`);
|
|
10309
10369
|
ctx.out.log("id state app title");
|
|
10310
|
-
for (const r of
|
|
10370
|
+
for (const r of page2.records) printRecord(ctx, entity, r);
|
|
10311
10371
|
});
|
|
10312
10372
|
}
|
|
10313
10373
|
async function pmAdd(ctx, entity, parsed) {
|
|
@@ -10328,6 +10388,17 @@ async function pmGet(ctx, entity, id) {
|
|
|
10328
10388
|
const { record: record10 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
|
|
10329
10389
|
emit2(ctx, record10, () => printRecord(ctx, entity, record10));
|
|
10330
10390
|
}
|
|
10391
|
+
async function pmReference(ctx, entity, id) {
|
|
10392
|
+
const { record: record10 } = await pmRequest(
|
|
10393
|
+
ctx,
|
|
10394
|
+
"GET",
|
|
10395
|
+
`/${entity}/${encodeURIComponent(id)}`
|
|
10396
|
+
);
|
|
10397
|
+
const markup = referenceMarkup(entity, record10);
|
|
10398
|
+
emit2(ctx, { kind: `pm:${entity}`, id: record10.id, label: record10.title ?? "", markup }, () => {
|
|
10399
|
+
ctx.out.log(markup);
|
|
10400
|
+
});
|
|
10401
|
+
}
|
|
10331
10402
|
async function pmSet(ctx, entity, id, parsed) {
|
|
10332
10403
|
const patch2 = collectEntityFields(entity, parsed, true);
|
|
10333
10404
|
if (Object.keys(patch2).length === 0)
|
|
@@ -10348,6 +10419,36 @@ async function pmDone(ctx, entity, id, parsed) {
|
|
|
10348
10419
|
});
|
|
10349
10420
|
emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
|
|
10350
10421
|
}
|
|
10422
|
+
async function pmTaskLifecycle(ctx, id, action2, parsed) {
|
|
10423
|
+
const rawRevision = stringOpt(parsed.options["expected-revision"]);
|
|
10424
|
+
const expectedRevision = Number(rawRevision);
|
|
10425
|
+
if (!rawRevision || !Number.isSafeInteger(expectedRevision) || expectedRevision < 1) {
|
|
10426
|
+
throw new Error(`pm task ${action2} needs --expected-revision <n>`);
|
|
10427
|
+
}
|
|
10428
|
+
const mutationId = writeMutationId2(parsed);
|
|
10429
|
+
const res = action2 === "ready" ? await pmRequest(
|
|
10430
|
+
ctx,
|
|
10431
|
+
"PATCH",
|
|
10432
|
+
`/task/${encodeURIComponent(id)}`,
|
|
10433
|
+
{
|
|
10434
|
+
patch: {
|
|
10435
|
+
...collectEntityFields("task", parsed, true),
|
|
10436
|
+
column: "todo",
|
|
10437
|
+
expectedRevision
|
|
10438
|
+
},
|
|
10439
|
+
mutationId
|
|
10440
|
+
}
|
|
10441
|
+
) : await pmRequest(
|
|
10442
|
+
ctx,
|
|
10443
|
+
"POST",
|
|
10444
|
+
`/task/${encodeURIComponent(id)}/${action2}`,
|
|
10445
|
+
{ expectedRevision, mutationId }
|
|
10446
|
+
);
|
|
10447
|
+
emit2(ctx, res, () => {
|
|
10448
|
+
const state2 = res.record ? statusCol("task", res.record) : action2;
|
|
10449
|
+
ctx.out.log(`task ${id} \u2192 ${state2}`);
|
|
10450
|
+
});
|
|
10451
|
+
}
|
|
10351
10452
|
async function allRecords(ctx, entity, appId) {
|
|
10352
10453
|
const records = [];
|
|
10353
10454
|
for (; ; ) {
|
|
@@ -10356,11 +10457,48 @@ async function allRecords(ctx, entity, appId) {
|
|
|
10356
10457
|
limit: "100",
|
|
10357
10458
|
offset: String(records.length)
|
|
10358
10459
|
});
|
|
10359
|
-
const
|
|
10360
|
-
records.push(...
|
|
10361
|
-
if (records.length >=
|
|
10460
|
+
const page2 = await pmRequest(ctx, "GET", `/${entity}?${q}`);
|
|
10461
|
+
records.push(...page2.records);
|
|
10462
|
+
if (records.length >= page2.total || page2.records.length === 0) return records;
|
|
10362
10463
|
}
|
|
10363
10464
|
}
|
|
10465
|
+
async function pmNext(ctx, parsed) {
|
|
10466
|
+
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
10467
|
+
if (!appId) throw new Error("pm next needs --app <appId>");
|
|
10468
|
+
const [goals, tasks] = await Promise.all([
|
|
10469
|
+
allRecords(ctx, "goal", appId),
|
|
10470
|
+
allRecords(ctx, "task", appId)
|
|
10471
|
+
]);
|
|
10472
|
+
const result = {
|
|
10473
|
+
appId,
|
|
10474
|
+
openGoals: goals.filter((record10) => record10.status === "open"),
|
|
10475
|
+
doing: tasks.filter((record10) => record10.column === "doing"),
|
|
10476
|
+
ready: tasks.filter((record10) => record10.column === "todo")
|
|
10477
|
+
};
|
|
10478
|
+
emit2(ctx, result, () => {
|
|
10479
|
+
ctx.out.log(`${appId}: goal-aligned work intake (read only)`);
|
|
10480
|
+
for (const [label, records] of [
|
|
10481
|
+
["doing", result.doing],
|
|
10482
|
+
["ready", result.ready],
|
|
10483
|
+
["open goals", result.openGoals]
|
|
10484
|
+
]) {
|
|
10485
|
+
ctx.out.log(`${label}:`);
|
|
10486
|
+
if (!records.length) ctx.out.log("- (none)");
|
|
10487
|
+
else for (const record10 of records) printRecord(
|
|
10488
|
+
ctx,
|
|
10489
|
+
label === "open goals" ? "goal" : "task",
|
|
10490
|
+
record10
|
|
10491
|
+
);
|
|
10492
|
+
}
|
|
10493
|
+
if (!result.openGoals.length) {
|
|
10494
|
+
ctx.out.log("next: discuss alignment with the user before creating or claiming project work");
|
|
10495
|
+
} else if (!result.ready.length) {
|
|
10496
|
+
ctx.out.log("next: refine a linked Backlog task and mark it Ready");
|
|
10497
|
+
} else {
|
|
10498
|
+
ctx.out.log("next: review a Ready task, then claim it with its revision");
|
|
10499
|
+
}
|
|
10500
|
+
});
|
|
10501
|
+
}
|
|
10364
10502
|
async function pmHandoff(ctx, parsed) {
|
|
10365
10503
|
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
10366
10504
|
if (!appId) throw new Error("pm handoff needs --app <appId>");
|
|
@@ -10400,6 +10538,12 @@ async function pmHandoff(ctx, parsed) {
|
|
|
10400
10538
|
}
|
|
10401
10539
|
});
|
|
10402
10540
|
}
|
|
10541
|
+
async function pmRemove(ctx, entity, id) {
|
|
10542
|
+
await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id)}`);
|
|
10543
|
+
ctx.out.log(`deleted ${entity} ${id}`);
|
|
10544
|
+
}
|
|
10545
|
+
|
|
10546
|
+
// src/pm-comments.ts
|
|
10403
10547
|
async function pmComment(ctx, entity, id, parsed) {
|
|
10404
10548
|
const body = stringOpt(parsed.options.body);
|
|
10405
10549
|
if (!body) throw new Error('pm comment needs --body "..."');
|
|
@@ -10410,19 +10554,218 @@ async function pmComment(ctx, entity, id, parsed) {
|
|
|
10410
10554
|
ctx.out.log(`commented on ${entity} ${id}`);
|
|
10411
10555
|
}
|
|
10412
10556
|
async function pmComments(ctx, entity, id) {
|
|
10413
|
-
const { messages } = await pmRequest(
|
|
10414
|
-
ctx,
|
|
10415
|
-
"GET",
|
|
10416
|
-
`/${entity}/${encodeURIComponent(id)}/comments`
|
|
10417
|
-
);
|
|
10557
|
+
const { messages } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}/comments`);
|
|
10418
10558
|
emit2(ctx, messages, () => {
|
|
10419
10559
|
if (messages.length === 0) ctx.out.log("(no comments)");
|
|
10420
|
-
else for (const
|
|
10560
|
+
else for (const message2 of messages) {
|
|
10561
|
+
ctx.out.log(
|
|
10562
|
+
`[${message2.authorId ?? "?"}] ${message2.markup ?? message2.body ?? ""}`
|
|
10563
|
+
);
|
|
10564
|
+
}
|
|
10421
10565
|
});
|
|
10422
10566
|
}
|
|
10423
|
-
|
|
10424
|
-
|
|
10425
|
-
|
|
10567
|
+
|
|
10568
|
+
// src/pm-watch-types.ts
|
|
10569
|
+
var PmWatchCheckpointError = class extends Error {
|
|
10570
|
+
constructor(cursor, streamId) {
|
|
10571
|
+
super("PM work cursor requires a new checkpoint");
|
|
10572
|
+
this.cursor = cursor;
|
|
10573
|
+
this.streamId = streamId;
|
|
10574
|
+
this.name = "PmWatchCheckpointError";
|
|
10575
|
+
}
|
|
10576
|
+
cursor;
|
|
10577
|
+
streamId;
|
|
10578
|
+
code = "checkpoint_required";
|
|
10579
|
+
};
|
|
10580
|
+
var PmWatchRequestError = class extends Error {
|
|
10581
|
+
constructor(message2, retryable, status) {
|
|
10582
|
+
super(message2);
|
|
10583
|
+
this.retryable = retryable;
|
|
10584
|
+
this.status = status;
|
|
10585
|
+
this.name = "PmWatchRequestError";
|
|
10586
|
+
}
|
|
10587
|
+
retryable;
|
|
10588
|
+
status;
|
|
10589
|
+
};
|
|
10590
|
+
|
|
10591
|
+
// src/pm-watch.ts
|
|
10592
|
+
var DEFAULT_INTERVAL_MS2 = 15e3;
|
|
10593
|
+
var MAX_CONSECUTIVE_FAILURES2 = 5;
|
|
10594
|
+
var MAX_BACKOFF_MS2 = 3e4;
|
|
10595
|
+
function positiveNumber(parsed, flag, fallback) {
|
|
10596
|
+
const raw = stringOpt(parsed.options[flag]);
|
|
10597
|
+
const value2 = raw == null ? NaN : Number(raw);
|
|
10598
|
+
return Number.isFinite(value2) && value2 > 0 ? value2 : fallback;
|
|
10599
|
+
}
|
|
10600
|
+
function jsonl2(ctx, parsed, value2) {
|
|
10601
|
+
if (parsed.options.jsonl === true) ctx.out.log(JSON.stringify({ v: 1, ...value2 }));
|
|
10602
|
+
}
|
|
10603
|
+
async function page(ctx, appId, cursor) {
|
|
10604
|
+
const params = new URLSearchParams({ app: appId });
|
|
10605
|
+
if (cursor) params.set("cursor", cursor);
|
|
10606
|
+
let response2;
|
|
10607
|
+
try {
|
|
10608
|
+
response2 = await ctx.doFetch(`${ctx.platformUrl}/registry/pm/work/watch?${params}`, {
|
|
10609
|
+
headers: { authorization: `Bearer ${ctx.token}` }
|
|
10610
|
+
});
|
|
10611
|
+
} catch (error) {
|
|
10612
|
+
throw new PmWatchRequestError(
|
|
10613
|
+
`pm watch request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
10614
|
+
true
|
|
10615
|
+
);
|
|
10616
|
+
}
|
|
10617
|
+
const data = await response2.json().catch(() => ({}));
|
|
10618
|
+
if (response2.status === 409 && data.code === "checkpoint_required") {
|
|
10619
|
+
throw new PmWatchCheckpointError(data.cursor, data.streamId);
|
|
10620
|
+
}
|
|
10621
|
+
if (!response2.ok) {
|
|
10622
|
+
throw new PmWatchRequestError(
|
|
10623
|
+
`pm watch failed: ${data.error ?? `registry returned ${response2.status}`}`,
|
|
10624
|
+
response2.status === 429 || response2.status >= 500,
|
|
10625
|
+
response2.status
|
|
10626
|
+
);
|
|
10627
|
+
}
|
|
10628
|
+
return data;
|
|
10629
|
+
}
|
|
10630
|
+
function recordState(record10) {
|
|
10631
|
+
if (record10.column) return record10.column === "todo" ? "ready" : record10.column;
|
|
10632
|
+
return String(record10.status ?? "");
|
|
10633
|
+
}
|
|
10634
|
+
function eventRecord(event) {
|
|
10635
|
+
return event.payload.payload;
|
|
10636
|
+
}
|
|
10637
|
+
function eventLabel(event) {
|
|
10638
|
+
const record10 = eventRecord(event);
|
|
10639
|
+
if (record10) return String(record10.title ?? event.payload.entityId);
|
|
10640
|
+
const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
|
|
10641
|
+
return body || event.payload.entityId;
|
|
10642
|
+
}
|
|
10643
|
+
function report3(ctx, parsed, result) {
|
|
10644
|
+
if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
|
|
10645
|
+
else if (parsed.options.jsonl !== true && result.found) {
|
|
10646
|
+
for (const event of result.events ?? []) {
|
|
10647
|
+
const record10 = eventRecord(event);
|
|
10648
|
+
const state2 = record10 ? recordState(record10) : "comment";
|
|
10649
|
+
ctx.out.log(
|
|
10650
|
+
`${event.id} ${event.type} ${state2}${record10?.revision ? `; r${record10.revision}` : ""} ${eventLabel(event)}`
|
|
10651
|
+
);
|
|
10652
|
+
}
|
|
10653
|
+
}
|
|
10654
|
+
return result;
|
|
10655
|
+
}
|
|
10656
|
+
async function pmWatch(ctx, parsed) {
|
|
10657
|
+
if (ctx.json && parsed.options.jsonl === true) {
|
|
10658
|
+
throw new Error("--json and --jsonl cannot be combined");
|
|
10659
|
+
}
|
|
10660
|
+
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
10661
|
+
if (!appId) throw new Error("pm watch needs --app <appId>");
|
|
10662
|
+
const sleep = ctx.sleep ?? ((ms) => new Promise((resolve13) => setTimeout(resolve13, ms)));
|
|
10663
|
+
const now = ctx.now ?? Date.now;
|
|
10664
|
+
const intervalMs = (positiveNumber(parsed, "interval", DEFAULT_INTERVAL_MS2 / 1e3) ?? DEFAULT_INTERVAL_MS2 / 1e3) * 1e3;
|
|
10665
|
+
const timeoutSeconds = positiveNumber(parsed, "timeout");
|
|
10666
|
+
const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
|
|
10667
|
+
const entity = stringOpt(parsed.options.entity);
|
|
10668
|
+
const action2 = stringOpt(parsed.options.action);
|
|
10669
|
+
const wantedState = stringOpt(parsed.options.state)?.toLowerCase();
|
|
10670
|
+
const by = stringOpt(parsed.options.by);
|
|
10671
|
+
const self = stringOpt(parsed.options.self);
|
|
10672
|
+
let cursor = stringOpt(parsed.options.cursor);
|
|
10673
|
+
let firstSuccess = true;
|
|
10674
|
+
let consecutiveFailures = 0;
|
|
10675
|
+
for (; ; ) {
|
|
10676
|
+
let current;
|
|
10677
|
+
try {
|
|
10678
|
+
current = await page(ctx, appId, cursor);
|
|
10679
|
+
consecutiveFailures = 0;
|
|
10680
|
+
} catch (error) {
|
|
10681
|
+
if (error instanceof PmWatchCheckpointError) {
|
|
10682
|
+
jsonl2(ctx, parsed, {
|
|
10683
|
+
type: "status",
|
|
10684
|
+
state: "checkpoint_required",
|
|
10685
|
+
retryable: false,
|
|
10686
|
+
...error.cursor ? { cursor: error.cursor } : {},
|
|
10687
|
+
...error.streamId ? { streamId: error.streamId } : {}
|
|
10688
|
+
});
|
|
10689
|
+
throw error;
|
|
10690
|
+
}
|
|
10691
|
+
if (!(error instanceof PmWatchRequestError) || !error.retryable) throw error;
|
|
10692
|
+
consecutiveFailures++;
|
|
10693
|
+
jsonl2(ctx, parsed, {
|
|
10694
|
+
type: "status",
|
|
10695
|
+
state: "degraded",
|
|
10696
|
+
retryable: true,
|
|
10697
|
+
attempt: consecutiveFailures,
|
|
10698
|
+
...cursor ? { cursor } : {},
|
|
10699
|
+
...error.status ? { status: error.status } : {}
|
|
10700
|
+
});
|
|
10701
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
|
|
10702
|
+
if (deadline !== void 0 && now() >= deadline) {
|
|
10703
|
+
return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
10704
|
+
}
|
|
10705
|
+
const backoff = Math.min(
|
|
10706
|
+
MAX_BACKOFF_MS2,
|
|
10707
|
+
Math.min(intervalMs, 1e3) * 2 ** (consecutiveFailures - 1)
|
|
10708
|
+
);
|
|
10709
|
+
await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
|
|
10710
|
+
continue;
|
|
10711
|
+
}
|
|
10712
|
+
cursor = current.cursor;
|
|
10713
|
+
const baseline = firstSuccess && current.events.length === 0;
|
|
10714
|
+
if (baseline) {
|
|
10715
|
+
jsonl2(ctx, parsed, {
|
|
10716
|
+
type: "checkpoint",
|
|
10717
|
+
streamId: current.streamId,
|
|
10718
|
+
cursor,
|
|
10719
|
+
serverTime: current.serverTime
|
|
10720
|
+
});
|
|
10721
|
+
}
|
|
10722
|
+
firstSuccess = false;
|
|
10723
|
+
const matching = current.events.filter((event) => {
|
|
10724
|
+
const record10 = eventRecord(event);
|
|
10725
|
+
const state2 = record10 ? recordState(record10).toLowerCase() : "";
|
|
10726
|
+
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);
|
|
10727
|
+
});
|
|
10728
|
+
for (const event of matching) {
|
|
10729
|
+
jsonl2(ctx, parsed, {
|
|
10730
|
+
type: "event",
|
|
10731
|
+
streamId: current.streamId,
|
|
10732
|
+
eventId: event.id,
|
|
10733
|
+
cursor: event.cursor,
|
|
10734
|
+
event
|
|
10735
|
+
});
|
|
10736
|
+
}
|
|
10737
|
+
if (matching.length > 0) {
|
|
10738
|
+
jsonl2(ctx, parsed, {
|
|
10739
|
+
type: "checkpoint",
|
|
10740
|
+
streamId: current.streamId,
|
|
10741
|
+
cursor,
|
|
10742
|
+
serverTime: current.serverTime
|
|
10743
|
+
});
|
|
10744
|
+
return report3(ctx, parsed, { found: true, cursor, events: matching });
|
|
10745
|
+
}
|
|
10746
|
+
if (current.events.length > 0) {
|
|
10747
|
+
jsonl2(ctx, parsed, {
|
|
10748
|
+
type: "checkpoint",
|
|
10749
|
+
streamId: current.streamId,
|
|
10750
|
+
cursor,
|
|
10751
|
+
serverTime: current.serverTime
|
|
10752
|
+
});
|
|
10753
|
+
} else if (!baseline) {
|
|
10754
|
+
jsonl2(ctx, parsed, {
|
|
10755
|
+
type: "heartbeat",
|
|
10756
|
+
streamId: current.streamId,
|
|
10757
|
+
cursor,
|
|
10758
|
+
serverTime: current.serverTime
|
|
10759
|
+
});
|
|
10760
|
+
}
|
|
10761
|
+
if (current.hasMore) continue;
|
|
10762
|
+
if (deadline !== void 0 && now() >= deadline) {
|
|
10763
|
+
return report3(ctx, parsed, { found: false, cursor });
|
|
10764
|
+
}
|
|
10765
|
+
await sleep(
|
|
10766
|
+
deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
|
|
10767
|
+
);
|
|
10768
|
+
}
|
|
10426
10769
|
}
|
|
10427
10770
|
|
|
10428
10771
|
// src/pm-command.ts
|
|
@@ -10443,7 +10786,11 @@ var ACTION_OPTIONS = {
|
|
|
10443
10786
|
done: ["mutation-id"],
|
|
10444
10787
|
comment: ["body", "mutation-id"],
|
|
10445
10788
|
comments: [],
|
|
10446
|
-
rm: []
|
|
10789
|
+
rm: [],
|
|
10790
|
+
ready: ["goal", "alignment-decision", "execution", "description", "desc", "body", "acceptance", "expected-revision", "mutation-id"],
|
|
10791
|
+
claim: ["expected-revision", "mutation-id"],
|
|
10792
|
+
release: ["expected-revision", "mutation-id"],
|
|
10793
|
+
ref: []
|
|
10447
10794
|
};
|
|
10448
10795
|
var ENTITY_OPTIONS = {
|
|
10449
10796
|
goal: {
|
|
@@ -10454,8 +10801,8 @@ var ENTITY_OPTIONS = {
|
|
|
10454
10801
|
},
|
|
10455
10802
|
task: {
|
|
10456
10803
|
list: ["column", "goal", "assignee"],
|
|
10457
|
-
add: ["column", "goal", "assignee", "due", "description", "desc", "body"],
|
|
10458
|
-
set: ["title", "column", "rank", "goal", "assignee", "due", "description", "desc", "body"],
|
|
10804
|
+
add: ["column", "goal", "alignment-decision", "execution", "assignee", "due", "description", "desc", "body", "acceptance"],
|
|
10805
|
+
set: ["title", "column", "rank", "goal", "alignment-decision", "execution", "assignee", "due", "description", "desc", "body", "acceptance", "expected-revision"],
|
|
10459
10806
|
done: []
|
|
10460
10807
|
},
|
|
10461
10808
|
decision: {
|
|
@@ -10506,11 +10853,33 @@ async function buildContext2(parsed, deps) {
|
|
|
10506
10853
|
json: parsed.options.json === true,
|
|
10507
10854
|
// Preserve PM's existing cross-project default when a config is present;
|
|
10508
10855
|
// only an explicit remote-agent environment or profile selects an app.
|
|
10509
|
-
appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
|
|
10856
|
+
appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0,
|
|
10857
|
+
...deps.sleep ? { sleep: deps.sleep } : {},
|
|
10858
|
+
...deps.now ? { now: deps.now } : {}
|
|
10510
10859
|
};
|
|
10511
10860
|
}
|
|
10512
10861
|
async function pmCommand(parsed, deps = {}) {
|
|
10513
10862
|
const word = parsed.positionals[1] ?? "";
|
|
10863
|
+
if (word === "next") {
|
|
10864
|
+
assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
|
|
10865
|
+
return pmNext(await buildContext2(parsed, deps), parsed);
|
|
10866
|
+
}
|
|
10867
|
+
if (word === "watch") {
|
|
10868
|
+
assertArgs(parsed, [
|
|
10869
|
+
...COMMON_OPTIONS,
|
|
10870
|
+
"app",
|
|
10871
|
+
"cursor",
|
|
10872
|
+
"interval",
|
|
10873
|
+
"timeout",
|
|
10874
|
+
"jsonl",
|
|
10875
|
+
"entity",
|
|
10876
|
+
"action",
|
|
10877
|
+
"state",
|
|
10878
|
+
"by",
|
|
10879
|
+
"self"
|
|
10880
|
+
], 2);
|
|
10881
|
+
return pmWatch(await buildContext2(parsed, deps), parsed).then(() => void 0);
|
|
10882
|
+
}
|
|
10514
10883
|
if (word === "handoff") {
|
|
10515
10884
|
assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
|
|
10516
10885
|
return pmHandoff(await buildContext2(parsed, deps), parsed);
|
|
@@ -10521,6 +10890,9 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
10521
10890
|
const action2 = canonicalAction(requestedAction);
|
|
10522
10891
|
if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|comment|comments|rm.`);
|
|
10523
10892
|
assertArgs(parsed, allowedOptions(entity, action2), 4);
|
|
10893
|
+
if ((action2 === "ready" || action2 === "claim" || action2 === "release") && entity !== "task") {
|
|
10894
|
+
throw new Error(`pm ${action2} is only valid for tasks`);
|
|
10895
|
+
}
|
|
10524
10896
|
const ctx = await buildContext2(parsed, deps);
|
|
10525
10897
|
const id = parsed.positionals[3];
|
|
10526
10898
|
switch (action2) {
|
|
@@ -10540,6 +10912,12 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
10540
10912
|
return pmComments(ctx, entity, requireId2(id, action2));
|
|
10541
10913
|
case "rm":
|
|
10542
10914
|
return pmRemove(ctx, entity, requireId2(id, action2));
|
|
10915
|
+
case "ref":
|
|
10916
|
+
return pmReference(ctx, entity, requireId2(id, action2));
|
|
10917
|
+
case "ready":
|
|
10918
|
+
case "claim":
|
|
10919
|
+
case "release":
|
|
10920
|
+
return pmTaskLifecycle(ctx, requireId2(id, action2), action2, parsed);
|
|
10543
10921
|
}
|
|
10544
10922
|
}
|
|
10545
10923
|
|
|
@@ -11154,12 +11532,12 @@ async function call(ctx, method, path, body) {
|
|
|
11154
11532
|
throw new Error(message2);
|
|
11155
11533
|
}
|
|
11156
11534
|
async function bySlug(ctx, slug) {
|
|
11157
|
-
const
|
|
11535
|
+
const page2 = await call(
|
|
11158
11536
|
ctx,
|
|
11159
11537
|
"GET",
|
|
11160
11538
|
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
|
|
11161
11539
|
);
|
|
11162
|
-
const found =
|
|
11540
|
+
const found = page2.records[0];
|
|
11163
11541
|
if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
|
|
11164
11542
|
return found;
|
|
11165
11543
|
}
|
|
@@ -11173,11 +11551,11 @@ async function runbookList(ctx, all, query) {
|
|
|
11173
11551
|
const params = new URLSearchParams();
|
|
11174
11552
|
if (!all) params.set("app", ctx.appId);
|
|
11175
11553
|
if (query) params.set("q", query);
|
|
11176
|
-
const
|
|
11177
|
-
if (ctx.json) return ctx.out.log(JSON.stringify(
|
|
11178
|
-
if (!
|
|
11554
|
+
const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
|
|
11555
|
+
if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
|
|
11556
|
+
if (!page2.records.length) return ctx.out.log("(no runbooks)");
|
|
11179
11557
|
ctx.out.log(["SLUG", "STATUS", "V", "SCOPE", "UPDATED", "TITLE"].join(" "));
|
|
11180
|
-
for (const r of
|
|
11558
|
+
for (const r of page2.records)
|
|
11181
11559
|
ctx.out.log(
|
|
11182
11560
|
[r.slug, r.status, `v${r.version}`, r.appId === PLATFORM_SCOPE ? "platform" : r.appId, stamp(r.updatedAt), r.title].join(" ")
|
|
11183
11561
|
);
|
|
@@ -11232,12 +11610,12 @@ async function runbookVisibility(ctx, slug, visibility) {
|
|
|
11232
11610
|
}
|
|
11233
11611
|
async function runbookHistory(ctx, slug) {
|
|
11234
11612
|
const runbook = await bySlug(ctx, slug);
|
|
11235
|
-
const
|
|
11236
|
-
if (ctx.json) return ctx.out.log(JSON.stringify(
|
|
11613
|
+
const page2 = await call(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
|
|
11614
|
+
if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
|
|
11237
11615
|
ctx.out.log(`${slug} is at v${runbook.version}`);
|
|
11238
|
-
if (!
|
|
11616
|
+
if (!page2.records.length) return ctx.out.log("(no earlier versions)");
|
|
11239
11617
|
ctx.out.log(["V", "WHEN", "BY", "NOTE"].join(" "));
|
|
11240
|
-
for (const r of
|
|
11618
|
+
for (const r of page2.records)
|
|
11241
11619
|
ctx.out.log([`v${r.version}`, stamp(r.createdAt), r.ownerEmail ?? "", r.note ?? ""].join(" "));
|
|
11242
11620
|
}
|
|
11243
11621
|
async function runbookRevert(ctx, slug, version) {
|
|
@@ -11312,12 +11690,12 @@ ${counts.created} created, ${counts.updated} updated, ${counts.unchanged} unchan
|
|
|
11312
11690
|
);
|
|
11313
11691
|
}
|
|
11314
11692
|
async function upsert(ctx, r, visibility) {
|
|
11315
|
-
const
|
|
11693
|
+
const page2 = await call(
|
|
11316
11694
|
ctx,
|
|
11317
11695
|
"GET",
|
|
11318
11696
|
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(r.slug)}&limit=1`
|
|
11319
11697
|
);
|
|
11320
|
-
const found =
|
|
11698
|
+
const found = page2.records[0];
|
|
11321
11699
|
if (!found) {
|
|
11322
11700
|
await call(ctx, "POST", "/runbook", {
|
|
11323
11701
|
appId: ctx.appId,
|
|
@@ -11573,7 +11951,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
11573
11951
|
return out;
|
|
11574
11952
|
}
|
|
11575
11953
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
11576
|
-
function
|
|
11954
|
+
function report4(ctx, impacts) {
|
|
11577
11955
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
11578
11956
|
ctx.out.log(
|
|
11579
11957
|
`${impacts.length} changed surface${impacts.length === 1 ? "" : "s"}; ${covered.length} covered by a runbook. Reread each one and fix any step this change made wrong.`
|
|
@@ -11611,7 +11989,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
11611
11989
|
}
|
|
11612
11990
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
11613
11991
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
11614
|
-
|
|
11992
|
+
report4(ctx, impacts);
|
|
11615
11993
|
}
|
|
11616
11994
|
|
|
11617
11995
|
// src/runbook-lint.ts
|
|
@@ -11649,17 +12027,17 @@ function lintRunbook(runbook, installed) {
|
|
|
11649
12027
|
async function runbookLint(ctx, all) {
|
|
11650
12028
|
const params = new URLSearchParams();
|
|
11651
12029
|
if (!all) params.set("app", ctx.appId);
|
|
11652
|
-
const
|
|
12030
|
+
const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
|
|
11653
12031
|
const installed = { "@odla-ai/cli": cliVersion() };
|
|
11654
|
-
const findings =
|
|
11655
|
-
if (ctx.json) return ctx.out.log(JSON.stringify({ checked:
|
|
11656
|
-
if (!
|
|
12032
|
+
const findings = page2.records.flatMap((runbook) => lintRunbook(runbook, installed));
|
|
12033
|
+
if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page2.records.length, findings }, null, 2));
|
|
12034
|
+
if (!page2.records.length) return ctx.out.log("(no runbooks in scope)");
|
|
11657
12035
|
if (!findings.length) {
|
|
11658
12036
|
return ctx.out.log(
|
|
11659
|
-
`${
|
|
12037
|
+
`${page2.records.length} runbook${page2.records.length === 1 ? "" : "s"} checked; every command they name is real for @odla-ai/cli ${cliVersion()}.`
|
|
11660
12038
|
);
|
|
11661
12039
|
}
|
|
11662
|
-
ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${
|
|
12040
|
+
ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${page2.records.length} runbooks:`);
|
|
11663
12041
|
for (const finding of findings) {
|
|
11664
12042
|
const scope = finding.appId === PLATFORM_SCOPE ? "" : ` --app ${finding.appId}`;
|
|
11665
12043
|
ctx.out.log("");
|
|
@@ -11722,12 +12100,12 @@ async function runbookAsk(ctx, question, all) {
|
|
|
11722
12100
|
ctx.out.log(JSDOC_POINTER);
|
|
11723
12101
|
}
|
|
11724
12102
|
async function runbookComment(ctx, slug, body) {
|
|
11725
|
-
const
|
|
12103
|
+
const page2 = await call(
|
|
11726
12104
|
ctx,
|
|
11727
12105
|
"GET",
|
|
11728
12106
|
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
|
|
11729
12107
|
);
|
|
11730
|
-
const found =
|
|
12108
|
+
const found = page2.records[0];
|
|
11731
12109
|
if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
|
|
11732
12110
|
await call(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
|
|
11733
12111
|
ctx.out.log(`commented on ${slug} (v${found.version})`);
|
|
@@ -11779,12 +12157,12 @@ var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
|
|
|
11779
12157
|
|
|
11780
12158
|
// src/runbook-edit-flow.ts
|
|
11781
12159
|
async function editRunbook(ctx, slug, deps = {}) {
|
|
11782
|
-
const
|
|
12160
|
+
const page2 = await call(
|
|
11783
12161
|
ctx,
|
|
11784
12162
|
"GET",
|
|
11785
12163
|
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
|
|
11786
12164
|
);
|
|
11787
|
-
const found =
|
|
12165
|
+
const found = page2.records[0];
|
|
11788
12166
|
if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
|
|
11789
12167
|
ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
|
|
11790
12168
|
const body = await editText(found.body, slug, deps);
|
|
@@ -12205,31 +12583,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
12205
12583
|
url.searchParams.set("job", job.jobId);
|
|
12206
12584
|
out.log(` Studio: ${url.toString()}`);
|
|
12207
12585
|
}
|
|
12208
|
-
function printHostedReport(out,
|
|
12209
|
-
out.log(`security report ${
|
|
12210
|
-
out.log(` coverage: ${
|
|
12211
|
-
out.log(` findings: confirmed=${
|
|
12212
|
-
out.log(` discovery: ${
|
|
12213
|
-
out.log(` validation: ${
|
|
12214
|
-
for (const finding of
|
|
12586
|
+
function printHostedReport(out, report5) {
|
|
12587
|
+
out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
|
|
12588
|
+
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}`);
|
|
12589
|
+
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
|
|
12590
|
+
out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
|
|
12591
|
+
out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
|
|
12592
|
+
for (const finding of report5.findings) {
|
|
12215
12593
|
const location = finding.locations[0];
|
|
12216
12594
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
12217
12595
|
}
|
|
12218
|
-
for (const limitation of
|
|
12596
|
+
for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
|
|
12219
12597
|
}
|
|
12220
|
-
function enforceHostedReportGate(
|
|
12598
|
+
function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
|
|
12221
12599
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
12222
12600
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
12223
12601
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
12224
12602
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
12225
|
-
const confirmed =
|
|
12226
|
-
const leads = failOnCandidates ?
|
|
12227
|
-
const incomplete =
|
|
12603
|
+
const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
12604
|
+
const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
12605
|
+
const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
12228
12606
|
if (confirmed.length || leads.length || incomplete) {
|
|
12229
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
12607
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
|
|
12230
12608
|
}
|
|
12231
12609
|
if (emitSuccess) {
|
|
12232
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
12610
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
|
|
12233
12611
|
}
|
|
12234
12612
|
}
|
|
12235
12613
|
function printHostedSecurityPlanRoute(out, label, route2) {
|
|
@@ -12333,13 +12711,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
12333
12711
|
}
|
|
12334
12712
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
12335
12713
|
}
|
|
12336
|
-
const
|
|
12714
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
12337
12715
|
if (parsed.options.json === true) {
|
|
12338
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
12716
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
|
|
12339
12717
|
} else {
|
|
12340
|
-
printHostedReport(context.stdout,
|
|
12718
|
+
printHostedReport(context.stdout, report5);
|
|
12341
12719
|
}
|
|
12342
|
-
enforceHostedReportGate(
|
|
12720
|
+
enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
|
|
12343
12721
|
}
|
|
12344
12722
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
12345
12723
|
if (parsed.options.source === true) {
|
|
@@ -12406,13 +12784,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
12406
12784
|
});
|
|
12407
12785
|
enforceLocalGate(result.report, parsed);
|
|
12408
12786
|
}
|
|
12409
|
-
function enforceLocalGate(
|
|
12787
|
+
function enforceLocalGate(report5, parsed) {
|
|
12410
12788
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
12411
12789
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
12412
12790
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
12413
|
-
const confirmed = findingsAtOrAbove(
|
|
12414
|
-
const leads = failOnCandidates ? findingsAtOrAbove(
|
|
12415
|
-
const incomplete =
|
|
12791
|
+
const confirmed = findingsAtOrAbove(report5, failOn);
|
|
12792
|
+
const leads = failOnCandidates ? findingsAtOrAbove(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
12793
|
+
const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
12416
12794
|
if (confirmed.length || leads.length || incomplete) {
|
|
12417
12795
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
12418
12796
|
}
|
|
@@ -12451,9 +12829,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
12451
12829
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
12452
12830
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
12453
12831
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
12454
|
-
const
|
|
12455
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
12456
|
-
else printHostedReport(context.stdout,
|
|
12832
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId });
|
|
12833
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
|
|
12834
|
+
else printHostedReport(context.stdout, report5);
|
|
12457
12835
|
return;
|
|
12458
12836
|
}
|
|
12459
12837
|
if (sub !== "run") {
|
|
@@ -12746,4 +13124,4 @@ export {
|
|
|
12746
13124
|
exitCodeFor,
|
|
12747
13125
|
runCli
|
|
12748
13126
|
};
|
|
12749
|
-
//# sourceMappingURL=chunk-
|
|
13127
|
+
//# sourceMappingURL=chunk-GENTB3VO.js.map
|