@ordewell/core 0.4.9 → 0.4.10
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/ModeResolver-Dx5gzNPC.d.ts +44 -0
- package/dist/ModeResolver-F5wnlSz1.d.mts +44 -0
- package/dist/{chunk-SRGQBGI5.mjs → chunk-IO6PEEN6.mjs} +3 -1
- package/dist/{chunk-SRGQBGI5.mjs.map → chunk-IO6PEEN6.mjs.map} +1 -1
- package/dist/{chunk-YXAIQKVE.mjs → chunk-TUGJXD4G.mjs} +2 -2
- package/dist/chunk-YI4ZDZEE.mjs +862 -0
- package/dist/chunk-YI4ZDZEE.mjs.map +1 -0
- package/dist/index.d.mts +170 -17
- package/dist/index.d.ts +170 -17
- package/dist/index.js +1031 -359
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +482 -160
- package/dist/index.mjs.map +1 -1
- package/dist/{parsing-RjN9JbyG.d.ts → parsing-BqbJySeo.d.mts} +5 -43
- package/dist/{parsing-xpozY5ac.d.mts → parsing-X0Gvuz-q.d.ts} +5 -43
- package/dist/parsing.d.mts +2 -1
- package/dist/parsing.d.ts +2 -1
- package/dist/parsing.js.map +1 -1
- package/dist/parsing.mjs +2 -2
- package/dist/{plan-utils-nfHKSbd3.d.mts → plan-utils-B5rVXNVI.d.mts} +27 -5
- package/dist/{plan-utils-Cgc0cCH1.d.ts → plan-utils-DtsfHnn_.d.ts} +27 -5
- package/dist/plan-utils.d.mts +2 -1
- package/dist/plan-utils.d.ts +2 -1
- package/dist/plan-utils.js +1 -9
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +2 -2
- package/package.json +1 -1
- package/dist/chunk-4KIWPW4K.mjs +0 -510
- package/dist/chunk-4KIWPW4K.mjs.map +0 -1
- /package/dist/{chunk-YXAIQKVE.mjs.map → chunk-TUGJXD4G.mjs.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -96,6 +96,11 @@ __export(index_exports, {
|
|
|
96
96
|
SettingsService: () => SettingsService,
|
|
97
97
|
StdioAgentAdapter: () => StdioAgentAdapter,
|
|
98
98
|
TASK_OPS_ENVELOPE_KEY: () => TASK_OPS_ENVELOPE_KEY,
|
|
99
|
+
TASK_QUERY_ANSWER_OR_OPS: () => TASK_QUERY_ANSWER_OR_OPS,
|
|
100
|
+
TASK_QUERY_ENVELOPE_KEY: () => TASK_QUERY_ENVELOPE_KEY,
|
|
101
|
+
TASK_QUERY_FIELDS: () => TASK_QUERY_FIELDS,
|
|
102
|
+
TASK_QUERY_PROTOCOL: () => TASK_QUERY_PROTOCOL,
|
|
103
|
+
TASK_QUERY_REMINDER: () => TASK_QUERY_REMINDER,
|
|
99
104
|
TRUNCATED_PLAN_REPAIR_INSTRUCTION: () => TRUNCATED_PLAN_REPAIR_INSTRUCTION,
|
|
100
105
|
TaskOrchestrator: () => TaskOrchestrator,
|
|
101
106
|
TmuxRunner: () => TmuxRunner,
|
|
@@ -216,6 +221,7 @@ __export(index_exports, {
|
|
|
216
221
|
parsePartialPlan: () => parsePartialPlan,
|
|
217
222
|
parsePlanJson: () => parsePlanJson,
|
|
218
223
|
parseTaskOpsJson: () => parseTaskOpsJson,
|
|
224
|
+
parseTaskQueryJson: () => parseTaskQueryJson,
|
|
219
225
|
pendingEditRulesBlock: () => pendingEditRulesBlock,
|
|
220
226
|
planDirectLaunch: () => planDirectLaunch,
|
|
221
227
|
planShellLaunch: () => planShellLaunch,
|
|
@@ -224,11 +230,13 @@ __export(index_exports, {
|
|
|
224
230
|
providerForRunner: () => providerForRunner,
|
|
225
231
|
reEmitPlanPrompt: () => reEmitPlanPrompt,
|
|
226
232
|
reEmitTaskOpsPrompt: () => reEmitTaskOpsPrompt,
|
|
233
|
+
reEmitTaskQueryPrompt: () => reEmitTaskQueryPrompt,
|
|
227
234
|
readDaemonToken: () => readDaemonToken,
|
|
228
235
|
referencePattern: () => referencePattern,
|
|
229
236
|
removeTaskFromPlan: () => removeTaskFromPlan,
|
|
230
237
|
renderPlanMap: () => renderPlanMap,
|
|
231
238
|
renderPriorOutputs: () => renderPriorOutputs,
|
|
239
|
+
renderTaskQueryAnswer: () => renderTaskQueryAnswer,
|
|
232
240
|
renumberTasks: () => renumberTasks,
|
|
233
241
|
repairLoop: () => repairLoop,
|
|
234
242
|
researchShellWarning: () => researchShellWarning,
|
|
@@ -262,7 +270,9 @@ __export(index_exports, {
|
|
|
262
270
|
summarizeOutput: () => summarizeOutput,
|
|
263
271
|
summarizeToolCall: () => summarizeToolCall,
|
|
264
272
|
taskOpsRejectedPrompt: () => taskOpsRejectedPrompt,
|
|
273
|
+
taskQuerySignature: () => taskQuerySignature,
|
|
265
274
|
textHasTaskOps: () => textHasTaskOps,
|
|
275
|
+
textHasTaskQuery: () => textHasTaskQuery,
|
|
266
276
|
tmuxSessionName: () => tmuxSessionName,
|
|
267
277
|
tmuxSocketName: () => tmuxSocketName,
|
|
268
278
|
tmuxWindowName: () => tmuxWindowName,
|
|
@@ -4237,7 +4247,7 @@ function formatError(err) {
|
|
|
4237
4247
|
var defaultLogger = new ConsoleLogger();
|
|
4238
4248
|
|
|
4239
4249
|
// src/services/BaseAiService.ts
|
|
4240
|
-
var
|
|
4250
|
+
var import_uuid3 = require("uuid");
|
|
4241
4251
|
|
|
4242
4252
|
// src/services/ModeResolver.ts
|
|
4243
4253
|
function runnerModesFrom(registry, runners) {
|
|
@@ -4360,6 +4370,321 @@ function modesFor(scope, modes) {
|
|
|
4360
4370
|
return scoped;
|
|
4361
4371
|
}
|
|
4362
4372
|
|
|
4373
|
+
// src/services/JsonExtractor.ts
|
|
4374
|
+
var PLAN_ENVELOPE_KEY = "tasks";
|
|
4375
|
+
var TASK_OPS_ENVELOPE_KEY = "taskOps";
|
|
4376
|
+
var TASK_QUERY_ENVELOPE_KEY = "taskQuery";
|
|
4377
|
+
var PlanParseError = class extends Error {
|
|
4378
|
+
raw;
|
|
4379
|
+
/**
|
|
4380
|
+
* True when the reply was cut off mid-object (output-token limit) rather
|
|
4381
|
+
* than malformed — the two need different repairs: a truncated emission will
|
|
4382
|
+
* be cut at the same point again unless the retry frees context or asks for
|
|
4383
|
+
* terser output.
|
|
4384
|
+
*/
|
|
4385
|
+
truncated;
|
|
4386
|
+
/**
|
|
4387
|
+
* True when the JSON parsed fine and a *shape* rule rejected it. The generic
|
|
4388
|
+
* "could not be parsed as JSON" corrective is a lie for these, so the model
|
|
4389
|
+
* re-sends the same object; a semantic rejection must be repaired by naming
|
|
4390
|
+
* the rule it broke.
|
|
4391
|
+
*/
|
|
4392
|
+
semantic;
|
|
4393
|
+
constructor(message, raw, opts) {
|
|
4394
|
+
super(message);
|
|
4395
|
+
this.name = "PlanParseError";
|
|
4396
|
+
this.raw = raw;
|
|
4397
|
+
this.truncated = opts?.truncated ?? false;
|
|
4398
|
+
this.semantic = opts?.semantic ?? false;
|
|
4399
|
+
}
|
|
4400
|
+
};
|
|
4401
|
+
function stripModelNoise(raw) {
|
|
4402
|
+
return raw.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/```json\s*/gi, "").replace(/```\s*/g, "").trim();
|
|
4403
|
+
}
|
|
4404
|
+
function balancedObjectAt(text, start) {
|
|
4405
|
+
let depth = 0;
|
|
4406
|
+
let inString = false;
|
|
4407
|
+
let escaped = false;
|
|
4408
|
+
for (let i = start; i < text.length; i++) {
|
|
4409
|
+
const ch = text[i];
|
|
4410
|
+
if (inString) {
|
|
4411
|
+
if (escaped) escaped = false;
|
|
4412
|
+
else if (ch === "\\") escaped = true;
|
|
4413
|
+
else if (ch === '"') inString = false;
|
|
4414
|
+
continue;
|
|
4415
|
+
}
|
|
4416
|
+
if (ch === '"') inString = true;
|
|
4417
|
+
else if (ch === "{") depth++;
|
|
4418
|
+
else if (ch === "}") {
|
|
4419
|
+
depth--;
|
|
4420
|
+
if (depth === 0) return { json: text.slice(start, i + 1), balanced: true };
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
return { json: text.slice(start), balanced: false };
|
|
4424
|
+
}
|
|
4425
|
+
function extractObjectsWithKey(raw, key) {
|
|
4426
|
+
const stripped = stripModelNoise(raw);
|
|
4427
|
+
const keyRe = new RegExp(`"${key}"\\s*:`);
|
|
4428
|
+
const matches = [];
|
|
4429
|
+
let firstObject = null;
|
|
4430
|
+
let sawBrace = false;
|
|
4431
|
+
let sawUnbalanced = false;
|
|
4432
|
+
let inString = false;
|
|
4433
|
+
let escaped = false;
|
|
4434
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
4435
|
+
const ch = stripped[i];
|
|
4436
|
+
if (inString) {
|
|
4437
|
+
if (escaped) escaped = false;
|
|
4438
|
+
else if (ch === "\\") escaped = true;
|
|
4439
|
+
else if (ch === '"') inString = false;
|
|
4440
|
+
continue;
|
|
4441
|
+
}
|
|
4442
|
+
if (ch === '"') {
|
|
4443
|
+
inString = true;
|
|
4444
|
+
continue;
|
|
4445
|
+
}
|
|
4446
|
+
if (ch !== "{") continue;
|
|
4447
|
+
sawBrace = true;
|
|
4448
|
+
const obj = balancedObjectAt(stripped, i);
|
|
4449
|
+
if (obj.balanced && keyRe.test(obj.json)) {
|
|
4450
|
+
matches.push(obj.json);
|
|
4451
|
+
i += obj.json.length - 1;
|
|
4452
|
+
continue;
|
|
4453
|
+
}
|
|
4454
|
+
if (!obj.balanced) sawUnbalanced = true;
|
|
4455
|
+
if (!firstObject && obj.balanced) firstObject = obj.json;
|
|
4456
|
+
}
|
|
4457
|
+
return {
|
|
4458
|
+
matches,
|
|
4459
|
+
fallback: firstObject !== null ? { json: firstObject, balanced: true } : { json: stripped, balanced: false },
|
|
4460
|
+
sawBrace,
|
|
4461
|
+
sawUnbalanced
|
|
4462
|
+
};
|
|
4463
|
+
}
|
|
4464
|
+
function extractObjectWithBalance(raw) {
|
|
4465
|
+
const { matches, fallback, sawBrace } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);
|
|
4466
|
+
if (matches.length > 0) return { json: matches[0], balanced: true, sawBrace };
|
|
4467
|
+
return { ...fallback, sawBrace };
|
|
4468
|
+
}
|
|
4469
|
+
function extractJsonObject(raw) {
|
|
4470
|
+
return extractObjectWithBalance(raw).json;
|
|
4471
|
+
}
|
|
4472
|
+
function stripTrailingCommas(json) {
|
|
4473
|
+
return json.replace(/,(\s*[}\]])/g, "$1");
|
|
4474
|
+
}
|
|
4475
|
+
function escapeControlCharsInStrings(json) {
|
|
4476
|
+
let out = "";
|
|
4477
|
+
let inString = false;
|
|
4478
|
+
let escaped = false;
|
|
4479
|
+
for (const ch of json) {
|
|
4480
|
+
if (inString) {
|
|
4481
|
+
if (escaped) {
|
|
4482
|
+
out += ch;
|
|
4483
|
+
escaped = false;
|
|
4484
|
+
continue;
|
|
4485
|
+
}
|
|
4486
|
+
if (ch === "\\") {
|
|
4487
|
+
out += ch;
|
|
4488
|
+
escaped = true;
|
|
4489
|
+
continue;
|
|
4490
|
+
}
|
|
4491
|
+
if (ch === '"') {
|
|
4492
|
+
out += ch;
|
|
4493
|
+
inString = false;
|
|
4494
|
+
continue;
|
|
4495
|
+
}
|
|
4496
|
+
if (ch === "\n") {
|
|
4497
|
+
out += "\\n";
|
|
4498
|
+
continue;
|
|
4499
|
+
}
|
|
4500
|
+
if (ch === "\r") {
|
|
4501
|
+
out += "\\r";
|
|
4502
|
+
continue;
|
|
4503
|
+
}
|
|
4504
|
+
if (ch === " ") {
|
|
4505
|
+
out += "\\t";
|
|
4506
|
+
continue;
|
|
4507
|
+
}
|
|
4508
|
+
out += ch;
|
|
4509
|
+
continue;
|
|
4510
|
+
}
|
|
4511
|
+
if (ch === '"') inString = true;
|
|
4512
|
+
out += ch;
|
|
4513
|
+
}
|
|
4514
|
+
return out;
|
|
4515
|
+
}
|
|
4516
|
+
|
|
4517
|
+
// src/services/TaskQuery.ts
|
|
4518
|
+
var TASK_QUERY_FIELDS = [
|
|
4519
|
+
"description",
|
|
4520
|
+
"prompt",
|
|
4521
|
+
"userSteps",
|
|
4522
|
+
"verdict",
|
|
4523
|
+
"outputSummary",
|
|
4524
|
+
"userStoriesCovered"
|
|
4525
|
+
];
|
|
4526
|
+
function textHasTaskQuery(text) {
|
|
4527
|
+
return text.includes(`"${TASK_QUERY_ENVELOPE_KEY}"`);
|
|
4528
|
+
}
|
|
4529
|
+
function parseTaskQueryJson(text) {
|
|
4530
|
+
const { matches, fallback } = extractObjectsWithKey(text, TASK_QUERY_ENVELOPE_KEY);
|
|
4531
|
+
const candidates = matches.length > 0 ? [...matches].reverse() : [fallback.json];
|
|
4532
|
+
let lastError = null;
|
|
4533
|
+
for (const json of candidates) {
|
|
4534
|
+
try {
|
|
4535
|
+
return parseTaskQueryObject(json, text);
|
|
4536
|
+
} catch (err) {
|
|
4537
|
+
if (!(err instanceof PlanParseError)) throw err;
|
|
4538
|
+
if (!lastError) lastError = err;
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
throw lastError ?? new PlanParseError("No JSON object found in task-query reply", text);
|
|
4542
|
+
}
|
|
4543
|
+
function parseTaskQueryObject(json, text) {
|
|
4544
|
+
let parsed;
|
|
4545
|
+
try {
|
|
4546
|
+
parsed = JSON.parse(escapeControlCharsInStrings(stripTrailingCommas(json)));
|
|
4547
|
+
} catch (err) {
|
|
4548
|
+
throw new PlanParseError(`Task-query JSON is invalid: ${err instanceof Error ? err.message : String(err)}`, text);
|
|
4549
|
+
}
|
|
4550
|
+
const raw = parsed.taskQuery;
|
|
4551
|
+
const body = Array.isArray(raw) ? { tasks: raw } : raw;
|
|
4552
|
+
if (typeof body !== "object" || body === null) {
|
|
4553
|
+
throw new PlanParseError('"taskQuery" must be an object (or an array of task references)', text);
|
|
4554
|
+
}
|
|
4555
|
+
const { tasks: rawTasks, fields: rawFields, catalog: rawCatalog } = body;
|
|
4556
|
+
const tasks = [];
|
|
4557
|
+
if (rawTasks !== void 0) {
|
|
4558
|
+
if (!Array.isArray(rawTasks)) throw new PlanParseError('"tasks" in a taskQuery must be an array of task references', text);
|
|
4559
|
+
for (const ref of rawTasks) {
|
|
4560
|
+
if (typeof ref !== "string" && typeof ref !== "number") {
|
|
4561
|
+
throw new PlanParseError(`Invalid task reference "${String(ref)}" in taskQuery`, text);
|
|
4562
|
+
}
|
|
4563
|
+
tasks.push(String(ref).trim());
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
let fields;
|
|
4567
|
+
if (rawFields !== void 0) {
|
|
4568
|
+
if (!Array.isArray(rawFields)) throw new PlanParseError('"fields" in a taskQuery must be an array of field names', text);
|
|
4569
|
+
fields = [];
|
|
4570
|
+
for (const f of rawFields) {
|
|
4571
|
+
if (typeof f !== "string" || !TASK_QUERY_FIELDS.includes(f)) {
|
|
4572
|
+
throw new PlanParseError(`Unknown taskQuery field "${String(f)}" \u2014 choose from ${TASK_QUERY_FIELDS.join(", ")}`, text);
|
|
4573
|
+
}
|
|
4574
|
+
fields.push(f);
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
const catalog = rawCatalog === true;
|
|
4578
|
+
if (tasks.length === 0 && !catalog) {
|
|
4579
|
+
throw new PlanParseError('A taskQuery must name at least one task or set "catalog": true', text);
|
|
4580
|
+
}
|
|
4581
|
+
return { tasks, fields, catalog };
|
|
4582
|
+
}
|
|
4583
|
+
function taskQuerySignature(query) {
|
|
4584
|
+
return JSON.stringify([query.tasks, query.fields ?? null, query.catalog]);
|
|
4585
|
+
}
|
|
4586
|
+
var TASK_QUERY_ANSWER_OR_OPS = "You have now read everything you asked for. Do not send another taskQuery this turn: answer the user in prose, or emit the taskOps JSON for the change you came to make.";
|
|
4587
|
+
var TASK_QUERY_PROTOCOL = [
|
|
4588
|
+
"READING A TASK BEFORE YOU EDIT IT:",
|
|
4589
|
+
"The plan block you are shown each turn carries only short fields \u2014 it never contains a task's prompt, its user steps, or a completed task's verdict. Never rewrite a field you have not read. To read one, reply with ONLY this JSON object:",
|
|
4590
|
+
` {"${TASK_QUERY_ENVELOPE_KEY}":{"tasks":["<id or #order>", "..."],"fields":[${TASK_QUERY_FIELDS.map((f) => `"${f}"`).join(", ")}],"catalog":true}}`,
|
|
4591
|
+
'- "tasks": one or more task references. Ask for everything you need in ONE query \u2014 each query costs a round-trip.',
|
|
4592
|
+
'- "fields": optional. Omit it to get every field above.',
|
|
4593
|
+
'- "catalog": optional. Set it to true for the full runner and model catalog \u2014 every model id with its label and thinking-effort variants, and every task mode with its description. It works before any task exists, and may be used on its own.',
|
|
4594
|
+
"Ordewell answers immediately with the detail and changes nothing. Then reply again with your taskOps JSON, or with prose for the user.",
|
|
4595
|
+
"Three queries per user message; after that every answer also tells you to land the turn. Do not ask the same question twice."
|
|
4596
|
+
];
|
|
4597
|
+
var TASK_QUERY_REMINDER = `- To READ a task in full (prompt, user steps, verdict, output, user stories) or the whole model/mode catalog before editing, reply with ONLY {"${TASK_QUERY_ENVELOPE_KEY}":{"tasks":["<id or #order>"],"catalog":true}} \u2014 it changes nothing, and you then reply again with your ops.`;
|
|
4598
|
+
function findTask(tasks, ref) {
|
|
4599
|
+
const byId = tasks.find((t) => t.id === ref);
|
|
4600
|
+
if (byId) return byId;
|
|
4601
|
+
const orderStr = ref.startsWith("#") ? ref.slice(1) : ref;
|
|
4602
|
+
if (/^\d+$/.test(orderStr)) {
|
|
4603
|
+
const byOrder = tasks.filter((t) => t.order === Number(orderStr));
|
|
4604
|
+
if (byOrder.length === 1) return byOrder[0];
|
|
4605
|
+
}
|
|
4606
|
+
return tasks.find((t) => t.title === ref);
|
|
4607
|
+
}
|
|
4608
|
+
function renderField(task, field) {
|
|
4609
|
+
switch (field) {
|
|
4610
|
+
case "description":
|
|
4611
|
+
return [`description: ${task.description || "(none)"}`];
|
|
4612
|
+
case "prompt":
|
|
4613
|
+
return task.prompt ? ["prompt:", task.prompt] : ["prompt: (none)"];
|
|
4614
|
+
case "userSteps": {
|
|
4615
|
+
const steps = task.userSteps ?? [];
|
|
4616
|
+
if (steps.length === 0) return ["userSteps: (none)"];
|
|
4617
|
+
return ["userSteps:", ...steps.map((s) => ` ${s.order}. [${s.completed ? "x" : " "}] ${s.instruction}`)];
|
|
4618
|
+
}
|
|
4619
|
+
case "verdict": {
|
|
4620
|
+
const v = task.verdict;
|
|
4621
|
+
if (!v) return ["verdict: (none)"];
|
|
4622
|
+
const checks = v.checks.map((c) => `${c.name}=${c.skipped ? "skipped" : c.passed ? "pass" : "fail"}`).join(", ");
|
|
4623
|
+
return [`verdict: ${v.outcome.toUpperCase()} \u2014 ${v.reason}${checks ? ` [${checks}]` : ""}`];
|
|
4624
|
+
}
|
|
4625
|
+
case "outputSummary": {
|
|
4626
|
+
const o = task.outputSummary;
|
|
4627
|
+
if (!o) return ["outputSummary: (none)"];
|
|
4628
|
+
return [`outputSummary: ${o.reviewReason}`, ...o.logTail ? ["log tail:", o.logTail] : []];
|
|
4629
|
+
}
|
|
4630
|
+
case "userStoriesCovered": {
|
|
4631
|
+
const stories = task.userStoriesCovered ?? [];
|
|
4632
|
+
if (stories.length === 0) return ["userStoriesCovered: (none)"];
|
|
4633
|
+
return ["userStoriesCovered:", ...stories.map((s) => ` - ${s}`)];
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
function renderTask(task, fields) {
|
|
4638
|
+
return [
|
|
4639
|
+
`#${task.order} id=${task.id} "${task.title}" [${task.status}] type:${task.type === "user" ? "MAN" : "AI"}`,
|
|
4640
|
+
...fields.flatMap((f) => renderField(task, f)),
|
|
4641
|
+
""
|
|
4642
|
+
];
|
|
4643
|
+
}
|
|
4644
|
+
function renderCatalog(catalog) {
|
|
4645
|
+
const lines = [];
|
|
4646
|
+
for (const runner of catalog.runners) {
|
|
4647
|
+
const models = catalog.models[runner] ?? [];
|
|
4648
|
+
lines.push(`${runner} models:`);
|
|
4649
|
+
if (models.length === 0) lines.push(" (none discovered)");
|
|
4650
|
+
for (const m of models) {
|
|
4651
|
+
const variants = m.variants?.length ? ` variants: ${m.variants.map((v) => `${v.id}${v.label && v.label !== v.id ? ` (${v.label})` : ""}`).join(", ")}` : " variants: none";
|
|
4652
|
+
lines.push(` - ${m.modelId} \u2014 ${m.modelLabel}${variants}`);
|
|
4653
|
+
}
|
|
4654
|
+
const modes = catalog.modes[runner] ?? [];
|
|
4655
|
+
lines.push(`${runner} task modes:`);
|
|
4656
|
+
if (modes.length === 0) lines.push(" (none declared)");
|
|
4657
|
+
const defaultId = resolveDefaultMode(modes, catalog.autonomousDefault);
|
|
4658
|
+
for (const mode of modes) {
|
|
4659
|
+
lines.push(` - ${mode.id} \u2014 ${mode.label}: ${mode.description}${mode.id === defaultId ? " (default)" : ""}`);
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
return lines;
|
|
4663
|
+
}
|
|
4664
|
+
function renderTaskQueryAnswer(query, tasks, catalog) {
|
|
4665
|
+
const fields = query.fields ?? TASK_QUERY_FIELDS;
|
|
4666
|
+
const blocks = [];
|
|
4667
|
+
if (query.tasks.length > 0) {
|
|
4668
|
+
const body = [];
|
|
4669
|
+
for (const ref of query.tasks) {
|
|
4670
|
+
const task = findTask(tasks, ref);
|
|
4671
|
+
if (!task) {
|
|
4672
|
+
body.push(`${ref}: no task matches this reference in the current plan.`, "");
|
|
4673
|
+
continue;
|
|
4674
|
+
}
|
|
4675
|
+
body.push(...renderTask(task, fields));
|
|
4676
|
+
}
|
|
4677
|
+
blocks.push(["<task_detail>", ...body, "</task_detail>"].join("\n"));
|
|
4678
|
+
}
|
|
4679
|
+
if (query.catalog) {
|
|
4680
|
+
blocks.push(["<runner_catalog>", ...renderCatalog(catalog), "</runner_catalog>"].join("\n"));
|
|
4681
|
+
}
|
|
4682
|
+
blocks.push(
|
|
4683
|
+
"That is everything you asked for. Nothing has changed. Reply now with the taskOps JSON for the edit you intended, or answer the user in prose."
|
|
4684
|
+
);
|
|
4685
|
+
return blocks.join("\n\n");
|
|
4686
|
+
}
|
|
4687
|
+
|
|
4363
4688
|
// src/services/PlanPrompts.ts
|
|
4364
4689
|
function buildResearchToolsPrompt(subagentsEnabled = false) {
|
|
4365
4690
|
const lines = [
|
|
@@ -4449,7 +4774,8 @@ function researchPhaseBlock(harnessMode) {
|
|
|
4449
4774
|
];
|
|
4450
4775
|
const toolSpecific = harnessMode ? [
|
|
4451
4776
|
"- Explore the repository with your own tools until you understand its architecture",
|
|
4452
|
-
"- You are planning, not implementing: do NOT edit, create, or delete any file, and do not run commands that change the workspace"
|
|
4777
|
+
"- You are planning, not implementing: do NOT edit, create, or delete any file, and do not run commands that change the workspace",
|
|
4778
|
+
"- If you delegate exploration to your own agents, WAIT for their results inside this reply. Do NOT launch them in the background or async and end your turn saying you will report back later: your turn ending is what hands the conversation back to the user, and anything you say after it never reaches them."
|
|
4453
4779
|
] : [
|
|
4454
4780
|
"- Use list_dir with depth=2-3 for a tree overview",
|
|
4455
4781
|
"- Use glob and grep to find relevant source files",
|
|
@@ -4614,6 +4940,11 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
4614
4940
|
"TASK MODE:",
|
|
4615
4941
|
modeGuide,
|
|
4616
4942
|
"",
|
|
4943
|
+
// Shared by both variants on purpose: the read channel is a text envelope
|
|
4944
|
+
// exactly so a harness planner, which Ordewell cannot hand tools to, speaks
|
|
4945
|
+
// the same protocol as an API-backed one (ADR-0009).
|
|
4946
|
+
...TASK_QUERY_PROTOCOL,
|
|
4947
|
+
"",
|
|
4617
4948
|
context ? `PROJECT CONTEXT:
|
|
4618
4949
|
${context}
|
|
4619
4950
|
` : "",
|
|
@@ -4921,7 +5252,8 @@ function buildMergePrompt(taskIds, tasks) {
|
|
|
4921
5252
|
"The merged task takes the union of their dependencies (excluding the merged tasks themselves) and preserves their user stories.",
|
|
4922
5253
|
'Set "assignedRunner" and "assignedModel" on the merged task \u2014 use one of the runners and models listed in <available_models> above. Prefer the strongest model if the merged work is complex.',
|
|
4923
5254
|
'Reply with ONLY a taskOps JSON object using a single "merge" op:',
|
|
4924
|
-
` {"taskOps":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}
|
|
5255
|
+
` {"taskOps":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}`,
|
|
5256
|
+
`If this merge needs a companion op in the same batch (e.g. an added task that should depend on the merge result), give the merge op a "handle" (any unused name) and reference it from the later op's taskId/dependencies.`
|
|
4925
5257
|
].join("\n");
|
|
4926
5258
|
}
|
|
4927
5259
|
function buildSplitPrompt(taskId, tasks) {
|
|
@@ -4933,153 +5265,11 @@ function buildSplitPrompt(taskId, tasks) {
|
|
|
4933
5265
|
"The first part inherits the original task's dependencies. Each later part depends on the previous part. Tasks that depended on the original now depend on the LAST part.",
|
|
4934
5266
|
'Set "assignedRunner" and "assignedModel" on each part \u2014 use the runners and models listed in <available_models> above. You may assign different models to different parts (e.g. a stronger model for a complex part, a faster one for a simple part).',
|
|
4935
5267
|
'Reply with ONLY a taskOps JSON object using a single "split" op:',
|
|
4936
|
-
` {"taskOps":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}
|
|
5268
|
+
` {"taskOps":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}`,
|
|
5269
|
+
`If this split needs a companion op in the same batch (e.g. another task that should depend on the last part), give the split op a "handle" (any unused name) \u2014 it names the last part \u2014 and reference it from the later op's taskId/dependencies.`
|
|
4937
5270
|
].join("\n");
|
|
4938
5271
|
}
|
|
4939
5272
|
|
|
4940
|
-
// src/services/JsonExtractor.ts
|
|
4941
|
-
var PLAN_ENVELOPE_KEY = "tasks";
|
|
4942
|
-
var TASK_OPS_ENVELOPE_KEY = "taskOps";
|
|
4943
|
-
var PlanParseError = class extends Error {
|
|
4944
|
-
raw;
|
|
4945
|
-
/**
|
|
4946
|
-
* True when the reply was cut off mid-object (output-token limit) rather
|
|
4947
|
-
* than malformed — the two need different repairs: a truncated emission will
|
|
4948
|
-
* be cut at the same point again unless the retry frees context or asks for
|
|
4949
|
-
* terser output.
|
|
4950
|
-
*/
|
|
4951
|
-
truncated;
|
|
4952
|
-
/**
|
|
4953
|
-
* True when the JSON parsed fine and a *shape* rule rejected it. The generic
|
|
4954
|
-
* "could not be parsed as JSON" corrective is a lie for these, so the model
|
|
4955
|
-
* re-sends the same object; a semantic rejection must be repaired by naming
|
|
4956
|
-
* the rule it broke.
|
|
4957
|
-
*/
|
|
4958
|
-
semantic;
|
|
4959
|
-
constructor(message, raw, opts) {
|
|
4960
|
-
super(message);
|
|
4961
|
-
this.name = "PlanParseError";
|
|
4962
|
-
this.raw = raw;
|
|
4963
|
-
this.truncated = opts?.truncated ?? false;
|
|
4964
|
-
this.semantic = opts?.semantic ?? false;
|
|
4965
|
-
}
|
|
4966
|
-
};
|
|
4967
|
-
function stripModelNoise(raw) {
|
|
4968
|
-
return raw.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/```json\s*/gi, "").replace(/```\s*/g, "").trim();
|
|
4969
|
-
}
|
|
4970
|
-
function balancedObjectAt(text, start) {
|
|
4971
|
-
let depth = 0;
|
|
4972
|
-
let inString = false;
|
|
4973
|
-
let escaped = false;
|
|
4974
|
-
for (let i = start; i < text.length; i++) {
|
|
4975
|
-
const ch = text[i];
|
|
4976
|
-
if (inString) {
|
|
4977
|
-
if (escaped) escaped = false;
|
|
4978
|
-
else if (ch === "\\") escaped = true;
|
|
4979
|
-
else if (ch === '"') inString = false;
|
|
4980
|
-
continue;
|
|
4981
|
-
}
|
|
4982
|
-
if (ch === '"') inString = true;
|
|
4983
|
-
else if (ch === "{") depth++;
|
|
4984
|
-
else if (ch === "}") {
|
|
4985
|
-
depth--;
|
|
4986
|
-
if (depth === 0) return { json: text.slice(start, i + 1), balanced: true };
|
|
4987
|
-
}
|
|
4988
|
-
}
|
|
4989
|
-
return { json: text.slice(start), balanced: false };
|
|
4990
|
-
}
|
|
4991
|
-
function extractObjectsWithKey(raw, key) {
|
|
4992
|
-
const stripped = stripModelNoise(raw);
|
|
4993
|
-
const keyRe = new RegExp(`"${key}"\\s*:`);
|
|
4994
|
-
const matches = [];
|
|
4995
|
-
let firstObject = null;
|
|
4996
|
-
let sawBrace = false;
|
|
4997
|
-
let sawUnbalanced = false;
|
|
4998
|
-
let inString = false;
|
|
4999
|
-
let escaped = false;
|
|
5000
|
-
for (let i = 0; i < stripped.length; i++) {
|
|
5001
|
-
const ch = stripped[i];
|
|
5002
|
-
if (inString) {
|
|
5003
|
-
if (escaped) escaped = false;
|
|
5004
|
-
else if (ch === "\\") escaped = true;
|
|
5005
|
-
else if (ch === '"') inString = false;
|
|
5006
|
-
continue;
|
|
5007
|
-
}
|
|
5008
|
-
if (ch === '"') {
|
|
5009
|
-
inString = true;
|
|
5010
|
-
continue;
|
|
5011
|
-
}
|
|
5012
|
-
if (ch !== "{") continue;
|
|
5013
|
-
sawBrace = true;
|
|
5014
|
-
const obj = balancedObjectAt(stripped, i);
|
|
5015
|
-
if (obj.balanced && keyRe.test(obj.json)) {
|
|
5016
|
-
matches.push(obj.json);
|
|
5017
|
-
i += obj.json.length - 1;
|
|
5018
|
-
continue;
|
|
5019
|
-
}
|
|
5020
|
-
if (!obj.balanced) sawUnbalanced = true;
|
|
5021
|
-
if (!firstObject && obj.balanced) firstObject = obj.json;
|
|
5022
|
-
}
|
|
5023
|
-
return {
|
|
5024
|
-
matches,
|
|
5025
|
-
fallback: firstObject !== null ? { json: firstObject, balanced: true } : { json: stripped, balanced: false },
|
|
5026
|
-
sawBrace,
|
|
5027
|
-
sawUnbalanced
|
|
5028
|
-
};
|
|
5029
|
-
}
|
|
5030
|
-
function extractObjectWithBalance(raw) {
|
|
5031
|
-
const { matches, fallback, sawBrace } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);
|
|
5032
|
-
if (matches.length > 0) return { json: matches[0], balanced: true, sawBrace };
|
|
5033
|
-
return { ...fallback, sawBrace };
|
|
5034
|
-
}
|
|
5035
|
-
function extractJsonObject(raw) {
|
|
5036
|
-
return extractObjectWithBalance(raw).json;
|
|
5037
|
-
}
|
|
5038
|
-
function stripTrailingCommas(json) {
|
|
5039
|
-
return json.replace(/,(\s*[}\]])/g, "$1");
|
|
5040
|
-
}
|
|
5041
|
-
function escapeControlCharsInStrings(json) {
|
|
5042
|
-
let out = "";
|
|
5043
|
-
let inString = false;
|
|
5044
|
-
let escaped = false;
|
|
5045
|
-
for (const ch of json) {
|
|
5046
|
-
if (inString) {
|
|
5047
|
-
if (escaped) {
|
|
5048
|
-
out += ch;
|
|
5049
|
-
escaped = false;
|
|
5050
|
-
continue;
|
|
5051
|
-
}
|
|
5052
|
-
if (ch === "\\") {
|
|
5053
|
-
out += ch;
|
|
5054
|
-
escaped = true;
|
|
5055
|
-
continue;
|
|
5056
|
-
}
|
|
5057
|
-
if (ch === '"') {
|
|
5058
|
-
out += ch;
|
|
5059
|
-
inString = false;
|
|
5060
|
-
continue;
|
|
5061
|
-
}
|
|
5062
|
-
if (ch === "\n") {
|
|
5063
|
-
out += "\\n";
|
|
5064
|
-
continue;
|
|
5065
|
-
}
|
|
5066
|
-
if (ch === "\r") {
|
|
5067
|
-
out += "\\r";
|
|
5068
|
-
continue;
|
|
5069
|
-
}
|
|
5070
|
-
if (ch === " ") {
|
|
5071
|
-
out += "\\t";
|
|
5072
|
-
continue;
|
|
5073
|
-
}
|
|
5074
|
-
out += ch;
|
|
5075
|
-
continue;
|
|
5076
|
-
}
|
|
5077
|
-
if (ch === '"') inString = true;
|
|
5078
|
-
out += ch;
|
|
5079
|
-
}
|
|
5080
|
-
return out;
|
|
5081
|
-
}
|
|
5082
|
-
|
|
5083
5273
|
// src/services/PlanValidator.ts
|
|
5084
5274
|
function parsePlanJson(raw, runners, runnerModes, autonomousDefault = true) {
|
|
5085
5275
|
const { matches, fallback, sawUnbalanced } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);
|
|
@@ -5320,6 +5510,171 @@ function validatePlanModification(ctx) {
|
|
|
5320
5510
|
return { valid: allErrors.length === 0, errors: allErrors };
|
|
5321
5511
|
}
|
|
5322
5512
|
|
|
5513
|
+
// src/services/TaskOps.ts
|
|
5514
|
+
var import_uuid2 = require("uuid");
|
|
5515
|
+
|
|
5516
|
+
// src/services/ModelAllowlistResolver.ts
|
|
5517
|
+
function effectiveAllowlist(allowlist, runner, modelsByRunner) {
|
|
5518
|
+
if (!allowlist || allowlist.length === 0) return void 0;
|
|
5519
|
+
if (!modelsByRunner) return allowlist;
|
|
5520
|
+
const own = new Set((modelsByRunner[runner] ?? []).map((m) => m.modelId));
|
|
5521
|
+
const elsewhere = /* @__PURE__ */ new Set();
|
|
5522
|
+
for (const [other, models] of Object.entries(modelsByRunner)) {
|
|
5523
|
+
if (other === runner) continue;
|
|
5524
|
+
for (const m of models ?? []) elsewhere.add(m.modelId);
|
|
5525
|
+
}
|
|
5526
|
+
const kept = allowlist.filter((id) => own.has(id) || !elsewhere.has(id));
|
|
5527
|
+
return kept.length > 0 ? kept : void 0;
|
|
5528
|
+
}
|
|
5529
|
+
function filterModelsForPrompt(modelsByRunner, perRunnerAllowlist) {
|
|
5530
|
+
const result = {};
|
|
5531
|
+
for (const [runner, models] of Object.entries(modelsByRunner)) {
|
|
5532
|
+
if (!models) continue;
|
|
5533
|
+
const allowed = effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
5534
|
+
if (!allowed) {
|
|
5535
|
+
result[runner] = models;
|
|
5536
|
+
continue;
|
|
5537
|
+
}
|
|
5538
|
+
const allowedSet = new Set(allowed);
|
|
5539
|
+
const intersection = models.filter((m) => allowedSet.has(m.modelId));
|
|
5540
|
+
const covered = new Set(intersection.map((m) => m.modelId));
|
|
5541
|
+
const missing = allowed.filter((id) => !covered.has(id)).map((id) => ({ modelId: id, modelLabel: id.split("/").pop() || id, variants: [] }));
|
|
5542
|
+
result[runner] = [...intersection, ...missing];
|
|
5543
|
+
}
|
|
5544
|
+
return result;
|
|
5545
|
+
}
|
|
5546
|
+
var EFFORT_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
5547
|
+
var EFFORT_ALIASES = { disabled: "none", adaptive: "medium" };
|
|
5548
|
+
function clampThinkingEffort(effort, variants) {
|
|
5549
|
+
if (!effort) return void 0;
|
|
5550
|
+
if (variants.length === 0) return void 0;
|
|
5551
|
+
const ids = new Set(variants.map((v) => v.id));
|
|
5552
|
+
if (ids.has(effort)) return effort;
|
|
5553
|
+
const canon = EFFORT_ALIASES[effort] ?? effort;
|
|
5554
|
+
if (ids.has(canon)) return canon;
|
|
5555
|
+
const idx = EFFORT_LADDER.indexOf(canon);
|
|
5556
|
+
if (idx < 0) return void 0;
|
|
5557
|
+
for (let d = 1; d < EFFORT_LADDER.length; d++) {
|
|
5558
|
+
const lower = EFFORT_LADDER[idx - d];
|
|
5559
|
+
if (lower && ids.has(lower)) return lower;
|
|
5560
|
+
const higher = EFFORT_LADDER[idx + d];
|
|
5561
|
+
if (higher && ids.has(higher)) return higher;
|
|
5562
|
+
}
|
|
5563
|
+
return void 0;
|
|
5564
|
+
}
|
|
5565
|
+
function coerceAssignments(tasks, perRunnerAllowlist, allowedRunners, modelsByRunner) {
|
|
5566
|
+
const clampVariant = (task) => {
|
|
5567
|
+
if (!task.assignedModel || !modelsByRunner) return task;
|
|
5568
|
+
const model = modelsByRunner[task.assignedRunner]?.find(
|
|
5569
|
+
(m) => m.modelId === task.assignedModel.modelId
|
|
5570
|
+
);
|
|
5571
|
+
if (!model) return task;
|
|
5572
|
+
const clamped = clampThinkingEffort(task.assignedModel.thinkingEffort, model.variants);
|
|
5573
|
+
return {
|
|
5574
|
+
...task,
|
|
5575
|
+
assignedModel: {
|
|
5576
|
+
...task.assignedModel,
|
|
5577
|
+
thinkingEffort: clamped,
|
|
5578
|
+
availableVariants: model.variants.map((v) => v.id)
|
|
5579
|
+
}
|
|
5580
|
+
};
|
|
5581
|
+
};
|
|
5582
|
+
const allowedFor = (runner) => effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
5583
|
+
const assignmentFor = (runner, modelId) => {
|
|
5584
|
+
const known = modelsByRunner?.[runner]?.find((m) => m.modelId === modelId);
|
|
5585
|
+
return {
|
|
5586
|
+
modelId,
|
|
5587
|
+
modelLabel: known?.modelLabel ?? modelId,
|
|
5588
|
+
thinkingEffort: void 0,
|
|
5589
|
+
...known ? { availableVariants: known.variants.map((v) => v.id) } : {}
|
|
5590
|
+
};
|
|
5591
|
+
};
|
|
5592
|
+
return tasks.map((task) => {
|
|
5593
|
+
if (task.type === "user") return task;
|
|
5594
|
+
if (allowedRunners && !allowedRunners.includes(task.assignedRunner)) {
|
|
5595
|
+
const fallbackRunner = allowedRunners[0];
|
|
5596
|
+
const fallbackModels = allowedFor(fallbackRunner);
|
|
5597
|
+
const fallbackModel = fallbackModels?.length ? assignmentFor(fallbackRunner, fallbackModels[0]) : modelsByRunner?.[fallbackRunner]?.[0] ? assignmentFor(fallbackRunner, modelsByRunner[fallbackRunner][0].modelId) : task.assignedModel;
|
|
5598
|
+
return clampVariant({
|
|
5599
|
+
...task,
|
|
5600
|
+
assignedRunner: fallbackRunner,
|
|
5601
|
+
assignedModel: fallbackModel
|
|
5602
|
+
});
|
|
5603
|
+
}
|
|
5604
|
+
const allowlist = allowedFor(task.assignedRunner);
|
|
5605
|
+
if (!task.assignedModel) {
|
|
5606
|
+
if (allowlist && allowlist.length > 0) {
|
|
5607
|
+
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) });
|
|
5608
|
+
}
|
|
5609
|
+
const discovered = modelsByRunner?.[task.assignedRunner]?.[0];
|
|
5610
|
+
if (discovered) {
|
|
5611
|
+
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, discovered.modelId) });
|
|
5612
|
+
}
|
|
5613
|
+
return task;
|
|
5614
|
+
}
|
|
5615
|
+
if (!allowlist || allowlist.length === 0) return clampVariant(task);
|
|
5616
|
+
if (allowlist.includes(task.assignedModel.modelId)) return clampVariant(task);
|
|
5617
|
+
return { ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) };
|
|
5618
|
+
});
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5621
|
+
// src/services/TaskEditValidator.ts
|
|
5622
|
+
var AI_ONLY_FIELDS = ["assignedModel", "thinkingEffort", "taskMode", "autonomy"];
|
|
5623
|
+
function checkModelAndModeValidity(runner, assignedModel, taskMode, catalog) {
|
|
5624
|
+
if (!catalog) return { ok: true };
|
|
5625
|
+
if (assignedModel) {
|
|
5626
|
+
const models = catalog.modelsByRunner[runner];
|
|
5627
|
+
if (models) {
|
|
5628
|
+
const modelId = assignedModel.modelId;
|
|
5629
|
+
if (!models.some((m) => m.modelId === modelId)) {
|
|
5630
|
+
return { ok: false, error: `Runner "${runner}" does not offer model "${modelId}"` };
|
|
5631
|
+
}
|
|
5632
|
+
const allowed = effectiveAllowlist(catalog.perRunnerAllowlist?.[runner], runner, catalog.modelsByRunner);
|
|
5633
|
+
if (allowed && !allowed.includes(modelId)) {
|
|
5634
|
+
return { ok: false, error: `Model "${modelId}" is excluded by the allowlist for runner "${runner}"` };
|
|
5635
|
+
}
|
|
5636
|
+
}
|
|
5637
|
+
}
|
|
5638
|
+
if (taskMode !== void 0) {
|
|
5639
|
+
const modes = catalog.runnerModes[runner];
|
|
5640
|
+
if (modes && modes.length > 0 && !modes.some((m) => m.id === taskMode)) {
|
|
5641
|
+
return { ok: false, error: `Runner "${runner}" has no task mode "${taskMode}" \u2014 valid modes: ${modes.map((m) => m.id).join(", ")}` };
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
return { ok: true };
|
|
5645
|
+
}
|
|
5646
|
+
function typeCoherenceCheck(target, changes) {
|
|
5647
|
+
if (!changes.type || changes.type === target.type) return null;
|
|
5648
|
+
if (changes.type === "user") {
|
|
5649
|
+
if (!Array.isArray(changes.userSteps) || changes.userSteps.length === 0) {
|
|
5650
|
+
return { ok: false, error: `"${target.title}" cannot become a manual task without user steps \u2014 include "userSteps" in this edit` };
|
|
5651
|
+
}
|
|
5652
|
+
return { ok: true, clear: AI_ONLY_FIELDS.filter((f) => target[f] !== void 0) };
|
|
5653
|
+
}
|
|
5654
|
+
if (!changes.prompt || !changes.prompt.trim()) {
|
|
5655
|
+
return { ok: false, error: `"${target.title}" cannot become an AI task without a prompt \u2014 include "prompt" in this edit` };
|
|
5656
|
+
}
|
|
5657
|
+
return { ok: true, clear: target.userSteps && target.userSteps.length > 0 ? ["userSteps"] : [] };
|
|
5658
|
+
}
|
|
5659
|
+
function validateTaskEdit(actor, tasks, taskId, changes, catalog) {
|
|
5660
|
+
const target = tasks.find((t) => t.id === taskId);
|
|
5661
|
+
if (!target) return { ok: false, error: "Task not found" };
|
|
5662
|
+
if (actor === "planner") {
|
|
5663
|
+
if (target.status === "in_progress") return { ok: false, error: `"${target.title}" is running and cannot be modified` };
|
|
5664
|
+
if (target.status === "completed") return { ok: false, error: `"${target.title}" is completed and cannot be modified` };
|
|
5665
|
+
}
|
|
5666
|
+
if (changes.dependencies) {
|
|
5667
|
+
const depCheck = canSetDependencies(tasks, taskId, changes.dependencies);
|
|
5668
|
+
if (!depCheck.ok) return depCheck;
|
|
5669
|
+
}
|
|
5670
|
+
const coherence = typeCoherenceCheck(target, changes);
|
|
5671
|
+
if (coherence && !coherence.ok) return coherence;
|
|
5672
|
+
const runner = changes.assignedRunner ?? target.assignedRunner;
|
|
5673
|
+
const validity = checkModelAndModeValidity(runner, changes.assignedModel, changes.taskMode, catalog);
|
|
5674
|
+
if (!validity.ok) return validity;
|
|
5675
|
+
return coherence ?? { ok: true };
|
|
5676
|
+
}
|
|
5677
|
+
|
|
5323
5678
|
// src/services/TaskOps.ts
|
|
5324
5679
|
var UPDATABLE_FIELDS = [
|
|
5325
5680
|
"title",
|
|
@@ -5332,7 +5687,8 @@ var UPDATABLE_FIELDS = [
|
|
|
5332
5687
|
"thinkingEffort",
|
|
5333
5688
|
"type",
|
|
5334
5689
|
"userSteps",
|
|
5335
|
-
"autonomy"
|
|
5690
|
+
"autonomy",
|
|
5691
|
+
"sliceType"
|
|
5336
5692
|
];
|
|
5337
5693
|
function textHasTaskOps(text) {
|
|
5338
5694
|
return text.includes(`"${TASK_OPS_ENVELOPE_KEY}"`);
|
|
@@ -5369,18 +5725,41 @@ function parseTaskOpsObject(json, text) {
|
|
|
5369
5725
|
}
|
|
5370
5726
|
return ops;
|
|
5371
5727
|
}
|
|
5372
|
-
function resolveRef(ref,
|
|
5373
|
-
if (typeof ref !== "string" && typeof ref !== "number") return
|
|
5728
|
+
function resolveRef(ref, originalScope, liveScope, handleOwner, handleId, opIndex) {
|
|
5729
|
+
if (typeof ref !== "string" && typeof ref !== "number") return { error: `invalid reference "${String(ref)}"` };
|
|
5374
5730
|
const s = String(ref).trim();
|
|
5375
|
-
const
|
|
5376
|
-
if (
|
|
5731
|
+
const ownerIdx = handleOwner.get(s);
|
|
5732
|
+
if (ownerIdx !== void 0) {
|
|
5733
|
+
if (ownerIdx >= opIndex) {
|
|
5734
|
+
return { error: `handle "${s}" is defined later in this batch (op ${ownerIdx + 1}) and cannot be referenced yet` };
|
|
5735
|
+
}
|
|
5736
|
+
const id = handleId.get(s);
|
|
5737
|
+
if (!id) return { error: `handle "${s}" was never resolved` };
|
|
5738
|
+
return { id };
|
|
5739
|
+
}
|
|
5740
|
+
const byLiveId = liveScope.find((t) => t.id === s);
|
|
5741
|
+
if (byLiveId) return { id: byLiveId.id };
|
|
5377
5742
|
const orderStr = s.startsWith("#") ? s.slice(1) : s;
|
|
5378
5743
|
if (/^\d+$/.test(orderStr)) {
|
|
5379
5744
|
const order = Number(orderStr);
|
|
5380
|
-
const byOrder =
|
|
5381
|
-
if (byOrder.length === 1) return byOrder[0];
|
|
5745
|
+
const byOrder = originalScope.filter((t) => t.order === order);
|
|
5746
|
+
if (byOrder.length === 1) return { id: byOrder[0].id };
|
|
5382
5747
|
}
|
|
5383
|
-
|
|
5748
|
+
const byTitle = originalScope.find((t) => t.title === s);
|
|
5749
|
+
if (byTitle) return { id: byTitle.id };
|
|
5750
|
+
return { error: `task "${s}" not found` };
|
|
5751
|
+
}
|
|
5752
|
+
function refCollidesWithExisting(name, originalFlatAll) {
|
|
5753
|
+
if (originalFlatAll.some((t) => t.id === name)) return true;
|
|
5754
|
+
const orderStr = name.startsWith("#") ? name.slice(1) : name;
|
|
5755
|
+
if (/^\d+$/.test(orderStr) && originalFlatAll.some((t) => t.order === Number(orderStr))) return true;
|
|
5756
|
+
return originalFlatAll.some((t) => t.title === name);
|
|
5757
|
+
}
|
|
5758
|
+
function findInTasks(tasks, id) {
|
|
5759
|
+
return flattenTasks(tasks).find((t) => t.id === id);
|
|
5760
|
+
}
|
|
5761
|
+
function findTopLevel(tasks, id) {
|
|
5762
|
+
return tasks.find((t) => t.id === id);
|
|
5384
5763
|
}
|
|
5385
5764
|
function detectCycle(tasks) {
|
|
5386
5765
|
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
@@ -5404,6 +5783,55 @@ function detectCycle(tasks) {
|
|
|
5404
5783
|
}
|
|
5405
5784
|
return null;
|
|
5406
5785
|
}
|
|
5786
|
+
function isPinned(t) {
|
|
5787
|
+
return t.status === "in_progress" || t.status === "completed";
|
|
5788
|
+
}
|
|
5789
|
+
function repairOrder(tasks) {
|
|
5790
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
5791
|
+
const placed = /* @__PURE__ */ new Set();
|
|
5792
|
+
const movable = tasks.filter((t) => !isPinned(t));
|
|
5793
|
+
const result = [];
|
|
5794
|
+
const blockerOf = (t) => {
|
|
5795
|
+
const id = t.dependencies.find((d) => byId.has(d) && !placed.has(d));
|
|
5796
|
+
return id ? byId.get(id) : void 0;
|
|
5797
|
+
};
|
|
5798
|
+
const place = (t) => {
|
|
5799
|
+
result.push(t);
|
|
5800
|
+
placed.add(t.id);
|
|
5801
|
+
};
|
|
5802
|
+
for (const occupant of tasks) {
|
|
5803
|
+
if (isPinned(occupant)) {
|
|
5804
|
+
const blocker = blockerOf(occupant);
|
|
5805
|
+
if (blocker) return { error: pinnedRefusal(blocker, occupant, "before") };
|
|
5806
|
+
place(occupant);
|
|
5807
|
+
continue;
|
|
5808
|
+
}
|
|
5809
|
+
const next = movable.findIndex((t) => !blockerOf(t));
|
|
5810
|
+
if (next < 0) {
|
|
5811
|
+
const [stuck, pin] = trailToPin(movable[0], blockerOf);
|
|
5812
|
+
return { error: pinnedRefusal(stuck, pin, "after") };
|
|
5813
|
+
}
|
|
5814
|
+
place(movable.splice(next, 1)[0]);
|
|
5815
|
+
}
|
|
5816
|
+
return { tasks: result };
|
|
5817
|
+
}
|
|
5818
|
+
function trailToPin(task, blockerOf) {
|
|
5819
|
+
let at = task;
|
|
5820
|
+
for (; ; ) {
|
|
5821
|
+
const blocker = blockerOf(at);
|
|
5822
|
+
if (!blocker || isPinned(blocker)) return [at, blocker ?? at];
|
|
5823
|
+
at = blocker;
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
function describeMoves(before, after) {
|
|
5827
|
+
const wasAt = new Map(before.map((t, i) => [t.id, i + 1]));
|
|
5828
|
+
const moved = after.map((t, i) => ({ t, from: wasAt.get(t.id), to: i + 1 })).filter((m) => m.from !== m.to);
|
|
5829
|
+
return moved.length ? moved.map((m) => `"${m.t.title}" #${m.from}\u2192#${m.to}`).join(", ") : null;
|
|
5830
|
+
}
|
|
5831
|
+
function pinnedRefusal(other, pin, side) {
|
|
5832
|
+
const word = pin.status === "in_progress" ? "running" : "completed";
|
|
5833
|
+
return `"${other.title}" would have to run ${side} "${pin.title}", which is ${word} and cannot be moved`;
|
|
5834
|
+
}
|
|
5407
5835
|
function canMergeTasks(tasks, selectedIds) {
|
|
5408
5836
|
if (selectedIds.length < 2) return { ok: false, error: "Select at least two tasks to merge" };
|
|
5409
5837
|
const idSet = new Set(selectedIds);
|
|
@@ -5420,15 +5848,6 @@ function canMergeTasks(tasks, selectedIds) {
|
|
|
5420
5848
|
sim.push({ ...ordered[0], id: survivorId, title: "__merge_sim__", order: ordered[0].order, dependencies: unionDeps });
|
|
5421
5849
|
const cycle = detectCycle(sim);
|
|
5422
5850
|
if (cycle) return { ok: false, error: `Merging would create a dependency cycle involving "${cycle}"` };
|
|
5423
|
-
const orderById = new Map(sim.map((t) => [t.id, t.order]));
|
|
5424
|
-
for (const t of sim) {
|
|
5425
|
-
for (const dep of t.dependencies) {
|
|
5426
|
-
const depOrder = orderById.get(dep);
|
|
5427
|
-
if (depOrder !== void 0 && depOrder >= t.order) {
|
|
5428
|
-
return { ok: false, error: "Merging would break task ordering \u2014 a dependency of the merged set comes after it" };
|
|
5429
|
-
}
|
|
5430
|
-
}
|
|
5431
|
-
}
|
|
5432
5851
|
return { ok: true };
|
|
5433
5852
|
}
|
|
5434
5853
|
function dependentsOf(tasks, taskId) {
|
|
@@ -5460,17 +5879,46 @@ function canSplitTask(tasks, taskId) {
|
|
|
5460
5879
|
if (target.status === "completed") return { ok: false, error: `"${target.title}" is completed and cannot be split` };
|
|
5461
5880
|
return { ok: true };
|
|
5462
5881
|
}
|
|
5463
|
-
function applyTaskOps(currentTasks, ops, runners) {
|
|
5882
|
+
function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
5464
5883
|
let tasks = JSON.parse(JSON.stringify(currentTasks));
|
|
5465
5884
|
const errors = [];
|
|
5466
5885
|
const summary = [];
|
|
5467
|
-
const
|
|
5886
|
+
const originalFlatAll = flattenTasks(currentTasks);
|
|
5887
|
+
const handleOwner = /* @__PURE__ */ new Map();
|
|
5888
|
+
const handleId = /* @__PURE__ */ new Map();
|
|
5889
|
+
for (const [i, op] of ops.entries()) {
|
|
5890
|
+
const handle = op.handle;
|
|
5891
|
+
if (handle === void 0) continue;
|
|
5892
|
+
if (typeof handle !== "string" || !handle.trim()) {
|
|
5893
|
+
errors.push(`op ${i + 1} (${op.op}): handle must be a non-empty string`);
|
|
5894
|
+
continue;
|
|
5895
|
+
}
|
|
5896
|
+
if (handleOwner.has(handle)) {
|
|
5897
|
+
errors.push(`op ${i + 1} (${op.op}): handle "${handle}" is already used earlier in this batch`);
|
|
5898
|
+
continue;
|
|
5899
|
+
}
|
|
5900
|
+
if (refCollidesWithExisting(handle, originalFlatAll)) {
|
|
5901
|
+
errors.push(`op ${i + 1} (${op.op}): handle "${handle}" collides with an existing task reference`);
|
|
5902
|
+
continue;
|
|
5903
|
+
}
|
|
5904
|
+
handleOwner.set(handle, i);
|
|
5905
|
+
}
|
|
5906
|
+
if (errors.length) return { ok: false, tasks: currentTasks, errors, summary: [] };
|
|
5907
|
+
const resolveDeps = (deps, opIndex) => {
|
|
5468
5908
|
const ids = [];
|
|
5469
5909
|
const bad = [];
|
|
5910
|
+
const liveFlat = flattenTasks(tasks);
|
|
5470
5911
|
for (const d of Array.isArray(deps) ? deps : []) {
|
|
5471
|
-
const
|
|
5472
|
-
if (
|
|
5473
|
-
|
|
5912
|
+
const r = resolveRef(d, originalFlatAll, liveFlat, handleOwner, handleId, opIndex);
|
|
5913
|
+
if ("error" in r) {
|
|
5914
|
+
bad.push(`${String(d)} (${r.error})`);
|
|
5915
|
+
continue;
|
|
5916
|
+
}
|
|
5917
|
+
if (!liveFlat.some((t) => t.id === r.id)) {
|
|
5918
|
+
bad.push(`${String(d)} (no longer exists in this batch)`);
|
|
5919
|
+
continue;
|
|
5920
|
+
}
|
|
5921
|
+
ids.push(r.id);
|
|
5474
5922
|
}
|
|
5475
5923
|
return { ids, bad };
|
|
5476
5924
|
};
|
|
@@ -5478,41 +5926,47 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5478
5926
|
const label = `op ${i + 1} (${op.op})`;
|
|
5479
5927
|
switch (op.op) {
|
|
5480
5928
|
case "update": {
|
|
5481
|
-
const
|
|
5482
|
-
if (
|
|
5483
|
-
errors.push(`${label}:
|
|
5484
|
-
break;
|
|
5485
|
-
}
|
|
5486
|
-
if (target.status === "in_progress") {
|
|
5487
|
-
errors.push(`${label}: "${target.title}" is running and cannot be modified`);
|
|
5929
|
+
const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
5930
|
+
if ("error" in ref) {
|
|
5931
|
+
errors.push(`${label}: ${ref.error}`);
|
|
5488
5932
|
break;
|
|
5489
5933
|
}
|
|
5490
|
-
|
|
5491
|
-
|
|
5934
|
+
const target = findInTasks(tasks, ref.id);
|
|
5935
|
+
if (!target) {
|
|
5936
|
+
errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
|
|
5492
5937
|
break;
|
|
5493
5938
|
}
|
|
5494
5939
|
const changes = {};
|
|
5495
5940
|
for (const key of UPDATABLE_FIELDS) {
|
|
5496
5941
|
if (op.changes && key in op.changes) changes[key] = op.changes[key];
|
|
5497
5942
|
}
|
|
5943
|
+
if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
|
|
5944
|
+
errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
|
|
5945
|
+
break;
|
|
5946
|
+
}
|
|
5947
|
+
const changesForCheck = { ...changes };
|
|
5948
|
+
delete changesForCheck.dependencies;
|
|
5949
|
+
const check = validateTaskEdit("planner", flattenTasks(tasks), target.id, changesForCheck, catalog);
|
|
5950
|
+
if (!check.ok) {
|
|
5951
|
+
errors.push(`${label}: ${check.error}`);
|
|
5952
|
+
break;
|
|
5953
|
+
}
|
|
5954
|
+
for (const field of check.clear ?? []) changes[field] = void 0;
|
|
5498
5955
|
if ("dependencies" in changes) {
|
|
5499
|
-
const { ids, bad } = resolveDeps(changes.dependencies);
|
|
5956
|
+
const { ids, bad } = resolveDeps(changes.dependencies, i);
|
|
5500
5957
|
if (bad.length) {
|
|
5501
5958
|
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
5502
5959
|
break;
|
|
5503
5960
|
}
|
|
5504
5961
|
changes.dependencies = ids.filter((id) => id !== target.id);
|
|
5505
5962
|
}
|
|
5506
|
-
if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
|
|
5507
|
-
errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
|
|
5508
|
-
break;
|
|
5509
|
-
}
|
|
5510
5963
|
if (Object.keys(changes).length === 0) {
|
|
5511
5964
|
errors.push(`${label}: no valid changes provided`);
|
|
5512
5965
|
break;
|
|
5513
5966
|
}
|
|
5514
5967
|
tasks = updateTaskInPlan(tasks, target.id, changes);
|
|
5515
|
-
|
|
5968
|
+
const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
|
|
5969
|
+
summary.push(`Updated "${target.title}" (${Object.keys(changes).join(", ")})${clearedNote}`);
|
|
5516
5970
|
break;
|
|
5517
5971
|
}
|
|
5518
5972
|
case "add": {
|
|
@@ -5521,27 +5975,39 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5521
5975
|
errors.push(`${label}: new task needs a title`);
|
|
5522
5976
|
break;
|
|
5523
5977
|
}
|
|
5524
|
-
const { ids, bad } = resolveDeps(spec.dependencies);
|
|
5978
|
+
const { ids, bad } = resolveDeps(spec.dependencies, i);
|
|
5525
5979
|
if (bad.length) {
|
|
5526
5980
|
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
5527
5981
|
break;
|
|
5528
5982
|
}
|
|
5529
5983
|
const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : runners[0];
|
|
5984
|
+
const validity = checkModelAndModeValidity(runner, spec.assignedModel, spec.taskMode, catalog);
|
|
5985
|
+
if (!validity.ok) {
|
|
5986
|
+
errors.push(`${label}: ${validity.error}`);
|
|
5987
|
+
break;
|
|
5988
|
+
}
|
|
5989
|
+
const newId = (0, import_uuid2.v4)();
|
|
5530
5990
|
tasks = addTaskToPlan(tasks, {
|
|
5531
5991
|
...spec,
|
|
5532
|
-
id:
|
|
5992
|
+
id: newId,
|
|
5533
5993
|
dependencies: ids,
|
|
5534
5994
|
assignedRunner: runner,
|
|
5535
5995
|
description: spec.description ?? spec.title,
|
|
5536
5996
|
prompt: spec.prompt ?? spec.description ?? spec.title
|
|
5537
5997
|
});
|
|
5998
|
+
if (op.handle) handleId.set(op.handle, newId);
|
|
5538
5999
|
summary.push(`Added "${spec.title}"`);
|
|
5539
6000
|
break;
|
|
5540
6001
|
}
|
|
5541
6002
|
case "remove": {
|
|
5542
|
-
const
|
|
6003
|
+
const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
6004
|
+
if ("error" in ref) {
|
|
6005
|
+
errors.push(`${label}: ${ref.error}`);
|
|
6006
|
+
break;
|
|
6007
|
+
}
|
|
6008
|
+
const target = findInTasks(tasks, ref.id);
|
|
5543
6009
|
if (!target) {
|
|
5544
|
-
errors.push(`${label}: task "${op.taskId}"
|
|
6010
|
+
errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
|
|
5545
6011
|
break;
|
|
5546
6012
|
}
|
|
5547
6013
|
if (target.status === "in_progress") {
|
|
@@ -5557,7 +6023,13 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5557
6023
|
break;
|
|
5558
6024
|
}
|
|
5559
6025
|
case "reorder": {
|
|
5560
|
-
const
|
|
6026
|
+
const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
|
|
6027
|
+
const badRef = refs.find((r) => "error" in r);
|
|
6028
|
+
if (badRef) {
|
|
6029
|
+
errors.push(`${label}: ${badRef.error}`);
|
|
6030
|
+
break;
|
|
6031
|
+
}
|
|
6032
|
+
const resolved = refs.map((r) => findTopLevel(tasks, r.id));
|
|
5561
6033
|
if (resolved.some((t) => !t)) {
|
|
5562
6034
|
errors.push(`${label}: unknown task in reorder list`);
|
|
5563
6035
|
break;
|
|
@@ -5573,9 +6045,15 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5573
6045
|
break;
|
|
5574
6046
|
}
|
|
5575
6047
|
case "merge": {
|
|
5576
|
-
const
|
|
6048
|
+
const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
|
|
6049
|
+
const badRef = refs.find((r) => "error" in r);
|
|
6050
|
+
if (badRef) {
|
|
6051
|
+
errors.push(`${label}: ${badRef.error}`);
|
|
6052
|
+
break;
|
|
6053
|
+
}
|
|
6054
|
+
const resolved = refs.map((r) => findTopLevel(tasks, r.id));
|
|
5577
6055
|
if (resolved.some((t) => !t)) {
|
|
5578
|
-
errors.push(`${label}: unknown task in merge list`);
|
|
6056
|
+
errors.push(`${label}: unknown task in merge list (already removed, merged, or split by an earlier op)`);
|
|
5579
6057
|
break;
|
|
5580
6058
|
}
|
|
5581
6059
|
const toMerge = resolved.map((t) => t).sort((a, b) => a.order - b.order);
|
|
@@ -5590,7 +6068,7 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5590
6068
|
}
|
|
5591
6069
|
const idSet = new Set(toMerge.map((t) => t.id));
|
|
5592
6070
|
const unionDepsRaw = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));
|
|
5593
|
-
const { ids, bad } = resolveDeps(unionDepsRaw);
|
|
6071
|
+
const { ids, bad } = resolveDeps(unionDepsRaw, i);
|
|
5594
6072
|
if (bad.length) {
|
|
5595
6073
|
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
5596
6074
|
break;
|
|
@@ -5631,11 +6109,17 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5631
6109
|
}
|
|
5632
6110
|
if (!inserted) result.push(mergedTask);
|
|
5633
6111
|
tasks = renumberTasks(result);
|
|
6112
|
+
if (op.handle) handleId.set(op.handle, mergedId);
|
|
5634
6113
|
summary.push(`Merged ${toMerge.length} tasks into "${spec.title}"`);
|
|
5635
6114
|
break;
|
|
5636
6115
|
}
|
|
5637
6116
|
case "split": {
|
|
5638
|
-
const
|
|
6117
|
+
const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, i);
|
|
6118
|
+
if ("error" in ref) {
|
|
6119
|
+
errors.push(`${label}: ${ref.error}`);
|
|
6120
|
+
break;
|
|
6121
|
+
}
|
|
6122
|
+
const target = findTopLevel(tasks, ref.id);
|
|
5639
6123
|
if (!target) {
|
|
5640
6124
|
errors.push(`${label}: task "${op.taskId}" not found`);
|
|
5641
6125
|
break;
|
|
@@ -5656,16 +6140,16 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5656
6140
|
const newTasks = [];
|
|
5657
6141
|
let prevId;
|
|
5658
6142
|
let buildError = false;
|
|
5659
|
-
for (let
|
|
5660
|
-
const spec = parts[
|
|
6143
|
+
for (let p = 0; p < parts.length; p++) {
|
|
6144
|
+
const spec = parts[p];
|
|
5661
6145
|
if (!spec || !spec.title) {
|
|
5662
|
-
errors.push(`${label}: split part ${
|
|
6146
|
+
errors.push(`${label}: split part ${p + 1} needs a title`);
|
|
5663
6147
|
buildError = true;
|
|
5664
6148
|
break;
|
|
5665
6149
|
}
|
|
5666
6150
|
let deps;
|
|
5667
|
-
if (
|
|
5668
|
-
const { ids, bad } = resolveDeps(target.dependencies);
|
|
6151
|
+
if (p === 0) {
|
|
6152
|
+
const { ids, bad } = resolveDeps(target.dependencies, i);
|
|
5669
6153
|
if (bad.length) {
|
|
5670
6154
|
errors.push(`${label}: split part 1 unknown dependencies: ${bad.join(", ")}`);
|
|
5671
6155
|
buildError = true;
|
|
@@ -5703,9 +6187,60 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5703
6187
|
result.push({ ...t, dependencies: t.dependencies.map((d) => d === target.id ? tailId : d) });
|
|
5704
6188
|
}
|
|
5705
6189
|
tasks = renumberTasks(result);
|
|
6190
|
+
if (op.handle) handleId.set(op.handle, tailId);
|
|
5706
6191
|
summary.push(`Split "${target.title}" into ${newTasks.length} tasks`);
|
|
5707
6192
|
break;
|
|
5708
6193
|
}
|
|
6194
|
+
case "rearm": {
|
|
6195
|
+
const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
6196
|
+
if ("error" in ref) {
|
|
6197
|
+
errors.push(`${label}: ${ref.error}`);
|
|
6198
|
+
break;
|
|
6199
|
+
}
|
|
6200
|
+
const target = findInTasks(tasks, ref.id);
|
|
6201
|
+
if (!target) {
|
|
6202
|
+
errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
|
|
6203
|
+
break;
|
|
6204
|
+
}
|
|
6205
|
+
if (target.status === "in_progress") {
|
|
6206
|
+
errors.push(`${label}: "${target.title}" is running and cannot be re-armed`);
|
|
6207
|
+
break;
|
|
6208
|
+
}
|
|
6209
|
+
const changes = {};
|
|
6210
|
+
for (const key of UPDATABLE_FIELDS) {
|
|
6211
|
+
if (op.changes && key in op.changes) changes[key] = op.changes[key];
|
|
6212
|
+
}
|
|
6213
|
+
if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
|
|
6214
|
+
errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
|
|
6215
|
+
break;
|
|
6216
|
+
}
|
|
6217
|
+
const changesForCheck = { ...changes };
|
|
6218
|
+
delete changesForCheck.dependencies;
|
|
6219
|
+
const check = validateTaskEdit("direct", flattenTasks(tasks), target.id, changesForCheck, catalog);
|
|
6220
|
+
if (!check.ok) {
|
|
6221
|
+
errors.push(`${label}: ${check.error}`);
|
|
6222
|
+
break;
|
|
6223
|
+
}
|
|
6224
|
+
for (const field of check.clear ?? []) changes[field] = void 0;
|
|
6225
|
+
if ("dependencies" in changes) {
|
|
6226
|
+
const { ids, bad } = resolveDeps(changes.dependencies, i);
|
|
6227
|
+
if (bad.length) {
|
|
6228
|
+
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
6229
|
+
break;
|
|
6230
|
+
}
|
|
6231
|
+
changes.dependencies = ids.filter((id) => id !== target.id);
|
|
6232
|
+
}
|
|
6233
|
+
tasks = updateTaskInPlan(tasks, target.id, { ...changes, status: "pending", verdict: void 0, outputSummary: void 0 });
|
|
6234
|
+
for (const dep of flattenTasks(tasks)) {
|
|
6235
|
+
if (dep.status === "blocked" && dep.dependencies.includes(target.id)) {
|
|
6236
|
+
tasks = updateTaskInPlan(tasks, dep.id, { status: "pending" });
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6239
|
+
const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
|
|
6240
|
+
const changedNote = Object.keys(changes).length ? ` (${Object.keys(changes).join(", ")})` : "";
|
|
6241
|
+
summary.push(`Re-armed "${target.title}"${changedNote}${clearedNote}`);
|
|
6242
|
+
break;
|
|
6243
|
+
}
|
|
5709
6244
|
default:
|
|
5710
6245
|
errors.push(`${label}: unknown op "${op.op}"`);
|
|
5711
6246
|
}
|
|
@@ -5721,13 +6256,14 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5721
6256
|
}
|
|
5722
6257
|
const cycleAt = detectCycle(flat);
|
|
5723
6258
|
if (cycleAt) errors.push(`Dependency cycle detected involving "${cycleAt}"`);
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
}
|
|
6259
|
+
if (!errors.length) {
|
|
6260
|
+
const repaired = repairOrder(tasks);
|
|
6261
|
+
if ("error" in repaired) {
|
|
6262
|
+
errors.push(repaired.error);
|
|
6263
|
+
} else {
|
|
6264
|
+
const moves = describeMoves(tasks, repaired.tasks);
|
|
6265
|
+
if (moves) summary.push(`Reordered to keep dependencies first: ${moves}`);
|
|
6266
|
+
tasks = renumberTasks(repaired.tasks);
|
|
5731
6267
|
}
|
|
5732
6268
|
}
|
|
5733
6269
|
}
|
|
@@ -5757,6 +6293,9 @@ function truncatedPlanReEmitPrompt(historyCompacted) {
|
|
|
5757
6293
|
function reEmitTaskOpsPrompt(detail) {
|
|
5758
6294
|
return `Your task edits could not be applied: ${detail}. Re-emit ONLY a corrected {"${TASK_OPS_ENVELOPE_KEY}":[...]} JSON object \u2014 no prose, no code fences \u2014 or reply in prose if you did not intend to edit tasks.`;
|
|
5759
6295
|
}
|
|
6296
|
+
function reEmitTaskQueryPrompt(detail) {
|
|
6297
|
+
return `Your task-detail request could not be read: ${detail}. Re-emit ONLY a corrected {"${TASK_QUERY_ENVELOPE_KEY}":{"tasks":["<id or #order>"],"fields"?:[...],"catalog"?:true}} JSON object \u2014 no prose, no code fences \u2014 or reply in prose if you did not mean to look anything up.`;
|
|
6298
|
+
}
|
|
5760
6299
|
function taskOpsRejectedPrompt(errors) {
|
|
5761
6300
|
return `Those task edits were rejected:
|
|
5762
6301
|
- ${errors.join("\n- ")}
|
|
@@ -5783,6 +6322,16 @@ function classifyPlannerReply(text, opts) {
|
|
|
5783
6322
|
}
|
|
5784
6323
|
}
|
|
5785
6324
|
}
|
|
6325
|
+
if (textHasTaskQuery(text)) {
|
|
6326
|
+
try {
|
|
6327
|
+
return { kind: "task_query", query: parseTaskQueryJson(text) };
|
|
6328
|
+
} catch (err) {
|
|
6329
|
+
if (!(err instanceof PlanParseError)) throw err;
|
|
6330
|
+
if (extractObjectsWithKey(text, TASK_QUERY_ENVELOPE_KEY).matches.length > 0) {
|
|
6331
|
+
return { kind: "broken_task_query", error: err };
|
|
6332
|
+
}
|
|
6333
|
+
}
|
|
6334
|
+
}
|
|
5786
6335
|
if (text.includes(`"${PLAN_ENVELOPE_KEY}"`)) {
|
|
5787
6336
|
try {
|
|
5788
6337
|
return { kind: "plan", tasks: parsePlanJson(text, opts.runners, opts.runnerModes, opts.autonomousDefault) };
|
|
@@ -6426,7 +6975,7 @@ var BaseAiService = class _BaseAiService {
|
|
|
6426
6975
|
const parallelIndexes = toolCalls.flatMap((tc, i) => PARALLEL_SAFE_TOOLS.has(tc.name) ? [i] : []);
|
|
6427
6976
|
await Promise.all([
|
|
6428
6977
|
spawnIndexes.length > 0 ? mapWithConcurrency(spawnIndexes, SUBAGENT_LIMITS.maxConcurrent, async (i) => {
|
|
6429
|
-
const subagentId = (0,
|
|
6978
|
+
const subagentId = (0, import_uuid3.v4)();
|
|
6430
6979
|
spawnSubagentIds.set(i, subagentId);
|
|
6431
6980
|
onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), subagentId, toolCallId: toolCalls[i].id });
|
|
6432
6981
|
precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args }, fs10, onProgress, signal, subagentId));
|
|
@@ -6581,6 +7130,10 @@ ${llmOutput}
|
|
|
6581
7130
|
switch (reply.kind) {
|
|
6582
7131
|
case "task_ops":
|
|
6583
7132
|
return { kind: "task_ops", ops: reply.ops, text: turn.text, researchLog };
|
|
7133
|
+
// A read is settled by the Session (it owns the plan and the catalog),
|
|
7134
|
+
// so it leaves this loop the same way a plan or an edit does.
|
|
7135
|
+
case "task_query":
|
|
7136
|
+
return { kind: "task_query", query: reply.query, text: turn.text, researchLog };
|
|
6584
7137
|
case "plan":
|
|
6585
7138
|
if (ctx.prdEnabled && !ctx.prdCaptured && !ctx.prdNudgeSent && !signal?.aborted) {
|
|
6586
7139
|
ctx.prdNudgeSent = true;
|
|
@@ -6603,6 +7156,13 @@ ${llmOutput}
|
|
|
6603
7156
|
continue;
|
|
6604
7157
|
}
|
|
6605
7158
|
break;
|
|
7159
|
+
case "broken_task_query":
|
|
7160
|
+
if (jsonRepairAttempts < MAX_JSON_REPAIRS2 && !signal?.aborted) {
|
|
7161
|
+
jsonRepairAttempts++;
|
|
7162
|
+
pending = reEmitTaskQueryPrompt(reply.error.message);
|
|
7163
|
+
continue;
|
|
7164
|
+
}
|
|
7165
|
+
break;
|
|
6606
7166
|
case "broken_plan":
|
|
6607
7167
|
if (jsonRepairAttempts < MAX_JSON_REPAIRS2 && !signal?.aborted) {
|
|
6608
7168
|
jsonRepairAttempts++;
|
|
@@ -8582,111 +9142,6 @@ ${output.slice(-3e3)}`);
|
|
|
8582
9142
|
}
|
|
8583
9143
|
};
|
|
8584
9144
|
|
|
8585
|
-
// src/services/ModelAllowlistResolver.ts
|
|
8586
|
-
function effectiveAllowlist(allowlist, runner, modelsByRunner) {
|
|
8587
|
-
if (!allowlist || allowlist.length === 0) return void 0;
|
|
8588
|
-
if (!modelsByRunner) return allowlist;
|
|
8589
|
-
const own = new Set((modelsByRunner[runner] ?? []).map((m) => m.modelId));
|
|
8590
|
-
const elsewhere = /* @__PURE__ */ new Set();
|
|
8591
|
-
for (const [other, models] of Object.entries(modelsByRunner)) {
|
|
8592
|
-
if (other === runner) continue;
|
|
8593
|
-
for (const m of models ?? []) elsewhere.add(m.modelId);
|
|
8594
|
-
}
|
|
8595
|
-
const kept = allowlist.filter((id) => own.has(id) || !elsewhere.has(id));
|
|
8596
|
-
return kept.length > 0 ? kept : void 0;
|
|
8597
|
-
}
|
|
8598
|
-
function filterModelsForPrompt(modelsByRunner, perRunnerAllowlist) {
|
|
8599
|
-
const result = {};
|
|
8600
|
-
for (const [runner, models] of Object.entries(modelsByRunner)) {
|
|
8601
|
-
if (!models) continue;
|
|
8602
|
-
const allowed = effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
8603
|
-
if (!allowed) {
|
|
8604
|
-
result[runner] = models;
|
|
8605
|
-
continue;
|
|
8606
|
-
}
|
|
8607
|
-
const allowedSet = new Set(allowed);
|
|
8608
|
-
const intersection = models.filter((m) => allowedSet.has(m.modelId));
|
|
8609
|
-
const covered = new Set(intersection.map((m) => m.modelId));
|
|
8610
|
-
const missing = allowed.filter((id) => !covered.has(id)).map((id) => ({ modelId: id, modelLabel: id.split("/").pop() || id, variants: [] }));
|
|
8611
|
-
result[runner] = [...intersection, ...missing];
|
|
8612
|
-
}
|
|
8613
|
-
return result;
|
|
8614
|
-
}
|
|
8615
|
-
var EFFORT_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
8616
|
-
var EFFORT_ALIASES = { disabled: "none", adaptive: "medium" };
|
|
8617
|
-
function clampThinkingEffort(effort, variants) {
|
|
8618
|
-
if (!effort) return void 0;
|
|
8619
|
-
if (variants.length === 0) return void 0;
|
|
8620
|
-
const ids = new Set(variants.map((v) => v.id));
|
|
8621
|
-
if (ids.has(effort)) return effort;
|
|
8622
|
-
const canon = EFFORT_ALIASES[effort] ?? effort;
|
|
8623
|
-
if (ids.has(canon)) return canon;
|
|
8624
|
-
const idx = EFFORT_LADDER.indexOf(canon);
|
|
8625
|
-
if (idx < 0) return void 0;
|
|
8626
|
-
for (let d = 1; d < EFFORT_LADDER.length; d++) {
|
|
8627
|
-
const lower = EFFORT_LADDER[idx - d];
|
|
8628
|
-
if (lower && ids.has(lower)) return lower;
|
|
8629
|
-
const higher = EFFORT_LADDER[idx + d];
|
|
8630
|
-
if (higher && ids.has(higher)) return higher;
|
|
8631
|
-
}
|
|
8632
|
-
return void 0;
|
|
8633
|
-
}
|
|
8634
|
-
function coerceAssignments(tasks, perRunnerAllowlist, allowedRunners, modelsByRunner) {
|
|
8635
|
-
const clampVariant = (task) => {
|
|
8636
|
-
if (!task.assignedModel || !modelsByRunner) return task;
|
|
8637
|
-
const model = modelsByRunner[task.assignedRunner]?.find(
|
|
8638
|
-
(m) => m.modelId === task.assignedModel.modelId
|
|
8639
|
-
);
|
|
8640
|
-
if (!model) return task;
|
|
8641
|
-
const clamped = clampThinkingEffort(task.assignedModel.thinkingEffort, model.variants);
|
|
8642
|
-
return {
|
|
8643
|
-
...task,
|
|
8644
|
-
assignedModel: {
|
|
8645
|
-
...task.assignedModel,
|
|
8646
|
-
thinkingEffort: clamped,
|
|
8647
|
-
availableVariants: model.variants.map((v) => v.id)
|
|
8648
|
-
}
|
|
8649
|
-
};
|
|
8650
|
-
};
|
|
8651
|
-
const allowedFor = (runner) => effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
8652
|
-
const assignmentFor = (runner, modelId) => {
|
|
8653
|
-
const known = modelsByRunner?.[runner]?.find((m) => m.modelId === modelId);
|
|
8654
|
-
return {
|
|
8655
|
-
modelId,
|
|
8656
|
-
modelLabel: known?.modelLabel ?? modelId,
|
|
8657
|
-
thinkingEffort: void 0,
|
|
8658
|
-
...known ? { availableVariants: known.variants.map((v) => v.id) } : {}
|
|
8659
|
-
};
|
|
8660
|
-
};
|
|
8661
|
-
return tasks.map((task) => {
|
|
8662
|
-
if (task.type === "user") return task;
|
|
8663
|
-
if (allowedRunners && !allowedRunners.includes(task.assignedRunner)) {
|
|
8664
|
-
const fallbackRunner = allowedRunners[0];
|
|
8665
|
-
const fallbackModels = allowedFor(fallbackRunner);
|
|
8666
|
-
const fallbackModel = fallbackModels?.length ? assignmentFor(fallbackRunner, fallbackModels[0]) : modelsByRunner?.[fallbackRunner]?.[0] ? assignmentFor(fallbackRunner, modelsByRunner[fallbackRunner][0].modelId) : task.assignedModel;
|
|
8667
|
-
return clampVariant({
|
|
8668
|
-
...task,
|
|
8669
|
-
assignedRunner: fallbackRunner,
|
|
8670
|
-
assignedModel: fallbackModel
|
|
8671
|
-
});
|
|
8672
|
-
}
|
|
8673
|
-
const allowlist = allowedFor(task.assignedRunner);
|
|
8674
|
-
if (!task.assignedModel) {
|
|
8675
|
-
if (allowlist && allowlist.length > 0) {
|
|
8676
|
-
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) });
|
|
8677
|
-
}
|
|
8678
|
-
const discovered = modelsByRunner?.[task.assignedRunner]?.[0];
|
|
8679
|
-
if (discovered) {
|
|
8680
|
-
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, discovered.modelId) });
|
|
8681
|
-
}
|
|
8682
|
-
return task;
|
|
8683
|
-
}
|
|
8684
|
-
if (!allowlist || allowlist.length === 0) return clampVariant(task);
|
|
8685
|
-
if (allowlist.includes(task.assignedModel.modelId)) return clampVariant(task);
|
|
8686
|
-
return { ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) };
|
|
8687
|
-
});
|
|
8688
|
-
}
|
|
8689
|
-
|
|
8690
9145
|
// src/services/Planner.ts
|
|
8691
9146
|
function plannerModesWith(req) {
|
|
8692
9147
|
const modes = req.modes ?? DEFAULT_PLANNER_MODES;
|
|
@@ -10398,12 +10853,19 @@ var StdioAgentAdapter = class {
|
|
|
10398
10853
|
/** Set for the duration of a turn. Outside one, events are buffered rather than dropped. */
|
|
10399
10854
|
turnEmit = null;
|
|
10400
10855
|
/**
|
|
10401
|
-
* Events the agent produced
|
|
10402
|
-
* that arrive during the handshake. Dropping them hid the one
|
|
10403
|
-
* that explains a planner which cannot read the workspace, so they
|
|
10404
|
-
* until a turn exists to show them in.
|
|
10856
|
+
* Events the agent produced before its first turn — in practice the startup
|
|
10857
|
+
* warnings that arrive during the handshake. Dropping them hid the one
|
|
10858
|
+
* diagnostic that explains a planner which cannot read the workspace, so they
|
|
10859
|
+
* are held until a turn exists to show them in.
|
|
10860
|
+
*
|
|
10861
|
+
* Only before the *first* turn. Once one has run, anything arriving outside a
|
|
10862
|
+
* turn is that closed turn's straggling work — a backgrounded subagent
|
|
10863
|
+
* finishing, a late read — and replaying it into the next turn presents it as
|
|
10864
|
+
* research done for the user's new message.
|
|
10405
10865
|
*/
|
|
10406
10866
|
betweenTurns = [];
|
|
10867
|
+
/** Set once a turn has ended: after that, out-of-turn events are stale, not startup. */
|
|
10868
|
+
hadTurn = false;
|
|
10407
10869
|
disposed = false;
|
|
10408
10870
|
/** Resolves when the process ends, so a handshake can lose the race instead of waiting out its timeout. */
|
|
10409
10871
|
processEnded;
|
|
@@ -10438,7 +10900,7 @@ var StdioAgentAdapter = class {
|
|
|
10438
10900
|
this.process.stdout?.on("data", (chunk) => {
|
|
10439
10901
|
this.stdout.push(chunk.toString(), (line) => this.handleLine(line, (event) => {
|
|
10440
10902
|
if (this.turnEmit) this.turnEmit(event);
|
|
10441
|
-
else if (!TURN_SCOPED_EVENTS.has(event.type) && this.betweenTurns.length < BETWEEN_TURN_EVENT_CAP) {
|
|
10903
|
+
else if (!this.hadTurn && !TURN_SCOPED_EVENTS.has(event.type) && this.betweenTurns.length < BETWEEN_TURN_EVENT_CAP) {
|
|
10442
10904
|
this.betweenTurns.push(event);
|
|
10443
10905
|
}
|
|
10444
10906
|
}));
|
|
@@ -10470,6 +10932,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
|
|
|
10470
10932
|
if (settled) return;
|
|
10471
10933
|
settled = true;
|
|
10472
10934
|
this.turnEmit = null;
|
|
10935
|
+
this.hadTurn = true;
|
|
10473
10936
|
signal?.removeEventListener("abort", onAbort);
|
|
10474
10937
|
this.process?.removeListener("exit", onExit);
|
|
10475
10938
|
this.process?.removeListener("error", onExit);
|
|
@@ -10548,6 +11011,7 @@ ${tail}` : ""}`;
|
|
|
10548
11011
|
|
|
10549
11012
|
// src/services/harness/ClaudeCodeAdapter.ts
|
|
10550
11013
|
var DISALLOWED_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit", "KillShell"];
|
|
11014
|
+
var ASYNC_LAUNCH_MARKER = "Async agent launched successfully";
|
|
10551
11015
|
function flattenContent(content) {
|
|
10552
11016
|
if (typeof content === "string") return content;
|
|
10553
11017
|
if (Array.isArray(content)) {
|
|
@@ -10557,6 +11021,8 @@ function flattenContent(content) {
|
|
|
10557
11021
|
}
|
|
10558
11022
|
var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
10559
11023
|
agentId = "claude-code";
|
|
11024
|
+
/** Whether this turn has already emitted reply text — see {@link handleLine}. */
|
|
11025
|
+
turnHasText = false;
|
|
10560
11026
|
spawnSpec(opts) {
|
|
10561
11027
|
const args = [
|
|
10562
11028
|
"-p",
|
|
@@ -10581,6 +11047,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
10581
11047
|
return { command: "claude", args };
|
|
10582
11048
|
}
|
|
10583
11049
|
turnPayload(message) {
|
|
11050
|
+
this.turnHasText = false;
|
|
10584
11051
|
return `${JSON.stringify({
|
|
10585
11052
|
type: "user",
|
|
10586
11053
|
message: { role: "user", content: [{ type: "text", text: message }] }
|
|
@@ -10591,6 +11058,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
10591
11058
|
const msg = StdioAgentAdapter.parse(line);
|
|
10592
11059
|
if (!msg) return;
|
|
10593
11060
|
if (msg.session_id) this.sessionId = msg.session_id;
|
|
11061
|
+
if (msg.parent_tool_use_id) return;
|
|
10594
11062
|
switch (msg.type) {
|
|
10595
11063
|
// The control channel: Claude asks whether a tool may run when its mode
|
|
10596
11064
|
// cannot decide alone. A read-only planner answers "deny", every time —
|
|
@@ -10615,8 +11083,12 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
10615
11083
|
}
|
|
10616
11084
|
case "assistant":
|
|
10617
11085
|
for (const block of Array.isArray(msg.message?.content) ? msg.message.content : []) {
|
|
10618
|
-
if (block.type === "text" && block.text)
|
|
10619
|
-
|
|
11086
|
+
if (block.type === "text" && block.text) {
|
|
11087
|
+
emit({ type: "assistant_text", text: this.turnHasText ? `
|
|
11088
|
+
|
|
11089
|
+
${block.text}` : block.text });
|
|
11090
|
+
this.turnHasText = true;
|
|
11091
|
+
} else if (block.type === "thinking" && block.thinking) emit({ type: "thinking", text: block.thinking });
|
|
10620
11092
|
else if (block.type === "tool_use" && block.name) {
|
|
10621
11093
|
emit({ type: "tool_call", id: block.id ?? block.name, name: block.name, args: block.input ?? {} });
|
|
10622
11094
|
}
|
|
@@ -10625,11 +11097,15 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
10625
11097
|
case "user":
|
|
10626
11098
|
for (const block of Array.isArray(msg.message?.content) ? msg.message.content : []) {
|
|
10627
11099
|
if (block.type !== "tool_result") continue;
|
|
11100
|
+
const output = flattenContent(block.content);
|
|
11101
|
+
if (output.includes(ASYNC_LAUNCH_MARKER)) {
|
|
11102
|
+
emit({ type: "background_agent", id: block.tool_use_id ?? "" });
|
|
11103
|
+
}
|
|
10628
11104
|
emit({
|
|
10629
11105
|
type: "tool_result",
|
|
10630
11106
|
id: block.tool_use_id ?? "",
|
|
10631
11107
|
name: "",
|
|
10632
|
-
output
|
|
11108
|
+
output,
|
|
10633
11109
|
success: block.is_error !== true
|
|
10634
11110
|
});
|
|
10635
11111
|
}
|
|
@@ -11049,6 +11525,8 @@ var OpenCodeAdapter = class {
|
|
|
11049
11525
|
exited = false;
|
|
11050
11526
|
disposed = false;
|
|
11051
11527
|
opts = null;
|
|
11528
|
+
/** Whether this turn has already emitted reply text — see {@link emitPart}. */
|
|
11529
|
+
turnHasText = false;
|
|
11052
11530
|
async start(opts) {
|
|
11053
11531
|
this.opts = opts;
|
|
11054
11532
|
assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
|
|
@@ -11116,6 +11594,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
11116
11594
|
return;
|
|
11117
11595
|
}
|
|
11118
11596
|
const seen = /* @__PURE__ */ new Set();
|
|
11597
|
+
this.turnHasText = false;
|
|
11119
11598
|
const streamAbort = new AbortController();
|
|
11120
11599
|
let connected = () => {
|
|
11121
11600
|
};
|
|
@@ -11184,7 +11663,10 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
11184
11663
|
if (part.type === "text" && part.text) {
|
|
11185
11664
|
if (seen.has(`text:${id}`)) return;
|
|
11186
11665
|
seen.add(`text:${id}`);
|
|
11187
|
-
onEvent({ type: "assistant_text", text:
|
|
11666
|
+
onEvent({ type: "assistant_text", text: this.turnHasText ? `
|
|
11667
|
+
|
|
11668
|
+
${part.text}` : part.text });
|
|
11669
|
+
this.turnHasText = true;
|
|
11188
11670
|
return;
|
|
11189
11671
|
}
|
|
11190
11672
|
if (part.type === "reasoning" && part.text) {
|
|
@@ -11351,6 +11833,7 @@ function normalizeAgentArgs(tool, args) {
|
|
|
11351
11833
|
|
|
11352
11834
|
// src/services/harness/CliAgentAiService.ts
|
|
11353
11835
|
var MAX_JSON_REPAIRS = 2;
|
|
11836
|
+
var MAX_AGENT_WAITS = 2;
|
|
11354
11837
|
var LOG_MAX_CHARS = 1e4;
|
|
11355
11838
|
function defaultAdapter(runner, deps) {
|
|
11356
11839
|
switch (runner) {
|
|
@@ -11491,13 +11974,16 @@ var CliAgentAiService = class {
|
|
|
11491
11974
|
let pending = message;
|
|
11492
11975
|
let emptyNudgeSent = false;
|
|
11493
11976
|
let jsonRepairAttempts = 0;
|
|
11977
|
+
let agentWaits = 0;
|
|
11978
|
+
const carried = [];
|
|
11979
|
+
const replyText = (text) => [...carried, text].filter((part) => part.trim()).join("\n\n");
|
|
11494
11980
|
try {
|
|
11495
11981
|
for (; ; ) {
|
|
11496
11982
|
const turn = await this.runTurn(pending, onProgress, combined);
|
|
11497
11983
|
researchLog.push(...turn.researchLog);
|
|
11498
11984
|
if (turn.aborted || combined?.aborted) {
|
|
11499
11985
|
onProgress({ type: "interrupted" });
|
|
11500
|
-
return { kind: "message", text: turn.text, researchLog };
|
|
11986
|
+
return { kind: "message", text: replyText(turn.text), researchLog };
|
|
11501
11987
|
}
|
|
11502
11988
|
if (turn.error) {
|
|
11503
11989
|
return { kind: "message", text: turn.error, researchLog };
|
|
@@ -11515,6 +12001,18 @@ var CliAgentAiService = class {
|
|
|
11515
12001
|
researchLog
|
|
11516
12002
|
};
|
|
11517
12003
|
}
|
|
12004
|
+
if (turn.backgroundAgents > 0 && agentWaits < MAX_AGENT_WAITS && !combined?.aborted) {
|
|
12005
|
+
agentWaits++;
|
|
12006
|
+
carried.push(turn.text);
|
|
12007
|
+
pending = [
|
|
12008
|
+
`You ended your turn with ${turn.backgroundAgents} subagent(s) still running in the background.`,
|
|
12009
|
+
"Ordewell hands the conversation back to the user when your turn ends, so anything you say after it never reaches them \u2014",
|
|
12010
|
+
"the results you promised to report would be lost.",
|
|
12011
|
+
"Wait for those agents to finish NOW, in this reply, and do not end your turn until you have their results.",
|
|
12012
|
+
"Then give the user your synthesis. In future replies, await your agents inside the turn rather than backgrounding them."
|
|
12013
|
+
].join(" ");
|
|
12014
|
+
continue;
|
|
12015
|
+
}
|
|
11518
12016
|
if (extractPrdBlock(turn.text)) conversation.prdCaptured = true;
|
|
11519
12017
|
const reply = classifyPlannerReply(turn.text, {
|
|
11520
12018
|
runners: conversation.runners,
|
|
@@ -11523,7 +12021,11 @@ var CliAgentAiService = class {
|
|
|
11523
12021
|
});
|
|
11524
12022
|
switch (reply.kind) {
|
|
11525
12023
|
case "task_ops":
|
|
11526
|
-
return { kind: "task_ops", ops: reply.ops, text: turn.text, researchLog };
|
|
12024
|
+
return { kind: "task_ops", ops: reply.ops, text: replyText(turn.text), researchLog };
|
|
12025
|
+
// The read channel is a text envelope precisely so it reaches here
|
|
12026
|
+
// too: a harness planner has no Ordewell tool loop to call into.
|
|
12027
|
+
case "task_query":
|
|
12028
|
+
return { kind: "task_query", query: reply.query, text: replyText(turn.text), researchLog };
|
|
11527
12029
|
case "plan":
|
|
11528
12030
|
if (conversation.prdEnabled && !conversation.prdCaptured && !conversation.prdNudgeSent && !combined?.aborted) {
|
|
11529
12031
|
conversation.prdNudgeSent = true;
|
|
@@ -11536,7 +12038,7 @@ var CliAgentAiService = class {
|
|
|
11536
12038
|
continue;
|
|
11537
12039
|
}
|
|
11538
12040
|
this.conversation = null;
|
|
11539
|
-
return { kind: "plan", tasks: reply.tasks, text: turn.text, researchLog };
|
|
12041
|
+
return { kind: "plan", tasks: reply.tasks, text: replyText(turn.text), researchLog };
|
|
11540
12042
|
case "broken_task_ops":
|
|
11541
12043
|
if (jsonRepairAttempts < MAX_JSON_REPAIRS && !combined?.aborted) {
|
|
11542
12044
|
jsonRepairAttempts++;
|
|
@@ -11544,6 +12046,13 @@ var CliAgentAiService = class {
|
|
|
11544
12046
|
continue;
|
|
11545
12047
|
}
|
|
11546
12048
|
break;
|
|
12049
|
+
case "broken_task_query":
|
|
12050
|
+
if (jsonRepairAttempts < MAX_JSON_REPAIRS && !combined?.aborted) {
|
|
12051
|
+
jsonRepairAttempts++;
|
|
12052
|
+
pending = reEmitTaskQueryPrompt(reply.error.message);
|
|
12053
|
+
continue;
|
|
12054
|
+
}
|
|
12055
|
+
break;
|
|
11547
12056
|
case "broken_plan":
|
|
11548
12057
|
if (jsonRepairAttempts < MAX_JSON_REPAIRS && !combined?.aborted) {
|
|
11549
12058
|
jsonRepairAttempts++;
|
|
@@ -11554,7 +12063,7 @@ var CliAgentAiService = class {
|
|
|
11554
12063
|
case "prose":
|
|
11555
12064
|
break;
|
|
11556
12065
|
}
|
|
11557
|
-
return { kind: "message", text: turn.text, researchLog };
|
|
12066
|
+
return { kind: "message", text: replyText(turn.text), researchLog };
|
|
11558
12067
|
}
|
|
11559
12068
|
} finally {
|
|
11560
12069
|
this.activeAbort = null;
|
|
@@ -11574,6 +12083,7 @@ var CliAgentAiService = class {
|
|
|
11574
12083
|
let text = "";
|
|
11575
12084
|
let error;
|
|
11576
12085
|
let stepIndex = 0;
|
|
12086
|
+
let backgroundAgents = 0;
|
|
11577
12087
|
const truncate = (value) => value.length > LOG_MAX_CHARS ? `${value.slice(0, LOG_MAX_CHARS)}
|
|
11578
12088
|
[... truncated, total ${value.length} chars]` : value;
|
|
11579
12089
|
const settle = (id, rawOutput, success, outcome) => {
|
|
@@ -11613,6 +12123,9 @@ var CliAgentAiService = class {
|
|
|
11613
12123
|
case "tool_result":
|
|
11614
12124
|
settle(event.id, event.output, event.success, event.success ? "success" : "failure");
|
|
11615
12125
|
return;
|
|
12126
|
+
case "background_agent":
|
|
12127
|
+
backgroundAgents++;
|
|
12128
|
+
return;
|
|
11616
12129
|
case "permission_request": {
|
|
11617
12130
|
const mapped = mapAgentTool(event.name);
|
|
11618
12131
|
const args = JSON.stringify({ detail: event.detail });
|
|
@@ -11643,7 +12156,7 @@ var CliAgentAiService = class {
|
|
|
11643
12156
|
adapter.dispose();
|
|
11644
12157
|
this.adapter = null;
|
|
11645
12158
|
}
|
|
11646
|
-
return { text, researchLog, error, aborted: signal?.aborted };
|
|
12159
|
+
return { text, researchLog, error, aborted: signal?.aborted, backgroundAgents };
|
|
11647
12160
|
}
|
|
11648
12161
|
// --- Process lifecycle ---
|
|
11649
12162
|
async startAdapter(opts) {
|
|
@@ -12250,6 +12763,12 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
|
12250
12763
|
}
|
|
12251
12764
|
|
|
12252
12765
|
// src/services/createSession.ts
|
|
12766
|
+
var CATALOG_MODEL_CAP = 100;
|
|
12767
|
+
var MAX_TASK_QUERIES = 3;
|
|
12768
|
+
var MAX_TASK_QUERIES_HARD = 6;
|
|
12769
|
+
function freshReadBudget() {
|
|
12770
|
+
return { answered: 0, seen: /* @__PURE__ */ new Set() };
|
|
12771
|
+
}
|
|
12253
12772
|
var PlanEditError = class extends Error {
|
|
12254
12773
|
constructor(message) {
|
|
12255
12774
|
super(message);
|
|
@@ -12486,6 +13005,21 @@ var Session = class {
|
|
|
12486
13005
|
runnerModesFor(runners) {
|
|
12487
13006
|
return runnerModesFrom(this.registry, runners);
|
|
12488
13007
|
}
|
|
13008
|
+
/**
|
|
13009
|
+
* The catalog a model/task-mode edit is checked against — the same
|
|
13010
|
+
* discovered models and manifest modes the per-turn catalog block shows the
|
|
13011
|
+
* planner, so a refusal here can never name something invalid that the
|
|
13012
|
+
* planner was never told about. `modelsCache` is read live, not filtered by
|
|
13013
|
+
* the allowlist here — {@link checkModelAndModeValidity} narrows by
|
|
13014
|
+
* allowlist itself, the same way `coerceAssignments` does.
|
|
13015
|
+
*/
|
|
13016
|
+
editCatalog() {
|
|
13017
|
+
return {
|
|
13018
|
+
modelsByRunner: this.modelsCache,
|
|
13019
|
+
runnerModes: this.runnerModesFor(this.plan?.runners ?? []),
|
|
13020
|
+
perRunnerAllowlist: this.settingsFn().modelAllowlist ?? this.currentAllowlist
|
|
13021
|
+
};
|
|
13022
|
+
}
|
|
12489
13023
|
get planState() {
|
|
12490
13024
|
return this.plan;
|
|
12491
13025
|
}
|
|
@@ -12665,10 +13199,12 @@ var Session = class {
|
|
|
12665
13199
|
async continueConversation(userMessage, options) {
|
|
12666
13200
|
if (!this.plan) throw new Error("No planning conversation to continue");
|
|
12667
13201
|
const priorHistory = this.plan.conversationHistory ?? [];
|
|
13202
|
+
const priorResearchLog = this.plan.researchLog ?? [];
|
|
12668
13203
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
12669
13204
|
this.appendTranscript("user", userMessage, now);
|
|
13205
|
+
const historyAfterOwnAppend = this.plan.conversationHistory;
|
|
12670
13206
|
this.plan.researchLog = [...this.plan.researchLog ?? [], { id: `up-${Date.now()}`, type: "user_prompt", content: userMessage, timestamp: now }];
|
|
12671
|
-
const contextBlock = this.planContextBlock();
|
|
13207
|
+
const contextBlock = [this.catalogBlock(), this.planContextBlock()].filter(Boolean).join("\n\n");
|
|
12672
13208
|
const outgoing = contextBlock ? `${contextBlock}
|
|
12673
13209
|
|
|
12674
13210
|
${userMessage}` : userMessage;
|
|
@@ -12681,6 +13217,8 @@ ${userMessage}` : userMessage;
|
|
|
12681
13217
|
(p) => this.translateProgress(p),
|
|
12682
13218
|
options?.signal
|
|
12683
13219
|
) : await this.resumeConversation(outgoing, priorHistory, options);
|
|
13220
|
+
const reads = freshReadBudget();
|
|
13221
|
+
turn = await this.drainTaskQueries(turn, reads, options);
|
|
12684
13222
|
if ((turn.kind === "task_ops" || turn.kind === "plan") && this.hasLiveWork) {
|
|
12685
13223
|
this.queueMessage(userMessage);
|
|
12686
13224
|
this.plan.queuedMessages = this.getQueuedMessages();
|
|
@@ -12690,11 +13228,74 @@ ${userMessage}` : userMessage;
|
|
|
12690
13228
|
researchLog: turn.researchLog
|
|
12691
13229
|
};
|
|
12692
13230
|
}
|
|
12693
|
-
return await this.settleTurn(turn, options);
|
|
13231
|
+
return await this.settleTurn(turn, options, reads);
|
|
13232
|
+
} catch (err) {
|
|
13233
|
+
if (this.plan && this.plan.conversationHistory === historyAfterOwnAppend) {
|
|
13234
|
+
this.plan.conversationHistory = priorHistory;
|
|
13235
|
+
this.plan.researchLog = priorResearchLog;
|
|
13236
|
+
}
|
|
13237
|
+
throw err;
|
|
12694
13238
|
} finally {
|
|
12695
13239
|
releaseAbort();
|
|
12696
13240
|
}
|
|
12697
13241
|
}
|
|
13242
|
+
/**
|
|
13243
|
+
* Answer every read the planner emits until it says something else.
|
|
13244
|
+
*
|
|
13245
|
+
* The channel is a text envelope rather than a registered tool because the
|
|
13246
|
+
* protocol has to be identical on both planner backends (ADR-0009): Ordewell
|
|
13247
|
+
* owns a tool loop only in the API case, and a harness planner running as a
|
|
13248
|
+
* coding-agent subprocess can only be reached this way.
|
|
13249
|
+
*
|
|
13250
|
+
* Draining here — outside {@link repairLoop} — is what keeps reads free of
|
|
13251
|
+
* the repair budget. A planner that looks a task up and *then* fumbles its
|
|
13252
|
+
* ops JSON still gets its two corrective retries; charging it for the read
|
|
13253
|
+
* would cost it the chance to fix the edit.
|
|
13254
|
+
*/
|
|
13255
|
+
async drainTaskQueries(turn, reads, options) {
|
|
13256
|
+
const carried = [];
|
|
13257
|
+
let current = turn;
|
|
13258
|
+
while (current.kind === "task_query") {
|
|
13259
|
+
carried.push(...current.researchLog);
|
|
13260
|
+
if (reads.answered >= MAX_TASK_QUERIES_HARD || !this.aiService.hasActiveConversation() || options?.signal?.aborted) {
|
|
13261
|
+
return {
|
|
13262
|
+
kind: "message",
|
|
13263
|
+
text: "The planner kept asking to read tasks instead of answering. Nothing was changed \u2014 ask again, or be more specific about the edit you want.",
|
|
13264
|
+
researchLog: carried
|
|
13265
|
+
};
|
|
13266
|
+
}
|
|
13267
|
+
const signature = taskQuerySignature(current.query);
|
|
13268
|
+
const insist = reads.seen.has(signature) || reads.answered >= MAX_TASK_QUERIES;
|
|
13269
|
+
reads.seen.add(signature);
|
|
13270
|
+
reads.answered++;
|
|
13271
|
+
const answer = this.taskQueryAnswer(current.query);
|
|
13272
|
+
current = await this.aiService.continueConversation(
|
|
13273
|
+
insist ? `${answer}
|
|
13274
|
+
|
|
13275
|
+
${TASK_QUERY_ANSWER_OR_OPS}` : answer,
|
|
13276
|
+
(p) => this.translateProgress(p),
|
|
13277
|
+
options?.signal
|
|
13278
|
+
);
|
|
13279
|
+
}
|
|
13280
|
+
return carried.length > 0 ? { ...current, researchLog: [...carried, ...current.researchLog] } : current;
|
|
13281
|
+
}
|
|
13282
|
+
/**
|
|
13283
|
+
* Render one read out of live state. Never persisted to the transcript: the
|
|
13284
|
+
* detail is context for the planner's next reply, and re-sending it on every
|
|
13285
|
+
* later turn is exactly the cost this channel exists to avoid.
|
|
13286
|
+
*/
|
|
13287
|
+
taskQueryAnswer(query) {
|
|
13288
|
+
const runners = this.plan?.runners ?? [];
|
|
13289
|
+
const allowlist = this.settingsFn().modelAllowlist ?? this.currentAllowlist;
|
|
13290
|
+
return renderTaskQueryAnswer(query, this.store.planTasks, {
|
|
13291
|
+
runners,
|
|
13292
|
+
// Allowlist-filtered like the always-on block: a read must not offer a
|
|
13293
|
+
// model the planner is forbidden to assign.
|
|
13294
|
+
models: filterModelsForPrompt(this.modelsCache, allowlist),
|
|
13295
|
+
modes: this.runnerModesFor(runners),
|
|
13296
|
+
autonomousDefault: this.config.autonomousMode
|
|
13297
|
+
});
|
|
13298
|
+
}
|
|
12698
13299
|
/**
|
|
12699
13300
|
* Drive a planner turn to a persisted, broadcast outcome. Task edits apply
|
|
12700
13301
|
* atomically; validation failures are fed back to the model for up to 2
|
|
@@ -12702,7 +13303,7 @@ ${userMessage}` : userMessage;
|
|
|
12702
13303
|
* first turn (startPlanning) and every later turn route through here — one
|
|
12703
13304
|
* path, not two.
|
|
12704
13305
|
*/
|
|
12705
|
-
async settleTurn(turn, options) {
|
|
13306
|
+
async settleTurn(turn, options, reads = freshReadBudget()) {
|
|
12706
13307
|
const invalidOps = (errors, researchLog) => ({
|
|
12707
13308
|
turn: {
|
|
12708
13309
|
kind: "message",
|
|
@@ -12714,11 +13315,15 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
12714
13315
|
}
|
|
12715
13316
|
});
|
|
12716
13317
|
const settled = await repairLoop({
|
|
12717
|
-
first: () => turn,
|
|
12718
|
-
resend: (corrective) => this.
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
13318
|
+
first: () => this.drainTaskQueries(turn, reads, options),
|
|
13319
|
+
resend: async (corrective) => this.drainTaskQueries(
|
|
13320
|
+
await this.aiService.continueConversation(
|
|
13321
|
+
corrective,
|
|
13322
|
+
(p) => this.translateProgress(p),
|
|
13323
|
+
options?.signal
|
|
13324
|
+
),
|
|
13325
|
+
reads,
|
|
13326
|
+
options
|
|
12722
13327
|
),
|
|
12723
13328
|
interpret: (t) => {
|
|
12724
13329
|
if (t.kind !== "task_ops") return { done: { turn: t } };
|
|
@@ -12738,6 +13343,52 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
12738
13343
|
}
|
|
12739
13344
|
return this.applyConversationTurn(settled.turn);
|
|
12740
13345
|
}
|
|
13346
|
+
/**
|
|
13347
|
+
* The catalog the planner may actually draw from — model ids and task-mode
|
|
13348
|
+
* ids, per selected runner — emitted on EVERY turn, before any plan exists
|
|
13349
|
+
* or after. The system prompt shows this once at conversation start; a long
|
|
13350
|
+
* grill-me interview or PRD negotiation outlives that single showing and the
|
|
13351
|
+
* planner starts misquoting it, so this re-states it per turn instead.
|
|
13352
|
+
*
|
|
13353
|
+
* Built from the allowlist-filtered view (`filterModelsForPrompt`), the same
|
|
13354
|
+
* one the system prompt used — a restricted allowlist stays a hard bound on
|
|
13355
|
+
* every turn, not just the first. `this.modelsCache` itself is never
|
|
13356
|
+
* filtered: `coerceAssignments` needs the full discovered catalog alongside
|
|
13357
|
+
* the allowlist to resolve labels and clamp effort to real variants.
|
|
13358
|
+
*
|
|
13359
|
+
* Reads `this.plan.runners` live, so a runner admitted mid-session by a
|
|
13360
|
+
* retarget (`admitRunner`) is filtered and shown like every other — there is
|
|
13361
|
+
* no separate "originally selected" list to fall stale.
|
|
13362
|
+
*/
|
|
13363
|
+
catalogBlock() {
|
|
13364
|
+
if (!this.plan) return null;
|
|
13365
|
+
const allowlist = this.settingsFn().modelAllowlist ?? this.currentAllowlist;
|
|
13366
|
+
const filteredModels = filterModelsForPrompt(this.modelsCache, allowlist);
|
|
13367
|
+
const runnerModes = this.runnerModesFor(this.plan.runners);
|
|
13368
|
+
const autonomousDefault = this.config.autonomousMode;
|
|
13369
|
+
const modelLines = this.plan.runners.map((runner) => {
|
|
13370
|
+
const models = filteredModels[runner] ?? [];
|
|
13371
|
+
const capped = models.slice(0, CATALOG_MODEL_CAP);
|
|
13372
|
+
const remainder = models.length - capped.length;
|
|
13373
|
+
const ids = capped.map((m) => m.modelId).join(", ") || "(no models discovered)";
|
|
13374
|
+
return `${runner}: ${ids}${remainder > 0 ? ` \u2026 +${remainder} more not shown` : ""}`;
|
|
13375
|
+
});
|
|
13376
|
+
const modeLines = this.plan.runners.map((runner) => {
|
|
13377
|
+
const modes = runnerModes[runner] ?? [];
|
|
13378
|
+
if (modes.length === 0) return `${runner}: (no modes declared)`;
|
|
13379
|
+
const defaultId = resolveDefaultMode(modes, autonomousDefault);
|
|
13380
|
+
const ids = modes.map((m) => `${m.id}${m.id === defaultId ? " (default)" : ""}`).join(", ");
|
|
13381
|
+
return `${runner}: ${ids}`;
|
|
13382
|
+
});
|
|
13383
|
+
return [
|
|
13384
|
+
"<available_models>",
|
|
13385
|
+
...modelLines,
|
|
13386
|
+
"</available_models>",
|
|
13387
|
+
"<available_task_modes>",
|
|
13388
|
+
...modeLines,
|
|
13389
|
+
"</available_task_modes>"
|
|
13390
|
+
].join("\n");
|
|
13391
|
+
}
|
|
12741
13392
|
/**
|
|
12742
13393
|
* The "you are here" block for post-plan chat: current tasks with stable
|
|
12743
13394
|
* references, plus the task-ops protocol. Injected per turn (never
|
|
@@ -12747,44 +13398,48 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
12747
13398
|
planContextBlock() {
|
|
12748
13399
|
if (!this.plan || this.store.planTasks.length === 0) return null;
|
|
12749
13400
|
const orderOf = new Map(this.store.planTasks.map((t) => [t.id, `#${t.order}`]));
|
|
12750
|
-
const lines = this.store.planTasks.map(
|
|
12751
|
-
|
|
12752
|
-
|
|
13401
|
+
const lines = this.store.planTasks.map((t) => {
|
|
13402
|
+
const isMan = t.type === "user";
|
|
13403
|
+
const runFields = isMan ? `steps:${t.userSteps?.length ?? 0}` : `${t.assignedModel ? `model:${t.assignedModel.modelId} ` : ""}mode:${t.taskMode ?? "build"} effort:${t.thinkingEffort ?? "-"}`;
|
|
13404
|
+
return `#${t.order} id=${t.id} "${t.title}" [${t.status}] type:${isMan ? "MAN" : "AI"} runner:${t.assignedRunner} ${runFields} autonomy:${t.autonomy ?? "-"} slice:${t.sliceType ?? "-"} deps:[${t.dependencies.map((d) => orderOf.get(d) ?? d).join(", ")}]`;
|
|
13405
|
+
});
|
|
12753
13406
|
const locked = this.store.planTasks.filter((t) => t.status === "in_progress");
|
|
12754
13407
|
const execNote = this.hasLiveWork ? `
|
|
12755
13408
|
Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.map((t) => `#${t.order}`).join(", ")}` : ""}. Any task edits you emit will be queued and applied between batches.` : "";
|
|
12756
|
-
const
|
|
12757
|
-
for (const runner of this.plan.runners) {
|
|
12758
|
-
const models = this.modelsCache[runner] ?? [];
|
|
12759
|
-
const ids = models.map((m) => m.modelId).join(", ");
|
|
12760
|
-
modelLines.push(`${runner}: ${ids || "(no models discovered)"}`);
|
|
12761
|
-
}
|
|
12762
|
-
const modelBlock = modelLines.length > 0 ? ["<available_models>", ...modelLines, "</available_models>"].join("\n") : "";
|
|
13409
|
+
const updateChanges = UPDATABLE_FIELDS.map((f) => f === "dependencies" ? '"dependencies"?:["<id or #order>"]' : `"${f}"?`).join(",");
|
|
12763
13410
|
return [
|
|
12764
13411
|
"<current_plan>",
|
|
12765
13412
|
...lines,
|
|
12766
13413
|
"</current_plan>",
|
|
12767
|
-
|
|
12768
|
-
"The block above is the CURRENT task plan. Choose how to respond:",
|
|
13414
|
+
"The block above is the CURRENT task plan \u2014 short fields only. Choose how to respond:",
|
|
12769
13415
|
"- To answer a question or discuss, reply in plain prose (no JSON).",
|
|
13416
|
+
// The reminder text is owned by TaskQuery beside the parser, so the
|
|
13417
|
+
// protocol the planner reads and the one Ordewell accepts stay one thing.
|
|
13418
|
+
TASK_QUERY_REMINDER,
|
|
12770
13419
|
"- To modify specific tasks, reply with ONLY this JSON object:",
|
|
12771
13420
|
' {"taskOps": [',
|
|
12772
|
-
|
|
12773
|
-
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?}},',
|
|
13421
|
+
` {"op":"update","taskId":"<id or #order>","changes":{${updateChanges}}},`,
|
|
13422
|
+
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?},"handle"?:"<name>"},',
|
|
12774
13423
|
' {"op":"remove","taskId":"<id or #order>"},',
|
|
12775
|
-
' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]},',
|
|
12776
|
-
' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}},',
|
|
12777
|
-
' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...]}',
|
|
13424
|
+
' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]}, // only to re-prioritise INDEPENDENT tasks',
|
|
13425
|
+
' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...},"handle"?:"<name>"},',
|
|
13426
|
+
' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...],"handle"?:"<name>"},',
|
|
13427
|
+
` {"op":"rearm","taskId":"<id or #order>","changes"?:{${updateChanges}}}`,
|
|
12778
13428
|
" ]}",
|
|
12779
13429
|
'- For sweeping changes, you may instead emit a full {"tasks":[...]} plan JSON.',
|
|
13430
|
+
'Every "<id or #order>" ref in a batch resolves against the plan shown above, before any op in the batch runs \u2014 an earlier remove/merge/split never shifts what a later "#N" means.',
|
|
13431
|
+
'Give "add", "merge", or "split" a "handle" (any name you choose, unused elsewhere in this batch) to let a LATER op in the same batch reference the task it produces \u2014 for "split", the handle names its last part. A handle used before its defining op is rejected.',
|
|
12780
13432
|
'When creating or changing tasks, set "assignedRunner" and "assignedModel" ({"modelId","modelLabel","thinkingEffort"?}) using only the runners and models listed in <available_models>.',
|
|
12781
|
-
|
|
13433
|
+
'Keep dependencies consistent: no cycles, no references to removed tasks, and never touch running or completed tasks \u2014 "rearm" is the one exception, below.',
|
|
13434
|
+
'Just declare the dependencies you want \u2014 display order is repaired for you afterwards, so a rewire or a newly added prerequisite never needs a "reorder" op. Only a task that is running or completed cannot be shifted, so an edit that would need one to move is rejected.' + execNote,
|
|
13435
|
+
'Flipping "type" between "ai" and "user" is a content change, not just a label: an update to "user" needs "userSteps" in the SAME op, and an update to "ai" needs "prompt" in the SAME op \u2014 the model/mode/effort/autonomy fields (flipping to "user") or the userSteps (flipping to "ai") are cleared automatically.',
|
|
13436
|
+
'"rearm" puts a failed OR completed task back to pending \u2014 verdict and output summary are cleared, any dependents it had blocked are released, and it may carry field changes (e.g. a corrected "prompt") applied in the same op. A running task cannot be re-armed. Never rearm a task just to relabel it \u2014 only when it should actually run again.'
|
|
12782
13437
|
].filter(Boolean).join("\n");
|
|
12783
13438
|
}
|
|
12784
13439
|
/** Validate + commit a task_ops turn atomically. Returns the errors on rejection (plan untouched). */
|
|
12785
13440
|
applyTaskOpsTurn(turn) {
|
|
12786
13441
|
if (!this.plan) throw new Error("No active plan state");
|
|
12787
|
-
const result = applyTaskOps(this.store.planTasks, turn.ops, this.plan.runners);
|
|
13442
|
+
const result = applyTaskOps(this.store.planTasks, turn.ops, this.plan.runners, this.editCatalog());
|
|
12788
13443
|
if (!result.ok) return { errors: result.errors };
|
|
12789
13444
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
12790
13445
|
const content = `Tasks updated:
|
|
@@ -13051,15 +13706,22 @@ Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.
|
|
|
13051
13706
|
return plan;
|
|
13052
13707
|
}
|
|
13053
13708
|
/**
|
|
13054
|
-
* Patch one task's fields. A hand-set dependency list
|
|
13055
|
-
*
|
|
13056
|
-
*
|
|
13057
|
-
*
|
|
13709
|
+
* Patch one task's fields. A hand-set dependency list, or a type flip
|
|
13710
|
+
* between AI and MAN, are the patches that can leave a task incoherent
|
|
13711
|
+
* (unschedulable, or carrying fields that mean nothing for its new type),
|
|
13712
|
+
* so both go through the same {@link validateTaskEdit} guard the planner's
|
|
13713
|
+
* task-ops applier uses (as the 'direct' actor, which skips the lock rule)
|
|
13714
|
+
* rather than a second copy of the rules — and throws, so the surface can
|
|
13715
|
+
* say why. Gated on the task existing so an edit to an unknown id still
|
|
13716
|
+
* falls through to the no-op `store.update` below instead of throwing.
|
|
13058
13717
|
*/
|
|
13059
13718
|
async updateTask(taskId, changes) {
|
|
13060
|
-
if (changes.dependencies) {
|
|
13061
|
-
const check =
|
|
13062
|
-
if (!check.ok) throw new PlanEditError(check.error ?? "Those
|
|
13719
|
+
if ((changes.dependencies || changes.type || changes.assignedModel || changes.taskMode) && this.store.get(taskId)) {
|
|
13720
|
+
const check = validateTaskEdit("direct", this.store.planTasks, taskId, changes, this.editCatalog());
|
|
13721
|
+
if (!check.ok) throw new PlanEditError(check.error ?? "Those changes are not valid");
|
|
13722
|
+
if (check.clear?.length) {
|
|
13723
|
+
changes = { ...changes, ...Object.fromEntries(check.clear.map((f) => [f, void 0])) };
|
|
13724
|
+
}
|
|
13063
13725
|
}
|
|
13064
13726
|
return this.editPlan(
|
|
13065
13727
|
() => Boolean(this.store.update(taskId, changes)),
|
|
@@ -15157,6 +15819,11 @@ var PlannerModelMemory = class {
|
|
|
15157
15819
|
SettingsService,
|
|
15158
15820
|
StdioAgentAdapter,
|
|
15159
15821
|
TASK_OPS_ENVELOPE_KEY,
|
|
15822
|
+
TASK_QUERY_ANSWER_OR_OPS,
|
|
15823
|
+
TASK_QUERY_ENVELOPE_KEY,
|
|
15824
|
+
TASK_QUERY_FIELDS,
|
|
15825
|
+
TASK_QUERY_PROTOCOL,
|
|
15826
|
+
TASK_QUERY_REMINDER,
|
|
15160
15827
|
TRUNCATED_PLAN_REPAIR_INSTRUCTION,
|
|
15161
15828
|
TaskOrchestrator,
|
|
15162
15829
|
TmuxRunner,
|
|
@@ -15277,6 +15944,7 @@ var PlannerModelMemory = class {
|
|
|
15277
15944
|
parsePartialPlan,
|
|
15278
15945
|
parsePlanJson,
|
|
15279
15946
|
parseTaskOpsJson,
|
|
15947
|
+
parseTaskQueryJson,
|
|
15280
15948
|
pendingEditRulesBlock,
|
|
15281
15949
|
planDirectLaunch,
|
|
15282
15950
|
planShellLaunch,
|
|
@@ -15285,11 +15953,13 @@ var PlannerModelMemory = class {
|
|
|
15285
15953
|
providerForRunner,
|
|
15286
15954
|
reEmitPlanPrompt,
|
|
15287
15955
|
reEmitTaskOpsPrompt,
|
|
15956
|
+
reEmitTaskQueryPrompt,
|
|
15288
15957
|
readDaemonToken,
|
|
15289
15958
|
referencePattern,
|
|
15290
15959
|
removeTaskFromPlan,
|
|
15291
15960
|
renderPlanMap,
|
|
15292
15961
|
renderPriorOutputs,
|
|
15962
|
+
renderTaskQueryAnswer,
|
|
15293
15963
|
renumberTasks,
|
|
15294
15964
|
repairLoop,
|
|
15295
15965
|
researchShellWarning,
|
|
@@ -15323,7 +15993,9 @@ var PlannerModelMemory = class {
|
|
|
15323
15993
|
summarizeOutput,
|
|
15324
15994
|
summarizeToolCall,
|
|
15325
15995
|
taskOpsRejectedPrompt,
|
|
15996
|
+
taskQuerySignature,
|
|
15326
15997
|
textHasTaskOps,
|
|
15998
|
+
textHasTaskQuery,
|
|
15327
15999
|
tmuxSessionName,
|
|
15328
16000
|
tmuxSocketName,
|
|
15329
16001
|
tmuxWindowName,
|