@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.
Files changed (46) hide show
  1. package/dist/{ITerminalRunner-CbE6gKLI.d.ts → ITerminalRunner-BTaKZ659.d.mts} +2 -2
  2. package/dist/{ITerminalRunner-1NAv8wCC.d.mts → ITerminalRunner-DVwORcV9.d.ts} +2 -2
  3. package/dist/ModeResolver-CCKYqdZC.d.mts +44 -0
  4. package/dist/ModeResolver-CzZd-i6T.d.ts +44 -0
  5. package/dist/{Task-1NwmUImI.d.mts → Task-cGrfYRXK.d.mts} +8 -1
  6. package/dist/{Task-1NwmUImI.d.ts → Task-cGrfYRXK.d.ts} +8 -1
  7. package/dist/chunk-DNNZGH3I.mjs +862 -0
  8. package/dist/chunk-DNNZGH3I.mjs.map +1 -0
  9. package/dist/chunk-HPNUFBIT.mjs +19 -0
  10. package/dist/chunk-HPNUFBIT.mjs.map +1 -0
  11. package/dist/{chunk-SRGQBGI5.mjs → chunk-N6HTBUKG.mjs} +13 -1
  12. package/dist/{chunk-SRGQBGI5.mjs.map → chunk-N6HTBUKG.mjs.map} +1 -1
  13. package/dist/{chunk-YXAIQKVE.mjs → chunk-UEJ3SZQ7.mjs} +2 -2
  14. package/dist/index.d.mts +284 -80
  15. package/dist/index.d.ts +284 -80
  16. package/dist/index.js +1670 -820
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +1067 -597
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/order-labels.d.mts +17 -0
  21. package/dist/order-labels.d.ts +17 -0
  22. package/dist/order-labels.js +44 -0
  23. package/dist/order-labels.js.map +1 -0
  24. package/dist/order-labels.mjs +9 -0
  25. package/dist/order-labels.mjs.map +1 -0
  26. package/dist/{parsing-RjN9JbyG.d.ts → parsing-BT-dvFcu.d.mts} +5 -43
  27. package/dist/{parsing-xpozY5ac.d.mts → parsing-D87zKtTe.d.ts} +5 -43
  28. package/dist/parsing.d.mts +3 -2
  29. package/dist/parsing.d.ts +3 -2
  30. package/dist/parsing.js.map +1 -1
  31. package/dist/parsing.mjs +3 -2
  32. package/dist/{plan-utils-nfHKSbd3.d.mts → plan-utils-BHLbBym3.d.mts} +27 -5
  33. package/dist/{plan-utils-Cgc0cCH1.d.ts → plan-utils-akoB6YCL.d.ts} +27 -5
  34. package/dist/plan-utils.d.mts +3 -2
  35. package/dist/plan-utils.d.ts +3 -2
  36. package/dist/plan-utils.js +1 -9
  37. package/dist/plan-utils.js.map +1 -1
  38. package/dist/plan-utils.mjs +3 -2
  39. package/dist/testing.d.mts +2 -2
  40. package/dist/testing.d.ts +2 -2
  41. package/package.json +12 -2
  42. package/skills/grilling/SKILL.md +31 -0
  43. package/skills/to-spec/SKILL.md +64 -0
  44. package/dist/chunk-4KIWPW4K.mjs +0 -510
  45. package/dist/chunk-4KIWPW4K.mjs.map +0 -1
  46. /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-YXAIQKVE.mjs";
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
- } from "./chunk-4KIWPW4K.mjs";
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-SRGQBGI5.mjs";
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, fs10, fetcher, signal) {
2611
- const outcome = await runTool(name, args, fs10, fetcher, signal);
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, fs10, fetcher, signal) {
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 fs10.readFile(String(args.path ?? ""), { offset, limit, maxBytes });
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 fs10.readFiles(paths);
2637
+ return fs12.readFiles(paths);
2626
2638
  }
2627
2639
  case "glob":
2628
- return fs10.glob(String(args.pattern ?? ""), { path: str(args.path), headLimit: num(args.head_limit ?? args.headLimit) });
2640
+ return fs12.glob(String(args.pattern ?? ""), { path: str(args.path), headLimit: num(args.head_limit ?? args.headLimit) });
2629
2641
  case "grep":
