@odla-ai/cli 0.42.2 → 0.43.1
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 +10 -4
- package/dist/bin.cjs +489 -214
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-K6ATAKGR.js → chunk-T4OPSRME.js} +453 -208
- package/dist/chunk-T4OPSRME.js.map +1 -0
- package/dist/{cli-4FCRBWED.js → cli-3JOB4OUV.js} +2 -2
- package/dist/index.cjs +452 -207
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/odla/SKILL.md +10 -3
- package/skills/odla/references/pm-work-intake.md +33 -11
- package/skills/odla/references/pm.md +23 -12
- package/dist/chunk-K6ATAKGR.js.map +0 -1
- /package/dist/{cli-4FCRBWED.js.map → cli-3JOB4OUV.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -452,10 +452,10 @@ function isManagedDevVar(line2) {
|
|
|
452
452
|
const match = line2.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
|
|
453
453
|
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
454
454
|
}
|
|
455
|
-
function writePrivateText(path,
|
|
455
|
+
function writePrivateText(path, text4) {
|
|
456
456
|
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
457
457
|
const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
458
|
-
(0, import_node_fs4.writeFileSync)(temporary,
|
|
458
|
+
(0, import_node_fs4.writeFileSync)(temporary, text4, { mode: 384 });
|
|
459
459
|
(0, import_node_fs4.chmodSync)(temporary, 384);
|
|
460
460
|
(0, import_node_fs4.renameSync)(temporary, path);
|
|
461
461
|
}
|
|
@@ -895,12 +895,12 @@ async function readAdminAiAudit(request3) {
|
|
|
895
895
|
}
|
|
896
896
|
}
|
|
897
897
|
async function responseBody(response2) {
|
|
898
|
-
const
|
|
899
|
-
if (!
|
|
898
|
+
const text4 = await response2.text();
|
|
899
|
+
if (!text4) return {};
|
|
900
900
|
try {
|
|
901
|
-
return JSON.parse(
|
|
901
|
+
return JSON.parse(text4);
|
|
902
902
|
} catch {
|
|
903
|
-
return { message:
|
|
903
|
+
return { message: text4.slice(0, 300) };
|
|
904
904
|
}
|
|
905
905
|
}
|
|
906
906
|
function apiError(status, body) {
|
|
@@ -1002,12 +1002,12 @@ function timestamp2(value2) {
|
|
|
1002
1002
|
return Number.isFinite(date.valueOf()) ? date.toISOString() : "";
|
|
1003
1003
|
}
|
|
1004
1004
|
async function responseBody2(res) {
|
|
1005
|
-
const
|
|
1006
|
-
if (!
|
|
1005
|
+
const text4 = await res.text();
|
|
1006
|
+
if (!text4) return {};
|
|
1007
1007
|
try {
|
|
1008
|
-
return JSON.parse(
|
|
1008
|
+
return JSON.parse(text4);
|
|
1009
1009
|
} catch {
|
|
1010
|
-
return { message:
|
|
1010
|
+
return { message: text4.slice(0, 300) };
|
|
1011
1011
|
}
|
|
1012
1012
|
}
|
|
1013
1013
|
function apiError2(action2, status, body) {
|
|
@@ -1183,12 +1183,12 @@ function catalogModels(body) {
|
|
|
1183
1183
|
return body.catalog.models.filter((value2) => isRecord3(value2) && typeof value2.id === "string" && typeof value2.provider === "string");
|
|
1184
1184
|
}
|
|
1185
1185
|
async function responseBody3(res) {
|
|
1186
|
-
const
|
|
1187
|
-
if (!
|
|
1186
|
+
const text4 = await res.text();
|
|
1187
|
+
if (!text4) return {};
|
|
1188
1188
|
try {
|
|
1189
|
-
return JSON.parse(
|
|
1189
|
+
return JSON.parse(text4);
|
|
1190
1190
|
} catch {
|
|
1191
|
-
return { message:
|
|
1191
|
+
return { message: text4.slice(0, 300) };
|
|
1192
1192
|
}
|
|
1193
1193
|
}
|
|
1194
1194
|
function apiError3(action2, status, body) {
|
|
@@ -2557,8 +2557,8 @@ async function appImport(options) {
|
|
|
2557
2557
|
const out = options.stdout ?? console;
|
|
2558
2558
|
const say = options.json ? (line2) => out.error(line2) : (line2) => out.log(line2);
|
|
2559
2559
|
const { tenant } = resolveTenant(cfg, options.env);
|
|
2560
|
-
const
|
|
2561
|
-
const { format, sources } = (0, import_import.parseImport)(
|
|
2560
|
+
const text4 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs9.readFileSync)(0, "utf8")))() : (0, import_node_fs9.readFileSync)(options.file, "utf8");
|
|
2561
|
+
const { format, sources } = (0, import_import.parseImport)(text4, options.ns);
|
|
2562
2562
|
if (format === "namespace-map" && options.ns) {
|
|
2563
2563
|
throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
|
|
2564
2564
|
}
|
|
@@ -2769,7 +2769,7 @@ var EXTENSIONS = {
|
|
|
2769
2769
|
"text/html": "html",
|
|
2770
2770
|
"application/json": "json"
|
|
2771
2771
|
};
|
|
2772
|
-
var encode = (
|
|
2772
|
+
var encode = (text4) => new TextEncoder().encode(text4);
|
|
2773
2773
|
function assetFileName(uuid, mime) {
|
|
2774
2774
|
const ext = EXTENSIONS[mime.split(";")[0].trim().toLowerCase()] ?? "bin";
|
|
2775
2775
|
return `${uuid.replace(/[^a-zA-Z0-9._-]/g, "_")}.${ext}`;
|
|
@@ -3679,9 +3679,9 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
|
|
|
3679
3679
|
}
|
|
3680
3680
|
throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
|
|
3681
3681
|
}
|
|
3682
|
-
function errorCode(
|
|
3682
|
+
function errorCode(text4) {
|
|
3683
3683
|
try {
|
|
3684
|
-
const body = JSON.parse(
|
|
3684
|
+
const body = JSON.parse(text4);
|
|
3685
3685
|
return typeof body.error?.code === "string" ? body.error.code : null;
|
|
3686
3686
|
} catch {
|
|
3687
3687
|
return null;
|
|
@@ -4416,15 +4416,15 @@ function readWranglerConfig(path) {
|
|
|
4416
4416
|
return null;
|
|
4417
4417
|
}
|
|
4418
4418
|
}
|
|
4419
|
-
function stripJsonComments(
|
|
4419
|
+
function stripJsonComments(text4) {
|
|
4420
4420
|
let result = "";
|
|
4421
4421
|
let inString = false;
|
|
4422
|
-
for (let i = 0; i <
|
|
4423
|
-
const ch =
|
|
4422
|
+
for (let i = 0; i < text4.length; i++) {
|
|
4423
|
+
const ch = text4[i];
|
|
4424
4424
|
if (inString) {
|
|
4425
4425
|
result += ch;
|
|
4426
4426
|
if (ch === "\\") {
|
|
4427
|
-
result +=
|
|
4427
|
+
result += text4[i + 1] ?? "";
|
|
4428
4428
|
i++;
|
|
4429
4429
|
} else if (ch === '"') {
|
|
4430
4430
|
inString = false;
|
|
@@ -4436,14 +4436,14 @@ function stripJsonComments(text3) {
|
|
|
4436
4436
|
result += ch;
|
|
4437
4437
|
continue;
|
|
4438
4438
|
}
|
|
4439
|
-
if (ch === "/" &&
|
|
4440
|
-
while (i <
|
|
4439
|
+
if (ch === "/" && text4[i + 1] === "/") {
|
|
4440
|
+
while (i < text4.length && text4[i] !== "\n") i++;
|
|
4441
4441
|
result += "\n";
|
|
4442
4442
|
continue;
|
|
4443
4443
|
}
|
|
4444
|
-
if (ch === "/" &&
|
|
4444
|
+
if (ch === "/" && text4[i + 1] === "*") {
|
|
4445
4445
|
i += 2;
|
|
4446
|
-
while (i <
|
|
4446
|
+
while (i < text4.length && !(text4[i] === "*" && text4[i + 1] === "/")) i++;
|
|
4447
4447
|
i++;
|
|
4448
4448
|
continue;
|
|
4449
4449
|
}
|
|
@@ -4957,9 +4957,9 @@ function initProject(options) {
|
|
|
4957
4957
|
out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
|
|
4958
4958
|
out.log("updated .gitignore for local odla credentials");
|
|
4959
4959
|
}
|
|
4960
|
-
function writeIfMissing(path,
|
|
4960
|
+
function writeIfMissing(path, text4) {
|
|
4961
4961
|
if ((0, import_node_fs14.existsSync)(path)) return;
|
|
4962
|
-
(0, import_node_fs14.writeFileSync)(path,
|
|
4962
|
+
(0, import_node_fs14.writeFileSync)(path, text4);
|
|
4963
4963
|
}
|
|
4964
4964
|
function configTemplate(input) {
|
|
4965
4965
|
const calendar = input.services.includes("calendar") ? ` calendar: {
|
|
@@ -5171,13 +5171,13 @@ async function secretsSetClerkKey(options) {
|
|
|
5171
5171
|
body: JSON.stringify({ value: value2 })
|
|
5172
5172
|
});
|
|
5173
5173
|
if (!res.ok) {
|
|
5174
|
-
const
|
|
5175
|
-
throw new Error(`store Clerk secret key failed (${res.status}): ${
|
|
5174
|
+
const text4 = scrubValue((await res.text().catch(() => "")).slice(0, 300), value2);
|
|
5175
|
+
throw new Error(`store Clerk secret key failed (${res.status}): ${text4 || "request failed"}`);
|
|
5176
5176
|
}
|
|
5177
5177
|
out.log(`Clerk secret key stored for ${tenantId} ($clerk_secret, reserved + write-only; the value was never echoed)`);
|
|
5178
5178
|
}
|
|
5179
|
-
function scrubValue(
|
|
5180
|
-
return redactSecrets(
|
|
5179
|
+
function scrubValue(text4, value2) {
|
|
5180
|
+
return redactSecrets(text4).split(value2).join("[value redacted]");
|
|
5181
5181
|
}
|
|
5182
5182
|
async function resolveVaultWrite(options) {
|
|
5183
5183
|
const out = options.stdout ?? console;
|
|
@@ -5469,8 +5469,8 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
5469
5469
|
harnesses: installations
|
|
5470
5470
|
};
|
|
5471
5471
|
}
|
|
5472
|
-
function pathsUnder(root,
|
|
5473
|
-
return [...
|
|
5472
|
+
function pathsUnder(root, paths2) {
|
|
5473
|
+
return [...paths2].map((path) => (0, import_node_path15.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path15.sep}`) && !(0, import_node_path15.isAbsolute)(path)).sort();
|
|
5474
5474
|
}
|
|
5475
5475
|
function normalizeHarnesses(values, global) {
|
|
5476
5476
|
const requested = values?.length ? values : ["claude"];
|
|
@@ -5899,10 +5899,10 @@ var import_node_fs16 = require("fs");
|
|
|
5899
5899
|
var import_node_os5 = require("os");
|
|
5900
5900
|
var import_node_path16 = require("path");
|
|
5901
5901
|
|
|
5902
|
-
// ../harness/dist/chunk-
|
|
5902
|
+
// ../harness/dist/chunk-RXNHCGWE.js
|
|
5903
5903
|
var HARNESS_PROTOCOL_VERSION = 1;
|
|
5904
5904
|
|
|
5905
|
-
// ../harness/dist/chunk-
|
|
5905
|
+
// ../harness/dist/chunk-CR6RE3A2.js
|
|
5906
5906
|
var import_child_process = require("child_process");
|
|
5907
5907
|
var import_fs = require("fs");
|
|
5908
5908
|
var import_promises2 = require("fs/promises");
|
|
@@ -6154,12 +6154,12 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
6154
6154
|
});
|
|
6155
6155
|
if (outputBytes > 8 * 1024 * 1024) throw new Error("git file inventory exceeds 8 MiB");
|
|
6156
6156
|
if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
|
|
6157
|
-
const
|
|
6158
|
-
if (
|
|
6157
|
+
const paths2 = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
|
|
6158
|
+
if (paths2.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
6159
6159
|
const root = (0, import_path4.resolve)(sourceDir);
|
|
6160
6160
|
const files = [];
|
|
6161
6161
|
let bytes = 0;
|
|
6162
|
-
for (const relativePath of
|
|
6162
|
+
for (const relativePath of paths2) {
|
|
6163
6163
|
if (!allowedWorkspacePath(relativePath)) continue;
|
|
6164
6164
|
const source = (0, import_path4.resolve)(root, relativePath);
|
|
6165
6165
|
if (!source.startsWith(`${root}${import_path4.sep}`)) throw new TypeError("git file path escapes workspace");
|
|
@@ -6275,7 +6275,7 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
|
|
|
6275
6275
|
}
|
|
6276
6276
|
}
|
|
6277
6277
|
|
|
6278
|
-
// ../harness/dist/chunk-
|
|
6278
|
+
// ../harness/dist/chunk-ISR434K7.js
|
|
6279
6279
|
var import_crypto = require("crypto");
|
|
6280
6280
|
var import_promises5 = require("fs/promises");
|
|
6281
6281
|
var import_path5 = require("path");
|
|
@@ -6598,13 +6598,13 @@ function validateSnapshot(snapshot, limits) {
|
|
|
6598
6598
|
if (!Number.isSafeInteger(limits.maximumFiles) || limits.maximumFiles < 1 || !Number.isSafeInteger(limits.maximumBytes) || limits.maximumBytes < 1 || snapshot.files.length > limits.maximumFiles) {
|
|
6599
6599
|
throw new CamelError("limit_exceeded", "Code snapshot exceeds its registered limits.");
|
|
6600
6600
|
}
|
|
6601
|
-
const
|
|
6601
|
+
const paths2 = /* @__PURE__ */ new Set();
|
|
6602
6602
|
let bytes = 0;
|
|
6603
6603
|
for (const file of snapshot.files) {
|
|
6604
|
-
if (!file.path || file.path.startsWith("/") || file.path.includes("\\") || file.path.split("/").some((part) => !part || part === "." || part === "..") ||
|
|
6604
|
+
if (!file.path || file.path.startsWith("/") || file.path.includes("\\") || file.path.split("/").some((part) => !part || part === "." || part === "..") || paths2.has(file.path) || typeof file.content !== "string") {
|
|
6605
6605
|
throw new CamelError("state_conflict", "Code snapshot contains an invalid or duplicate path.");
|
|
6606
6606
|
}
|
|
6607
|
-
|
|
6607
|
+
paths2.add(file.path);
|
|
6608
6608
|
bytes += utf8Length(file.path) + utf8Length(file.content);
|
|
6609
6609
|
}
|
|
6610
6610
|
if (bytes > limits.maximumBytes) {
|
|
@@ -6612,7 +6612,7 @@ function validateSnapshot(snapshot, limits) {
|
|
|
6612
6612
|
}
|
|
6613
6613
|
}
|
|
6614
6614
|
|
|
6615
|
-
// ../harness/dist/chunk-
|
|
6615
|
+
// ../harness/dist/chunk-ISR434K7.js
|
|
6616
6616
|
var import_child_process4 = require("child_process");
|
|
6617
6617
|
var import_promises6 = require("fs/promises");
|
|
6618
6618
|
var import_path6 = require("path");
|
|
@@ -6755,8 +6755,8 @@ function boundedInteger(value2, spec) {
|
|
|
6755
6755
|
}
|
|
6756
6756
|
function boundedNumber(value2, spec) {
|
|
6757
6757
|
if (spec.kind !== "finite_number" || typeof value2 !== "number" || !Number.isFinite(value2) || value2 < spec.minimum || value2 > spec.maximum) throw new CamelError("conversion_rejected", "Finite-number conversion rejected the structured value.");
|
|
6758
|
-
const
|
|
6759
|
-
if (/e/i.test(
|
|
6758
|
+
const text4 = String(value2);
|
|
6759
|
+
if (/e/i.test(text4) || (text4.split(".")[1]?.length ?? 0) > spec.maximumDecimalPlaces) throw new CamelError("conversion_rejected", "Finite-number conversion rejected a non-canonical decimal.");
|
|
6760
6760
|
return value2;
|
|
6761
6761
|
}
|
|
6762
6762
|
function enumMember(value2, spec) {
|
|
@@ -6912,11 +6912,11 @@ function validateUnsafeSelector(path, value2, tool) {
|
|
|
6912
6912
|
return void 0;
|
|
6913
6913
|
}
|
|
6914
6914
|
function looksLikeDestination(value2) {
|
|
6915
|
-
const
|
|
6916
|
-
return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(
|
|
6915
|
+
const text4 = value2.trim();
|
|
6916
|
+
return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text4) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text4);
|
|
6917
6917
|
}
|
|
6918
6918
|
|
|
6919
|
-
// ../harness/dist/chunk-
|
|
6919
|
+
// ../harness/dist/chunk-ISR434K7.js
|
|
6920
6920
|
var import_promises10 = require("fs/promises");
|
|
6921
6921
|
var import_promises11 = require("fs/promises");
|
|
6922
6922
|
var import_path10 = require("path");
|
|
@@ -7081,9 +7081,9 @@ async function extractImports(builder, input) {
|
|
|
7081
7081
|
const sources = input.paths.filter(isSourcePath);
|
|
7082
7082
|
const known = new Set(sources);
|
|
7083
7083
|
for (const path of sources) {
|
|
7084
|
-
let
|
|
7084
|
+
let text4;
|
|
7085
7085
|
try {
|
|
7086
|
-
|
|
7086
|
+
text4 = await input.read(path);
|
|
7087
7087
|
} catch {
|
|
7088
7088
|
continue;
|
|
7089
7089
|
}
|
|
@@ -7091,13 +7091,13 @@ async function extractImports(builder, input) {
|
|
|
7091
7091
|
const file = builder.node(FILE, path, pkg ? { pkg } : void 0);
|
|
7092
7092
|
if (pkg) builder.edge(builder.node(PACKAGE, pkg), CONTAINS, file);
|
|
7093
7093
|
const specifiers = /* @__PURE__ */ new Set();
|
|
7094
|
-
for (const match of
|
|
7095
|
-
for (const match of
|
|
7094
|
+
for (const match of text4.matchAll(IMPORT_FROM)) specifiers.add(match[1]);
|
|
7095
|
+
for (const match of text4.matchAll(BARE_IMPORT)) specifiers.add(match[1]);
|
|
7096
7096
|
for (const specifier of specifiers) {
|
|
7097
7097
|
const resolved = resolveImport(path, specifier, known);
|
|
7098
7098
|
if (resolved) builder.edge(file, IMPORTS, nodeId(FILE, resolved));
|
|
7099
7099
|
}
|
|
7100
|
-
for (const name of exportedNames(
|
|
7100
|
+
for (const name of exportedNames(text4)) {
|
|
7101
7101
|
builder.edge(file, EXPORTS, builder.node(SYMBOL, name));
|
|
7102
7102
|
}
|
|
7103
7103
|
}
|
|
@@ -7138,16 +7138,16 @@ async function extractData(builder, input) {
|
|
|
7138
7138
|
};
|
|
7139
7139
|
for (const path of input.paths) {
|
|
7140
7140
|
if (!SOURCE_FILE.test(path) || input.ignore?.(path)) continue;
|
|
7141
|
-
let
|
|
7141
|
+
let text4;
|
|
7142
7142
|
try {
|
|
7143
|
-
|
|
7143
|
+
text4 = await input.read(path);
|
|
7144
7144
|
} catch {
|
|
7145
7145
|
continue;
|
|
7146
7146
|
}
|
|
7147
|
-
for (const statement of
|
|
7147
|
+
for (const statement of text4.matchAll(STATEMENT)) {
|
|
7148
7148
|
const verb = statement[1].toUpperCase().replace(/\s+/g, " ");
|
|
7149
7149
|
const start = statement.index ?? 0;
|
|
7150
|
-
const rest =
|
|
7150
|
+
const rest = text4.slice(start + statement[0].length, start + STATEMENT_WINDOW);
|
|
7151
7151
|
if (verb === "SELECT") {
|
|
7152
7152
|
for (const read3 of rest.matchAll(READ_TABLES)) touch(path, read3[1].toLowerCase(), TABLE, READS);
|
|
7153
7153
|
continue;
|
|
@@ -7163,16 +7163,16 @@ async function extractData(builder, input) {
|
|
|
7163
7163
|
for (const read3 of rest.matchAll(READ_TABLES)) touch(path, read3[1].toLowerCase(), TABLE, READS);
|
|
7164
7164
|
}
|
|
7165
7165
|
}
|
|
7166
|
-
for (const match of
|
|
7167
|
-
touch(path, `${match[1]}.${match[2]}`, NAMESPACE, accessFor(
|
|
7166
|
+
for (const match of text4.matchAll(NS_CONST)) {
|
|
7167
|
+
touch(path, `${match[1]}.${match[2]}`, NAMESPACE, accessFor(text4, match.index ?? 0));
|
|
7168
7168
|
}
|
|
7169
|
-
for (const match of
|
|
7170
|
-
touch(path, match[1], NAMESPACE, accessFor(
|
|
7169
|
+
for (const match of text4.matchAll(NS_LITERAL)) {
|
|
7170
|
+
touch(path, match[1], NAMESPACE, accessFor(text4, match.index ?? 0));
|
|
7171
7171
|
}
|
|
7172
7172
|
}
|
|
7173
7173
|
}
|
|
7174
|
-
function accessFor(
|
|
7175
|
-
const window =
|
|
7174
|
+
function accessFor(text4, index) {
|
|
7175
|
+
const window = text4.slice(Math.max(0, index - 160), index + 40);
|
|
7176
7176
|
return /\b(?:transact|update|delete|create|insert|Ops)\b/.test(window) ? WRITES : READS;
|
|
7177
7177
|
}
|
|
7178
7178
|
async function buildCodeGraph(input) {
|
|
@@ -7184,7 +7184,7 @@ async function buildCodeGraph(input) {
|
|
|
7184
7184
|
return builder.build();
|
|
7185
7185
|
}
|
|
7186
7186
|
|
|
7187
|
-
// ../harness/dist/chunk-
|
|
7187
|
+
// ../harness/dist/chunk-ISR434K7.js
|
|
7188
7188
|
var import_crypto4 = require("crypto");
|
|
7189
7189
|
async function digestStagedWorkspace(root, limits) {
|
|
7190
7190
|
const files = [];
|
|
@@ -7512,7 +7512,7 @@ function validateCodePatch(rawPatch, maxBytes) {
|
|
|
7512
7512
|
if (FORBIDDEN.test(patch2) || /(?:old|new)(?: file)? mode 120000/.test(patch2)) {
|
|
7513
7513
|
throw new TypeError("patch uses a forbidden binary, link, mode, rename, or copy operation");
|
|
7514
7514
|
}
|
|
7515
|
-
const
|
|
7515
|
+
const paths2 = [];
|
|
7516
7516
|
const lines = patch2.split("\n");
|
|
7517
7517
|
for (let index = 0; index < lines.length; index += 1) {
|
|
7518
7518
|
const line2 = lines[index];
|
|
@@ -7528,10 +7528,10 @@ function validateCodePatch(rawPatch, maxBytes) {
|
|
|
7528
7528
|
if (!validHeaderPath(oldPath, path, "a") || !validHeaderPath(newPath, path, "b")) {
|
|
7529
7529
|
throw new TypeError("patch file headers do not match the declared path");
|
|
7530
7530
|
}
|
|
7531
|
-
|
|
7531
|
+
paths2.push(path);
|
|
7532
7532
|
}
|
|
7533
|
-
if (!
|
|
7534
|
-
return
|
|
7533
|
+
if (!paths2.length || new Set(paths2).size !== paths2.length) throw new TypeError("patch has no diffs or repeats a path");
|
|
7534
|
+
return paths2;
|
|
7535
7535
|
}
|
|
7536
7536
|
function validHeaderPath(value2, path, prefix) {
|
|
7537
7537
|
return value2 === "/dev/null" || value2 === `${prefix}/${path}`;
|
|
@@ -7556,11 +7556,11 @@ function describePatchFailure(patch2, detail) {
|
|
|
7556
7556
|
const hint = hunks.length > 0 && contextless ? " A hunk has no context lines; include at least one unchanged line above or below each change." : "";
|
|
7557
7557
|
return `patch did not apply: ${detail}${hint}`;
|
|
7558
7558
|
}
|
|
7559
|
-
async function applyCodePatch(workspaceDir, rawPatch,
|
|
7559
|
+
async function applyCodePatch(workspaceDir, rawPatch, paths2) {
|
|
7560
7560
|
const patch2 = stripPatchEnvelope(rawPatch);
|
|
7561
7561
|
await gitApply(workspaceDir, patch2, true);
|
|
7562
7562
|
await gitApply(workspaceDir, patch2, false);
|
|
7563
|
-
for (const path of
|
|
7563
|
+
for (const path of paths2) {
|
|
7564
7564
|
try {
|
|
7565
7565
|
const info = await (0, import_promises6.lstat)(resolveCodePath(workspaceDir, path));
|
|
7566
7566
|
if (info.isSymbolicLink() || !info.isFile() && !info.isDirectory()) {
|
|
@@ -7582,8 +7582,8 @@ function gitApply(cwd, patch2, check) {
|
|
|
7582
7582
|
});
|
|
7583
7583
|
let stderr2 = "";
|
|
7584
7584
|
child.stderr.setEncoding("utf8");
|
|
7585
|
-
child.stderr.on("data", (
|
|
7586
|
-
if (stderr2.length < 4e3) stderr2 +=
|
|
7585
|
+
child.stderr.on("data", (text22) => {
|
|
7586
|
+
if (stderr2.length < 4e3) stderr2 += text22.slice(0, 4e3);
|
|
7587
7587
|
});
|
|
7588
7588
|
child.once("error", reject);
|
|
7589
7589
|
child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr2.trim().slice(0, 500)))));
|
|
@@ -7607,8 +7607,8 @@ async function restoreCodeWorkspaceCheckpoint(input) {
|
|
|
7607
7607
|
const workspace = await stageWorkspace(input.trustedBaseDir, input.stage);
|
|
7608
7608
|
try {
|
|
7609
7609
|
if (checkpoint.patch) {
|
|
7610
|
-
const
|
|
7611
|
-
await applyCodePatch(workspace.workspaceDir, checkpoint.patch,
|
|
7610
|
+
const paths2 = validateCodePatch(checkpoint.patch, 256 * 1024);
|
|
7611
|
+
await applyCodePatch(workspace.workspaceDir, checkpoint.patch, paths2);
|
|
7612
7612
|
}
|
|
7613
7613
|
return { workspace, checkpoint };
|
|
7614
7614
|
} catch (error) {
|
|
@@ -7757,8 +7757,8 @@ async function verifyCodeCandidate(input) {
|
|
|
7757
7757
|
try {
|
|
7758
7758
|
const baseDigest = await digestStagedWorkspace(staged.workspaceDir, limits);
|
|
7759
7759
|
if (baseDigest !== input.trustedBaseDigest) throw new TypeError("trusted base does not match its registered digest");
|
|
7760
|
-
const
|
|
7761
|
-
await applyCodePatch(staged.workspaceDir, input.candidatePatch,
|
|
7760
|
+
const paths2 = validateCodePatch(input.candidatePatch, policy.maximumPatchBytes);
|
|
7761
|
+
await applyCodePatch(staged.workspaceDir, input.candidatePatch, paths2);
|
|
7762
7762
|
const sourceDigest = await digestStagedWorkspace(staged.workspaceDir, limits);
|
|
7763
7763
|
const policyDigest = digestPolicy(policy);
|
|
7764
7764
|
const patchDigest = digestBytes(input.candidatePatch);
|
|
@@ -7767,7 +7767,7 @@ async function verifyCodeCandidate(input) {
|
|
|
7767
7767
|
trustedBaseDigest: input.trustedBaseDigest,
|
|
7768
7768
|
patchDigest
|
|
7769
7769
|
});
|
|
7770
|
-
const changedTests = changedTestPaths(
|
|
7770
|
+
const changedTests = changedTestPaths(paths2, policy);
|
|
7771
7771
|
if (changedTests.length > policy.maximumChangedTests) throw new TypeError("candidate changes too many test files");
|
|
7772
7772
|
const recipes = [];
|
|
7773
7773
|
const logs = [];
|
|
@@ -7834,8 +7834,8 @@ function validate2(input) {
|
|
|
7834
7834
|
}
|
|
7835
7835
|
return result;
|
|
7836
7836
|
}
|
|
7837
|
-
function changedTestPaths(
|
|
7838
|
-
return
|
|
7837
|
+
function changedTestPaths(paths2, policy) {
|
|
7838
|
+
return paths2.filter((path) => policy.testPathSuffixes.some((suffix) => path.endsWith(suffix)) || policy.testPathPrefixes.some((prefix) => path.startsWith(prefix) || path.includes(`/${prefix}`))).sort();
|
|
7839
7839
|
}
|
|
7840
7840
|
function recipeReceipt(recipe2, result, artifacts) {
|
|
7841
7841
|
const status = result.timedOut ? "timed_out" : result.outputLimitExceeded ? "output_limited" : result.exitCode === 0 && artifacts.every((item) => item.status === "verified") ? "passed" : "failed";
|
|
@@ -8566,7 +8566,7 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
|
|
|
8566
8566
|
files(root) {
|
|
8567
8567
|
const existing = cache2.get(root);
|
|
8568
8568
|
if (existing) return existing;
|
|
8569
|
-
const pending = enumerate(root, limit).then((
|
|
8569
|
+
const pending = enumerate(root, limit).then((paths2) => Object.freeze(paths2));
|
|
8570
8570
|
cache2.set(root, pending);
|
|
8571
8571
|
void pending.catch(() => {
|
|
8572
8572
|
if (cache2.get(root) === pending) cache2.delete(root);
|
|
@@ -8579,7 +8579,7 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
|
|
|
8579
8579
|
};
|
|
8580
8580
|
}
|
|
8581
8581
|
async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
|
|
8582
|
-
const
|
|
8582
|
+
const paths2 = [];
|
|
8583
8583
|
const walk = async (directory) => {
|
|
8584
8584
|
for (const entry of await (0, import_promises9.readdir)(directory, { withFileTypes: true })) {
|
|
8585
8585
|
if (SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
|
|
@@ -8593,26 +8593,26 @@ async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
|
|
|
8593
8593
|
} catch {
|
|
8594
8594
|
continue;
|
|
8595
8595
|
}
|
|
8596
|
-
|
|
8597
|
-
if (
|
|
8596
|
+
paths2.push(path);
|
|
8597
|
+
if (paths2.length > limit) throw new TypeError("workspace file registry exceeds its bound");
|
|
8598
8598
|
}
|
|
8599
8599
|
}
|
|
8600
8600
|
};
|
|
8601
8601
|
await walk((0, import_path9.resolve)(root));
|
|
8602
|
-
return
|
|
8602
|
+
return paths2.sort();
|
|
8603
8603
|
}
|
|
8604
|
-
function listWorkspace(
|
|
8604
|
+
function listWorkspace(paths2, options = {}) {
|
|
8605
8605
|
const max = options.maxEntries ?? 1e3;
|
|
8606
8606
|
const prefix = options.prefix?.replace(/\/+$/, "");
|
|
8607
|
-
const scoped = prefix ?
|
|
8607
|
+
const scoped = prefix ? paths2.filter((path) => path === prefix || path.startsWith(`${prefix}/`)) : [...paths2];
|
|
8608
8608
|
return scoped.slice(0, max);
|
|
8609
8609
|
}
|
|
8610
|
-
async function searchWorkspace(root,
|
|
8610
|
+
async function searchWorkspace(root, paths2, options) {
|
|
8611
8611
|
options.signal?.throwIfAborted();
|
|
8612
8612
|
if (!options.query) throw new TypeError("search query must be a non-empty string");
|
|
8613
8613
|
const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
|
|
8614
8614
|
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
8615
|
-
const scoped = listWorkspace(
|
|
8615
|
+
const scoped = listWorkspace(paths2, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths2.length });
|
|
8616
8616
|
if (scoped.length === 0) return [];
|
|
8617
8617
|
try {
|
|
8618
8618
|
return await nativeSearch(root, scoped, { ...options, maxResults, maxFileBytes });
|
|
@@ -8622,11 +8622,11 @@ async function searchWorkspace(root, paths, options) {
|
|
|
8622
8622
|
}
|
|
8623
8623
|
}
|
|
8624
8624
|
var MAX_NATIVE_ARG_BYTES = 96 * 1024;
|
|
8625
|
-
async function nativeSearch(root,
|
|
8625
|
+
async function nativeSearch(root, paths2, options) {
|
|
8626
8626
|
const batches = [];
|
|
8627
8627
|
let batch = [];
|
|
8628
8628
|
let bytes = 0;
|
|
8629
|
-
for (const path of
|
|
8629
|
+
for (const path of paths2) {
|
|
8630
8630
|
const size = Buffer.byteLength(path) + 1;
|
|
8631
8631
|
if (batch.length > 0 && bytes + size > MAX_NATIVE_ARG_BYTES) {
|
|
8632
8632
|
batches.push(batch);
|
|
@@ -8645,7 +8645,7 @@ async function nativeSearch(root, paths, options) {
|
|
|
8645
8645
|
}
|
|
8646
8646
|
return matches;
|
|
8647
8647
|
}
|
|
8648
|
-
function nativeSearchBatch(root,
|
|
8648
|
+
function nativeSearchBatch(root, paths2, options, remaining) {
|
|
8649
8649
|
return new Promise((resolveMatches, reject) => {
|
|
8650
8650
|
const args = [
|
|
8651
8651
|
"--fixed-strings",
|
|
@@ -8658,7 +8658,7 @@ function nativeSearchBatch(root, paths, options, remaining) {
|
|
|
8658
8658
|
options.caseSensitive === false ? "--ignore-case" : "--case-sensitive",
|
|
8659
8659
|
"--",
|
|
8660
8660
|
options.query,
|
|
8661
|
-
...
|
|
8661
|
+
...paths2
|
|
8662
8662
|
];
|
|
8663
8663
|
const child = (0, import_child_process6.spawn)("rg", args, {
|
|
8664
8664
|
cwd: root,
|
|
@@ -8844,16 +8844,16 @@ function createCodePolicyGate(options) {
|
|
|
8844
8844
|
}
|
|
8845
8845
|
};
|
|
8846
8846
|
}
|
|
8847
|
-
function directoryPrefixes(
|
|
8847
|
+
function directoryPrefixes(paths2) {
|
|
8848
8848
|
const prefixes = /* @__PURE__ */ new Set(["."]);
|
|
8849
|
-
for (const path of
|
|
8849
|
+
for (const path of paths2) {
|
|
8850
8850
|
const parts = path.split("/");
|
|
8851
8851
|
for (let index = 1; index < parts.length; index += 1) prefixes.add(parts.slice(0, index).join("/"));
|
|
8852
8852
|
}
|
|
8853
8853
|
return [...prefixes].sort();
|
|
8854
8854
|
}
|
|
8855
|
-
async function safePrefix(base,
|
|
8856
|
-
const prefixes = directoryPrefixes(
|
|
8855
|
+
async function safePrefix(base, paths2, prefix) {
|
|
8856
|
+
const prefixes = directoryPrefixes(paths2);
|
|
8857
8857
|
const conversions = await conversionRegistry(
|
|
8858
8858
|
[await registeredPolicy("code.prefix.v1", "code.prefixes.v1", prefixes)],
|
|
8859
8859
|
{ "code.prefixes.v1": prefixes }
|
|
@@ -8965,7 +8965,7 @@ function response(request3, ok, content2, details) {
|
|
|
8965
8965
|
return { requestId: request3.requestId, ok, content: content2, ...details ? { details } : {} };
|
|
8966
8966
|
}
|
|
8967
8967
|
var cache = /* @__PURE__ */ new Map();
|
|
8968
|
-
function workspaceGraphs(workspaceDir,
|
|
8968
|
+
function workspaceGraphs(workspaceDir, paths2) {
|
|
8969
8969
|
const existing = cache.get(workspaceDir);
|
|
8970
8970
|
if (existing) return existing;
|
|
8971
8971
|
const read22 = (path) => (0, import_promises11.readFile)((0, import_path10.join)(workspaceDir, path), "utf8");
|
|
@@ -8973,7 +8973,7 @@ function workspaceGraphs(workspaceDir, paths) {
|
|
|
8973
8973
|
// No knownTables: a staged workspace may not carry migrations, and a filter
|
|
8974
8974
|
// that silently drops every table is worse than an unfiltered one. Callers
|
|
8975
8975
|
// with ground truth should build the graph themselves.
|
|
8976
|
-
graph: await buildCodeGraph({ paths, read: read22, data: { ignore: (path) => path.includes(".generated.") } })
|
|
8976
|
+
graph: await buildCodeGraph({ paths: paths2, read: read22, data: { ignore: (path) => path.includes(".generated.") } })
|
|
8977
8977
|
}))();
|
|
8978
8978
|
cache.set(workspaceDir, built);
|
|
8979
8979
|
return built;
|
|
@@ -9033,11 +9033,11 @@ async function read(context, request3, options, policy, registry) {
|
|
|
9033
9033
|
if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
|
|
9034
9034
|
throw new TypeError("requested line range exceeds its bound");
|
|
9035
9035
|
}
|
|
9036
|
-
const
|
|
9037
|
-
if (!
|
|
9036
|
+
const paths2 = await registry.files(context.workspaceDir);
|
|
9037
|
+
if (!paths2.includes(path)) {
|
|
9038
9038
|
throw new TypeError(`no such file in the staged workspace: "${path}". Use sandbox.overview, sandbox.where_is or sandbox.search to find the correct path.`);
|
|
9039
9039
|
}
|
|
9040
|
-
const allowed = await policy.read(policyContext(context, request3, options, { paths, path, startLine, endLine }));
|
|
9040
|
+
const allowed = await policy.read(policyContext(context, request3, options, { paths: paths2, path, startLine, endLine }));
|
|
9041
9041
|
if (!allowed) return response(request3, false, "tool denied by CaMeL policy");
|
|
9042
9042
|
const target = resolveCodePath(context.workspaceDir, path);
|
|
9043
9043
|
const info = await (0, import_promises10.stat)(target);
|
|
@@ -9059,16 +9059,16 @@ async function list(context, request3, options, policy, registry) {
|
|
|
9059
9059
|
const prefix = typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
9060
9060
|
const maxEntries = optionalInteger(request3.input.maxEntries) ?? 1e3;
|
|
9061
9061
|
if (maxEntries > 5e3) throw new TypeError("maxEntries exceeds its bound");
|
|
9062
|
-
const
|
|
9063
|
-
const allowed = await policy.list(policyContext(context, request3, options, { paths, ...prefix ? { prefix } : {} }));
|
|
9062
|
+
const paths2 = await registry.files(context.workspaceDir);
|
|
9063
|
+
const allowed = await policy.list(policyContext(context, request3, options, { paths: paths2, ...prefix ? { prefix } : {} }));
|
|
9064
9064
|
if (!allowed) return response(request3, false, "tool denied by CaMeL policy");
|
|
9065
|
-
const entries = listWorkspace(
|
|
9065
|
+
const entries = listWorkspace(paths2, { ...prefix ? { prefix } : {}, maxEntries });
|
|
9066
9066
|
if (!entries.length) {
|
|
9067
9067
|
return response(request3, true, prefix ? `No files under "${prefix}".` : "Workspace is empty.", { count: 0 });
|
|
9068
9068
|
}
|
|
9069
|
-
const truncated = entries.length <
|
|
9070
|
-
const hint = !prefix &&
|
|
9071
|
-
\u2026 ${
|
|
9069
|
+
const truncated = entries.length < paths2.length && entries.length === maxEntries;
|
|
9070
|
+
const hint = !prefix && paths2.length > 500 ? `
|
|
9071
|
+
\u2026 ${paths2.length} files total. sandbox.overview is far cheaper for orientation; use a prefix here once you know the area.` : "";
|
|
9072
9072
|
return response(
|
|
9073
9073
|
request3,
|
|
9074
9074
|
true,
|
|
@@ -9086,10 +9086,10 @@ async function search(context, request3, options, policy, registry) {
|
|
|
9086
9086
|
const maxResults = optionalInteger(request3.input.maxResults) ?? 100;
|
|
9087
9087
|
if (maxResults > 500) throw new TypeError("maxResults exceeds its bound");
|
|
9088
9088
|
const caseSensitive = request3.input.caseSensitive === void 0 ? true : request3.input.caseSensitive === true;
|
|
9089
|
-
const
|
|
9090
|
-
const allowed = await policy.search(policyContext(context, request3, options, { paths, query, ...prefix ? { prefix } : {} }));
|
|
9089
|
+
const paths2 = await registry.files(context.workspaceDir);
|
|
9090
|
+
const allowed = await policy.search(policyContext(context, request3, options, { paths: paths2, query, ...prefix ? { prefix } : {} }));
|
|
9091
9091
|
if (!allowed) return response(request3, false, "tool denied by CaMeL policy");
|
|
9092
|
-
const matches = await searchWorkspace(context.workspaceDir,
|
|
9092
|
+
const matches = await searchWorkspace(context.workspaceDir, paths2, {
|
|
9093
9093
|
query,
|
|
9094
9094
|
maxResults,
|
|
9095
9095
|
caseSensitive,
|
|
@@ -9111,8 +9111,8 @@ async function graphQuery(context, request3, options, policy, registry) {
|
|
|
9111
9111
|
selector: query
|
|
9112
9112
|
}));
|
|
9113
9113
|
if (!allowed) return response(request3, false, "tool denied by CaMeL policy");
|
|
9114
|
-
const
|
|
9115
|
-
const graphs = await workspaceGraphs(context.workspaceDir,
|
|
9114
|
+
const paths2 = await registry.files(context.workspaceDir);
|
|
9115
|
+
const graphs = await workspaceGraphs(context.workspaceDir, paths2);
|
|
9116
9116
|
if (request3.tool === "sandbox.overview") {
|
|
9117
9117
|
return response(request3, true, renderOverview(graphs, query || void 0));
|
|
9118
9118
|
}
|
|
@@ -9174,16 +9174,16 @@ function toolFailureMessage(reason) {
|
|
|
9174
9174
|
async function patch(context, request3, options, policy, registry) {
|
|
9175
9175
|
exactKeys(request3.input, ["patch"]);
|
|
9176
9176
|
const value2 = stringField(request3.input, "patch");
|
|
9177
|
-
const
|
|
9178
|
-
if (
|
|
9177
|
+
const paths2 = validateCodePatch(value2, options.maxPatchBytes ?? 256 * 1024);
|
|
9178
|
+
if (paths2.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
|
|
9179
9179
|
throw new TypeError("patch targets a read-only reference source");
|
|
9180
9180
|
}
|
|
9181
9181
|
const allowed = await policy.patch(policyContext(context, request3, options, { patch: value2 }));
|
|
9182
9182
|
if (!allowed) return response(request3, false, "tool denied by CaMeL policy");
|
|
9183
|
-
await applyCodePatch(context.workspaceDir, value2,
|
|
9183
|
+
await applyCodePatch(context.workspaceDir, value2, paths2);
|
|
9184
9184
|
registry.invalidate(context.workspaceDir);
|
|
9185
9185
|
forgetWorkspaceGraphs(context.workspaceDir);
|
|
9186
|
-
return response(request3, true, `Applied patch to ${
|
|
9186
|
+
return response(request3, true, `Applied patch to ${paths2.length} file(s).`, { paths: paths2 });
|
|
9187
9187
|
}
|
|
9188
9188
|
async function recipe(context, request3, options, recipes, policy) {
|
|
9189
9189
|
exactKeys(request3.input, ["recipeId"]);
|
|
@@ -9532,11 +9532,126 @@ async function startGoalPursuit(input) {
|
|
|
9532
9532
|
async function appendCodeRuntimeEvent(control, command, event, refs) {
|
|
9533
9533
|
const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
|
|
9534
9534
|
refs.push(eventId);
|
|
9535
|
-
const
|
|
9535
|
+
const attributed = { ...event, interactionId: command.commandId };
|
|
9536
|
+
const bounded = attributed.type === "message" ? { ...attributed, body: attributed.body.trim().slice(0, 2e4) || `${attributed.actor} event` } : attributed;
|
|
9536
9537
|
await control.appendSessionEvent(command.sessionId, eventId, bounded);
|
|
9537
9538
|
}
|
|
9538
9539
|
var digestRuntimeValue = (value2) => `sha256:${(0, import_crypto4.createHash)("sha256").update(value2).digest("hex")}`;
|
|
9539
9540
|
var runtimeErrorMessage = (value2) => value2 instanceof Error ? value2.message : String(value2);
|
|
9541
|
+
var text3 = (value2, maximum) => {
|
|
9542
|
+
if (typeof value2 !== "string") return void 0;
|
|
9543
|
+
const bounded = value2.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim();
|
|
9544
|
+
return bounded ? bounded.slice(0, maximum) : void 0;
|
|
9545
|
+
};
|
|
9546
|
+
var integer2 = (value2) => Number.isSafeInteger(value2) && Number(value2) >= 0 ? Number(value2) : void 0;
|
|
9547
|
+
var record32 = (value2) => value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : void 0;
|
|
9548
|
+
var excerpt = (value2, tail = false) => {
|
|
9549
|
+
if (typeof value2 !== "string") return void 0;
|
|
9550
|
+
const safe = value2.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ");
|
|
9551
|
+
const source = (tail ? safe.slice(-1e4) : safe.slice(0, 1e4)).trim();
|
|
9552
|
+
if (!source) return void 0;
|
|
9553
|
+
const lines = source.split("\n").filter((line2) => line2.trim()).map((line2) => line2.slice(0, 240));
|
|
9554
|
+
const selected = tail ? lines.slice(-10) : lines.slice(0, 10);
|
|
9555
|
+
return text3(selected.join("\n"), 2400);
|
|
9556
|
+
};
|
|
9557
|
+
var paths = (value2) => {
|
|
9558
|
+
if (!Array.isArray(value2)) return void 0;
|
|
9559
|
+
const items = value2.flatMap((item) => {
|
|
9560
|
+
const path = text3(item, 1024);
|
|
9561
|
+
return path ? [path] : [];
|
|
9562
|
+
}).slice(0, 12);
|
|
9563
|
+
return items.length ? items : void 0;
|
|
9564
|
+
};
|
|
9565
|
+
function patchStats(value2) {
|
|
9566
|
+
if (typeof value2 !== "string") return {};
|
|
9567
|
+
let additions = 0;
|
|
9568
|
+
let deletions = 0;
|
|
9569
|
+
for (const line2 of value2.slice(0, 262144).split("\n")) {
|
|
9570
|
+
if (line2.startsWith("+++") || line2.startsWith("---")) continue;
|
|
9571
|
+
if (line2.startsWith("+")) additions += 1;
|
|
9572
|
+
else if (line2.startsWith("-")) deletions += 1;
|
|
9573
|
+
}
|
|
9574
|
+
return { ...additions ? { additions } : {}, ...deletions ? { deletions } : {} };
|
|
9575
|
+
}
|
|
9576
|
+
function searchResults(value2) {
|
|
9577
|
+
if (typeof value2 !== "string") return void 0;
|
|
9578
|
+
const results = value2.split("\n").flatMap((line2) => {
|
|
9579
|
+
const match = /^([^:\n]{1,1024}):(\d+):\s?(.*)$/.exec(line2);
|
|
9580
|
+
if (!match) return [];
|
|
9581
|
+
const lineNumber = Number(match[2]);
|
|
9582
|
+
const itemText = text3(match[3], 240);
|
|
9583
|
+
if (!Number.isSafeInteger(lineNumber) || lineNumber < 1) return [];
|
|
9584
|
+
return [{ path: match[1], line: lineNumber, ...itemText ? { text: itemText } : {} }];
|
|
9585
|
+
}).slice(0, 5);
|
|
9586
|
+
return results.length ? results : void 0;
|
|
9587
|
+
}
|
|
9588
|
+
function codeToolRequestPresentation(request3) {
|
|
9589
|
+
const input = request3.input;
|
|
9590
|
+
if (request3.tool === "sandbox.read") {
|
|
9591
|
+
const path = text3(input.path, 1024);
|
|
9592
|
+
if (!path) return void 0;
|
|
9593
|
+
const startLine = integer2(input.startLine);
|
|
9594
|
+
const endLine = integer2(input.endLine);
|
|
9595
|
+
return { kind: "read", path, ...startLine ? { startLine } : {}, ...endLine ? { endLine } : {} };
|
|
9596
|
+
}
|
|
9597
|
+
if (request3.tool === "sandbox.list") {
|
|
9598
|
+
const scope = text3(input.prefix, 1024);
|
|
9599
|
+
return { kind: "list", ...scope ? { scope } : {} };
|
|
9600
|
+
}
|
|
9601
|
+
if (request3.tool === "sandbox.search" || request3.tool === "sandbox.overview" || request3.tool === "sandbox.where_is" || request3.tool === "sandbox.who_imports" || request3.tool === "sandbox.who_touches") {
|
|
9602
|
+
const query = text3(input.query, 512);
|
|
9603
|
+
const scope = request3.tool === "sandbox.search" ? text3(input.prefix, 1024) : void 0;
|
|
9604
|
+
if (request3.tool === "sandbox.search" && !query) return void 0;
|
|
9605
|
+
return { kind: "query", ...query ? { query } : {}, ...scope ? { scope } : {} };
|
|
9606
|
+
}
|
|
9607
|
+
if (request3.tool === "sandbox.apply_patch") {
|
|
9608
|
+
return { kind: "patch", ...patchStats(input.patch) };
|
|
9609
|
+
}
|
|
9610
|
+
const recipeId = text3(input.recipeId, 120);
|
|
9611
|
+
return recipeId ? { kind: "recipe", recipeId } : void 0;
|
|
9612
|
+
}
|
|
9613
|
+
function codeToolResultPresentation(request3, response2) {
|
|
9614
|
+
const started = codeToolRequestPresentation(request3);
|
|
9615
|
+
if (!started || !response2.ok) return started;
|
|
9616
|
+
const details = record32(response2.details);
|
|
9617
|
+
if (started.kind === "read") {
|
|
9618
|
+
return {
|
|
9619
|
+
...started,
|
|
9620
|
+
...integer2(details?.startLine) ? { startLine: integer2(details?.startLine) } : {},
|
|
9621
|
+
...integer2(details?.endLine) ? { endLine: integer2(details?.endLine) } : {},
|
|
9622
|
+
...excerpt(response2.content) ? { excerpt: excerpt(response2.content) } : {}
|
|
9623
|
+
};
|
|
9624
|
+
}
|
|
9625
|
+
if (started.kind === "list") {
|
|
9626
|
+
const listed = response2.content.split("\n").filter((line2) => line2 && !line2.startsWith("\u2026") && !line2.startsWith("Workspace ")).map((line2) => text3(line2, 1024)).filter((line2) => Boolean(line2)).slice(0, 8);
|
|
9627
|
+
return {
|
|
9628
|
+
...started,
|
|
9629
|
+
...integer2(details?.count) !== void 0 ? { count: integer2(details?.count) } : {},
|
|
9630
|
+
...listed.length ? { paths: listed } : {}
|
|
9631
|
+
};
|
|
9632
|
+
}
|
|
9633
|
+
if (started.kind === "query") {
|
|
9634
|
+
const results = request3.tool === "sandbox.search" ? searchResults(response2.content) : void 0;
|
|
9635
|
+
const resultExcerpt = request3.tool === "sandbox.search" ? void 0 : excerpt(response2.content);
|
|
9636
|
+
return {
|
|
9637
|
+
...started,
|
|
9638
|
+
...integer2(details?.count) !== void 0 ? { count: integer2(details?.count) } : {},
|
|
9639
|
+
...results ? { results } : {},
|
|
9640
|
+
...resultExcerpt ? { excerpt: resultExcerpt } : {}
|
|
9641
|
+
};
|
|
9642
|
+
}
|
|
9643
|
+
if (started.kind === "patch") {
|
|
9644
|
+
return { ...started, ...paths(details?.paths) ? { paths: paths(details?.paths) } : {} };
|
|
9645
|
+
}
|
|
9646
|
+
const output = response2.content.replace(/^Recipe [^\n]*\.?\s*/u, "");
|
|
9647
|
+
return {
|
|
9648
|
+
...started,
|
|
9649
|
+
...integer2(details?.exitCode) !== void 0 ? { exitCode: integer2(details?.exitCode) } : {},
|
|
9650
|
+
...typeof details?.timedOut === "boolean" ? { timedOut: details.timedOut } : {},
|
|
9651
|
+
...typeof details?.outputLimitExceeded === "boolean" ? { outputLimitExceeded: details.outputLimitExceeded } : {},
|
|
9652
|
+
...excerpt(output, true) ? { excerpt: excerpt(output, true) } : {}
|
|
9653
|
+
};
|
|
9654
|
+
}
|
|
9540
9655
|
var TheseusRuntimeEngine = class {
|
|
9541
9656
|
constructor(options) {
|
|
9542
9657
|
this.options = options;
|
|
@@ -9770,18 +9885,29 @@ var TheseusRuntimeEngine = class {
|
|
|
9770
9885
|
return {
|
|
9771
9886
|
execute: async (context, request3) => {
|
|
9772
9887
|
const startedAt = Date.now();
|
|
9888
|
+
const operationId = digestRuntimeValue(`${command.commandId}:${request3.requestId}`);
|
|
9889
|
+
const startedPresentation = codeToolRequestPresentation(request3);
|
|
9773
9890
|
await this.#event(
|
|
9774
9891
|
command,
|
|
9775
|
-
{
|
|
9892
|
+
{
|
|
9893
|
+
type: "tool",
|
|
9894
|
+
phase: "started",
|
|
9895
|
+
tool: request3.tool,
|
|
9896
|
+
operationId,
|
|
9897
|
+
...startedPresentation ? { presentation: startedPresentation } : {}
|
|
9898
|
+
},
|
|
9776
9899
|
active.conversationRefs
|
|
9777
9900
|
).catch(() => void 0);
|
|
9778
9901
|
const response2 = await broker.execute(context, request3);
|
|
9902
|
+
const completedPresentation = codeToolResultPresentation(request3, response2);
|
|
9779
9903
|
await this.#event(command, {
|
|
9780
9904
|
type: "tool",
|
|
9781
9905
|
phase: "completed",
|
|
9782
9906
|
tool: request3.tool,
|
|
9783
9907
|
ok: response2.ok,
|
|
9784
|
-
durationMs: Date.now() - startedAt
|
|
9908
|
+
durationMs: Date.now() - startedAt,
|
|
9909
|
+
operationId,
|
|
9910
|
+
...completedPresentation ? { presentation: completedPresentation } : {}
|
|
9785
9911
|
}, active.conversationRefs).catch(() => void 0);
|
|
9786
9912
|
return response2;
|
|
9787
9913
|
}
|
|
@@ -10824,7 +10950,8 @@ Usage:
|
|
|
10824
10950
|
odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
10825
10951
|
odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
|
|
10826
10952
|
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]
|
|
10827
|
-
odla-ai pm next --app <id> [--project <id>] [--json]
|
|
10953
|
+
odla-ai pm next --app <id> [--project <id>] [--verbose] [--json]
|
|
10954
|
+
odla-ai pm start --app <id> [<task-id>] [--goal <id>] [--verbose] [--mutation-id <id>] [--json] [claims the top Ready task in one call]
|
|
10828
10955
|
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]
|
|
10829
10956
|
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]
|
|
10830
10957
|
odla-ai pm task claim <id> --expected-revision <n> [--mutation-id <id>] [--json]
|
|
@@ -10845,7 +10972,7 @@ Usage:
|
|
|
10845
10972
|
odla-ai pm <goal|task|decision|bug> comments <id> [--json]
|
|
10846
10973
|
odla-ai pm <goal|task|decision|bug> history <id> [--limit <n>] [--json]
|
|
10847
10974
|
odla-ai pm <goal|task|decision|bug> rm <id>
|
|
10848
|
-
odla-ai pm handoff --app <id> [--project <id>] [--json]
|
|
10975
|
+
odla-ai pm handoff --app <id> [--project <id>] [--verbose] [--json]
|
|
10849
10976
|
odla-ai discuss groups [--json]
|
|
10850
10977
|
odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--mentions] [--json]
|
|
10851
10978
|
odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
|
|
@@ -11010,6 +11137,16 @@ Commands:
|
|
|
11010
11137
|
execution contract and a reviewed revision. Any caller with PM
|
|
11011
11138
|
mutation access to that app/project may do it; claims coordinate
|
|
11012
11139
|
execution but never own or lock the PM record.
|
|
11140
|
+
To pick up work: "pm next" reads open goals, Ready candidates,
|
|
11141
|
+
and work in progress; "pm start" claims the top Ready task in
|
|
11142
|
+
one call, compare-and-swapped on the revision it just read, and
|
|
11143
|
+
returns the task, its goal, and the acceptance criteria to work
|
|
11144
|
+
against. Finish with "pm task done <id>", or hand it back with
|
|
11145
|
+
"pm task release <id> --expected-revision <n>". "pm start" never
|
|
11146
|
+
marks work Ready: that contract is reviewed separately so an
|
|
11147
|
+
agent cannot authorize its own work.
|
|
11148
|
+
Intake output is projected to the fields an executing caller
|
|
11149
|
+
acts on; --verbose returns whole records with their audit trail.
|
|
11013
11150
|
bug Intent-first alias for PM bugs. "bug report" writes to
|
|
11014
11151
|
odla PM; odla product defects do not belong in GitHub Issues.
|
|
11015
11152
|
discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
|
|
@@ -11760,6 +11897,15 @@ var FIELD_MAP = {
|
|
|
11760
11897
|
execution: { key: "executionMode" },
|
|
11761
11898
|
"expected-revision": { key: "expectedRevision", num: true }
|
|
11762
11899
|
};
|
|
11900
|
+
var PmRequestError = class extends Error {
|
|
11901
|
+
constructor(status, message2) {
|
|
11902
|
+
super(message2);
|
|
11903
|
+
this.status = status;
|
|
11904
|
+
this.name = "PmRequestError";
|
|
11905
|
+
}
|
|
11906
|
+
status;
|
|
11907
|
+
};
|
|
11908
|
+
var isRevisionConflict = (error) => error instanceof PmRequestError && error.status === 409;
|
|
11763
11909
|
async function pmRequest(ctx, method, path, body) {
|
|
11764
11910
|
const response2 = await ctx.doFetch(`${ctx.platformUrl}/registry/pm${path}`, {
|
|
11765
11911
|
method,
|
|
@@ -11779,7 +11925,8 @@ async function pmRequest(ctx, method, path, body) {
|
|
|
11779
11925
|
const message2 = error.message;
|
|
11780
11926
|
if (typeof message2 === "string" && message2.length > 0) detail = message2;
|
|
11781
11927
|
}
|
|
11782
|
-
throw new
|
|
11928
|
+
throw new PmRequestError(
|
|
11929
|
+
response2.status,
|
|
11783
11930
|
`pm ${method} ${path} failed: ${detail ?? `registry returned ${response2.status}`} (${response2.status})`
|
|
11784
11931
|
);
|
|
11785
11932
|
}
|
|
@@ -11794,8 +11941,8 @@ function collectFields(parsed, allowClear) {
|
|
|
11794
11941
|
if (allowClear) out[spec.key] = null;
|
|
11795
11942
|
continue;
|
|
11796
11943
|
}
|
|
11797
|
-
const
|
|
11798
|
-
out[spec.key] = spec.num ? Number(
|
|
11944
|
+
const text4 = stringOpt(value2);
|
|
11945
|
+
out[spec.key] = spec.num ? Number(text4) : text4;
|
|
11799
11946
|
}
|
|
11800
11947
|
return out;
|
|
11801
11948
|
}
|
|
@@ -11968,103 +12115,196 @@ async function pmTaskLifecycle(ctx, id2, action2, parsed) {
|
|
|
11968
12115
|
ctx.out.log(`task: ${label} \u2192 ${state2}`);
|
|
11969
12116
|
});
|
|
11970
12117
|
}
|
|
11971
|
-
async function
|
|
12118
|
+
async function pmRemove(ctx, entity, id2) {
|
|
12119
|
+
await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id2)}`);
|
|
12120
|
+
ctx.out.log(`deleted ${entity} ${id2}`);
|
|
12121
|
+
}
|
|
12122
|
+
|
|
12123
|
+
// src/pm-lean.ts
|
|
12124
|
+
var COMMON = ["id", "appId", "projectId", "title", "revision"];
|
|
12125
|
+
var PER_ENTITY = {
|
|
12126
|
+
goal: ["status", "proof", "targetPct", "currentPct"],
|
|
12127
|
+
task: [
|
|
12128
|
+
"column",
|
|
12129
|
+
"goalId",
|
|
12130
|
+
"alignmentDecisionId",
|
|
12131
|
+
"description",
|
|
12132
|
+
"acceptanceCriteria",
|
|
12133
|
+
"executionMode",
|
|
12134
|
+
"assigneeId",
|
|
12135
|
+
"claimedByPrincipalId",
|
|
12136
|
+
"dueAt"
|
|
12137
|
+
],
|
|
12138
|
+
decision: ["status", "body", "supersedesId"],
|
|
12139
|
+
bug: ["status", "severity", "description", "goalId", "assigneeId", "decisionId"]
|
|
12140
|
+
};
|
|
12141
|
+
function leanRecord(entity, record11) {
|
|
12142
|
+
const out = {};
|
|
12143
|
+
for (const key of [...COMMON, ...PER_ENTITY[entity]]) {
|
|
12144
|
+
const value2 = record11[key];
|
|
12145
|
+
if (value2 !== void 0 && value2 !== null) out[key] = value2;
|
|
12146
|
+
}
|
|
12147
|
+
return out;
|
|
12148
|
+
}
|
|
12149
|
+
function leanRecords(entity, records, verbose) {
|
|
12150
|
+
return verbose ? records : records.map((record11) => leanRecord(entity, record11));
|
|
12151
|
+
}
|
|
12152
|
+
|
|
12153
|
+
// src/pm-intake.ts
|
|
12154
|
+
var READY_COLUMN = "todo";
|
|
12155
|
+
var UNMET_GOAL_STATUS = "open";
|
|
12156
|
+
var ACTIVE_TASK_COLUMNS = "backlog,todo,doing,review";
|
|
12157
|
+
var OPEN_BUG_STATUSES = "open,triaged";
|
|
12158
|
+
var wantsVerbose = (parsed) => parsed.options.verbose === true;
|
|
12159
|
+
async function listFiltered(ctx, entity, appId, projectId, filters) {
|
|
11972
12160
|
const records = [];
|
|
11973
12161
|
for (; ; ) {
|
|
11974
|
-
const q = new URLSearchParams({
|
|
11975
|
-
app: appId,
|
|
11976
|
-
limit: "100",
|
|
11977
|
-
offset: String(records.length)
|
|
11978
|
-
});
|
|
12162
|
+
const q = new URLSearchParams({ app: appId, limit: "100", offset: String(records.length) });
|
|
11979
12163
|
if (projectId) q.set("project", projectId);
|
|
12164
|
+
for (const [key, value2] of Object.entries(filters)) q.set(key, value2);
|
|
11980
12165
|
const page2 = await pmRequest(ctx, "GET", `/${entity}?${q}`);
|
|
11981
12166
|
records.push(...page2.records);
|
|
11982
12167
|
if (records.length >= page2.total || page2.records.length === 0) return records;
|
|
11983
12168
|
}
|
|
11984
12169
|
}
|
|
11985
|
-
|
|
12170
|
+
function requireApp(ctx, parsed, command) {
|
|
11986
12171
|
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
11987
|
-
|
|
11988
|
-
|
|
12172
|
+
if (!appId) throw new Error(`pm ${command} needs --app <appId>`);
|
|
12173
|
+
return { appId, projectId: stringOpt(parsed.options.project) ?? ctx.projectId };
|
|
12174
|
+
}
|
|
12175
|
+
async function pmNext(ctx, parsed) {
|
|
12176
|
+
const { appId, projectId } = requireApp(ctx, parsed, "next");
|
|
12177
|
+
const verbose = wantsVerbose(parsed);
|
|
11989
12178
|
const [goals, tasks] = await Promise.all([
|
|
11990
|
-
|
|
11991
|
-
|
|
12179
|
+
listFiltered(ctx, "goal", appId, projectId, { status: UNMET_GOAL_STATUS }),
|
|
12180
|
+
// One request for both live columns; the split below is over open work only.
|
|
12181
|
+
listFiltered(ctx, "task", appId, projectId, { column: `${READY_COLUMN},doing` })
|
|
11992
12182
|
]);
|
|
12183
|
+
const ready = tasks.filter((record11) => record11.column === READY_COLUMN);
|
|
12184
|
+
const doing = tasks.filter((record11) => record11.column === "doing");
|
|
12185
|
+
const guidance = !goals.length ? "discuss alignment with the user before creating or claiming project work" : !ready.length ? 'refine a linked Backlog task and mark it Ready with "pm task ready <id>"' : `claim the top Ready task in one call: odla-ai pm start --app ${appId}`;
|
|
11993
12186
|
const result = {
|
|
11994
12187
|
appId,
|
|
11995
12188
|
projectId,
|
|
11996
|
-
openGoals:
|
|
11997
|
-
doing:
|
|
11998
|
-
ready:
|
|
12189
|
+
openGoals: leanRecords("goal", goals, verbose),
|
|
12190
|
+
doing: leanRecords("task", doing, verbose),
|
|
12191
|
+
ready: leanRecords("task", ready, verbose),
|
|
12192
|
+
next: guidance
|
|
11999
12193
|
};
|
|
12000
12194
|
emit2(ctx, result, () => {
|
|
12001
12195
|
ctx.out.log(`${appId}: goal-aligned work intake (read only)`);
|
|
12002
|
-
for (const [label, records] of [
|
|
12003
|
-
["doing",
|
|
12004
|
-
["ready",
|
|
12005
|
-
["open goals",
|
|
12196
|
+
for (const [label, entity, records] of [
|
|
12197
|
+
["doing", "task", doing],
|
|
12198
|
+
["ready", "task", ready],
|
|
12199
|
+
["open goals", "goal", goals]
|
|
12006
12200
|
]) {
|
|
12007
12201
|
ctx.out.log(`${label}:`);
|
|
12008
12202
|
if (!records.length) ctx.out.log("- (none)");
|
|
12009
|
-
else for (const record11 of records) printRecord(
|
|
12010
|
-
ctx,
|
|
12011
|
-
label === "open goals" ? "goal" : "task",
|
|
12012
|
-
record11
|
|
12013
|
-
);
|
|
12014
|
-
}
|
|
12015
|
-
if (!result.openGoals.length) {
|
|
12016
|
-
ctx.out.log("next: discuss alignment with the user before creating or claiming project work");
|
|
12017
|
-
} else if (!result.ready.length) {
|
|
12018
|
-
ctx.out.log("next: refine a linked Backlog task and mark it Ready");
|
|
12019
|
-
} else {
|
|
12020
|
-
ctx.out.log("next: review a Ready task, then claim it with its revision");
|
|
12203
|
+
else for (const record11 of records) printRecord(ctx, entity, record11);
|
|
12021
12204
|
}
|
|
12205
|
+
ctx.out.log(`next: ${guidance}`);
|
|
12022
12206
|
});
|
|
12023
12207
|
}
|
|
12024
12208
|
async function pmHandoff(ctx, parsed) {
|
|
12025
|
-
const appId =
|
|
12026
|
-
const
|
|
12027
|
-
if (!appId) throw new Error("pm handoff needs --app <appId>");
|
|
12209
|
+
const { appId, projectId } = requireApp(ctx, parsed, "handoff");
|
|
12210
|
+
const verbose = wantsVerbose(parsed);
|
|
12028
12211
|
const [goals, tasks, bugs] = await Promise.all([
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
|
|
12212
|
+
listFiltered(ctx, "goal", appId, projectId, { status: UNMET_GOAL_STATUS }),
|
|
12213
|
+
listFiltered(ctx, "task", appId, projectId, { column: ACTIVE_TASK_COLUMNS }),
|
|
12214
|
+
listFiltered(ctx, "bug", appId, projectId, { status: OPEN_BUG_STATUSES })
|
|
12032
12215
|
]);
|
|
12033
|
-
const
|
|
12216
|
+
const clean4 = !goals.length && !tasks.length && !bugs.length;
|
|
12217
|
+
const result = {
|
|
12034
12218
|
appId,
|
|
12035
12219
|
projectId,
|
|
12036
|
-
unmetGoals:
|
|
12037
|
-
activeTasks:
|
|
12038
|
-
openBugs:
|
|
12039
|
-
|
|
12040
|
-
const result = {
|
|
12041
|
-
...handoff,
|
|
12042
|
-
clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
|
|
12220
|
+
unmetGoals: leanRecords("goal", goals, verbose),
|
|
12221
|
+
activeTasks: leanRecords("task", tasks, verbose),
|
|
12222
|
+
openBugs: leanRecords("bug", bugs, verbose),
|
|
12223
|
+
clean: clean4
|
|
12043
12224
|
};
|
|
12044
12225
|
emit2(ctx, result, () => {
|
|
12045
|
-
if (
|
|
12226
|
+
if (clean4) {
|
|
12046
12227
|
ctx.out.log(`${appId}: no unresolved PM work`);
|
|
12047
12228
|
return;
|
|
12048
12229
|
}
|
|
12049
12230
|
ctx.out.log(`${appId}: authoritative PM handoff`);
|
|
12050
|
-
for (const [label, records] of [
|
|
12051
|
-
["unmet goals",
|
|
12052
|
-
["active tasks",
|
|
12053
|
-
["open bugs",
|
|
12231
|
+
for (const [label, entity, records] of [
|
|
12232
|
+
["unmet goals", "goal", goals],
|
|
12233
|
+
["active tasks", "task", tasks],
|
|
12234
|
+
["open bugs", "bug", bugs]
|
|
12054
12235
|
]) {
|
|
12055
12236
|
ctx.out.log(`${label}:`);
|
|
12056
12237
|
if (!records.length) ctx.out.log("- (none)");
|
|
12057
|
-
else for (const record11 of records) printRecord(
|
|
12058
|
-
ctx,
|
|
12059
|
-
label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
|
|
12060
|
-
record11
|
|
12061
|
-
);
|
|
12238
|
+
else for (const record11 of records) printRecord(ctx, entity, record11);
|
|
12062
12239
|
}
|
|
12063
12240
|
});
|
|
12064
12241
|
}
|
|
12065
|
-
|
|
12066
|
-
|
|
12067
|
-
|
|
12242
|
+
|
|
12243
|
+
// src/pm-start.ts
|
|
12244
|
+
var getTask = async (ctx, id2) => (await pmRequest(ctx, "GET", `/task/${encodeURIComponent(id2)}`)).record;
|
|
12245
|
+
async function claimAt(ctx, task, parsed) {
|
|
12246
|
+
const res = await pmRequest(
|
|
12247
|
+
ctx,
|
|
12248
|
+
"POST",
|
|
12249
|
+
`/task/${encodeURIComponent(task.id)}/claim`,
|
|
12250
|
+
{ expectedRevision: task.revision, mutationId: writeMutationId2(parsed) }
|
|
12251
|
+
);
|
|
12252
|
+
return res.record;
|
|
12253
|
+
}
|
|
12254
|
+
async function selectTask(ctx, parsed, appId, projectId) {
|
|
12255
|
+
const named = parsed.positionals[2];
|
|
12256
|
+
if (named) return getTask(ctx, named);
|
|
12257
|
+
const goalId = stringOpt(parsed.options.goal);
|
|
12258
|
+
const ready = await listFiltered(ctx, "task", appId, projectId, {
|
|
12259
|
+
column: READY_COLUMN,
|
|
12260
|
+
...goalId ? { goalId } : {}
|
|
12261
|
+
});
|
|
12262
|
+
const candidate = ready[0];
|
|
12263
|
+
if (!candidate) {
|
|
12264
|
+
throw new Error(
|
|
12265
|
+
`no Ready task to claim in ${appId}${goalId ? ` for goal ${goalId}` : ""}. Run "odla-ai pm next --app ${appId}" to see open goals and Backlog work, then "odla-ai pm task ready <id>" once a task has a complete contract.`
|
|
12266
|
+
);
|
|
12267
|
+
}
|
|
12268
|
+
return candidate;
|
|
12269
|
+
}
|
|
12270
|
+
async function pmStart(ctx, parsed) {
|
|
12271
|
+
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
12272
|
+
if (!appId) throw new Error("pm start needs --app <appId>");
|
|
12273
|
+
const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
|
|
12274
|
+
const verbose = wantsVerbose(parsed);
|
|
12275
|
+
const selected = await selectTask(ctx, parsed, appId, projectId);
|
|
12276
|
+
if (selected.column !== READY_COLUMN) {
|
|
12277
|
+
throw new Error(
|
|
12278
|
+
`task ${selected.id} is ${selected.column === "doing" ? "already claimed" : `in ${selected.column}`}, not Ready. Only Ready work can be started; run "odla-ai pm task ready ${selected.id} --expected-revision ${selected.revision}" once its goal, description, and acceptance criteria are complete.`
|
|
12279
|
+
);
|
|
12280
|
+
}
|
|
12281
|
+
let claimed;
|
|
12282
|
+
try {
|
|
12283
|
+
claimed = await claimAt(ctx, selected, parsed);
|
|
12284
|
+
} catch (error) {
|
|
12285
|
+
if (!isRevisionConflict(error)) throw error;
|
|
12286
|
+
const reloaded = await getTask(ctx, selected.id);
|
|
12287
|
+
if (reloaded.column !== READY_COLUMN) {
|
|
12288
|
+
throw new Error(
|
|
12289
|
+
`task ${selected.id} was taken while starting it (now ${reloaded.column}). Run "odla-ai pm start --app ${appId}" again to take the next Ready task.`
|
|
12290
|
+
);
|
|
12291
|
+
}
|
|
12292
|
+
claimed = await claimAt(ctx, reloaded, parsed);
|
|
12293
|
+
}
|
|
12294
|
+
const task = claimed ?? await getTask(ctx, selected.id);
|
|
12295
|
+
const goalId = typeof task.goalId === "string" ? task.goalId : void 0;
|
|
12296
|
+
const goal = goalId ? await pmRequest(ctx, "GET", `/goal/${encodeURIComponent(goalId)}`).then((res) => res.record).catch(() => null) : null;
|
|
12297
|
+
const result = {
|
|
12298
|
+
claimed: verbose ? task : leanRecord("task", task),
|
|
12299
|
+
goal: goal ? verbose ? goal : leanRecord("goal", goal) : null,
|
|
12300
|
+
next: 'work the acceptance criteria, then "odla-ai pm task done <id>" (or "odla-ai pm task release <id> --expected-revision <n>" to hand it back)'
|
|
12301
|
+
};
|
|
12302
|
+
emit2(ctx, result, () => {
|
|
12303
|
+
ctx.out.log(`claimed: ${studioRecordLink(ctx, "task", task)}`);
|
|
12304
|
+
if (goal) ctx.out.log(`goal: ${studioRecordLink(ctx, "goal", goal)}`);
|
|
12305
|
+
if (typeof task.acceptanceCriteria === "string") ctx.out.log(`acceptance: ${task.acceptanceCriteria}`);
|
|
12306
|
+
ctx.out.log(`next: ${result.next}`);
|
|
12307
|
+
});
|
|
12068
12308
|
}
|
|
12069
12309
|
|
|
12070
12310
|
// src/pm-links.ts
|
|
@@ -12538,9 +12778,13 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
12538
12778
|
throw new Error(`unknown pm project action "${action3}". Try list|add|use.`);
|
|
12539
12779
|
}
|
|
12540
12780
|
if (word === "next") {
|
|
12541
|
-
assertArgs(parsed, [...COMMON_OPTIONS, "app", "project"], 2);
|
|
12781
|
+
assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "verbose"], 2);
|
|
12542
12782
|
return pmNext(await buildContext2(parsed, deps), parsed);
|
|
12543
12783
|
}
|
|
12784
|
+
if (word === "start") {
|
|
12785
|
+
assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "goal", "verbose", "mutation-id"], 3);
|
|
12786
|
+
return pmStart(await buildContext2(parsed, deps), parsed);
|
|
12787
|
+
}
|
|
12544
12788
|
if (word === "watch") {
|
|
12545
12789
|
assertArgs(parsed, [
|
|
12546
12790
|
...COMMON_OPTIONS,
|
|
@@ -12558,11 +12802,11 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
12558
12802
|
return pmWatch(await buildContext2(parsed, deps), parsed).then(() => void 0);
|
|
12559
12803
|
}
|
|
12560
12804
|
if (word === "handoff") {
|
|
12561
|
-
assertArgs(parsed, [...COMMON_OPTIONS, "app", "project"], 2);
|
|
12805
|
+
assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "verbose"], 2);
|
|
12562
12806
|
return pmHandoff(await buildContext2(parsed, deps), parsed);
|
|
12563
12807
|
}
|
|
12564
12808
|
const entity = ALIASES[word];
|
|
12565
|
-
if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
|
|
12809
|
+
if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm start" to claim Ready work, or "odla-ai pm bug list" (goal|task|decision|bug).`);
|
|
12566
12810
|
const requestedAction = parsed.positionals[2] ?? "list";
|
|
12567
12811
|
const action2 = canonicalAction(requestedAction);
|
|
12568
12812
|
if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|link|ref|comment|comments|history|rm.`);
|
|
@@ -13110,14 +13354,14 @@ function statusMinutes(value2) {
|
|
|
13110
13354
|
}
|
|
13111
13355
|
async function read2(url, headers, doFetch) {
|
|
13112
13356
|
const response2 = await doFetch(url, { headers });
|
|
13113
|
-
const
|
|
13357
|
+
const text4 = await response2.text();
|
|
13114
13358
|
let body = {};
|
|
13115
|
-
if (
|
|
13359
|
+
if (text4) {
|
|
13116
13360
|
try {
|
|
13117
|
-
const value2 = JSON.parse(
|
|
13361
|
+
const value2 = JSON.parse(text4);
|
|
13118
13362
|
body = value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : { value: value2 };
|
|
13119
13363
|
} catch {
|
|
13120
|
-
body = { message:
|
|
13364
|
+
body = { message: text4.slice(0, 300) };
|
|
13121
13365
|
}
|
|
13122
13366
|
}
|
|
13123
13367
|
return { httpStatus: response2.status, body };
|
|
@@ -13347,13 +13591,13 @@ async function monitorCommand(parsed, deps = {}) {
|
|
|
13347
13591
|
}
|
|
13348
13592
|
async function request2(url, init, doFetch) {
|
|
13349
13593
|
const response2 = await doFetch(url, init);
|
|
13350
|
-
const
|
|
13594
|
+
const text4 = await response2.text();
|
|
13351
13595
|
let body = {};
|
|
13352
13596
|
try {
|
|
13353
|
-
const parsed =
|
|
13597
|
+
const parsed = text4 ? JSON.parse(text4) : {};
|
|
13354
13598
|
body = record10(parsed) ? parsed : { value: parsed };
|
|
13355
13599
|
} catch {
|
|
13356
|
-
body = { message:
|
|
13600
|
+
body = { message: text4.slice(0, 500) };
|
|
13357
13601
|
}
|
|
13358
13602
|
if (!response2.ok) {
|
|
13359
13603
|
const error = record10(body.error) ? body.error : body;
|
|
@@ -13556,8 +13800,8 @@ function runtimeUrl(cfg, suffix = "") {
|
|
|
13556
13800
|
return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials${suffix}`;
|
|
13557
13801
|
}
|
|
13558
13802
|
async function safeError(response2) {
|
|
13559
|
-
const
|
|
13560
|
-
return redactSecrets(
|
|
13803
|
+
const text4 = await response2.text();
|
|
13804
|
+
return redactSecrets(text4.slice(0, 1e3));
|
|
13561
13805
|
}
|
|
13562
13806
|
async function finish(doFetch, cfg, token, sessionId, method) {
|
|
13563
13807
|
return doFetch(runtimeUrl(cfg, `/${encodeURIComponent(sessionId)}`), {
|
|
@@ -13976,6 +14220,7 @@ var COMMAND_SURFACE = {
|
|
|
13976
14220
|
project: { list: {}, add: {}, create: {}, use: {} },
|
|
13977
14221
|
handoff: {},
|
|
13978
14222
|
next: {},
|
|
14223
|
+
start: {},
|
|
13979
14224
|
watch: {}
|
|
13980
14225
|
},
|
|
13981
14226
|
provision: {},
|
|
@@ -14050,13 +14295,13 @@ function invocationPath(words2) {
|
|
|
14050
14295
|
return path;
|
|
14051
14296
|
}
|
|
14052
14297
|
function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
|
|
14053
|
-
const
|
|
14298
|
+
const paths2 = [];
|
|
14054
14299
|
for (const [word, child] of Object.entries(node)) {
|
|
14055
14300
|
const path = [...prefix, word];
|
|
14056
|
-
|
|
14057
|
-
|
|
14301
|
+
paths2.push(path);
|
|
14302
|
+
paths2.push(...surfacePaths(child, path));
|
|
14058
14303
|
}
|
|
14059
|
-
return
|
|
14304
|
+
return paths2;
|
|
14060
14305
|
}
|
|
14061
14306
|
|
|
14062
14307
|
// src/record.ts
|
|
@@ -14114,10 +14359,10 @@ var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
|
14114
14359
|
var UNITS = { d: DAY_MS, w: 7 * DAY_MS, y: 365 * DAY_MS };
|
|
14115
14360
|
function parseDeviceTtl(raw) {
|
|
14116
14361
|
if (raw === void 0 || raw === true) return void 0;
|
|
14117
|
-
const
|
|
14118
|
-
if (!
|
|
14119
|
-
if (
|
|
14120
|
-
const match = /^(\d+)\s*([dwy])$/.exec(
|
|
14362
|
+
const text4 = String(raw).trim().toLowerCase();
|
|
14363
|
+
if (!text4) return void 0;
|
|
14364
|
+
if (text4 === "forever" || text4 === "never") return 100 * 365 * DAY_MS;
|
|
14365
|
+
const match = /^(\d+)\s*([dwy])$/.exec(text4);
|
|
14121
14366
|
if (!match) {
|
|
14122
14367
|
throw new Error(
|
|
14123
14368
|
`--device-ttl expects a duration like 30d, 6w, 2y, or "forever" (got "${raw}")`
|
|
@@ -14499,12 +14744,12 @@ async function runbookRemove(ctx, slug) {
|
|
|
14499
14744
|
// src/runbook-import.ts
|
|
14500
14745
|
var import_node_fs21 = require("fs");
|
|
14501
14746
|
var import_node_path19 = require("path");
|
|
14502
|
-
function parseRunbook(
|
|
14503
|
-
let rest =
|
|
14747
|
+
function parseRunbook(text4, slug) {
|
|
14748
|
+
let rest = text4;
|
|
14504
14749
|
const meta = {};
|
|
14505
|
-
const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(
|
|
14750
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text4);
|
|
14506
14751
|
if (fm) {
|
|
14507
|
-
rest =
|
|
14752
|
+
rest = text4.slice(fm[0].length);
|
|
14508
14753
|
for (const line2 of fm[1].split(/\r?\n/)) {
|
|
14509
14754
|
const pair = /^(\w+)\s*:\s*(.+)$/.exec(line2.trim());
|
|
14510
14755
|
if (!pair) continue;
|