@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
package/dist/bin.cjs
CHANGED
|
@@ -7915,7 +7915,7 @@ var init_code2 = __esm({
|
|
|
7915
7915
|
}
|
|
7916
7916
|
});
|
|
7917
7917
|
|
|
7918
|
-
// ../harness/dist/chunk-
|
|
7918
|
+
// ../harness/dist/chunk-GYWQM76X.js
|
|
7919
7919
|
async function digestStagedWorkspace(root, limits) {
|
|
7920
7920
|
const files = [];
|
|
7921
7921
|
const walk = async (directory) => {
|
|
@@ -8862,18 +8862,26 @@ async function materializeCommandWorkspace(input) {
|
|
|
8862
8862
|
}
|
|
8863
8863
|
function codeSkill(opts) {
|
|
8864
8864
|
let seq = 0;
|
|
8865
|
+
let nextCompletion = 1;
|
|
8866
|
+
const completed = /* @__PURE__ */ new Map();
|
|
8865
8867
|
const call4 = async (tool, input, signal) => {
|
|
8868
|
+
const sequence = ++seq;
|
|
8866
8869
|
const startedAt = Date.now();
|
|
8867
8870
|
const response2 = await opts.broker.execute(
|
|
8868
8871
|
{ lease: opts.lease, workspaceDir: opts.workspaceDir, signal },
|
|
8869
|
-
{ requestId: `bench-${tool}-${
|
|
8872
|
+
{ requestId: `bench-${tool}-${sequence}`, tool, input }
|
|
8870
8873
|
);
|
|
8871
|
-
|
|
8874
|
+
completed.set(sequence, {
|
|
8872
8875
|
tool,
|
|
8873
8876
|
ok: response2.ok,
|
|
8874
8877
|
durationMs: Date.now() - startedAt,
|
|
8875
8878
|
...response2.ok ? {} : { error: String(response2.content).slice(0, 300) }
|
|
8876
8879
|
});
|
|
8880
|
+
while (completed.has(nextCompletion)) {
|
|
8881
|
+
const completion = completed.get(nextCompletion);
|
|
8882
|
+
completed.delete(nextCompletion++);
|
|
8883
|
+
opts.onToolCall?.(completion);
|
|
8884
|
+
}
|
|
8877
8885
|
return { content: response2.content, isError: !response2.ok };
|
|
8878
8886
|
};
|
|
8879
8887
|
const read22 = {
|
|
@@ -8889,6 +8897,7 @@ function codeSkill(opts) {
|
|
|
8889
8897
|
},
|
|
8890
8898
|
additionalProperties: false
|
|
8891
8899
|
},
|
|
8900
|
+
concurrency: "parallel",
|
|
8892
8901
|
handler: (input, ctx) => call4("sandbox.read", input, ctx.signal)
|
|
8893
8902
|
};
|
|
8894
8903
|
const applyPatch = {
|
|
@@ -8930,6 +8939,7 @@ function codeSkill(opts) {
|
|
|
8930
8939
|
},
|
|
8931
8940
|
additionalProperties: false
|
|
8932
8941
|
},
|
|
8942
|
+
concurrency: "parallel",
|
|
8933
8943
|
handler: (input, ctx) => call4("sandbox.list", input, ctx.signal)
|
|
8934
8944
|
};
|
|
8935
8945
|
const searchFiles = {
|
|
@@ -8946,11 +8956,13 @@ function codeSkill(opts) {
|
|
|
8946
8956
|
},
|
|
8947
8957
|
additionalProperties: false
|
|
8948
8958
|
},
|
|
8959
|
+
concurrency: "parallel",
|
|
8949
8960
|
handler: (input, ctx) => call4("sandbox.search", input, ctx.signal)
|
|
8950
8961
|
};
|
|
8951
8962
|
const graphTool = (name, tool, description, required) => ({
|
|
8952
8963
|
name,
|
|
8953
8964
|
description,
|
|
8965
|
+
concurrency: "parallel",
|
|
8954
8966
|
inputSchema: {
|
|
8955
8967
|
type: "object",
|
|
8956
8968
|
...required ? { required: ["query"] } : {},
|
|
@@ -9125,6 +9137,24 @@ function createCodeRuntimeInference(options) {
|
|
|
9125
9137
|
catalog: {}
|
|
9126
9138
|
};
|
|
9127
9139
|
}
|
|
9140
|
+
function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = registeredFiles) {
|
|
9141
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
9142
|
+
return {
|
|
9143
|
+
files(root) {
|
|
9144
|
+
const existing = cache2.get(root);
|
|
9145
|
+
if (existing) return existing;
|
|
9146
|
+
const pending = enumerate(root, limit).then((paths) => Object.freeze(paths));
|
|
9147
|
+
cache2.set(root, pending);
|
|
9148
|
+
void pending.catch(() => {
|
|
9149
|
+
if (cache2.get(root) === pending) cache2.delete(root);
|
|
9150
|
+
});
|
|
9151
|
+
return pending;
|
|
9152
|
+
},
|
|
9153
|
+
invalidate(root) {
|
|
9154
|
+
cache2.delete(root);
|
|
9155
|
+
}
|
|
9156
|
+
};
|
|
9157
|
+
}
|
|
9128
9158
|
async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
|
|
9129
9159
|
const paths = [];
|
|
9130
9160
|
const walk = async (directory) => {
|
|
@@ -9155,28 +9185,121 @@ function listWorkspace(paths, options = {}) {
|
|
|
9155
9185
|
return scoped.slice(0, max);
|
|
9156
9186
|
}
|
|
9157
9187
|
async function searchWorkspace(root, paths, options) {
|
|
9158
|
-
|
|
9159
|
-
if (!query) throw new TypeError("search query must be a non-empty string");
|
|
9188
|
+
options.signal?.throwIfAborted();
|
|
9189
|
+
if (!options.query) throw new TypeError("search query must be a non-empty string");
|
|
9160
9190
|
const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
|
|
9161
9191
|
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
9162
9192
|
const scoped = listWorkspace(paths, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths.length });
|
|
9193
|
+
if (scoped.length === 0) return [];
|
|
9194
|
+
try {
|
|
9195
|
+
return await nativeSearch(root, scoped, { ...options, maxResults, maxFileBytes });
|
|
9196
|
+
} catch (error) {
|
|
9197
|
+
options.signal?.throwIfAborted();
|
|
9198
|
+
return fallbackSearch(root, scoped, { ...options, maxResults, maxFileBytes });
|
|
9199
|
+
}
|
|
9200
|
+
}
|
|
9201
|
+
async function nativeSearch(root, paths, options) {
|
|
9202
|
+
const batches = [];
|
|
9203
|
+
let batch = [];
|
|
9204
|
+
let bytes = 0;
|
|
9205
|
+
for (const path of paths) {
|
|
9206
|
+
const size = Buffer.byteLength(path) + 1;
|
|
9207
|
+
if (batch.length > 0 && bytes + size > MAX_NATIVE_ARG_BYTES) {
|
|
9208
|
+
batches.push(batch);
|
|
9209
|
+
batch = [];
|
|
9210
|
+
bytes = 0;
|
|
9211
|
+
}
|
|
9212
|
+
batch.push(path);
|
|
9213
|
+
bytes += size;
|
|
9214
|
+
}
|
|
9215
|
+
if (batch.length > 0) batches.push(batch);
|
|
9216
|
+
const matches = [];
|
|
9217
|
+
for (const files of batches) {
|
|
9218
|
+
const remaining = options.maxResults - matches.length;
|
|
9219
|
+
if (remaining <= 0) break;
|
|
9220
|
+
matches.push(...await nativeSearchBatch(root, files, options, remaining));
|
|
9221
|
+
}
|
|
9222
|
+
return matches;
|
|
9223
|
+
}
|
|
9224
|
+
function nativeSearchBatch(root, paths, options, remaining) {
|
|
9225
|
+
return new Promise((resolveMatches, reject) => {
|
|
9226
|
+
const args = [
|
|
9227
|
+
"--fixed-strings",
|
|
9228
|
+
"--json",
|
|
9229
|
+
"--no-messages",
|
|
9230
|
+
"--sort=path",
|
|
9231
|
+
`--max-filesize=${options.maxFileBytes}`,
|
|
9232
|
+
"--max-columns=4096",
|
|
9233
|
+
"--max-columns-preview",
|
|
9234
|
+
options.caseSensitive === false ? "--ignore-case" : "--case-sensitive",
|
|
9235
|
+
"--",
|
|
9236
|
+
options.query,
|
|
9237
|
+
...paths
|
|
9238
|
+
];
|
|
9239
|
+
const child = (0, import_child_process6.spawn)("rg", args, {
|
|
9240
|
+
cwd: root,
|
|
9241
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
9242
|
+
...options.signal ? { signal: options.signal } : {}
|
|
9243
|
+
});
|
|
9244
|
+
const matches = [];
|
|
9245
|
+
let carry = "";
|
|
9246
|
+
let stopped = false;
|
|
9247
|
+
const consume = (line2) => {
|
|
9248
|
+
if (matches.length >= remaining) return;
|
|
9249
|
+
let event;
|
|
9250
|
+
try {
|
|
9251
|
+
event = JSON.parse(line2);
|
|
9252
|
+
} catch {
|
|
9253
|
+
return;
|
|
9254
|
+
}
|
|
9255
|
+
const path = event.data?.path?.text;
|
|
9256
|
+
const lineNumber = event.data?.line_number;
|
|
9257
|
+
const source = event.data?.lines?.text;
|
|
9258
|
+
if (event.type !== "match" || path === void 0 || lineNumber === void 0 || source === void 0) return;
|
|
9259
|
+
matches.push({ path, line: lineNumber, text: source.trim().slice(0, 240) });
|
|
9260
|
+
if (matches.length >= remaining) {
|
|
9261
|
+
stopped = true;
|
|
9262
|
+
child.kill();
|
|
9263
|
+
}
|
|
9264
|
+
};
|
|
9265
|
+
child.stdout.setEncoding("utf8");
|
|
9266
|
+
child.stdout.on("data", (chunk) => {
|
|
9267
|
+
carry += chunk;
|
|
9268
|
+
let newline = carry.indexOf("\n");
|
|
9269
|
+
while (newline >= 0) {
|
|
9270
|
+
consume(carry.slice(0, newline));
|
|
9271
|
+
carry = carry.slice(newline + 1);
|
|
9272
|
+
newline = carry.indexOf("\n");
|
|
9273
|
+
}
|
|
9274
|
+
});
|
|
9275
|
+
child.once("error", reject);
|
|
9276
|
+
child.once("close", (code) => {
|
|
9277
|
+
if (carry) consume(carry);
|
|
9278
|
+
if (stopped || code === 0 || code === 1) resolveMatches(matches);
|
|
9279
|
+
else reject(new Error(`native search exited with status ${code ?? "unknown"}`));
|
|
9280
|
+
});
|
|
9281
|
+
});
|
|
9282
|
+
}
|
|
9283
|
+
async function fallbackSearch(root, scoped, options) {
|
|
9284
|
+
const query = options.caseSensitive === false ? options.query.toLowerCase() : options.query;
|
|
9163
9285
|
const matches = [];
|
|
9164
9286
|
for (const path of scoped) {
|
|
9165
|
-
|
|
9287
|
+
options.signal?.throwIfAborted();
|
|
9288
|
+
if (matches.length >= options.maxResults) break;
|
|
9166
9289
|
let source;
|
|
9167
9290
|
try {
|
|
9168
9291
|
source = await (0, import_promises9.readFile)((0, import_path9.resolve)(root, path));
|
|
9169
9292
|
} catch {
|
|
9170
9293
|
continue;
|
|
9171
9294
|
}
|
|
9172
|
-
if (source.byteLength > maxFileBytes || source.includes(0)) continue;
|
|
9295
|
+
if (source.byteLength > options.maxFileBytes || source.includes(0)) continue;
|
|
9173
9296
|
const lines = source.toString("utf8").split("\n");
|
|
9174
9297
|
for (let index = 0; index < lines.length; index += 1) {
|
|
9175
9298
|
const raw = lines[index];
|
|
9176
9299
|
const haystack = options.caseSensitive === false ? raw.toLowerCase() : raw;
|
|
9177
9300
|
if (!haystack.includes(query)) continue;
|
|
9178
9301
|
matches.push({ path, line: index + 1, text: raw.trim().slice(0, 240) });
|
|
9179
|
-
if (matches.length >= maxResults) break;
|
|
9302
|
+
if (matches.length >= options.maxResults) break;
|
|
9180
9303
|
}
|
|
9181
9304
|
}
|
|
9182
9305
|
return matches;
|
|
@@ -9390,6 +9513,9 @@ function workspaceGraphs(workspaceDir, paths) {
|
|
|
9390
9513
|
cache.set(workspaceDir, built);
|
|
9391
9514
|
return built;
|
|
9392
9515
|
}
|
|
9516
|
+
function forgetWorkspaceGraphs(workspaceDir) {
|
|
9517
|
+
cache.delete(workspaceDir);
|
|
9518
|
+
}
|
|
9393
9519
|
function renderOverview(graphs, prefix) {
|
|
9394
9520
|
const rows = rollup(graphs.graph, FILE, prefix === void 0 ? {} : { prefix });
|
|
9395
9521
|
if (rows.length === 0) return prefix ? `No source under "${prefix}".` : "No source files.";
|
|
@@ -9427,7 +9553,7 @@ function renderWhoTouches(graphs, query) {
|
|
|
9427
9553
|
].join("\n");
|
|
9428
9554
|
}).join("\n\n");
|
|
9429
9555
|
}
|
|
9430
|
-
async function read(context, request3, options, policy) {
|
|
9556
|
+
async function read(context, request3, options, policy, registry) {
|
|
9431
9557
|
exactKeys(request3.input, ["path", "startLine", "endLine"]);
|
|
9432
9558
|
const path = stringField(request3.input, "path");
|
|
9433
9559
|
const startLine = optionalInteger(request3.input.startLine) ?? 1;
|
|
@@ -9435,7 +9561,7 @@ async function read(context, request3, options, policy) {
|
|
|
9435
9561
|
if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
|
|
9436
9562
|
throw new TypeError("requested line range exceeds its bound");
|
|
9437
9563
|
}
|
|
9438
|
-
const paths = await
|
|
9564
|
+
const paths = await registry.files(context.workspaceDir);
|
|
9439
9565
|
if (!paths.includes(path)) {
|
|
9440
9566
|
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.`);
|
|
9441
9567
|
}
|
|
@@ -9455,13 +9581,13 @@ async function read(context, request3, options, policy) {
|
|
|
9455
9581
|
}
|
|
9456
9582
|
return response(request3, true, content2, { path, startLine, endLine: Math.min(endLine, lines.length) });
|
|
9457
9583
|
}
|
|
9458
|
-
async function list(context, request3, options, policy) {
|
|
9584
|
+
async function list(context, request3, options, policy, registry) {
|
|
9459
9585
|
exactKeys(request3.input, ["prefix", "maxEntries"]);
|
|
9460
9586
|
const raw = request3.input.prefix;
|
|
9461
9587
|
const prefix = typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
9462
9588
|
const maxEntries = optionalInteger(request3.input.maxEntries) ?? 1e3;
|
|
9463
9589
|
if (maxEntries > 5e3) throw new TypeError("maxEntries exceeds its bound");
|
|
9464
|
-
const paths = await
|
|
9590
|
+
const paths = await registry.files(context.workspaceDir);
|
|
9465
9591
|
const allowed = await policy.list(policyContext(context, request3, options, { paths, ...prefix ? { prefix } : {} }));
|
|
9466
9592
|
if (!allowed) return response(request3, false, "tool denied by CaMeL policy");
|
|
9467
9593
|
const entries = listWorkspace(paths, { ...prefix ? { prefix } : {}, maxEntries });
|
|
@@ -9479,7 +9605,7 @@ async function list(context, request3, options, policy) {
|
|
|
9479
9605
|
{ count: entries.length, truncated }
|
|
9480
9606
|
);
|
|
9481
9607
|
}
|
|
9482
|
-
async function search(context, request3, options, policy) {
|
|
9608
|
+
async function search(context, request3, options, policy, registry) {
|
|
9483
9609
|
exactKeys(request3.input, ["query", "prefix", "maxResults", "caseSensitive"]);
|
|
9484
9610
|
const query = stringField(request3.input, "query");
|
|
9485
9611
|
if (query.length > 512) throw new TypeError("search query exceeds its bound");
|
|
@@ -9488,21 +9614,22 @@ async function search(context, request3, options, policy) {
|
|
|
9488
9614
|
const maxResults = optionalInteger(request3.input.maxResults) ?? 100;
|
|
9489
9615
|
if (maxResults > 500) throw new TypeError("maxResults exceeds its bound");
|
|
9490
9616
|
const caseSensitive = request3.input.caseSensitive === void 0 ? true : request3.input.caseSensitive === true;
|
|
9491
|
-
const paths = await
|
|
9617
|
+
const paths = await registry.files(context.workspaceDir);
|
|
9492
9618
|
const allowed = await policy.search(policyContext(context, request3, options, { paths, query, ...prefix ? { prefix } : {} }));
|
|
9493
9619
|
if (!allowed) return response(request3, false, "tool denied by CaMeL policy");
|
|
9494
9620
|
const matches = await searchWorkspace(context.workspaceDir, paths, {
|
|
9495
9621
|
query,
|
|
9496
9622
|
maxResults,
|
|
9497
9623
|
caseSensitive,
|
|
9498
|
-
...prefix ? { prefix } : {}
|
|
9624
|
+
...prefix ? { prefix } : {},
|
|
9625
|
+
...context.signal ? { signal: context.signal } : {}
|
|
9499
9626
|
});
|
|
9500
9627
|
if (!matches.length) return response(request3, true, `No match for "${query}".`, { count: 0 });
|
|
9501
9628
|
return response(request3, true, matches.map((match) => `${match.path}:${match.line}: ${match.text}`).join("\n"), {
|
|
9502
9629
|
count: matches.length
|
|
9503
9630
|
});
|
|
9504
9631
|
}
|
|
9505
|
-
async function graphQuery(context, request3, options, policy) {
|
|
9632
|
+
async function graphQuery(context, request3, options, policy, registry) {
|
|
9506
9633
|
exactKeys(request3.input, ["query"]);
|
|
9507
9634
|
const raw = request3.input.query;
|
|
9508
9635
|
const query = typeof raw === "string" ? raw : "";
|
|
@@ -9512,7 +9639,7 @@ async function graphQuery(context, request3, options, policy) {
|
|
|
9512
9639
|
selector: query
|
|
9513
9640
|
}));
|
|
9514
9641
|
if (!allowed) return response(request3, false, "tool denied by CaMeL policy");
|
|
9515
|
-
const paths = await
|
|
9642
|
+
const paths = await registry.files(context.workspaceDir);
|
|
9516
9643
|
const graphs = await workspaceGraphs(context.workspaceDir, paths);
|
|
9517
9644
|
if (request3.tool === "sandbox.overview") {
|
|
9518
9645
|
return response(request3, true, renderOverview(graphs, query || void 0));
|
|
@@ -9526,23 +9653,38 @@ function createCodeToolBroker(options) {
|
|
|
9526
9653
|
validateOptions(options);
|
|
9527
9654
|
const recipes = new Map(options.recipes.map((recipe2) => [recipe2.id, recipe2]));
|
|
9528
9655
|
const policy = createCodePolicyGate(options);
|
|
9529
|
-
|
|
9656
|
+
const registry = createWorkspaceFileRegistry();
|
|
9657
|
+
let barrier = Promise.resolve();
|
|
9658
|
+
const activeReads = /* @__PURE__ */ new Set();
|
|
9530
9659
|
return {
|
|
9531
9660
|
execute(context, request3) {
|
|
9532
|
-
|
|
9533
|
-
|
|
9661
|
+
if (isReadTool(request3.tool)) {
|
|
9662
|
+
const result2 = barrier.then(() => route2(context, request3, options, recipes, policy, registry));
|
|
9663
|
+
const settled = result2.then(() => void 0, () => void 0);
|
|
9664
|
+
activeReads.add(settled);
|
|
9665
|
+
void settled.then(() => {
|
|
9666
|
+
activeReads.delete(settled);
|
|
9667
|
+
});
|
|
9668
|
+
return result2;
|
|
9669
|
+
}
|
|
9670
|
+
const earlierReads = [...activeReads];
|
|
9671
|
+
const result = barrier.then(() => Promise.all(earlierReads)).then(() => route2(context, request3, options, recipes, policy, registry));
|
|
9672
|
+
barrier = result.then(() => void 0, () => void 0);
|
|
9534
9673
|
return result;
|
|
9535
9674
|
}
|
|
9536
9675
|
};
|
|
9537
9676
|
}
|
|
9538
|
-
|
|
9677
|
+
function isReadTool(tool) {
|
|
9678
|
+
return tool === "sandbox.read" || tool === "sandbox.list" || tool === "sandbox.search" || GRAPH_TOOLS.has(tool);
|
|
9679
|
+
}
|
|
9680
|
+
async function route2(context, request3, options, recipes, policy, registry) {
|
|
9539
9681
|
try {
|
|
9540
9682
|
if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
|
|
9541
|
-
if (request3.tool === "sandbox.read") return await read(context, request3, options, policy);
|
|
9542
|
-
if (request3.tool === "sandbox.list") return await list(context, request3, options, policy);
|
|
9543
|
-
if (request3.tool === "sandbox.search") return await search(context, request3, options, policy);
|
|
9544
|
-
if (GRAPH_TOOLS.has(request3.tool)) return await graphQuery(context, request3, options, policy);
|
|
9545
|
-
if (request3.tool === "sandbox.apply_patch") return await patch(context, request3, options, policy);
|
|
9683
|
+
if (request3.tool === "sandbox.read") return await read(context, request3, options, policy, registry);
|
|
9684
|
+
if (request3.tool === "sandbox.list") return await list(context, request3, options, policy, registry);
|
|
9685
|
+
if (request3.tool === "sandbox.search") return await search(context, request3, options, policy, registry);
|
|
9686
|
+
if (GRAPH_TOOLS.has(request3.tool)) return await graphQuery(context, request3, options, policy, registry);
|
|
9687
|
+
if (request3.tool === "sandbox.apply_patch") return await patch(context, request3, options, policy, registry);
|
|
9546
9688
|
return await recipe(context, request3, options, recipes, policy);
|
|
9547
9689
|
} catch (reason) {
|
|
9548
9690
|
return response(request3, false, toolFailureMessage(reason));
|
|
@@ -9557,7 +9699,7 @@ function toolFailureMessage(reason) {
|
|
|
9557
9699
|
if (code === "EACCES" || code === "EPERM") return "that path is not readable through this tool";
|
|
9558
9700
|
return "tool failed closed";
|
|
9559
9701
|
}
|
|
9560
|
-
async function patch(context, request3, options, policy) {
|
|
9702
|
+
async function patch(context, request3, options, policy, registry) {
|
|
9561
9703
|
exactKeys(request3.input, ["patch"]);
|
|
9562
9704
|
const value2 = stringField(request3.input, "patch");
|
|
9563
9705
|
const paths = validateCodePatch(value2, options.maxPatchBytes ?? 256 * 1024);
|
|
@@ -9567,6 +9709,8 @@ async function patch(context, request3, options, policy) {
|
|
|
9567
9709
|
const allowed = await policy.patch(policyContext(context, request3, options, { patch: value2 }));
|
|
9568
9710
|
if (!allowed) return response(request3, false, "tool denied by CaMeL policy");
|
|
9569
9711
|
await applyCodePatch(context.workspaceDir, value2, paths);
|
|
9712
|
+
registry.invalidate(context.workspaceDir);
|
|
9713
|
+
forgetWorkspaceGraphs(context.workspaceDir);
|
|
9570
9714
|
return response(request3, true, `Applied patch to ${paths.length} file(s).`, { paths });
|
|
9571
9715
|
}
|
|
9572
9716
|
async function recipe(context, request3, options, recipes, policy) {
|
|
@@ -9917,9 +10061,9 @@ async function appendCodeRuntimeEvent(control, command, event, refs) {
|
|
|
9917
10061
|
const bounded = event.type === "message" ? { ...event, body: event.body.trim().slice(0, 2e4) || `${event.actor} event` } : event;
|
|
9918
10062
|
await control.appendSessionEvent(command.sessionId, eventId, bounded);
|
|
9919
10063
|
}
|
|
9920
|
-
var import_crypto, import_promises5, import_path5, import_child_process4, import_promises6, import_path6, import_child_process5, import_process2, import_crypto2, import_crypto3, import_fs2, import_promises7, import_path7, import_promises8, import_os3, import_path8, import_ai4, import_ai5, import_promises9, import_path9, import_promises10, import_promises11, import_path10, import_crypto4, CODE_RUNTIME_PROTOCOL_VERSION, CodeRuntimeReconciler, CodeRuntimeControlError, record5, invalid, RESERVED, SECRET, PATH, FORBIDDEN, ARTIFACT_PATH, PRIVATE_ARTIFACT_PART, SHA3, DIGEST3, ID3, RULE, DEFAULT_PREFIXES, DEFAULT_SUFFIXES, message, CodeRuntimeCheckpointManager, record22, SOURCE_LIMITS, RESERVED2, SECRET2, SOURCE_MAX_FILES, SOURCE_MAX_BYTES, SOURCE_SET_MAX_BYTES, V1_SYSTEM_PROMPT, V2_SYSTEM_PROMPT, V3_SYSTEM_PROMPT, SYSTEM_PROMPT_FOR, DEFAULT_MAX_FILES, DEFAULT_MAX_RESULTS, DEFAULT_MAX_FILE_BYTES, DESTINATIONS, READ, LIST, SEARCH, GRAPH, PATCH, RECIPE, cache, shortId, GRAPH_TOOLS, MAX_MEMORY_BODY, POSITIVE, digestRuntimeValue, runtimeErrorMessage, TheseusRuntimeEngine;
|
|
9921
|
-
var
|
|
9922
|
-
"../harness/dist/chunk-
|
|
10064
|
+
var import_crypto, import_promises5, import_path5, import_child_process4, import_promises6, import_path6, import_child_process5, import_process2, import_crypto2, import_crypto3, import_fs2, import_promises7, import_path7, import_promises8, import_os3, import_path8, import_ai4, import_ai5, import_child_process6, import_promises9, import_path9, import_promises10, import_promises11, import_path10, import_crypto4, CODE_RUNTIME_PROTOCOL_VERSION, CodeRuntimeReconciler, CodeRuntimeControlError, record5, invalid, RESERVED, SECRET, PATH, FORBIDDEN, ARTIFACT_PATH, PRIVATE_ARTIFACT_PART, SHA3, DIGEST3, ID3, RULE, DEFAULT_PREFIXES, DEFAULT_SUFFIXES, message, CodeRuntimeCheckpointManager, record22, SOURCE_LIMITS, RESERVED2, SECRET2, SOURCE_MAX_FILES, SOURCE_MAX_BYTES, SOURCE_SET_MAX_BYTES, V1_SYSTEM_PROMPT, V2_SYSTEM_PROMPT, V3_SYSTEM_PROMPT, SYSTEM_PROMPT_FOR, DEFAULT_MAX_FILES, DEFAULT_MAX_RESULTS, DEFAULT_MAX_FILE_BYTES, MAX_NATIVE_ARG_BYTES, DESTINATIONS, READ, LIST, SEARCH, GRAPH, PATCH, RECIPE, cache, shortId, GRAPH_TOOLS, MAX_MEMORY_BODY, POSITIVE, digestRuntimeValue, runtimeErrorMessage, TheseusRuntimeEngine;
|
|
10065
|
+
var init_chunk_GYWQM76X = __esm({
|
|
10066
|
+
"../harness/dist/chunk-GYWQM76X.js"() {
|
|
9923
10067
|
"use strict";
|
|
9924
10068
|
init_cjs_shims();
|
|
9925
10069
|
init_chunk_K76I2TCQ();
|
|
@@ -9945,6 +10089,7 @@ var init_chunk_QZXCQSPZ = __esm({
|
|
|
9945
10089
|
import_path8 = require("path");
|
|
9946
10090
|
import_ai4 = require("@odla-ai/ai");
|
|
9947
10091
|
import_ai5 = require("@odla-ai/ai");
|
|
10092
|
+
import_child_process6 = require("child_process");
|
|
9948
10093
|
import_promises9 = require("fs/promises");
|
|
9949
10094
|
import_path9 = require("path");
|
|
9950
10095
|
init_dist();
|
|
@@ -10073,7 +10218,10 @@ Never claim a build or test passed unless odla_run_recipe returned that result.`
|
|
|
10073
10218
|
V2_SYSTEM_PROMPT = `You are the coding agent inside an odla Code harness.
|
|
10074
10219
|
Start by orienting: odla_list shows the files in the workspace and odla_search
|
|
10075
10220
|
finds a literal string across them. Prefer those over guessing a path.
|
|
10076
|
-
Then odla_read a bounded range, and odla_apply_git_diff to mutate.
|
|
10221
|
+
Then odla_read a bounded range, and odla_apply_git_diff to mutate. When you
|
|
10222
|
+
need several independent searches or file ranges, issue those read-only calls
|
|
10223
|
+
together in one turn; their results stay ordered and the harness overlaps them.
|
|
10224
|
+
Never issue odla_apply_git_diff or odla_run_recipe alongside another tool call.
|
|
10077
10225
|
For mutations, call odla_apply_git_diff with raw git diff text. It must start
|
|
10078
10226
|
with "diff --git a/<path> b/<path>", include matching "---" and "+++" file
|
|
10079
10227
|
headers and numbered "@@" hunks, and never use "*** Begin Patch" wrappers.
|
|
@@ -10105,6 +10253,7 @@ Never claim a build or test passed unless odla_run_recipe returned that result.`
|
|
|
10105
10253
|
DEFAULT_MAX_FILES = 2e4;
|
|
10106
10254
|
DEFAULT_MAX_RESULTS = 100;
|
|
10107
10255
|
DEFAULT_MAX_FILE_BYTES = 512 * 1024;
|
|
10256
|
+
MAX_NATIVE_ARG_BYTES = 96 * 1024;
|
|
10108
10257
|
DESTINATIONS = "code-workspaces.v1";
|
|
10109
10258
|
READ = descriptor("sandbox.read", "scoped_data_read", {
|
|
10110
10259
|
workspace: "destination",
|
|
@@ -10442,7 +10591,7 @@ var init_node = __esm({
|
|
|
10442
10591
|
"../harness/dist/node.js"() {
|
|
10443
10592
|
"use strict";
|
|
10444
10593
|
init_cjs_shims();
|
|
10445
|
-
|
|
10594
|
+
init_chunk_GYWQM76X();
|
|
10446
10595
|
init_chunk_K76I2TCQ();
|
|
10447
10596
|
MEASURED_PREMIUM = Object.freeze({
|
|
10448
10597
|
/** 3 racers vs pure depth at equal budget: 21,044 / 7,936. */
|
|
@@ -11500,7 +11649,8 @@ Usage:
|
|
|
11500
11649
|
odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
11501
11650
|
odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
|
|
11502
11651
|
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]
|
|
11503
|
-
odla-ai pm next --app <id> [--project <id>] [--json]
|
|
11652
|
+
odla-ai pm next --app <id> [--project <id>] [--verbose] [--json]
|
|
11653
|
+
odla-ai pm start --app <id> [<task-id>] [--goal <id>] [--verbose] [--mutation-id <id>] [--json] [claims the top Ready task in one call]
|
|
11504
11654
|
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]
|
|
11505
11655
|
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]
|
|
11506
11656
|
odla-ai pm task claim <id> --expected-revision <n> [--mutation-id <id>] [--json]
|
|
@@ -11521,7 +11671,7 @@ Usage:
|
|
|
11521
11671
|
odla-ai pm <goal|task|decision|bug> comments <id> [--json]
|
|
11522
11672
|
odla-ai pm <goal|task|decision|bug> history <id> [--limit <n>] [--json]
|
|
11523
11673
|
odla-ai pm <goal|task|decision|bug> rm <id>
|
|
11524
|
-
odla-ai pm handoff --app <id> [--project <id>] [--json]
|
|
11674
|
+
odla-ai pm handoff --app <id> [--project <id>] [--verbose] [--json]
|
|
11525
11675
|
odla-ai discuss groups [--json]
|
|
11526
11676
|
odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--mentions] [--json]
|
|
11527
11677
|
odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
|
|
@@ -11688,6 +11838,16 @@ Commands:
|
|
|
11688
11838
|
execution contract and a reviewed revision. Any caller with PM
|
|
11689
11839
|
mutation access to that app/project may do it; claims coordinate
|
|
11690
11840
|
execution but never own or lock the PM record.
|
|
11841
|
+
To pick up work: "pm next" reads open goals, Ready candidates,
|
|
11842
|
+
and work in progress; "pm start" claims the top Ready task in
|
|
11843
|
+
one call, compare-and-swapped on the revision it just read, and
|
|
11844
|
+
returns the task, its goal, and the acceptance criteria to work
|
|
11845
|
+
against. Finish with "pm task done <id>", or hand it back with
|
|
11846
|
+
"pm task release <id> --expected-revision <n>". "pm start" never
|
|
11847
|
+
marks work Ready: that contract is reviewed separately so an
|
|
11848
|
+
agent cannot authorize its own work.
|
|
11849
|
+
Intake output is projected to the fields an executing caller
|
|
11850
|
+
acts on; --verbose returns whole records with their audit trail.
|
|
11691
11851
|
bug Intent-first alias for PM bugs. "bug report" writes to
|
|
11692
11852
|
odla PM; odla product defects do not belong in GitHub Issues.
|
|
11693
11853
|
discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
|
|
@@ -12499,7 +12659,8 @@ async function pmRequest(ctx, method, path, body) {
|
|
|
12499
12659
|
const message2 = error.message;
|
|
12500
12660
|
if (typeof message2 === "string" && message2.length > 0) detail = message2;
|
|
12501
12661
|
}
|
|
12502
|
-
throw new
|
|
12662
|
+
throw new PmRequestError(
|
|
12663
|
+
response2.status,
|
|
12503
12664
|
`pm ${method} ${path} failed: ${detail ?? `registry returned ${response2.status}`} (${response2.status})`
|
|
12504
12665
|
);
|
|
12505
12666
|
}
|
|
@@ -12559,7 +12720,7 @@ function emit2(ctx, value2, human) {
|
|
|
12559
12720
|
if (ctx.json) ctx.out.log(JSON.stringify(value2, null, 2));
|
|
12560
12721
|
else human();
|
|
12561
12722
|
}
|
|
12562
|
-
var DONE, writeMutationId2, FIELD_MAP, STUDIO_SECTION;
|
|
12723
|
+
var DONE, writeMutationId2, FIELD_MAP, PmRequestError, isRevisionConflict, STUDIO_SECTION;
|
|
12563
12724
|
var init_pm_action_core = __esm({
|
|
12564
12725
|
"src/pm-action-core.ts"() {
|
|
12565
12726
|
"use strict";
|
|
@@ -12592,6 +12753,15 @@ var init_pm_action_core = __esm({
|
|
|
12592
12753
|
execution: { key: "executionMode" },
|
|
12593
12754
|
"expected-revision": { key: "expectedRevision", num: true }
|
|
12594
12755
|
};
|
|
12756
|
+
PmRequestError = class extends Error {
|
|
12757
|
+
constructor(status, message2) {
|
|
12758
|
+
super(message2);
|
|
12759
|
+
this.status = status;
|
|
12760
|
+
this.name = "PmRequestError";
|
|
12761
|
+
}
|
|
12762
|
+
status;
|
|
12763
|
+
};
|
|
12764
|
+
isRevisionConflict = (error) => error instanceof PmRequestError && error.status === 409;
|
|
12595
12765
|
STUDIO_SECTION = {
|
|
12596
12766
|
goal: "goals",
|
|
12597
12767
|
task: "board",
|
|
@@ -12723,110 +12893,231 @@ async function pmTaskLifecycle(ctx, id2, action2, parsed) {
|
|
|
12723
12893
|
ctx.out.log(`task: ${label} \u2192 ${state2}`);
|
|
12724
12894
|
});
|
|
12725
12895
|
}
|
|
12726
|
-
async function
|
|
12896
|
+
async function pmRemove(ctx, entity, id2) {
|
|
12897
|
+
await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id2)}`);
|
|
12898
|
+
ctx.out.log(`deleted ${entity} ${id2}`);
|
|
12899
|
+
}
|
|
12900
|
+
var init_pm_actions = __esm({
|
|
12901
|
+
"src/pm-actions.ts"() {
|
|
12902
|
+
"use strict";
|
|
12903
|
+
init_cjs_shims();
|
|
12904
|
+
init_argv();
|
|
12905
|
+
init_pm_action_core();
|
|
12906
|
+
}
|
|
12907
|
+
});
|
|
12908
|
+
|
|
12909
|
+
// src/pm-lean.ts
|
|
12910
|
+
function leanRecord(entity, record11) {
|
|
12911
|
+
const out = {};
|
|
12912
|
+
for (const key of [...COMMON, ...PER_ENTITY[entity]]) {
|
|
12913
|
+
const value2 = record11[key];
|
|
12914
|
+
if (value2 !== void 0 && value2 !== null) out[key] = value2;
|
|
12915
|
+
}
|
|
12916
|
+
return out;
|
|
12917
|
+
}
|
|
12918
|
+
function leanRecords(entity, records, verbose) {
|
|
12919
|
+
return verbose ? records : records.map((record11) => leanRecord(entity, record11));
|
|
12920
|
+
}
|
|
12921
|
+
var COMMON, PER_ENTITY;
|
|
12922
|
+
var init_pm_lean = __esm({
|
|
12923
|
+
"src/pm-lean.ts"() {
|
|
12924
|
+
"use strict";
|
|
12925
|
+
init_cjs_shims();
|
|
12926
|
+
COMMON = ["id", "appId", "projectId", "title", "revision"];
|
|
12927
|
+
PER_ENTITY = {
|
|
12928
|
+
goal: ["status", "proof", "targetPct", "currentPct"],
|
|
12929
|
+
task: [
|
|
12930
|
+
"column",
|
|
12931
|
+
"goalId",
|
|
12932
|
+
"alignmentDecisionId",
|
|
12933
|
+
"description",
|
|
12934
|
+
"acceptanceCriteria",
|
|
12935
|
+
"executionMode",
|
|
12936
|
+
"assigneeId",
|
|
12937
|
+
"claimedByPrincipalId",
|
|
12938
|
+
"dueAt"
|
|
12939
|
+
],
|
|
12940
|
+
decision: ["status", "body", "supersedesId"],
|
|
12941
|
+
bug: ["status", "severity", "description", "goalId", "assigneeId", "decisionId"]
|
|
12942
|
+
};
|
|
12943
|
+
}
|
|
12944
|
+
});
|
|
12945
|
+
|
|
12946
|
+
// src/pm-intake.ts
|
|
12947
|
+
async function listFiltered(ctx, entity, appId, projectId, filters) {
|
|
12727
12948
|
const records = [];
|
|
12728
12949
|
for (; ; ) {
|
|
12729
|
-
const q = new URLSearchParams({
|
|
12730
|
-
app: appId,
|
|
12731
|
-
limit: "100",
|
|
12732
|
-
offset: String(records.length)
|
|
12733
|
-
});
|
|
12950
|
+
const q = new URLSearchParams({ app: appId, limit: "100", offset: String(records.length) });
|
|
12734
12951
|
if (projectId) q.set("project", projectId);
|
|
12952
|
+
for (const [key, value2] of Object.entries(filters)) q.set(key, value2);
|
|
12735
12953
|
const page2 = await pmRequest(ctx, "GET", `/${entity}?${q}`);
|
|
12736
12954
|
records.push(...page2.records);
|
|
12737
12955
|
if (records.length >= page2.total || page2.records.length === 0) return records;
|
|
12738
12956
|
}
|
|
12739
12957
|
}
|
|
12740
|
-
|
|
12958
|
+
function requireApp(ctx, parsed, command) {
|
|
12741
12959
|
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
12742
|
-
|
|
12743
|
-
|
|
12960
|
+
if (!appId) throw new Error(`pm ${command} needs --app <appId>`);
|
|
12961
|
+
return { appId, projectId: stringOpt(parsed.options.project) ?? ctx.projectId };
|
|
12962
|
+
}
|
|
12963
|
+
async function pmNext(ctx, parsed) {
|
|
12964
|
+
const { appId, projectId } = requireApp(ctx, parsed, "next");
|
|
12965
|
+
const verbose = wantsVerbose(parsed);
|
|
12744
12966
|
const [goals, tasks] = await Promise.all([
|
|
12745
|
-
|
|
12746
|
-
|
|
12967
|
+
listFiltered(ctx, "goal", appId, projectId, { status: UNMET_GOAL_STATUS }),
|
|
12968
|
+
// One request for both live columns; the split below is over open work only.
|
|
12969
|
+
listFiltered(ctx, "task", appId, projectId, { column: `${READY_COLUMN},doing` })
|
|
12747
12970
|
]);
|
|
12971
|
+
const ready = tasks.filter((record11) => record11.column === READY_COLUMN);
|
|
12972
|
+
const doing = tasks.filter((record11) => record11.column === "doing");
|
|
12973
|
+
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}`;
|
|
12748
12974
|
const result = {
|
|
12749
12975
|
appId,
|
|
12750
12976
|
projectId,
|
|
12751
|
-
openGoals:
|
|
12752
|
-
doing:
|
|
12753
|
-
ready:
|
|
12977
|
+
openGoals: leanRecords("goal", goals, verbose),
|
|
12978
|
+
doing: leanRecords("task", doing, verbose),
|
|
12979
|
+
ready: leanRecords("task", ready, verbose),
|
|
12980
|
+
next: guidance
|
|
12754
12981
|
};
|
|
12755
12982
|
emit2(ctx, result, () => {
|
|
12756
12983
|
ctx.out.log(`${appId}: goal-aligned work intake (read only)`);
|
|
12757
|
-
for (const [label, records] of [
|
|
12758
|
-
["doing",
|
|
12759
|
-
["ready",
|
|
12760
|
-
["open goals",
|
|
12984
|
+
for (const [label, entity, records] of [
|
|
12985
|
+
["doing", "task", doing],
|
|
12986
|
+
["ready", "task", ready],
|
|
12987
|
+
["open goals", "goal", goals]
|
|
12761
12988
|
]) {
|
|
12762
12989
|
ctx.out.log(`${label}:`);
|
|
12763
12990
|
if (!records.length) ctx.out.log("- (none)");
|
|
12764
|
-
else for (const record11 of records) printRecord(
|
|
12765
|
-
ctx,
|
|
12766
|
-
label === "open goals" ? "goal" : "task",
|
|
12767
|
-
record11
|
|
12768
|
-
);
|
|
12769
|
-
}
|
|
12770
|
-
if (!result.openGoals.length) {
|
|
12771
|
-
ctx.out.log("next: discuss alignment with the user before creating or claiming project work");
|
|
12772
|
-
} else if (!result.ready.length) {
|
|
12773
|
-
ctx.out.log("next: refine a linked Backlog task and mark it Ready");
|
|
12774
|
-
} else {
|
|
12775
|
-
ctx.out.log("next: review a Ready task, then claim it with its revision");
|
|
12991
|
+
else for (const record11 of records) printRecord(ctx, entity, record11);
|
|
12776
12992
|
}
|
|
12993
|
+
ctx.out.log(`next: ${guidance}`);
|
|
12777
12994
|
});
|
|
12778
12995
|
}
|
|
12779
12996
|
async function pmHandoff(ctx, parsed) {
|
|
12780
|
-
const appId =
|
|
12781
|
-
const
|
|
12782
|
-
if (!appId) throw new Error("pm handoff needs --app <appId>");
|
|
12997
|
+
const { appId, projectId } = requireApp(ctx, parsed, "handoff");
|
|
12998
|
+
const verbose = wantsVerbose(parsed);
|
|
12783
12999
|
const [goals, tasks, bugs] = await Promise.all([
|
|
12784
|
-
|
|
12785
|
-
|
|
12786
|
-
|
|
13000
|
+
listFiltered(ctx, "goal", appId, projectId, { status: UNMET_GOAL_STATUS }),
|
|
13001
|
+
listFiltered(ctx, "task", appId, projectId, { column: ACTIVE_TASK_COLUMNS }),
|
|
13002
|
+
listFiltered(ctx, "bug", appId, projectId, { status: OPEN_BUG_STATUSES })
|
|
12787
13003
|
]);
|
|
12788
|
-
const
|
|
13004
|
+
const clean4 = !goals.length && !tasks.length && !bugs.length;
|
|
13005
|
+
const result = {
|
|
12789
13006
|
appId,
|
|
12790
13007
|
projectId,
|
|
12791
|
-
unmetGoals:
|
|
12792
|
-
activeTasks:
|
|
12793
|
-
openBugs:
|
|
12794
|
-
|
|
12795
|
-
const result = {
|
|
12796
|
-
...handoff,
|
|
12797
|
-
clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
|
|
13008
|
+
unmetGoals: leanRecords("goal", goals, verbose),
|
|
13009
|
+
activeTasks: leanRecords("task", tasks, verbose),
|
|
13010
|
+
openBugs: leanRecords("bug", bugs, verbose),
|
|
13011
|
+
clean: clean4
|
|
12798
13012
|
};
|
|
12799
13013
|
emit2(ctx, result, () => {
|
|
12800
|
-
if (
|
|
13014
|
+
if (clean4) {
|
|
12801
13015
|
ctx.out.log(`${appId}: no unresolved PM work`);
|
|
12802
13016
|
return;
|
|
12803
13017
|
}
|
|
12804
13018
|
ctx.out.log(`${appId}: authoritative PM handoff`);
|
|
12805
|
-
for (const [label, records] of [
|
|
12806
|
-
["unmet goals",
|
|
12807
|
-
["active tasks",
|
|
12808
|
-
["open bugs",
|
|
13019
|
+
for (const [label, entity, records] of [
|
|
13020
|
+
["unmet goals", "goal", goals],
|
|
13021
|
+
["active tasks", "task", tasks],
|
|
13022
|
+
["open bugs", "bug", bugs]
|
|
12809
13023
|
]) {
|
|
12810
13024
|
ctx.out.log(`${label}:`);
|
|
12811
13025
|
if (!records.length) ctx.out.log("- (none)");
|
|
12812
|
-
else for (const record11 of records) printRecord(
|
|
12813
|
-
ctx,
|
|
12814
|
-
label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
|
|
12815
|
-
record11
|
|
12816
|
-
);
|
|
13026
|
+
else for (const record11 of records) printRecord(ctx, entity, record11);
|
|
12817
13027
|
}
|
|
12818
13028
|
});
|
|
12819
13029
|
}
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
|
|
13030
|
+
var READY_COLUMN, UNMET_GOAL_STATUS, ACTIVE_TASK_COLUMNS, OPEN_BUG_STATUSES, wantsVerbose;
|
|
13031
|
+
var init_pm_intake = __esm({
|
|
13032
|
+
"src/pm-intake.ts"() {
|
|
13033
|
+
"use strict";
|
|
13034
|
+
init_cjs_shims();
|
|
13035
|
+
init_argv();
|
|
13036
|
+
init_pm_action_core();
|
|
13037
|
+
init_pm_lean();
|
|
13038
|
+
READY_COLUMN = "todo";
|
|
13039
|
+
UNMET_GOAL_STATUS = "open";
|
|
13040
|
+
ACTIVE_TASK_COLUMNS = "backlog,todo,doing,review";
|
|
13041
|
+
OPEN_BUG_STATUSES = "open,triaged";
|
|
13042
|
+
wantsVerbose = (parsed) => parsed.options.verbose === true;
|
|
13043
|
+
}
|
|
13044
|
+
});
|
|
13045
|
+
|
|
13046
|
+
// src/pm-start.ts
|
|
13047
|
+
async function claimAt(ctx, task, parsed) {
|
|
13048
|
+
const res = await pmRequest(
|
|
13049
|
+
ctx,
|
|
13050
|
+
"POST",
|
|
13051
|
+
`/task/${encodeURIComponent(task.id)}/claim`,
|
|
13052
|
+
{ expectedRevision: task.revision, mutationId: writeMutationId2(parsed) }
|
|
13053
|
+
);
|
|
13054
|
+
return res.record;
|
|
13055
|
+
}
|
|
13056
|
+
async function selectTask(ctx, parsed, appId, projectId) {
|
|
13057
|
+
const named = parsed.positionals[2];
|
|
13058
|
+
if (named) return getTask(ctx, named);
|
|
13059
|
+
const goalId = stringOpt(parsed.options.goal);
|
|
13060
|
+
const ready = await listFiltered(ctx, "task", appId, projectId, {
|
|
13061
|
+
column: READY_COLUMN,
|
|
13062
|
+
...goalId ? { goalId } : {}
|
|
13063
|
+
});
|
|
13064
|
+
const candidate = ready[0];
|
|
13065
|
+
if (!candidate) {
|
|
13066
|
+
throw new Error(
|
|
13067
|
+
`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.`
|
|
13068
|
+
);
|
|
13069
|
+
}
|
|
13070
|
+
return candidate;
|
|
12823
13071
|
}
|
|
12824
|
-
|
|
12825
|
-
|
|
13072
|
+
async function pmStart(ctx, parsed) {
|
|
13073
|
+
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
13074
|
+
if (!appId) throw new Error("pm start needs --app <appId>");
|
|
13075
|
+
const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
|
|
13076
|
+
const verbose = wantsVerbose(parsed);
|
|
13077
|
+
const selected = await selectTask(ctx, parsed, appId, projectId);
|
|
13078
|
+
if (selected.column !== READY_COLUMN) {
|
|
13079
|
+
throw new Error(
|
|
13080
|
+
`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.`
|
|
13081
|
+
);
|
|
13082
|
+
}
|
|
13083
|
+
let claimed;
|
|
13084
|
+
try {
|
|
13085
|
+
claimed = await claimAt(ctx, selected, parsed);
|
|
13086
|
+
} catch (error) {
|
|
13087
|
+
if (!isRevisionConflict(error)) throw error;
|
|
13088
|
+
const reloaded = await getTask(ctx, selected.id);
|
|
13089
|
+
if (reloaded.column !== READY_COLUMN) {
|
|
13090
|
+
throw new Error(
|
|
13091
|
+
`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.`
|
|
13092
|
+
);
|
|
13093
|
+
}
|
|
13094
|
+
claimed = await claimAt(ctx, reloaded, parsed);
|
|
13095
|
+
}
|
|
13096
|
+
const task = claimed ?? await getTask(ctx, selected.id);
|
|
13097
|
+
const goalId = typeof task.goalId === "string" ? task.goalId : void 0;
|
|
13098
|
+
const goal = goalId ? await pmRequest(ctx, "GET", `/goal/${encodeURIComponent(goalId)}`).then((res) => res.record).catch(() => null) : null;
|
|
13099
|
+
const result = {
|
|
13100
|
+
claimed: verbose ? task : leanRecord("task", task),
|
|
13101
|
+
goal: goal ? verbose ? goal : leanRecord("goal", goal) : null,
|
|
13102
|
+
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)'
|
|
13103
|
+
};
|
|
13104
|
+
emit2(ctx, result, () => {
|
|
13105
|
+
ctx.out.log(`claimed: ${studioRecordLink(ctx, "task", task)}`);
|
|
13106
|
+
if (goal) ctx.out.log(`goal: ${studioRecordLink(ctx, "goal", goal)}`);
|
|
13107
|
+
if (typeof task.acceptanceCriteria === "string") ctx.out.log(`acceptance: ${task.acceptanceCriteria}`);
|
|
13108
|
+
ctx.out.log(`next: ${result.next}`);
|
|
13109
|
+
});
|
|
13110
|
+
}
|
|
13111
|
+
var getTask;
|
|
13112
|
+
var init_pm_start = __esm({
|
|
13113
|
+
"src/pm-start.ts"() {
|
|
12826
13114
|
"use strict";
|
|
12827
13115
|
init_cjs_shims();
|
|
12828
13116
|
init_argv();
|
|
12829
13117
|
init_pm_action_core();
|
|
13118
|
+
init_pm_lean();
|
|
13119
|
+
init_pm_intake();
|
|
13120
|
+
getTask = async (ctx, id2) => (await pmRequest(ctx, "GET", `/task/${encodeURIComponent(id2)}`)).record;
|
|
12830
13121
|
}
|
|
12831
13122
|
});
|
|
12832
13123
|
|
|
@@ -13308,9 +13599,13 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
13308
13599
|
throw new Error(`unknown pm project action "${action3}". Try list|add|use.`);
|
|
13309
13600
|
}
|
|
13310
13601
|
if (word === "next") {
|
|
13311
|
-
assertArgs(parsed, [...COMMON_OPTIONS, "app", "project"], 2);
|
|
13602
|
+
assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "verbose"], 2);
|
|
13312
13603
|
return pmNext(await buildContext2(parsed, deps), parsed);
|
|
13313
13604
|
}
|
|
13605
|
+
if (word === "start") {
|
|
13606
|
+
assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "goal", "verbose", "mutation-id"], 3);
|
|
13607
|
+
return pmStart(await buildContext2(parsed, deps), parsed);
|
|
13608
|
+
}
|
|
13314
13609
|
if (word === "watch") {
|
|
13315
13610
|
assertArgs(parsed, [
|
|
13316
13611
|
...COMMON_OPTIONS,
|
|
@@ -13328,11 +13623,11 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
13328
13623
|
return pmWatch(await buildContext2(parsed, deps), parsed).then(() => void 0);
|
|
13329
13624
|
}
|
|
13330
13625
|
if (word === "handoff") {
|
|
13331
|
-
assertArgs(parsed, [...COMMON_OPTIONS, "app", "project"], 2);
|
|
13626
|
+
assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "verbose"], 2);
|
|
13332
13627
|
return pmHandoff(await buildContext2(parsed, deps), parsed);
|
|
13333
13628
|
}
|
|
13334
13629
|
const entity = ALIASES[word];
|
|
13335
|
-
if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
|
|
13630
|
+
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).`);
|
|
13336
13631
|
const requestedAction = parsed.positionals[2] ?? "list";
|
|
13337
13632
|
const action2 = canonicalAction(requestedAction);
|
|
13338
13633
|
if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|link|ref|comment|comments|history|rm.`);
|
|
@@ -13379,6 +13674,8 @@ var init_pm_command = __esm({
|
|
|
13379
13674
|
init_argv();
|
|
13380
13675
|
init_operator_context();
|
|
13381
13676
|
init_pm_actions();
|
|
13677
|
+
init_pm_intake();
|
|
13678
|
+
init_pm_start();
|
|
13382
13679
|
init_pm_links();
|
|
13383
13680
|
init_pm_comments();
|
|
13384
13681
|
init_pm_history();
|