2630
- return fs10.grep(String(args.pattern ?? ""), grepOptions(args));
2642
+ return fs12.grep(String(args.pattern ?? ""), grepOptions(args));
2631
2643
  case "find_symbol":
2632
- return fs10.findSymbol(String(args.symbol ?? ""), { language: str(args.language), path: str(args.path) });
2644
+ return fs12.findSymbol(String(args.symbol ?? ""), { language: str(args.language), path: str(args.path) });
2633
2645
  case "list_dir":
2634
- return fs10.listDir(String(args.path ?? ""), num(args.depth));
2646
+ return fs12.listDir(String(args.path ?? ""), num(args.depth));
2635
2647
  case "bash":
2636
- return fs10.bash(String(args.command ?? ""), signal);
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
- grillMe: false,
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
- grillMe: settings.grillMeEnabled ?? false,
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(subagentsEnabled = false) {
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(reviewAlsoEnabled = false) {
3992
+ function verificationModeBlock() {
3833
3993
  return [
3834
3994
  "",
3835
3995
  "VERIFICATION MODE:",
3836
- reviewAlsoEnabled ? "Add a FINAL verification task immediately before the review task. The review task keeps the highest order number and must list the verification task among its dependencies." : "Add a FINAL verification task to the end of the plan (highest order number).",
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, grillMeEnabled = false, prdEnabled = false, reviewEnabled = false, verificationEnabled = false, harnessMode = false) {
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 grillMeBlock = grillMeEnabled ? [
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 reviewModeBlock() {
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("{{REVIEW_BLOCK}}", scoped.review ? `
4178
- ${reviewModeBlock()}
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(includeSubagents) {
4529
- return includeSubagents ? [...RESEARCH_TOOLS, SPAWN_RESEARCH_AGENT_SPEC] : RESEARCH_TOOLS;
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(includeSubagents = false) {
4551
- return projectOpenAi(withSpawn(includeSubagents));
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(includeSubagents = false) {
4563
- return withSpawn(includeSubagents).map((t) => {
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(fs10, runners) {
4686
- const collector = new ContextCollector(fs10);
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(fs10, registry) {
4714
- this.fs = fs10;
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(fs10) {
4763
- const withinWorkspace = (p) => resolveWithin(fs10.getWorkspaceRoot(), p ?? ".").inside;
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: () => fs10.getWorkspaceRoot(),
4769
- readFile: (p, opts) => withinWorkspace(p) ? fs10.readFile(p, opts) : Promise.resolve(outsideRefusal(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)) : fs10.readFiles(paths);
4879
+ return escaping ? Promise.resolve(outsideRefusal(escaping)) : fs12.readFiles(paths);
4773
4880
  },
4774
- glob: (pattern, opts) => withinWorkspace(opts?.path) ? fs10.glob(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
4775
- grep: (pattern, opts) => withinWorkspace(opts?.path) ? fs10.grep(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
4776
- findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs10.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
4777
- listDir: (p, depth) => withinWorkspace(p) ? fs10.listDir(p, depth) : Promise.resolve(outsideRefusal(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 fs10.bash(command, signal);
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 fs10 = nonPromptingFs(deps.fs);
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, fs10, void 0, deps.signal);
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, fs10, onProgress, signal, subagentId) {
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: fs10,
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(fs10, runners) {
4947
- return collectResearchContext(fs10, runners);
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, fs10, onProgress, fetcher, stepIndex, thinking, signal) {
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 }, fs10, onProgress, signal, subagentId));
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], fs10, fetcher, signal));
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, fs10, fetcher, signal);
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, fs10, onProgress, runners, initialLog, fetcher, userGoal, runnerModes, autonomousDefault, signal) {
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
- fs10,
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, fs10, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
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(fs10, runners);
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, fs10, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
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, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
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 (fs10) {
5459
- const collected = await new ContextCollector(fs10).collect(runners[0] ?? "claude-code");
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, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
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 (fs10) {
5480
- const collected = await new ContextCollector(fs10).collect(existingPlan.runners[0] ?? "claude-code");
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(this.researchSubagentsEnabled)}` }
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(this.researchSubagentsEnabled),
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, fs10, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
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, researchSubagents: researchSubagentsEnabled } = modes;
5724
- this.researchSubagentsEnabled = researchSubagentsEnabled;
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(researchSubagentsEnabled)}
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(researchSubagentsEnabled),
5832
+ toOpenAiTools(),
5741
5833
  (delta) => onProgress({ type: "thinking", text: delta })
5742
5834
  );
5743
- const result = await this.runResearchLoop(researchChat, firstMessage, fs10, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
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, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
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 (fs10) {
5753
- const collected = await new ContextCollector(fs10).collect(runners[0] ?? "claude-code");
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, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
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 (fs10) {
5774
- const collected = await new ContextCollector(fs10).collect(existingPlan.runners[0] ?? "claude-code");
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(allTasks, currentTaskId, opts) {
5864
- if (allTasks.length < PLAN_MAP_MIN_TASKS) return "";
5865
- const sorted = [...allTasks].sort((a, b) => a.order - b.order);
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((t) => t.id === currentTaskId);
5967
+ const currentIdx = sorted.findIndex((r) => r.task.id === currentTaskId);
5868
5968
  const { window, omitted } = pickWindow(sorted, currentIdx, max);
5869
- const currentTask = sorted.find((t) => t.id === currentTaskId);
5870
- const widthOrder = Math.max(2, String(sorted[sorted.length - 1]?.order ?? sorted.length).length);
5871
- const lines = window.map((t) => {
5872
- const isCurrent = t.id === currentTaskId;
5873
- const tag = planMapStatus(t, isCurrent);
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 = t.assignedRunner ? ` (${t.assignedRunner})` : "";
5877
- return `${order}. [${tag.padEnd(7, " ")}] ${t.title}${runner}${arrow}`;
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 outputs = collectDirectDependencyOutputs(task, allTasks);
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
- for (const t of remaining) {
7062
- console.log(` - ${t.order}. ${t.title} [${t.type}/${t.status}] prompt=${!!t.prompt}`);
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.allTasks, {
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 path5 from "path";
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
- path5.join(home, ".local", "bin"),
7380
- path5.join(home, ".opencode", "bin"),
7381
- path5.join(home, ".claude", "bin"),
7382
- path5.join(home, ".bun", "bin"),
7383
- path5.join(home, ".volta", "bin"),
7384
- path5.join(home, ".npm-global", "bin"),
7385
- path5.join(home, "bin"),
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 join16 = (...parts) => path5.win32.join(...parts);
7392
- const under = (base, ...parts) => base ? [join16(base, ...parts)] : [];
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
- join16(home, ".local", "bin"),
7399
- join16(home, ".opencode", "bin"),
7400
- join16(home, ".claude", "bin"),
7401
- join16(home, ".codex", "bin"),
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
- join16(home, "AppData", "Roaming", "npm"),
7454
+ join19(home, "AppData", "Roaming", "npm"),
7408
7455
  ...under(localAppData, "pnpm"),
7409
7456
  ...under(localAppData, "Yarn", "bin"),
7410
- join16(home, ".bun", "bin"),
7457
+ join19(home, ".bun", "bin"),
7411
7458
  // Windows package managers.
7412
- join16(home, "scoop", "shims"),
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
- join16(home, ".volta", "bin")
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(path5.delimiter)) {
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(path5.delimiter);
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 = path7.join(osMod.homedir(), ".config", "ordewell");
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 = path8.join(os3.homedir(), ".config", "ordewell");
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 = path8.join(os3.homedir(), ".config", "ordewell");
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 = path8.join(os3.homedir(), ".config", "ordewell");
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 between turns — in practice the startup warnings
8944
- * that arrive during the handshake. Dropping them hid the one diagnostic
8945
- * that explains a planner which cannot read the workspace, so they are held
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) emit({ type: "assistant_text", text: block.text });
9161
- else if (block.type === "thinking" && block.thinking) emit({ type: "thinking", text: block.thinking });
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: flattenContent(block.content),
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: part.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, path17, body, signal) {
9818
- const response = await this.deps.fetch(`${this.baseUrl}${path17}`, {
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} ${path17} failed: ${response.status} ${response.statusText}`);
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 three policies layered on top: the PRD gate,
10027
- * the empty-reply nudge, and a bounded corrective re-emit for botched JSON.
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 (extractPrdBlock(turn.text)) conversation.prdCaptured = true;
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, fs10, onProgress, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
10268
- const contextStr = await collectResearchContext(fs10, runners);
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 fs5 from "fs";
10639
- import * as path11 from "path";
10944
+ import * as fs6 from "fs";
10945
+ import * as path13 from "path";
10640
10946
 
10641
10947
  // src/utils/fsHelpers.ts
10642
- import * as fs4 from "fs";
10643
- import * as path10 from "path";
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 (!fs4.existsSync(dir)) {
10647
- fs4.mkdirSync(dir, { recursive: true });
10952
+ if (!fs5.existsSync(dir)) {
10953
+ fs5.mkdirSync(dir, { recursive: true });
10648
10954
  }
10649
10955
  }
10650
10956
  function getStateDir(baseDir) {
10651
- return path10.join(baseDir ?? process.cwd(), STATE_DIR);
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
- fs4.writeFileSync(path10.join(dir, IGNORE_FILE), "*\n", { flag: "wx" });
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 path11.join(getStateDir(baseDir), SESSIONS_SUBDIR);
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 = path11.join(sessionsDir, filename);
10709
- fs5.writeFileSync(filePath, JSON.stringify(session, null, 2));
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 (!fs5.existsSync(sessionsDir)) return [];
10715
- const files = fs5.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
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 = fs5.readFileSync(path11.join(sessionsDir, f), "utf-8");
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 = fs5.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
11039
+ const files = fs6.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
10734
11040
  for (const f of files) {
10735
11041
  try {
10736
- const raw = fs5.readFileSync(path11.join(sessionsDir, f), "utf-8");
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 (!fs5.existsSync(sessionsDir)) return null;
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 = fs5.readFileSync(path11.join(sessionsDir, file), "utf-8");
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 (!fs5.existsSync(sessionsDir)) return false;
11087
+ if (!fs6.existsSync(sessionsDir)) return false;
10782
11088
  try {
10783
11089
  const file = findSessionFile(sessionsDir, sessionId, logger);
10784
11090
  if (file) {
10785
- fs5.unlinkSync(path11.join(sessionsDir, file));
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 { grillMeEnabled, prdEnabled, reviewEnabled, verificationEnabled, researchSubagentsEnabled, modelAllowlist } = this.settingsFn();
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 — grill-me
11205
- * answers, PRD accept/adjust, outline confirm. One branch, no phase ladder.
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.appendTranscript("user", userMessage, now);
11212
- this.plan.researchLog = [...this.plan.researchLog ?? [], { id: `up-${Date.now()}`, type: "user_prompt", content: userMessage, timestamp: now }];
11213
- const contextBlock = this.planContextBlock();
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
- ${userMessage}` : userMessage;
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.aiService.continueConversation(
11261
- corrective,
11262
- (p) => this.translateProgress(p),
11263
- options?.signal
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
- (t) => `#${t.order} id=${t.id} "${t.title}" [${t.status}] runner:${t.assignedRunner}${t.assignedModel ? ` model:${t.assignedModel.modelId}` : ""} deps:[${t.dependencies.map((d) => orderOf.get(d) ?? d).join(", ")}]`
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 modelLines = [];
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
- modelBlock,
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
- ' {"op":"update","taskId":"<id or #order>","changes":{"title"?,"description"?,"prompt"?,"dependencies"?:["<id or #order>"],"assignedRunner"?,"assignedModel"?,"taskMode"?}},',
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
- "Keep dependencies consistent: no cycles, no references to removed tasks, dependencies must come before dependents, and never touch running or completed tasks." + execNote
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 { grillMeEnabled, prdEnabled, reviewEnabled, verificationEnabled, researchSubagentsEnabled, modelAllowlist } = this.settingsFn();
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 is the one patch that
11597
- * can leave the plan unschedulable, so it goes through the same
11598
- * {@link canSetDependencies} guard the planner's pickers use rather than a
11599
- * second copy of the rule — and throws, so the surface can say why.
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 = canSetDependencies(this.store.planTasks, taskId, changes.dependencies);
11604
- if (!check.ok) throw new PlanEditError(check.error ?? "Those dependencies are not valid");
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 join10 } from "path";
12245
- import { existsSync as existsSync6, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
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 = existsSync6(this.logPath) ? readFileSync4(this.logPath, "utf8") : "";
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
- join10(this.logDir, `${id}.log`),
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 mkdirSync5, openSync, readFileSync as readFileSync5, unlinkSync as unlinkSync3, writeSync } from "fs";
12542
- import { homedir as homedir5 } from "os";
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 join11(homedir5(), ".config", "ordewell");
13042
+ return globalDataDir();
12549
13043
  }
12550
13044
  function daemonTokenPath(port) {
12551
- return join11(configDir(), `server-${port}.token`);
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
- mkdirSync5(configDir(), { recursive: true });
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 = readFileSync5(daemonTokenPath(port), "utf8").trim();
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 fs7 from "fs";
12613
- import * as path14 from "path";
12614
- import * as os5 from "os";
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 fs6 from "fs";
12620
- import * as path13 from "path";
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 path12 from "path";
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 = path12.resolve(pluginsDir);
12640
- const dest = path12.resolve(root, name);
12641
- const relative3 = path12.relative(root, dest);
12642
- if (relative3 !== name || relative3 === "" || path12.isAbsolute(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 path13.join(os4.homedir(), ".config", "ordewell", "plugins");
13150
+ return path16.join(globalDataDir(), "plugins");
12658
13151
  }
12659
13152
  function loadManifestFromDir(dir) {
12660
- const manifestPath = path13.join(dir, "manifest.json");
12661
- if (!fs6.existsSync(manifestPath)) return null;
13153
+ const manifestPath = path16.join(dir, "manifest.json");
13154
+ if (!fs8.existsSync(manifestPath)) return null;
12662
13155
  try {
12663
- const raw = fs6.readFileSync(manifestPath, "utf8");
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 copyDirSync(src, dest) {
13164
+ function copyDirSync3(src, dest) {
12672
13165
  ensureDir(dest);
12673
- const entries = fs6.readdirSync(src, { withFileTypes: true });
13166
+ const entries = fs8.readdirSync(src, { withFileTypes: true });
12674
13167
  for (const entry of entries) {
12675
- const srcPath = path13.join(src, entry.name);
12676
- const destPath = path13.join(dest, entry.name);
13168
+ const srcPath = path16.join(src, entry.name);
13169
+ const destPath = path16.join(dest, entry.name);
12677
13170
  if (entry.isDirectory()) {
12678
- copyDirSync(srcPath, destPath);
13171
+ copyDirSync3(srcPath, destPath);
12679
13172
  } else {
12680
- fs6.copyFileSync(srcPath, destPath);
13173
+ fs8.copyFileSync(srcPath, destPath);
12681
13174
  }
12682
13175
  }
12683
13176
  }
12684
13177
  function rmDirSync(dir) {
12685
- if (!fs6.existsSync(dir)) return;
12686
- const entries = fs6.readdirSync(dir, { withFileTypes: true });
13178
+ if (!fs8.existsSync(dir)) return;
13179
+ const entries = fs8.readdirSync(dir, { withFileTypes: true });
12687
13180
  for (const entry of entries) {
12688
- const p = path13.join(dir, entry.name);
13181
+ const p = path16.join(dir, entry.name);
12689
13182
  if (entry.isDirectory()) {
12690
13183
  rmDirSync(p);
12691
13184
  } else {
12692
- fs6.unlinkSync(p);
13185
+ fs8.unlinkSync(p);
12693
13186
  }
12694
13187
  }
12695
- fs6.rmdirSync(dir);
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 (!fs6.existsSync(dir)) return [];
13196
+ if (!fs8.existsSync(dir)) return [];
12704
13197
  let entries;
12705
13198
  try {
12706
- entries = fs6.readdirSync(dir, { withFileTypes: true });
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 fs6.readdirSync(dir);
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
- copyDirSync(sourceDir, destDir);
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
- fs6.writeFileSync(filePath, content);
13225
+ fs8.writeFileSync(filePath, content);
12733
13226
  }
12734
13227
  readFile(filePath) {
12735
13228
  try {
12736
- return fs6.readFileSync(filePath, "utf8");
13229
+ return fs8.readFileSync(filePath, "utf8");
12737
13230
  } catch {
12738
13231
  return null;
12739
13232
  }
12740
13233
  }
12741
- dirExists(path17) {
12742
- return fs6.existsSync(path17) && fs6.statSync(path17).isDirectory();
13234
+ dirExists(path20) {
13235
+ return fs8.existsSync(path20) && fs8.statSync(path20).isDirectory();
12743
13236
  }
12744
- exists(path17) {
12745
- return fs6.existsSync(path17);
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 = path14.resolve(sourcePath);
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 = path14.join(os5.tmpdir(), `ordewell-plugin-${randomBytes3(12).toString("hex")}`);
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 ? path14.join(tmpDir, contents[0]) : void 0;
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 = path14.resolve(outputDir, name);
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(path14.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
13278
- const runSh = path14.join(dir, "run.sh");
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
- fs7.chmodSync(fpath, mode);
13797
+ fs9.chmodSync(fpath, mode);
13305
13798
  } catch {
13306
13799
  }
13307
13800
  }
13308
13801
 
13309
13802
  // src/utils/stateStore.ts
13310
- import * as fs8 from "fs";
13311
- import * as path15 from "path";
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 path15.join(getStateDir(baseDir), STATE_FILE);
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(path15.dirname(statePath));
13811
+ ensureDir(path18.dirname(statePath));
13319
13812
  plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
13320
- fs8.writeFileSync(statePath, JSON.stringify(plan, null, 2));
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 (!fs8.existsSync(statePath)) return null;
13818
+ if (!fs10.existsSync(statePath)) return null;
13326
13819
  try {
13327
- const raw = fs8.readFileSync(statePath, "utf-8");
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 (fs8.existsSync(statePath)) fs8.unlinkSync(statePath);
13835
+ if (fs10.existsSync(statePath)) fs10.unlinkSync(statePath);
13343
13836
  }
13344
13837
  function stateExists(baseDir) {
13345
13838
  const dir = baseDir ?? process.cwd();
13346
- return fs8.existsSync(getStatePath(dir));
13839
+ return fs10.existsSync(getStatePath(dir));
13347
13840
  }
13348
13841
 
13349
13842
  // src/services/SettingsService.ts
13350
- import * as fs9 from "fs";
13351
- import * as path16 from "path";
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
- prd: { enabled: false },
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 path16.join(os6.homedir(), ".config", "ordewell", "settings.json");
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 fs9.statSync(this.filePath).mtimeMs;
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 (fs9.existsSync(this.filePath)) {
13501
- const raw = JSON.parse(fs9.readFileSync(this.filePath, "utf-8"));
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
- prd: { enabled: raw.prd?.enabled ?? DEFAULTS.prd.enabled },
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, grillMe: { ...DEFAULTS.grillMe }, tdd: { ...DEFAULTS.tdd }, prd: { ...DEFAULTS.prd }, review: { ...DEFAULTS.review }, verification: { ...DEFAULTS.verification }, researchSubagents: { ...DEFAULTS.researchSubagents } };
13977
+ return { ...DEFAULTS, tdd: { ...DEFAULTS.tdd }, verification: { ...DEFAULTS.verification } };
13527
13978
  }
13528
13979
  persist() {
13529
- const dir = path16.dirname(this.filePath);
13530
- if (!fs9.existsSync(dir)) {
13531
- fs9.mkdirSync(dir, { recursive: true });
13980
+ const dir = path19.dirname(this.filePath);
13981
+ if (!fs11.existsSync(dir)) {
13982
+ fs11.mkdirSync(dir, { recursive: true });
13532
13983
  }
13533
- fs9.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
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,