@odla-ai/cli 0.42.1 → 0.43.0
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/dist/bin.cjs +395 -98
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-FWPQSTNF.js → chunk-WDWJ7HC7.js} +362 -95
- package/dist/chunk-WDWJ7HC7.js.map +1 -0
- package/dist/{cli-K5SBFFAY.js → cli-OITEPXW4.js} +2 -2
- package/dist/index.cjs +361 -94
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-FWPQSTNF.js.map +0 -1
- /package/dist/{cli-K5SBFFAY.js.map → cli-OITEPXW4.js.map} +0 -0
|
@@ -6145,7 +6145,7 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
|
|
|
6145
6145
|
}
|
|
6146
6146
|
}
|
|
6147
6147
|
|
|
6148
|
-
// ../harness/dist/chunk-
|
|
6148
|
+
// ../harness/dist/chunk-GYWQM76X.js
|
|
6149
6149
|
import { createHash as createHash3 } from "crypto";
|
|
6150
6150
|
import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
|
|
6151
6151
|
import { relative as relative4, resolve as resolve10 } from "path";
|
|
@@ -6482,7 +6482,7 @@ function validateSnapshot(snapshot, limits) {
|
|
|
6482
6482
|
}
|
|
6483
6483
|
}
|
|
6484
6484
|
|
|
6485
|
-
// ../harness/dist/chunk-
|
|
6485
|
+
// ../harness/dist/chunk-GYWQM76X.js
|
|
6486
6486
|
import { spawn as spawn4 } from "child_process";
|
|
6487
6487
|
import { lstat as lstat2 } from "fs/promises";
|
|
6488
6488
|
import { resolve as resolve23, sep as sep3 } from "path";
|
|
@@ -6501,6 +6501,7 @@ import {
|
|
|
6501
6501
|
runAgent
|
|
6502
6502
|
} from "@odla-ai/ai";
|
|
6503
6503
|
import { extractText } from "@odla-ai/ai";
|
|
6504
|
+
import { spawn as spawn33 } from "child_process";
|
|
6504
6505
|
import { readFile as readFile22, readdir as readdir22 } from "fs/promises";
|
|
6505
6506
|
import { relative as relative22, resolve as resolve42 } from "path";
|
|
6506
6507
|
|
|
@@ -6788,7 +6789,7 @@ function looksLikeDestination(value2) {
|
|
|
6788
6789
|
return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text3) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text3);
|
|
6789
6790
|
}
|
|
6790
6791
|
|
|
6791
|
-
// ../harness/dist/chunk-
|
|
6792
|
+
// ../harness/dist/chunk-GYWQM76X.js
|
|
6792
6793
|
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
6793
6794
|
import { readFile as readFile3 } from "fs/promises";
|
|
6794
6795
|
import { join as join33 } from "path";
|
|
@@ -7056,7 +7057,7 @@ async function buildCodeGraph(input) {
|
|
|
7056
7057
|
return builder.build();
|
|
7057
7058
|
}
|
|
7058
7059
|
|
|
7059
|
-
// ../harness/dist/chunk-
|
|
7060
|
+
// ../harness/dist/chunk-GYWQM76X.js
|
|
7060
7061
|
import { createHash as createHash32 } from "crypto";
|
|
7061
7062
|
async function digestStagedWorkspace(root, limits) {
|
|
7062
7063
|
const files = [];
|
|
@@ -8120,7 +8121,10 @@ Never claim a build or test passed unless odla_run_recipe returned that result.`
|
|
|
8120
8121
|
var V2_SYSTEM_PROMPT = `You are the coding agent inside an odla Code harness.
|
|
8121
8122
|
Start by orienting: odla_list shows the files in the workspace and odla_search
|
|
8122
8123
|
finds a literal string across them. Prefer those over guessing a path.
|
|
8123
|
-
Then odla_read a bounded range, and odla_apply_git_diff to mutate.
|
|
8124
|
+
Then odla_read a bounded range, and odla_apply_git_diff to mutate. When you
|
|
8125
|
+
need several independent searches or file ranges, issue those read-only calls
|
|
8126
|
+
together in one turn; their results stay ordered and the harness overlaps them.
|
|
8127
|
+
Never issue odla_apply_git_diff or odla_run_recipe alongside another tool call.
|
|
8124
8128
|
For mutations, call odla_apply_git_diff with raw git diff text. It must start
|
|
8125
8129
|
with "diff --git a/<path> b/<path>", include matching "---" and "+++" file
|
|
8126
8130
|
headers and numbered "@@" hunks, and never use "*** Begin Patch" wrappers.
|
|
@@ -8151,18 +8155,26 @@ var SYSTEM_PROMPT_FOR = {
|
|
|
8151
8155
|
};
|
|
8152
8156
|
function codeSkill(opts) {
|
|
8153
8157
|
let seq = 0;
|
|
8158
|
+
let nextCompletion = 1;
|
|
8159
|
+
const completed = /* @__PURE__ */ new Map();
|
|
8154
8160
|
const call4 = async (tool, input, signal) => {
|
|
8161
|
+
const sequence = ++seq;
|
|
8155
8162
|
const startedAt = Date.now();
|
|
8156
8163
|
const response2 = await opts.broker.execute(
|
|
8157
8164
|
{ lease: opts.lease, workspaceDir: opts.workspaceDir, signal },
|
|
8158
|
-
{ requestId: `bench-${tool}-${
|
|
8165
|
+
{ requestId: `bench-${tool}-${sequence}`, tool, input }
|
|
8159
8166
|
);
|
|
8160
|
-
|
|
8167
|
+
completed.set(sequence, {
|
|
8161
8168
|
tool,
|
|
8162
8169
|
ok: response2.ok,
|
|
8163
8170
|
durationMs: Date.now() - startedAt,
|
|
8164
8171
|
...response2.ok ? {} : { error: String(response2.content).slice(0, 300) }
|
|
8165
8172
|
});
|
|
8173
|
+
while (completed.has(nextCompletion)) {
|
|
8174
|
+
const completion = completed.get(nextCompletion);
|
|
8175
|
+
completed.delete(nextCompletion++);
|
|
8176
|
+
opts.onToolCall?.(completion);
|
|
8177
|
+
}
|
|
8166
8178
|
return { content: response2.content, isError: !response2.ok };
|
|
8167
8179
|
};
|
|
8168
8180
|
const read22 = {
|
|
@@ -8178,6 +8190,7 @@ function codeSkill(opts) {
|
|
|
8178
8190
|
},
|
|
8179
8191
|
additionalProperties: false
|
|
8180
8192
|
},
|
|
8193
|
+
concurrency: "parallel",
|
|
8181
8194
|
handler: (input, ctx) => call4("sandbox.read", input, ctx.signal)
|
|
8182
8195
|
};
|
|
8183
8196
|
const applyPatch = {
|
|
@@ -8219,6 +8232,7 @@ function codeSkill(opts) {
|
|
|
8219
8232
|
},
|
|
8220
8233
|
additionalProperties: false
|
|
8221
8234
|
},
|
|
8235
|
+
concurrency: "parallel",
|
|
8222
8236
|
handler: (input, ctx) => call4("sandbox.list", input, ctx.signal)
|
|
8223
8237
|
};
|
|
8224
8238
|
const searchFiles = {
|
|
@@ -8235,11 +8249,13 @@ function codeSkill(opts) {
|
|
|
8235
8249
|
},
|
|
8236
8250
|
additionalProperties: false
|
|
8237
8251
|
},
|
|
8252
|
+
concurrency: "parallel",
|
|
8238
8253
|
handler: (input, ctx) => call4("sandbox.search", input, ctx.signal)
|
|
8239
8254
|
};
|
|
8240
8255
|
const graphTool = (name, tool, description, required) => ({
|
|
8241
8256
|
name,
|
|
8242
8257
|
description,
|
|
8258
|
+
concurrency: "parallel",
|
|
8243
8259
|
inputSchema: {
|
|
8244
8260
|
type: "object",
|
|
8245
8261
|
...required ? { required: ["query"] } : {},
|
|
@@ -8417,6 +8433,24 @@ function createCodeRuntimeInference(options) {
|
|
|
8417
8433
|
var DEFAULT_MAX_FILES = 2e4;
|
|
8418
8434
|
var DEFAULT_MAX_RESULTS = 100;
|
|
8419
8435
|
var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
|
|
8436
|
+
function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = registeredFiles) {
|
|
8437
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
8438
|
+
return {
|
|
8439
|
+
files(root) {
|
|
8440
|
+
const existing = cache2.get(root);
|
|
8441
|
+
if (existing) return existing;
|
|
8442
|
+
const pending = enumerate(root, limit).then((paths) => Object.freeze(paths));
|
|
8443
|
+
cache2.set(root, pending);
|
|
8444
|
+
void pending.catch(() => {
|
|
8445
|
+
if (cache2.get(root) === pending) cache2.delete(root);
|
|
8446
|
+
});
|
|
8447
|
+
return pending;
|
|
8448
|
+
},
|
|
8449
|
+
invalidate(root) {
|
|
8450
|
+
cache2.delete(root);
|
|
8451
|
+
}
|
|
8452
|
+
};
|
|
8453
|
+
}
|
|
8420
8454
|
async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
|
|
8421
8455
|
const paths = [];
|
|
8422
8456
|
const walk = async (directory) => {
|
|
@@ -8447,28 +8481,122 @@ function listWorkspace(paths, options = {}) {
|
|
|
8447
8481
|
return scoped.slice(0, max);
|
|
8448
8482
|
}
|
|
8449
8483
|
async function searchWorkspace(root, paths, options) {
|
|
8450
|
-
|
|
8451
|
-
if (!query) throw new TypeError("search query must be a non-empty string");
|
|
8484
|
+
options.signal?.throwIfAborted();
|
|
8485
|
+
if (!options.query) throw new TypeError("search query must be a non-empty string");
|
|
8452
8486
|
const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
|
|
8453
8487
|
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
8454
8488
|
const scoped = listWorkspace(paths, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths.length });
|
|
8489
|
+
if (scoped.length === 0) return [];
|
|
8490
|
+
try {
|
|
8491
|
+
return await nativeSearch(root, scoped, { ...options, maxResults, maxFileBytes });
|
|
8492
|
+
} catch (error) {
|
|
8493
|
+
options.signal?.throwIfAborted();
|
|
8494
|
+
return fallbackSearch(root, scoped, { ...options, maxResults, maxFileBytes });
|
|
8495
|
+
}
|
|
8496
|
+
}
|
|
8497
|
+
var MAX_NATIVE_ARG_BYTES = 96 * 1024;
|
|
8498
|
+
async function nativeSearch(root, paths, options) {
|
|
8499
|
+
const batches = [];
|
|
8500
|
+
let batch = [];
|
|
8501
|
+
let bytes = 0;
|
|
8502
|
+
for (const path of paths) {
|
|
8503
|
+
const size = Buffer.byteLength(path) + 1;
|
|
8504
|
+
if (batch.length > 0 && bytes + size > MAX_NATIVE_ARG_BYTES) {
|
|
8505
|
+
batches.push(batch);
|
|
8506
|
+
batch = [];
|
|
8507
|
+
bytes = 0;
|
|
8508
|
+
}
|
|
8509
|
+
batch.push(path);
|
|
8510
|
+
bytes += size;
|
|
8511
|
+
}
|
|
8512
|
+
if (batch.length > 0) batches.push(batch);
|
|
8513
|
+
const matches = [];
|
|
8514
|
+
for (const files of batches) {
|
|
8515
|
+
const remaining = options.maxResults - matches.length;
|
|
8516
|
+
if (remaining <= 0) break;
|
|
8517
|
+
matches.push(...await nativeSearchBatch(root, files, options, remaining));
|
|
8518
|
+
}
|
|
8519
|
+
return matches;
|
|
8520
|
+
}
|
|
8521
|
+
function nativeSearchBatch(root, paths, options, remaining) {
|
|
8522
|
+
return new Promise((resolveMatches, reject) => {
|
|
8523
|
+
const args = [
|
|
8524
|
+
"--fixed-strings",
|
|
8525
|
+
"--json",
|
|
8526
|
+
"--no-messages",
|
|
8527
|
+
"--sort=path",
|
|
8528
|
+
`--max-filesize=${options.maxFileBytes}`,
|
|
8529
|
+
"--max-columns=4096",
|
|
8530
|
+
"--max-columns-preview",
|
|
8531
|
+
options.caseSensitive === false ? "--ignore-case" : "--case-sensitive",
|
|
8532
|
+
"--",
|
|
8533
|
+
options.query,
|
|
8534
|
+
...paths
|
|
8535
|
+
];
|
|
8536
|
+
const child = spawn33("rg", args, {
|
|
8537
|
+
cwd: root,
|
|
8538
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
8539
|
+
...options.signal ? { signal: options.signal } : {}
|
|
8540
|
+
});
|
|
8541
|
+
const matches = [];
|
|
8542
|
+
let carry = "";
|
|
8543
|
+
let stopped = false;
|
|
8544
|
+
const consume = (line2) => {
|
|
8545
|
+
if (matches.length >= remaining) return;
|
|
8546
|
+
let event;
|
|
8547
|
+
try {
|
|
8548
|
+
event = JSON.parse(line2);
|
|
8549
|
+
} catch {
|
|
8550
|
+
return;
|
|
8551
|
+
}
|
|
8552
|
+
const path = event.data?.path?.text;
|
|
8553
|
+
const lineNumber = event.data?.line_number;
|
|
8554
|
+
const source = event.data?.lines?.text;
|
|
8555
|
+
if (event.type !== "match" || path === void 0 || lineNumber === void 0 || source === void 0) return;
|
|
8556
|
+
matches.push({ path, line: lineNumber, text: source.trim().slice(0, 240) });
|
|
8557
|
+
if (matches.length >= remaining) {
|
|
8558
|
+
stopped = true;
|
|
8559
|
+
child.kill();
|
|
8560
|
+
}
|
|
8561
|
+
};
|
|
8562
|
+
child.stdout.setEncoding("utf8");
|
|
8563
|
+
child.stdout.on("data", (chunk) => {
|
|
8564
|
+
carry += chunk;
|
|
8565
|
+
let newline = carry.indexOf("\n");
|
|
8566
|
+
while (newline >= 0) {
|
|
8567
|
+
consume(carry.slice(0, newline));
|
|
8568
|
+
carry = carry.slice(newline + 1);
|
|
8569
|
+
newline = carry.indexOf("\n");
|
|
8570
|
+
}
|
|
8571
|
+
});
|
|
8572
|
+
child.once("error", reject);
|
|
8573
|
+
child.once("close", (code) => {
|
|
8574
|
+
if (carry) consume(carry);
|
|
8575
|
+
if (stopped || code === 0 || code === 1) resolveMatches(matches);
|
|
8576
|
+
else reject(new Error(`native search exited with status ${code ?? "unknown"}`));
|
|
8577
|
+
});
|
|
8578
|
+
});
|
|
8579
|
+
}
|
|
8580
|
+
async function fallbackSearch(root, scoped, options) {
|
|
8581
|
+
const query = options.caseSensitive === false ? options.query.toLowerCase() : options.query;
|
|
8455
8582
|
const matches = [];
|
|
8456
8583
|
for (const path of scoped) {
|
|
8457
|
-
|
|
8584
|
+
options.signal?.throwIfAborted();
|
|
8585
|
+
if (matches.length >= options.maxResults) break;
|
|
8458
8586
|
let source;
|
|
8459
8587
|
try {
|
|
8460
8588
|
source = await readFile22(resolve42(root, path));
|
|
8461
8589
|
} catch {
|
|
8462
8590
|
continue;
|
|
8463
8591
|
}
|
|
8464
|
-
if (source.byteLength > maxFileBytes || source.includes(0)) continue;
|
|
8592
|
+
if (source.byteLength > options.maxFileBytes || source.includes(0)) continue;
|
|
8465
8593
|
const lines = source.toString("utf8").split("\n");
|
|
8466
8594
|
for (let index = 0; index < lines.length; index += 1) {
|
|
8467
8595
|
const raw = lines[index];
|
|
8468
8596
|
const haystack = options.caseSensitive === false ? raw.toLowerCase() : raw;
|
|
8469
8597
|
if (!haystack.includes(query)) continue;
|
|
8470
8598
|
matches.push({ path, line: index + 1, text: raw.trim().slice(0, 240) });
|
|
8471
|
-
if (matches.length >= maxResults) break;
|
|
8599
|
+
if (matches.length >= options.maxResults) break;
|
|
8472
8600
|
}
|
|
8473
8601
|
}
|
|
8474
8602
|
return matches;
|
|
@@ -8723,6 +8851,9 @@ function workspaceGraphs(workspaceDir, paths) {
|
|
|
8723
8851
|
cache.set(workspaceDir, built);
|
|
8724
8852
|
return built;
|
|
8725
8853
|
}
|
|
8854
|
+
function forgetWorkspaceGraphs(workspaceDir) {
|
|
8855
|
+
cache.delete(workspaceDir);
|
|
8856
|
+
}
|
|
8726
8857
|
var shortId = (id2) => id2.slice(id2.indexOf(":") + 1);
|
|
8727
8858
|
function renderOverview(graphs, prefix) {
|
|
8728
8859
|
const rows = rollup(graphs.graph, FILE, prefix === void 0 ? {} : { prefix });
|
|
@@ -8767,7 +8898,7 @@ var GRAPH_TOOLS = /* @__PURE__ */ new Set([
|
|
|
8767
8898
|
"sandbox.who_imports",
|
|
8768
8899
|
"sandbox.who_touches"
|
|
8769
8900
|
]);
|
|
8770
|
-
async function read(context, request3, options, policy) {
|
|
8901
|
+
async function read(context, request3, options, policy, registry) {
|
|
8771
8902
|
exactKeys(request3.input, ["path", "startLine", "endLine"]);
|
|
8772
8903
|
const path = stringField(request3.input, "path");
|
|
8773
8904
|
const startLine = optionalInteger(request3.input.startLine) ?? 1;
|
|
@@ -8775,7 +8906,7 @@ async function read(context, request3, options, policy) {
|
|
|
8775
8906
|
if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
|
|
8776
8907
|
throw new TypeError("requested line range exceeds its bound");
|
|
8777
8908
|
}
|
|
8778
|
-
const paths = await
|
|
8909
|
+
const paths = await registry.files(context.workspaceDir);
|
|
8779
8910
|
if (!paths.includes(path)) {
|
|
8780
8911
|
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.`);
|
|
8781
8912
|
}
|
|
@@ -8795,13 +8926,13 @@ async function read(context, request3, options, policy) {
|
|
|
8795
8926
|
}
|
|
8796
8927
|
return response(request3, true, content2, { path, startLine, endLine: Math.min(endLine, lines.length) });
|
|
8797
8928
|
}
|
|
8798
|
-
async function list(context, request3, options, policy) {
|
|
8929
|
+
async function list(context, request3, options, policy, registry) {
|
|
8799
8930
|
exactKeys(request3.input, ["prefix", "maxEntries"]);
|
|
8800
8931
|
const raw = request3.input.prefix;
|
|
8801
8932
|
const prefix = typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
8802
8933
|
const maxEntries = optionalInteger(request3.input.maxEntries) ?? 1e3;
|
|
8803
8934
|
if (maxEntries > 5e3) throw new TypeError("maxEntries exceeds its bound");
|
|
8804
|
-
const paths = await
|
|
8935
|
+
const paths = await registry.files(context.workspaceDir);
|
|
8805
8936
|
const allowed = await policy.list(policyContext(context, request3, options, { paths, ...prefix ? { prefix } : {} }));
|
|
8806
8937
|
if (!allowed) return response(request3, false, "tool denied by CaMeL policy");
|
|
8807
8938
|
const entries = listWorkspace(paths, { ...prefix ? { prefix } : {}, maxEntries });
|
|
@@ -8819,7 +8950,7 @@ async function list(context, request3, options, policy) {
|
|
|
8819
8950
|
{ count: entries.length, truncated }
|
|
8820
8951
|
);
|
|
8821
8952
|
}
|
|
8822
|
-
async function search(context, request3, options, policy) {
|
|
8953
|
+
async function search(context, request3, options, policy, registry) {
|
|
8823
8954
|
exactKeys(request3.input, ["query", "prefix", "maxResults", "caseSensitive"]);
|
|
8824
8955
|
const query = stringField(request3.input, "query");
|
|
8825
8956
|
if (query.length > 512) throw new TypeError("search query exceeds its bound");
|
|
@@ -8828,21 +8959,22 @@ async function search(context, request3, options, policy) {
|
|
|
8828
8959
|
const maxResults = optionalInteger(request3.input.maxResults) ?? 100;
|
|
8829
8960
|
if (maxResults > 500) throw new TypeError("maxResults exceeds its bound");
|
|
8830
8961
|
const caseSensitive = request3.input.caseSensitive === void 0 ? true : request3.input.caseSensitive === true;
|
|
8831
|
-
const paths = await
|
|
8962
|
+
const paths = await registry.files(context.workspaceDir);
|
|
8832
8963
|
const allowed = await policy.search(policyContext(context, request3, options, { paths, query, ...prefix ? { prefix } : {} }));
|
|
8833
8964
|
if (!allowed) return response(request3, false, "tool denied by CaMeL policy");
|
|
8834
8965
|
const matches = await searchWorkspace(context.workspaceDir, paths, {
|
|
8835
8966
|
query,
|
|
8836
8967
|
maxResults,
|
|
8837
8968
|
caseSensitive,
|
|
8838
|
-
...prefix ? { prefix } : {}
|
|
8969
|
+
...prefix ? { prefix } : {},
|
|
8970
|
+
...context.signal ? { signal: context.signal } : {}
|
|
8839
8971
|
});
|
|
8840
8972
|
if (!matches.length) return response(request3, true, `No match for "${query}".`, { count: 0 });
|
|
8841
8973
|
return response(request3, true, matches.map((match) => `${match.path}:${match.line}: ${match.text}`).join("\n"), {
|
|
8842
8974
|
count: matches.length
|
|
8843
8975
|
});
|
|
8844
8976
|
}
|
|
8845
|
-
async function graphQuery(context, request3, options, policy) {
|
|
8977
|
+
async function graphQuery(context, request3, options, policy, registry) {
|
|
8846
8978
|
exactKeys(request3.input, ["query"]);
|
|
8847
8979
|
const raw = request3.input.query;
|
|
8848
8980
|
const query = typeof raw === "string" ? raw : "";
|
|
@@ -8852,7 +8984,7 @@ async function graphQuery(context, request3, options, policy) {
|
|
|
8852
8984
|
selector: query
|
|
8853
8985
|
}));
|
|
8854
8986
|
if (!allowed) return response(request3, false, "tool denied by CaMeL policy");
|
|
8855
|
-
const paths = await
|
|
8987
|
+
const paths = await registry.files(context.workspaceDir);
|
|
8856
8988
|
const graphs = await workspaceGraphs(context.workspaceDir, paths);
|
|
8857
8989
|
if (request3.tool === "sandbox.overview") {
|
|
8858
8990
|
return response(request3, true, renderOverview(graphs, query || void 0));
|
|
@@ -8866,23 +8998,38 @@ function createCodeToolBroker(options) {
|
|
|
8866
8998
|
validateOptions(options);
|
|
8867
8999
|
const recipes = new Map(options.recipes.map((recipe2) => [recipe2.id, recipe2]));
|
|
8868
9000
|
const policy = createCodePolicyGate(options);
|
|
8869
|
-
|
|
9001
|
+
const registry = createWorkspaceFileRegistry();
|
|
9002
|
+
let barrier = Promise.resolve();
|
|
9003
|
+
const activeReads = /* @__PURE__ */ new Set();
|
|
8870
9004
|
return {
|
|
8871
9005
|
execute(context, request3) {
|
|
8872
|
-
|
|
8873
|
-
|
|
9006
|
+
if (isReadTool(request3.tool)) {
|
|
9007
|
+
const result2 = barrier.then(() => route2(context, request3, options, recipes, policy, registry));
|
|
9008
|
+
const settled = result2.then(() => void 0, () => void 0);
|
|
9009
|
+
activeReads.add(settled);
|
|
9010
|
+
void settled.then(() => {
|
|
9011
|
+
activeReads.delete(settled);
|
|
9012
|
+
});
|
|
9013
|
+
return result2;
|
|
9014
|
+
}
|
|
9015
|
+
const earlierReads = [...activeReads];
|
|
9016
|
+
const result = barrier.then(() => Promise.all(earlierReads)).then(() => route2(context, request3, options, recipes, policy, registry));
|
|
9017
|
+
barrier = result.then(() => void 0, () => void 0);
|
|
8874
9018
|
return result;
|
|
8875
9019
|
}
|
|
8876
9020
|
};
|
|
8877
9021
|
}
|
|
8878
|
-
|
|
9022
|
+
function isReadTool(tool) {
|
|
9023
|
+
return tool === "sandbox.read" || tool === "sandbox.list" || tool === "sandbox.search" || GRAPH_TOOLS.has(tool);
|
|
9024
|
+
}
|
|
9025
|
+
async function route2(context, request3, options, recipes, policy, registry) {
|
|
8879
9026
|
try {
|
|
8880
9027
|
if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
|
|
8881
|
-
if (request3.tool === "sandbox.read") return await read(context, request3, options, policy);
|
|
8882
|
-
if (request3.tool === "sandbox.list") return await list(context, request3, options, policy);
|
|
8883
|
-
if (request3.tool === "sandbox.search") return await search(context, request3, options, policy);
|
|
8884
|
-
if (GRAPH_TOOLS.has(request3.tool)) return await graphQuery(context, request3, options, policy);
|
|
8885
|
-
if (request3.tool === "sandbox.apply_patch") return await patch(context, request3, options, policy);
|
|
9028
|
+
if (request3.tool === "sandbox.read") return await read(context, request3, options, policy, registry);
|
|
9029
|
+
if (request3.tool === "sandbox.list") return await list(context, request3, options, policy, registry);
|
|
9030
|
+
if (request3.tool === "sandbox.search") return await search(context, request3, options, policy, registry);
|
|
9031
|
+
if (GRAPH_TOOLS.has(request3.tool)) return await graphQuery(context, request3, options, policy, registry);
|
|
9032
|
+
if (request3.tool === "sandbox.apply_patch") return await patch(context, request3, options, policy, registry);
|
|
8886
9033
|
return await recipe(context, request3, options, recipes, policy);
|
|
8887
9034
|
} catch (reason) {
|
|
8888
9035
|
return response(request3, false, toolFailureMessage(reason));
|
|
@@ -8897,7 +9044,7 @@ function toolFailureMessage(reason) {
|
|
|
8897
9044
|
if (code === "EACCES" || code === "EPERM") return "that path is not readable through this tool";
|
|
8898
9045
|
return "tool failed closed";
|
|
8899
9046
|
}
|
|
8900
|
-
async function patch(context, request3, options, policy) {
|
|
9047
|
+
async function patch(context, request3, options, policy, registry) {
|
|
8901
9048
|
exactKeys(request3.input, ["patch"]);
|
|
8902
9049
|
const value2 = stringField(request3.input, "patch");
|
|
8903
9050
|
const paths = validateCodePatch(value2, options.maxPatchBytes ?? 256 * 1024);
|
|
@@ -8907,6 +9054,8 @@ async function patch(context, request3, options, policy) {
|
|
|
8907
9054
|
const allowed = await policy.patch(policyContext(context, request3, options, { patch: value2 }));
|
|
8908
9055
|
if (!allowed) return response(request3, false, "tool denied by CaMeL policy");
|
|
8909
9056
|
await applyCodePatch(context.workspaceDir, value2, paths);
|
|
9057
|
+
registry.invalidate(context.workspaceDir);
|
|
9058
|
+
forgetWorkspaceGraphs(context.workspaceDir);
|
|
8910
9059
|
return response(request3, true, `Applied patch to ${paths.length} file(s).`, { paths });
|
|
8911
9060
|
}
|
|
8912
9061
|
async function recipe(context, request3, options, recipes, policy) {
|
|
@@ -10548,7 +10697,8 @@ Usage:
|
|
|
10548
10697
|
odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
10549
10698
|
odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
|
|
10550
10699
|
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]
|
|
10551
|
-
odla-ai pm next --app <id> [--project <id>] [--json]
|
|
10700
|
+
odla-ai pm next --app <id> [--project <id>] [--verbose] [--json]
|
|
10701
|
+
odla-ai pm start --app <id> [<task-id>] [--goal <id>] [--verbose] [--mutation-id <id>] [--json] [claims the top Ready task in one call]
|
|
10552
10702
|
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]
|
|
10553
10703
|
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]
|
|
10554
10704
|
odla-ai pm task claim <id> --expected-revision <n> [--mutation-id <id>] [--json]
|
|
@@ -10569,7 +10719,7 @@ Usage:
|
|
|
10569
10719
|
odla-ai pm <goal|task|decision|bug> comments <id> [--json]
|
|
10570
10720
|
odla-ai pm <goal|task|decision|bug> history <id> [--limit <n>] [--json]
|
|
10571
10721
|
odla-ai pm <goal|task|decision|bug> rm <id>
|
|
10572
|
-
odla-ai pm handoff --app <id> [--project <id>] [--json]
|
|
10722
|
+
odla-ai pm handoff --app <id> [--project <id>] [--verbose] [--json]
|
|
10573
10723
|
odla-ai discuss groups [--json]
|
|
10574
10724
|
odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--mentions] [--json]
|
|
10575
10725
|
odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
|
|
@@ -10734,6 +10884,16 @@ Commands:
|
|
|
10734
10884
|
execution contract and a reviewed revision. Any caller with PM
|
|
10735
10885
|
mutation access to that app/project may do it; claims coordinate
|
|
10736
10886
|
execution but never own or lock the PM record.
|
|
10887
|
+
To pick up work: "pm next" reads open goals, Ready candidates,
|
|
10888
|
+
and work in progress; "pm start" claims the top Ready task in
|
|
10889
|
+
one call, compare-and-swapped on the revision it just read, and
|
|
10890
|
+
returns the task, its goal, and the acceptance criteria to work
|
|
10891
|
+
against. Finish with "pm task done <id>", or hand it back with
|
|
10892
|
+
"pm task release <id> --expected-revision <n>". "pm start" never
|
|
10893
|
+
marks work Ready: that contract is reviewed separately so an
|
|
10894
|
+
agent cannot authorize its own work.
|
|
10895
|
+
Intake output is projected to the fields an executing caller
|
|
10896
|
+
acts on; --verbose returns whole records with their audit trail.
|
|
10737
10897
|
bug Intent-first alias for PM bugs. "bug report" writes to
|
|
10738
10898
|
odla PM; odla product defects do not belong in GitHub Issues.
|
|
10739
10899
|
discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
|
|
@@ -11484,6 +11644,15 @@ var FIELD_MAP = {
|
|
|
11484
11644
|
execution: { key: "executionMode" },
|
|
11485
11645
|
"expected-revision": { key: "expectedRevision", num: true }
|
|
11486
11646
|
};
|
|
11647
|
+
var PmRequestError = class extends Error {
|
|
11648
|
+
constructor(status, message2) {
|
|
11649
|
+
super(message2);
|
|
11650
|
+
this.status = status;
|
|
11651
|
+
this.name = "PmRequestError";
|
|
11652
|
+
}
|
|
11653
|
+
status;
|
|
11654
|
+
};
|
|
11655
|
+
var isRevisionConflict = (error) => error instanceof PmRequestError && error.status === 409;
|
|
11487
11656
|
async function pmRequest(ctx, method, path, body) {
|
|
11488
11657
|
const response2 = await ctx.doFetch(`${ctx.platformUrl}/registry/pm${path}`, {
|
|
11489
11658
|
method,
|
|
@@ -11503,7 +11672,8 @@ async function pmRequest(ctx, method, path, body) {
|
|
|
11503
11672
|
const message2 = error.message;
|
|
11504
11673
|
if (typeof message2 === "string" && message2.length > 0) detail = message2;
|
|
11505
11674
|
}
|
|
11506
|
-
throw new
|
|
11675
|
+
throw new PmRequestError(
|
|
11676
|
+
response2.status,
|
|
11507
11677
|
`pm ${method} ${path} failed: ${detail ?? `registry returned ${response2.status}`} (${response2.status})`
|
|
11508
11678
|
);
|
|
11509
11679
|
}
|
|
@@ -11692,103 +11862,196 @@ async function pmTaskLifecycle(ctx, id2, action2, parsed) {
|
|
|
11692
11862
|
ctx.out.log(`task: ${label} \u2192 ${state2}`);
|
|
11693
11863
|
});
|
|
11694
11864
|
}
|
|
11695
|
-
async function
|
|
11865
|
+
async function pmRemove(ctx, entity, id2) {
|
|
11866
|
+
await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id2)}`);
|
|
11867
|
+
ctx.out.log(`deleted ${entity} ${id2}`);
|
|
11868
|
+
}
|
|
11869
|
+
|
|
11870
|
+
// src/pm-lean.ts
|
|
11871
|
+
var COMMON = ["id", "appId", "projectId", "title", "revision"];
|
|
11872
|
+
var PER_ENTITY = {
|
|
11873
|
+
goal: ["status", "proof", "targetPct", "currentPct"],
|
|
11874
|
+
task: [
|
|
11875
|
+
"column",
|
|
11876
|
+
"goalId",
|
|
11877
|
+
"alignmentDecisionId",
|
|
11878
|
+
"description",
|
|
11879
|
+
"acceptanceCriteria",
|
|
11880
|
+
"executionMode",
|
|
11881
|
+
"assigneeId",
|
|
11882
|
+
"claimedByPrincipalId",
|
|
11883
|
+
"dueAt"
|
|
11884
|
+
],
|
|
11885
|
+
decision: ["status", "body", "supersedesId"],
|
|
11886
|
+
bug: ["status", "severity", "description", "goalId", "assigneeId", "decisionId"]
|
|
11887
|
+
};
|
|
11888
|
+
function leanRecord(entity, record11) {
|
|
11889
|
+
const out = {};
|
|
11890
|
+
for (const key of [...COMMON, ...PER_ENTITY[entity]]) {
|
|
11891
|
+
const value2 = record11[key];
|
|
11892
|
+
if (value2 !== void 0 && value2 !== null) out[key] = value2;
|
|
11893
|
+
}
|
|
11894
|
+
return out;
|
|
11895
|
+
}
|
|
11896
|
+
function leanRecords(entity, records, verbose) {
|
|
11897
|
+
return verbose ? records : records.map((record11) => leanRecord(entity, record11));
|
|
11898
|
+
}
|
|
11899
|
+
|
|
11900
|
+
// src/pm-intake.ts
|
|
11901
|
+
var READY_COLUMN = "todo";
|
|
11902
|
+
var UNMET_GOAL_STATUS = "open";
|
|
11903
|
+
var ACTIVE_TASK_COLUMNS = "backlog,todo,doing,review";
|
|
11904
|
+
var OPEN_BUG_STATUSES = "open,triaged";
|
|
11905
|
+
var wantsVerbose = (parsed) => parsed.options.verbose === true;
|
|
11906
|
+
async function listFiltered(ctx, entity, appId, projectId, filters) {
|
|
11696
11907
|
const records = [];
|
|
11697
11908
|
for (; ; ) {
|
|
11698
|
-
const q = new URLSearchParams({
|
|
11699
|
-
app: appId,
|
|
11700
|
-
limit: "100",
|
|
11701
|
-
offset: String(records.length)
|
|
11702
|
-
});
|
|
11909
|
+
const q = new URLSearchParams({ app: appId, limit: "100", offset: String(records.length) });
|
|
11703
11910
|
if (projectId) q.set("project", projectId);
|
|
11911
|
+
for (const [key, value2] of Object.entries(filters)) q.set(key, value2);
|
|
11704
11912
|
const page2 = await pmRequest(ctx, "GET", `/${entity}?${q}`);
|
|
11705
11913
|
records.push(...page2.records);
|
|
11706
11914
|
if (records.length >= page2.total || page2.records.length === 0) return records;
|
|
11707
11915
|
}
|
|
11708
11916
|
}
|
|
11709
|
-
|
|
11917
|
+
function requireApp(ctx, parsed, command) {
|
|
11710
11918
|
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
11711
|
-
|
|
11712
|
-
|
|
11919
|
+
if (!appId) throw new Error(`pm ${command} needs --app <appId>`);
|
|
11920
|
+
return { appId, projectId: stringOpt(parsed.options.project) ?? ctx.projectId };
|
|
11921
|
+
}
|
|
11922
|
+
async function pmNext(ctx, parsed) {
|
|
11923
|
+
const { appId, projectId } = requireApp(ctx, parsed, "next");
|
|
11924
|
+
const verbose = wantsVerbose(parsed);
|
|
11713
11925
|
const [goals, tasks] = await Promise.all([
|
|
11714
|
-
|
|
11715
|
-
|
|
11926
|
+
listFiltered(ctx, "goal", appId, projectId, { status: UNMET_GOAL_STATUS }),
|
|
11927
|
+
// One request for both live columns; the split below is over open work only.
|
|
11928
|
+
listFiltered(ctx, "task", appId, projectId, { column: `${READY_COLUMN},doing` })
|
|
11716
11929
|
]);
|
|
11930
|
+
const ready = tasks.filter((record11) => record11.column === READY_COLUMN);
|
|
11931
|
+
const doing = tasks.filter((record11) => record11.column === "doing");
|
|
11932
|
+
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}`;
|
|
11717
11933
|
const result = {
|
|
11718
11934
|
appId,
|
|
11719
11935
|
projectId,
|
|
11720
|
-
openGoals:
|
|
11721
|
-
doing:
|
|
11722
|
-
ready:
|
|
11936
|
+
openGoals: leanRecords("goal", goals, verbose),
|
|
11937
|
+
doing: leanRecords("task", doing, verbose),
|
|
11938
|
+
ready: leanRecords("task", ready, verbose),
|
|
11939
|
+
next: guidance
|
|
11723
11940
|
};
|
|
11724
11941
|
emit2(ctx, result, () => {
|
|
11725
11942
|
ctx.out.log(`${appId}: goal-aligned work intake (read only)`);
|
|
11726
|
-
for (const [label, records] of [
|
|
11727
|
-
["doing",
|
|
11728
|
-
["ready",
|
|
11729
|
-
["open goals",
|
|
11943
|
+
for (const [label, entity, records] of [
|
|
11944
|
+
["doing", "task", doing],
|
|
11945
|
+
["ready", "task", ready],
|
|
11946
|
+
["open goals", "goal", goals]
|
|
11730
11947
|
]) {
|
|
11731
11948
|
ctx.out.log(`${label}:`);
|
|
11732
11949
|
if (!records.length) ctx.out.log("- (none)");
|
|
11733
|
-
else for (const record11 of records) printRecord(
|
|
11734
|
-
ctx,
|
|
11735
|
-
label === "open goals" ? "goal" : "task",
|
|
11736
|
-
record11
|
|
11737
|
-
);
|
|
11738
|
-
}
|
|
11739
|
-
if (!result.openGoals.length) {
|
|
11740
|
-
ctx.out.log("next: discuss alignment with the user before creating or claiming project work");
|
|
11741
|
-
} else if (!result.ready.length) {
|
|
11742
|
-
ctx.out.log("next: refine a linked Backlog task and mark it Ready");
|
|
11743
|
-
} else {
|
|
11744
|
-
ctx.out.log("next: review a Ready task, then claim it with its revision");
|
|
11950
|
+
else for (const record11 of records) printRecord(ctx, entity, record11);
|
|
11745
11951
|
}
|
|
11952
|
+
ctx.out.log(`next: ${guidance}`);
|
|
11746
11953
|
});
|
|
11747
11954
|
}
|
|
11748
11955
|
async function pmHandoff(ctx, parsed) {
|
|
11749
|
-
const appId =
|
|
11750
|
-
const
|
|
11751
|
-
if (!appId) throw new Error("pm handoff needs --app <appId>");
|
|
11956
|
+
const { appId, projectId } = requireApp(ctx, parsed, "handoff");
|
|
11957
|
+
const verbose = wantsVerbose(parsed);
|
|
11752
11958
|
const [goals, tasks, bugs] = await Promise.all([
|
|
11753
|
-
|
|
11754
|
-
|
|
11755
|
-
|
|
11959
|
+
listFiltered(ctx, "goal", appId, projectId, { status: UNMET_GOAL_STATUS }),
|
|
11960
|
+
listFiltered(ctx, "task", appId, projectId, { column: ACTIVE_TASK_COLUMNS }),
|
|
11961
|
+
listFiltered(ctx, "bug", appId, projectId, { status: OPEN_BUG_STATUSES })
|
|
11756
11962
|
]);
|
|
11757
|
-
const
|
|
11963
|
+
const clean4 = !goals.length && !tasks.length && !bugs.length;
|
|
11964
|
+
const result = {
|
|
11758
11965
|
appId,
|
|
11759
11966
|
projectId,
|
|
11760
|
-
unmetGoals:
|
|
11761
|
-
activeTasks:
|
|
11762
|
-
openBugs:
|
|
11763
|
-
|
|
11764
|
-
const result = {
|
|
11765
|
-
...handoff,
|
|
11766
|
-
clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
|
|
11967
|
+
unmetGoals: leanRecords("goal", goals, verbose),
|
|
11968
|
+
activeTasks: leanRecords("task", tasks, verbose),
|
|
11969
|
+
openBugs: leanRecords("bug", bugs, verbose),
|
|
11970
|
+
clean: clean4
|
|
11767
11971
|
};
|
|
11768
11972
|
emit2(ctx, result, () => {
|
|
11769
|
-
if (
|
|
11973
|
+
if (clean4) {
|
|
11770
11974
|
ctx.out.log(`${appId}: no unresolved PM work`);
|
|
11771
11975
|
return;
|
|
11772
11976
|
}
|
|
11773
11977
|
ctx.out.log(`${appId}: authoritative PM handoff`);
|
|
11774
|
-
for (const [label, records] of [
|
|
11775
|
-
["unmet goals",
|
|
11776
|
-
["active tasks",
|
|
11777
|
-
["open bugs",
|
|
11978
|
+
for (const [label, entity, records] of [
|
|
11979
|
+
["unmet goals", "goal", goals],
|
|
11980
|
+
["active tasks", "task", tasks],
|
|
11981
|
+
["open bugs", "bug", bugs]
|
|
11778
11982
|
]) {
|
|
11779
11983
|
ctx.out.log(`${label}:`);
|
|
11780
11984
|
if (!records.length) ctx.out.log("- (none)");
|
|
11781
|
-
else for (const record11 of records) printRecord(
|
|
11782
|
-
ctx,
|
|
11783
|
-
label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
|
|
11784
|
-
record11
|
|
11785
|
-
);
|
|
11985
|
+
else for (const record11 of records) printRecord(ctx, entity, record11);
|
|
11786
11986
|
}
|
|
11787
11987
|
});
|
|
11788
11988
|
}
|
|
11789
|
-
|
|
11790
|
-
|
|
11791
|
-
|
|
11989
|
+
|
|
11990
|
+
// src/pm-start.ts
|
|
11991
|
+
var getTask = async (ctx, id2) => (await pmRequest(ctx, "GET", `/task/${encodeURIComponent(id2)}`)).record;
|
|
11992
|
+
async function claimAt(ctx, task, parsed) {
|
|
11993
|
+
const res = await pmRequest(
|
|
11994
|
+
ctx,
|
|
11995
|
+
"POST",
|
|
11996
|
+
`/task/${encodeURIComponent(task.id)}/claim`,
|
|
11997
|
+
{ expectedRevision: task.revision, mutationId: writeMutationId2(parsed) }
|
|
11998
|
+
);
|
|
11999
|
+
return res.record;
|
|
12000
|
+
}
|
|
12001
|
+
async function selectTask(ctx, parsed, appId, projectId) {
|
|
12002
|
+
const named = parsed.positionals[2];
|
|
12003
|
+
if (named) return getTask(ctx, named);
|
|
12004
|
+
const goalId = stringOpt(parsed.options.goal);
|
|
12005
|
+
const ready = await listFiltered(ctx, "task", appId, projectId, {
|
|
12006
|
+
column: READY_COLUMN,
|
|
12007
|
+
...goalId ? { goalId } : {}
|
|
12008
|
+
});
|
|
12009
|
+
const candidate = ready[0];
|
|
12010
|
+
if (!candidate) {
|
|
12011
|
+
throw new Error(
|
|
12012
|
+
`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.`
|
|
12013
|
+
);
|
|
12014
|
+
}
|
|
12015
|
+
return candidate;
|
|
12016
|
+
}
|
|
12017
|
+
async function pmStart(ctx, parsed) {
|
|
12018
|
+
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
12019
|
+
if (!appId) throw new Error("pm start needs --app <appId>");
|
|
12020
|
+
const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
|
|
12021
|
+
const verbose = wantsVerbose(parsed);
|
|
12022
|
+
const selected = await selectTask(ctx, parsed, appId, projectId);
|
|
12023
|
+
if (selected.column !== READY_COLUMN) {
|
|
12024
|
+
throw new Error(
|
|
12025
|
+
`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.`
|
|
12026
|
+
);
|
|
12027
|
+
}
|
|
12028
|
+
let claimed;
|
|
12029
|
+
try {
|
|
12030
|
+
claimed = await claimAt(ctx, selected, parsed);
|
|
12031
|
+
} catch (error) {
|
|
12032
|
+
if (!isRevisionConflict(error)) throw error;
|
|
12033
|
+
const reloaded = await getTask(ctx, selected.id);
|
|
12034
|
+
if (reloaded.column !== READY_COLUMN) {
|
|
12035
|
+
throw new Error(
|
|
12036
|
+
`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.`
|
|
12037
|
+
);
|
|
12038
|
+
}
|
|
12039
|
+
claimed = await claimAt(ctx, reloaded, parsed);
|
|
12040
|
+
}
|
|
12041
|
+
const task = claimed ?? await getTask(ctx, selected.id);
|
|
12042
|
+
const goalId = typeof task.goalId === "string" ? task.goalId : void 0;
|
|
12043
|
+
const goal = goalId ? await pmRequest(ctx, "GET", `/goal/${encodeURIComponent(goalId)}`).then((res) => res.record).catch(() => null) : null;
|
|
12044
|
+
const result = {
|
|
12045
|
+
claimed: verbose ? task : leanRecord("task", task),
|
|
12046
|
+
goal: goal ? verbose ? goal : leanRecord("goal", goal) : null,
|
|
12047
|
+
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)'
|
|
12048
|
+
};
|
|
12049
|
+
emit2(ctx, result, () => {
|
|
12050
|
+
ctx.out.log(`claimed: ${studioRecordLink(ctx, "task", task)}`);
|
|
12051
|
+
if (goal) ctx.out.log(`goal: ${studioRecordLink(ctx, "goal", goal)}`);
|
|
12052
|
+
if (typeof task.acceptanceCriteria === "string") ctx.out.log(`acceptance: ${task.acceptanceCriteria}`);
|
|
12053
|
+
ctx.out.log(`next: ${result.next}`);
|
|
12054
|
+
});
|
|
11792
12055
|
}
|
|
11793
12056
|
|
|
11794
12057
|
// src/pm-links.ts
|
|
@@ -12262,9 +12525,13 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
12262
12525
|
throw new Error(`unknown pm project action "${action3}". Try list|add|use.`);
|
|
12263
12526
|
}
|
|
12264
12527
|
if (word === "next") {
|
|
12265
|
-
assertArgs(parsed, [...COMMON_OPTIONS, "app", "project"], 2);
|
|
12528
|
+
assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "verbose"], 2);
|
|
12266
12529
|
return pmNext(await buildContext2(parsed, deps), parsed);
|
|
12267
12530
|
}
|
|
12531
|
+
if (word === "start") {
|
|
12532
|
+
assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "goal", "verbose", "mutation-id"], 3);
|
|
12533
|
+
return pmStart(await buildContext2(parsed, deps), parsed);
|
|
12534
|
+
}
|
|
12268
12535
|
if (word === "watch") {
|
|
12269
12536
|
assertArgs(parsed, [
|
|
12270
12537
|
...COMMON_OPTIONS,
|
|
@@ -12282,11 +12549,11 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
12282
12549
|
return pmWatch(await buildContext2(parsed, deps), parsed).then(() => void 0);
|
|
12283
12550
|
}
|
|
12284
12551
|
if (word === "handoff") {
|
|
12285
|
-
assertArgs(parsed, [...COMMON_OPTIONS, "app", "project"], 2);
|
|
12552
|
+
assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "verbose"], 2);
|
|
12286
12553
|
return pmHandoff(await buildContext2(parsed, deps), parsed);
|
|
12287
12554
|
}
|
|
12288
12555
|
const entity = ALIASES[word];
|
|
12289
|
-
if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
|
|
12556
|
+
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).`);
|
|
12290
12557
|
const requestedAction = parsed.positionals[2] ?? "list";
|
|
12291
12558
|
const action2 = canonicalAction(requestedAction);
|
|
12292
12559
|
if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|link|ref|comment|comments|history|rm.`);
|
|
@@ -15809,4 +16076,4 @@ export {
|
|
|
15809
16076
|
isTerminalHostedSecurityStatus,
|
|
15810
16077
|
runCli
|
|
15811
16078
|
};
|
|
15812
|
-
//# sourceMappingURL=chunk-
|
|
16079
|
+
//# sourceMappingURL=chunk-WDWJ7HC7.js.map
|