@ordewell/core 0.4.9 → 0.4.11
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/{ITerminalRunner-CbE6gKLI.d.ts → ITerminalRunner-BTaKZ659.d.mts} +2 -2
- package/dist/{ITerminalRunner-1NAv8wCC.d.mts → ITerminalRunner-DVwORcV9.d.ts} +2 -2
- package/dist/ModeResolver-CCKYqdZC.d.mts +44 -0
- package/dist/ModeResolver-CzZd-i6T.d.ts +44 -0
- package/dist/{Task-1NwmUImI.d.mts → Task-cGrfYRXK.d.mts} +8 -1
- package/dist/{Task-1NwmUImI.d.ts → Task-cGrfYRXK.d.ts} +8 -1
- package/dist/chunk-DNNZGH3I.mjs +862 -0
- package/dist/chunk-DNNZGH3I.mjs.map +1 -0
- package/dist/chunk-HPNUFBIT.mjs +19 -0
- package/dist/chunk-HPNUFBIT.mjs.map +1 -0
- package/dist/{chunk-SRGQBGI5.mjs → chunk-N6HTBUKG.mjs} +13 -1
- package/dist/{chunk-SRGQBGI5.mjs.map → chunk-N6HTBUKG.mjs.map} +1 -1
- package/dist/{chunk-YXAIQKVE.mjs → chunk-UEJ3SZQ7.mjs} +2 -2
- package/dist/index.d.mts +284 -80
- package/dist/index.d.ts +284 -80
- package/dist/index.js +1670 -820
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1067 -597
- package/dist/index.mjs.map +1 -1
- package/dist/order-labels.d.mts +17 -0
- package/dist/order-labels.d.ts +17 -0
- package/dist/order-labels.js +44 -0
- package/dist/order-labels.js.map +1 -0
- package/dist/order-labels.mjs +9 -0
- package/dist/order-labels.mjs.map +1 -0
- package/dist/{parsing-RjN9JbyG.d.ts → parsing-BT-dvFcu.d.mts} +5 -43
- package/dist/{parsing-xpozY5ac.d.mts → parsing-D87zKtTe.d.ts} +5 -43
- package/dist/parsing.d.mts +3 -2
- package/dist/parsing.d.ts +3 -2
- package/dist/parsing.js.map +1 -1
- package/dist/parsing.mjs +3 -2
- package/dist/{plan-utils-nfHKSbd3.d.mts → plan-utils-BHLbBym3.d.mts} +27 -5
- package/dist/{plan-utils-Cgc0cCH1.d.ts → plan-utils-akoB6YCL.d.ts} +27 -5
- package/dist/plan-utils.d.mts +3 -2
- package/dist/plan-utils.d.ts +3 -2
- package/dist/plan-utils.js +1 -9
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +3 -2
- package/dist/testing.d.mts +2 -2
- package/dist/testing.d.ts +2 -2
- package/package.json +12 -2
- package/skills/grilling/SKILL.md +31 -0
- package/skills/to-spec/SKILL.md +64 -0
- package/dist/chunk-4KIWPW4K.mjs +0 -510
- package/dist/chunk-4KIWPW4K.mjs.map +0 -1
- /package/dist/{chunk-YXAIQKVE.mjs.map → chunk-UEJ3SZQ7.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-UEJ3SZQ7.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-DNNZGH3I.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,
|
|
@@ -39,6 +46,7 @@ import {
|
|
|
39
46
|
extractObjectWithBalance,
|
|
40
47
|
extractObjectsWithKey,
|
|
41
48
|
flattenTasks,
|
|
49
|
+
flattenTasksWithParents,
|
|
42
50
|
migrateLegacyPlan,
|
|
43
51
|
migratePlanState,
|
|
44
52
|
migrateTask,
|
|
@@ -49,7 +57,11 @@ import {
|
|
|
49
57
|
updateTaskInPlan,
|
|
50
58
|
validateModifiedPlan,
|
|
51
59
|
warningsText
|
|
52
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-N6HTBUKG.mjs";
|
|
61
|
+
import {
|
|
62
|
+
resolveOrderLabel,
|
|
63
|
+
taskOrderLabel
|
|
64
|
+
} from "./chunk-HPNUFBIT.mjs";
|
|
53
65
|
|
|
54
66
|
// src/interfaces/IFileSystem.ts
|
|
55
67
|
var GREP_DEFAULT_HEAD_LIMIT = 100;
|
|
@@ -2568,7 +2580,7 @@ function redactSecrets(text) {
|
|
|
2568
2580
|
const { value, tail } = trimTrailingPunctuation(raw);
|
|
2569
2581
|
if (value.length < 8) return full;
|
|
2570
2582
|
if (looksLikeCode(value) || value.startsWith("$")) return full;
|
|
2571
|
-
if (UNAMBIGUOUS_CREDENTIAL_NAME.test(head) && /\d/.test(value)) {
|
|
2583
|
+
if (UNAMBIGUOUS_CREDENTIAL_NAME.test(head) && (/\d/.test(value) || value.length >= 12)) {
|
|
2572
2584
|
return `${head}${quote}${marker("credential", value.length)}${quote}${tail}`;
|
|
2573
2585
|
}
|
|
2574
2586
|
if (IDENTIFIER.test(value) || SLUG.test(value)) return full;
|
|
@@ -2607,33 +2619,33 @@ function grepOptions(args) {
|
|
|
2607
2619
|
};
|
|
2608
2620
|
}
|
|
2609
2621
|
var STOPPED_TOOL_RESULT = "Stopped by the user \u2014 this call was not executed, and no further tool calls will run this turn.";
|
|
2610
|
-
async function executeTool(name, args,
|
|
2611
|
-
const outcome = await runTool(name, args,
|
|
2622
|
+
async function executeTool(name, args, fs12, fetcher, signal) {
|
|
2623
|
+
const outcome = await runTool(name, args, fs12, fetcher, signal);
|
|
2612
2624
|
return { ...outcome, output: redactSecrets(outcome.output) };
|
|
2613
2625
|
}
|
|
2614
|
-
async function runTool(name, args,
|
|
2626
|
+
async function runTool(name, args, fs12, fetcher, signal) {
|
|
2615
2627
|
if (signal?.aborted) return { success: false, output: STOPPED_TOOL_RESULT, truncated: false };
|
|
2616
2628
|
switch (name) {
|
|
2617
2629
|
case "read_file": {
|
|
2618
2630
|
const offset = num(args.offset);
|
|
2619
2631
|
const limit = num(args.limit);
|
|
2620
2632
|
const maxBytes = num(args.maxBytes);
|
|
2621
|
-
return
|
|
2633
|
+
return fs12.readFile(String(args.path ?? ""), { offset, limit, maxBytes });
|
|
2622
2634
|
}
|
|
2623
2635
|
case "read_files": {
|
|
2624
2636
|
const paths = Array.isArray(args.paths) ? args.paths.map(String) : [];
|
|
2625
|
-
return
|
|
2637
|
+
return fs12.readFiles(paths);
|
|
2626
2638
|
}
|
|
2627
2639
|
case "glob":
|
|
2628
|
-
return
|
|
2640
|
+
return fs12.glob(String(args.pattern ?? ""), { path: str(args.path), headLimit: num(args.head_limit ?? args.headLimit) });
|
|
2629
2641
|
case "grep":
|
|
2630
|
-
return
|
|
2642
|
+
return fs12.grep(String(args.pattern ?? ""), grepOptions(args));
|
|
2631
2643
|
case "find_symbol":
|
|
2632
|
-
return
|
|
2644
|
+
return fs12.findSymbol(String(args.symbol ?? ""), { language: str(args.language), path: str(args.path) });
|
|
2633
2645
|
case "list_dir":
|
|
2634
|
-
return
|
|
2646
|
+
return fs12.listDir(String(args.path ?? ""), num(args.depth));
|
|
2635
2647
|
case "bash":
|
|
2636
|
-
return
|
|
2648
|
+
return fs12.bash(String(args.command ?? ""), signal);
|
|
2637
2649
|
case "fetch": {
|
|
2638
2650
|
if (!fetcher) return { success: false, output: "Web fetching is not available in this environment.", truncated: false };
|
|
2639
2651
|
const url = String(args.url ?? "");
|
|
@@ -3716,14 +3728,7 @@ import { v4 as uuidv4 } from "uuid";
|
|
|
3716
3728
|
|
|
3717
3729
|
// src/services/plannerModes.ts
|
|
3718
3730
|
var MODE_TOGGLES = [
|
|
3719
|
-
// Both of these interview the user, and a one-shot run has nobody to ask —
|
|
3720
|
-
// its prompt says so outright. Their absence there is a decision, not a gap.
|
|
3721
|
-
{ id: "grill-me", settingsKey: "grillMe", runtimeKey: "grillMeEnabled", scopes: ["chat"] },
|
|
3722
|
-
{ id: "prd", settingsKey: "prd", runtimeKey: "prdEnabled", scopes: ["chat"] },
|
|
3723
|
-
// Structural: append a final review task. Nothing about it needs a dialogue.
|
|
3724
|
-
{ id: "review", settingsKey: "review", runtimeKey: "reviewEnabled", scopes: ["chat", "one-shot"] },
|
|
3725
3731
|
{ id: "verify", settingsKey: "verification", runtimeKey: "verificationEnabled", scopes: ["chat", "one-shot"] },
|
|
3726
|
-
{ id: "research-subagents", settingsKey: "researchSubagents", runtimeKey: "researchSubagentsEnabled", scopes: ["chat", "one-shot"] },
|
|
3727
3732
|
{ id: "tdd", settingsKey: "tdd", runtimeKey: "tddEnabled", scopes: ["task"] }
|
|
3728
3733
|
];
|
|
3729
3734
|
function plannerRuntimeToggles(settings) {
|
|
@@ -3733,28 +3738,16 @@ function plannerRuntimeToggles(settings) {
|
|
|
3733
3738
|
}
|
|
3734
3739
|
var DEFAULT_PLANNER_MODES = {
|
|
3735
3740
|
autonomousDefault: true,
|
|
3736
|
-
|
|
3737
|
-
prd: false,
|
|
3738
|
-
review: false,
|
|
3739
|
-
verification: false,
|
|
3740
|
-
researchSubagents: false
|
|
3741
|
+
verification: false
|
|
3741
3742
|
};
|
|
3742
3743
|
function plannerModesFrom(settings, autonomousDefault) {
|
|
3743
3744
|
return {
|
|
3744
3745
|
autonomousDefault,
|
|
3745
|
-
|
|
3746
|
-
prd: settings.prdEnabled ?? false,
|
|
3747
|
-
review: settings.reviewEnabled ?? false,
|
|
3748
|
-
verification: settings.verificationEnabled ?? false,
|
|
3749
|
-
researchSubagents: settings.researchSubagentsEnabled ?? false
|
|
3746
|
+
verification: settings.verificationEnabled ?? false
|
|
3750
3747
|
};
|
|
3751
3748
|
}
|
|
3752
3749
|
var MODE_FIELD = {
|
|
3753
|
-
"grill-me": "grillMe",
|
|
3754
|
-
prd: "prd",
|
|
3755
|
-
review: "review",
|
|
3756
3750
|
verify: "verification",
|
|
3757
|
-
"research-subagents": "researchSubagents",
|
|
3758
3751
|
tdd: null
|
|
3759
3752
|
};
|
|
3760
3753
|
function modesFor(scope, modes) {
|
|
@@ -3766,8 +3759,179 @@ function modesFor(scope, modes) {
|
|
|
3766
3759
|
return scoped;
|
|
3767
3760
|
}
|
|
3768
3761
|
|
|
3762
|
+
// src/services/TaskQuery.ts
|
|
3763
|
+
var TASK_QUERY_FIELDS = [
|
|
3764
|
+
"description",
|
|
3765
|
+
"prompt",
|
|
3766
|
+
"userSteps",
|
|
3767
|
+
"verdict",
|
|
3768
|
+
"outputSummary",
|
|
3769
|
+
"userStoriesCovered"
|
|
3770
|
+
];
|
|
3771
|
+
function textHasTaskQuery(text) {
|
|
3772
|
+
return text.includes(`"${TASK_QUERY_ENVELOPE_KEY}"`);
|
|
3773
|
+
}
|
|
3774
|
+
function parseTaskQueryJson(text) {
|
|
3775
|
+
const { matches, fallback } = extractObjectsWithKey(text, TASK_QUERY_ENVELOPE_KEY);
|
|
3776
|
+
const candidates = matches.length > 0 ? [...matches].reverse() : [fallback.json];
|
|
3777
|
+
let lastError = null;
|
|
3778
|
+
for (const json of candidates) {
|
|
3779
|
+
try {
|
|
3780
|
+
return parseTaskQueryObject(json, text);
|
|
3781
|
+
} catch (err) {
|
|
3782
|
+
if (!(err instanceof PlanParseError)) throw err;
|
|
3783
|
+
if (!lastError) lastError = err;
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
throw lastError ?? new PlanParseError("No JSON object found in task-query reply", text);
|
|
3787
|
+
}
|
|
3788
|
+
function parseTaskQueryObject(json, text) {
|
|
3789
|
+
let parsed;
|
|
3790
|
+
try {
|
|
3791
|
+
parsed = JSON.parse(escapeControlCharsInStrings(stripTrailingCommas(json)));
|
|
3792
|
+
} catch (err) {
|
|
3793
|
+
throw new PlanParseError(`Task-query JSON is invalid: ${err instanceof Error ? err.message : String(err)}`, text);
|
|
3794
|
+
}
|
|
3795
|
+
const raw = parsed.taskQuery;
|
|
3796
|
+
const body = Array.isArray(raw) ? { tasks: raw } : raw;
|
|
3797
|
+
if (typeof body !== "object" || body === null) {
|
|
3798
|
+
throw new PlanParseError('"taskQuery" must be an object (or an array of task references)', text);
|
|
3799
|
+
}
|
|
3800
|
+
const { tasks: rawTasks, fields: rawFields, catalog: rawCatalog } = body;
|
|
3801
|
+
const tasks = [];
|
|
3802
|
+
if (rawTasks !== void 0) {
|
|
3803
|
+
if (!Array.isArray(rawTasks)) throw new PlanParseError('"tasks" in a taskQuery must be an array of task references', text);
|
|
3804
|
+
for (const ref of rawTasks) {
|
|
3805
|
+
if (typeof ref !== "string" && typeof ref !== "number") {
|
|
3806
|
+
throw new PlanParseError(`Invalid task reference "${String(ref)}" in taskQuery`, text);
|
|
3807
|
+
}
|
|
3808
|
+
tasks.push(String(ref).trim());
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
let fields;
|
|
3812
|
+
if (rawFields !== void 0) {
|
|
3813
|
+
if (!Array.isArray(rawFields)) throw new PlanParseError('"fields" in a taskQuery must be an array of field names', text);
|
|
3814
|
+
fields = [];
|
|
3815
|
+
for (const f of rawFields) {
|
|
3816
|
+
if (typeof f !== "string" || !TASK_QUERY_FIELDS.includes(f)) {
|
|
3817
|
+
throw new PlanParseError(`Unknown taskQuery field "${String(f)}" \u2014 choose from ${TASK_QUERY_FIELDS.join(", ")}`, text);
|
|
3818
|
+
}
|
|
3819
|
+
fields.push(f);
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
const catalog = rawCatalog === true;
|
|
3823
|
+
if (tasks.length === 0 && !catalog) {
|
|
3824
|
+
throw new PlanParseError('A taskQuery must name at least one task or set "catalog": true', text);
|
|
3825
|
+
}
|
|
3826
|
+
return { tasks, fields, catalog };
|
|
3827
|
+
}
|
|
3828
|
+
function taskQuerySignature(query) {
|
|
3829
|
+
return JSON.stringify([query.tasks, query.fields ?? null, query.catalog]);
|
|
3830
|
+
}
|
|
3831
|
+
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.";
|
|
3832
|
+
var TASK_QUERY_PROTOCOL = [
|
|
3833
|
+
"READING A TASK BEFORE YOU EDIT IT:",
|
|
3834
|
+
"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:",
|
|
3835
|
+
` {"${TASK_QUERY_ENVELOPE_KEY}":{"tasks":["<id or #order>", "..."],"fields":[${TASK_QUERY_FIELDS.map((f) => `"${f}"`).join(", ")}],"catalog":true}}`,
|
|
3836
|
+
'- "tasks": one or more task references. Ask for everything you need in ONE query \u2014 each query costs a round-trip.',
|
|
3837
|
+
'- "fields": optional. Omit it to get every field above.',
|
|
3838
|
+
'- "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.',
|
|
3839
|
+
"Ordewell answers immediately with the detail and changes nothing. Then reply again with your taskOps JSON, or with prose for the user.",
|
|
3840
|
+
"Three queries per user message; after that every answer also tells you to land the turn. Do not ask the same question twice."
|
|
3841
|
+
];
|
|
3842
|
+
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.`;
|
|
3843
|
+
function findTask(tasks, ref) {
|
|
3844
|
+
const byId = tasks.find((t) => t.id === ref);
|
|
3845
|
+
if (byId) return byId;
|
|
3846
|
+
const orderStr = ref.startsWith("#") ? ref.slice(1) : ref;
|
|
3847
|
+
if (/^\d+$/.test(orderStr)) {
|
|
3848
|
+
const byOrder = tasks.filter((t) => t.order === Number(orderStr));
|
|
3849
|
+
if (byOrder.length === 1) return byOrder[0];
|
|
3850
|
+
}
|
|
3851
|
+
return tasks.find((t) => t.title === ref);
|
|
3852
|
+
}
|
|
3853
|
+
function renderField(task, field) {
|
|
3854
|
+
switch (field) {
|
|
3855
|
+
case "description":
|
|
3856
|
+
return [`description: ${task.description || "(none)"}`];
|
|
3857
|
+
case "prompt":
|
|
3858
|
+
return task.prompt ? ["prompt:", task.prompt] : ["prompt: (none)"];
|
|
3859
|
+
case "userSteps": {
|
|
3860
|
+
const steps = task.userSteps ?? [];
|
|
3861
|
+
if (steps.length === 0) return ["userSteps: (none)"];
|
|
3862
|
+
return ["userSteps:", ...steps.map((s) => ` ${s.order}. [${s.completed ? "x" : " "}] ${s.instruction}`)];
|
|
3863
|
+
}
|
|
3864
|
+
case "verdict": {
|
|
3865
|
+
const v = task.verdict;
|
|
3866
|
+
if (!v) return ["verdict: (none)"];
|
|
3867
|
+
const checks = v.checks.map((c) => `${c.name}=${c.skipped ? "skipped" : c.passed ? "pass" : "fail"}`).join(", ");
|
|
3868
|
+
return [`verdict: ${v.outcome.toUpperCase()} \u2014 ${v.reason}${checks ? ` [${checks}]` : ""}`];
|
|
3869
|
+
}
|
|
3870
|
+
case "outputSummary": {
|
|
3871
|
+
const o = task.outputSummary;
|
|
3872
|
+
if (!o) return ["outputSummary: (none)"];
|
|
3873
|
+
return [`outputSummary: ${o.reviewReason}`, ...o.logTail ? ["log tail:", o.logTail] : []];
|
|
3874
|
+
}
|
|
3875
|
+
case "userStoriesCovered": {
|
|
3876
|
+
const stories = task.userStoriesCovered ?? [];
|
|
3877
|
+
if (stories.length === 0) return ["userStoriesCovered: (none)"];
|
|
3878
|
+
return ["userStoriesCovered:", ...stories.map((s) => ` - ${s}`)];
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
function renderTask(task, fields) {
|
|
3883
|
+
return [
|
|
3884
|
+
`#${task.order} id=${task.id} "${task.title}" [${task.status}] type:${task.type === "user" ? "MAN" : "AI"}`,
|
|
3885
|
+
...fields.flatMap((f) => renderField(task, f)),
|
|
3886
|
+
""
|
|
3887
|
+
];
|
|
3888
|
+
}
|
|
3889
|
+
function renderCatalog(catalog) {
|
|
3890
|
+
const lines = [];
|
|
3891
|
+
for (const runner of catalog.runners) {
|
|
3892
|
+
const models = catalog.models[runner] ?? [];
|
|
3893
|
+
lines.push(`${runner} models:`);
|
|
3894
|
+
if (models.length === 0) lines.push(" (none discovered)");
|
|
3895
|
+
for (const m of models) {
|
|
3896
|
+
const variants = m.variants?.length ? ` variants: ${m.variants.map((v) => `${v.id}${v.label && v.label !== v.id ? ` (${v.label})` : ""}`).join(", ")}` : " variants: none";
|
|
3897
|
+
lines.push(` - ${m.modelId} \u2014 ${m.modelLabel}${variants}`);
|
|
3898
|
+
}
|
|
3899
|
+
const modes = catalog.modes[runner] ?? [];
|
|
3900
|
+
lines.push(`${runner} task modes:`);
|
|
3901
|
+
if (modes.length === 0) lines.push(" (none declared)");
|
|
3902
|
+
const defaultId = resolveDefaultMode(modes, catalog.autonomousDefault);
|
|
3903
|
+
for (const mode of modes) {
|
|
3904
|
+
lines.push(` - ${mode.id} \u2014 ${mode.label}: ${mode.description}${mode.id === defaultId ? " (default)" : ""}`);
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
return lines;
|
|
3908
|
+
}
|
|
3909
|
+
function renderTaskQueryAnswer(query, tasks, catalog) {
|
|
3910
|
+
const fields = query.fields ?? TASK_QUERY_FIELDS;
|
|
3911
|
+
const blocks = [];
|
|
3912
|
+
if (query.tasks.length > 0) {
|
|
3913
|
+
const body = [];
|
|
3914
|
+
for (const ref of query.tasks) {
|
|
3915
|
+
const task = findTask(tasks, ref);
|
|
3916
|
+
if (!task) {
|
|
3917
|
+
body.push(`${ref}: no task matches this reference in the current plan.`, "");
|
|
3918
|
+
continue;
|
|
3919
|
+
}
|
|
3920
|
+
body.push(...renderTask(task, fields));
|
|
3921
|
+
}
|
|
3922
|
+
blocks.push(["<task_detail>", ...body, "</task_detail>"].join("\n"));
|
|
3923
|
+
}
|
|
3924
|
+
if (query.catalog) {
|
|
3925
|
+
blocks.push(["<runner_catalog>", ...renderCatalog(catalog), "</runner_catalog>"].join("\n"));
|
|
3926
|
+
}
|
|
3927
|
+
blocks.push(
|
|
3928
|
+
"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."
|
|
3929
|
+
);
|
|
3930
|
+
return blocks.join("\n\n");
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3769
3933
|
// src/services/PlanPrompts.ts
|
|
3770
|
-
function buildResearchToolsPrompt(
|
|
3934
|
+
function buildResearchToolsPrompt() {
|
|
3771
3935
|
const lines = [
|
|
3772
3936
|
"You have access to the following tools to explore the workspace:",
|
|
3773
3937
|
"",
|
|
@@ -3783,15 +3947,11 @@ function buildResearchToolsPrompt(subagentsEnabled = false) {
|
|
|
3783
3947
|
"- glob: for finding files by name/path pattern",
|
|
3784
3948
|
"- grep: for searching inside file contents",
|
|
3785
3949
|
"",
|
|
3786
|
-
"When multiple independent tool calls are needed, batch them in a single response to save round-trips. For example, if you need to read README.md and AGENTS.md, call read_files with both paths at once."
|
|
3950
|
+
"When multiple independent tool calls are needed, batch them in a single response to save round-trips. For example, if you need to read README.md and AGENTS.md, call read_files with both paths at once.",
|
|
3951
|
+
"",
|
|
3952
|
+
'spawn_research_agent(prompt: string) - Launch a stateless read-only research agent that explores the workspace with its own tools and returns a digest. Use it to delegate an open-ended exploration thread \u2014 a subsystem to map, or a "how does X work here" question \u2014 whenever one emerges during research. To explore several independent areas, launch the agents CONCURRENTLY: put multiple spawn_research_agent calls in one reply and they run in parallel.',
|
|
3953
|
+
"The agent sees nothing of this conversation, so its prompt must be self-contained: the area or question, the paths/symbols to start from, and exactly what the digest must report back. Do NOT use it on small or single-subsystem repos, or to read one known file \u2014 direct tools are faster there."
|
|
3787
3954
|
];
|
|
3788
|
-
if (subagentsEnabled) {
|
|
3789
|
-
lines.push(
|
|
3790
|
-
"",
|
|
3791
|
-
'spawn_research_agent(prompt: string) - Launch a stateless read-only research agent that explores the workspace with its own tools and returns a digest. Use it to delegate an open-ended exploration thread \u2014 a subsystem to map, or a "how does X work here" question \u2014 whenever one emerges during research. To explore several independent areas, launch the agents CONCURRENTLY: put multiple spawn_research_agent calls in one reply and they run in parallel.',
|
|
3792
|
-
"The agent sees nothing of this conversation, so its prompt must be self-contained: the area or question, the paths/symbols to start from, and exactly what the digest must report back. Do NOT use it on small or single-subsystem repos, or to read one known file \u2014 direct tools are faster there."
|
|
3793
|
-
);
|
|
3794
|
-
}
|
|
3795
3955
|
return lines.join("\n");
|
|
3796
3956
|
}
|
|
3797
3957
|
function buildSubagentSystemPrompt() {
|
|
@@ -3829,11 +3989,11 @@ function buildModeExamplesForRunners(runners) {
|
|
|
3829
3989
|
return `${r}: default|plan`;
|
|
3830
3990
|
}).join(", ");
|
|
3831
3991
|
}
|
|
3832
|
-
function verificationModeBlock(
|
|
3992
|
+
function verificationModeBlock() {
|
|
3833
3993
|
return [
|
|
3834
3994
|
"",
|
|
3835
3995
|
"VERIFICATION MODE:",
|
|
3836
|
-
|
|
3996
|
+
"Add a FINAL verification task to the end of the plan (highest order number).",
|
|
3837
3997
|
'This task closes the gap between "every task finished" and "the feature is correct": tasks executed in isolated sessions can each pass while the integrated feature is still short. Its outcome must come from commands and exit codes, never from judgement.',
|
|
3838
3998
|
"This task must:",
|
|
3839
3999
|
'- Have type "ai" and autonomy "AFK" \u2014 it needs no human input',
|
|
@@ -3855,7 +4015,8 @@ function researchPhaseBlock(harnessMode) {
|
|
|
3855
4015
|
];
|
|
3856
4016
|
const toolSpecific = harnessMode ? [
|
|
3857
4017
|
"- 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"
|
|
4018
|
+
"- You are planning, not implementing: do NOT edit, create, or delete any file, and do not run commands that change the workspace",
|
|
4019
|
+
"- 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
4020
|
] : [
|
|
3860
4021
|
"- Use list_dir with depth=2-3 for a tree overview",
|
|
3861
4022
|
"- Use glob and grep to find relevant source files",
|
|
@@ -3868,35 +4029,14 @@ function researchPhaseBlock(harnessMode) {
|
|
|
3868
4029
|
"- Research and questions can interleave \u2014 you can explore a file, then ask a question grounded in it, in one response"
|
|
3869
4030
|
].join("\n");
|
|
3870
4031
|
}
|
|
3871
|
-
function buildConversationSystemPrompt(goal, context, modelsByRunner, runners, runnerModes, autonomousDefault = true,
|
|
4032
|
+
function buildConversationSystemPrompt(goal, context, modelsByRunner, runners, runnerModes, autonomousDefault = true, verificationEnabled = false, harnessMode = false) {
|
|
3872
4033
|
const modelsJson = modelsJsonFor(modelsByRunner, runners);
|
|
3873
4034
|
const modeGuide = runnerModes ? buildModeGuide(runnerModes, autonomousDefault) : buildModeGuideForRunners(runners, autonomousDefault);
|
|
3874
4035
|
const modeExamples = runnerModes ? Object.entries(runnerModes).map(([r, ms]) => {
|
|
3875
4036
|
const filtered = filteredBuildModes(ms, autonomousDefault);
|
|
3876
4037
|
return `${r}: ${filtered.map((m) => m.id).join("|")}${ms.some((m) => m.id === "plan") ? "|plan" : ""}`;
|
|
3877
4038
|
}).join(", ") : buildModeExamplesForRunners(runners);
|
|
3878
|
-
const
|
|
3879
|
-
"",
|
|
3880
|
-
"INTERVIEW MODE \u2014 GRILL-ME:",
|
|
3881
|
-
"Interview the user relentlessly about every aspect of their goal until you reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one.",
|
|
3882
|
-
"Ask one question at a time, waiting for the user's answer before continuing. For each question, propose your recommended answer so they can confirm or correct it. Asking multiple questions at once is bewildering.",
|
|
3883
|
-
"If a fact can be found by exploring the codebase, look it up instead of asking. The decisions, though, are the user's \u2014 put each one to them and wait.",
|
|
3884
|
-
"Only propose an outline after the user has answered enough questions that you genuinely understand the goal. Then present the prose outline and wait for explicit confirmation before emitting the task plan JSON."
|
|
3885
|
-
].join("\n") : "";
|
|
3886
|
-
const prdBlock = prdEnabled ? [
|
|
3887
|
-
"",
|
|
3888
|
-
"PRD MODE: Before producing the outline, generate a PRD in two steps.",
|
|
3889
|
-
"Step 1 \u2014 Propose a short prose preview containing: the problem statement, the proposed approach, the testing seams, and the main risks. Prefer existing seams over new ones; propose new seams at the highest point possible. The fewer seams across the codebase, the better \u2014 the ideal number is one. Propose a feature-slug for saving the PRD (e.g. `my-feature`). Ask the user whether they agree.",
|
|
3890
|
-
"Step 2 \u2014 Only after the user explicitly agrees, write the full markdown PRD using the standard PRD format (problem, user stories, implementation decisions, testing seams, out of scope). The user may edit the proposed feature-slug before you write the PRD; use the agreed slug in the final output.",
|
|
3891
|
-
'PRD content rules: user stories are an extensive numbered list, each "As an <actor>, I want <feature>, so that <benefit>". Record implementation decisions as decisions (modules, interfaces, schema changes, API contracts) \u2014 do NOT include specific file paths or code snippets; they go stale fast. Exception: a snippet from a prototype that encodes a decision more precisely than prose (state machine, reducer, schema, type shape) may be inlined, trimmed to the decision-rich parts.',
|
|
3892
|
-
"When writing the full PRD, wrap it EXACTLY with these markers so the system can save it automatically:",
|
|
3893
|
-
'<!-- ORDEWELL_PRD_START slug="<feature-slug>" -->',
|
|
3894
|
-
"... full markdown PRD ...",
|
|
3895
|
-
"<!-- ORDEWELL_PRD_END -->",
|
|
3896
|
-
"Do NOT emit the full PRD before the user agrees to the preview. Do NOT emit task plan JSON before the PRD is agreed and the outline is confirmed."
|
|
3897
|
-
].join("\n") : "";
|
|
3898
|
-
const reviewBlock = reviewEnabled ? reviewModeBlock() : "";
|
|
3899
|
-
const verificationBlock = verificationEnabled ? verificationModeBlock(reviewEnabled) : "";
|
|
4039
|
+
const verificationBlock = verificationEnabled ? verificationModeBlock() : "";
|
|
3900
4040
|
return buildConversationBody(
|
|
3901
4041
|
goal,
|
|
3902
4042
|
context,
|
|
@@ -3905,34 +4045,10 @@ function buildConversationSystemPrompt(goal, context, modelsByRunner, runners, r
|
|
|
3905
4045
|
modeGuide,
|
|
3906
4046
|
modeExamples,
|
|
3907
4047
|
harnessMode,
|
|
3908
|
-
grillMeBlock,
|
|
3909
|
-
prdBlock,
|
|
3910
|
-
reviewBlock,
|
|
3911
4048
|
verificationBlock
|
|
3912
4049
|
);
|
|
3913
4050
|
}
|
|
3914
|
-
function
|
|
3915
|
-
return [
|
|
3916
|
-
"",
|
|
3917
|
-
"REVIEW MODE:",
|
|
3918
|
-
"Add a FINAL review task to the end of the plan (highest order number).",
|
|
3919
|
-
"This task must:",
|
|
3920
|
-
'- Have type "ai" and use the STRONGEST available model',
|
|
3921
|
-
"- Have dependencies on ALL other AI/user tasks in the plan",
|
|
3922
|
-
"- Be assigned appropriately as HITL (human review) since the results need human sign-off",
|
|
3923
|
-
"- Its prompt should instruct the agent to:",
|
|
3924
|
-
" * Review every completed task's outputs and verify the original goal was met",
|
|
3925
|
-
" * Check that each task delivers what its description promised",
|
|
3926
|
-
" * Review the work on two axes, reported SEPARATELY so one cannot mask the other:",
|
|
3927
|
-
" - SPEC axis: if a PRD exists for this plan (`.scratch/<slug>/PRD.md`), verify against it: (a) requirements asked for but missing or partial; (b) behaviour delivered that the PRD never asked for (scope creep); (c) requirements that look implemented but wrongly. Quote the PRD line for each finding. With no PRD, review against the original goal.",
|
|
3928
|
-
" - STANDARDS axis: check the diff against the repo's documented coding standards (CONTRIBUTING.md, CLAUDE.md, etc.) plus this smell baseline: Mysterious Name (rename), Duplicated Code (extract shared shape), Feature Envy (move method to the data), Data Clumps (bundle into a type), Primitive Obsession (give the concept a type), Repeated Switches (polymorphism or shared map), Shotgun Surgery (gather into one module), Divergent Change (split by reason to change), Speculative Generality (delete it), Message Chains (hide the walk), Middle Man (call the target direct), Refused Bequest (composition over inheritance). Smell findings are always judgement calls, never hard failures; documented repo standards override the baseline; skip anything tooling already enforces.",
|
|
3929
|
-
" * Produce a structured review report listing what was checked and whether each check passed",
|
|
3930
|
-
" * Flag any gaps, bugs, or incomplete work",
|
|
3931
|
-
" * Output a final PASS/FAIL conclusion with evidence (standards-axis judgement calls alone must not fail the verification)",
|
|
3932
|
-
"- Use the same runner as the other tasks"
|
|
3933
|
-
].join("\n");
|
|
3934
|
-
}
|
|
3935
|
-
function buildConversationBody(goal, context, modelsJson, runners, modeGuide, modeExamples, harnessMode, grillMeBlock, prdBlock, reviewBlock, verificationBlock) {
|
|
4051
|
+
function buildConversationBody(goal, context, modelsJson, runners, modeGuide, modeExamples, harnessMode, verificationBlock) {
|
|
3936
4052
|
return [
|
|
3937
4053
|
"You are Ordewell's project planner. You explore the codebase, ask concise clarifying questions grounded in your findings, and produce structured task plans as JSON. Be direct: avoid unnecessary preamble, summaries, or explanations unless the user asks for detail.",
|
|
3938
4054
|
"",
|
|
@@ -3943,9 +4059,6 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
3943
4059
|
'4. After the user confirms the outline, emit the final task plan as a JSON object with a "tasks" array.',
|
|
3944
4060
|
"",
|
|
3945
4061
|
researchPhaseBlock(harnessMode),
|
|
3946
|
-
grillMeBlock,
|
|
3947
|
-
prdBlock,
|
|
3948
|
-
reviewBlock,
|
|
3949
4062
|
verificationBlock,
|
|
3950
4063
|
"",
|
|
3951
4064
|
"OUTLINE PHASE:",
|
|
@@ -4020,6 +4133,11 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
4020
4133
|
"TASK MODE:",
|
|
4021
4134
|
modeGuide,
|
|
4022
4135
|
"",
|
|
4136
|
+
// Shared by both variants on purpose: the read channel is a text envelope
|
|
4137
|
+
// exactly so a harness planner, which Ordewell cannot hand tools to, speaks
|
|
4138
|
+
// the same protocol as an API-backed one (ADR-0009).
|
|
4139
|
+
...TASK_QUERY_PROTOCOL,
|
|
4140
|
+
"",
|
|
4023
4141
|
context ? `PROJECT CONTEXT:
|
|
4024
4142
|
${context}
|
|
4025
4143
|
` : "",
|
|
@@ -4127,7 +4245,6 @@ function getPlanTemplate() {
|
|
|
4127
4245
|
"",
|
|
4128
4246
|
"TASK MODE:",
|
|
4129
4247
|
"{{MODE_GUIDE}}",
|
|
4130
|
-
"{{REVIEW_BLOCK}}",
|
|
4131
4248
|
"{{VERIFICATION_BLOCK}}",
|
|
4132
4249
|
"",
|
|
4133
4250
|
"CONTEXT:",
|
|
@@ -4174,10 +4291,8 @@ function buildPlanPromptBase(userGoal, context, researchResults, modelsByRunner,
|
|
|
4174
4291
|
const filtered = filteredBuildModes(ms, autonomousDefault);
|
|
4175
4292
|
return `${r}: ${filtered.map((m) => m.id).join("|")}${ms.some((m) => m.id === "plan") ? "|plan" : ""}`;
|
|
4176
4293
|
}).join(", ") : buildModeExamplesForRunners(runners);
|
|
4177
|
-
return template.replace("{{RESEARCH_SECTION}}", researchReplacement).replace("{{CLARIFICATION_INSTRUCTIONS}}", ONE_SHOT_INSTRUCTIONS).replace("{{MODELS_JSON}}", modelsJson).replace("{{RUNNER_CHOICES}}", runners.join("|")).replace("{{RUNNER_INSTRUCTION}}", runnerInstruction(runners)).replace("{{MODE_GUIDE}}", modeGuide).replace("{{
|
|
4178
|
-
${
|
|
4179
|
-
` : "").replace("{{VERIFICATION_BLOCK}}", scoped.verification ? `
|
|
4180
|
-
${verificationModeBlock(scoped.review)}
|
|
4294
|
+
return template.replace("{{RESEARCH_SECTION}}", researchReplacement).replace("{{CLARIFICATION_INSTRUCTIONS}}", ONE_SHOT_INSTRUCTIONS).replace("{{MODELS_JSON}}", modelsJson).replace("{{RUNNER_CHOICES}}", runners.join("|")).replace("{{RUNNER_INSTRUCTION}}", runnerInstruction(runners)).replace("{{MODE_GUIDE}}", modeGuide).replace("{{VERIFICATION_BLOCK}}", scoped.verification ? `
|
|
4295
|
+
${verificationModeBlock()}
|
|
4181
4296
|
` : "").replace("{{MODE_EXAMPLES}}", modeExamples).replace("{{CONTEXT}}", context || "(no additional context)").replace("{{RESEARCH_RESULTS}}", researchResults ? `
|
|
4182
4297
|
|
|
4183
4298
|
RESEARCH FINDINGS:
|
|
@@ -4327,7 +4442,8 @@ function buildMergePrompt(taskIds, tasks) {
|
|
|
4327
4442
|
"The merged task takes the union of their dependencies (excluding the merged tasks themselves) and preserves their user stories.",
|
|
4328
4443
|
'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
4444
|
'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":"..."}}}]}
|
|
4445
|
+
` {"taskOps":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}`,
|
|
4446
|
+
`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
4447
|
].join("\n");
|
|
4332
4448
|
}
|
|
4333
4449
|
function buildSplitPrompt(taskId, tasks) {
|
|
@@ -4339,7 +4455,8 @@ function buildSplitPrompt(taskId, tasks) {
|
|
|
4339
4455
|
"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
4456
|
'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
4457
|
'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":"..."}},...]}]}
|
|
4458
|
+
` {"taskOps":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}`,
|
|
4459
|
+
`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
4460
|
].join("\n");
|
|
4344
4461
|
}
|
|
4345
4462
|
|
|
@@ -4366,6 +4483,9 @@ function truncatedPlanReEmitPrompt(historyCompacted) {
|
|
|
4366
4483
|
function reEmitTaskOpsPrompt(detail) {
|
|
4367
4484
|
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
4485
|
}
|
|
4486
|
+
function reEmitTaskQueryPrompt(detail) {
|
|
4487
|
+
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.`;
|
|
4488
|
+
}
|
|
4369
4489
|
function taskOpsRejectedPrompt(errors) {
|
|
4370
4490
|
return `Those task edits were rejected:
|
|
4371
4491
|
- ${errors.join("\n- ")}
|
|
@@ -4392,6 +4512,16 @@ function classifyPlannerReply(text, opts) {
|
|
|
4392
4512
|
}
|
|
4393
4513
|
}
|
|
4394
4514
|
}
|
|
4515
|
+
if (textHasTaskQuery(text)) {
|
|
4516
|
+
try {
|
|
4517
|
+
return { kind: "task_query", query: parseTaskQueryJson(text) };
|
|
4518
|
+
} catch (err) {
|
|
4519
|
+
if (!(err instanceof PlanParseError)) throw err;
|
|
4520
|
+
if (extractObjectsWithKey(text, TASK_QUERY_ENVELOPE_KEY).matches.length > 0) {
|
|
4521
|
+
return { kind: "broken_task_query", error: err };
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4395
4525
|
if (text.includes(`"${PLAN_ENVELOPE_KEY}"`)) {
|
|
4396
4526
|
try {
|
|
4397
4527
|
return { kind: "plan", tasks: parsePlanJson(text, opts.runners, opts.runnerModes, opts.autonomousDefault) };
|
|
@@ -4525,8 +4655,8 @@ var SUBAGENT_EXCLUDED = /* @__PURE__ */ new Set(["fetch", "web_search"]);
|
|
|
4525
4655
|
function subagentToolSpecs() {
|
|
4526
4656
|
return RESEARCH_TOOLS.filter((t) => !SUBAGENT_EXCLUDED.has(t.name));
|
|
4527
4657
|
}
|
|
4528
|
-
function withSpawn(
|
|
4529
|
-
return
|
|
4658
|
+
function withSpawn() {
|
|
4659
|
+
return [...RESEARCH_TOOLS, SPAWN_RESEARCH_AGENT_SPEC];
|
|
4530
4660
|
}
|
|
4531
4661
|
function projectOpenAi(specs) {
|
|
4532
4662
|
return specs.map((t) => ({
|
|
@@ -4547,8 +4677,8 @@ function projectOpenAi(specs) {
|
|
|
4547
4677
|
}
|
|
4548
4678
|
}));
|
|
4549
4679
|
}
|
|
4550
|
-
function toOpenAiTools(
|
|
4551
|
-
return projectOpenAi(withSpawn(
|
|
4680
|
+
function toOpenAiTools() {
|
|
4681
|
+
return projectOpenAi(withSpawn());
|
|
4552
4682
|
}
|
|
4553
4683
|
function toOpenAiSubagentTools() {
|
|
4554
4684
|
return projectOpenAi(subagentToolSpecs());
|
|
@@ -4559,8 +4689,8 @@ var SCHEMA_TYPE = {
|
|
|
4559
4689
|
array: SchemaType.ARRAY,
|
|
4560
4690
|
boolean: SchemaType.BOOLEAN
|
|
4561
4691
|
};
|
|
4562
|
-
function toGeminiToolDeclarations(
|
|
4563
|
-
return withSpawn(
|
|
4692
|
+
function toGeminiToolDeclarations() {
|
|
4693
|
+
return withSpawn().map((t) => {
|
|
4564
4694
|
const properties = {};
|
|
4565
4695
|
for (const [key, param] of Object.entries(t.properties)) {
|
|
4566
4696
|
properties[key] = {
|
|
@@ -4657,33 +4787,10 @@ function compactResearchResults(resultsText, maxChars = COMPACTION_LIMITS.resear
|
|
|
4657
4787
|
return (blocks[0].lines.length === 0 ? texts.slice(1) : texts).join("\n");
|
|
4658
4788
|
}
|
|
4659
4789
|
|
|
4660
|
-
// src/utils/prdStore.ts
|
|
4661
|
-
import * as fs from "fs";
|
|
4662
|
-
import * as path4 from "path";
|
|
4663
|
-
var PRD_BLOCK_RE = /<!--\s*ORDEWELL_PRD_START\s+slug="([^"]+)"\s*-->\s*([\s\S]*?)\s*<!--\s*ORDEWELL_PRD_END\s*-->/;
|
|
4664
|
-
function extractPrdBlock(text) {
|
|
4665
|
-
const match = PRD_BLOCK_RE.exec(text);
|
|
4666
|
-
if (!match) return null;
|
|
4667
|
-
const slug2 = sanitizeSlug(match[1]);
|
|
4668
|
-
const markdown = match[2].trim();
|
|
4669
|
-
if (!slug2 || !markdown) return null;
|
|
4670
|
-
return { slug: slug2, markdown };
|
|
4671
|
-
}
|
|
4672
|
-
function sanitizeSlug(raw) {
|
|
4673
|
-
return raw.toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
4674
|
-
}
|
|
4675
|
-
function savePrdMarkdown(workspace, slug2, markdown) {
|
|
4676
|
-
const dir = path4.join(workspace, ".scratch", sanitizeSlug(slug2));
|
|
4677
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
4678
|
-
const file = path4.join(dir, "PRD.md");
|
|
4679
|
-
fs.writeFileSync(file, markdown.endsWith("\n") ? markdown : markdown + "\n", "utf8");
|
|
4680
|
-
return file;
|
|
4681
|
-
}
|
|
4682
|
-
|
|
4683
4790
|
// src/services/ContextCollector.ts
|
|
4684
4791
|
var ORDEWELL_CONTEXT_MAX = 8e3;
|
|
4685
|
-
async function collectResearchContext(
|
|
4686
|
-
const collector = new ContextCollector(
|
|
4792
|
+
async function collectResearchContext(fs12, runners) {
|
|
4793
|
+
const collector = new ContextCollector(fs12);
|
|
4687
4794
|
let contextStr = "";
|
|
4688
4795
|
let firstContext = null;
|
|
4689
4796
|
for (const r of runners) {
|
|
@@ -4710,8 +4817,8 @@ ${firstContext.dirStructure}
|
|
|
4710
4817
|
return contextStr;
|
|
4711
4818
|
}
|
|
4712
4819
|
var ContextCollector = class {
|
|
4713
|
-
constructor(
|
|
4714
|
-
this.fs =
|
|
4820
|
+
constructor(fs12, registry) {
|
|
4821
|
+
this.fs = fs12;
|
|
4715
4822
|
this.registry = registry;
|
|
4716
4823
|
}
|
|
4717
4824
|
fs;
|
|
@@ -4759,22 +4866,22 @@ var ALLOWED_TOOL_NAMES = new Set(subagentToolSpecs().map((t) => t.name));
|
|
|
4759
4866
|
function refused(output) {
|
|
4760
4867
|
return { success: false, output, truncated: false };
|
|
4761
4868
|
}
|
|
4762
|
-
function nonPromptingFs(
|
|
4763
|
-
const withinWorkspace = (p) => resolveWithin(
|
|
4869
|
+
function nonPromptingFs(fs12) {
|
|
4870
|
+
const withinWorkspace = (p) => resolveWithin(fs12.getWorkspaceRoot(), p ?? ".").inside;
|
|
4764
4871
|
const outsideRefusal = (p) => refused(
|
|
4765
4872
|
`Path "${p}" is outside the workspace. Research agents are confined to the workspace root \u2014 note the gap in your digest and let the planner request it directly.`
|
|
4766
4873
|
);
|
|
4767
4874
|
return {
|
|
4768
|
-
getWorkspaceRoot: () =>
|
|
4769
|
-
readFile: (p, opts) => withinWorkspace(p) ?
|
|
4875
|
+
getWorkspaceRoot: () => fs12.getWorkspaceRoot(),
|
|
4876
|
+
readFile: (p, opts) => withinWorkspace(p) ? fs12.readFile(p, opts) : Promise.resolve(outsideRefusal(p)),
|
|
4770
4877
|
readFiles: (paths) => {
|
|
4771
4878
|
const escaping = paths.find((p) => !withinWorkspace(p));
|
|
4772
|
-
return escaping ? Promise.resolve(outsideRefusal(escaping)) :
|
|
4879
|
+
return escaping ? Promise.resolve(outsideRefusal(escaping)) : fs12.readFiles(paths);
|
|
4773
4880
|
},
|
|
4774
|
-
glob: (pattern, opts) => withinWorkspace(opts?.path) ?
|
|
4775
|
-
grep: (pattern, opts) => withinWorkspace(opts?.path) ?
|
|
4776
|
-
findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ?
|
|
4777
|
-
listDir: (p, depth) => withinWorkspace(p) ?
|
|
4881
|
+
glob: (pattern, opts) => withinWorkspace(opts?.path) ? fs12.glob(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
4882
|
+
grep: (pattern, opts) => withinWorkspace(opts?.path) ? fs12.grep(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
4883
|
+
findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs12.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
4884
|
+
listDir: (p, depth) => withinWorkspace(p) ? fs12.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
|
|
4778
4885
|
bash: async (command, signal) => {
|
|
4779
4886
|
const dialect = resolveResearchShell().dialect;
|
|
4780
4887
|
const { tier, reason } = classifyCommand(command, { dialect });
|
|
@@ -4785,13 +4892,13 @@ function nonPromptingFs(fs10) {
|
|
|
4785
4892
|
}
|
|
4786
4893
|
const escaping = pathLikeArgs(command, { dialect }).find((p) => !withinWorkspace(p));
|
|
4787
4894
|
if (escaping) return outsideRefusal(escaping);
|
|
4788
|
-
return
|
|
4895
|
+
return fs12.bash(command, signal);
|
|
4789
4896
|
}
|
|
4790
4897
|
};
|
|
4791
4898
|
}
|
|
4792
4899
|
async function runLoop(prompt, deps) {
|
|
4793
4900
|
if (deps.signal?.aborted) return "[research agent aborted before starting]";
|
|
4794
|
-
const
|
|
4901
|
+
const fs12 = nonPromptingFs(deps.fs);
|
|
4795
4902
|
const chat = deps.createChat((delta) => deps.onProgress?.({ type: "thinking", text: delta }));
|
|
4796
4903
|
let turn = await chat.sendMessage(prompt, deps.signal);
|
|
4797
4904
|
for (let step = 0; turn.hasToolCalls && step < SUBAGENT_LIMITS.maxSteps; step++) {
|
|
@@ -4808,7 +4915,7 @@ ${turn.text}`;
|
|
|
4808
4915
|
if (tc.name === "read_file" && !("maxBytes" in args) && !("limit" in args)) args.limit = 2e3;
|
|
4809
4916
|
const toolArgs = JSON.stringify(tc.args);
|
|
4810
4917
|
deps.onProgress?.({ type: "tool_call", tool: tc.name, toolArgs, toolCallId: tc.id });
|
|
4811
|
-
const res = await executeTool(tc.name, args,
|
|
4918
|
+
const res = await executeTool(tc.name, args, fs12, void 0, deps.signal);
|
|
4812
4919
|
const output = res.output.length > SUBAGENT_LIMITS.toolOutputMaxChars ? res.output.slice(0, SUBAGENT_LIMITS.toolOutputMaxChars) + `
|
|
4813
4920
|
[... truncated to ${SUBAGENT_LIMITS.toolOutputMaxChars} chars, total ${res.output.length}]` : res.output;
|
|
4814
4921
|
const stepEntry = {
|
|
@@ -4866,8 +4973,6 @@ var BaseAiService = class _BaseAiService {
|
|
|
4866
4973
|
config;
|
|
4867
4974
|
conversation = null;
|
|
4868
4975
|
activeAbort = null;
|
|
4869
|
-
/** researchSubagents toggle (issue #34): set per operation from the live settings snapshot; off means bit-for-bit sequential behavior. */
|
|
4870
|
-
researchSubagentsEnabled = false;
|
|
4871
4976
|
hasActiveConversation() {
|
|
4872
4977
|
return this.conversation !== null;
|
|
4873
4978
|
}
|
|
@@ -4923,10 +5028,7 @@ var BaseAiService = class _BaseAiService {
|
|
|
4923
5028
|
* steering/failed result and the loop continues sequentially: a subagent can
|
|
4924
5029
|
* never fail a plan or a turn.
|
|
4925
5030
|
*/
|
|
4926
|
-
async executeSpawnAgent(args,
|
|
4927
|
-
if (!this.researchSubagentsEnabled) {
|
|
4928
|
-
return { success: false, output: "spawn_research_agent is not available in this session. Continue researching sequentially with your other tools (read_file, glob, grep, list_dir, bash).", truncated: false };
|
|
4929
|
-
}
|
|
5031
|
+
async executeSpawnAgent(args, fs12, onProgress, signal, subagentId) {
|
|
4930
5032
|
const prompt = typeof args.prompt === "string" ? args.prompt.trim() : "";
|
|
4931
5033
|
if (!prompt) {
|
|
4932
5034
|
return { success: false, output: 'spawn_research_agent requires a non-empty "prompt" string: a self-contained task for the agent, including what its digest must report back.', truncated: false };
|
|
@@ -4937,20 +5039,20 @@ var BaseAiService = class _BaseAiService {
|
|
|
4937
5039
|
if (!chat) throw new Error("subagent chats are not available for this provider");
|
|
4938
5040
|
return chat;
|
|
4939
5041
|
},
|
|
4940
|
-
fs:
|
|
5042
|
+
fs: fs12,
|
|
4941
5043
|
signal,
|
|
4942
5044
|
onProgress: (progress) => onProgress({ ...progress, subagentId })
|
|
4943
5045
|
});
|
|
4944
5046
|
}
|
|
4945
5047
|
/** Collect project context for the planning phase. Shared with the harness backend. */
|
|
4946
|
-
static async collectResearchContext(
|
|
4947
|
-
return collectResearchContext(
|
|
5048
|
+
static async collectResearchContext(fs12, runners) {
|
|
5049
|
+
return collectResearchContext(fs12, runners);
|
|
4948
5050
|
}
|
|
4949
5051
|
/**
|
|
4950
5052
|
* Execute one round of tool calls and report each through onProgress.
|
|
4951
5053
|
* Returns the results to feed back plus the log entries produced.
|
|
4952
5054
|
*/
|
|
4953
|
-
async executeToolCalls(toolCalls,
|
|
5055
|
+
async executeToolCalls(toolCalls, fs12, onProgress, fetcher, stepIndex, thinking, signal) {
|
|
4954
5056
|
const toolResults = [];
|
|
4955
5057
|
const logEntries = [];
|
|
4956
5058
|
let resultsText = "";
|
|
@@ -4969,18 +5071,18 @@ var BaseAiService = class _BaseAiService {
|
|
|
4969
5071
|
const subagentId = uuidv4();
|
|
4970
5072
|
spawnSubagentIds.set(i, subagentId);
|
|
4971
5073
|
onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), subagentId, toolCallId: toolCalls[i].id });
|
|
4972
|
-
precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args },
|
|
5074
|
+
precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args }, fs12, onProgress, signal, subagentId));
|
|
4973
5075
|
}) : Promise.resolve(),
|
|
4974
5076
|
parallelIndexes.length > 1 ? mapWithConcurrency(parallelIndexes, TOOL_ROUND_CONCURRENCY, async (i) => {
|
|
4975
5077
|
onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), toolCallId: toolCalls[i].id });
|
|
4976
|
-
precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i],
|
|
5078
|
+
precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i], fs12, fetcher, signal));
|
|
4977
5079
|
}) : Promise.resolve()
|
|
4978
5080
|
]);
|
|
4979
5081
|
for (const [index, tc] of toolCalls.entries()) {
|
|
4980
5082
|
const toolArgs = normalizedArgs[index];
|
|
4981
5083
|
const preResult = precomputed.get(index);
|
|
4982
5084
|
if (!preResult) onProgress({ type: "tool_call", tool: tc.name, toolArgs: JSON.stringify(tc.args), toolCallId: tc.id });
|
|
4983
|
-
const toolResult = preResult ?? await executeTool(tc.name, toolArgs,
|
|
5085
|
+
const toolResult = preResult ?? await executeTool(tc.name, toolArgs, fs12, fetcher, signal);
|
|
4984
5086
|
const MAX_RESPONSE_CHARS = 5e4;
|
|
4985
5087
|
const truncatedResponse = toolResult.success && (toolResult.truncated || toolResult.output.length > MAX_RESPONSE_CHARS);
|
|
4986
5088
|
const llmOutput = toolResult.output.length > MAX_RESPONSE_CHARS ? toolResult.output.slice(0, MAX_RESPONSE_CHARS) + `
|
|
@@ -5112,7 +5214,6 @@ ${llmOutput}
|
|
|
5112
5214
|
researchLog
|
|
5113
5215
|
};
|
|
5114
5216
|
}
|
|
5115
|
-
if (extractPrdBlock(turn.text)) ctx.prdCaptured = true;
|
|
5116
5217
|
const reply = classifyPlannerReply(turn.text, {
|
|
5117
5218
|
runners: ctx.runners,
|
|
5118
5219
|
runnerModes: ctx.runnerModes,
|
|
@@ -5121,17 +5222,11 @@ ${llmOutput}
|
|
|
5121
5222
|
switch (reply.kind) {
|
|
5122
5223
|
case "task_ops":
|
|
5123
5224
|
return { kind: "task_ops", ops: reply.ops, text: turn.text, researchLog };
|
|
5225
|
+
// A read is settled by the Session (it owns the plan and the catalog),
|
|
5226
|
+
// so it leaves this loop the same way a plan or an edit does.
|
|
5227
|
+
case "task_query":
|
|
5228
|
+
return { kind: "task_query", query: reply.query, text: turn.text, researchLog };
|
|
5124
5229
|
case "plan":
|
|
5125
|
-
if (ctx.prdEnabled && !ctx.prdCaptured && !ctx.prdNudgeSent && !signal?.aborted) {
|
|
5126
|
-
ctx.prdNudgeSent = true;
|
|
5127
|
-
pending = [
|
|
5128
|
-
"PRD mode is enabled but no PRD has been produced yet, so the plan was NOT committed.",
|
|
5129
|
-
"In your next reply: first output the full markdown PRD wrapped EXACTLY in",
|
|
5130
|
-
'<!-- ORDEWELL_PRD_START slug="<feature-slug>" --> and <!-- ORDEWELL_PRD_END -->,',
|
|
5131
|
-
"then re-emit the exact same task plan JSON after the PRD block."
|
|
5132
|
-
].join(" ");
|
|
5133
|
-
continue;
|
|
5134
|
-
}
|
|
5135
5230
|
return { kind: "plan", tasks: reply.tasks, text: turn.text, researchLog };
|
|
5136
5231
|
// Botched attempts get a bounded corrective retry — otherwise the
|
|
5137
5232
|
// broken JSON would surface as a prose bubble and the edit or plan
|
|
@@ -5143,6 +5238,13 @@ ${llmOutput}
|
|
|
5143
5238
|
continue;
|
|
5144
5239
|
}
|
|
5145
5240
|
break;
|
|
5241
|
+
case "broken_task_query":
|
|
5242
|
+
if (jsonRepairAttempts < MAX_JSON_REPAIRS2 && !signal?.aborted) {
|
|
5243
|
+
jsonRepairAttempts++;
|
|
5244
|
+
pending = reEmitTaskQueryPrompt(reply.error.message);
|
|
5245
|
+
continue;
|
|
5246
|
+
}
|
|
5247
|
+
break;
|
|
5146
5248
|
case "broken_plan":
|
|
5147
5249
|
if (jsonRepairAttempts < MAX_JSON_REPAIRS2 && !signal?.aborted) {
|
|
5148
5250
|
jsonRepairAttempts++;
|
|
@@ -5166,7 +5268,7 @@ ${llmOutput}
|
|
|
5166
5268
|
* parsed tasks if a plan was emitted mid-loop, or null if the loop
|
|
5167
5269
|
* exhausted without a valid plan.
|
|
5168
5270
|
*/
|
|
5169
|
-
async runResearchLoop(chat, firstMessage,
|
|
5271
|
+
async runResearchLoop(chat, firstMessage, fs12, onProgress, runners, initialLog, fetcher, userGoal, runnerModes, autonomousDefault, signal) {
|
|
5170
5272
|
const researchLog = initialLog ? [...initialLog] : [];
|
|
5171
5273
|
if (!initialLog && userGoal) {
|
|
5172
5274
|
researchLog.push({
|
|
@@ -5193,7 +5295,7 @@ ${llmOutput}
|
|
|
5193
5295
|
if (!turn.hasToolCalls) break;
|
|
5194
5296
|
const { toolResults, logEntries, resultsText } = await this.executeToolCalls(
|
|
5195
5297
|
turn.toolCalls,
|
|
5196
|
-
|
|
5298
|
+
fs12,
|
|
5197
5299
|
onProgress,
|
|
5198
5300
|
fetcher,
|
|
5199
5301
|
step,
|
|
@@ -5331,9 +5433,6 @@ var GeminiService = class extends BaseAiService {
|
|
|
5331
5433
|
req.runners,
|
|
5332
5434
|
req.runnerModes,
|
|
5333
5435
|
req.autonomousDefault ?? true,
|
|
5334
|
-
req.grillMeEnabled ?? false,
|
|
5335
|
-
req.prdEnabled ?? false,
|
|
5336
|
-
req.reviewEnabled ?? false,
|
|
5337
5436
|
req.verificationEnabled ?? false
|
|
5338
5437
|
);
|
|
5339
5438
|
const seededHistory = [
|
|
@@ -5372,8 +5471,7 @@ ${m.content}` });
|
|
|
5372
5471
|
runners: req.runners,
|
|
5373
5472
|
runnerModes: req.runnerModes,
|
|
5374
5473
|
autonomousDefault: req.autonomousDefault,
|
|
5375
|
-
fetcher: req.fetcher
|
|
5376
|
-
prdEnabled: req.prdEnabled ?? false
|
|
5474
|
+
fetcher: req.fetcher
|
|
5377
5475
|
};
|
|
5378
5476
|
this.conversation = { ctx, setProgress: (onProgress) => {
|
|
5379
5477
|
currentProgress = onProgress;
|
|
@@ -5431,11 +5529,11 @@ ${repairHint}` }] : parts;
|
|
|
5431
5529
|
return this.consumePlanStream(result, onToken);
|
|
5432
5530
|
}
|
|
5433
5531
|
// --- IAiService implementation ---
|
|
5434
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
5532
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs12, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
5435
5533
|
this.ensureInit();
|
|
5436
5534
|
const { autonomousDefault } = modes;
|
|
5437
5535
|
const genModel = this.getPlanningModel();
|
|
5438
|
-
const contextStr = await BaseAiService.collectResearchContext(
|
|
5536
|
+
const contextStr = await BaseAiService.collectResearchContext(fs12, runners);
|
|
5439
5537
|
const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
|
|
5440
5538
|
const firstMessage = `${buildResearchToolsPrompt()}
|
|
5441
5539
|
|
|
@@ -5446,17 +5544,17 @@ Explore the workspace to understand the codebase, then generate the plan.`;
|
|
|
5446
5544
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
5447
5545
|
});
|
|
5448
5546
|
const researchChat = new GeminiResearchChat(chat);
|
|
5449
|
-
const result = await this.runResearchLoop(researchChat, firstMessage,
|
|
5547
|
+
const result = await this.runResearchLoop(researchChat, firstMessage, fs12, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
|
|
5450
5548
|
if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
|
|
5451
5549
|
const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
|
|
5452
5550
|
return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
|
|
5453
5551
|
}
|
|
5454
|
-
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken,
|
|
5552
|
+
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs12, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
|
|
5455
5553
|
this.ensureInit();
|
|
5456
5554
|
const { autonomousDefault } = modes;
|
|
5457
5555
|
let ctx = "";
|
|
5458
|
-
if (
|
|
5459
|
-
const collected = await new ContextCollector(
|
|
5556
|
+
if (fs12) {
|
|
5557
|
+
const collected = await new ContextCollector(fs12).collect(runners[0] ?? "claude-code");
|
|
5460
5558
|
if (collected.aiflowContext) ctx = `
|
|
5461
5559
|
<aiflow_context>
|
|
5462
5560
|
${collected.aiflowContext}
|
|
@@ -5472,12 +5570,12 @@ ${collected.aiflowContext}
|
|
|
5472
5570
|
autonomousDefault
|
|
5473
5571
|
);
|
|
5474
5572
|
}
|
|
5475
|
-
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress,
|
|
5573
|
+
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs12, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
|
|
5476
5574
|
this.ensureInit();
|
|
5477
5575
|
const { autonomousDefault } = modes;
|
|
5478
5576
|
let aiflowContext;
|
|
5479
|
-
if (
|
|
5480
|
-
const collected = await new ContextCollector(
|
|
5577
|
+
if (fs12) {
|
|
5578
|
+
const collected = await new ContextCollector(fs12).collect(existingPlan.runners[0] ?? "claude-code");
|
|
5481
5579
|
if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
|
|
5482
5580
|
}
|
|
5483
5581
|
const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
|
|
@@ -5660,7 +5758,6 @@ var OpenAiService = class extends BaseAiService {
|
|
|
5660
5758
|
async startConversation(req) {
|
|
5661
5759
|
this.ensureInit();
|
|
5662
5760
|
const client = this.getClient();
|
|
5663
|
-
this.researchSubagentsEnabled = req.researchSubagentsEnabled ?? false;
|
|
5664
5761
|
const contextStr = await BaseAiService.collectResearchContext(req.fs, req.runners);
|
|
5665
5762
|
const systemPrompt = buildConversationSystemPrompt(
|
|
5666
5763
|
req.goal,
|
|
@@ -5669,16 +5766,13 @@ var OpenAiService = class extends BaseAiService {
|
|
|
5669
5766
|
req.runners,
|
|
5670
5767
|
req.runnerModes,
|
|
5671
5768
|
req.autonomousDefault ?? true,
|
|
5672
|
-
req.grillMeEnabled ?? false,
|
|
5673
|
-
req.prdEnabled ?? false,
|
|
5674
|
-
req.reviewEnabled ?? false,
|
|
5675
5769
|
req.verificationEnabled ?? false
|
|
5676
5770
|
);
|
|
5677
5771
|
const userText = req.goal;
|
|
5678
5772
|
const messages = [
|
|
5679
5773
|
{ role: "system", content: `${systemPrompt}
|
|
5680
5774
|
|
|
5681
|
-
${buildResearchToolsPrompt(
|
|
5775
|
+
${buildResearchToolsPrompt()}` }
|
|
5682
5776
|
];
|
|
5683
5777
|
for (const m of req.priorHistory ?? []) {
|
|
5684
5778
|
messages.push({ role: m.role, content: m.content });
|
|
@@ -5688,7 +5782,7 @@ ${buildResearchToolsPrompt(this.researchSubagentsEnabled)}` }
|
|
|
5688
5782
|
messages,
|
|
5689
5783
|
client,
|
|
5690
5784
|
this.requireModel("orchestratorModel", this.config.orchestratorModel),
|
|
5691
|
-
toOpenAiTools(
|
|
5785
|
+
toOpenAiTools(),
|
|
5692
5786
|
(delta) => currentProgress({ type: "thinking", text: delta }),
|
|
5693
5787
|
(delta) => currentProgress({ type: "plan_token", planToken: delta })
|
|
5694
5788
|
);
|
|
@@ -5698,8 +5792,7 @@ ${buildResearchToolsPrompt(this.researchSubagentsEnabled)}` }
|
|
|
5698
5792
|
runners: req.runners,
|
|
5699
5793
|
runnerModes: req.runnerModes,
|
|
5700
5794
|
autonomousDefault: req.autonomousDefault,
|
|
5701
|
-
fetcher: req.fetcher
|
|
5702
|
-
prdEnabled: req.prdEnabled ?? false
|
|
5795
|
+
fetcher: req.fetcher
|
|
5703
5796
|
};
|
|
5704
5797
|
this.conversation = {
|
|
5705
5798
|
ctx,
|
|
@@ -5717,15 +5810,14 @@ ${buildResearchToolsPrompt(this.researchSubagentsEnabled)}` }
|
|
|
5717
5810
|
}
|
|
5718
5811
|
}
|
|
5719
5812
|
// --- IAiService implementation ---
|
|
5720
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
5813
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs12, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
5721
5814
|
this.ensureInit();
|
|
5722
5815
|
const client = this.getClient();
|
|
5723
|
-
const { autonomousDefault
|
|
5724
|
-
|
|
5725
|
-
const contextStr = await BaseAiService.collectResearchContext(fs10, runners);
|
|
5816
|
+
const { autonomousDefault } = modes;
|
|
5817
|
+
const contextStr = await BaseAiService.collectResearchContext(fs12, runners);
|
|
5726
5818
|
const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
|
|
5727
5819
|
const userText = `User goal: ${userDescription}`;
|
|
5728
|
-
const firstMessage = `${buildResearchToolsPrompt(
|
|
5820
|
+
const firstMessage = `${buildResearchToolsPrompt()}
|
|
5729
5821
|
|
|
5730
5822
|
Explore the workspace to understand the codebase, then generate the plan.
|
|
5731
5823
|
|
|
@@ -5737,20 +5829,20 @@ ${userText}`;
|
|
|
5737
5829
|
messages,
|
|
5738
5830
|
client,
|
|
5739
5831
|
this.requireModel("orchestratorModel", this.config.orchestratorModel),
|
|
5740
|
-
toOpenAiTools(
|
|
5832
|
+
toOpenAiTools(),
|
|
5741
5833
|
(delta) => onProgress({ type: "thinking", text: delta })
|
|
5742
5834
|
);
|
|
5743
|
-
const result = await this.runResearchLoop(researchChat, firstMessage,
|
|
5835
|
+
const result = await this.runResearchLoop(researchChat, firstMessage, fs12, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
|
|
5744
5836
|
if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
|
|
5745
5837
|
const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
|
|
5746
5838
|
return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
|
|
5747
5839
|
}
|
|
5748
|
-
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken,
|
|
5840
|
+
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs12, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
5749
5841
|
this.ensureInit();
|
|
5750
5842
|
const { autonomousDefault } = modes;
|
|
5751
5843
|
let ctx = "";
|
|
5752
|
-
if (
|
|
5753
|
-
const collected = await new ContextCollector(
|
|
5844
|
+
if (fs12) {
|
|
5845
|
+
const collected = await new ContextCollector(fs12).collect(runners[0] ?? "claude-code");
|
|
5754
5846
|
if (collected.aiflowContext) ctx = `
|
|
5755
5847
|
<aiflow_context>
|
|
5756
5848
|
${collected.aiflowContext}
|
|
@@ -5766,12 +5858,12 @@ ${collected.aiflowContext}
|
|
|
5766
5858
|
autonomousDefault
|
|
5767
5859
|
);
|
|
5768
5860
|
}
|
|
5769
|
-
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress,
|
|
5861
|
+
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs12, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
5770
5862
|
this.ensureInit();
|
|
5771
5863
|
const { autonomousDefault } = modes;
|
|
5772
5864
|
let aiflowContext;
|
|
5773
|
-
if (
|
|
5774
|
-
const collected = await new ContextCollector(
|
|
5865
|
+
if (fs12) {
|
|
5866
|
+
const collected = await new ContextCollector(fs12).collect(existingPlan.runners[0] ?? "claude-code");
|
|
5775
5867
|
if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
|
|
5776
5868
|
}
|
|
5777
5869
|
const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
|
|
@@ -5860,21 +5952,28 @@ function pickWindow(sorted, currentIdx, max) {
|
|
|
5860
5952
|
const window = sorted.slice(start, start + max);
|
|
5861
5953
|
return { window, omitted: sorted.length - window.length };
|
|
5862
5954
|
}
|
|
5863
|
-
function renderPlanMap(
|
|
5864
|
-
|
|
5865
|
-
|
|
5955
|
+
function renderPlanMap(planTasks, currentTaskId, opts) {
|
|
5956
|
+
const rows = flattenTasksWithParents(planTasks);
|
|
5957
|
+
if (rows.length < PLAN_MAP_MIN_TASKS) return "";
|
|
5958
|
+
const sorted = rows.map((r) => ({
|
|
5959
|
+
...r,
|
|
5960
|
+
label: taskOrderLabel(r.task, r.parent ?? void 0)
|
|
5961
|
+
})).sort((a, b) => {
|
|
5962
|
+
const parentDiff = (a.parent?.order ?? -1) - (b.parent?.order ?? -1);
|
|
5963
|
+
if (parentDiff !== 0) return parentDiff;
|
|
5964
|
+
return a.task.order - b.task.order;
|
|
5965
|
+
});
|
|
5866
5966
|
const max = opts?.maxEntries ?? DEFAULT_PLAN_MAP_MAX_ENTRIES;
|
|
5867
|
-
const currentIdx = sorted.findIndex((
|
|
5967
|
+
const currentIdx = sorted.findIndex((r) => r.task.id === currentTaskId);
|
|
5868
5968
|
const { window, omitted } = pickWindow(sorted, currentIdx, max);
|
|
5869
|
-
const currentTask = sorted.find((
|
|
5870
|
-
const
|
|
5871
|
-
|
|
5872
|
-
const
|
|
5873
|
-
const
|
|
5874
|
-
const order = String(t.order).padStart(widthOrder, " ");
|
|
5969
|
+
const currentTask = sorted.find((r) => r.task.id === currentTaskId)?.task;
|
|
5970
|
+
const lines = window.map((row) => {
|
|
5971
|
+
const isCurrent = row.task.id === currentTaskId;
|
|
5972
|
+
const tag = planMapStatus(row.task, isCurrent);
|
|
5973
|
+
const order = row.parent ? row.label : row.label.padStart(2, " ");
|
|
5875
5974
|
const arrow = isCurrent ? " \u2190 you are here" : "";
|
|
5876
|
-
const runner =
|
|
5877
|
-
return `${order}. [${tag.padEnd(7, " ")}] ${
|
|
5975
|
+
const runner = row.task.assignedRunner ? ` (${row.task.assignedRunner})` : "";
|
|
5976
|
+
return `${order}. [${tag.padEnd(7, " ")}] ${row.task.title}${runner}${arrow}`;
|
|
5878
5977
|
});
|
|
5879
5978
|
const footer = omitted > 0 ? `
|
|
5880
5979
|
(${omitted} task${omitted === 1 ? "" : "s"} omitted from this view)` : "";
|
|
@@ -5939,7 +6038,8 @@ function composeAugmentedPrompt(task, allTasks, opts) {
|
|
|
5939
6038
|
const map = renderPlanMap(allTasks, task.id, { maxEntries: opts?.planMapMaxEntries });
|
|
5940
6039
|
if (map) blocks.push(map);
|
|
5941
6040
|
}
|
|
5942
|
-
const
|
|
6041
|
+
const flat = flattenTasks(allTasks);
|
|
6042
|
+
const outputs = collectDirectDependencyOutputs(task, flat);
|
|
5943
6043
|
if (outputs.length > 0) blocks.push(renderPriorOutputs(outputs));
|
|
5944
6044
|
if (opts?.tddEnabled) {
|
|
5945
6045
|
blocks.push(renderTddInstruction());
|
|
@@ -6118,6 +6218,7 @@ function renderTerminalOutput(raw) {
|
|
|
6118
6218
|
return populated.map((r) => cells(r).join("")).join("\n");
|
|
6119
6219
|
}
|
|
6120
6220
|
var MARKER_SCAN_TAIL = 16384;
|
|
6221
|
+
var IDLE_TIMEOUT_MS = 6e4;
|
|
6121
6222
|
var VerdictEngine = class {
|
|
6122
6223
|
markerSeen = /* @__PURE__ */ new Set();
|
|
6123
6224
|
buffers = /* @__PURE__ */ new Map();
|
|
@@ -6125,6 +6226,7 @@ var VerdictEngine = class {
|
|
|
6125
6226
|
pausedSessions = /* @__PURE__ */ new Map();
|
|
6126
6227
|
listeners = [];
|
|
6127
6228
|
checkpointListeners = [];
|
|
6229
|
+
idleListeners = [];
|
|
6128
6230
|
/**
|
|
6129
6231
|
* Per-task generation counter. Incremented on every watch() and clear().
|
|
6130
6232
|
* Stale callbacks (from a prior session whose generation doesn't match
|
|
@@ -6132,12 +6234,47 @@ var VerdictEngine = class {
|
|
|
6132
6234
|
* wrong session.
|
|
6133
6235
|
*/
|
|
6134
6236
|
generations = /* @__PURE__ */ new Map();
|
|
6237
|
+
idleTimers = /* @__PURE__ */ new Map();
|
|
6238
|
+
idleSince = /* @__PURE__ */ new Map();
|
|
6135
6239
|
onVerdict(listener) {
|
|
6136
6240
|
this.listeners.push(listener);
|
|
6137
6241
|
}
|
|
6138
6242
|
onCheckpoint(listener) {
|
|
6139
6243
|
this.checkpointListeners.push(listener);
|
|
6140
6244
|
}
|
|
6245
|
+
onIdleChange(listener) {
|
|
6246
|
+
this.idleListeners.push(listener);
|
|
6247
|
+
}
|
|
6248
|
+
/** Advisory silence timestamp for a task, or null if it isn't idle. */
|
|
6249
|
+
getIdleSince(taskId) {
|
|
6250
|
+
return this.idleSince.get(taskId) ?? null;
|
|
6251
|
+
}
|
|
6252
|
+
/** Restart the silence timer on fresh output; broadcasts the null transition if it was idle. */
|
|
6253
|
+
touchIdle(taskId, gen) {
|
|
6254
|
+
const existing = this.idleTimers.get(taskId);
|
|
6255
|
+
if (existing) clearTimeout(existing);
|
|
6256
|
+
if (this.idleSince.get(taskId)) {
|
|
6257
|
+
this.idleSince.set(taskId, null);
|
|
6258
|
+
for (const l of this.idleListeners) l(taskId, null);
|
|
6259
|
+
}
|
|
6260
|
+
this.idleTimers.set(taskId, setTimeout(() => {
|
|
6261
|
+
if (this.generations.get(taskId) !== gen) return;
|
|
6262
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6263
|
+
this.idleSince.set(taskId, now);
|
|
6264
|
+
for (const l of this.idleListeners) l(taskId, now);
|
|
6265
|
+
}, IDLE_TIMEOUT_MS));
|
|
6266
|
+
}
|
|
6267
|
+
/** Tear down idle tracking for a task; broadcasts the null transition if it was idle. */
|
|
6268
|
+
clearIdle(taskId) {
|
|
6269
|
+
const timer = this.idleTimers.get(taskId);
|
|
6270
|
+
if (timer) clearTimeout(timer);
|
|
6271
|
+
this.idleTimers.delete(taskId);
|
|
6272
|
+
const wasIdle = this.idleSince.get(taskId);
|
|
6273
|
+
this.idleSince.delete(taskId);
|
|
6274
|
+
if (wasIdle) {
|
|
6275
|
+
for (const l of this.idleListeners) l(taskId, null);
|
|
6276
|
+
}
|
|
6277
|
+
}
|
|
6141
6278
|
approveCheckpoint(taskId) {
|
|
6142
6279
|
const session = this.pausedSessions.get(taskId);
|
|
6143
6280
|
if (session) {
|
|
@@ -6168,6 +6305,7 @@ ORDEWELL_REJECT: ${reason}
|
|
|
6168
6305
|
this.checkpointCounts.set(task.id, 0);
|
|
6169
6306
|
session.onOutput((text) => {
|
|
6170
6307
|
if (this.generations.get(task.id) !== gen) return;
|
|
6308
|
+
this.touchIdle(task.id, gen);
|
|
6171
6309
|
if (this.markerSeen.has(task.id)) return;
|
|
6172
6310
|
const buf = (this.buffers.get(task.id) ?? "") + text;
|
|
6173
6311
|
this.buffers.set(task.id, buf);
|
|
@@ -6179,6 +6317,7 @@ ORDEWELL_REJECT: ${reason}
|
|
|
6179
6317
|
this.buffers.delete(task.id);
|
|
6180
6318
|
this.checkpointCounts.delete(task.id);
|
|
6181
6319
|
this.pausedSessions.delete(task.id);
|
|
6320
|
+
this.clearIdle(task.id);
|
|
6182
6321
|
this.generations.set(task.id, gen + 1);
|
|
6183
6322
|
const verdict = this.decide(task, 0);
|
|
6184
6323
|
for (const l of this.listeners) l(task.id, verdict, output);
|
|
@@ -6203,6 +6342,7 @@ ORDEWELL_REJECT: ${reason}
|
|
|
6203
6342
|
this.buffers.delete(task.id);
|
|
6204
6343
|
this.checkpointCounts.delete(task.id);
|
|
6205
6344
|
this.pausedSessions.delete(task.id);
|
|
6345
|
+
this.clearIdle(task.id);
|
|
6206
6346
|
const verdict = this.decide(task, exitCode);
|
|
6207
6347
|
for (const l of this.listeners) l(task.id, verdict, output);
|
|
6208
6348
|
});
|
|
@@ -6213,6 +6353,7 @@ ORDEWELL_REJECT: ${reason}
|
|
|
6213
6353
|
this.buffers.delete(task.id);
|
|
6214
6354
|
this.checkpointCounts.delete(task.id);
|
|
6215
6355
|
this.pausedSessions.delete(task.id);
|
|
6356
|
+
this.clearIdle(task.id);
|
|
6216
6357
|
this.generations.set(task.id, (this.generations.get(task.id) ?? 0) + 1);
|
|
6217
6358
|
return {
|
|
6218
6359
|
outcome: "pass",
|
|
@@ -6234,6 +6375,7 @@ ORDEWELL_REJECT: ${reason}
|
|
|
6234
6375
|
this.buffers.delete(task.id);
|
|
6235
6376
|
this.checkpointCounts.delete(task.id);
|
|
6236
6377
|
this.pausedSessions.delete(task.id);
|
|
6378
|
+
this.clearIdle(task.id);
|
|
6237
6379
|
this.generations.set(task.id, (this.generations.get(task.id) ?? 0) + 1);
|
|
6238
6380
|
}
|
|
6239
6381
|
/** Drop all tracking state (used on stop / loadPlan). */
|
|
@@ -6242,6 +6384,9 @@ ORDEWELL_REJECT: ${reason}
|
|
|
6242
6384
|
this.buffers.clear();
|
|
6243
6385
|
this.checkpointCounts.clear();
|
|
6244
6386
|
this.pausedSessions.clear();
|
|
6387
|
+
for (const timer of this.idleTimers.values()) clearTimeout(timer);
|
|
6388
|
+
this.idleTimers.clear();
|
|
6389
|
+
this.idleSince.clear();
|
|
6245
6390
|
this.generations.clear();
|
|
6246
6391
|
}
|
|
6247
6392
|
decide(task, exitCode) {
|
|
@@ -6647,6 +6792,7 @@ var TaskOrchestrator = class {
|
|
|
6647
6792
|
this.store.markAwaitingUser(taskId);
|
|
6648
6793
|
this.emit("onCheckpoint", { taskId, taskTitle: task.title, summary });
|
|
6649
6794
|
});
|
|
6795
|
+
this.verifier.onIdleChange(() => this.emit("onTaskChanged"));
|
|
6650
6796
|
}
|
|
6651
6797
|
config;
|
|
6652
6798
|
notifications;
|
|
@@ -6671,6 +6817,10 @@ var TaskOrchestrator = class {
|
|
|
6671
6817
|
workspaceRootFn = () => process.cwd();
|
|
6672
6818
|
observers = [];
|
|
6673
6819
|
tddEnabled = () => false;
|
|
6820
|
+
/** Advisory silence timestamp for a task's live runner, or null if not idle. */
|
|
6821
|
+
getIdleSince(taskId) {
|
|
6822
|
+
return this.verifier.getIdleSince(taskId);
|
|
6823
|
+
}
|
|
6674
6824
|
get storeInstance() {
|
|
6675
6825
|
return this.store;
|
|
6676
6826
|
}
|
|
@@ -7058,8 +7208,10 @@ ${output.slice(-3e3)}`);
|
|
|
7058
7208
|
if (ready.length === 0 && this.activeTaskSessions.size === 0) {
|
|
7059
7209
|
const remaining = this.store.allTasks.filter((t) => t.status !== "completed");
|
|
7060
7210
|
console.log(`[TaskOrchestrator] tick(): no work. remaining=${remaining.length}, completed=${this.store.isAllComplete()}`);
|
|
7061
|
-
|
|
7062
|
-
|
|
7211
|
+
const remainingById = new Map(remaining.map((t) => [t.id, t]));
|
|
7212
|
+
for (const { task, parent } of flattenTasksWithParents(this.store.planTasks)) {
|
|
7213
|
+
if (!remainingById.has(task.id)) continue;
|
|
7214
|
+
console.log(` - ${taskOrderLabel(task, parent ?? void 0)}. ${task.title} [${task.type}/${task.status}] prompt=${!!task.prompt}`);
|
|
7063
7215
|
}
|
|
7064
7216
|
if (this.store.isAllComplete()) {
|
|
7065
7217
|
this.running = false;
|
|
@@ -7086,7 +7238,7 @@ ${output.slice(-3e3)}`);
|
|
|
7086
7238
|
try {
|
|
7087
7239
|
const cwd = this.workspaceRootFn();
|
|
7088
7240
|
const runner = this.store.resolveTaskRunner(task);
|
|
7089
|
-
const finalPrompt = composeAugmentedPrompt(task, this.store.
|
|
7241
|
+
const finalPrompt = composeAugmentedPrompt(task, this.store.planTasks, {
|
|
7090
7242
|
planMapEnabled: this.config.planMapEnabled,
|
|
7091
7243
|
tddEnabled: this.tddEnabled()
|
|
7092
7244
|
});
|
|
@@ -7124,111 +7276,6 @@ ${output.slice(-3e3)}`);
|
|
|
7124
7276
|
}
|
|
7125
7277
|
};
|
|
7126
7278
|
|
|
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
7279
|
// src/services/Planner.ts
|
|
7233
7280
|
function plannerModesWith(req) {
|
|
7234
7281
|
const modes = req.modes ?? DEFAULT_PLANNER_MODES;
|
|
@@ -7365,7 +7412,7 @@ import * as osMod from "os";
|
|
|
7365
7412
|
import { exec } from "child_process";
|
|
7366
7413
|
import { promisify } from "util";
|
|
7367
7414
|
import * as os2 from "os";
|
|
7368
|
-
import * as
|
|
7415
|
+
import * as path4 from "path";
|
|
7369
7416
|
var execAsync = promisify(exec);
|
|
7370
7417
|
var LOGIN_SHELL_TIMEOUT_MS = 5e3;
|
|
7371
7418
|
var PATH_MARKER = "__ORDEWELL_PATH__";
|
|
@@ -7376,40 +7423,40 @@ var defaultExec = async (command, options) => {
|
|
|
7376
7423
|
function posixBinDirs() {
|
|
7377
7424
|
const home = os2.homedir();
|
|
7378
7425
|
return [
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7426
|
+
path4.join(home, ".local", "bin"),
|
|
7427
|
+
path4.join(home, ".opencode", "bin"),
|
|
7428
|
+
path4.join(home, ".claude", "bin"),
|
|
7429
|
+
path4.join(home, ".bun", "bin"),
|
|
7430
|
+
path4.join(home, ".volta", "bin"),
|
|
7431
|
+
path4.join(home, ".npm-global", "bin"),
|
|
7432
|
+
path4.join(home, "bin"),
|
|
7386
7433
|
"/usr/local/bin",
|
|
7387
7434
|
"/opt/homebrew/bin"
|
|
7388
7435
|
];
|
|
7389
7436
|
}
|
|
7390
7437
|
function windowsBinDirs(env, home) {
|
|
7391
|
-
const
|
|
7392
|
-
const under = (base, ...parts) => base ? [
|
|
7438
|
+
const join19 = (...parts) => path4.win32.join(...parts);
|
|
7439
|
+
const under = (base, ...parts) => base ? [join19(base, ...parts)] : [];
|
|
7393
7440
|
const appData = env.APPDATA;
|
|
7394
7441
|
const localAppData = env.LOCALAPPDATA;
|
|
7395
7442
|
const programData = env.ProgramData;
|
|
7396
7443
|
return [
|
|
7397
7444
|
// Native per-user installers, PowerShell-driven and otherwise.
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7445
|
+
join19(home, ".local", "bin"),
|
|
7446
|
+
join19(home, ".opencode", "bin"),
|
|
7447
|
+
join19(home, ".claude", "bin"),
|
|
7448
|
+
join19(home, ".codex", "bin"),
|
|
7402
7449
|
...under(localAppData, "Programs", "claude"),
|
|
7403
7450
|
// Node package managers. npm's global prefix is `%APPDATA%\npm`; the
|
|
7404
7451
|
// literal spelling is kept as a fallback for a host that does not export
|
|
7405
7452
|
// APPDATA (a service account, a stripped CI container).
|
|
7406
7453
|
...under(appData, "npm"),
|
|
7407
|
-
|
|
7454
|
+
join19(home, "AppData", "Roaming", "npm"),
|
|
7408
7455
|
...under(localAppData, "pnpm"),
|
|
7409
7456
|
...under(localAppData, "Yarn", "bin"),
|
|
7410
|
-
|
|
7457
|
+
join19(home, ".bun", "bin"),
|
|
7411
7458
|
// Windows package managers.
|
|
7412
|
-
|
|
7459
|
+
join19(home, "scoop", "shims"),
|
|
7413
7460
|
...under(env.SCOOP, "shims"),
|
|
7414
7461
|
...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
|
|
7415
7462
|
...under(programData, "chocolatey", "bin"),
|
|
@@ -7419,7 +7466,7 @@ function windowsBinDirs(env, home) {
|
|
|
7419
7466
|
...under(localAppData, "Microsoft", "WindowsApps"),
|
|
7420
7467
|
// Version managers. Volta's Windows home is not the POSIX `~/.volta`.
|
|
7421
7468
|
...under(localAppData, "Volta", "bin"),
|
|
7422
|
-
|
|
7469
|
+
join19(home, ".volta", "bin")
|
|
7423
7470
|
];
|
|
7424
7471
|
}
|
|
7425
7472
|
function wellKnownBinDirs(deps = {}) {
|
|
@@ -7448,14 +7495,14 @@ function augmentedPath(execImpl = defaultExec) {
|
|
|
7448
7495
|
const segments = [];
|
|
7449
7496
|
const push = (p) => {
|
|
7450
7497
|
if (!p) return;
|
|
7451
|
-
for (const seg of p.split(
|
|
7498
|
+
for (const seg of p.split(path4.delimiter)) {
|
|
7452
7499
|
if (seg && !segments.includes(seg)) segments.push(seg);
|
|
7453
7500
|
}
|
|
7454
7501
|
};
|
|
7455
7502
|
push(process.env.PATH);
|
|
7456
7503
|
push(await loginShellPath(execImpl));
|
|
7457
7504
|
for (const dir of wellKnownBinDirs()) push(dir);
|
|
7458
|
-
return segments.join(
|
|
7505
|
+
return segments.join(path4.delimiter);
|
|
7459
7506
|
})();
|
|
7460
7507
|
}
|
|
7461
7508
|
return cachedPath;
|
|
@@ -7475,6 +7522,50 @@ function withPath(base, resolvedPath, overrides = {}) {
|
|
|
7475
7522
|
return env;
|
|
7476
7523
|
}
|
|
7477
7524
|
|
|
7525
|
+
// src/utils/globalDataDir.ts
|
|
7526
|
+
import * as fs from "fs";
|
|
7527
|
+
import * as path5 from "path";
|
|
7528
|
+
import * as os3 from "os";
|
|
7529
|
+
function globalDataDir() {
|
|
7530
|
+
return path5.join(os3.homedir(), ".ordewell");
|
|
7531
|
+
}
|
|
7532
|
+
function copyDirSync(src, dest) {
|
|
7533
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
7534
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
7535
|
+
for (const entry of entries) {
|
|
7536
|
+
const srcPath = path5.join(src, entry.name);
|
|
7537
|
+
const destPath = path5.join(dest, entry.name);
|
|
7538
|
+
if (entry.isDirectory()) {
|
|
7539
|
+
copyDirSync(srcPath, destPath);
|
|
7540
|
+
} else {
|
|
7541
|
+
fs.copyFileSync(srcPath, destPath);
|
|
7542
|
+
}
|
|
7543
|
+
}
|
|
7544
|
+
}
|
|
7545
|
+
var migrated = false;
|
|
7546
|
+
function migrateOldConfigDir() {
|
|
7547
|
+
if (migrated) return;
|
|
7548
|
+
migrated = true;
|
|
7549
|
+
const oldDir = path5.join(os3.homedir(), ".config", "ordewell");
|
|
7550
|
+
if (!fs.existsSync(oldDir)) return;
|
|
7551
|
+
const newDir = globalDataDir();
|
|
7552
|
+
const liftFile = (name) => {
|
|
7553
|
+
const oldFile = path5.join(oldDir, name);
|
|
7554
|
+
const newFile = path5.join(newDir, name);
|
|
7555
|
+
if (fs.existsSync(oldFile) && !fs.existsSync(newFile)) {
|
|
7556
|
+
fs.mkdirSync(newDir, { recursive: true });
|
|
7557
|
+
fs.copyFileSync(oldFile, newFile);
|
|
7558
|
+
}
|
|
7559
|
+
};
|
|
7560
|
+
liftFile("settings.json");
|
|
7561
|
+
liftFile(".env");
|
|
7562
|
+
const oldPlugins = path5.join(oldDir, "plugins");
|
|
7563
|
+
const newPlugins = path5.join(newDir, "plugins");
|
|
7564
|
+
if (fs.existsSync(oldPlugins) && !fs.existsSync(newPlugins)) {
|
|
7565
|
+
copyDirSync(oldPlugins, newPlugins);
|
|
7566
|
+
}
|
|
7567
|
+
}
|
|
7568
|
+
|
|
7478
7569
|
// src/utils/launch.ts
|
|
7479
7570
|
import * as fsSync2 from "fs";
|
|
7480
7571
|
import * as path6 from "path";
|
|
@@ -7800,7 +7891,7 @@ function parseCodexModelsCache(content) {
|
|
|
7800
7891
|
}
|
|
7801
7892
|
var GEMINI_MODELS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
7802
7893
|
function geminiDiscoveryCacheKey(apiKey) {
|
|
7803
|
-
const dir =
|
|
7894
|
+
const dir = globalDataDir();
|
|
7804
7895
|
const hash = apiKey.slice(-8);
|
|
7805
7896
|
return {
|
|
7806
7897
|
key: `gemini-models:${hash}`,
|
|
@@ -8330,12 +8421,14 @@ var ModelDiscovery = class {
|
|
|
8330
8421
|
// src/services/ModelCatalog.ts
|
|
8331
8422
|
import * as fs2 from "fs";
|
|
8332
8423
|
import * as path8 from "path";
|
|
8333
|
-
import * as os3 from "os";
|
|
8334
8424
|
var CACHE_KEY = "model-catalog";
|
|
8335
8425
|
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
8336
8426
|
function storageKey(baseUrl) {
|
|
8337
8427
|
return `${CACHE_KEY}:${baseUrl}`;
|
|
8338
8428
|
}
|
|
8429
|
+
function globalCacheDir() {
|
|
8430
|
+
return globalDataDir();
|
|
8431
|
+
}
|
|
8339
8432
|
var ModelCatalog = class {
|
|
8340
8433
|
static async fetchModels(apiKey, baseUrl, fetchImpl) {
|
|
8341
8434
|
const url = (baseUrl || "https://openrouter.ai/api/v1").replace(/\/+$/, "");
|
|
@@ -8378,7 +8471,7 @@ var ModelCatalog = class {
|
|
|
8378
8471
|
try {
|
|
8379
8472
|
const url = (baseUrl || "https://openrouter.ai/api/v1").replace(/\/+$/, "");
|
|
8380
8473
|
const key = storageKey(url);
|
|
8381
|
-
const cacheDir =
|
|
8474
|
+
const cacheDir = globalCacheDir();
|
|
8382
8475
|
const cacheFile = path8.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
8383
8476
|
if (fs2.existsSync(cacheFile)) fs2.rmSync(cacheFile);
|
|
8384
8477
|
} catch {
|
|
@@ -8386,7 +8479,7 @@ var ModelCatalog = class {
|
|
|
8386
8479
|
}
|
|
8387
8480
|
static readCache(key) {
|
|
8388
8481
|
try {
|
|
8389
|
-
const cacheDir =
|
|
8482
|
+
const cacheDir = globalCacheDir();
|
|
8390
8483
|
const cacheFile = path8.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
8391
8484
|
if (!fs2.existsSync(cacheFile)) return null;
|
|
8392
8485
|
const raw = fs2.readFileSync(cacheFile, "utf8");
|
|
@@ -8399,7 +8492,7 @@ var ModelCatalog = class {
|
|
|
8399
8492
|
}
|
|
8400
8493
|
static writeCache(key, models) {
|
|
8401
8494
|
try {
|
|
8402
|
-
const cacheDir =
|
|
8495
|
+
const cacheDir = globalCacheDir();
|
|
8403
8496
|
fs2.mkdirSync(cacheDir, { recursive: true });
|
|
8404
8497
|
const cacheFile = path8.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
8405
8498
|
fs2.writeFileSync(
|
|
@@ -8940,12 +9033,19 @@ var StdioAgentAdapter = class {
|
|
|
8940
9033
|
/** Set for the duration of a turn. Outside one, events are buffered rather than dropped. */
|
|
8941
9034
|
turnEmit = null;
|
|
8942
9035
|
/**
|
|
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.
|
|
9036
|
+
* Events the agent produced before its first turn — in practice the startup
|
|
9037
|
+
* warnings that arrive during the handshake. Dropping them hid the one
|
|
9038
|
+
* diagnostic that explains a planner which cannot read the workspace, so they
|
|
9039
|
+
* are held until a turn exists to show them in.
|
|
9040
|
+
*
|
|
9041
|
+
* Only before the *first* turn. Once one has run, anything arriving outside a
|
|
9042
|
+
* turn is that closed turn's straggling work — a backgrounded subagent
|
|
9043
|
+
* finishing, a late read — and replaying it into the next turn presents it as
|
|
9044
|
+
* research done for the user's new message.
|
|
8947
9045
|
*/
|
|
8948
9046
|
betweenTurns = [];
|
|
9047
|
+
/** Set once a turn has ended: after that, out-of-turn events are stale, not startup. */
|
|
9048
|
+
hadTurn = false;
|
|
8949
9049
|
disposed = false;
|
|
8950
9050
|
/** Resolves when the process ends, so a handshake can lose the race instead of waiting out its timeout. */
|
|
8951
9051
|
processEnded;
|
|
@@ -8980,7 +9080,7 @@ var StdioAgentAdapter = class {
|
|
|
8980
9080
|
this.process.stdout?.on("data", (chunk) => {
|
|
8981
9081
|
this.stdout.push(chunk.toString(), (line) => this.handleLine(line, (event) => {
|
|
8982
9082
|
if (this.turnEmit) this.turnEmit(event);
|
|
8983
|
-
else if (!TURN_SCOPED_EVENTS.has(event.type) && this.betweenTurns.length < BETWEEN_TURN_EVENT_CAP) {
|
|
9083
|
+
else if (!this.hadTurn && !TURN_SCOPED_EVENTS.has(event.type) && this.betweenTurns.length < BETWEEN_TURN_EVENT_CAP) {
|
|
8984
9084
|
this.betweenTurns.push(event);
|
|
8985
9085
|
}
|
|
8986
9086
|
}));
|
|
@@ -9012,6 +9112,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
|
|
|
9012
9112
|
if (settled) return;
|
|
9013
9113
|
settled = true;
|
|
9014
9114
|
this.turnEmit = null;
|
|
9115
|
+
this.hadTurn = true;
|
|
9015
9116
|
signal?.removeEventListener("abort", onAbort);
|
|
9016
9117
|
this.process?.removeListener("exit", onExit);
|
|
9017
9118
|
this.process?.removeListener("error", onExit);
|
|
@@ -9090,6 +9191,7 @@ ${tail}` : ""}`;
|
|
|
9090
9191
|
|
|
9091
9192
|
// src/services/harness/ClaudeCodeAdapter.ts
|
|
9092
9193
|
var DISALLOWED_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit", "KillShell"];
|
|
9194
|
+
var ASYNC_LAUNCH_MARKER = "Async agent launched successfully";
|
|
9093
9195
|
function flattenContent(content) {
|
|
9094
9196
|
if (typeof content === "string") return content;
|
|
9095
9197
|
if (Array.isArray(content)) {
|
|
@@ -9099,6 +9201,8 @@ function flattenContent(content) {
|
|
|
9099
9201
|
}
|
|
9100
9202
|
var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
9101
9203
|
agentId = "claude-code";
|
|
9204
|
+
/** Whether this turn has already emitted reply text — see {@link handleLine}. */
|
|
9205
|
+
turnHasText = false;
|
|
9102
9206
|
spawnSpec(opts) {
|
|
9103
9207
|
const args = [
|
|
9104
9208
|
"-p",
|
|
@@ -9123,6 +9227,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9123
9227
|
return { command: "claude", args };
|
|
9124
9228
|
}
|
|
9125
9229
|
turnPayload(message) {
|
|
9230
|
+
this.turnHasText = false;
|
|
9126
9231
|
return `${JSON.stringify({
|
|
9127
9232
|
type: "user",
|
|
9128
9233
|
message: { role: "user", content: [{ type: "text", text: message }] }
|
|
@@ -9133,6 +9238,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9133
9238
|
const msg = StdioAgentAdapter.parse(line);
|
|
9134
9239
|
if (!msg) return;
|
|
9135
9240
|
if (msg.session_id) this.sessionId = msg.session_id;
|
|
9241
|
+
if (msg.parent_tool_use_id) return;
|
|
9136
9242
|
switch (msg.type) {
|
|
9137
9243
|
// The control channel: Claude asks whether a tool may run when its mode
|
|
9138
9244
|
// cannot decide alone. A read-only planner answers "deny", every time —
|
|
@@ -9157,8 +9263,12 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9157
9263
|
}
|
|
9158
9264
|
case "assistant":
|
|
9159
9265
|
for (const block of Array.isArray(msg.message?.content) ? msg.message.content : []) {
|
|
9160
|
-
if (block.type === "text" && block.text)
|
|
9161
|
-
|
|
9266
|
+
if (block.type === "text" && block.text) {
|
|
9267
|
+
emit({ type: "assistant_text", text: this.turnHasText ? `
|
|
9268
|
+
|
|
9269
|
+
${block.text}` : block.text });
|
|
9270
|
+
this.turnHasText = true;
|
|
9271
|
+
} else if (block.type === "thinking" && block.thinking) emit({ type: "thinking", text: block.thinking });
|
|
9162
9272
|
else if (block.type === "tool_use" && block.name) {
|
|
9163
9273
|
emit({ type: "tool_call", id: block.id ?? block.name, name: block.name, args: block.input ?? {} });
|
|
9164
9274
|
}
|
|
@@ -9167,11 +9277,15 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9167
9277
|
case "user":
|
|
9168
9278
|
for (const block of Array.isArray(msg.message?.content) ? msg.message.content : []) {
|
|
9169
9279
|
if (block.type !== "tool_result") continue;
|
|
9280
|
+
const output = flattenContent(block.content);
|
|
9281
|
+
if (output.includes(ASYNC_LAUNCH_MARKER)) {
|
|
9282
|
+
emit({ type: "background_agent", id: block.tool_use_id ?? "" });
|
|
9283
|
+
}
|
|
9170
9284
|
emit({
|
|
9171
9285
|
type: "tool_result",
|
|
9172
9286
|
id: block.tool_use_id ?? "",
|
|
9173
9287
|
name: "",
|
|
9174
|
-
output
|
|
9288
|
+
output,
|
|
9175
9289
|
success: block.is_error !== true
|
|
9176
9290
|
});
|
|
9177
9291
|
}
|
|
@@ -9591,6 +9705,8 @@ var OpenCodeAdapter = class {
|
|
|
9591
9705
|
exited = false;
|
|
9592
9706
|
disposed = false;
|
|
9593
9707
|
opts = null;
|
|
9708
|
+
/** Whether this turn has already emitted reply text — see {@link emitPart}. */
|
|
9709
|
+
turnHasText = false;
|
|
9594
9710
|
async start(opts) {
|
|
9595
9711
|
this.opts = opts;
|
|
9596
9712
|
assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
|
|
@@ -9658,6 +9774,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
9658
9774
|
return;
|
|
9659
9775
|
}
|
|
9660
9776
|
const seen = /* @__PURE__ */ new Set();
|
|
9777
|
+
this.turnHasText = false;
|
|
9661
9778
|
const streamAbort = new AbortController();
|
|
9662
9779
|
let connected = () => {
|
|
9663
9780
|
};
|
|
@@ -9726,7 +9843,10 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
9726
9843
|
if (part.type === "text" && part.text) {
|
|
9727
9844
|
if (seen.has(`text:${id}`)) return;
|
|
9728
9845
|
seen.add(`text:${id}`);
|
|
9729
|
-
onEvent({ type: "assistant_text", text:
|
|
9846
|
+
onEvent({ type: "assistant_text", text: this.turnHasText ? `
|
|
9847
|
+
|
|
9848
|
+
${part.text}` : part.text });
|
|
9849
|
+
this.turnHasText = true;
|
|
9730
9850
|
return;
|
|
9731
9851
|
}
|
|
9732
9852
|
if (part.type === "reasoning" && part.text) {
|
|
@@ -9814,14 +9934,14 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
9814
9934
|
}
|
|
9815
9935
|
}
|
|
9816
9936
|
}
|
|
9817
|
-
async json(method,
|
|
9818
|
-
const response = await this.deps.fetch(`${this.baseUrl}${
|
|
9937
|
+
async json(method, path20, body, signal) {
|
|
9938
|
+
const response = await this.deps.fetch(`${this.baseUrl}${path20}`, {
|
|
9819
9939
|
method,
|
|
9820
9940
|
headers: { "content-type": "application/json" },
|
|
9821
9941
|
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
9822
9942
|
...signal ? { signal } : {}
|
|
9823
9943
|
});
|
|
9824
|
-
if (!response.ok) throw new Error(`${method} ${
|
|
9944
|
+
if (!response.ok) throw new Error(`${method} ${path20} failed: ${response.status} ${response.statusText}`);
|
|
9825
9945
|
return await response.json().catch(() => null);
|
|
9826
9946
|
}
|
|
9827
9947
|
nativeSessionId() {
|
|
@@ -9893,6 +10013,7 @@ function normalizeAgentArgs(tool, args) {
|
|
|
9893
10013
|
|
|
9894
10014
|
// src/services/harness/CliAgentAiService.ts
|
|
9895
10015
|
var MAX_JSON_REPAIRS = 2;
|
|
10016
|
+
var MAX_AGENT_WAITS = 2;
|
|
9896
10017
|
var LOG_MAX_CHARS = 1e4;
|
|
9897
10018
|
function defaultAdapter(runner, deps) {
|
|
9898
10019
|
switch (runner) {
|
|
@@ -9975,9 +10096,6 @@ var CliAgentAiService = class {
|
|
|
9975
10096
|
req.runners,
|
|
9976
10097
|
req.runnerModes,
|
|
9977
10098
|
req.autonomousDefault ?? true,
|
|
9978
|
-
req.grillMeEnabled ?? false,
|
|
9979
|
-
req.prdEnabled ?? false,
|
|
9980
|
-
req.reviewEnabled ?? false,
|
|
9981
10099
|
req.verificationEnabled ?? false,
|
|
9982
10100
|
true
|
|
9983
10101
|
);
|
|
@@ -9992,10 +10110,7 @@ var CliAgentAiService = class {
|
|
|
9992
10110
|
startOptions,
|
|
9993
10111
|
runners: req.runners,
|
|
9994
10112
|
runnerModes: req.runnerModes,
|
|
9995
|
-
autonomousDefault: req.autonomousDefault
|
|
9996
|
-
prdEnabled: req.prdEnabled ?? false,
|
|
9997
|
-
prdCaptured: false,
|
|
9998
|
-
prdNudgeSent: false
|
|
10113
|
+
autonomousDefault: req.autonomousDefault
|
|
9999
10114
|
};
|
|
10000
10115
|
const opening = this.openingMessage(req);
|
|
10001
10116
|
return this.runConversation(opening, req.onProgress, req.signal);
|
|
@@ -10023,8 +10138,8 @@ var CliAgentAiService = class {
|
|
|
10023
10138
|
/**
|
|
10024
10139
|
* Drive one user message to a settled planner turn. Same shape as the API
|
|
10025
10140
|
* backend's conversation loop minus the tool rounds — those belong to the
|
|
10026
|
-
* agent now — and with the same
|
|
10027
|
-
*
|
|
10141
|
+
* agent now — and with the same two policies layered on top: the
|
|
10142
|
+
* empty-reply nudge, and a bounded corrective re-emit for botched JSON.
|
|
10028
10143
|
*/
|
|
10029
10144
|
async runConversation(message, onProgress, signal) {
|
|
10030
10145
|
const conversation = this.conversation;
|
|
@@ -10033,13 +10148,16 @@ var CliAgentAiService = class {
|
|
|
10033
10148
|
let pending = message;
|
|
10034
10149
|
let emptyNudgeSent = false;
|
|
10035
10150
|
let jsonRepairAttempts = 0;
|
|
10151
|
+
let agentWaits = 0;
|
|
10152
|
+
const carried = [];
|
|
10153
|
+
const replyText = (text) => [...carried, text].filter((part) => part.trim()).join("\n\n");
|
|
10036
10154
|
try {
|
|
10037
10155
|
for (; ; ) {
|
|
10038
10156
|
const turn = await this.runTurn(pending, onProgress, combined);
|
|
10039
10157
|
researchLog.push(...turn.researchLog);
|
|
10040
10158
|
if (turn.aborted || combined?.aborted) {
|
|
10041
10159
|
onProgress({ type: "interrupted" });
|
|
10042
|
-
return { kind: "message", text: turn.text, researchLog };
|
|
10160
|
+
return { kind: "message", text: replyText(turn.text), researchLog };
|
|
10043
10161
|
}
|
|
10044
10162
|
if (turn.error) {
|
|
10045
10163
|
return { kind: "message", text: turn.error, researchLog };
|
|
@@ -10057,7 +10175,18 @@ var CliAgentAiService = class {
|
|
|
10057
10175
|
researchLog
|
|
10058
10176
|
};
|
|
10059
10177
|
}
|
|
10060
|
-
if (
|
|
10178
|
+
if (turn.backgroundAgents > 0 && agentWaits < MAX_AGENT_WAITS && !combined?.aborted) {
|
|
10179
|
+
agentWaits++;
|
|
10180
|
+
carried.push(turn.text);
|
|
10181
|
+
pending = [
|
|
10182
|
+
`You ended your turn with ${turn.backgroundAgents} subagent(s) still running in the background.`,
|
|
10183
|
+
"Ordewell hands the conversation back to the user when your turn ends, so anything you say after it never reaches them \u2014",
|
|
10184
|
+
"the results you promised to report would be lost.",
|
|
10185
|
+
"Wait for those agents to finish NOW, in this reply, and do not end your turn until you have their results.",
|
|
10186
|
+
"Then give the user your synthesis. In future replies, await your agents inside the turn rather than backgrounding them."
|
|
10187
|
+
].join(" ");
|
|
10188
|
+
continue;
|
|
10189
|
+
}
|
|
10061
10190
|
const reply = classifyPlannerReply(turn.text, {
|
|
10062
10191
|
runners: conversation.runners,
|
|
10063
10192
|
runnerModes: conversation.runnerModes,
|
|
@@ -10065,20 +10194,14 @@ var CliAgentAiService = class {
|
|
|
10065
10194
|
});
|
|
10066
10195
|
switch (reply.kind) {
|
|
10067
10196
|
case "task_ops":
|
|
10068
|
-
return { kind: "task_ops", ops: reply.ops, text: turn.text, researchLog };
|
|
10197
|
+
return { kind: "task_ops", ops: reply.ops, text: replyText(turn.text), researchLog };
|
|
10198
|
+
// The read channel is a text envelope precisely so it reaches here
|
|
10199
|
+
// too: a harness planner has no Ordewell tool loop to call into.
|
|
10200
|
+
case "task_query":
|
|
10201
|
+
return { kind: "task_query", query: reply.query, text: replyText(turn.text), researchLog };
|
|
10069
10202
|
case "plan":
|
|
10070
|
-
if (conversation.prdEnabled && !conversation.prdCaptured && !conversation.prdNudgeSent && !combined?.aborted) {
|
|
10071
|
-
conversation.prdNudgeSent = true;
|
|
10072
|
-
pending = [
|
|
10073
|
-
"PRD mode is enabled but no PRD has been produced yet, so the plan was NOT committed.",
|
|
10074
|
-
"In your next reply: first output the full markdown PRD wrapped EXACTLY in",
|
|
10075
|
-
'<!-- ORDEWELL_PRD_START slug="<feature-slug>" --> and <!-- ORDEWELL_PRD_END -->,',
|
|
10076
|
-
"then re-emit the exact same task plan JSON after the PRD block."
|
|
10077
|
-
].join(" ");
|
|
10078
|
-
continue;
|
|
10079
|
-
}
|
|
10080
10203
|
this.conversation = null;
|
|
10081
|
-
return { kind: "plan", tasks: reply.tasks, text: turn.text, researchLog };
|
|
10204
|
+
return { kind: "plan", tasks: reply.tasks, text: replyText(turn.text), researchLog };
|
|
10082
10205
|
case "broken_task_ops":
|
|
10083
10206
|
if (jsonRepairAttempts < MAX_JSON_REPAIRS && !combined?.aborted) {
|
|
10084
10207
|
jsonRepairAttempts++;
|
|
@@ -10086,6 +10209,13 @@ var CliAgentAiService = class {
|
|
|
10086
10209
|
continue;
|
|
10087
10210
|
}
|
|
10088
10211
|
break;
|
|
10212
|
+
case "broken_task_query":
|
|
10213
|
+
if (jsonRepairAttempts < MAX_JSON_REPAIRS && !combined?.aborted) {
|
|
10214
|
+
jsonRepairAttempts++;
|
|
10215
|
+
pending = reEmitTaskQueryPrompt(reply.error.message);
|
|
10216
|
+
continue;
|
|
10217
|
+
}
|
|
10218
|
+
break;
|
|
10089
10219
|
case "broken_plan":
|
|
10090
10220
|
if (jsonRepairAttempts < MAX_JSON_REPAIRS && !combined?.aborted) {
|
|
10091
10221
|
jsonRepairAttempts++;
|
|
@@ -10096,7 +10226,7 @@ var CliAgentAiService = class {
|
|
|
10096
10226
|
case "prose":
|
|
10097
10227
|
break;
|
|
10098
10228
|
}
|
|
10099
|
-
return { kind: "message", text: turn.text, researchLog };
|
|
10229
|
+
return { kind: "message", text: replyText(turn.text), researchLog };
|
|
10100
10230
|
}
|
|
10101
10231
|
} finally {
|
|
10102
10232
|
this.activeAbort = null;
|
|
@@ -10116,6 +10246,7 @@ var CliAgentAiService = class {
|
|
|
10116
10246
|
let text = "";
|
|
10117
10247
|
let error;
|
|
10118
10248
|
let stepIndex = 0;
|
|
10249
|
+
let backgroundAgents = 0;
|
|
10119
10250
|
const truncate = (value) => value.length > LOG_MAX_CHARS ? `${value.slice(0, LOG_MAX_CHARS)}
|
|
10120
10251
|
[... truncated, total ${value.length} chars]` : value;
|
|
10121
10252
|
const settle = (id, rawOutput, success, outcome) => {
|
|
@@ -10155,6 +10286,9 @@ var CliAgentAiService = class {
|
|
|
10155
10286
|
case "tool_result":
|
|
10156
10287
|
settle(event.id, event.output, event.success, event.success ? "success" : "failure");
|
|
10157
10288
|
return;
|
|
10289
|
+
case "background_agent":
|
|
10290
|
+
backgroundAgents++;
|
|
10291
|
+
return;
|
|
10158
10292
|
case "permission_request": {
|
|
10159
10293
|
const mapped = mapAgentTool(event.name);
|
|
10160
10294
|
const args = JSON.stringify({ detail: event.detail });
|
|
@@ -10185,7 +10319,7 @@ var CliAgentAiService = class {
|
|
|
10185
10319
|
adapter.dispose();
|
|
10186
10320
|
this.adapter = null;
|
|
10187
10321
|
}
|
|
10188
|
-
return { text, researchLog, error, aborted: signal?.aborted };
|
|
10322
|
+
return { text, researchLog, error, aborted: signal?.aborted, backgroundAgents };
|
|
10189
10323
|
}
|
|
10190
10324
|
// --- Process lifecycle ---
|
|
10191
10325
|
async startAdapter(opts) {
|
|
@@ -10244,10 +10378,7 @@ var CliAgentAiService = class {
|
|
|
10244
10378
|
oneShotAdapter = await this.startAdapter(startOptions);
|
|
10245
10379
|
this.conversation = {
|
|
10246
10380
|
startOptions,
|
|
10247
|
-
runners: []
|
|
10248
|
-
prdEnabled: false,
|
|
10249
|
-
prdCaptured: false,
|
|
10250
|
-
prdNudgeSent: false
|
|
10381
|
+
runners: []
|
|
10251
10382
|
};
|
|
10252
10383
|
const turn = await this.runTurn(
|
|
10253
10384
|
"Follow the instructions in your system prompt and produce the plan now.",
|
|
@@ -10264,8 +10395,8 @@ var CliAgentAiService = class {
|
|
|
10264
10395
|
this.lastNativeSessionId = previousSessionId;
|
|
10265
10396
|
}
|
|
10266
10397
|
}
|
|
10267
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
10268
|
-
const contextStr = await collectResearchContext(
|
|
10398
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs12, onProgress, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
10399
|
+
const contextStr = await collectResearchContext(fs12, runners);
|
|
10269
10400
|
const researchLog = [
|
|
10270
10401
|
{ id: `up-${Date.now()}`, type: "user_prompt", content: userDescription, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
10271
10402
|
];
|
|
@@ -10588,6 +10719,180 @@ function retargetTaskRunner(task, runner, catalog) {
|
|
|
10588
10719
|
return { assignedRunner: runner, ...runnerAssignment(catalog, task) };
|
|
10589
10720
|
}
|
|
10590
10721
|
|
|
10722
|
+
// src/services/SkillsService.ts
|
|
10723
|
+
import * as fs4 from "fs";
|
|
10724
|
+
import * as path11 from "path";
|
|
10725
|
+
|
|
10726
|
+
// src/services/builtinSkills.ts
|
|
10727
|
+
import * as path10 from "path";
|
|
10728
|
+
import { fileURLToPath } from "url";
|
|
10729
|
+
function builtinSkillsDir() {
|
|
10730
|
+
const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path10.dirname(fileURLToPath(
|
|
10731
|
+
// @ts-expect-error - import.meta is only valid under an ESM module setting
|
|
10732
|
+
import.meta.url
|
|
10733
|
+
));
|
|
10734
|
+
return path10.join(dir, "..", "skills");
|
|
10735
|
+
}
|
|
10736
|
+
|
|
10737
|
+
// src/services/SkillsService.ts
|
|
10738
|
+
var BUILTIN_SKILL_NAMES = ["grilling", "to-spec"];
|
|
10739
|
+
var RETIRED_BUILTIN_SKILL_NAMES = ["grill-me"];
|
|
10740
|
+
function parseSkillFile(filePath, name, source) {
|
|
10741
|
+
const raw = fs4.readFileSync(filePath, "utf8");
|
|
10742
|
+
const frontmatter = {};
|
|
10743
|
+
let content = raw;
|
|
10744
|
+
if (raw.startsWith("---\n")) {
|
|
10745
|
+
const end = raw.indexOf("\n---", 4);
|
|
10746
|
+
if (end !== -1) {
|
|
10747
|
+
const fmText = raw.slice(4, end);
|
|
10748
|
+
for (const line of fmText.split("\n")) {
|
|
10749
|
+
const idx = line.indexOf(":");
|
|
10750
|
+
if (idx === -1) continue;
|
|
10751
|
+
const key = line.slice(0, idx).trim();
|
|
10752
|
+
const value = line.slice(idx + 1).trim();
|
|
10753
|
+
if (key === "disable-model-invocation") {
|
|
10754
|
+
frontmatter[key] = value === "true";
|
|
10755
|
+
} else if (key === "name" || key === "description") {
|
|
10756
|
+
frontmatter[key] = value.replace(/^"|"$/g, "");
|
|
10757
|
+
}
|
|
10758
|
+
}
|
|
10759
|
+
content = raw.slice(end + 4).replace(/^\n+/, "");
|
|
10760
|
+
}
|
|
10761
|
+
}
|
|
10762
|
+
const metadata = {
|
|
10763
|
+
name: frontmatter.name ?? name,
|
|
10764
|
+
description: frontmatter.description ?? "",
|
|
10765
|
+
...frontmatter["disable-model-invocation"] !== void 0 ? { disableModelInvocation: frontmatter["disable-model-invocation"] } : {}
|
|
10766
|
+
};
|
|
10767
|
+
return { name, description: metadata.description, metadata, content, path: filePath, source };
|
|
10768
|
+
}
|
|
10769
|
+
function copyDirSync2(src, dest) {
|
|
10770
|
+
fs4.mkdirSync(dest, { recursive: true });
|
|
10771
|
+
const entries = fs4.readdirSync(src, { withFileTypes: true });
|
|
10772
|
+
for (const entry of entries) {
|
|
10773
|
+
const srcPath = path11.join(src, entry.name);
|
|
10774
|
+
const destPath = path11.join(dest, entry.name);
|
|
10775
|
+
if (entry.isDirectory()) copyDirSync2(srcPath, destPath);
|
|
10776
|
+
else fs4.copyFileSync(srcPath, destPath);
|
|
10777
|
+
}
|
|
10778
|
+
}
|
|
10779
|
+
function readDir(dir, source) {
|
|
10780
|
+
if (!fs4.existsSync(dir)) return [];
|
|
10781
|
+
const entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
10782
|
+
const skills = [];
|
|
10783
|
+
for (const entry of entries) {
|
|
10784
|
+
if (!entry.isDirectory()) continue;
|
|
10785
|
+
const skillFile = path11.join(dir, entry.name, "SKILL.md");
|
|
10786
|
+
if (!fs4.existsSync(skillFile)) continue;
|
|
10787
|
+
const parsed = parseSkillFile(skillFile, entry.name, source);
|
|
10788
|
+
if (parsed) skills.push(parsed);
|
|
10789
|
+
}
|
|
10790
|
+
return skills;
|
|
10791
|
+
}
|
|
10792
|
+
var SkillsService = class {
|
|
10793
|
+
constructor(workspaceRoot) {
|
|
10794
|
+
this.workspaceRoot = workspaceRoot;
|
|
10795
|
+
}
|
|
10796
|
+
workspaceRoot;
|
|
10797
|
+
localDir() {
|
|
10798
|
+
return this.workspaceRoot ? path11.join(this.workspaceRoot, ".ordewell", "skills") : void 0;
|
|
10799
|
+
}
|
|
10800
|
+
globalDir() {
|
|
10801
|
+
return path11.join(globalDataDir(), "skills");
|
|
10802
|
+
}
|
|
10803
|
+
seed(name) {
|
|
10804
|
+
const dest = path11.join(this.globalDir(), name);
|
|
10805
|
+
if (fs4.existsSync(dest)) return false;
|
|
10806
|
+
let srcDir;
|
|
10807
|
+
try {
|
|
10808
|
+
srcDir = path11.join(builtinSkillsDir(), name);
|
|
10809
|
+
} catch (err) {
|
|
10810
|
+
console.warn(`Could not locate built-in skills dir: ${String(err)}`);
|
|
10811
|
+
return false;
|
|
10812
|
+
}
|
|
10813
|
+
if (!fs4.existsSync(srcDir)) {
|
|
10814
|
+
console.warn(`Built-in skill not found in package: ${name}`);
|
|
10815
|
+
return false;
|
|
10816
|
+
}
|
|
10817
|
+
copyDirSync2(srcDir, dest);
|
|
10818
|
+
return true;
|
|
10819
|
+
}
|
|
10820
|
+
seedBuiltinSkill(name) {
|
|
10821
|
+
return this.seed(name);
|
|
10822
|
+
}
|
|
10823
|
+
/** Removes a retired built-in's seed from the global dir, but only if it's untouched by the user. */
|
|
10824
|
+
isUnmodifiedRetiredSeed(dir, expectedName) {
|
|
10825
|
+
let entries;
|
|
10826
|
+
try {
|
|
10827
|
+
entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
10828
|
+
} catch {
|
|
10829
|
+
return false;
|
|
10830
|
+
}
|
|
10831
|
+
if (entries.length !== 1 || entries[0].name !== "SKILL.md" || !entries[0].isFile()) return false;
|
|
10832
|
+
let raw;
|
|
10833
|
+
try {
|
|
10834
|
+
raw = fs4.readFileSync(path11.join(dir, "SKILL.md"), "utf8");
|
|
10835
|
+
} catch {
|
|
10836
|
+
return false;
|
|
10837
|
+
}
|
|
10838
|
+
if (!raw.startsWith("---\n")) return false;
|
|
10839
|
+
const end = raw.indexOf("\n---", 4);
|
|
10840
|
+
if (end === -1) return false;
|
|
10841
|
+
for (const line of raw.slice(4, end).split("\n")) {
|
|
10842
|
+
const idx = line.indexOf(":");
|
|
10843
|
+
if (idx === -1) continue;
|
|
10844
|
+
const key = line.slice(0, idx).trim();
|
|
10845
|
+
if (key === "name") return line.slice(idx + 1).trim().replace(/^"|"$/g, "") === expectedName;
|
|
10846
|
+
}
|
|
10847
|
+
return false;
|
|
10848
|
+
}
|
|
10849
|
+
pruneRetired() {
|
|
10850
|
+
for (const name of RETIRED_BUILTIN_SKILL_NAMES) {
|
|
10851
|
+
const dest = path11.join(this.globalDir(), name);
|
|
10852
|
+
if (!fs4.existsSync(dest)) continue;
|
|
10853
|
+
if (!this.isUnmodifiedRetiredSeed(dest, name)) continue;
|
|
10854
|
+
try {
|
|
10855
|
+
fs4.rmSync(dest, { recursive: true, force: true });
|
|
10856
|
+
} catch (err) {
|
|
10857
|
+
console.warn(`Could not prune retired skill ${name}: ${String(err)}`);
|
|
10858
|
+
}
|
|
10859
|
+
}
|
|
10860
|
+
}
|
|
10861
|
+
findSkill(name) {
|
|
10862
|
+
this.pruneRetired();
|
|
10863
|
+
if (BUILTIN_SKILL_NAMES.includes(name)) this.seed(name);
|
|
10864
|
+
const localDir = this.localDir();
|
|
10865
|
+
if (localDir) {
|
|
10866
|
+
const localFile = path11.join(localDir, name, "SKILL.md");
|
|
10867
|
+
if (fs4.existsSync(localFile)) return parseSkillFile(localFile, name, "local");
|
|
10868
|
+
}
|
|
10869
|
+
const globalFile = path11.join(this.globalDir(), name, "SKILL.md");
|
|
10870
|
+
if (fs4.existsSync(globalFile)) return parseSkillFile(globalFile, name, "global");
|
|
10871
|
+
return void 0;
|
|
10872
|
+
}
|
|
10873
|
+
listSkills() {
|
|
10874
|
+
this.pruneRetired();
|
|
10875
|
+
for (const name of BUILTIN_SKILL_NAMES) this.seed(name);
|
|
10876
|
+
const byName = /* @__PURE__ */ new Map();
|
|
10877
|
+
for (const skill of readDir(this.globalDir(), "global")) {
|
|
10878
|
+
byName.set(skill.name, skill);
|
|
10879
|
+
}
|
|
10880
|
+
const localDir = this.localDir();
|
|
10881
|
+
if (localDir) {
|
|
10882
|
+
for (const skill of readDir(localDir, "local")) {
|
|
10883
|
+
byName.set(skill.name, skill);
|
|
10884
|
+
}
|
|
10885
|
+
}
|
|
10886
|
+
return [...byName.values()];
|
|
10887
|
+
}
|
|
10888
|
+
getSkillContent(name) {
|
|
10889
|
+
return this.findSkill(name)?.content;
|
|
10890
|
+
}
|
|
10891
|
+
};
|
|
10892
|
+
function createSkillsService(workspaceRoot) {
|
|
10893
|
+
return new SkillsService(workspaceRoot);
|
|
10894
|
+
}
|
|
10895
|
+
|
|
10591
10896
|
// src/services/SessionMessage.ts
|
|
10592
10897
|
function serializeTask(t) {
|
|
10593
10898
|
return {
|
|
@@ -10609,11 +10914,12 @@ function serializeTask(t) {
|
|
|
10609
10914
|
userStoriesCovered: t.userStoriesCovered || void 0
|
|
10610
10915
|
};
|
|
10611
10916
|
}
|
|
10612
|
-
function serializeTaskStatus(t) {
|
|
10917
|
+
function serializeTaskStatus(t, idleSince = null) {
|
|
10613
10918
|
return {
|
|
10614
10919
|
id: t.id,
|
|
10615
10920
|
status: t.status,
|
|
10616
|
-
verdict: t.verdict ? { outcome: t.verdict.outcome, reason: t.verdict.reason, checks: t.verdict.checks || [] } : null
|
|
10921
|
+
verdict: t.verdict ? { outcome: t.verdict.outcome, reason: t.verdict.reason, checks: t.verdict.checks || [] } : null,
|
|
10922
|
+
idleSince
|
|
10617
10923
|
};
|
|
10618
10924
|
}
|
|
10619
10925
|
function serializePlan(plan) {
|
|
@@ -10635,27 +10941,27 @@ function executionSummary(tasks) {
|
|
|
10635
10941
|
}
|
|
10636
10942
|
|
|
10637
10943
|
// src/utils/sessionStore.ts
|
|
10638
|
-
import * as
|
|
10639
|
-
import * as
|
|
10944
|
+
import * as fs6 from "fs";
|
|
10945
|
+
import * as path13 from "path";
|
|
10640
10946
|
|
|
10641
10947
|
// src/utils/fsHelpers.ts
|
|
10642
|
-
import * as
|
|
10643
|
-
import * as
|
|
10948
|
+
import * as fs5 from "fs";
|
|
10949
|
+
import * as path12 from "path";
|
|
10644
10950
|
var STATE_DIR = ".ordewell";
|
|
10645
10951
|
function ensureDir(dir) {
|
|
10646
|
-
if (!
|
|
10647
|
-
|
|
10952
|
+
if (!fs5.existsSync(dir)) {
|
|
10953
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
10648
10954
|
}
|
|
10649
10955
|
}
|
|
10650
10956
|
function getStateDir(baseDir) {
|
|
10651
|
-
return
|
|
10957
|
+
return path12.join(baseDir ?? process.cwd(), STATE_DIR);
|
|
10652
10958
|
}
|
|
10653
10959
|
var IGNORE_FILE = ".gitignore";
|
|
10654
10960
|
function ensureStateDirIgnored(baseDir) {
|
|
10655
10961
|
const dir = getStateDir(baseDir);
|
|
10656
10962
|
ensureDir(dir);
|
|
10657
10963
|
try {
|
|
10658
|
-
|
|
10964
|
+
fs5.writeFileSync(path12.join(dir, IGNORE_FILE), "*\n", { flag: "wx" });
|
|
10659
10965
|
} catch (err) {
|
|
10660
10966
|
if (err.code !== "EEXIST") throw err;
|
|
10661
10967
|
}
|
|
@@ -10670,7 +10976,7 @@ function mintSessionId() {
|
|
|
10670
10976
|
// src/utils/sessionStore.ts
|
|
10671
10977
|
var SESSIONS_SUBDIR = "sessions";
|
|
10672
10978
|
function getSessionsDir(baseDir) {
|
|
10673
|
-
return
|
|
10979
|
+
return path13.join(getStateDir(baseDir), SESSIONS_SUBDIR);
|
|
10674
10980
|
}
|
|
10675
10981
|
var REJECTION_MESSAGE = "This session was created with an older version of Ordewell. Please re-plan.";
|
|
10676
10982
|
function hasScalarRunner(parsed) {
|
|
@@ -10705,17 +11011,17 @@ function saveSession(plan, goal, baseDir, id) {
|
|
|
10705
11011
|
plan: JSON.parse(JSON.stringify(plan))
|
|
10706
11012
|
};
|
|
10707
11013
|
const filename = sessionFilename(meta);
|
|
10708
|
-
const filePath =
|
|
10709
|
-
|
|
11014
|
+
const filePath = path13.join(sessionsDir, filename);
|
|
11015
|
+
fs6.writeFileSync(filePath, JSON.stringify(session, null, 2));
|
|
10710
11016
|
return meta;
|
|
10711
11017
|
}
|
|
10712
11018
|
function listSessions(baseDir, logger = defaultLogger) {
|
|
10713
11019
|
const sessionsDir = getSessionsDir(baseDir);
|
|
10714
|
-
if (!
|
|
10715
|
-
const files =
|
|
11020
|
+
if (!fs6.existsSync(sessionsDir)) return [];
|
|
11021
|
+
const files = fs6.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
10716
11022
|
return files.map((f) => {
|
|
10717
11023
|
try {
|
|
10718
|
-
const raw =
|
|
11024
|
+
const raw = fs6.readFileSync(path13.join(sessionsDir, f), "utf-8");
|
|
10719
11025
|
const parsed = JSON.parse(raw);
|
|
10720
11026
|
if (hasScalarRunner(parsed)) {
|
|
10721
11027
|
logger.warn("sessionStore", `session file ${f}: ${REJECTION_MESSAGE}`);
|
|
@@ -10730,10 +11036,10 @@ function listSessions(baseDir, logger = defaultLogger) {
|
|
|
10730
11036
|
}).filter((m) => m !== null).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
10731
11037
|
}
|
|
10732
11038
|
function findSessionFile(sessionsDir, sessionId, logger) {
|
|
10733
|
-
const files =
|
|
11039
|
+
const files = fs6.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
10734
11040
|
for (const f of files) {
|
|
10735
11041
|
try {
|
|
10736
|
-
const raw =
|
|
11042
|
+
const raw = fs6.readFileSync(path13.join(sessionsDir, f), "utf-8");
|
|
10737
11043
|
const session = JSON.parse(raw);
|
|
10738
11044
|
if (session.meta.id === sessionId) return f;
|
|
10739
11045
|
} catch (err) {
|
|
@@ -10744,11 +11050,11 @@ function findSessionFile(sessionsDir, sessionId, logger) {
|
|
|
10744
11050
|
}
|
|
10745
11051
|
function loadSession(sessionId, baseDir, logger = defaultLogger) {
|
|
10746
11052
|
const sessionsDir = getSessionsDir(baseDir);
|
|
10747
|
-
if (!
|
|
11053
|
+
if (!fs6.existsSync(sessionsDir)) return null;
|
|
10748
11054
|
try {
|
|
10749
11055
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
10750
11056
|
if (file) {
|
|
10751
|
-
const raw =
|
|
11057
|
+
const raw = fs6.readFileSync(path13.join(sessionsDir, file), "utf-8");
|
|
10752
11058
|
const parsed = JSON.parse(raw);
|
|
10753
11059
|
if (hasScalarRunner(parsed)) {
|
|
10754
11060
|
logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
|
|
@@ -10778,11 +11084,11 @@ function getLatestSession(baseDir, logger = defaultLogger) {
|
|
|
10778
11084
|
}
|
|
10779
11085
|
function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
10780
11086
|
const sessionsDir = getSessionsDir(baseDir);
|
|
10781
|
-
if (!
|
|
11087
|
+
if (!fs6.existsSync(sessionsDir)) return false;
|
|
10782
11088
|
try {
|
|
10783
11089
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
10784
11090
|
if (file) {
|
|
10785
|
-
|
|
11091
|
+
fs6.unlinkSync(path13.join(sessionsDir, file));
|
|
10786
11092
|
return true;
|
|
10787
11093
|
}
|
|
10788
11094
|
} catch (err) {
|
|
@@ -10791,7 +11097,36 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
|
10791
11097
|
return false;
|
|
10792
11098
|
}
|
|
10793
11099
|
|
|
11100
|
+
// src/utils/prdStore.ts
|
|
11101
|
+
import * as fs7 from "fs";
|
|
11102
|
+
import * as path14 from "path";
|
|
11103
|
+
var PRD_BLOCK_RE = /<!--\s*ORDEWELL_PRD_START\s+slug="([^"]+)"\s*-->\s*([\s\S]*?)\s*<!--\s*ORDEWELL_PRD_END\s*-->/;
|
|
11104
|
+
function extractPrdBlock(text) {
|
|
11105
|
+
const match = PRD_BLOCK_RE.exec(text);
|
|
11106
|
+
if (!match) return null;
|
|
11107
|
+
const slug2 = sanitizeSlug(match[1]);
|
|
11108
|
+
const markdown = match[2].trim();
|
|
11109
|
+
if (!slug2 || !markdown) return null;
|
|
11110
|
+
return { slug: slug2, markdown };
|
|
11111
|
+
}
|
|
11112
|
+
function sanitizeSlug(raw) {
|
|
11113
|
+
return raw.toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
11114
|
+
}
|
|
11115
|
+
function savePrdMarkdown(workspace, slug2, markdown) {
|
|
11116
|
+
const dir = path14.join(workspace, ".scratch", sanitizeSlug(slug2));
|
|
11117
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
11118
|
+
const file = path14.join(dir, "PRD.md");
|
|
11119
|
+
fs7.writeFileSync(file, markdown.endsWith("\n") ? markdown : markdown + "\n", "utf8");
|
|
11120
|
+
return file;
|
|
11121
|
+
}
|
|
11122
|
+
|
|
10794
11123
|
// src/services/createSession.ts
|
|
11124
|
+
var CATALOG_MODEL_CAP = 100;
|
|
11125
|
+
var MAX_TASK_QUERIES = 3;
|
|
11126
|
+
var MAX_TASK_QUERIES_HARD = 6;
|
|
11127
|
+
function freshReadBudget() {
|
|
11128
|
+
return { answered: 0, seen: /* @__PURE__ */ new Set() };
|
|
11129
|
+
}
|
|
10795
11130
|
var PlanEditError = class extends Error {
|
|
10796
11131
|
constructor(message) {
|
|
10797
11132
|
super(message);
|
|
@@ -10801,6 +11136,17 @@ var PlanEditError = class extends Error {
|
|
|
10801
11136
|
function sessionRuntimeSettings(settings) {
|
|
10802
11137
|
return { ...plannerRuntimeToggles(settings), modelAllowlist: settings.modelAllowlist };
|
|
10803
11138
|
}
|
|
11139
|
+
function resolveSkillInvocation(text, skillsService) {
|
|
11140
|
+
const match = text.trim().match(/^\/([a-z][a-z0-9_-]*)$/im);
|
|
11141
|
+
if (!match) return text;
|
|
11142
|
+
const skillName = match[1].toLowerCase();
|
|
11143
|
+
const skill = skillsService.findSkill(skillName);
|
|
11144
|
+
if (!skill) {
|
|
11145
|
+
const available = typeof skillsService.listSkills === "function" ? skillsService.listSkills().map((s) => s.name).join(", ") : "";
|
|
11146
|
+
return `Unknown skill: ${skillName}. Available skills: ${available}`;
|
|
11147
|
+
}
|
|
11148
|
+
return skill.content;
|
|
11149
|
+
}
|
|
10804
11150
|
var Session = class {
|
|
10805
11151
|
/** Injected by a test; when present it is the service, forever. */
|
|
10806
11152
|
pinnedAiService;
|
|
@@ -10828,6 +11174,7 @@ var Session = class {
|
|
|
10828
11174
|
unsubObserver = null;
|
|
10829
11175
|
hostSessionId;
|
|
10830
11176
|
currentSessionId;
|
|
11177
|
+
skillsService;
|
|
10831
11178
|
constructor(deps) {
|
|
10832
11179
|
this.config = deps.config;
|
|
10833
11180
|
this.registry = deps.registry;
|
|
@@ -10846,6 +11193,7 @@ var Session = class {
|
|
|
10846
11193
|
this.settingsFn = deps.settings;
|
|
10847
11194
|
this.hostSessionId = deps.sessionId;
|
|
10848
11195
|
this.currentSessionId = deps.sessionId ?? mintSessionId();
|
|
11196
|
+
this.skillsService = deps.skillsService ?? new SkillsService(this.workspaceRootFn());
|
|
10849
11197
|
this.approvals = new PendingApprovals({
|
|
10850
11198
|
onRequest: ({ id, request }) => this.broadcast({
|
|
10851
11199
|
type: "approval_request",
|
|
@@ -10940,7 +11288,7 @@ var Session = class {
|
|
|
10940
11288
|
onTaskChanged: () => {
|
|
10941
11289
|
if (!this.plan) return;
|
|
10942
11290
|
const tasks = this.store.allTasks;
|
|
10943
|
-
this.broadcast({ type: "status_update", tasks: tasks.map(serializeTaskStatus) });
|
|
11291
|
+
this.broadcast({ type: "status_update", tasks: tasks.map((t) => serializeTaskStatus(t, this.orchestrator.getIdleSince(t.id))) });
|
|
10944
11292
|
},
|
|
10945
11293
|
onQueueReady: () => {
|
|
10946
11294
|
this.broadcast({ type: "queue_ready" });
|
|
@@ -10958,7 +11306,7 @@ var Session = class {
|
|
|
10958
11306
|
onTick: () => {
|
|
10959
11307
|
if (!this.plan) return;
|
|
10960
11308
|
const tasks = this.store.allTasks;
|
|
10961
|
-
this.broadcast({ type: "status_update", tasks: tasks.map(serializeTaskStatus) });
|
|
11309
|
+
this.broadcast({ type: "status_update", tasks: tasks.map((t) => serializeTaskStatus(t, this.orchestrator.getIdleSince(t.id))) });
|
|
10962
11310
|
},
|
|
10963
11311
|
onExecutionComplete: () => {
|
|
10964
11312
|
if (!this.plan) return;
|
|
@@ -11028,6 +11376,21 @@ var Session = class {
|
|
|
11028
11376
|
runnerModesFor(runners) {
|
|
11029
11377
|
return runnerModesFrom(this.registry, runners);
|
|
11030
11378
|
}
|
|
11379
|
+
/**
|
|
11380
|
+
* The catalog a model/task-mode edit is checked against — the same
|
|
11381
|
+
* discovered models and manifest modes the per-turn catalog block shows the
|
|
11382
|
+
* planner, so a refusal here can never name something invalid that the
|
|
11383
|
+
* planner was never told about. `modelsCache` is read live, not filtered by
|
|
11384
|
+
* the allowlist here — {@link checkModelAndModeValidity} narrows by
|
|
11385
|
+
* allowlist itself, the same way `coerceAssignments` does.
|
|
11386
|
+
*/
|
|
11387
|
+
editCatalog() {
|
|
11388
|
+
return {
|
|
11389
|
+
modelsByRunner: this.modelsCache,
|
|
11390
|
+
runnerModes: this.runnerModesFor(this.plan?.runners ?? []),
|
|
11391
|
+
perRunnerAllowlist: this.settingsFn().modelAllowlist ?? this.currentAllowlist
|
|
11392
|
+
};
|
|
11393
|
+
}
|
|
11031
11394
|
get planState() {
|
|
11032
11395
|
return this.plan;
|
|
11033
11396
|
}
|
|
@@ -11155,7 +11518,7 @@ var Session = class {
|
|
|
11155
11518
|
*/
|
|
11156
11519
|
async startPlanning(goal, runners, options) {
|
|
11157
11520
|
this.plan = null;
|
|
11158
|
-
this.goal = goal;
|
|
11521
|
+
this.goal = this.resolveSkillInvocation(goal);
|
|
11159
11522
|
this.remintSessionId();
|
|
11160
11523
|
this.beginFreshPlan();
|
|
11161
11524
|
const enabled = this.config.enabledRunners;
|
|
@@ -11164,7 +11527,7 @@ var Session = class {
|
|
|
11164
11527
|
const modelsByRunner = await this.modelResolver.modelsForRunners(chosenRunners);
|
|
11165
11528
|
this.modelsCache = modelsByRunner;
|
|
11166
11529
|
const runnerModes = this.runnerModesFor(chosenRunners);
|
|
11167
|
-
const {
|
|
11530
|
+
const { verificationEnabled, modelAllowlist } = this.settingsFn();
|
|
11168
11531
|
this.currentAllowlist = modelAllowlist ?? {};
|
|
11169
11532
|
const filteredModels = filterModelsForPrompt(modelsByRunner, this.currentAllowlist);
|
|
11170
11533
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -11174,22 +11537,18 @@ var Session = class {
|
|
|
11174
11537
|
status: "draft",
|
|
11175
11538
|
runners: chosenRunners,
|
|
11176
11539
|
lastUpdated: now,
|
|
11177
|
-
researchLog: [{ id: `up-${Date.now()}`, type: "user_prompt", content: goal, timestamp: now }],
|
|
11178
|
-
conversationHistory: [{ role: "user", content: goal, timestamp: now }]
|
|
11540
|
+
researchLog: [{ id: `up-${Date.now()}`, type: "user_prompt", content: this.goal, timestamp: now }],
|
|
11541
|
+
conversationHistory: [{ role: "user", content: this.goal, timestamp: now }]
|
|
11179
11542
|
};
|
|
11180
11543
|
const releaseAbort = this.denyApprovalsOnAbort(options?.signal);
|
|
11181
11544
|
try {
|
|
11182
11545
|
const turn = await this.aiService.startConversation({
|
|
11183
|
-
goal,
|
|
11546
|
+
goal: this.goal,
|
|
11184
11547
|
runners: chosenRunners,
|
|
11185
11548
|
modelsByRunner: filteredModels,
|
|
11186
11549
|
runnerModes,
|
|
11187
11550
|
autonomousDefault: this.config.autonomousMode,
|
|
11188
|
-
grillMeEnabled: grillMeEnabled ?? false,
|
|
11189
|
-
prdEnabled: prdEnabled ?? false,
|
|
11190
|
-
reviewEnabled: reviewEnabled ?? false,
|
|
11191
11551
|
verificationEnabled: verificationEnabled ?? false,
|
|
11192
|
-
researchSubagentsEnabled: researchSubagentsEnabled ?? false,
|
|
11193
11552
|
fs: this.fsAdapter,
|
|
11194
11553
|
fetcher: this.fetcher,
|
|
11195
11554
|
onProgress: (p) => this.translateProgress(p),
|
|
@@ -11201,19 +11560,22 @@ var Session = class {
|
|
|
11201
11560
|
}
|
|
11202
11561
|
}
|
|
11203
11562
|
/**
|
|
11204
|
-
* Every subsequent user reply in the planning conversation —
|
|
11205
|
-
* answers,
|
|
11563
|
+
* Every subsequent user reply in the planning conversation — clarifying
|
|
11564
|
+
* answers, outline confirm. One branch, no phase ladder.
|
|
11206
11565
|
*/
|
|
11207
11566
|
async continueConversation(userMessage, options) {
|
|
11208
11567
|
if (!this.plan) throw new Error("No planning conversation to continue");
|
|
11209
11568
|
const priorHistory = this.plan.conversationHistory ?? [];
|
|
11569
|
+
const priorResearchLog = this.plan.researchLog ?? [];
|
|
11210
11570
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11211
|
-
this.
|
|
11212
|
-
this.
|
|
11213
|
-
const
|
|
11571
|
+
const resolved = this.resolveSkillInvocation(userMessage);
|
|
11572
|
+
this.appendTranscript("user", resolved, now);
|
|
11573
|
+
const historyAfterOwnAppend = this.plan.conversationHistory;
|
|
11574
|
+
this.plan.researchLog = [...this.plan.researchLog ?? [], { id: `up-${Date.now()}`, type: "user_prompt", content: resolved, timestamp: now }];
|
|
11575
|
+
const contextBlock = [this.catalogBlock(), this.planContextBlock()].filter(Boolean).join("\n\n");
|
|
11214
11576
|
const outgoing = contextBlock ? `${contextBlock}
|
|
11215
11577
|
|
|
11216
|
-
${
|
|
11578
|
+
${resolved}` : resolved;
|
|
11217
11579
|
const aiService = this.aiService;
|
|
11218
11580
|
const canContinueLive = aiService.hasActiveConversation() && (aiService.conversationMatchesConfig?.() ?? true);
|
|
11219
11581
|
const releaseAbort = this.denyApprovalsOnAbort(options?.signal);
|
|
@@ -11223,6 +11585,8 @@ ${userMessage}` : userMessage;
|
|
|
11223
11585
|
(p) => this.translateProgress(p),
|
|
11224
11586
|
options?.signal
|
|
11225
11587
|
) : await this.resumeConversation(outgoing, priorHistory, options);
|
|
11588
|
+
const reads = freshReadBudget();
|
|
11589
|
+
turn = await this.drainTaskQueries(turn, reads, options);
|
|
11226
11590
|
if ((turn.kind === "task_ops" || turn.kind === "plan") && this.hasLiveWork) {
|
|
11227
11591
|
this.queueMessage(userMessage);
|
|
11228
11592
|
this.plan.queuedMessages = this.getQueuedMessages();
|
|
@@ -11232,11 +11596,74 @@ ${userMessage}` : userMessage;
|
|
|
11232
11596
|
researchLog: turn.researchLog
|
|
11233
11597
|
};
|
|
11234
11598
|
}
|
|
11235
|
-
return await this.settleTurn(turn, options);
|
|
11599
|
+
return await this.settleTurn(turn, options, reads);
|
|
11600
|
+
} catch (err) {
|
|
11601
|
+
if (this.plan && this.plan.conversationHistory === historyAfterOwnAppend) {
|
|
11602
|
+
this.plan.conversationHistory = priorHistory;
|
|
11603
|
+
this.plan.researchLog = priorResearchLog;
|
|
11604
|
+
}
|
|
11605
|
+
throw err;
|
|
11236
11606
|
} finally {
|
|
11237
11607
|
releaseAbort();
|
|
11238
11608
|
}
|
|
11239
11609
|
}
|
|
11610
|
+
/**
|
|
11611
|
+
* Answer every read the planner emits until it says something else.
|
|
11612
|
+
*
|
|
11613
|
+
* The channel is a text envelope rather than a registered tool because the
|
|
11614
|
+
* protocol has to be identical on both planner backends (ADR-0009): Ordewell
|
|
11615
|
+
* owns a tool loop only in the API case, and a harness planner running as a
|
|
11616
|
+
* coding-agent subprocess can only be reached this way.
|
|
11617
|
+
*
|
|
11618
|
+
* Draining here — outside {@link repairLoop} — is what keeps reads free of
|
|
11619
|
+
* the repair budget. A planner that looks a task up and *then* fumbles its
|
|
11620
|
+
* ops JSON still gets its two corrective retries; charging it for the read
|
|
11621
|
+
* would cost it the chance to fix the edit.
|
|
11622
|
+
*/
|
|
11623
|
+
async drainTaskQueries(turn, reads, options) {
|
|
11624
|
+
const carried = [];
|
|
11625
|
+
let current = turn;
|
|
11626
|
+
while (current.kind === "task_query") {
|
|
11627
|
+
carried.push(...current.researchLog);
|
|
11628
|
+
if (reads.answered >= MAX_TASK_QUERIES_HARD || !this.aiService.hasActiveConversation() || options?.signal?.aborted) {
|
|
11629
|
+
return {
|
|
11630
|
+
kind: "message",
|
|
11631
|
+
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.",
|
|
11632
|
+
researchLog: carried
|
|
11633
|
+
};
|
|
11634
|
+
}
|
|
11635
|
+
const signature = taskQuerySignature(current.query);
|
|
11636
|
+
const insist = reads.seen.has(signature) || reads.answered >= MAX_TASK_QUERIES;
|
|
11637
|
+
reads.seen.add(signature);
|
|
11638
|
+
reads.answered++;
|
|
11639
|
+
const answer = this.taskQueryAnswer(current.query);
|
|
11640
|
+
current = await this.aiService.continueConversation(
|
|
11641
|
+
insist ? `${answer}
|
|
11642
|
+
|
|
11643
|
+
${TASK_QUERY_ANSWER_OR_OPS}` : answer,
|
|
11644
|
+
(p) => this.translateProgress(p),
|
|
11645
|
+
options?.signal
|
|
11646
|
+
);
|
|
11647
|
+
}
|
|
11648
|
+
return carried.length > 0 ? { ...current, researchLog: [...carried, ...current.researchLog] } : current;
|
|
11649
|
+
}
|
|
11650
|
+
/**
|
|
11651
|
+
* Render one read out of live state. Never persisted to the transcript: the
|
|
11652
|
+
* detail is context for the planner's next reply, and re-sending it on every
|
|
11653
|
+
* later turn is exactly the cost this channel exists to avoid.
|
|
11654
|
+
*/
|
|
11655
|
+
taskQueryAnswer(query) {
|
|
11656
|
+
const runners = this.plan?.runners ?? [];
|
|
11657
|
+
const allowlist = this.settingsFn().modelAllowlist ?? this.currentAllowlist;
|
|
11658
|
+
return renderTaskQueryAnswer(query, this.store.planTasks, {
|
|
11659
|
+
runners,
|
|
11660
|
+
// Allowlist-filtered like the always-on block: a read must not offer a
|
|
11661
|
+
// model the planner is forbidden to assign.
|
|
11662
|
+
models: filterModelsForPrompt(this.modelsCache, allowlist),
|
|
11663
|
+
modes: this.runnerModesFor(runners),
|
|
11664
|
+
autonomousDefault: this.config.autonomousMode
|
|
11665
|
+
});
|
|
11666
|
+
}
|
|
11240
11667
|
/**
|
|
11241
11668
|
* Drive a planner turn to a persisted, broadcast outcome. Task edits apply
|
|
11242
11669
|
* atomically; validation failures are fed back to the model for up to 2
|
|
@@ -11244,7 +11671,7 @@ ${userMessage}` : userMessage;
|
|
|
11244
11671
|
* first turn (startPlanning) and every later turn route through here — one
|
|
11245
11672
|
* path, not two.
|
|
11246
11673
|
*/
|
|
11247
|
-
async settleTurn(turn, options) {
|
|
11674
|
+
async settleTurn(turn, options, reads = freshReadBudget()) {
|
|
11248
11675
|
const invalidOps = (errors, researchLog) => ({
|
|
11249
11676
|
turn: {
|
|
11250
11677
|
kind: "message",
|
|
@@ -11256,11 +11683,15 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
11256
11683
|
}
|
|
11257
11684
|
});
|
|
11258
11685
|
const settled = await repairLoop({
|
|
11259
|
-
first: () => turn,
|
|
11260
|
-
resend: (corrective) => this.
|
|
11261
|
-
|
|
11262
|
-
|
|
11263
|
-
|
|
11686
|
+
first: () => this.drainTaskQueries(turn, reads, options),
|
|
11687
|
+
resend: async (corrective) => this.drainTaskQueries(
|
|
11688
|
+
await this.aiService.continueConversation(
|
|
11689
|
+
corrective,
|
|
11690
|
+
(p) => this.translateProgress(p),
|
|
11691
|
+
options?.signal
|
|
11692
|
+
),
|
|
11693
|
+
reads,
|
|
11694
|
+
options
|
|
11264
11695
|
),
|
|
11265
11696
|
interpret: (t) => {
|
|
11266
11697
|
if (t.kind !== "task_ops") return { done: { turn: t } };
|
|
@@ -11280,6 +11711,52 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
11280
11711
|
}
|
|
11281
11712
|
return this.applyConversationTurn(settled.turn);
|
|
11282
11713
|
}
|
|
11714
|
+
/**
|
|
11715
|
+
* The catalog the planner may actually draw from — model ids and task-mode
|
|
11716
|
+
* ids, per selected runner — emitted on EVERY turn, before any plan exists
|
|
11717
|
+
* or after. The system prompt shows this once at conversation start; a long
|
|
11718
|
+
* clarifying conversation outlives that single showing and the planner
|
|
11719
|
+
* starts misquoting it, so this re-states it per turn instead.
|
|
11720
|
+
*
|
|
11721
|
+
* Built from the allowlist-filtered view (`filterModelsForPrompt`), the same
|
|
11722
|
+
* one the system prompt used — a restricted allowlist stays a hard bound on
|
|
11723
|
+
* every turn, not just the first. `this.modelsCache` itself is never
|
|
11724
|
+
* filtered: `coerceAssignments` needs the full discovered catalog alongside
|
|
11725
|
+
* the allowlist to resolve labels and clamp effort to real variants.
|
|
11726
|
+
*
|
|
11727
|
+
* Reads `this.plan.runners` live, so a runner admitted mid-session by a
|
|
11728
|
+
* retarget (`admitRunner`) is filtered and shown like every other — there is
|
|
11729
|
+
* no separate "originally selected" list to fall stale.
|
|
11730
|
+
*/
|
|
11731
|
+
catalogBlock() {
|
|
11732
|
+
if (!this.plan) return null;
|
|
11733
|
+
const allowlist = this.settingsFn().modelAllowlist ?? this.currentAllowlist;
|
|
11734
|
+
const filteredModels = filterModelsForPrompt(this.modelsCache, allowlist);
|
|
11735
|
+
const runnerModes = this.runnerModesFor(this.plan.runners);
|
|
11736
|
+
const autonomousDefault = this.config.autonomousMode;
|
|
11737
|
+
const modelLines = this.plan.runners.map((runner) => {
|
|
11738
|
+
const models = filteredModels[runner] ?? [];
|
|
11739
|
+
const capped = models.slice(0, CATALOG_MODEL_CAP);
|
|
11740
|
+
const remainder = models.length - capped.length;
|
|
11741
|
+
const ids = capped.map((m) => m.modelId).join(", ") || "(no models discovered)";
|
|
11742
|
+
return `${runner}: ${ids}${remainder > 0 ? ` \u2026 +${remainder} more not shown` : ""}`;
|
|
11743
|
+
});
|
|
11744
|
+
const modeLines = this.plan.runners.map((runner) => {
|
|
11745
|
+
const modes = runnerModes[runner] ?? [];
|
|
11746
|
+
if (modes.length === 0) return `${runner}: (no modes declared)`;
|
|
11747
|
+
const defaultId = resolveDefaultMode(modes, autonomousDefault);
|
|
11748
|
+
const ids = modes.map((m) => `${m.id}${m.id === defaultId ? " (default)" : ""}`).join(", ");
|
|
11749
|
+
return `${runner}: ${ids}`;
|
|
11750
|
+
});
|
|
11751
|
+
return [
|
|
11752
|
+
"<available_models>",
|
|
11753
|
+
...modelLines,
|
|
11754
|
+
"</available_models>",
|
|
11755
|
+
"<available_task_modes>",
|
|
11756
|
+
...modeLines,
|
|
11757
|
+
"</available_task_modes>"
|
|
11758
|
+
].join("\n");
|
|
11759
|
+
}
|
|
11283
11760
|
/**
|
|
11284
11761
|
* The "you are here" block for post-plan chat: current tasks with stable
|
|
11285
11762
|
* references, plus the task-ops protocol. Injected per turn (never
|
|
@@ -11289,44 +11766,48 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
11289
11766
|
planContextBlock() {
|
|
11290
11767
|
if (!this.plan || this.store.planTasks.length === 0) return null;
|
|
11291
11768
|
const orderOf = new Map(this.store.planTasks.map((t) => [t.id, `#${t.order}`]));
|
|
11292
|
-
const lines = this.store.planTasks.map(
|
|
11293
|
-
|
|
11294
|
-
|
|
11769
|
+
const lines = this.store.planTasks.map((t) => {
|
|
11770
|
+
const isMan = t.type === "user";
|
|
11771
|
+
const runFields = isMan ? `steps:${t.userSteps?.length ?? 0}` : `${t.assignedModel ? `model:${t.assignedModel.modelId} ` : ""}mode:${t.taskMode ?? "build"} effort:${t.thinkingEffort ?? "-"}`;
|
|
11772
|
+
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(", ")}]`;
|
|
11773
|
+
});
|
|
11295
11774
|
const locked = this.store.planTasks.filter((t) => t.status === "in_progress");
|
|
11296
11775
|
const execNote = this.hasLiveWork ? `
|
|
11297
11776
|
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") : "";
|
|
11777
|
+
const updateChanges = UPDATABLE_FIELDS.map((f) => f === "dependencies" ? '"dependencies"?:["<id or #order>"]' : `"${f}"?`).join(",");
|
|
11305
11778
|
return [
|
|
11306
11779
|
"<current_plan>",
|
|
11307
11780
|
...lines,
|
|
11308
11781
|
"</current_plan>",
|
|
11309
|
-
|
|
11310
|
-
"The block above is the CURRENT task plan. Choose how to respond:",
|
|
11782
|
+
"The block above is the CURRENT task plan \u2014 short fields only. Choose how to respond:",
|
|
11311
11783
|
"- To answer a question or discuss, reply in plain prose (no JSON).",
|
|
11784
|
+
// The reminder text is owned by TaskQuery beside the parser, so the
|
|
11785
|
+
// protocol the planner reads and the one Ordewell accepts stay one thing.
|
|
11786
|
+
TASK_QUERY_REMINDER,
|
|
11312
11787
|
"- To modify specific tasks, reply with ONLY this JSON object:",
|
|
11313
11788
|
' {"taskOps": [',
|
|
11314
|
-
|
|
11315
|
-
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?}},',
|
|
11789
|
+
` {"op":"update","taskId":"<id or #order>","changes":{${updateChanges}}},`,
|
|
11790
|
+
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?},"handle"?:"<name>"},',
|
|
11316
11791
|
' {"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"?,...}, ...]}',
|
|
11792
|
+
' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]}, // only to re-prioritise INDEPENDENT tasks',
|
|
11793
|
+
' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...},"handle"?:"<name>"},',
|
|
11794
|
+
' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...],"handle"?:"<name>"},',
|
|
11795
|
+
` {"op":"rearm","taskId":"<id or #order>","changes"?:{${updateChanges}}}`,
|
|
11320
11796
|
" ]}",
|
|
11321
11797
|
'- For sweeping changes, you may instead emit a full {"tasks":[...]} plan JSON.',
|
|
11798
|
+
'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.',
|
|
11799
|
+
'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
11800
|
'When creating or changing tasks, set "assignedRunner" and "assignedModel" ({"modelId","modelLabel","thinkingEffort"?}) using only the runners and models listed in <available_models>.',
|
|
11323
|
-
|
|
11801
|
+
'Keep dependencies consistent: no cycles, no references to removed tasks, and never touch running or completed tasks \u2014 "rearm" is the one exception, below.',
|
|
11802
|
+
'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,
|
|
11803
|
+
'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.',
|
|
11804
|
+
'"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
11805
|
].filter(Boolean).join("\n");
|
|
11325
11806
|
}
|
|
11326
11807
|
/** Validate + commit a task_ops turn atomically. Returns the errors on rejection (plan untouched). */
|
|
11327
11808
|
applyTaskOpsTurn(turn) {
|
|
11328
11809
|
if (!this.plan) throw new Error("No active plan state");
|
|
11329
|
-
const result = applyTaskOps(this.store.planTasks, turn.ops, this.plan.runners);
|
|
11810
|
+
const result = applyTaskOps(this.store.planTasks, turn.ops, this.plan.runners, this.editCatalog());
|
|
11330
11811
|
if (!result.ok) return { errors: result.errors };
|
|
11331
11812
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11332
11813
|
const content = `Tasks updated:
|
|
@@ -11366,7 +11847,7 @@ Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.
|
|
|
11366
11847
|
const modelsByRunner = await this.modelResolver.modelsForRunners(runners);
|
|
11367
11848
|
this.modelsCache = modelsByRunner;
|
|
11368
11849
|
const runnerModes = this.runnerModesFor(runners);
|
|
11369
|
-
const {
|
|
11850
|
+
const { verificationEnabled, modelAllowlist } = this.settingsFn();
|
|
11370
11851
|
this.currentAllowlist = modelAllowlist ?? {};
|
|
11371
11852
|
const filteredModels = filterModelsForPrompt(modelsByRunner, this.currentAllowlist);
|
|
11372
11853
|
const goal = this.goal || priorHistory.find((m) => m.role === "user")?.content || userMessage;
|
|
@@ -11376,11 +11857,7 @@ Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.
|
|
|
11376
11857
|
modelsByRunner: filteredModels,
|
|
11377
11858
|
runnerModes,
|
|
11378
11859
|
autonomousDefault: this.config.autonomousMode,
|
|
11379
|
-
grillMeEnabled: grillMeEnabled ?? false,
|
|
11380
|
-
prdEnabled: prdEnabled ?? false,
|
|
11381
|
-
reviewEnabled: reviewEnabled ?? false,
|
|
11382
11860
|
verificationEnabled: verificationEnabled ?? false,
|
|
11383
|
-
researchSubagentsEnabled: researchSubagentsEnabled ?? false,
|
|
11384
11861
|
fs: this.fsAdapter,
|
|
11385
11862
|
fetcher: this.fetcher,
|
|
11386
11863
|
onProgress: (p) => this.translateProgress(p),
|
|
@@ -11393,6 +11870,17 @@ Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.
|
|
|
11393
11870
|
get isConversationActive() {
|
|
11394
11871
|
return this.aiService.hasActiveConversation();
|
|
11395
11872
|
}
|
|
11873
|
+
/**
|
|
11874
|
+
* Intercept a skill invocation (/skill-name) and substitute the skill's
|
|
11875
|
+
* markdown content BEFORE the message reaches the planner. Prevents runners
|
|
11876
|
+
* (Claude Code, OpenCode) from trying to resolve the skill in their own
|
|
11877
|
+
* directory instead of .ordewell/skills/. An unknown skill is surfaced to
|
|
11878
|
+
* the planner as a notice naming what IS available, rather than passing a
|
|
11879
|
+
* bare /unknown through to be mis-resolved.
|
|
11880
|
+
*/
|
|
11881
|
+
resolveSkillInvocation(text) {
|
|
11882
|
+
return resolveSkillInvocation(text, this.skillsService);
|
|
11883
|
+
}
|
|
11396
11884
|
/** Append one dialogue entry to the persisted transcript. Callers persist via the mutatePlan ritual. */
|
|
11397
11885
|
appendTranscript(role, content, timestamp, kind) {
|
|
11398
11886
|
if (!this.plan) return;
|
|
@@ -11593,15 +12081,22 @@ Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.
|
|
|
11593
12081
|
return plan;
|
|
11594
12082
|
}
|
|
11595
12083
|
/**
|
|
11596
|
-
* Patch one task's fields. A hand-set dependency list
|
|
11597
|
-
*
|
|
11598
|
-
*
|
|
11599
|
-
*
|
|
12084
|
+
* Patch one task's fields. A hand-set dependency list, or a type flip
|
|
12085
|
+
* between AI and MAN, are the patches that can leave a task incoherent
|
|
12086
|
+
* (unschedulable, or carrying fields that mean nothing for its new type),
|
|
12087
|
+
* so both go through the same {@link validateTaskEdit} guard the planner's
|
|
12088
|
+
* task-ops applier uses (as the 'direct' actor, which skips the lock rule)
|
|
12089
|
+
* rather than a second copy of the rules — and throws, so the surface can
|
|
12090
|
+
* say why. Gated on the task existing so an edit to an unknown id still
|
|
12091
|
+
* falls through to the no-op `store.update` below instead of throwing.
|
|
11600
12092
|
*/
|
|
11601
12093
|
async updateTask(taskId, changes) {
|
|
11602
|
-
if (changes.dependencies) {
|
|
11603
|
-
const check =
|
|
11604
|
-
if (!check.ok) throw new PlanEditError(check.error ?? "Those
|
|
12094
|
+
if ((changes.dependencies || changes.type || changes.assignedModel || changes.taskMode) && this.store.get(taskId)) {
|
|
12095
|
+
const check = validateTaskEdit("direct", this.store.planTasks, taskId, changes, this.editCatalog());
|
|
12096
|
+
if (!check.ok) throw new PlanEditError(check.error ?? "Those changes are not valid");
|
|
12097
|
+
if (check.clear?.length) {
|
|
12098
|
+
changes = { ...changes, ...Object.fromEntries(check.clear.map((f) => [f, void 0])) };
|
|
12099
|
+
}
|
|
11605
12100
|
}
|
|
11606
12101
|
return this.editPlan(
|
|
11607
12102
|
() => Boolean(this.store.update(taskId, changes)),
|
|
@@ -12241,8 +12736,8 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
12241
12736
|
import { execFile as execFile2 } from "child_process";
|
|
12242
12737
|
import { promisify as promisify4 } from "util";
|
|
12243
12738
|
import { tmpdir } from "os";
|
|
12244
|
-
import { join as
|
|
12245
|
-
import { existsSync as
|
|
12739
|
+
import { join as join13 } from "path";
|
|
12740
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
12246
12741
|
|
|
12247
12742
|
// src/utils/tmux.ts
|
|
12248
12743
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -12347,7 +12842,7 @@ var TmuxSession = class extends AbstractTerminalSession {
|
|
|
12347
12842
|
if (this.exited) return;
|
|
12348
12843
|
let content;
|
|
12349
12844
|
try {
|
|
12350
|
-
content =
|
|
12845
|
+
content = existsSync8(this.logPath) ? readFileSync5(this.logPath, "utf8") : "";
|
|
12351
12846
|
} catch {
|
|
12352
12847
|
return;
|
|
12353
12848
|
}
|
|
@@ -12526,7 +13021,7 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
12526
13021
|
windowName,
|
|
12527
13022
|
this.execFileImpl,
|
|
12528
13023
|
this.pollIntervalMs,
|
|
12529
|
-
|
|
13024
|
+
join13(this.logDir, `${id}.log`),
|
|
12530
13025
|
this.socket
|
|
12531
13026
|
);
|
|
12532
13027
|
invocation.env = { ...invocation.env, PATH: await this.resolvePath() };
|
|
@@ -12538,22 +13033,21 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
12538
13033
|
|
|
12539
13034
|
// src/utils/daemonToken.ts
|
|
12540
13035
|
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
12541
|
-
import { closeSync, mkdirSync as
|
|
12542
|
-
import {
|
|
12543
|
-
import { join as join11 } from "path";
|
|
13036
|
+
import { closeSync, mkdirSync as mkdirSync7, openSync, readFileSync as readFileSync6, unlinkSync as unlinkSync3, writeSync } from "fs";
|
|
13037
|
+
import { join as join14 } from "path";
|
|
12544
13038
|
var BEARER_PREFIX = "bearer ";
|
|
12545
13039
|
var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
|
|
12546
13040
|
var DAEMON_SUBPROTOCOL = "ordewell.v1";
|
|
12547
13041
|
function configDir() {
|
|
12548
|
-
return
|
|
13042
|
+
return globalDataDir();
|
|
12549
13043
|
}
|
|
12550
13044
|
function daemonTokenPath(port) {
|
|
12551
|
-
return
|
|
13045
|
+
return join14(configDir(), `server-${port}.token`);
|
|
12552
13046
|
}
|
|
12553
13047
|
function mintDaemonToken(port) {
|
|
12554
13048
|
const token = randomBytes2(32).toString("base64url");
|
|
12555
13049
|
const file = daemonTokenPath(port);
|
|
12556
|
-
|
|
13050
|
+
mkdirSync7(configDir(), { recursive: true });
|
|
12557
13051
|
try {
|
|
12558
13052
|
unlinkSync3(file);
|
|
12559
13053
|
} catch {
|
|
@@ -12568,7 +13062,7 @@ function mintDaemonToken(port) {
|
|
|
12568
13062
|
}
|
|
12569
13063
|
function readDaemonToken(port) {
|
|
12570
13064
|
try {
|
|
12571
|
-
const token =
|
|
13065
|
+
const token = readFileSync6(daemonTokenPath(port), "utf8").trim();
|
|
12572
13066
|
return token === "" ? void 0 : token;
|
|
12573
13067
|
} catch {
|
|
12574
13068
|
return void 0;
|
|
@@ -12609,19 +13103,18 @@ function tokensMatch(presented, expected) {
|
|
|
12609
13103
|
}
|
|
12610
13104
|
|
|
12611
13105
|
// src/plugins/RunnerRegistry.ts
|
|
12612
|
-
import * as
|
|
12613
|
-
import * as
|
|
12614
|
-
import * as
|
|
13106
|
+
import * as fs9 from "fs";
|
|
13107
|
+
import * as path17 from "path";
|
|
13108
|
+
import * as os4 from "os";
|
|
12615
13109
|
import { execFileSync } from "child_process";
|
|
12616
13110
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
12617
13111
|
|
|
12618
13112
|
// src/plugins/FsPluginStore.ts
|
|
12619
|
-
import * as
|
|
12620
|
-
import * as
|
|
12621
|
-
import * as os4 from "os";
|
|
13113
|
+
import * as fs8 from "fs";
|
|
13114
|
+
import * as path16 from "path";
|
|
12622
13115
|
|
|
12623
13116
|
// src/plugins/pluginNames.ts
|
|
12624
|
-
import * as
|
|
13117
|
+
import * as path15 from "path";
|
|
12625
13118
|
var PLUGIN_NAME_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
12626
13119
|
var MAX_PLUGIN_NAME_LENGTH = 64;
|
|
12627
13120
|
function isPlainPluginName(name) {
|
|
@@ -12636,10 +13129,10 @@ function assertPlainPluginName(name) {
|
|
|
12636
13129
|
}
|
|
12637
13130
|
function resolvePluginInstallDir(pluginsDir, name) {
|
|
12638
13131
|
assertPlainPluginName(name);
|
|
12639
|
-
const root =
|
|
12640
|
-
const dest =
|
|
12641
|
-
const relative3 =
|
|
12642
|
-
if (relative3 !== name || relative3 === "" ||
|
|
13132
|
+
const root = path15.resolve(pluginsDir);
|
|
13133
|
+
const dest = path15.resolve(root, name);
|
|
13134
|
+
const relative3 = path15.relative(root, dest);
|
|
13135
|
+
if (relative3 !== name || relative3 === "" || path15.isAbsolute(relative3)) {
|
|
12643
13136
|
throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
|
|
12644
13137
|
}
|
|
12645
13138
|
return dest;
|
|
@@ -12654,13 +13147,13 @@ function isValidManifest(obj) {
|
|
|
12654
13147
|
|
|
12655
13148
|
// src/plugins/FsPluginStore.ts
|
|
12656
13149
|
function userPluginsDir() {
|
|
12657
|
-
return
|
|
13150
|
+
return path16.join(globalDataDir(), "plugins");
|
|
12658
13151
|
}
|
|
12659
13152
|
function loadManifestFromDir(dir) {
|
|
12660
|
-
const manifestPath =
|
|
12661
|
-
if (!
|
|
13153
|
+
const manifestPath = path16.join(dir, "manifest.json");
|
|
13154
|
+
if (!fs8.existsSync(manifestPath)) return null;
|
|
12662
13155
|
try {
|
|
12663
|
-
const raw =
|
|
13156
|
+
const raw = fs8.readFileSync(manifestPath, "utf8");
|
|
12664
13157
|
const parsed = JSON.parse(raw);
|
|
12665
13158
|
if (!isValidManifest(parsed)) return null;
|
|
12666
13159
|
return parsed;
|
|
@@ -12668,31 +13161,31 @@ function loadManifestFromDir(dir) {
|
|
|
12668
13161
|
return null;
|
|
12669
13162
|
}
|
|
12670
13163
|
}
|
|
12671
|
-
function
|
|
13164
|
+
function copyDirSync3(src, dest) {
|
|
12672
13165
|
ensureDir(dest);
|
|
12673
|
-
const entries =
|
|
13166
|
+
const entries = fs8.readdirSync(src, { withFileTypes: true });
|
|
12674
13167
|
for (const entry of entries) {
|
|
12675
|
-
const srcPath =
|
|
12676
|
-
const destPath =
|
|
13168
|
+
const srcPath = path16.join(src, entry.name);
|
|
13169
|
+
const destPath = path16.join(dest, entry.name);
|
|
12677
13170
|
if (entry.isDirectory()) {
|
|
12678
|
-
|
|
13171
|
+
copyDirSync3(srcPath, destPath);
|
|
12679
13172
|
} else {
|
|
12680
|
-
|
|
13173
|
+
fs8.copyFileSync(srcPath, destPath);
|
|
12681
13174
|
}
|
|
12682
13175
|
}
|
|
12683
13176
|
}
|
|
12684
13177
|
function rmDirSync(dir) {
|
|
12685
|
-
if (!
|
|
12686
|
-
const entries =
|
|
13178
|
+
if (!fs8.existsSync(dir)) return;
|
|
13179
|
+
const entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
12687
13180
|
for (const entry of entries) {
|
|
12688
|
-
const p =
|
|
13181
|
+
const p = path16.join(dir, entry.name);
|
|
12689
13182
|
if (entry.isDirectory()) {
|
|
12690
13183
|
rmDirSync(p);
|
|
12691
13184
|
} else {
|
|
12692
|
-
|
|
13185
|
+
fs8.unlinkSync(p);
|
|
12693
13186
|
}
|
|
12694
13187
|
}
|
|
12695
|
-
|
|
13188
|
+
fs8.rmdirSync(dir);
|
|
12696
13189
|
}
|
|
12697
13190
|
var FsPluginStore = class {
|
|
12698
13191
|
getUserPluginsDir() {
|
|
@@ -12700,10 +13193,10 @@ var FsPluginStore = class {
|
|
|
12700
13193
|
}
|
|
12701
13194
|
listUserPluginDirs() {
|
|
12702
13195
|
const dir = userPluginsDir();
|
|
12703
|
-
if (!
|
|
13196
|
+
if (!fs8.existsSync(dir)) return [];
|
|
12704
13197
|
let entries;
|
|
12705
13198
|
try {
|
|
12706
|
-
entries =
|
|
13199
|
+
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
12707
13200
|
} catch {
|
|
12708
13201
|
return [];
|
|
12709
13202
|
}
|
|
@@ -12711,7 +13204,7 @@ var FsPluginStore = class {
|
|
|
12711
13204
|
}
|
|
12712
13205
|
listDir(dir) {
|
|
12713
13206
|
try {
|
|
12714
|
-
return
|
|
13207
|
+
return fs8.readdirSync(dir);
|
|
12715
13208
|
} catch {
|
|
12716
13209
|
return [];
|
|
12717
13210
|
}
|
|
@@ -12720,7 +13213,7 @@ var FsPluginStore = class {
|
|
|
12720
13213
|
return loadManifestFromDir(pluginDir);
|
|
12721
13214
|
}
|
|
12722
13215
|
copyDir(sourceDir, destDir) {
|
|
12723
|
-
|
|
13216
|
+
copyDirSync3(sourceDir, destDir);
|
|
12724
13217
|
}
|
|
12725
13218
|
removeDir(dir) {
|
|
12726
13219
|
rmDirSync(dir);
|
|
@@ -12729,20 +13222,20 @@ var FsPluginStore = class {
|
|
|
12729
13222
|
ensureDir(dir);
|
|
12730
13223
|
}
|
|
12731
13224
|
writeFile(filePath, content) {
|
|
12732
|
-
|
|
13225
|
+
fs8.writeFileSync(filePath, content);
|
|
12733
13226
|
}
|
|
12734
13227
|
readFile(filePath) {
|
|
12735
13228
|
try {
|
|
12736
|
-
return
|
|
13229
|
+
return fs8.readFileSync(filePath, "utf8");
|
|
12737
13230
|
} catch {
|
|
12738
13231
|
return null;
|
|
12739
13232
|
}
|
|
12740
13233
|
}
|
|
12741
|
-
dirExists(
|
|
12742
|
-
return
|
|
13234
|
+
dirExists(path20) {
|
|
13235
|
+
return fs8.existsSync(path20) && fs8.statSync(path20).isDirectory();
|
|
12743
13236
|
}
|
|
12744
|
-
exists(
|
|
12745
|
-
return
|
|
13237
|
+
exists(path20) {
|
|
13238
|
+
return fs8.existsSync(path20);
|
|
12746
13239
|
}
|
|
12747
13240
|
};
|
|
12748
13241
|
|
|
@@ -13169,7 +13662,7 @@ var RunnerRegistry = class {
|
|
|
13169
13662
|
return entry?.source === "builtin";
|
|
13170
13663
|
}
|
|
13171
13664
|
installFromPath(sourcePath) {
|
|
13172
|
-
const absPath =
|
|
13665
|
+
const absPath = path17.resolve(sourcePath);
|
|
13173
13666
|
if (!this.store.dirExists(absPath)) {
|
|
13174
13667
|
throw new Error(`Plugin source must be a directory: ${absPath}`);
|
|
13175
13668
|
}
|
|
@@ -13181,7 +13674,7 @@ var RunnerRegistry = class {
|
|
|
13181
13674
|
}
|
|
13182
13675
|
installFromGit(url) {
|
|
13183
13676
|
const validated = assertInstallablePluginUrl(url);
|
|
13184
|
-
const tmpDir =
|
|
13677
|
+
const tmpDir = path17.join(os4.tmpdir(), `ordewell-plugin-${randomBytes3(12).toString("hex")}`);
|
|
13185
13678
|
this.store.ensureDir(tmpDir);
|
|
13186
13679
|
try {
|
|
13187
13680
|
try {
|
|
@@ -13194,7 +13687,7 @@ var RunnerRegistry = class {
|
|
|
13194
13687
|
const hasManifest = contents.some((f) => f === "manifest.json");
|
|
13195
13688
|
let sourceDir = tmpDir;
|
|
13196
13689
|
if (!hasManifest) {
|
|
13197
|
-
const onlyEntry = contents.length === 1 ?
|
|
13690
|
+
const onlyEntry = contents.length === 1 ? path17.join(tmpDir, contents[0]) : void 0;
|
|
13198
13691
|
if (!onlyEntry || !this.store.dirExists(onlyEntry)) {
|
|
13199
13692
|
throw new Error(`No manifest.json found in repository: ${validated.toString()}`);
|
|
13200
13693
|
}
|
|
@@ -13242,7 +13735,7 @@ var RunnerRegistry = class {
|
|
|
13242
13735
|
}
|
|
13243
13736
|
createSkeleton(name, outputDir) {
|
|
13244
13737
|
assertPlainPluginName(name);
|
|
13245
|
-
const dir =
|
|
13738
|
+
const dir = path17.resolve(outputDir, name);
|
|
13246
13739
|
this.store.ensureDir(dir);
|
|
13247
13740
|
const manifest = {
|
|
13248
13741
|
name,
|
|
@@ -13274,8 +13767,8 @@ var RunnerRegistry = class {
|
|
|
13274
13767
|
]
|
|
13275
13768
|
}
|
|
13276
13769
|
};
|
|
13277
|
-
this.store.writeFile(
|
|
13278
|
-
const runSh =
|
|
13770
|
+
this.store.writeFile(path17.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
13771
|
+
const runSh = path17.join(dir, "run.sh");
|
|
13279
13772
|
const scriptContent = `#!/usr/bin/env bash
|
|
13280
13773
|
# Custom runner script for ${name}
|
|
13281
13774
|
# Receives:
|
|
@@ -13301,30 +13794,30 @@ echo "Running ${name} with prompt: $prompt"
|
|
|
13301
13794
|
};
|
|
13302
13795
|
function fsChmodSync(fpath, mode) {
|
|
13303
13796
|
try {
|
|
13304
|
-
|
|
13797
|
+
fs9.chmodSync(fpath, mode);
|
|
13305
13798
|
} catch {
|
|
13306
13799
|
}
|
|
13307
13800
|
}
|
|
13308
13801
|
|
|
13309
13802
|
// src/utils/stateStore.ts
|
|
13310
|
-
import * as
|
|
13311
|
-
import * as
|
|
13803
|
+
import * as fs10 from "fs";
|
|
13804
|
+
import * as path18 from "path";
|
|
13312
13805
|
var STATE_FILE = "state.json";
|
|
13313
13806
|
function getStatePath(baseDir) {
|
|
13314
|
-
return
|
|
13807
|
+
return path18.join(getStateDir(baseDir), STATE_FILE);
|
|
13315
13808
|
}
|
|
13316
13809
|
function saveState(plan, baseDir) {
|
|
13317
13810
|
const statePath = getStatePath(baseDir ?? process.cwd());
|
|
13318
|
-
ensureDir(
|
|
13811
|
+
ensureDir(path18.dirname(statePath));
|
|
13319
13812
|
plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
13320
|
-
|
|
13813
|
+
fs10.writeFileSync(statePath, JSON.stringify(plan, null, 2));
|
|
13321
13814
|
}
|
|
13322
13815
|
function loadState(baseDir, logger = defaultLogger) {
|
|
13323
13816
|
const dir = baseDir ?? process.cwd();
|
|
13324
13817
|
const statePath = getStatePath(dir);
|
|
13325
|
-
if (!
|
|
13818
|
+
if (!fs10.existsSync(statePath)) return null;
|
|
13326
13819
|
try {
|
|
13327
|
-
const raw =
|
|
13820
|
+
const raw = fs10.readFileSync(statePath, "utf-8");
|
|
13328
13821
|
const parsed = JSON.parse(raw);
|
|
13329
13822
|
if (!Array.isArray(parsed.runners)) {
|
|
13330
13823
|
logger.warn("stateStore", `rejecting state from ${statePath}: old format (scalar "runner" instead of "runners" array). Please re-plan.`);
|
|
@@ -13339,29 +13832,24 @@ function loadState(baseDir, logger = defaultLogger) {
|
|
|
13339
13832
|
function clearState(baseDir) {
|
|
13340
13833
|
const dir = baseDir ?? process.cwd();
|
|
13341
13834
|
const statePath = getStatePath(dir);
|
|
13342
|
-
if (
|
|
13835
|
+
if (fs10.existsSync(statePath)) fs10.unlinkSync(statePath);
|
|
13343
13836
|
}
|
|
13344
13837
|
function stateExists(baseDir) {
|
|
13345
13838
|
const dir = baseDir ?? process.cwd();
|
|
13346
|
-
return
|
|
13839
|
+
return fs10.existsSync(getStatePath(dir));
|
|
13347
13840
|
}
|
|
13348
13841
|
|
|
13349
13842
|
// src/services/SettingsService.ts
|
|
13350
|
-
import * as
|
|
13351
|
-
import * as
|
|
13352
|
-
import * as os6 from "os";
|
|
13843
|
+
import * as fs11 from "fs";
|
|
13844
|
+
import * as path19 from "path";
|
|
13353
13845
|
var DEFAULTS = {
|
|
13354
|
-
grillMe: { enabled: false },
|
|
13355
13846
|
tdd: { enabled: true },
|
|
13356
|
-
|
|
13357
|
-
review: { enabled: false },
|
|
13358
|
-
verification: { enabled: false },
|
|
13359
|
-
researchSubagents: { enabled: false }
|
|
13847
|
+
verification: { enabled: false }
|
|
13360
13848
|
};
|
|
13361
13849
|
function getSettingsPath() {
|
|
13362
13850
|
const override = process.env.ORDEWELL_SETTINGS_PATH;
|
|
13363
13851
|
if (override && override.trim()) return override.trim();
|
|
13364
|
-
return
|
|
13852
|
+
return path19.join(globalDataDir(), "settings.json");
|
|
13365
13853
|
}
|
|
13366
13854
|
function readPlannerModels(raw) {
|
|
13367
13855
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
@@ -13394,43 +13882,19 @@ var SettingsService = class {
|
|
|
13394
13882
|
}
|
|
13395
13883
|
fileMtimeMs() {
|
|
13396
13884
|
try {
|
|
13397
|
-
return
|
|
13885
|
+
return fs11.statSync(this.filePath).mtimeMs;
|
|
13398
13886
|
} catch {
|
|
13399
13887
|
return null;
|
|
13400
13888
|
}
|
|
13401
13889
|
}
|
|
13402
|
-
getGrillMe() {
|
|
13403
|
-
return this.getAll().grillMe.enabled;
|
|
13404
|
-
}
|
|
13405
13890
|
getTdd() {
|
|
13406
13891
|
return this.getAll().tdd.enabled;
|
|
13407
13892
|
}
|
|
13408
|
-
getPrd() {
|
|
13409
|
-
return this.getAll().prd.enabled;
|
|
13410
|
-
}
|
|
13411
|
-
setGrillMe(enabled) {
|
|
13412
|
-
this.getAll();
|
|
13413
|
-
this.cache.grillMe.enabled = enabled;
|
|
13414
|
-
this.persist();
|
|
13415
|
-
}
|
|
13416
13893
|
setTdd(enabled) {
|
|
13417
13894
|
this.getAll();
|
|
13418
13895
|
this.cache.tdd.enabled = enabled;
|
|
13419
13896
|
this.persist();
|
|
13420
13897
|
}
|
|
13421
|
-
setPrd(enabled) {
|
|
13422
|
-
this.getAll();
|
|
13423
|
-
this.cache.prd.enabled = enabled;
|
|
13424
|
-
this.persist();
|
|
13425
|
-
}
|
|
13426
|
-
getReview() {
|
|
13427
|
-
return this.getAll().review.enabled;
|
|
13428
|
-
}
|
|
13429
|
-
setReview(enabled) {
|
|
13430
|
-
this.getAll();
|
|
13431
|
-
this.cache.review.enabled = enabled;
|
|
13432
|
-
this.persist();
|
|
13433
|
-
}
|
|
13434
13898
|
getVerification() {
|
|
13435
13899
|
return this.getAll().verification.enabled;
|
|
13436
13900
|
}
|
|
@@ -13439,14 +13903,6 @@ var SettingsService = class {
|
|
|
13439
13903
|
this.cache.verification.enabled = enabled;
|
|
13440
13904
|
this.persist();
|
|
13441
13905
|
}
|
|
13442
|
-
getResearchSubagents() {
|
|
13443
|
-
return this.getAll().researchSubagents.enabled;
|
|
13444
|
-
}
|
|
13445
|
-
setResearchSubagents(enabled) {
|
|
13446
|
-
this.getAll();
|
|
13447
|
-
this.cache.researchSubagents.enabled = enabled;
|
|
13448
|
-
this.persist();
|
|
13449
|
-
}
|
|
13450
13906
|
getModelAllowlist(runner) {
|
|
13451
13907
|
return this.getAll().modelAllowlist?.[runner];
|
|
13452
13908
|
}
|
|
@@ -13496,18 +13952,13 @@ var SettingsService = class {
|
|
|
13496
13952
|
this.persist();
|
|
13497
13953
|
}
|
|
13498
13954
|
load() {
|
|
13955
|
+
migrateOldConfigDir();
|
|
13499
13956
|
try {
|
|
13500
|
-
if (
|
|
13501
|
-
const raw = JSON.parse(
|
|
13957
|
+
if (fs11.existsSync(this.filePath)) {
|
|
13958
|
+
const raw = JSON.parse(fs11.readFileSync(this.filePath, "utf-8"));
|
|
13502
13959
|
const settings = {
|
|
13503
|
-
grillMe: { enabled: raw.grillMe?.enabled ?? DEFAULTS.grillMe.enabled },
|
|
13504
13960
|
tdd: { enabled: raw.tdd?.enabled ?? DEFAULTS.tdd.enabled },
|
|
13505
|
-
|
|
13506
|
-
// `verify` is the legacy key for review mode; keep reading it so old settings files survive the rename.
|
|
13507
|
-
// The evidence-based verification mode uses the distinct `verification` key to avoid colliding with it.
|
|
13508
|
-
review: { enabled: raw.review?.enabled ?? raw.verify?.enabled ?? DEFAULTS.review.enabled },
|
|
13509
|
-
verification: { enabled: raw.verification?.enabled ?? DEFAULTS.verification.enabled },
|
|
13510
|
-
researchSubagents: { enabled: raw.researchSubagents?.enabled ?? DEFAULTS.researchSubagents.enabled }
|
|
13961
|
+
verification: { enabled: raw.verification?.enabled ?? DEFAULTS.verification.enabled }
|
|
13511
13962
|
};
|
|
13512
13963
|
if (raw.modelAllowlist !== void 0) {
|
|
13513
13964
|
settings.modelAllowlist = raw.modelAllowlist;
|
|
@@ -13523,14 +13974,14 @@ var SettingsService = class {
|
|
|
13523
13974
|
}
|
|
13524
13975
|
} catch {
|
|
13525
13976
|
}
|
|
13526
|
-
return { ...DEFAULTS,
|
|
13977
|
+
return { ...DEFAULTS, tdd: { ...DEFAULTS.tdd }, verification: { ...DEFAULTS.verification } };
|
|
13527
13978
|
}
|
|
13528
13979
|
persist() {
|
|
13529
|
-
const dir =
|
|
13530
|
-
if (!
|
|
13531
|
-
|
|
13980
|
+
const dir = path19.dirname(this.filePath);
|
|
13981
|
+
if (!fs11.existsSync(dir)) {
|
|
13982
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
13532
13983
|
}
|
|
13533
|
-
|
|
13984
|
+
fs11.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
|
|
13534
13985
|
this.cachedMtimeMs = this.fileMtimeMs();
|
|
13535
13986
|
}
|
|
13536
13987
|
};
|
|
@@ -13576,6 +14027,7 @@ export {
|
|
|
13576
14027
|
AbstractRunner,
|
|
13577
14028
|
AbstractTerminalSession,
|
|
13578
14029
|
ApprovalPolicy,
|
|
14030
|
+
BUILTIN_SKILL_NAMES,
|
|
13579
14031
|
BaseAiService,
|
|
13580
14032
|
BaseConfig,
|
|
13581
14033
|
BaseFileSystem,
|
|
@@ -13634,8 +14086,14 @@ export {
|
|
|
13634
14086
|
SYMBOL_LANGUAGES,
|
|
13635
14087
|
Session,
|
|
13636
14088
|
SettingsService,
|
|
14089
|
+
SkillsService,
|
|
13637
14090
|
StdioAgentAdapter,
|
|
13638
14091
|
TASK_OPS_ENVELOPE_KEY,
|
|
14092
|
+
TASK_QUERY_ANSWER_OR_OPS,
|
|
14093
|
+
TASK_QUERY_ENVELOPE_KEY,
|
|
14094
|
+
TASK_QUERY_FIELDS,
|
|
14095
|
+
TASK_QUERY_PROTOCOL,
|
|
14096
|
+
TASK_QUERY_REMINDER,
|
|
13639
14097
|
TRUNCATED_PLAN_REPAIR_INSTRUCTION,
|
|
13640
14098
|
TaskOrchestrator,
|
|
13641
14099
|
TmuxRunner,
|
|
@@ -13694,6 +14152,7 @@ export {
|
|
|
13694
14152
|
configuredProviders,
|
|
13695
14153
|
createAiService,
|
|
13696
14154
|
createEmptyPlan,
|
|
14155
|
+
createSkillsService,
|
|
13697
14156
|
createTask,
|
|
13698
14157
|
daemonTokenPath,
|
|
13699
14158
|
defaultLogger,
|
|
@@ -13720,12 +14179,14 @@ export {
|
|
|
13720
14179
|
filterModelsForPrompt,
|
|
13721
14180
|
filteredBuildModes,
|
|
13722
14181
|
flattenTasks,
|
|
14182
|
+
flattenTasksWithParents,
|
|
13723
14183
|
formatSearchOutput,
|
|
13724
14184
|
generatePlanWithRepair,
|
|
13725
14185
|
getLatestSession,
|
|
13726
14186
|
getProviderMeta,
|
|
13727
14187
|
getSettingsPath,
|
|
13728
14188
|
getStateDir,
|
|
14189
|
+
globalDataDir,
|
|
13729
14190
|
grantScopeFor,
|
|
13730
14191
|
hasTmux,
|
|
13731
14192
|
includeGlobFor,
|
|
@@ -13745,6 +14206,7 @@ export {
|
|
|
13745
14206
|
looksLikePlanAttempt,
|
|
13746
14207
|
mapAgentTool,
|
|
13747
14208
|
migrateLegacyPlan,
|
|
14209
|
+
migrateOldConfigDir,
|
|
13748
14210
|
migratePlanState,
|
|
13749
14211
|
migrateTask,
|
|
13750
14212
|
mintDaemonToken,
|
|
@@ -13756,6 +14218,7 @@ export {
|
|
|
13756
14218
|
parsePartialPlan,
|
|
13757
14219
|
parsePlanJson,
|
|
13758
14220
|
parseTaskOpsJson,
|
|
14221
|
+
parseTaskQueryJson,
|
|
13759
14222
|
pendingEditRulesBlock,
|
|
13760
14223
|
planDirectLaunch,
|
|
13761
14224
|
planShellLaunch,
|
|
@@ -13764,11 +14227,13 @@ export {
|
|
|
13764
14227
|
providerForRunner,
|
|
13765
14228
|
reEmitPlanPrompt,
|
|
13766
14229
|
reEmitTaskOpsPrompt,
|
|
14230
|
+
reEmitTaskQueryPrompt,
|
|
13767
14231
|
readDaemonToken,
|
|
13768
14232
|
referencePattern,
|
|
13769
14233
|
removeTaskFromPlan,
|
|
13770
14234
|
renderPlanMap,
|
|
13771
14235
|
renderPriorOutputs,
|
|
14236
|
+
renderTaskQueryAnswer,
|
|
13772
14237
|
renumberTasks,
|
|
13773
14238
|
repairLoop,
|
|
13774
14239
|
researchShellWarning,
|
|
@@ -13776,10 +14241,12 @@ export {
|
|
|
13776
14241
|
resolveArgs,
|
|
13777
14242
|
resolveDefaultMode,
|
|
13778
14243
|
resolveModelShortcut,
|
|
14244
|
+
resolveOrderLabel,
|
|
13779
14245
|
resolvePluginInstallDir,
|
|
13780
14246
|
resolveProvider,
|
|
13781
14247
|
resolveProviderFromPrefix,
|
|
13782
14248
|
resolveResearchShell,
|
|
14249
|
+
resolveSkillInvocation,
|
|
13783
14250
|
resolveTaskMode,
|
|
13784
14251
|
resolveWithin,
|
|
13785
14252
|
retargetTaskRunner,
|
|
@@ -13802,7 +14269,10 @@ export {
|
|
|
13802
14269
|
summarizeOutput,
|
|
13803
14270
|
summarizeToolCall,
|
|
13804
14271
|
taskOpsRejectedPrompt,
|
|
14272
|
+
taskOrderLabel,
|
|
14273
|
+
taskQuerySignature,
|
|
13805
14274
|
textHasTaskOps,
|
|
14275
|
+
textHasTaskQuery,
|
|
13806
14276
|
tmuxSessionName,
|
|
13807
14277
|
tmuxSocketName,
|
|
13808
14278
|
tmuxWindowName,
|