@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.mjs
CHANGED
|
@@ -13,23 +13,30 @@ import {
|
|
|
13
13
|
resolveTaskMode,
|
|
14
14
|
runnerModesFrom,
|
|
15
15
|
validatePlanModification
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-TUGJXD4G.mjs";
|
|
17
17
|
import {
|
|
18
|
+
UPDATABLE_FIELDS,
|
|
18
19
|
applyTaskOps,
|
|
19
20
|
canMergeTasks,
|
|
20
21
|
canSetDependencies,
|
|
21
22
|
canSplitTask,
|
|
23
|
+
clampThinkingEffort,
|
|
22
24
|
classifyOutcome,
|
|
25
|
+
coerceAssignments,
|
|
23
26
|
dependencyCandidates,
|
|
24
27
|
dependentsOf,
|
|
28
|
+
effectiveAllowlist,
|
|
29
|
+
filterModelsForPrompt,
|
|
25
30
|
parseTaskOpsJson,
|
|
26
31
|
summarizeToolCall,
|
|
27
|
-
textHasTaskOps
|
|
28
|
-
|
|
32
|
+
textHasTaskOps,
|
|
33
|
+
validateTaskEdit
|
|
34
|
+
} from "./chunk-YI4ZDZEE.mjs";
|
|
29
35
|
import {
|
|
30
36
|
PLAN_ENVELOPE_KEY,
|
|
31
37
|
PlanParseError,
|
|
32
38
|
TASK_OPS_ENVELOPE_KEY,
|
|
39
|
+
TASK_QUERY_ENVELOPE_KEY,
|
|
33
40
|
addTaskToPlan,
|
|
34
41
|
createEmptyPlan,
|
|
35
42
|
createTask,
|
|
@@ -49,7 +56,7 @@ import {
|
|
|
49
56
|
updateTaskInPlan,
|
|
50
57
|
validateModifiedPlan,
|
|
51
58
|
warningsText
|
|
52
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-IO6PEEN6.mjs";
|
|
53
60
|
|
|
54
61
|
// src/interfaces/IFileSystem.ts
|
|
55
62
|
var GREP_DEFAULT_HEAD_LIMIT = 100;
|
|
@@ -3766,6 +3773,177 @@ function modesFor(scope, modes) {
|
|
|
3766
3773
|
return scoped;
|
|
3767
3774
|
}
|
|
3768
3775
|
|
|
3776
|
+
// src/services/TaskQuery.ts
|
|
3777
|
+
var TASK_QUERY_FIELDS = [
|
|
3778
|
+
"description",
|
|
3779
|
+
"prompt",
|
|
3780
|
+
"userSteps",
|
|
3781
|
+
"verdict",
|
|
3782
|
+
"outputSummary",
|
|
3783
|
+
"userStoriesCovered"
|
|
3784
|
+
];
|
|
3785
|
+
function textHasTaskQuery(text) {
|
|
3786
|
+
return text.includes(`"${TASK_QUERY_ENVELOPE_KEY}"`);
|
|
3787
|
+
}
|
|
3788
|
+
function parseTaskQueryJson(text) {
|
|
3789
|
+
const { matches, fallback } = extractObjectsWithKey(text, TASK_QUERY_ENVELOPE_KEY);
|
|
3790
|
+
const candidates = matches.length > 0 ? [...matches].reverse() : [fallback.json];
|
|
3791
|
+
let lastError = null;
|
|
3792
|
+
for (const json of candidates) {
|
|
3793
|
+
try {
|
|
3794
|
+
return parseTaskQueryObject(json, text);
|
|
3795
|
+
} catch (err) {
|
|
3796
|
+
if (!(err instanceof PlanParseError)) throw err;
|
|
3797
|
+
if (!lastError) lastError = err;
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
throw lastError ?? new PlanParseError("No JSON object found in task-query reply", text);
|
|
3801
|
+
}
|
|
3802
|
+
function parseTaskQueryObject(json, text) {
|
|
3803
|
+
let parsed;
|
|
3804
|
+
try {
|
|
3805
|
+
parsed = JSON.parse(escapeControlCharsInStrings(stripTrailingCommas(json)));
|
|
3806
|
+
} catch (err) {
|
|
3807
|
+
throw new PlanParseError(`Task-query JSON is invalid: ${err instanceof Error ? err.message : String(err)}`, text);
|
|
3808
|
+
}
|
|
3809
|
+
const raw = parsed.taskQuery;
|
|
3810
|
+
const body = Array.isArray(raw) ? { tasks: raw } : raw;
|
|
3811
|
+
if (typeof body !== "object" || body === null) {
|
|
3812
|
+
throw new PlanParseError('"taskQuery" must be an object (or an array of task references)', text);
|
|
3813
|
+
}
|
|
3814
|
+
const { tasks: rawTasks, fields: rawFields, catalog: rawCatalog } = body;
|
|
3815
|
+
const tasks = [];
|
|
3816
|
+
if (rawTasks !== void 0) {
|
|
3817
|
+
if (!Array.isArray(rawTasks)) throw new PlanParseError('"tasks" in a taskQuery must be an array of task references', text);
|
|
3818
|
+
for (const ref of rawTasks) {
|
|
3819
|
+
if (typeof ref !== "string" && typeof ref !== "number") {
|
|
3820
|
+
throw new PlanParseError(`Invalid task reference "${String(ref)}" in taskQuery`, text);
|
|
3821
|
+
}
|
|
3822
|
+
tasks.push(String(ref).trim());
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
let fields;
|
|
3826
|
+
if (rawFields !== void 0) {
|
|
3827
|
+
if (!Array.isArray(rawFields)) throw new PlanParseError('"fields" in a taskQuery must be an array of field names', text);
|
|
3828
|
+
fields = [];
|
|
3829
|
+
for (const f of rawFields) {
|
|
3830
|
+
if (typeof f !== "string" || !TASK_QUERY_FIELDS.includes(f)) {
|
|
3831
|
+
throw new PlanParseError(`Unknown taskQuery field "${String(f)}" \u2014 choose from ${TASK_QUERY_FIELDS.join(", ")}`, text);
|
|
3832
|
+
}
|
|
3833
|
+
fields.push(f);
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
const catalog = rawCatalog === true;
|
|
3837
|
+
if (tasks.length === 0 && !catalog) {
|
|
3838
|
+
throw new PlanParseError('A taskQuery must name at least one task or set "catalog": true', text);
|
|
3839
|
+
}
|
|
3840
|
+
return { tasks, fields, catalog };
|
|
3841
|
+
}
|
|
3842
|
+
function taskQuerySignature(query) {
|
|
3843
|
+
return JSON.stringify([query.tasks, query.fields ?? null, query.catalog]);
|
|
3844
|
+
}
|
|
3845
|
+
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.";
|
|
3846
|
+
var TASK_QUERY_PROTOCOL = [
|
|
3847
|
+
"READING A TASK BEFORE YOU EDIT IT:",
|
|
3848
|
+
"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:",
|
|
3849
|
+
` {"${TASK_QUERY_ENVELOPE_KEY}":{"tasks":["<id or #order>", "..."],"fields":[${TASK_QUERY_FIELDS.map((f) => `"${f}"`).join(", ")}],"catalog":true}}`,
|
|
3850
|
+
'- "tasks": one or more task references. Ask for everything you need in ONE query \u2014 each query costs a round-trip.',
|
|
3851
|
+
'- "fields": optional. Omit it to get every field above.',
|
|
3852
|
+
'- "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.',
|
|
3853
|
+
"Ordewell answers immediately with the detail and changes nothing. Then reply again with your taskOps JSON, or with prose for the user.",
|
|
3854
|
+
"Three queries per user message; after that every answer also tells you to land the turn. Do not ask the same question twice."
|
|
3855
|
+
];
|
|
3856
|
+
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.`;
|
|
3857
|
+
function findTask(tasks, ref) {
|
|
3858
|
+
const byId = tasks.find((t) => t.id === ref);
|
|
3859
|
+
if (byId) return byId;
|
|
3860
|
+
const orderStr = ref.startsWith("#") ? ref.slice(1) : ref;
|
|
3861
|
+
if (/^\d+$/.test(orderStr)) {
|
|
3862
|
+
const byOrder = tasks.filter((t) => t.order === Number(orderStr));
|
|
3863
|
+
if (byOrder.length === 1) return byOrder[0];
|
|
3864
|
+
}
|
|
3865
|
+
return tasks.find((t) => t.title === ref);
|
|
3866
|
+
}
|
|
3867
|
+
function renderField(task, field) {
|
|
3868
|
+
switch (field) {
|
|
3869
|
+
case "description":
|
|
3870
|
+
return [`description: ${task.description || "(none)"}`];
|
|
3871
|
+
case "prompt":
|
|
3872
|
+
return task.prompt ? ["prompt:", task.prompt] : ["prompt: (none)"];
|
|
3873
|
+
case "userSteps": {
|
|
3874
|
+
const steps = task.userSteps ?? [];
|
|
3875
|
+
if (steps.length === 0) return ["userSteps: (none)"];
|
|
3876
|
+
return ["userSteps:", ...steps.map((s) => ` ${s.order}. [${s.completed ? "x" : " "}] ${s.instruction}`)];
|
|
3877
|
+
}
|
|
3878
|
+
case "verdict": {
|
|
3879
|
+
const v = task.verdict;
|
|
3880
|
+
if (!v) return ["verdict: (none)"];
|
|
3881
|
+
const checks = v.checks.map((c) => `${c.name}=${c.skipped ? "skipped" : c.passed ? "pass" : "fail"}`).join(", ");
|
|
3882
|
+
return [`verdict: ${v.outcome.toUpperCase()} \u2014 ${v.reason}${checks ? ` [${checks}]` : ""}`];
|
|
3883
|
+
}
|
|
3884
|
+
case "outputSummary": {
|
|
3885
|
+
const o = task.outputSummary;
|
|
3886
|
+
if (!o) return ["outputSummary: (none)"];
|
|
3887
|
+
return [`outputSummary: ${o.reviewReason}`, ...o.logTail ? ["log tail:", o.logTail] : []];
|
|
3888
|
+
}
|
|
3889
|
+
case "userStoriesCovered": {
|
|
3890
|
+
const stories = task.userStoriesCovered ?? [];
|
|
3891
|
+
if (stories.length === 0) return ["userStoriesCovered: (none)"];
|
|
3892
|
+
return ["userStoriesCovered:", ...stories.map((s) => ` - ${s}`)];
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
function renderTask(task, fields) {
|
|
3897
|
+
return [
|
|
3898
|
+
`#${task.order} id=${task.id} "${task.title}" [${task.status}] type:${task.type === "user" ? "MAN" : "AI"}`,
|
|
3899
|
+
...fields.flatMap((f) => renderField(task, f)),
|
|
3900
|
+
""
|
|
3901
|
+
];
|
|
3902
|
+
}
|
|
3903
|
+
function renderCatalog(catalog) {
|
|
3904
|
+
const lines = [];
|
|
3905
|
+
for (const runner of catalog.runners) {
|
|
3906
|
+
const models = catalog.models[runner] ?? [];
|
|
3907
|
+
lines.push(`${runner} models:`);
|
|
3908
|
+
if (models.length === 0) lines.push(" (none discovered)");
|
|
3909
|
+
for (const m of models) {
|
|
3910
|
+
const variants = m.variants?.length ? ` variants: ${m.variants.map((v) => `${v.id}${v.label && v.label !== v.id ? ` (${v.label})` : ""}`).join(", ")}` : " variants: none";
|
|
3911
|
+
lines.push(` - ${m.modelId} \u2014 ${m.modelLabel}${variants}`);
|
|
3912
|
+
}
|
|
3913
|
+
const modes = catalog.modes[runner] ?? [];
|
|
3914
|
+
lines.push(`${runner} task modes:`);
|
|
3915
|
+
if (modes.length === 0) lines.push(" (none declared)");
|
|
3916
|
+
const defaultId = resolveDefaultMode(modes, catalog.autonomousDefault);
|
|
3917
|
+
for (const mode of modes) {
|
|
3918
|
+
lines.push(` - ${mode.id} \u2014 ${mode.label}: ${mode.description}${mode.id === defaultId ? " (default)" : ""}`);
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
return lines;
|
|
3922
|
+
}
|
|
3923
|
+
function renderTaskQueryAnswer(query, tasks, catalog) {
|
|
3924
|
+
const fields = query.fields ?? TASK_QUERY_FIELDS;
|
|
3925
|
+
const blocks = [];
|
|
3926
|
+
if (query.tasks.length > 0) {
|
|
3927
|
+
const body = [];
|
|
3928
|
+
for (const ref of query.tasks) {
|
|
3929
|
+
const task = findTask(tasks, ref);
|
|
3930
|
+
if (!task) {
|
|
3931
|
+
body.push(`${ref}: no task matches this reference in the current plan.`, "");
|
|
3932
|
+
continue;
|
|
3933
|
+
}
|
|
3934
|
+
body.push(...renderTask(task, fields));
|
|
3935
|
+
}
|
|
3936
|
+
blocks.push(["<task_detail>", ...body, "</task_detail>"].join("\n"));
|
|
3937
|
+
}
|
|
3938
|
+
if (query.catalog) {
|
|
3939
|
+
blocks.push(["<runner_catalog>", ...renderCatalog(catalog), "</runner_catalog>"].join("\n"));
|
|
3940
|
+
}
|
|
3941
|
+
blocks.push(
|
|
3942
|
+
"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."
|
|
3943
|
+
);
|
|
3944
|
+
return blocks.join("\n\n");
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3769
3947
|
// src/services/PlanPrompts.ts
|
|
3770
3948
|
function buildResearchToolsPrompt(subagentsEnabled = false) {
|
|
3771
3949
|
const lines = [
|
|
@@ -3855,7 +4033,8 @@ function researchPhaseBlock(harnessMode) {
|
|
|
3855
4033
|
];
|
|
3856
4034
|
const toolSpecific = harnessMode ? [
|
|
3857
4035
|
"- Explore the repository with your own tools until you understand its architecture",
|
|
3858
|
-
"- You are planning, not implementing: do NOT edit, create, or delete any file, and do not run commands that change the workspace"
|
|
4036
|
+
"- You are planning, not implementing: do NOT edit, create, or delete any file, and do not run commands that change the workspace",
|
|
4037
|
+
"- 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."
|
|
3859
4038
|
] : [
|
|
3860
4039
|
"- Use list_dir with depth=2-3 for a tree overview",
|
|
3861
4040
|
"- Use glob and grep to find relevant source files",
|
|
@@ -4020,6 +4199,11 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
4020
4199
|
"TASK MODE:",
|
|
4021
4200
|
modeGuide,
|
|
4022
4201
|
"",
|
|
4202
|
+
// Shared by both variants on purpose: the read channel is a text envelope
|
|
4203
|
+
// exactly so a harness planner, which Ordewell cannot hand tools to, speaks
|
|
4204
|
+
// the same protocol as an API-backed one (ADR-0009).
|
|
4205
|
+
...TASK_QUERY_PROTOCOL,
|
|
4206
|
+
"",
|
|
4023
4207
|
context ? `PROJECT CONTEXT:
|
|
4024
4208
|
${context}
|
|
4025
4209
|
` : "",
|
|
@@ -4327,7 +4511,8 @@ function buildMergePrompt(taskIds, tasks) {
|
|
|
4327
4511
|
"The merged task takes the union of their dependencies (excluding the merged tasks themselves) and preserves their user stories.",
|
|
4328
4512
|
'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.',
|
|
4329
4513
|
'Reply with ONLY a taskOps JSON object using a single "merge" op:',
|
|
4330
|
-
` {"taskOps":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}
|
|
4514
|
+
` {"taskOps":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}`,
|
|
4515
|
+
`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.`
|
|
4331
4516
|
].join("\n");
|
|
4332
4517
|
}
|
|
4333
4518
|
function buildSplitPrompt(taskId, tasks) {
|
|
@@ -4339,7 +4524,8 @@ function buildSplitPrompt(taskId, tasks) {
|
|
|
4339
4524
|
"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.",
|
|
4340
4525
|
'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).',
|
|
4341
4526
|
'Reply with ONLY a taskOps JSON object using a single "split" op:',
|
|
4342
|
-
` {"taskOps":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}
|
|
4527
|
+
` {"taskOps":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}`,
|
|
4528
|
+
`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.`
|
|
4343
4529
|
].join("\n");
|
|
4344
4530
|
}
|
|
4345
4531
|
|
|
@@ -4366,6 +4552,9 @@ function truncatedPlanReEmitPrompt(historyCompacted) {
|
|
|
4366
4552
|
function reEmitTaskOpsPrompt(detail) {
|
|
4367
4553
|
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.`;
|
|
4368
4554
|
}
|
|
4555
|
+
function reEmitTaskQueryPrompt(detail) {
|
|
4556
|
+
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.`;
|
|
4557
|
+
}
|
|
4369
4558
|
function taskOpsRejectedPrompt(errors) {
|
|
4370
4559
|
return `Those task edits were rejected:
|
|
4371
4560
|
- ${errors.join("\n- ")}
|
|
@@ -4392,6 +4581,16 @@ function classifyPlannerReply(text, opts) {
|
|
|
4392
4581
|
}
|
|
4393
4582
|
}
|
|
4394
4583
|
}
|
|
4584
|
+
if (textHasTaskQuery(text)) {
|
|
4585
|
+
try {
|
|
4586
|
+
return { kind: "task_query", query: parseTaskQueryJson(text) };
|
|
4587
|
+
} catch (err) {
|
|
4588
|
+
if (!(err instanceof PlanParseError)) throw err;
|
|
4589
|
+
if (extractObjectsWithKey(text, TASK_QUERY_ENVELOPE_KEY).matches.length > 0) {
|
|
4590
|
+
return { kind: "broken_task_query", error: err };
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4395
4594
|
if (text.includes(`"${PLAN_ENVELOPE_KEY}"`)) {
|
|
4396
4595
|
try {
|
|
4397
4596
|
return { kind: "plan", tasks: parsePlanJson(text, opts.runners, opts.runnerModes, opts.autonomousDefault) };
|
|
@@ -5121,6 +5320,10 @@ ${llmOutput}
|
|
|
5121
5320
|
switch (reply.kind) {
|
|
5122
5321
|
case "task_ops":
|
|
5123
5322
|
return { kind: "task_ops", ops: reply.ops, text: turn.text, researchLog };
|
|
5323
|
+
// A read is settled by the Session (it owns the plan and the catalog),
|
|
5324
|
+
// so it leaves this loop the same way a plan or an edit does.
|
|
5325
|
+
case "task_query":
|
|
5326
|
+
return { kind: "task_query", query: reply.query, text: turn.text, researchLog };
|
|
5124
5327
|
case "plan":
|
|
5125
5328
|
if (ctx.prdEnabled && !ctx.prdCaptured && !ctx.prdNudgeSent && !signal?.aborted) {
|
|
5126
5329
|
ctx.prdNudgeSent = true;
|
|
@@ -5143,6 +5346,13 @@ ${llmOutput}
|
|
|
5143
5346
|
continue;
|
|
5144
5347
|
}
|
|
5145
5348
|
break;
|
|
5349
|
+
case "broken_task_query":
|
|
5350
|
+
if (jsonRepairAttempts < MAX_JSON_REPAIRS2 && !signal?.aborted) {
|
|
5351
|
+
jsonRepairAttempts++;
|
|
5352
|
+
pending = reEmitTaskQueryPrompt(reply.error.message);
|
|
5353
|
+
continue;
|
|
5354
|
+
}
|
|
5355
|
+
break;
|
|
5146
5356
|
case "broken_plan":
|
|
5147
5357
|
if (jsonRepairAttempts < MAX_JSON_REPAIRS2 && !signal?.aborted) {
|
|
5148
5358
|
jsonRepairAttempts++;
|
|
@@ -7124,111 +7334,6 @@ ${output.slice(-3e3)}`);
|
|
|
7124
7334
|
}
|
|
7125
7335
|
};
|
|
7126
7336
|
|
|
7127
|
-
// src/services/ModelAllowlistResolver.ts
|
|
7128
|
-
function effectiveAllowlist(allowlist, runner, modelsByRunner) {
|
|
7129
|
-
if (!allowlist || allowlist.length === 0) return void 0;
|
|
7130
|
-
if (!modelsByRunner) return allowlist;
|
|
7131
|
-
const own = new Set((modelsByRunner[runner] ?? []).map((m) => m.modelId));
|
|
7132
|
-
const elsewhere = /* @__PURE__ */ new Set();
|
|
7133
|
-
for (const [other, models] of Object.entries(modelsByRunner)) {
|
|
7134
|
-
if (other === runner) continue;
|
|
7135
|
-
for (const m of models ?? []) elsewhere.add(m.modelId);
|
|
7136
|
-
}
|
|
7137
|
-
const kept = allowlist.filter((id) => own.has(id) || !elsewhere.has(id));
|
|
7138
|
-
return kept.length > 0 ? kept : void 0;
|
|
7139
|
-
}
|
|
7140
|
-
function filterModelsForPrompt(modelsByRunner, perRunnerAllowlist) {
|
|
7141
|
-
const result = {};
|
|
7142
|
-
for (const [runner, models] of Object.entries(modelsByRunner)) {
|
|
7143
|
-
if (!models) continue;
|
|
7144
|
-
const allowed = effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
7145
|
-
if (!allowed) {
|
|
7146
|
-
result[runner] = models;
|
|
7147
|
-
continue;
|
|
7148
|
-
}
|
|
7149
|
-
const allowedSet = new Set(allowed);
|
|
7150
|
-
const intersection = models.filter((m) => allowedSet.has(m.modelId));
|
|
7151
|
-
const covered = new Set(intersection.map((m) => m.modelId));
|
|
7152
|
-
const missing = allowed.filter((id) => !covered.has(id)).map((id) => ({ modelId: id, modelLabel: id.split("/").pop() || id, variants: [] }));
|
|
7153
|
-
result[runner] = [...intersection, ...missing];
|
|
7154
|
-
}
|
|
7155
|
-
return result;
|
|
7156
|
-
}
|
|
7157
|
-
var EFFORT_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
7158
|
-
var EFFORT_ALIASES = { disabled: "none", adaptive: "medium" };
|
|
7159
|
-
function clampThinkingEffort(effort, variants) {
|
|
7160
|
-
if (!effort) return void 0;
|
|
7161
|
-
if (variants.length === 0) return void 0;
|
|
7162
|
-
const ids = new Set(variants.map((v) => v.id));
|
|
7163
|
-
if (ids.has(effort)) return effort;
|
|
7164
|
-
const canon = EFFORT_ALIASES[effort] ?? effort;
|
|
7165
|
-
if (ids.has(canon)) return canon;
|
|
7166
|
-
const idx = EFFORT_LADDER.indexOf(canon);
|
|
7167
|
-
if (idx < 0) return void 0;
|
|
7168
|
-
for (let d = 1; d < EFFORT_LADDER.length; d++) {
|
|
7169
|
-
const lower = EFFORT_LADDER[idx - d];
|
|
7170
|
-
if (lower && ids.has(lower)) return lower;
|
|
7171
|
-
const higher = EFFORT_LADDER[idx + d];
|
|
7172
|
-
if (higher && ids.has(higher)) return higher;
|
|
7173
|
-
}
|
|
7174
|
-
return void 0;
|
|
7175
|
-
}
|
|
7176
|
-
function coerceAssignments(tasks, perRunnerAllowlist, allowedRunners, modelsByRunner) {
|
|
7177
|
-
const clampVariant = (task) => {
|
|
7178
|
-
if (!task.assignedModel || !modelsByRunner) return task;
|
|
7179
|
-
const model = modelsByRunner[task.assignedRunner]?.find(
|
|
7180
|
-
(m) => m.modelId === task.assignedModel.modelId
|
|
7181
|
-
);
|
|
7182
|
-
if (!model) return task;
|
|
7183
|
-
const clamped = clampThinkingEffort(task.assignedModel.thinkingEffort, model.variants);
|
|
7184
|
-
return {
|
|
7185
|
-
...task,
|
|
7186
|
-
assignedModel: {
|
|
7187
|
-
...task.assignedModel,
|
|
7188
|
-
thinkingEffort: clamped,
|
|
7189
|
-
availableVariants: model.variants.map((v) => v.id)
|
|
7190
|
-
}
|
|
7191
|
-
};
|
|
7192
|
-
};
|
|
7193
|
-
const allowedFor = (runner) => effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
7194
|
-
const assignmentFor = (runner, modelId) => {
|
|
7195
|
-
const known = modelsByRunner?.[runner]?.find((m) => m.modelId === modelId);
|
|
7196
|
-
return {
|
|
7197
|
-
modelId,
|
|
7198
|
-
modelLabel: known?.modelLabel ?? modelId,
|
|
7199
|
-
thinkingEffort: void 0,
|
|
7200
|
-
...known ? { availableVariants: known.variants.map((v) => v.id) } : {}
|
|
7201
|
-
};
|
|
7202
|
-
};
|
|
7203
|
-
return tasks.map((task) => {
|
|
7204
|
-
if (task.type === "user") return task;
|
|
7205
|
-
if (allowedRunners && !allowedRunners.includes(task.assignedRunner)) {
|
|
7206
|
-
const fallbackRunner = allowedRunners[0];
|
|
7207
|
-
const fallbackModels = allowedFor(fallbackRunner);
|
|
7208
|
-
const fallbackModel = fallbackModels?.length ? assignmentFor(fallbackRunner, fallbackModels[0]) : modelsByRunner?.[fallbackRunner]?.[0] ? assignmentFor(fallbackRunner, modelsByRunner[fallbackRunner][0].modelId) : task.assignedModel;
|
|
7209
|
-
return clampVariant({
|
|
7210
|
-
...task,
|
|
7211
|
-
assignedRunner: fallbackRunner,
|
|
7212
|
-
assignedModel: fallbackModel
|
|
7213
|
-
});
|
|
7214
|
-
}
|
|
7215
|
-
const allowlist = allowedFor(task.assignedRunner);
|
|
7216
|
-
if (!task.assignedModel) {
|
|
7217
|
-
if (allowlist && allowlist.length > 0) {
|
|
7218
|
-
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) });
|
|
7219
|
-
}
|
|
7220
|
-
const discovered = modelsByRunner?.[task.assignedRunner]?.[0];
|
|
7221
|
-
if (discovered) {
|
|
7222
|
-
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, discovered.modelId) });
|
|
7223
|
-
}
|
|
7224
|
-
return task;
|
|
7225
|
-
}
|
|
7226
|
-
if (!allowlist || allowlist.length === 0) return clampVariant(task);
|
|
7227
|
-
if (allowlist.includes(task.assignedModel.modelId)) return clampVariant(task);
|
|
7228
|
-
return { ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) };
|
|
7229
|
-
});
|
|
7230
|
-
}
|
|
7231
|
-
|
|
7232
7337
|
// src/services/Planner.ts
|
|
7233
7338
|
function plannerModesWith(req) {
|
|
7234
7339
|
const modes = req.modes ?? DEFAULT_PLANNER_MODES;
|
|
@@ -8940,12 +9045,19 @@ var StdioAgentAdapter = class {
|
|
|
8940
9045
|
/** Set for the duration of a turn. Outside one, events are buffered rather than dropped. */
|
|
8941
9046
|
turnEmit = null;
|
|
8942
9047
|
/**
|
|
8943
|
-
* Events the agent produced
|
|
8944
|
-
* that arrive during the handshake. Dropping them hid the one
|
|
8945
|
-
* that explains a planner which cannot read the workspace, so they
|
|
8946
|
-
* until a turn exists to show them in.
|
|
9048
|
+
* Events the agent produced before its first turn — in practice the startup
|
|
9049
|
+
* warnings that arrive during the handshake. Dropping them hid the one
|
|
9050
|
+
* diagnostic that explains a planner which cannot read the workspace, so they
|
|
9051
|
+
* are held until a turn exists to show them in.
|
|
9052
|
+
*
|
|
9053
|
+
* Only before the *first* turn. Once one has run, anything arriving outside a
|
|
9054
|
+
* turn is that closed turn's straggling work — a backgrounded subagent
|
|
9055
|
+
* finishing, a late read — and replaying it into the next turn presents it as
|
|
9056
|
+
* research done for the user's new message.
|
|
8947
9057
|
*/
|
|
8948
9058
|
betweenTurns = [];
|
|
9059
|
+
/** Set once a turn has ended: after that, out-of-turn events are stale, not startup. */
|
|
9060
|
+
hadTurn = false;
|
|
8949
9061
|
disposed = false;
|
|
8950
9062
|
/** Resolves when the process ends, so a handshake can lose the race instead of waiting out its timeout. */
|
|
8951
9063
|
processEnded;
|
|
@@ -8980,7 +9092,7 @@ var StdioAgentAdapter = class {
|
|
|
8980
9092
|
this.process.stdout?.on("data", (chunk) => {
|
|
8981
9093
|
this.stdout.push(chunk.toString(), (line) => this.handleLine(line, (event) => {
|
|
8982
9094
|
if (this.turnEmit) this.turnEmit(event);
|
|
8983
|
-
else if (!TURN_SCOPED_EVENTS.has(event.type) && this.betweenTurns.length < BETWEEN_TURN_EVENT_CAP) {
|
|
9095
|
+
else if (!this.hadTurn && !TURN_SCOPED_EVENTS.has(event.type) && this.betweenTurns.length < BETWEEN_TURN_EVENT_CAP) {
|
|
8984
9096
|
this.betweenTurns.push(event);
|
|
8985
9097
|
}
|
|
8986
9098
|
}));
|
|
@@ -9012,6 +9124,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
|
|
|
9012
9124
|
if (settled) return;
|
|
9013
9125
|
settled = true;
|
|
9014
9126
|
this.turnEmit = null;
|
|
9127
|
+
this.hadTurn = true;
|
|
9015
9128
|
signal?.removeEventListener("abort", onAbort);
|
|
9016
9129
|
this.process?.removeListener("exit", onExit);
|
|
9017
9130
|
this.process?.removeListener("error", onExit);
|
|
@@ -9090,6 +9203,7 @@ ${tail}` : ""}`;
|
|
|
9090
9203
|
|
|
9091
9204
|
// src/services/harness/ClaudeCodeAdapter.ts
|
|
9092
9205
|
var DISALLOWED_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit", "KillShell"];
|
|
9206
|
+
var ASYNC_LAUNCH_MARKER = "Async agent launched successfully";
|
|
9093
9207
|
function flattenContent(content) {
|
|
9094
9208
|
if (typeof content === "string") return content;
|
|
9095
9209
|
if (Array.isArray(content)) {
|
|
@@ -9099,6 +9213,8 @@ function flattenContent(content) {
|
|
|
9099
9213
|
}
|
|
9100
9214
|
var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
9101
9215
|
agentId = "claude-code";
|
|
9216
|
+
/** Whether this turn has already emitted reply text — see {@link handleLine}. */
|
|
9217
|
+
turnHasText = false;
|
|
9102
9218
|
spawnSpec(opts) {
|
|
9103
9219
|
const args = [
|
|
9104
9220
|
"-p",
|
|
@@ -9123,6 +9239,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9123
9239
|
return { command: "claude", args };
|
|
9124
9240
|
}
|
|
9125
9241
|
turnPayload(message) {
|
|
9242
|
+
this.turnHasText = false;
|
|
9126
9243
|
return `${JSON.stringify({
|
|
9127
9244
|
type: "user",
|
|
9128
9245
|
message: { role: "user", content: [{ type: "text", text: message }] }
|
|
@@ -9133,6 +9250,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9133
9250
|
const msg = StdioAgentAdapter.parse(line);
|
|
9134
9251
|
if (!msg) return;
|
|
9135
9252
|
if (msg.session_id) this.sessionId = msg.session_id;
|
|
9253
|
+
if (msg.parent_tool_use_id) return;
|
|
9136
9254
|
switch (msg.type) {
|
|
9137
9255
|
// The control channel: Claude asks whether a tool may run when its mode
|
|
9138
9256
|
// cannot decide alone. A read-only planner answers "deny", every time —
|
|
@@ -9157,8 +9275,12 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9157
9275
|
}
|
|
9158
9276
|
case "assistant":
|
|
9159
9277
|
for (const block of Array.isArray(msg.message?.content) ? msg.message.content : []) {
|
|
9160
|
-
if (block.type === "text" && block.text)
|
|
9161
|
-
|
|
9278
|
+
if (block.type === "text" && block.text) {
|
|
9279
|
+
emit({ type: "assistant_text", text: this.turnHasText ? `
|
|
9280
|
+
|
|
9281
|
+
${block.text}` : block.text });
|
|
9282
|
+
this.turnHasText = true;
|
|
9283
|
+
} else if (block.type === "thinking" && block.thinking) emit({ type: "thinking", text: block.thinking });
|
|
9162
9284
|
else if (block.type === "tool_use" && block.name) {
|
|
9163
9285
|
emit({ type: "tool_call", id: block.id ?? block.name, name: block.name, args: block.input ?? {} });
|
|
9164
9286
|
}
|
|
@@ -9167,11 +9289,15 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9167
9289
|
case "user":
|
|
9168
9290
|
for (const block of Array.isArray(msg.message?.content) ? msg.message.content : []) {
|
|
9169
9291
|
if (block.type !== "tool_result") continue;
|
|
9292
|
+
const output = flattenContent(block.content);
|
|
9293
|
+
if (output.includes(ASYNC_LAUNCH_MARKER)) {
|
|
9294
|
+
emit({ type: "background_agent", id: block.tool_use_id ?? "" });
|
|
9295
|
+
}
|
|
9170
9296
|
emit({
|
|
9171
9297
|
type: "tool_result",
|
|
9172
9298
|
id: block.tool_use_id ?? "",
|
|
9173
9299
|
name: "",
|
|
9174
|
-
output
|
|
9300
|
+
output,
|
|
9175
9301
|
success: block.is_error !== true
|
|
9176
9302
|
});
|
|
9177
9303
|
}
|
|
@@ -9591,6 +9717,8 @@ var OpenCodeAdapter = class {
|
|
|
9591
9717
|
exited = false;
|
|
9592
9718
|
disposed = false;
|
|
9593
9719
|
opts = null;
|
|
9720
|
+
/** Whether this turn has already emitted reply text — see {@link emitPart}. */
|
|
9721
|
+
turnHasText = false;
|
|
9594
9722
|
async start(opts) {
|
|
9595
9723
|
this.opts = opts;
|
|
9596
9724
|
assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
|
|
@@ -9658,6 +9786,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
9658
9786
|
return;
|
|
9659
9787
|
}
|
|
9660
9788
|
const seen = /* @__PURE__ */ new Set();
|
|
9789
|
+
this.turnHasText = false;
|
|
9661
9790
|
const streamAbort = new AbortController();
|
|
9662
9791
|
let connected = () => {
|
|
9663
9792
|
};
|
|
@@ -9726,7 +9855,10 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
9726
9855
|
if (part.type === "text" && part.text) {
|
|
9727
9856
|
if (seen.has(`text:${id}`)) return;
|
|
9728
9857
|
seen.add(`text:${id}`);
|
|
9729
|
-
onEvent({ type: "assistant_text", text:
|
|
9858
|
+
onEvent({ type: "assistant_text", text: this.turnHasText ? `
|
|
9859
|
+
|
|
9860
|
+
${part.text}` : part.text });
|
|
9861
|
+
this.turnHasText = true;
|
|
9730
9862
|
return;
|
|
9731
9863
|
}
|
|
9732
9864
|
if (part.type === "reasoning" && part.text) {
|
|
@@ -9893,6 +10025,7 @@ function normalizeAgentArgs(tool, args) {
|
|
|
9893
10025
|
|
|
9894
10026
|
// src/services/harness/CliAgentAiService.ts
|
|
9895
10027
|
var MAX_JSON_REPAIRS = 2;
|
|
10028
|
+
var MAX_AGENT_WAITS = 2;
|
|
9896
10029
|
var LOG_MAX_CHARS = 1e4;
|
|
9897
10030
|
function defaultAdapter(runner, deps) {
|
|
9898
10031
|
switch (runner) {
|
|
@@ -10033,13 +10166,16 @@ var CliAgentAiService = class {
|
|
|
10033
10166
|
let pending = message;
|
|
10034
10167
|
let emptyNudgeSent = false;
|
|
10035
10168
|
let jsonRepairAttempts = 0;
|
|
10169
|
+
let agentWaits = 0;
|
|
10170
|
+
const carried = [];
|
|
10171
|
+
const replyText = (text) => [...carried, text].filter((part) => part.trim()).join("\n\n");
|
|
10036
10172
|
try {
|
|
10037
10173
|
for (; ; ) {
|
|
10038
10174
|
const turn = await this.runTurn(pending, onProgress, combined);
|
|
10039
10175
|
researchLog.push(...turn.researchLog);
|
|
10040
10176
|
if (turn.aborted || combined?.aborted) {
|
|
10041
10177
|
onProgress({ type: "interrupted" });
|
|
10042
|
-
return { kind: "message", text: turn.text, researchLog };
|
|
10178
|
+
return { kind: "message", text: replyText(turn.text), researchLog };
|
|
10043
10179
|
}
|
|
10044
10180
|
if (turn.error) {
|
|
10045
10181
|
return { kind: "message", text: turn.error, researchLog };
|
|
@@ -10057,6 +10193,18 @@ var CliAgentAiService = class {
|
|
|
10057
10193
|
researchLog
|
|
10058
10194
|
};
|
|
10059
10195
|
}
|
|
10196
|
+
if (turn.backgroundAgents > 0 && agentWaits < MAX_AGENT_WAITS && !combined?.aborted) {
|
|
10197
|
+
agentWaits++;
|
|
10198
|
+
carried.push(turn.text);
|
|
10199
|
+
pending = [
|
|
10200
|
+
`You ended your turn with ${turn.backgroundAgents} subagent(s) still running in the background.`,
|
|
10201
|
+
"Ordewell hands the conversation back to the user when your turn ends, so anything you say after it never reaches them \u2014",
|
|
10202
|
+
"the results you promised to report would be lost.",
|
|
10203
|
+
"Wait for those agents to finish NOW, in this reply, and do not end your turn until you have their results.",
|
|
10204
|
+
"Then give the user your synthesis. In future replies, await your agents inside the turn rather than backgrounding them."
|
|
10205
|
+
].join(" ");
|
|
10206
|
+
continue;
|
|
10207
|
+
}
|
|
10060
10208
|
if (extractPrdBlock(turn.text)) conversation.prdCaptured = true;
|
|
10061
10209
|
const reply = classifyPlannerReply(turn.text, {
|
|
10062
10210
|
runners: conversation.runners,
|
|
@@ -10065,7 +10213,11 @@ var CliAgentAiService = class {
|
|
|
10065
10213
|
});
|
|
10066
10214
|
switch (reply.kind) {
|
|
10067
10215
|
case "task_ops":
|
|
10068
|
-
return { kind: "task_ops", ops: reply.ops, text: turn.text, researchLog };
|
|
10216
|
+
return { kind: "task_ops", ops: reply.ops, text: replyText(turn.text), researchLog };
|
|
10217
|
+
// The read channel is a text envelope precisely so it reaches here
|
|
10218
|
+
// too: a harness planner has no Ordewell tool loop to call into.
|
|
10219
|
+
case "task_query":
|
|
10220
|
+
return { kind: "task_query", query: reply.query, text: replyText(turn.text), researchLog };
|
|
10069
10221
|
case "plan":
|
|
10070
10222
|
if (conversation.prdEnabled && !conversation.prdCaptured && !conversation.prdNudgeSent && !combined?.aborted) {
|
|
10071
10223
|
conversation.prdNudgeSent = true;
|
|
@@ -10078,7 +10230,7 @@ var CliAgentAiService = class {
|
|
|
10078
10230
|
continue;
|
|
10079
10231
|
}
|
|
10080
10232
|
this.conversation = null;
|
|
10081
|
-
return { kind: "plan", tasks: reply.tasks, text: turn.text, researchLog };
|
|
10233
|
+
return { kind: "plan", tasks: reply.tasks, text: replyText(turn.text), researchLog };
|
|
10082
10234
|
case "broken_task_ops":
|
|
10083
10235
|
if (jsonRepairAttempts < MAX_JSON_REPAIRS && !combined?.aborted) {
|
|
10084
10236
|
jsonRepairAttempts++;
|
|
@@ -10086,6 +10238,13 @@ var CliAgentAiService = class {
|
|
|
10086
10238
|
continue;
|
|
10087
10239
|
}
|
|
10088
10240
|
break;
|
|
10241
|
+
case "broken_task_query":
|
|
10242
|
+
if (jsonRepairAttempts < MAX_JSON_REPAIRS && !combined?.aborted) {
|
|
10243
|
+
jsonRepairAttempts++;
|
|
10244
|
+
pending = reEmitTaskQueryPrompt(reply.error.message);
|
|
10245
|
+
continue;
|
|
10246
|
+
}
|
|
10247
|
+
break;
|
|
10089
10248
|
case "broken_plan":
|
|
10090
10249
|
if (jsonRepairAttempts < MAX_JSON_REPAIRS && !combined?.aborted) {
|
|
10091
10250
|
jsonRepairAttempts++;
|
|
@@ -10096,7 +10255,7 @@ var CliAgentAiService = class {
|
|
|
10096
10255
|
case "prose":
|
|
10097
10256
|
break;
|
|
10098
10257
|
}
|
|
10099
|
-
return { kind: "message", text: turn.text, researchLog };
|
|
10258
|
+
return { kind: "message", text: replyText(turn.text), researchLog };
|
|
10100
10259
|
}
|
|
10101
10260
|
} finally {
|
|
10102
10261
|
this.activeAbort = null;
|
|
@@ -10116,6 +10275,7 @@ var CliAgentAiService = class {
|
|
|
10116
10275
|
let text = "";
|
|
10117
10276
|
let error;
|
|
10118
10277
|
let stepIndex = 0;
|
|
10278
|
+
let backgroundAgents = 0;
|
|
10119
10279
|
const truncate = (value) => value.length > LOG_MAX_CHARS ? `${value.slice(0, LOG_MAX_CHARS)}
|
|
10120
10280
|
[... truncated, total ${value.length} chars]` : value;
|
|
10121
10281
|
const settle = (id, rawOutput, success, outcome) => {
|
|
@@ -10155,6 +10315,9 @@ var CliAgentAiService = class {
|
|
|
10155
10315
|
case "tool_result":
|
|
10156
10316
|
settle(event.id, event.output, event.success, event.success ? "success" : "failure");
|
|
10157
10317
|
return;
|
|
10318
|
+
case "background_agent":
|
|
10319
|
+
backgroundAgents++;
|
|
10320
|
+
return;
|
|
10158
10321
|
case "permission_request": {
|
|
10159
10322
|
const mapped = mapAgentTool(event.name);
|
|
10160
10323
|
const args = JSON.stringify({ detail: event.detail });
|
|
@@ -10185,7 +10348,7 @@ var CliAgentAiService = class {
|
|
|
10185
10348
|
adapter.dispose();
|
|
10186
10349
|
this.adapter = null;
|
|
10187
10350
|
}
|
|
10188
|
-
return { text, researchLog, error, aborted: signal?.aborted };
|
|
10351
|
+
return { text, researchLog, error, aborted: signal?.aborted, backgroundAgents };
|
|
10189
10352
|
}
|
|
10190
10353
|
// --- Process lifecycle ---
|
|
10191
10354
|
async startAdapter(opts) {
|
|
@@ -10792,6 +10955,12 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
|
10792
10955
|
}
|
|
10793
10956
|
|
|
10794
10957
|
// src/services/createSession.ts
|
|
10958
|
+
var CATALOG_MODEL_CAP = 100;
|
|
10959
|
+
var MAX_TASK_QUERIES = 3;
|
|
10960
|
+
var MAX_TASK_QUERIES_HARD = 6;
|
|
10961
|
+
function freshReadBudget() {
|
|
10962
|
+
return { answered: 0, seen: /* @__PURE__ */ new Set() };
|
|
10963
|
+
}
|
|
10795
10964
|
var PlanEditError = class extends Error {
|
|
10796
10965
|
constructor(message) {
|
|
10797
10966
|
super(message);
|
|
@@ -11028,6 +11197,21 @@ var Session = class {
|
|
|
11028
11197
|
runnerModesFor(runners) {
|
|
11029
11198
|
return runnerModesFrom(this.registry, runners);
|
|
11030
11199
|
}
|
|
11200
|
+
/**
|
|
11201
|
+
* The catalog a model/task-mode edit is checked against — the same
|
|
11202
|
+
* discovered models and manifest modes the per-turn catalog block shows the
|
|
11203
|
+
* planner, so a refusal here can never name something invalid that the
|
|
11204
|
+
* planner was never told about. `modelsCache` is read live, not filtered by
|
|
11205
|
+
* the allowlist here — {@link checkModelAndModeValidity} narrows by
|
|
11206
|
+
* allowlist itself, the same way `coerceAssignments` does.
|
|
11207
|
+
*/
|
|
11208
|
+
editCatalog() {
|
|
11209
|
+
return {
|
|
11210
|
+
modelsByRunner: this.modelsCache,
|
|
11211
|
+
runnerModes: this.runnerModesFor(this.plan?.runners ?? []),
|
|
11212
|
+
perRunnerAllowlist: this.settingsFn().modelAllowlist ?? this.currentAllowlist
|
|
11213
|
+
};
|
|
11214
|
+
}
|
|
11031
11215
|
get planState() {
|
|
11032
11216
|
return this.plan;
|
|
11033
11217
|
}
|
|
@@ -11207,10 +11391,12 @@ var Session = class {
|
|
|
11207
11391
|
async continueConversation(userMessage, options) {
|
|
11208
11392
|
if (!this.plan) throw new Error("No planning conversation to continue");
|
|
11209
11393
|
const priorHistory = this.plan.conversationHistory ?? [];
|
|
11394
|
+
const priorResearchLog = this.plan.researchLog ?? [];
|
|
11210
11395
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11211
11396
|
this.appendTranscript("user", userMessage, now);
|
|
11397
|
+
const historyAfterOwnAppend = this.plan.conversationHistory;
|
|
11212
11398
|
this.plan.researchLog = [...this.plan.researchLog ?? [], { id: `up-${Date.now()}`, type: "user_prompt", content: userMessage, timestamp: now }];
|
|
11213
|
-
const contextBlock = this.planContextBlock();
|
|
11399
|
+
const contextBlock = [this.catalogBlock(), this.planContextBlock()].filter(Boolean).join("\n\n");
|
|
11214
11400
|
const outgoing = contextBlock ? `${contextBlock}
|
|
11215
11401
|
|
|
11216
11402
|
${userMessage}` : userMessage;
|
|
@@ -11223,6 +11409,8 @@ ${userMessage}` : userMessage;
|
|
|
11223
11409
|
(p) => this.translateProgress(p),
|
|
11224
11410
|
options?.signal
|
|
11225
11411
|
) : await this.resumeConversation(outgoing, priorHistory, options);
|
|
11412
|
+
const reads = freshReadBudget();
|
|
11413
|
+
turn = await this.drainTaskQueries(turn, reads, options);
|
|
11226
11414
|
if ((turn.kind === "task_ops" || turn.kind === "plan") && this.hasLiveWork) {
|
|
11227
11415
|
this.queueMessage(userMessage);
|
|
11228
11416
|
this.plan.queuedMessages = this.getQueuedMessages();
|
|
@@ -11232,11 +11420,74 @@ ${userMessage}` : userMessage;
|
|
|
11232
11420
|
researchLog: turn.researchLog
|
|
11233
11421
|
};
|
|
11234
11422
|
}
|
|
11235
|
-
return await this.settleTurn(turn, options);
|
|
11423
|
+
return await this.settleTurn(turn, options, reads);
|
|
11424
|
+
} catch (err) {
|
|
11425
|
+
if (this.plan && this.plan.conversationHistory === historyAfterOwnAppend) {
|
|
11426
|
+
this.plan.conversationHistory = priorHistory;
|
|
11427
|
+
this.plan.researchLog = priorResearchLog;
|
|
11428
|
+
}
|
|
11429
|
+
throw err;
|
|
11236
11430
|
} finally {
|
|
11237
11431
|
releaseAbort();
|
|
11238
11432
|
}
|
|
11239
11433
|
}
|
|
11434
|
+
/**
|
|
11435
|
+
* Answer every read the planner emits until it says something else.
|
|
11436
|
+
*
|
|
11437
|
+
* The channel is a text envelope rather than a registered tool because the
|
|
11438
|
+
* protocol has to be identical on both planner backends (ADR-0009): Ordewell
|
|
11439
|
+
* owns a tool loop only in the API case, and a harness planner running as a
|
|
11440
|
+
* coding-agent subprocess can only be reached this way.
|
|
11441
|
+
*
|
|
11442
|
+
* Draining here — outside {@link repairLoop} — is what keeps reads free of
|
|
11443
|
+
* the repair budget. A planner that looks a task up and *then* fumbles its
|
|
11444
|
+
* ops JSON still gets its two corrective retries; charging it for the read
|
|
11445
|
+
* would cost it the chance to fix the edit.
|
|
11446
|
+
*/
|
|
11447
|
+
async drainTaskQueries(turn, reads, options) {
|
|
11448
|
+
const carried = [];
|
|
11449
|
+
let current = turn;
|
|
11450
|
+
while (current.kind === "task_query") {
|
|
11451
|
+
carried.push(...current.researchLog);
|
|
11452
|
+
if (reads.answered >= MAX_TASK_QUERIES_HARD || !this.aiService.hasActiveConversation() || options?.signal?.aborted) {
|
|
11453
|
+
return {
|
|
11454
|
+
kind: "message",
|
|
11455
|
+
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.",
|
|
11456
|
+
researchLog: carried
|
|
11457
|
+
};
|
|
11458
|
+
}
|
|
11459
|
+
const signature = taskQuerySignature(current.query);
|
|
11460
|
+
const insist = reads.seen.has(signature) || reads.answered >= MAX_TASK_QUERIES;
|
|
11461
|
+
reads.seen.add(signature);
|
|
11462
|
+
reads.answered++;
|
|
11463
|
+
const answer = this.taskQueryAnswer(current.query);
|
|
11464
|
+
current = await this.aiService.continueConversation(
|
|
11465
|
+
insist ? `${answer}
|
|
11466
|
+
|
|
11467
|
+
${TASK_QUERY_ANSWER_OR_OPS}` : answer,
|
|
11468
|
+
(p) => this.translateProgress(p),
|
|
11469
|
+
options?.signal
|
|
11470
|
+
);
|
|
11471
|
+
}
|
|
11472
|
+
return carried.length > 0 ? { ...current, researchLog: [...carried, ...current.researchLog] } : current;
|
|
11473
|
+
}
|
|
11474
|
+
/**
|
|
11475
|
+
* Render one read out of live state. Never persisted to the transcript: the
|
|
11476
|
+
* detail is context for the planner's next reply, and re-sending it on every
|
|
11477
|
+
* later turn is exactly the cost this channel exists to avoid.
|
|
11478
|
+
*/
|
|
11479
|
+
taskQueryAnswer(query) {
|
|
11480
|
+
const runners = this.plan?.runners ?? [];
|
|
11481
|
+
const allowlist = this.settingsFn().modelAllowlist ?? this.currentAllowlist;
|
|
11482
|
+
return renderTaskQueryAnswer(query, this.store.planTasks, {
|
|
11483
|
+
runners,
|
|
11484
|
+
// Allowlist-filtered like the always-on block: a read must not offer a
|
|
11485
|
+
// model the planner is forbidden to assign.
|
|
11486
|
+
models: filterModelsForPrompt(this.modelsCache, allowlist),
|
|
11487
|
+
modes: this.runnerModesFor(runners),
|
|
11488
|
+
autonomousDefault: this.config.autonomousMode
|
|
11489
|
+
});
|
|
11490
|
+
}
|
|
11240
11491
|
/**
|
|
11241
11492
|
* Drive a planner turn to a persisted, broadcast outcome. Task edits apply
|
|
11242
11493
|
* atomically; validation failures are fed back to the model for up to 2
|
|
@@ -11244,7 +11495,7 @@ ${userMessage}` : userMessage;
|
|
|
11244
11495
|
* first turn (startPlanning) and every later turn route through here — one
|
|
11245
11496
|
* path, not two.
|
|
11246
11497
|
*/
|
|
11247
|
-
async settleTurn(turn, options) {
|
|
11498
|
+
async settleTurn(turn, options, reads = freshReadBudget()) {
|
|
11248
11499
|
const invalidOps = (errors, researchLog) => ({
|
|
11249
11500
|
turn: {
|
|
11250
11501
|
kind: "message",
|
|
@@ -11256,11 +11507,15 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
11256
11507
|
}
|
|
11257
11508
|
});
|
|
11258
11509
|
const settled = await repairLoop({
|
|
11259
|
-
first: () => turn,
|
|
11260
|
-
resend: (corrective) => this.
|
|
11261
|
-
|
|
11262
|
-
|
|
11263
|
-
|
|
11510
|
+
first: () => this.drainTaskQueries(turn, reads, options),
|
|
11511
|
+
resend: async (corrective) => this.drainTaskQueries(
|
|
11512
|
+
await this.aiService.continueConversation(
|
|
11513
|
+
corrective,
|
|
11514
|
+
(p) => this.translateProgress(p),
|
|
11515
|
+
options?.signal
|
|
11516
|
+
),
|
|
11517
|
+
reads,
|
|
11518
|
+
options
|
|
11264
11519
|
),
|
|
11265
11520
|
interpret: (t) => {
|
|
11266
11521
|
if (t.kind !== "task_ops") return { done: { turn: t } };
|
|
@@ -11280,6 +11535,52 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
11280
11535
|
}
|
|
11281
11536
|
return this.applyConversationTurn(settled.turn);
|
|
11282
11537
|
}
|
|
11538
|
+
/**
|
|
11539
|
+
* The catalog the planner may actually draw from — model ids and task-mode
|
|
11540
|
+
* ids, per selected runner — emitted on EVERY turn, before any plan exists
|
|
11541
|
+
* or after. The system prompt shows this once at conversation start; a long
|
|
11542
|
+
* grill-me interview or PRD negotiation outlives that single showing and the
|
|
11543
|
+
* planner starts misquoting it, so this re-states it per turn instead.
|
|
11544
|
+
*
|
|
11545
|
+
* Built from the allowlist-filtered view (`filterModelsForPrompt`), the same
|
|
11546
|
+
* one the system prompt used — a restricted allowlist stays a hard bound on
|
|
11547
|
+
* every turn, not just the first. `this.modelsCache` itself is never
|
|
11548
|
+
* filtered: `coerceAssignments` needs the full discovered catalog alongside
|
|
11549
|
+
* the allowlist to resolve labels and clamp effort to real variants.
|
|
11550
|
+
*
|
|
11551
|
+
* Reads `this.plan.runners` live, so a runner admitted mid-session by a
|
|
11552
|
+
* retarget (`admitRunner`) is filtered and shown like every other — there is
|
|
11553
|
+
* no separate "originally selected" list to fall stale.
|
|
11554
|
+
*/
|
|
11555
|
+
catalogBlock() {
|
|
11556
|
+
if (!this.plan) return null;
|
|
11557
|
+
const allowlist = this.settingsFn().modelAllowlist ?? this.currentAllowlist;
|
|
11558
|
+
const filteredModels = filterModelsForPrompt(this.modelsCache, allowlist);
|
|
11559
|
+
const runnerModes = this.runnerModesFor(this.plan.runners);
|
|
11560
|
+
const autonomousDefault = this.config.autonomousMode;
|
|
11561
|
+
const modelLines = this.plan.runners.map((runner) => {
|
|
11562
|
+
const models = filteredModels[runner] ?? [];
|
|
11563
|
+
const capped = models.slice(0, CATALOG_MODEL_CAP);
|
|
11564
|
+
const remainder = models.length - capped.length;
|
|
11565
|
+
const ids = capped.map((m) => m.modelId).join(", ") || "(no models discovered)";
|
|
11566
|
+
return `${runner}: ${ids}${remainder > 0 ? ` \u2026 +${remainder} more not shown` : ""}`;
|
|
11567
|
+
});
|
|
11568
|
+
const modeLines = this.plan.runners.map((runner) => {
|
|
11569
|
+
const modes = runnerModes[runner] ?? [];
|
|
11570
|
+
if (modes.length === 0) return `${runner}: (no modes declared)`;
|
|
11571
|
+
const defaultId = resolveDefaultMode(modes, autonomousDefault);
|
|
11572
|
+
const ids = modes.map((m) => `${m.id}${m.id === defaultId ? " (default)" : ""}`).join(", ");
|
|
11573
|
+
return `${runner}: ${ids}`;
|
|
11574
|
+
});
|
|
11575
|
+
return [
|
|
11576
|
+
"<available_models>",
|
|
11577
|
+
...modelLines,
|
|
11578
|
+
"</available_models>",
|
|
11579
|
+
"<available_task_modes>",
|
|
11580
|
+
...modeLines,
|
|
11581
|
+
"</available_task_modes>"
|
|
11582
|
+
].join("\n");
|
|
11583
|
+
}
|
|
11283
11584
|
/**
|
|
11284
11585
|
* The "you are here" block for post-plan chat: current tasks with stable
|
|
11285
11586
|
* references, plus the task-ops protocol. Injected per turn (never
|
|
@@ -11289,44 +11590,48 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
11289
11590
|
planContextBlock() {
|
|
11290
11591
|
if (!this.plan || this.store.planTasks.length === 0) return null;
|
|
11291
11592
|
const orderOf = new Map(this.store.planTasks.map((t) => [t.id, `#${t.order}`]));
|
|
11292
|
-
const lines = this.store.planTasks.map(
|
|
11293
|
-
|
|
11294
|
-
|
|
11593
|
+
const lines = this.store.planTasks.map((t) => {
|
|
11594
|
+
const isMan = t.type === "user";
|
|
11595
|
+
const runFields = isMan ? `steps:${t.userSteps?.length ?? 0}` : `${t.assignedModel ? `model:${t.assignedModel.modelId} ` : ""}mode:${t.taskMode ?? "build"} effort:${t.thinkingEffort ?? "-"}`;
|
|
11596
|
+
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(", ")}]`;
|
|
11597
|
+
});
|
|
11295
11598
|
const locked = this.store.planTasks.filter((t) => t.status === "in_progress");
|
|
11296
11599
|
const execNote = this.hasLiveWork ? `
|
|
11297
11600
|
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.` : "";
|
|
11298
|
-
const
|
|
11299
|
-
for (const runner of this.plan.runners) {
|
|
11300
|
-
const models = this.modelsCache[runner] ?? [];
|
|
11301
|
-
const ids = models.map((m) => m.modelId).join(", ");
|
|
11302
|
-
modelLines.push(`${runner}: ${ids || "(no models discovered)"}`);
|
|
11303
|
-
}
|
|
11304
|
-
const modelBlock = modelLines.length > 0 ? ["<available_models>", ...modelLines, "</available_models>"].join("\n") : "";
|
|
11601
|
+
const updateChanges = UPDATABLE_FIELDS.map((f) => f === "dependencies" ? '"dependencies"?:["<id or #order>"]' : `"${f}"?`).join(",");
|
|
11305
11602
|
return [
|
|
11306
11603
|
"<current_plan>",
|
|
11307
11604
|
...lines,
|
|
11308
11605
|
"</current_plan>",
|
|
11309
|
-
|
|
11310
|
-
"The block above is the CURRENT task plan. Choose how to respond:",
|
|
11606
|
+
"The block above is the CURRENT task plan \u2014 short fields only. Choose how to respond:",
|
|
11311
11607
|
"- To answer a question or discuss, reply in plain prose (no JSON).",
|
|
11608
|
+
// The reminder text is owned by TaskQuery beside the parser, so the
|
|
11609
|
+
// protocol the planner reads and the one Ordewell accepts stay one thing.
|
|
11610
|
+
TASK_QUERY_REMINDER,
|
|
11312
11611
|
"- To modify specific tasks, reply with ONLY this JSON object:",
|
|
11313
11612
|
' {"taskOps": [',
|
|
11314
|
-
|
|
11315
|
-
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?}},',
|
|
11613
|
+
` {"op":"update","taskId":"<id or #order>","changes":{${updateChanges}}},`,
|
|
11614
|
+
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?},"handle"?:"<name>"},',
|
|
11316
11615
|
' {"op":"remove","taskId":"<id or #order>"},',
|
|
11317
|
-
' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]},',
|
|
11318
|
-
' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}},',
|
|
11319
|
-
' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...]}',
|
|
11616
|
+
' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]}, // only to re-prioritise INDEPENDENT tasks',
|
|
11617
|
+
' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...},"handle"?:"<name>"},',
|
|
11618
|
+
' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...],"handle"?:"<name>"},',
|
|
11619
|
+
` {"op":"rearm","taskId":"<id or #order>","changes"?:{${updateChanges}}}`,
|
|
11320
11620
|
" ]}",
|
|
11321
11621
|
'- For sweeping changes, you may instead emit a full {"tasks":[...]} plan JSON.',
|
|
11622
|
+
'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.',
|
|
11623
|
+
'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.',
|
|
11322
11624
|
'When creating or changing tasks, set "assignedRunner" and "assignedModel" ({"modelId","modelLabel","thinkingEffort"?}) using only the runners and models listed in <available_models>.',
|
|
11323
|
-
|
|
11625
|
+
'Keep dependencies consistent: no cycles, no references to removed tasks, and never touch running or completed tasks \u2014 "rearm" is the one exception, below.',
|
|
11626
|
+
'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,
|
|
11627
|
+
'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.',
|
|
11628
|
+
'"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.'
|
|
11324
11629
|
].filter(Boolean).join("\n");
|
|
11325
11630
|
}
|
|
11326
11631
|
/** Validate + commit a task_ops turn atomically. Returns the errors on rejection (plan untouched). */
|
|
11327
11632
|
applyTaskOpsTurn(turn) {
|
|
11328
11633
|
if (!this.plan) throw new Error("No active plan state");
|
|
11329
|
-
const result = applyTaskOps(this.store.planTasks, turn.ops, this.plan.runners);
|
|
11634
|
+
const result = applyTaskOps(this.store.planTasks, turn.ops, this.plan.runners, this.editCatalog());
|
|
11330
11635
|
if (!result.ok) return { errors: result.errors };
|
|
11331
11636
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11332
11637
|
const content = `Tasks updated:
|
|
@@ -11593,15 +11898,22 @@ Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.
|
|
|
11593
11898
|
return plan;
|
|
11594
11899
|
}
|
|
11595
11900
|
/**
|
|
11596
|
-
* Patch one task's fields. A hand-set dependency list
|
|
11597
|
-
*
|
|
11598
|
-
*
|
|
11599
|
-
*
|
|
11901
|
+
* Patch one task's fields. A hand-set dependency list, or a type flip
|
|
11902
|
+
* between AI and MAN, are the patches that can leave a task incoherent
|
|
11903
|
+
* (unschedulable, or carrying fields that mean nothing for its new type),
|
|
11904
|
+
* so both go through the same {@link validateTaskEdit} guard the planner's
|
|
11905
|
+
* task-ops applier uses (as the 'direct' actor, which skips the lock rule)
|
|
11906
|
+
* rather than a second copy of the rules — and throws, so the surface can
|
|
11907
|
+
* say why. Gated on the task existing so an edit to an unknown id still
|
|
11908
|
+
* falls through to the no-op `store.update` below instead of throwing.
|
|
11600
11909
|
*/
|
|
11601
11910
|
async updateTask(taskId, changes) {
|
|
11602
|
-
if (changes.dependencies) {
|
|
11603
|
-
const check =
|
|
11604
|
-
if (!check.ok) throw new PlanEditError(check.error ?? "Those
|
|
11911
|
+
if ((changes.dependencies || changes.type || changes.assignedModel || changes.taskMode) && this.store.get(taskId)) {
|
|
11912
|
+
const check = validateTaskEdit("direct", this.store.planTasks, taskId, changes, this.editCatalog());
|
|
11913
|
+
if (!check.ok) throw new PlanEditError(check.error ?? "Those changes are not valid");
|
|
11914
|
+
if (check.clear?.length) {
|
|
11915
|
+
changes = { ...changes, ...Object.fromEntries(check.clear.map((f) => [f, void 0])) };
|
|
11916
|
+
}
|
|
11605
11917
|
}
|
|
11606
11918
|
return this.editPlan(
|
|
11607
11919
|
() => Boolean(this.store.update(taskId, changes)),
|
|
@@ -13636,6 +13948,11 @@ export {
|
|
|
13636
13948
|
SettingsService,
|
|
13637
13949
|
StdioAgentAdapter,
|
|
13638
13950
|
TASK_OPS_ENVELOPE_KEY,
|
|
13951
|
+
TASK_QUERY_ANSWER_OR_OPS,
|
|
13952
|
+
TASK_QUERY_ENVELOPE_KEY,
|
|
13953
|
+
TASK_QUERY_FIELDS,
|
|
13954
|
+
TASK_QUERY_PROTOCOL,
|
|
13955
|
+
TASK_QUERY_REMINDER,
|
|
13639
13956
|
TRUNCATED_PLAN_REPAIR_INSTRUCTION,
|
|
13640
13957
|
TaskOrchestrator,
|
|
13641
13958
|
TmuxRunner,
|
|
@@ -13756,6 +14073,7 @@ export {
|
|
|
13756
14073
|
parsePartialPlan,
|
|
13757
14074
|
parsePlanJson,
|
|
13758
14075
|
parseTaskOpsJson,
|
|
14076
|
+
parseTaskQueryJson,
|
|
13759
14077
|
pendingEditRulesBlock,
|
|
13760
14078
|
planDirectLaunch,
|
|
13761
14079
|
planShellLaunch,
|
|
@@ -13764,11 +14082,13 @@ export {
|
|
|
13764
14082
|
providerForRunner,
|
|
13765
14083
|
reEmitPlanPrompt,
|
|
13766
14084
|
reEmitTaskOpsPrompt,
|
|
14085
|
+
reEmitTaskQueryPrompt,
|
|
13767
14086
|
readDaemonToken,
|
|
13768
14087
|
referencePattern,
|
|
13769
14088
|
removeTaskFromPlan,
|
|
13770
14089
|
renderPlanMap,
|
|
13771
14090
|
renderPriorOutputs,
|
|
14091
|
+
renderTaskQueryAnswer,
|
|
13772
14092
|
renumberTasks,
|
|
13773
14093
|
repairLoop,
|
|
13774
14094
|
researchShellWarning,
|
|
@@ -13802,7 +14122,9 @@ export {
|
|
|
13802
14122
|
summarizeOutput,
|
|
13803
14123
|
summarizeToolCall,
|
|
13804
14124
|
taskOpsRejectedPrompt,
|
|
14125
|
+
taskQuerySignature,
|
|
13805
14126
|
textHasTaskOps,
|
|
14127
|
+
textHasTaskQuery,
|
|
13806
14128
|
tmuxSessionName,
|
|
13807
14129
|
tmuxSocketName,
|
|
13808
14130
|
tmuxWindowName,
|