@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
@@ -1,4 +1,18 @@
1
- import { T as Task, h as RunnerId, f as ResearchStepOutcome } from './Task-1NwmUImI.js';
1
+ import { h as RunnerId, a as DiscoveredModel, T as Task, f as ResearchStepOutcome } from './Task-cGrfYRXK.js';
2
+ import { R as RunnerModeInfo } from './ModeResolver-CzZd-i6T.js';
3
+
4
+ /**
5
+ * The catalog a model/task-mode edit is checked against — the same discovered
6
+ * models and manifest modes the planner was shown in the per-turn catalog
7
+ * block (`Session.catalogBlock`), so a refusal here can never name something
8
+ * as invalid that the planner was never told about, or vice versa.
9
+ */
10
+ interface EditCatalog {
11
+ modelsByRunner: Partial<Record<RunnerId, DiscoveredModel[]>>;
12
+ runnerModes: Partial<Record<RunnerId, RunnerModeInfo[]>>;
13
+ /** Raw (unfiltered) allowlist, keyed by runner — the same shape `coerceAssignments` takes. */
14
+ perRunnerAllowlist?: Partial<Record<RunnerId, string[]>>;
15
+ }
2
16
 
3
17
  /**
4
18
  * Targeted task edits emitted by the planner conversation (the 'task_ops'
@@ -12,6 +26,7 @@ type TaskOp = {
12
26
  } | {
13
27
  op: 'add';
14
28
  task: Partial<Task>;
29
+ handle?: string;
15
30
  } | {
16
31
  op: 'remove';
17
32
  taskId: string;
@@ -22,10 +37,16 @@ type TaskOp = {
22
37
  op: 'merge';
23
38
  taskIds: string[];
24
39
  merged: Partial<Task>;
40
+ handle?: string;
25
41
  } | {
26
42
  op: 'split';
27
43
  taskId: string;
28
44
  parts: Partial<Task>[];
45
+ handle?: string;
46
+ } | {
47
+ op: 'rearm';
48
+ taskId: string;
49
+ changes?: Partial<Task>;
29
50
  };
30
51
  declare function textHasTaskOps(text: string): boolean;
31
52
  /** Parse a `{"taskOps":[...]}` reply. Throws PlanParseError when the JSON is unusable. */
@@ -41,9 +62,10 @@ interface ApplyTaskOpsResult {
41
62
  * Pre-flight check for a merge: would collapsing the selected tasks into one
42
63
  * (rewiring every dependent of any selected task to the survivor) keep the
43
64
  * dependency graph valid? Rejects locked tasks and merges whose rewiring would
44
- * introduce a cycle or an order-violating dependency. Consecutiveness in
45
- * display order is NOT checked here — that is a UI concern; this validates the
46
- * structural compatibility the issue calls out.
65
+ * introduce a cycle. Display order is NOT checked here — neither
66
+ * consecutiveness (a UI concern) nor a dependency the merge pulls out of order,
67
+ * which {@link applyTaskOps} repairs; refusing on order would make this
68
+ * pre-flight stricter than the applier it stands in for.
47
69
  */
48
70
  declare function canMergeTasks(tasks: Task[], selectedIds: string[]): {
49
71
  ok: boolean;
@@ -89,7 +111,7 @@ declare function canSplitTask(tasks: Task[], taskId: string): {
89
111
  * completed tasks untouched), or nothing is returned and `errors` explains
90
112
  * why. The caller commits `tasks` on ok.
91
113
  */
92
- declare function applyTaskOps(currentTasks: Task[], ops: TaskOp[], runners: RunnerId[]): ApplyTaskOpsResult;
114
+ declare function applyTaskOps(currentTasks: Task[], ops: TaskOp[], runners: RunnerId[], catalog?: EditCatalog): ApplyTaskOpsResult;
93
115
 
94
116
  /**
95
117
  * @param toolLabel The agent's own name for the tool, when a harness planner
@@ -1,2 +1,3 @@
1
- export { a as TaskRef, c as canMergeTasks, d as canSetDependencies, e as canSplitTask, g as dependencyCandidates, h as dependentsOf, s as summarizeToolCall } from './plan-utils-nfHKSbd3.mjs';
2
- import './Task-1NwmUImI.mjs';
1
+ export { a as TaskRef, c as canMergeTasks, d as canSetDependencies, e as canSplitTask, g as dependencyCandidates, h as dependentsOf, s as summarizeToolCall } from './plan-utils-BHLbBym3.mjs';
2
+ import './Task-cGrfYRXK.mjs';
3
+ import './ModeResolver-CCKYqdZC.mjs';
@@ -1,2 +1,3 @@
1
- export { a as TaskRef, c as canMergeTasks, d as canSetDependencies, e as canSplitTask, g as dependencyCandidates, h as dependentsOf, s as summarizeToolCall } from './plan-utils-Cgc0cCH1.js';
2
- import './Task-1NwmUImI.js';
1
+ export { a as TaskRef, c as canMergeTasks, d as canSetDependencies, e as canSplitTask, g as dependencyCandidates, h as dependentsOf, s as summarizeToolCall } from './plan-utils-akoB6YCL.js';
2
+ import './Task-cGrfYRXK.js';
3
+ import './ModeResolver-CzZd-i6T.js';
@@ -30,6 +30,7 @@ __export(plan_utils_exports, {
30
30
  module.exports = __toCommonJS(plan_utils_exports);
31
31
 
32
32
  // src/services/TaskOps.ts
33
+ var import_uuid = require("uuid");
33
34
  function detectCycle(tasks) {
34
35
  const byId = new Map(tasks.map((t) => [t.id, t]));
35
36
  const visiting = /* @__PURE__ */ new Set();
@@ -68,15 +69,6 @@ function canMergeTasks(tasks, selectedIds) {
68
69
  sim.push({ ...ordered[0], id: survivorId, title: "__merge_sim__", order: ordered[0].order, dependencies: unionDeps });
69
70
  const cycle = detectCycle(sim);
70
71
  if (cycle) return { ok: false, error: `Merging would create a dependency cycle involving "${cycle}"` };
71
- const orderById = new Map(sim.map((t) => [t.id, t.order]));
72
- for (const t of sim) {
73
- for (const dep of t.dependencies) {
74
- const depOrder = orderById.get(dep);
75
- if (depOrder !== void 0 && depOrder >= t.order) {
76
- return { ok: false, error: "Merging would break task ordering \u2014 a dependency of the merged set comes after it" };
77
- }
78
- }
79
- }
80
72
  return { ok: true };
81
73
  }
82
74
  function dependentsOf(tasks, taskId) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/plan-utils.ts","../src/services/TaskOps.ts","../src/services/researchStepSummary.ts"],"sourcesContent":["/**\n * Browser-safe entry point: pure plan-validation utilities with no Node or\n * AI-service dependencies. UI surfaces (the VS Code webview runs in a browser)\n * import runtime values from here instead of the main barrel, which bundles the\n * Gemini/OpenAI services and their `fs`/`child_process` imports.\n */\nexport { canMergeTasks, canSplitTask, canSetDependencies, dependencyCandidates, dependentsOf } from './services/TaskOps';\nexport type { TaskRef } from './services/TaskOps';\nexport { summarizeToolCall } from './services/researchStepSummary';\n","import {\n Task, RunnerId, flattenTasks,\n addTaskToPlan, removeTaskFromPlan, updateTaskInPlan, renumberTasks,\n createTask,\n} from '../models/Task';\nimport { extractObjectsWithKey, stripTrailingCommas, escapeControlCharsInStrings, PlanParseError, TASK_OPS_ENVELOPE_KEY } from './JsonExtractor';\n\n/**\n * Targeted task edits emitted by the planner conversation (the 'task_ops'\n * ConversationTurn). Task references accept a task id, a \"#<order>\" ref, or a\n * bare order number — cheap models rarely echo UUIDs correctly.\n */\nexport type TaskOp =\n | { op: 'update'; taskId: string; changes: Partial<Task> }\n | { op: 'add'; task: Partial<Task> }\n | { op: 'remove'; taskId: string }\n | { op: 'reorder'; taskIds: string[] }\n | { op: 'merge'; taskIds: string[]; merged: Partial<Task> }\n | { op: 'split'; taskId: string; parts: Partial<Task>[] };\n\n/** Fields the planner may change on an existing task. Everything else (id, status, verdict…) is system-owned. */\nconst UPDATABLE_FIELDS: (keyof Task)[] = [\n 'title', 'description', 'prompt', 'dependencies', 'assignedRunner', 'assignedModel', 'taskMode', 'thinkingEffort', 'type', 'userSteps', 'autonomy',\n];\n\nexport function textHasTaskOps(text: string): boolean {\n return text.includes(`\"${TASK_OPS_ENVELOPE_KEY}\"`);\n}\n\n/** Parse a `{\"taskOps\":[...]}` reply. Throws PlanParseError when the JSON is unusable. */\nexport function parseTaskOpsJson(text: string): TaskOp[] {\n // Extract by the taskOps key specifically — a reply may also carry a\n // tasks-keyed object (schema echo, example), which must not shadow the ops.\n // Candidates are tried back to front: the last emission is the final answer.\n const { matches, fallback } = extractObjectsWithKey(text, TASK_OPS_ENVELOPE_KEY);\n const candidates = matches.length > 0 ? [...matches].reverse() : [fallback.json];\n\n let lastError: PlanParseError | null = null;\n for (const json of candidates) {\n try {\n return parseTaskOpsObject(json, text);\n } catch (err) {\n if (!(err instanceof PlanParseError)) throw err;\n if (!lastError) lastError = err;\n }\n }\n throw lastError ?? new PlanParseError('No JSON object found in task-ops reply', text);\n}\n\nfunction parseTaskOpsObject(json: string, text: string): TaskOp[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(escapeControlCharsInStrings(stripTrailingCommas(json)));\n } catch (err) {\n throw new PlanParseError(`Task-ops JSON is invalid: ${err instanceof Error ? err.message : String(err)}`, text);\n }\n const ops = (parsed as { taskOps?: unknown }).taskOps;\n if (!Array.isArray(ops) || ops.length === 0) {\n throw new PlanParseError('\"taskOps\" must be a non-empty array', text);\n }\n for (const op of ops) {\n if (typeof op !== 'object' || op === null || typeof (op as { op?: unknown }).op !== 'string') {\n throw new PlanParseError('Every task op needs an \"op\" field (update | add | remove | reorder | merge | split)', text);\n }\n }\n return ops as TaskOp[];\n}\n\n/** Resolve a task reference (id, \"#3\", or \"3\") against the current tasks. */\nfunction resolveRef(ref: unknown, tasks: Task[]): Task | undefined {\n if (typeof ref !== 'string' && typeof ref !== 'number') return undefined;\n const s = String(ref).trim();\n const byId = tasks.find((t) => t.id === s);\n if (byId) return byId;\n const orderStr = s.startsWith('#') ? s.slice(1) : s;\n if (/^\\d+$/.test(orderStr)) {\n const order = Number(orderStr);\n const byOrder = tasks.filter((t) => t.order === order);\n if (byOrder.length === 1) return byOrder[0];\n }\n return tasks.find((t) => t.title === s);\n}\n\nfunction detectCycle(tasks: Task[]): string | null {\n const byId = new Map(tasks.map((t) => [t.id, t]));\n const visiting = new Set<string>();\n const done = new Set<string>();\n const visit = (id: string): string | null => {\n if (done.has(id)) return null;\n if (visiting.has(id)) return id;\n visiting.add(id);\n for (const dep of byId.get(id)?.dependencies ?? []) {\n const hit = visit(dep);\n if (hit) return hit;\n }\n visiting.delete(id);\n done.add(id);\n return null;\n };\n for (const t of tasks) {\n const hit = visit(t.id);\n if (hit) return byId.get(hit)?.title ?? hit;\n }\n return null;\n}\n\nexport interface ApplyTaskOpsResult {\n ok: boolean;\n tasks: Task[];\n errors: string[];\n /** Human-readable summary of what changed, for the chat transcript. */\n summary: string[];\n}\n\n/**\n * Pre-flight check for a merge: would collapsing the selected tasks into one\n * (rewiring every dependent of any selected task to the survivor) keep the\n * dependency graph valid? Rejects locked tasks and merges whose rewiring would\n * introduce a cycle or an order-violating dependency. Consecutiveness in\n * display order is NOT checked here — that is a UI concern; this validates the\n * structural compatibility the issue calls out.\n */\nexport function canMergeTasks(tasks: Task[], selectedIds: string[]): { ok: boolean; error?: string } {\n if (selectedIds.length < 2) return { ok: false, error: 'Select at least two tasks to merge' };\n const idSet = new Set(selectedIds);\n const toMerge = tasks.filter((t) => idSet.has(t.id));\n if (toMerge.length < 2) return { ok: false, error: 'At least two valid tasks are required to merge' };\n for (const t of toMerge) {\n if (t.status === 'in_progress') return { ok: false, error: `\"${t.title}\" is running and cannot be merged` };\n if (t.status === 'completed') return { ok: false, error: `\"${t.title}\" is completed and cannot be merged` };\n }\n const ordered = [...toMerge].sort((a, b) => a.order - b.order);\n const survivorId = '__merge_sim__';\n const sim: Task[] = tasks\n .filter((t) => !idSet.has(t.id))\n .map((t) => ({ ...t, dependencies: t.dependencies.map((d) => (idSet.has(d) ? survivorId : d)).filter((d) => d !== t.id) }));\n const unionDeps = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));\n sim.push({ ...ordered[0], id: survivorId, title: '__merge_sim__', order: ordered[0].order, dependencies: unionDeps });\n const cycle = detectCycle(sim);\n if (cycle) return { ok: false, error: `Merging would create a dependency cycle involving \"${cycle}\"` };\n const orderById = new Map(sim.map((t) => [t.id, t.order]));\n for (const t of sim) {\n for (const dep of t.dependencies) {\n const depOrder = orderById.get(dep);\n if (depOrder !== undefined && depOrder >= t.order) {\n return { ok: false, error: 'Merging would break task ordering — a dependency of the merged set comes after it' };\n }\n }\n }\n return { ok: true };\n}\n\n/**\n * The shape the dependency helpers below read. Structural rather than `Task`,\n * because the TUI projects tasks into its own `TaskView`: a surface's\n * dependency picker and the API's validation must agree on which dependencies\n * are legal, and a signature only core can satisfy would have forced the TUI\n * to keep a second copy of the rule.\n */\nexport interface TaskRef {\n id: string;\n order: number;\n title: string;\n dependencies: string[];\n}\n\n/** The tasks listing `taskId` as a dependency — exactly what removing it detaches. */\nexport function dependentsOf<T extends Pick<TaskRef, 'id' | 'dependencies'>>(tasks: T[], taskId: string): T[] {\n return tasks.filter((t) => t.id !== taskId && t.dependencies.includes(taskId));\n}\n\n/**\n * The tasks that may become dependencies of `taskId`: those displayed before it.\n *\n * Offering only earlier tasks is what keeps a hand-edited graph valid without a\n * cycle check — dependencies then only ever point backwards in display order,\n * the same invariant `applyTaskOps` enforces. Omit `taskId` for a task that does\n * not exist yet: it lands last, so every current task is a candidate.\n */\nexport function dependencyCandidates<T extends Pick<TaskRef, 'id' | 'order'>>(tasks: T[], taskId?: string): T[] {\n if (!taskId) return [...tasks];\n const target = tasks.find((t) => t.id === taskId);\n return target ? tasks.filter((t) => t.order < target.order) : [];\n}\n\n/** Pre-flight check for a hand-edited dependency list: every id exists and comes earlier. */\nexport function canSetDependencies<T extends TaskRef>(\n tasks: T[],\n taskId: string,\n dependencies: string[],\n): { ok: boolean; error?: string } {\n const target = tasks.find((t) => t.id === taskId);\n if (!target) return { ok: false, error: 'Task not found' };\n const allowed = new Set(dependencyCandidates(tasks, taskId).map((t) => t.id));\n for (const dep of dependencies) {\n if (dep === taskId) return { ok: false, error: `\"${target.title}\" cannot depend on itself` };\n const found = tasks.find((t) => t.id === dep);\n if (!found) return { ok: false, error: `Unknown dependency \"${dep}\"` };\n if (!allowed.has(dep)) {\n return { ok: false, error: `\"${target.title}\" cannot depend on \"${found.title}\", which comes after it in the plan` };\n }\n }\n return { ok: true };\n}\n\n/** Pre-flight check for a split: the task exists and is not locked by execution. */\nexport function canSplitTask(tasks: Task[], taskId: string): { ok: boolean; error?: string } {\n const target = tasks.find((t) => t.id === taskId);\n if (!target) return { ok: false, error: 'Task not found' };\n if (target.status === 'in_progress') return { ok: false, error: `\"${target.title}\" is running and cannot be split` };\n if (target.status === 'completed') return { ok: false, error: `\"${target.title}\" is completed and cannot be split` };\n return { ok: true };\n}\n\n/**\n * Apply task ops to a snapshot of the plan, atomically: either every op\n * applies and the result validates (deps resolve, no cycles, running and\n * completed tasks untouched), or nothing is returned and `errors` explains\n * why. The caller commits `tasks` on ok.\n */\nexport function applyTaskOps(currentTasks: Task[], ops: TaskOp[], runners: RunnerId[]): ApplyTaskOpsResult {\n let tasks: Task[] = JSON.parse(JSON.stringify(currentTasks));\n const errors: string[] = [];\n const summary: string[] = [];\n\n const resolveDeps = (deps: unknown): { ids: string[]; bad: string[] } => {\n const ids: string[] = [];\n const bad: string[] = [];\n for (const d of Array.isArray(deps) ? deps : []) {\n const t = resolveRef(d, flattenTasks(tasks));\n if (t) ids.push(t.id);\n else bad.push(String(d));\n }\n return { ids, bad };\n };\n\n for (const [i, op] of ops.entries()) {\n const label = `op ${i + 1} (${op.op})`;\n switch (op.op) {\n case 'update': {\n const target = resolveRef(op.taskId, flattenTasks(tasks));\n if (!target) { errors.push(`${label}: task \"${op.taskId}\" not found`); break; }\n if (target.status === 'in_progress') { errors.push(`${label}: \"${target.title}\" is running and cannot be modified`); break; }\n if (target.status === 'completed') { errors.push(`${label}: \"${target.title}\" is completed and cannot be modified`); break; }\n const changes: Partial<Task> = {};\n for (const key of UPDATABLE_FIELDS) {\n if (op.changes && key in op.changes) (changes as Record<string, unknown>)[key] = (op.changes as Record<string, unknown>)[key];\n }\n if ('dependencies' in changes) {\n const { ids, bad } = resolveDeps(changes.dependencies);\n if (bad.length) { errors.push(`${label}: unknown dependencies: ${bad.join(', ')}`); break; }\n changes.dependencies = ids.filter((id) => id !== target.id);\n }\n if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {\n errors.push(`${label}: runner \"${changes.assignedRunner}\" is not in this plan's runner set [${runners.join(', ')}]`);\n break;\n }\n if (Object.keys(changes).length === 0) { errors.push(`${label}: no valid changes provided`); break; }\n tasks = updateTaskInPlan(tasks, target.id, changes);\n summary.push(`Updated \"${target.title}\" (${Object.keys(changes).join(', ')})`);\n break;\n }\n case 'add': {\n const spec = op.task ?? {};\n if (!spec.title) { errors.push(`${label}: new task needs a title`); break; }\n const { ids, bad } = resolveDeps(spec.dependencies);\n if (bad.length) { errors.push(`${label}: unknown dependencies: ${bad.join(', ')}`); break; }\n const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : runners[0];\n tasks = addTaskToPlan(tasks, {\n ...spec,\n id: undefined,\n dependencies: ids,\n assignedRunner: runner,\n description: spec.description ?? spec.title,\n prompt: spec.prompt ?? spec.description ?? spec.title,\n });\n summary.push(`Added \"${spec.title}\"`);\n break;\n }\n case 'remove': {\n const target = resolveRef(op.taskId, flattenTasks(tasks));\n if (!target) { errors.push(`${label}: task \"${op.taskId}\" not found`); break; }\n if (target.status === 'in_progress') { errors.push(`${label}: \"${target.title}\" is running and cannot be removed`); break; }\n if (target.status === 'completed') { errors.push(`${label}: \"${target.title}\" is completed and cannot be removed`); break; }\n tasks = removeTaskFromPlan(tasks, target.id);\n summary.push(`Removed \"${target.title}\"`);\n break;\n }\n case 'reorder': {\n const resolved = (op.taskIds ?? []).map((r) => resolveRef(r, tasks));\n if (resolved.some((t) => !t)) { errors.push(`${label}: unknown task in reorder list`); break; }\n const orderIds = resolved.map((t) => t!.id);\n if (new Set(orderIds).size !== tasks.length) {\n errors.push(`${label}: reorder must list every top-level task exactly once (${tasks.length} tasks)`);\n break;\n }\n const byId = new Map(tasks.map((t) => [t.id, t]));\n tasks = renumberTasks(orderIds.map((id) => byId.get(id)!));\n summary.push('Reordered tasks');\n break;\n }\n case 'merge': {\n const resolved = (op.taskIds ?? []).map((r) => resolveRef(r, tasks));\n if (resolved.some((t) => !t)) { errors.push(`${label}: unknown task in merge list`); break; }\n const toMerge = resolved.map((t) => t!).sort((a, b) => a.order - b.order);\n if (toMerge.length < 2) { errors.push(`${label}: merge needs at least two tasks`); break; }\n const locked = toMerge.find((t) => t.status === 'in_progress' || t.status === 'completed');\n if (locked) {\n errors.push(`${label}: \"${locked.title}\" is ${locked.status === 'in_progress' ? 'running' : 'completed'} and cannot be merged`);\n break;\n }\n const idSet = new Set(toMerge.map((t) => t.id));\n const unionDepsRaw = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));\n const { ids, bad } = resolveDeps(unionDepsRaw);\n if (bad.length) { errors.push(`${label}: unknown dependencies: ${bad.join(', ')}`); break; }\n const spec = op.merged ?? {};\n if (!spec.title) { errors.push(`${label}: merged task needs a title`); break; }\n const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : toMerge[0].assignedRunner;\n const mergedTask = createTask({\n title: spec.title,\n description: spec.description ?? toMerge.map((t) => t.description).filter(Boolean).join('\\n\\n'),\n prompt: spec.prompt ?? (toMerge.map((t) => t.prompt).filter(Boolean).join('\\n\\n---\\n\\n') || undefined),\n type: spec.type ?? (toMerge.some((t) => t.type === 'user') ? 'user' : 'ai'),\n dependencies: ids,\n assignedRunner: runner,\n assignedModel: spec.assignedModel ?? toMerge.find((t) => t.assignedModel)?.assignedModel,\n thinkingEffort: spec.thinkingEffort ?? toMerge.find((t) => t.thinkingEffort)?.thinkingEffort,\n taskMode: spec.taskMode ?? toMerge[0].taskMode,\n autonomy: spec.autonomy ?? toMerge.find((t) => t.autonomy)?.autonomy,\n sliceType: spec.sliceType ?? toMerge.find((t) => t.sliceType)?.sliceType,\n userStoriesCovered: spec.userStoriesCovered ?? (toMerge.flatMap((t) => t.userStoriesCovered ?? []).length\n ? [...new Set(toMerge.flatMap((t) => t.userStoriesCovered ?? []))]\n : undefined),\n });\n const mergedId = mergedTask.id;\n const firstId = toMerge[0].id;\n const result: Task[] = [];\n let inserted = false;\n for (const t of tasks) {\n if (idSet.has(t.id)) {\n if (!inserted && t.id === firstId) { result.push(mergedTask); inserted = true; }\n continue;\n }\n result.push({ ...t, dependencies: t.dependencies.map((d) => (idSet.has(d) ? mergedId : d)) });\n }\n if (!inserted) result.push(mergedTask);\n tasks = renumberTasks(result);\n summary.push(`Merged ${toMerge.length} tasks into \"${spec.title}\"`);\n break;\n }\n case 'split': {\n const target = resolveRef(op.taskId, tasks);\n if (!target) { errors.push(`${label}: task \"${op.taskId}\" not found`); break; }\n if (target.status === 'in_progress') { errors.push(`${label}: \"${target.title}\" is running and cannot be split`); break; }\n if (target.status === 'completed') { errors.push(`${label}: \"${target.title}\" is completed and cannot be split`); break; }\n const parts = op.parts ?? [];\n if (parts.length < 2) { errors.push(`${label}: split needs at least two parts`); break; }\n const newTasks: Task[] = [];\n let prevId: string | undefined;\n let buildError = false;\n for (let i = 0; i < parts.length; i++) {\n const spec = parts[i];\n if (!spec || !spec.title) { errors.push(`${label}: split part ${i + 1} needs a title`); buildError = true; break; }\n // Part 0 inherits the original's (external) dependencies, resolved\n // against the current plan. Later parts chain onto the previous part\n // — a sibling id created in this same op, so it needs no resolution.\n let deps: string[];\n if (i === 0) {\n const { ids, bad } = resolveDeps(target.dependencies);\n if (bad.length) { errors.push(`${label}: split part 1 unknown dependencies: ${bad.join(', ')}`); buildError = true; break; }\n deps = ids;\n } else {\n deps = prevId ? [prevId] : [];\n }\n const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : target.assignedRunner;\n const nt = createTask({\n title: spec.title,\n description: spec.description ?? spec.title,\n prompt: spec.prompt ?? spec.description ?? spec.title,\n type: spec.type ?? target.type,\n dependencies: deps,\n assignedRunner: runner,\n assignedModel: spec.assignedModel ?? target.assignedModel,\n taskMode: spec.taskMode ?? target.taskMode,\n autonomy: spec.autonomy ?? target.autonomy,\n sliceType: spec.sliceType ?? target.sliceType,\n userStoriesCovered: spec.userStoriesCovered ?? target.userStoriesCovered,\n });\n newTasks.push(nt);\n prevId = nt.id;\n }\n if (buildError) break;\n const tailId = newTasks[newTasks.length - 1].id;\n const result: Task[] = [];\n for (const t of tasks) {\n if (t.id === target.id) { result.push(...newTasks); continue; }\n result.push({ ...t, dependencies: t.dependencies.map((d) => (d === target.id ? tailId : d)) });\n }\n tasks = renumberTasks(result);\n summary.push(`Split \"${target.title}\" into ${newTasks.length} tasks`);\n break;\n }\n default:\n errors.push(`${label}: unknown op \"${(op as { op: string }).op}\"`);\n }\n if (errors.length) break;\n }\n\n if (!errors.length) {\n const flat = flattenTasks(tasks);\n const idSet = new Set(flat.map((t) => t.id));\n for (const t of flat) {\n for (const dep of t.dependencies) {\n if (!idSet.has(dep)) errors.push(`\"${t.title}\" depends on a task that no longer exists (${dep})`);\n }\n }\n const cycleAt = detectCycle(flat);\n if (cycleAt) errors.push(`Dependency cycle detected involving \"${cycleAt}\"`);\n // Order must respect dependencies. This is the only enforcer of that\n // invariant now that no surface can reposition a task by hand.\n const orderById = new Map(tasks.map((t) => [t.id, t.order]));\n for (const t of tasks) {\n for (const dep of t.dependencies) {\n const depOrder = orderById.get(dep);\n if (depOrder !== undefined && depOrder >= t.order) {\n errors.push(`\"${t.title}\" is ordered before its dependency; move the dependency earlier`);\n }\n }\n }\n }\n\n return errors.length\n ? { ok: false, tasks: currentTasks, errors, summary: [] }\n : { ok: true, tasks, errors: [], summary };\n}\n","import type { ResearchStepOutcome } from '../models/Task';\n\n/**\n * One-line human summary of a research tool call, shared by every surface that\n * renders live planner progress (VS Code webview, CLI, TUI). Kept in core so\n * the arg-shape heuristics (file path vs. pattern vs. shell command) live in\n * one place instead of being re-derived per surface — which also means a new\n * tool becomes readable in all three UIs by being handled here once.\n */\n\nfunction ellipsize(text: string, max: number): string {\n const oneLine = text.replace(/\\s+/g, ' ').trim();\n return oneLine.length > max ? `${oneLine.slice(0, max)}…` : oneLine;\n}\n\n/**\n * @param toolLabel The agent's own name for the tool, when a harness planner\n * produced the call (ADR-0009). It replaces the member name in the summary, so\n * the timeline says `Edit` rather than the catch-all `agent_tool` — and says\n * `WebFetch` rather than claiming a shell command ran.\n */\nexport function summarizeToolCall(tool: string, argsJson: string, toolLabel?: string): string {\n const display = toolLabel?.trim() || tool;\n if (tool === 'agent_tool') {\n // No Ordewell equivalent, so no arg shape to rely on: show the agent's name\n // plus whichever conventional field the call happens to carry.\n try {\n const args = JSON.parse(argsJson);\n const hint = args.path ?? args.file_path ?? args.filePath ?? args.pattern ?? args.query ?? args.url ?? args.command;\n if (hint) return `${display} ${ellipsize(String(hint), 50)}`;\n } catch {\n // fall through to the bare name\n }\n return display;\n }\n\n if (tool === 'spawn_research_agent') {\n try {\n const args = JSON.parse(argsJson);\n const prompt = typeof args.prompt === 'string' ? args.prompt : '';\n const oneLine = prompt.replace(/\\s+/g, ' ').trim();\n return `spawn: \"${oneLine.slice(0, 60)}${oneLine.length > 60 ? '…' : ''}\"`;\n } catch {\n return 'spawn_research_agent';\n }\n }\n\n try {\n const args = JSON.parse(argsJson);\n\n // Ordered by specificity: a tool's defining argument wins over the generic\n // `path`, which several of the newer tools also accept as a search root.\n if (tool === 'find_symbol' && args.symbol) {\n return `find_symbol ${args.symbol}${args.language ? ` [${args.language}]` : ''}`;\n }\n if (tool === 'web_search' && args.query) {\n return `web_search \"${ellipsize(String(args.query), 50)}\"`;\n }\n if (tool === 'fetch' && args.url) {\n return `fetch ${ellipsize(String(args.url), 60)}`;\n }\n // The pattern is what a glob is about; the generic `path` below is only its\n // search root, and reporting that read as \"glob src\" for every call.\n if (tool === 'glob' && args.pattern) {\n return `glob ${ellipsize(String(args.pattern), 50)}`;\n }\n if (tool === 'grep' && args.pattern) {\n const mode = args.output_mode && args.output_mode !== 'content' ? ` (${args.output_mode})` : '';\n const scope = args.include ? ` in ${args.include}` : '';\n return `grep ${ellipsize(String(args.pattern), 40)}${scope}${mode}`;\n }\n\n const path = args.filePath || args.file_path || args.path;\n if (path) {\n const basename = String(path).split('/').pop() || String(path);\n return `${display} ${basename}`;\n }\n if (args.pattern) {\n return `${display} ${args.pattern}`;\n }\n if (args.command) {\n return `bash: ${String(args.command).slice(0, 40)}`;\n }\n } catch {\n // fall through to the bare tool name below\n }\n return display;\n}\n\n/**\n * Classify a tool outcome from its result text and success flag, so a surface\n * can render a refused `rm` or a denied `npm test` distinctly from a\n * successful run without re-deriving the refusal signatures per surface. The\n * refusal/denial strings come from `commandPolicy.ts`, `BaseFileSystem.ts`,\n * and the research-subagent wrapper — keeping the matching here means the\n * signatures stay in one place alongside the human summary.\n */\nexport function classifyOutcome(success: boolean, output: string): ResearchStepOutcome {\n if (!success) {\n if (/^Command refused:/.test(output) || /is not available\\. You are a read-only research subagent/.test(output)) return 'refused';\n if (/not approved/i.test(output) || /^Access denied:/.test(output) || /needs user approval/i.test(output)) return 'denied';\n return 'failure';\n }\n return 'success';\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/plan-utils.ts","../src/services/TaskOps.ts","../src/services/researchStepSummary.ts"],"sourcesContent":["/**\n * Browser-safe entry point: pure plan-validation utilities with no Node or\n * AI-service dependencies. UI surfaces (the VS Code webview runs in a browser)\n * import runtime values from here instead of the main barrel, which bundles the\n * Gemini/OpenAI services and their `fs`/`child_process` imports.\n */\nexport { canMergeTasks, canSplitTask, canSetDependencies, dependencyCandidates, dependentsOf } from './services/TaskOps';\nexport type { TaskRef } from './services/TaskOps';\nexport { summarizeToolCall } from './services/researchStepSummary';\n","import { v4 as uuidv4 } from 'uuid';\nimport {\n Task, RunnerId, flattenTasks,\n addTaskToPlan, removeTaskFromPlan, updateTaskInPlan, renumberTasks,\n createTask,\n} from '../models/Task';\nimport { extractObjectsWithKey, stripTrailingCommas, escapeControlCharsInStrings, PlanParseError, TASK_OPS_ENVELOPE_KEY } from './JsonExtractor';\nimport { validateTaskEdit, checkModelAndModeValidity, type EditCatalog } from './TaskEditValidator';\n\n/**\n * Targeted task edits emitted by the planner conversation (the 'task_ops'\n * ConversationTurn). Task references accept a task id, a \"#<order>\" ref, or a\n * bare order number — cheap models rarely echo UUIDs correctly.\n */\nexport type TaskOp =\n | { op: 'update'; taskId: string; changes: Partial<Task> }\n | { op: 'add'; task: Partial<Task>; handle?: string }\n | { op: 'remove'; taskId: string }\n | { op: 'reorder'; taskIds: string[] }\n | { op: 'merge'; taskIds: string[]; merged: Partial<Task>; handle?: string }\n | { op: 'split'; taskId: string; parts: Partial<Task>[]; handle?: string }\n | { op: 'rearm'; taskId: string; changes?: Partial<Task> };\n\n/** Fields the planner may change on an existing task. Everything else (id, status, verdict…) is system-owned. */\nexport const UPDATABLE_FIELDS: (keyof Task)[] = [\n 'title', 'description', 'prompt', 'dependencies', 'assignedRunner', 'assignedModel', 'taskMode', 'thinkingEffort', 'type', 'userSteps', 'autonomy', 'sliceType',\n];\n\nexport function textHasTaskOps(text: string): boolean {\n return text.includes(`\"${TASK_OPS_ENVELOPE_KEY}\"`);\n}\n\n/** Parse a `{\"taskOps\":[...]}` reply. Throws PlanParseError when the JSON is unusable. */\nexport function parseTaskOpsJson(text: string): TaskOp[] {\n // Extract by the taskOps key specifically — a reply may also carry a\n // tasks-keyed object (schema echo, example), which must not shadow the ops.\n // Candidates are tried back to front: the last emission is the final answer.\n const { matches, fallback } = extractObjectsWithKey(text, TASK_OPS_ENVELOPE_KEY);\n const candidates = matches.length > 0 ? [...matches].reverse() : [fallback.json];\n\n let lastError: PlanParseError | null = null;\n for (const json of candidates) {\n try {\n return parseTaskOpsObject(json, text);\n } catch (err) {\n if (!(err instanceof PlanParseError)) throw err;\n if (!lastError) lastError = err;\n }\n }\n throw lastError ?? new PlanParseError('No JSON object found in task-ops reply', text);\n}\n\nfunction parseTaskOpsObject(json: string, text: string): TaskOp[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(escapeControlCharsInStrings(stripTrailingCommas(json)));\n } catch (err) {\n throw new PlanParseError(`Task-ops JSON is invalid: ${err instanceof Error ? err.message : String(err)}`, text);\n }\n const ops = (parsed as { taskOps?: unknown }).taskOps;\n if (!Array.isArray(ops) || ops.length === 0) {\n throw new PlanParseError('\"taskOps\" must be a non-empty array', text);\n }\n for (const op of ops) {\n if (typeof op !== 'object' || op === null || typeof (op as { op?: unknown }).op !== 'string') {\n throw new PlanParseError('Every task op needs an \"op\" field (update | add | remove | reorder | merge | split)', text);\n }\n }\n return ops as TaskOp[];\n}\n\ntype RefResolution = { id: string } | { error: string };\n\n/**\n * Resolve a task reference (id, \"#3\", \"3\", or a batch handle) to a task id.\n *\n * `#order` and title matches are pinned to `originalScope` — the plan as it\n * stood before the batch began — so an earlier op's renumbering can never\n * make a later op's \"#4\" land on a different task than the planner saw. A\n * literal id has no such drift (ids are stable across mutations), so id\n * matches check `liveScope` — the batch's current, mutated tasks — which is\n * also how a later op reaches a task created earlier in the same batch\n * without a handle (e.g. a split's own chained parts).\n */\nfunction resolveRef(\n ref: unknown,\n originalScope: Task[],\n liveScope: Task[],\n handleOwner: Map<string, number>,\n handleId: Map<string, string>,\n opIndex: number,\n): RefResolution {\n if (typeof ref !== 'string' && typeof ref !== 'number') return { error: `invalid reference \"${String(ref)}\"` };\n const s = String(ref).trim();\n\n const ownerIdx = handleOwner.get(s);\n if (ownerIdx !== undefined) {\n if (ownerIdx >= opIndex) {\n return { error: `handle \"${s}\" is defined later in this batch (op ${ownerIdx + 1}) and cannot be referenced yet` };\n }\n const id = handleId.get(s);\n if (!id) return { error: `handle \"${s}\" was never resolved` };\n return { id };\n }\n\n const byLiveId = liveScope.find((t) => t.id === s);\n if (byLiveId) return { id: byLiveId.id };\n const orderStr = s.startsWith('#') ? s.slice(1) : s;\n if (/^\\d+$/.test(orderStr)) {\n const order = Number(orderStr);\n const byOrder = originalScope.filter((t) => t.order === order);\n if (byOrder.length === 1) return { id: byOrder[0].id };\n }\n const byTitle = originalScope.find((t) => t.title === s);\n if (byTitle) return { id: byTitle.id };\n return { error: `task \"${s}\" not found` };\n}\n\n/** Whether `name` already denotes an existing task by id, `#order`, or title. */\nfunction refCollidesWithExisting(name: string, originalFlatAll: Task[]): boolean {\n if (originalFlatAll.some((t) => t.id === name)) return true;\n const orderStr = name.startsWith('#') ? name.slice(1) : name;\n if (/^\\d+$/.test(orderStr) && originalFlatAll.some((t) => t.order === Number(orderStr))) return true;\n return originalFlatAll.some((t) => t.title === name);\n}\n\nfunction findInTasks(tasks: Task[], id: string): Task | undefined {\n return flattenTasks(tasks).find((t) => t.id === id);\n}\n\nfunction findTopLevel(tasks: Task[], id: string): Task | undefined {\n return tasks.find((t) => t.id === id);\n}\n\nfunction detectCycle(tasks: Task[]): string | null {\n const byId = new Map(tasks.map((t) => [t.id, t]));\n const visiting = new Set<string>();\n const done = new Set<string>();\n const visit = (id: string): string | null => {\n if (done.has(id)) return null;\n if (visiting.has(id)) return id;\n visiting.add(id);\n for (const dep of byId.get(id)?.dependencies ?? []) {\n const hit = visit(dep);\n if (hit) return hit;\n }\n visiting.delete(id);\n done.add(id);\n return null;\n };\n for (const t of tasks) {\n const hit = visit(t.id);\n if (hit) return byId.get(hit)?.title ?? hit;\n }\n return null;\n}\n\n/** A task the repair may not reposition: execution has already reached it. */\nfunction isPinned(t: Task): boolean {\n return t.status === 'in_progress' || t.status === 'completed';\n}\n\n/**\n * Reorder top-level tasks so every dependency comes before its dependents,\n * moving as little as possible: pinned tasks keep the exact slot they are in,\n * and each free slot goes to the earliest-listed movable task whose\n * dependencies are already placed — so tasks the graph does not constrain keep\n * their relative order. Assumes an acyclic graph (the caller checks first).\n */\nfunction repairOrder(tasks: Task[]): { tasks: Task[] } | { error: string } {\n const byId = new Map(tasks.map((t) => [t.id, t]));\n const placed = new Set<string>();\n const movable = tasks.filter((t) => !isPinned(t));\n const result: Task[] = [];\n // Dependencies outside this list (a subtask id) constrain nothing here.\n const blockerOf = (t: Task): Task | undefined => {\n const id = t.dependencies.find((d) => byId.has(d) && !placed.has(d));\n return id ? byId.get(id) : undefined;\n };\n const place = (t: Task) => { result.push(t); placed.add(t.id); };\n\n for (const occupant of tasks) {\n if (isPinned(occupant)) {\n const blocker = blockerOf(occupant);\n if (blocker) return { error: pinnedRefusal(blocker, occupant, 'before') };\n place(occupant);\n continue;\n }\n const next = movable.findIndex((t) => !blockerOf(t));\n if (next < 0) {\n // Nothing is schedulable and the graph is acyclic, so every movable task\n // left waits — directly or through others — on a pinned task further down\n // that would have to come up to free this slot.\n const [stuck, pin] = trailToPin(movable[0], blockerOf);\n return { error: pinnedRefusal(stuck, pin, 'after') };\n }\n place(movable.splice(next, 1)[0]);\n }\n return { tasks: result };\n}\n\n/** Walk the waiting-on chain from `task` down to the pinned task holding it up. */\nfunction trailToPin(task: Task, blockerOf: (t: Task) => Task | undefined): [Task, Task] {\n let at = task;\n for (;;) {\n const blocker = blockerOf(at);\n if (!blocker || isPinned(blocker)) return [at, blocker ?? at];\n at = blocker;\n }\n}\n\n/** `\"B\" #2→#1, \"A\" #1→#3` for every task the repair actually moved, or null. */\nfunction describeMoves(before: Task[], after: Task[]): string | null {\n const wasAt = new Map(before.map((t, i) => [t.id, i + 1]));\n const moved = after\n .map((t, i) => ({ t, from: wasAt.get(t.id)!, to: i + 1 }))\n .filter((m) => m.from !== m.to);\n return moved.length ? moved.map((m) => `\"${m.t.title}\" #${m.from}→#${m.to}`).join(', ') : null;\n}\n\nfunction pinnedRefusal(other: Task, pin: Task, side: 'before' | 'after'): string {\n const word = pin.status === 'in_progress' ? 'running' : 'completed';\n return `\"${other.title}\" would have to run ${side} \"${pin.title}\", which is ${word} and cannot be moved`;\n}\n\nexport interface ApplyTaskOpsResult {\n ok: boolean;\n tasks: Task[];\n errors: string[];\n /** Human-readable summary of what changed, for the chat transcript. */\n summary: string[];\n}\n\n/**\n * Pre-flight check for a merge: would collapsing the selected tasks into one\n * (rewiring every dependent of any selected task to the survivor) keep the\n * dependency graph valid? Rejects locked tasks and merges whose rewiring would\n * introduce a cycle. Display order is NOT checked here — neither\n * consecutiveness (a UI concern) nor a dependency the merge pulls out of order,\n * which {@link applyTaskOps} repairs; refusing on order would make this\n * pre-flight stricter than the applier it stands in for.\n */\nexport function canMergeTasks(tasks: Task[], selectedIds: string[]): { ok: boolean; error?: string } {\n if (selectedIds.length < 2) return { ok: false, error: 'Select at least two tasks to merge' };\n const idSet = new Set(selectedIds);\n const toMerge = tasks.filter((t) => idSet.has(t.id));\n if (toMerge.length < 2) return { ok: false, error: 'At least two valid tasks are required to merge' };\n for (const t of toMerge) {\n if (t.status === 'in_progress') return { ok: false, error: `\"${t.title}\" is running and cannot be merged` };\n if (t.status === 'completed') return { ok: false, error: `\"${t.title}\" is completed and cannot be merged` };\n }\n const ordered = [...toMerge].sort((a, b) => a.order - b.order);\n const survivorId = '__merge_sim__';\n const sim: Task[] = tasks\n .filter((t) => !idSet.has(t.id))\n .map((t) => ({ ...t, dependencies: t.dependencies.map((d) => (idSet.has(d) ? survivorId : d)).filter((d) => d !== t.id) }));\n const unionDeps = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));\n sim.push({ ...ordered[0], id: survivorId, title: '__merge_sim__', order: ordered[0].order, dependencies: unionDeps });\n const cycle = detectCycle(sim);\n if (cycle) return { ok: false, error: `Merging would create a dependency cycle involving \"${cycle}\"` };\n return { ok: true };\n}\n\n/**\n * The shape the dependency helpers below read. Structural rather than `Task`,\n * because the TUI projects tasks into its own `TaskView`: a surface's\n * dependency picker and the API's validation must agree on which dependencies\n * are legal, and a signature only core can satisfy would have forced the TUI\n * to keep a second copy of the rule.\n */\nexport interface TaskRef {\n id: string;\n order: number;\n title: string;\n dependencies: string[];\n}\n\n/** The tasks listing `taskId` as a dependency — exactly what removing it detaches. */\nexport function dependentsOf<T extends Pick<TaskRef, 'id' | 'dependencies'>>(tasks: T[], taskId: string): T[] {\n return tasks.filter((t) => t.id !== taskId && t.dependencies.includes(taskId));\n}\n\n/**\n * The tasks that may become dependencies of `taskId`: those displayed before it.\n *\n * Offering only earlier tasks is what keeps a hand-edited graph valid without a\n * cycle check — dependencies then only ever point backwards in display order,\n * the same invariant `applyTaskOps` enforces. Omit `taskId` for a task that does\n * not exist yet: it lands last, so every current task is a candidate.\n */\nexport function dependencyCandidates<T extends Pick<TaskRef, 'id' | 'order'>>(tasks: T[], taskId?: string): T[] {\n if (!taskId) return [...tasks];\n const target = tasks.find((t) => t.id === taskId);\n return target ? tasks.filter((t) => t.order < target.order) : [];\n}\n\n/** Pre-flight check for a hand-edited dependency list: every id exists and comes earlier. */\nexport function canSetDependencies<T extends TaskRef>(\n tasks: T[],\n taskId: string,\n dependencies: string[],\n): { ok: boolean; error?: string } {\n const target = tasks.find((t) => t.id === taskId);\n if (!target) return { ok: false, error: 'Task not found' };\n const allowed = new Set(dependencyCandidates(tasks, taskId).map((t) => t.id));\n for (const dep of dependencies) {\n if (dep === taskId) return { ok: false, error: `\"${target.title}\" cannot depend on itself` };\n const found = tasks.find((t) => t.id === dep);\n if (!found) return { ok: false, error: `Unknown dependency \"${dep}\"` };\n if (!allowed.has(dep)) {\n return { ok: false, error: `\"${target.title}\" cannot depend on \"${found.title}\", which comes after it in the plan` };\n }\n }\n return { ok: true };\n}\n\n/** Pre-flight check for a split: the task exists and is not locked by execution. */\nexport function canSplitTask(tasks: Task[], taskId: string): { ok: boolean; error?: string } {\n const target = tasks.find((t) => t.id === taskId);\n if (!target) return { ok: false, error: 'Task not found' };\n if (target.status === 'in_progress') return { ok: false, error: `\"${target.title}\" is running and cannot be split` };\n if (target.status === 'completed') return { ok: false, error: `\"${target.title}\" is completed and cannot be split` };\n return { ok: true };\n}\n\n/**\n * Apply task ops to a snapshot of the plan, atomically: either every op\n * applies and the result validates (deps resolve, no cycles, running and\n * completed tasks untouched), or nothing is returned and `errors` explains\n * why. The caller commits `tasks` on ok.\n */\nexport function applyTaskOps(currentTasks: Task[], ops: TaskOp[], runners: RunnerId[], catalog?: EditCatalog): ApplyTaskOpsResult {\n let tasks: Task[] = JSON.parse(JSON.stringify(currentTasks));\n const errors: string[] = [];\n const summary: string[] = [];\n\n // Frozen snapshot every ref resolves `#order`/title matches against, so an\n // earlier op's renumbering can never shift what a later op's \"#N\" means.\n const originalFlatAll = flattenTasks(currentTasks);\n\n // Batch handles: planner-chosen names for a task an add/merge/split op is\n // about to create, so a later op in the same batch can reference it before\n // it has a real id. Pre-scanned up front — a bad handle refuses the whole\n // batch before anything mutates.\n const handleOwner = new Map<string, number>();\n const handleId = new Map<string, string>();\n for (const [i, op] of ops.entries()) {\n const handle = (op as { handle?: unknown }).handle;\n if (handle === undefined) continue;\n if (typeof handle !== 'string' || !handle.trim()) {\n errors.push(`op ${i + 1} (${op.op}): handle must be a non-empty string`);\n continue;\n }\n if (handleOwner.has(handle)) {\n errors.push(`op ${i + 1} (${op.op}): handle \"${handle}\" is already used earlier in this batch`);\n continue;\n }\n if (refCollidesWithExisting(handle, originalFlatAll)) {\n errors.push(`op ${i + 1} (${op.op}): handle \"${handle}\" collides with an existing task reference`);\n continue;\n }\n handleOwner.set(handle, i);\n }\n if (errors.length) return { ok: false, tasks: currentTasks, errors, summary: [] };\n\n const resolveDeps = (deps: unknown, opIndex: number): { ids: string[]; bad: string[] } => {\n const ids: string[] = [];\n const bad: string[] = [];\n const liveFlat = flattenTasks(tasks);\n for (const d of Array.isArray(deps) ? deps : []) {\n const r = resolveRef(d, originalFlatAll, liveFlat, handleOwner, handleId, opIndex);\n if ('error' in r) { bad.push(`${String(d)} (${r.error})`); continue; }\n if (!liveFlat.some((t) => t.id === r.id)) { bad.push(`${String(d)} (no longer exists in this batch)`); continue; }\n ids.push(r.id);\n }\n return { ids, bad };\n };\n\n for (const [i, op] of ops.entries()) {\n const label = `op ${i + 1} (${op.op})`;\n switch (op.op) {\n case 'update': {\n const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);\n if ('error' in ref) { errors.push(`${label}: ${ref.error}`); break; }\n const target = findInTasks(tasks, ref.id);\n if (!target) { errors.push(`${label}: task \"${op.taskId}\" no longer exists in this batch (removed, merged, or split by an earlier op)`); break; }\n const changes: Partial<Task> = {};\n for (const key of UPDATABLE_FIELDS) {\n if (op.changes && key in op.changes) (changes as Record<string, unknown>)[key] = (op.changes as Record<string, unknown>)[key];\n }\n if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {\n errors.push(`${label}: runner \"${changes.assignedRunner}\" is not in this plan's runner set [${runners.join(', ')}]`);\n break;\n }\n // Dependency refs are still unresolved (e.g. \"#3\") at this point, so\n // this check runs on everything but them; resolveDeps below handles\n // dependency well-formedness once they're real ids.\n const changesForCheck: Partial<Task> = { ...changes };\n delete changesForCheck.dependencies;\n const check = validateTaskEdit('planner', flattenTasks(tasks), target.id, changesForCheck, catalog);\n if (!check.ok) { errors.push(`${label}: ${check.error}`); break; }\n for (const field of check.clear ?? []) (changes as Record<string, unknown>)[field] = undefined;\n if ('dependencies' in changes) {\n const { ids, bad } = resolveDeps(changes.dependencies, i);\n if (bad.length) { errors.push(`${label}: unknown dependencies: ${bad.join(', ')}`); break; }\n changes.dependencies = ids.filter((id) => id !== target.id);\n }\n if (Object.keys(changes).length === 0) { errors.push(`${label}: no valid changes provided`); break; }\n tasks = updateTaskInPlan(tasks, target.id, changes);\n const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(', ')}` : '';\n summary.push(`Updated \"${target.title}\" (${Object.keys(changes).join(', ')})${clearedNote}`);\n break;\n }\n case 'add': {\n const spec = op.task ?? {};\n if (!spec.title) { errors.push(`${label}: new task needs a title`); break; }\n const { ids, bad } = resolveDeps(spec.dependencies, i);\n if (bad.length) { errors.push(`${label}: unknown dependencies: ${bad.join(', ')}`); break; }\n const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : runners[0];\n const validity = checkModelAndModeValidity(runner, spec.assignedModel, spec.taskMode, catalog);\n if (!validity.ok) { errors.push(`${label}: ${validity.error}`); break; }\n const newId = uuidv4();\n tasks = addTaskToPlan(tasks, {\n ...spec,\n id: newId,\n dependencies: ids,\n assignedRunner: runner,\n description: spec.description ?? spec.title,\n prompt: spec.prompt ?? spec.description ?? spec.title,\n });\n if (op.handle) handleId.set(op.handle, newId);\n summary.push(`Added \"${spec.title}\"`);\n break;\n }\n case 'remove': {\n const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);\n if ('error' in ref) { errors.push(`${label}: ${ref.error}`); break; }\n const target = findInTasks(tasks, ref.id);\n if (!target) { errors.push(`${label}: task \"${op.taskId}\" no longer exists in this batch (removed, merged, or split by an earlier op)`); break; }\n if (target.status === 'in_progress') { errors.push(`${label}: \"${target.title}\" is running and cannot be removed`); break; }\n if (target.status === 'completed') { errors.push(`${label}: \"${target.title}\" is completed and cannot be removed`); break; }\n tasks = removeTaskFromPlan(tasks, target.id);\n summary.push(`Removed \"${target.title}\"`);\n break;\n }\n case 'reorder': {\n const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));\n const badRef = refs.find((r): r is { error: string } => 'error' in r);\n if (badRef) { errors.push(`${label}: ${badRef.error}`); break; }\n const resolved = (refs as { id: string }[]).map((r) => findTopLevel(tasks, r.id));\n if (resolved.some((t) => !t)) { errors.push(`${label}: unknown task in reorder list`); break; }\n const orderIds = resolved.map((t) => t!.id);\n if (new Set(orderIds).size !== tasks.length) {\n errors.push(`${label}: reorder must list every top-level task exactly once (${tasks.length} tasks)`);\n break;\n }\n const byId = new Map(tasks.map((t) => [t.id, t]));\n tasks = renumberTasks(orderIds.map((id) => byId.get(id)!));\n summary.push('Reordered tasks');\n break;\n }\n case 'merge': {\n const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));\n const badRef = refs.find((r): r is { error: string } => 'error' in r);\n if (badRef) { errors.push(`${label}: ${badRef.error}`); break; }\n const resolved = (refs as { id: string }[]).map((r) => findTopLevel(tasks, r.id));\n if (resolved.some((t) => !t)) { errors.push(`${label}: unknown task in merge list (already removed, merged, or split by an earlier op)`); break; }\n const toMerge = resolved.map((t) => t!).sort((a, b) => a.order - b.order);\n if (toMerge.length < 2) { errors.push(`${label}: merge needs at least two tasks`); break; }\n const locked = toMerge.find((t) => t.status === 'in_progress' || t.status === 'completed');\n if (locked) {\n errors.push(`${label}: \"${locked.title}\" is ${locked.status === 'in_progress' ? 'running' : 'completed'} and cannot be merged`);\n break;\n }\n const idSet = new Set(toMerge.map((t) => t.id));\n const unionDepsRaw = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));\n const { ids, bad } = resolveDeps(unionDepsRaw, i);\n if (bad.length) { errors.push(`${label}: unknown dependencies: ${bad.join(', ')}`); break; }\n const spec = op.merged ?? {};\n if (!spec.title) { errors.push(`${label}: merged task needs a title`); break; }\n const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : toMerge[0].assignedRunner;\n const mergedTask = createTask({\n title: spec.title,\n description: spec.description ?? toMerge.map((t) => t.description).filter(Boolean).join('\\n\\n'),\n prompt: spec.prompt ?? (toMerge.map((t) => t.prompt).filter(Boolean).join('\\n\\n---\\n\\n') || undefined),\n type: spec.type ?? (toMerge.some((t) => t.type === 'user') ? 'user' : 'ai'),\n dependencies: ids,\n assignedRunner: runner,\n assignedModel: spec.assignedModel ?? toMerge.find((t) => t.assignedModel)?.assignedModel,\n thinkingEffort: spec.thinkingEffort ?? toMerge.find((t) => t.thinkingEffort)?.thinkingEffort,\n taskMode: spec.taskMode ?? toMerge[0].taskMode,\n autonomy: spec.autonomy ?? toMerge.find((t) => t.autonomy)?.autonomy,\n sliceType: spec.sliceType ?? toMerge.find((t) => t.sliceType)?.sliceType,\n userStoriesCovered: spec.userStoriesCovered ?? (toMerge.flatMap((t) => t.userStoriesCovered ?? []).length\n ? [...new Set(toMerge.flatMap((t) => t.userStoriesCovered ?? []))]\n : undefined),\n });\n const mergedId = mergedTask.id;\n const firstId = toMerge[0].id;\n const result: Task[] = [];\n let inserted = false;\n for (const t of tasks) {\n if (idSet.has(t.id)) {\n if (!inserted && t.id === firstId) { result.push(mergedTask); inserted = true; }\n continue;\n }\n result.push({ ...t, dependencies: t.dependencies.map((d) => (idSet.has(d) ? mergedId : d)) });\n }\n if (!inserted) result.push(mergedTask);\n tasks = renumberTasks(result);\n if (op.handle) handleId.set(op.handle, mergedId);\n summary.push(`Merged ${toMerge.length} tasks into \"${spec.title}\"`);\n break;\n }\n case 'split': {\n const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, i);\n if ('error' in ref) { errors.push(`${label}: ${ref.error}`); break; }\n const target = findTopLevel(tasks, ref.id);\n if (!target) { errors.push(`${label}: task \"${op.taskId}\" not found`); break; }\n if (target.status === 'in_progress') { errors.push(`${label}: \"${target.title}\" is running and cannot be split`); break; }\n if (target.status === 'completed') { errors.push(`${label}: \"${target.title}\" is completed and cannot be split`); break; }\n const parts = op.parts ?? [];\n if (parts.length < 2) { errors.push(`${label}: split needs at least two parts`); break; }\n const newTasks: Task[] = [];\n let prevId: string | undefined;\n let buildError = false;\n for (let p = 0; p < parts.length; p++) {\n const spec = parts[p];\n if (!spec || !spec.title) { errors.push(`${label}: split part ${p + 1} needs a title`); buildError = true; break; }\n // Part 0 inherits the original's (external) dependencies, resolved\n // against the current plan. Later parts chain onto the previous part\n // — a sibling id created in this same op, so it needs no resolution.\n let deps: string[];\n if (p === 0) {\n const { ids, bad } = resolveDeps(target.dependencies, i);\n if (bad.length) { errors.push(`${label}: split part 1 unknown dependencies: ${bad.join(', ')}`); buildError = true; break; }\n deps = ids;\n } else {\n deps = prevId ? [prevId] : [];\n }\n const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : target.assignedRunner;\n const nt = createTask({\n title: spec.title,\n description: spec.description ?? spec.title,\n prompt: spec.prompt ?? spec.description ?? spec.title,\n type: spec.type ?? target.type,\n dependencies: deps,\n assignedRunner: runner,\n assignedModel: spec.assignedModel ?? target.assignedModel,\n taskMode: spec.taskMode ?? target.taskMode,\n autonomy: spec.autonomy ?? target.autonomy,\n sliceType: spec.sliceType ?? target.sliceType,\n userStoriesCovered: spec.userStoriesCovered ?? target.userStoriesCovered,\n });\n newTasks.push(nt);\n prevId = nt.id;\n }\n if (buildError) break;\n const tailId = newTasks[newTasks.length - 1].id;\n const result: Task[] = [];\n for (const t of tasks) {\n if (t.id === target.id) { result.push(...newTasks); continue; }\n result.push({ ...t, dependencies: t.dependencies.map((d) => (d === target.id ? tailId : d)) });\n }\n tasks = renumberTasks(result);\n if (op.handle) handleId.set(op.handle, tailId);\n summary.push(`Split \"${target.title}\" into ${newTasks.length} tasks`);\n break;\n }\n case 'rearm': {\n const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);\n if ('error' in ref) { errors.push(`${label}: ${ref.error}`); break; }\n const target = findInTasks(tasks, ref.id);\n if (!target) { errors.push(`${label}: task \"${op.taskId}\" no longer exists in this batch (removed, merged, or split by an earlier op)`); break; }\n if (target.status === 'in_progress') { errors.push(`${label}: \"${target.title}\" is running and cannot be re-armed`); break; }\n const changes: Partial<Task> = {};\n for (const key of UPDATABLE_FIELDS) {\n if (op.changes && key in op.changes) (changes as Record<string, unknown>)[key] = (op.changes as Record<string, unknown>)[key];\n }\n if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {\n errors.push(`${label}: runner \"${changes.assignedRunner}\" is not in this plan's runner set [${runners.join(', ')}]`);\n break;\n }\n // Validated as a direct edit (not 'planner'): re-arming is the sanctioned\n // exception to the planner lock — its whole point is to touch a task the\n // lock would otherwise refuse. Well-formedness (type coherence, model/mode\n // validity, dependency shape) still applies, same as every other edit.\n const changesForCheck: Partial<Task> = { ...changes };\n delete changesForCheck.dependencies;\n const check = validateTaskEdit('direct', flattenTasks(tasks), target.id, changesForCheck, catalog);\n if (!check.ok) { errors.push(`${label}: ${check.error}`); break; }\n for (const field of check.clear ?? []) (changes as Record<string, unknown>)[field] = undefined;\n if ('dependencies' in changes) {\n const { ids, bad } = resolveDeps(changes.dependencies, i);\n if (bad.length) { errors.push(`${label}: unknown dependencies: ${bad.join(', ')}`); break; }\n changes.dependencies = ids.filter((id) => id !== target.id);\n }\n tasks = updateTaskInPlan(tasks, target.id, { ...changes, status: 'pending', verdict: undefined, outputSummary: undefined });\n // Dependents parked at 'blocked' by this task's earlier failure have\n // nothing else to release them — the scheduler reads `status` directly,\n // not a live recomputation of the dependency graph.\n for (const dep of flattenTasks(tasks)) {\n if (dep.status === 'blocked' && dep.dependencies.includes(target.id)) {\n tasks = updateTaskInPlan(tasks, dep.id, { status: 'pending' });\n }\n }\n const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(', ')}` : '';\n const changedNote = Object.keys(changes).length ? ` (${Object.keys(changes).join(', ')})` : '';\n summary.push(`Re-armed \"${target.title}\"${changedNote}${clearedNote}`);\n break;\n }\n default:\n errors.push(`${label}: unknown op \"${(op as { op: string }).op}\"`);\n }\n if (errors.length) break;\n }\n\n if (!errors.length) {\n const flat = flattenTasks(tasks);\n const idSet = new Set(flat.map((t) => t.id));\n for (const t of flat) {\n for (const dep of t.dependencies) {\n if (!idSet.has(dep)) errors.push(`\"${t.title}\" depends on a task that no longer exists (${dep})`);\n }\n }\n const cycleAt = detectCycle(flat);\n if (cycleAt) errors.push(`Dependency cycle detected involving \"${cycleAt}\"`);\n // Display order is maintained, not demanded: whatever the batch produced,\n // the plan comes back with dependencies ahead of their dependents. The\n // planner never has to re-declare the whole order to move one task.\n if (!errors.length) {\n const repaired = repairOrder(tasks);\n if ('error' in repaired) {\n errors.push(repaired.error);\n } else {\n const moves = describeMoves(tasks, repaired.tasks);\n if (moves) summary.push(`Reordered to keep dependencies first: ${moves}`);\n tasks = renumberTasks(repaired.tasks);\n }\n }\n }\n\n return errors.length\n ? { ok: false, tasks: currentTasks, errors, summary: [] }\n : { ok: true, tasks, errors: [], summary };\n}\n","import type { ResearchStepOutcome } from '../models/Task';\n\n/**\n * One-line human summary of a research tool call, shared by every surface that\n * renders live planner progress (VS Code webview, CLI, TUI). Kept in core so\n * the arg-shape heuristics (file path vs. pattern vs. shell command) live in\n * one place instead of being re-derived per surface — which also means a new\n * tool becomes readable in all three UIs by being handled here once.\n */\n\nfunction ellipsize(text: string, max: number): string {\n const oneLine = text.replace(/\\s+/g, ' ').trim();\n return oneLine.length > max ? `${oneLine.slice(0, max)}…` : oneLine;\n}\n\n/**\n * @param toolLabel The agent's own name for the tool, when a harness planner\n * produced the call (ADR-0009). It replaces the member name in the summary, so\n * the timeline says `Edit` rather than the catch-all `agent_tool` — and says\n * `WebFetch` rather than claiming a shell command ran.\n */\nexport function summarizeToolCall(tool: string, argsJson: string, toolLabel?: string): string {\n const display = toolLabel?.trim() || tool;\n if (tool === 'agent_tool') {\n // No Ordewell equivalent, so no arg shape to rely on: show the agent's name\n // plus whichever conventional field the call happens to carry.\n try {\n const args = JSON.parse(argsJson);\n const hint = args.path ?? args.file_path ?? args.filePath ?? args.pattern ?? args.query ?? args.url ?? args.command;\n if (hint) return `${display} ${ellipsize(String(hint), 50)}`;\n } catch {\n // fall through to the bare name\n }\n return display;\n }\n\n if (tool === 'spawn_research_agent') {\n try {\n const args = JSON.parse(argsJson);\n const prompt = typeof args.prompt === 'string' ? args.prompt : '';\n const oneLine = prompt.replace(/\\s+/g, ' ').trim();\n return `spawn: \"${oneLine.slice(0, 60)}${oneLine.length > 60 ? '…' : ''}\"`;\n } catch {\n return 'spawn_research_agent';\n }\n }\n\n try {\n const args = JSON.parse(argsJson);\n\n // Ordered by specificity: a tool's defining argument wins over the generic\n // `path`, which several of the newer tools also accept as a search root.\n if (tool === 'find_symbol' && args.symbol) {\n return `find_symbol ${args.symbol}${args.language ? ` [${args.language}]` : ''}`;\n }\n if (tool === 'web_search' && args.query) {\n return `web_search \"${ellipsize(String(args.query), 50)}\"`;\n }\n if (tool === 'fetch' && args.url) {\n return `fetch ${ellipsize(String(args.url), 60)}`;\n }\n // The pattern is what a glob is about; the generic `path` below is only its\n // search root, and reporting that read as \"glob src\" for every call.\n if (tool === 'glob' && args.pattern) {\n return `glob ${ellipsize(String(args.pattern), 50)}`;\n }\n if (tool === 'grep' && args.pattern) {\n const mode = args.output_mode && args.output_mode !== 'content' ? ` (${args.output_mode})` : '';\n const scope = args.include ? ` in ${args.include}` : '';\n return `grep ${ellipsize(String(args.pattern), 40)}${scope}${mode}`;\n }\n\n const path = args.filePath || args.file_path || args.path;\n if (path) {\n const basename = String(path).split('/').pop() || String(path);\n return `${display} ${basename}`;\n }\n if (args.pattern) {\n return `${display} ${args.pattern}`;\n }\n if (args.command) {\n return `bash: ${String(args.command).slice(0, 40)}`;\n }\n } catch {\n // fall through to the bare tool name below\n }\n return display;\n}\n\n/**\n * Classify a tool outcome from its result text and success flag, so a surface\n * can render a refused `rm` or a denied `npm test` distinctly from a\n * successful run without re-deriving the refusal signatures per surface. The\n * refusal/denial strings come from `commandPolicy.ts`, `BaseFileSystem.ts`,\n * and the research-subagent wrapper — keeping the matching here means the\n * signatures stay in one place alongside the human summary.\n */\nexport function classifyOutcome(success: boolean, output: string): ResearchStepOutcome {\n if (!success) {\n if (/^Command refused:/.test(output) || /is not available\\. You are a read-only research subagent/.test(output)) return 'refused';\n if (/not approved/i.test(output) || /^Access denied:/.test(output) || /needs user approval/i.test(output)) return 'denied';\n return 'failure';\n }\n return 'success';\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAA6B;AAsI7B,SAAS,YAAY,OAA8B;AACjD,QAAM,OAAO,IAAI,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AAChD,QAAM,WAAW,oBAAI,IAAY;AACjC,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,QAAQ,CAAC,OAA8B;AAC3C,QAAI,KAAK,IAAI,EAAE,EAAG,QAAO;AACzB,QAAI,SAAS,IAAI,EAAE,EAAG,QAAO;AAC7B,aAAS,IAAI,EAAE;AACf,eAAW,OAAO,KAAK,IAAI,EAAE,GAAG,gBAAgB,CAAC,GAAG;AAClD,YAAM,MAAM,MAAM,GAAG;AACrB,UAAI,IAAK,QAAO;AAAA,IAClB;AACA,aAAS,OAAO,EAAE;AAClB,SAAK,IAAI,EAAE;AACX,WAAO;AAAA,EACT;AACA,aAAW,KAAK,OAAO;AACrB,UAAM,MAAM,MAAM,EAAE,EAAE;AACtB,QAAI,IAAK,QAAO,KAAK,IAAI,GAAG,GAAG,SAAS;AAAA,EAC1C;AACA,SAAO;AACT;AAuFO,SAAS,cAAc,OAAe,aAAwD;AACnG,MAAI,YAAY,SAAS,EAAG,QAAO,EAAE,IAAI,OAAO,OAAO,qCAAqC;AAC5F,QAAM,QAAQ,IAAI,IAAI,WAAW;AACjC,QAAM,UAAU,MAAM,OAAO,CAAC,MAAM,MAAM,IAAI,EAAE,EAAE,CAAC;AACnD,MAAI,QAAQ,SAAS,EAAG,QAAO,EAAE,IAAI,OAAO,OAAO,iDAAiD;AACpG,aAAW,KAAK,SAAS;AACvB,QAAI,EAAE,WAAW,cAAe,QAAO,EAAE,IAAI,OAAO,OAAO,IAAI,EAAE,KAAK,oCAAoC;AAC1G,QAAI,EAAE,WAAW,YAAa,QAAO,EAAE,IAAI,OAAO,OAAO,IAAI,EAAE,KAAK,sCAAsC;AAAA,EAC5G;AACA,QAAM,UAAU,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAC7D,QAAM,aAAa;AACnB,QAAM,MAAc,MACjB,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC,EAC9B,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,cAAc,EAAE,aAAa,IAAI,CAAC,MAAO,MAAM,IAAI,CAAC,IAAI,aAAa,CAAE,EAAE,OAAO,CAAC,MAAM,MAAM,EAAE,EAAE,EAAE,EAAE;AAC5H,QAAM,YAAY,CAAC,GAAG,IAAI,IAAI,QAAQ,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;AAClG,MAAI,KAAK,EAAE,GAAG,QAAQ,CAAC,GAAG,IAAI,YAAY,OAAO,iBAAiB,OAAO,QAAQ,CAAC,EAAE,OAAO,cAAc,UAAU,CAAC;AACpH,QAAM,QAAQ,YAAY,GAAG;AAC7B,MAAI,MAAO,QAAO,EAAE,IAAI,OAAO,OAAO,sDAAsD,KAAK,IAAI;AACrG,SAAO,EAAE,IAAI,KAAK;AACpB;AAiBO,SAAS,aAA6D,OAAY,QAAqB;AAC5G,SAAO,MAAM,OAAO,CAAC,MAAM,EAAE,OAAO,UAAU,EAAE,aAAa,SAAS,MAAM,CAAC;AAC/E;AAUO,SAAS,qBAA8D,OAAY,QAAsB;AAC9G,MAAI,CAAC,OAAQ,QAAO,CAAC,GAAG,KAAK;AAC7B,QAAM,SAAS,MAAM,KAAK,CAAC,MAAM,EAAE,OAAO,MAAM;AAChD,SAAO,SAAS,MAAM,OAAO,CAAC,MAAM,EAAE,QAAQ,OAAO,KAAK,IAAI,CAAC;AACjE;AAGO,SAAS,mBACd,OACA,QACA,cACiC;AACjC,QAAM,SAAS,MAAM,KAAK,CAAC,MAAM,EAAE,OAAO,MAAM;AAChD,MAAI,CAAC,OAAQ,QAAO,EAAE,IAAI,OAAO,OAAO,iBAAiB;AACzD,QAAM,UAAU,IAAI,IAAI,qBAAqB,OAAO,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAC5E,aAAW,OAAO,cAAc;AAC9B,QAAI,QAAQ,OAAQ,QAAO,EAAE,IAAI,OAAO,OAAO,IAAI,OAAO,KAAK,4BAA4B;AAC3F,UAAM,QAAQ,MAAM,KAAK,CAAC,MAAM,EAAE,OAAO,GAAG;AAC5C,QAAI,CAAC,MAAO,QAAO,EAAE,IAAI,OAAO,OAAO,uBAAuB,GAAG,IAAI;AACrE,QAAI,CAAC,QAAQ,IAAI,GAAG,GAAG;AACrB,aAAO,EAAE,IAAI,OAAO,OAAO,IAAI,OAAO,KAAK,uBAAuB,MAAM,KAAK,sCAAsC;AAAA,IACrH;AAAA,EACF;AACA,SAAO,EAAE,IAAI,KAAK;AACpB;AAGO,SAAS,aAAa,OAAe,QAAiD;AAC3F,QAAM,SAAS,MAAM,KAAK,CAAC,MAAM,EAAE,OAAO,MAAM;AAChD,MAAI,CAAC,OAAQ,QAAO,EAAE,IAAI,OAAO,OAAO,iBAAiB;AACzD,MAAI,OAAO,WAAW,cAAe,QAAO,EAAE,IAAI,OAAO,OAAO,IAAI,OAAO,KAAK,mCAAmC;AACnH,MAAI,OAAO,WAAW,YAAa,QAAO,EAAE,IAAI,OAAO,OAAO,IAAI,OAAO,KAAK,qCAAqC;AACnH,SAAO,EAAE,IAAI,KAAK;AACpB;;;ACzTA,SAAS,UAAU,MAAc,KAAqB;AACpD,QAAM,UAAU,KAAK,QAAQ,QAAQ,GAAG,EAAE,KAAK;AAC/C,SAAO,QAAQ,SAAS,MAAM,GAAG,QAAQ,MAAM,GAAG,GAAG,CAAC,WAAM;AAC9D;AAQO,SAAS,kBAAkB,MAAc,UAAkB,WAA4B;AAC5F,QAAM,UAAU,WAAW,KAAK,KAAK;AACrC,MAAI,SAAS,cAAc;AAGzB,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,QAAQ;AAChC,YAAM,OAAO,KAAK,QAAQ,KAAK,aAAa,KAAK,YAAY,KAAK,WAAW,KAAK,SAAS,KAAK,OAAO,KAAK;AAC5G,UAAI,KAAM,QAAO,GAAG,OAAO,IAAI,UAAU,OAAO,IAAI,GAAG,EAAE,CAAC;AAAA,IAC5D,QAAQ;AAAA,IAER;AACA,WAAO;AAAA,EACT;AAEA,MAAI,SAAS,wBAAwB;AACnC,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,QAAQ;AAChC,YAAM,SAAS,OAAO,KAAK,WAAW,WAAW,KAAK,SAAS;AAC/D,YAAM,UAAU,OAAO,QAAQ,QAAQ,GAAG,EAAE,KAAK;AACjD,aAAO,WAAW,QAAQ,MAAM,GAAG,EAAE,CAAC,GAAG,QAAQ,SAAS,KAAK,WAAM,EAAE;AAAA,IACzE,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI;AACF,UAAM,OAAO,KAAK,MAAM,QAAQ;AAIhC,QAAI,SAAS,iBAAiB,KAAK,QAAQ;AACzC,aAAO,eAAe,KAAK,MAAM,GAAG,KAAK,WAAW,KAAK,KAAK,QAAQ,MAAM,EAAE;AAAA,IAChF;AACA,QAAI,SAAS,gBAAgB,KAAK,OAAO;AACvC,aAAO,eAAe,UAAU,OAAO,KAAK,KAAK,GAAG,EAAE,CAAC;AAAA,IACzD;AACA,QAAI,SAAS,WAAW,KAAK,KAAK;AAChC,aAAO,SAAS,UAAU,OAAO,KAAK,GAAG,GAAG,EAAE,CAAC;AAAA,IACjD;AAGA,QAAI,SAAS,UAAU,KAAK,SAAS;AACnC,aAAO,QAAQ,UAAU,OAAO,KAAK,OAAO,GAAG,EAAE,CAAC;AAAA,IACpD;AACA,QAAI,SAAS,UAAU,KAAK,SAAS;AACnC,YAAM,OAAO,KAAK,eAAe,KAAK,gBAAgB,YAAY,KAAK,KAAK,WAAW,MAAM;AAC7F,YAAM,QAAQ,KAAK,UAAU,OAAO,KAAK,OAAO,KAAK;AACrD,aAAO,QAAQ,UAAU,OAAO,KAAK,OAAO,GAAG,EAAE,CAAC,GAAG,KAAK,GAAG,IAAI;AAAA,IACnE;AAEA,UAAM,OAAO,KAAK,YAAY,KAAK,aAAa,KAAK;AACrD,QAAI,MAAM;AACR,YAAM,WAAW,OAAO,IAAI,EAAE,MAAM,GAAG,EAAE,IAAI,KAAK,OAAO,IAAI;AAC7D,aAAO,GAAG,OAAO,IAAI,QAAQ;AAAA,IAC/B;AACA,QAAI,KAAK,SAAS;AAChB,aAAO,GAAG,OAAO,IAAI,KAAK,OAAO;AAAA,IACnC;AACA,QAAI,KAAK,SAAS;AAChB,aAAO,SAAS,OAAO,KAAK,OAAO,EAAE,MAAM,GAAG,EAAE,CAAC;AAAA,IACnD;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;","names":[]}
@@ -5,8 +5,9 @@ import {
5
5
  dependencyCandidates,
6
6
  dependentsOf,
7
7
  summarizeToolCall
8
- } from "./chunk-4KIWPW4K.mjs";
9
- import "./chunk-SRGQBGI5.mjs";
8
+ } from "./chunk-DNNZGH3I.mjs";
9
+ import "./chunk-N6HTBUKG.mjs";
10
+ import "./chunk-HPNUFBIT.mjs";
10
11
  export {
11
12
  canMergeTasks,
12
13
  canSetDependencies,
@@ -1,5 +1,5 @@
1
- import { q as ITerminalSession, m as IConfig, n as IFileSystem } from './ITerminalRunner-1NAv8wCC.mjs';
2
- import './Task-1NwmUImI.mjs';
1
+ import { q as ITerminalSession, m as IConfig, n as IFileSystem } from './ITerminalRunner-BTaKZ659.mjs';
2
+ import './Task-cGrfYRXK.mjs';
3
3
 
4
4
  declare function fakeConfig(overrides?: Partial<IConfig>): IConfig;
5
5
  /**
package/dist/testing.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { q as ITerminalSession, m as IConfig, n as IFileSystem } from './ITerminalRunner-CbE6gKLI.js';
2
- import './Task-1NwmUImI.js';
1
+ import { q as ITerminalSession, m as IConfig, n as IFileSystem } from './ITerminalRunner-DVwORcV9.js';
2
+ import './Task-cGrfYRXK.js';
3
3
 
4
4
  declare function fakeConfig(overrides?: Partial<IConfig>): IConfig;
5
5
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ordewell/core",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "description": "Ordewell core — planner, orchestrator, runners and verification engine.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Alessandro Costanzo Ciano",
@@ -36,11 +36,17 @@
36
36
  "import": "./dist/index.mjs",
37
37
  "require": "./dist/index.js"
38
38
  },
39
+ "./package.json": "./package.json",
39
40
  "./parsing": {
40
41
  "types": "./dist/parsing.d.ts",
41
42
  "import": "./dist/parsing.mjs",
42
43
  "require": "./dist/parsing.js"
43
44
  },
45
+ "./order-labels": {
46
+ "types": "./dist/order-labels.d.ts",
47
+ "import": "./dist/order-labels.mjs",
48
+ "require": "./dist/order-labels.js"
49
+ },
44
50
  "./plan-utils": {
45
51
  "types": "./dist/plan-utils.d.ts",
46
52
  "import": "./dist/plan-utils.mjs",
@@ -57,6 +63,9 @@
57
63
  "parsing": [
58
64
  "./dist/parsing.d.ts"
59
65
  ],
66
+ "order-labels": [
67
+ "./dist/order-labels.d.ts"
68
+ ],
60
69
  "plan-utils": [
61
70
  "./dist/plan-utils.d.ts"
62
71
  ],
@@ -67,11 +76,12 @@
67
76
  },
68
77
  "files": [
69
78
  "dist",
79
+ "skills",
70
80
  "README.md",
71
81
  "LICENSE"
72
82
  ],
73
83
  "scripts": {
74
- "build": "tsup src/index.ts src/parsing.ts src/testing.ts src/plan-utils.ts --dts --sourcemap --format cjs,esm --clean --tsconfig tsconfig.json",
84
+ "build": "tsup src/index.ts src/parsing.ts src/testing.ts src/plan-utils.ts src/order-labels.ts --dts --sourcemap --format cjs,esm --clean --tsconfig tsconfig.json",
75
85
  "prepublishOnly": "npm run build",
76
86
  "watch": "tsup src/index.ts --watch --sourcemap",
77
87
  "test": "vitest run",
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: grilling
3
+ description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it.
8
+
9
+ Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled: the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round.
10
+
11
+ Format a round like so:
12
+
13
+
14
+ ❓ **Q1** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
15
+
16
+ ➡️ <your recommended answer>
17
+
18
+ ---
19
+
20
+ ❓ **Q2** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
21
+
22
+ ➡️ <your recommended answer>
23
+
24
+
25
+ Each round the user answers reshapes the tree: settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one.
26
+
27
+ Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment, explore the workspace yourself with your own read-only tools, and, where a research subagent is available to you, delegate exploration to it instead of reading everything inline. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for it to report back; ask the rest of the frontier now. The _decisions_ are the user's — put each to them and wait.
28
+
29
+ The goal is a shared understanding sharp enough to decompose into independently demoable slices: questions about slice boundaries, dependencies between slices, and what's out of scope are legitimate branches of the design tree, not distractions from it.
30
+
31
+ The session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. An empty frontier doesn't end the session — it means you now propose a prose outline of vertical tracer-bullet slices, the same outline-before-JSON step you'd reach at the end of any research phase. Do not emit the task plan JSON until the user has confirmed that outline.
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: to-spec
3
+ description: Turn the current conversation into a product spec and save it to the project.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ This skill takes the current conversation context and codebase understanding and produces a spec document. Do NOT interview the user — just synthesize what you already know.
8
+
9
+ ## Process
10
+
11
+ 1. Explore the repo to understand the current state of the codebase, if you haven't already. Use the project's domain glossary vocabulary throughout the spec.
12
+
13
+ 2. Sketch out the seams at which you're going to test the feature. Existing seams should be preferred to new ones. Use the highest seam possible. The fewer seams across the codebase, the better — the ideal number is one.
14
+
15
+ Check with the user that these seams match their expectations.
16
+
17
+ 3. Write the spec using the template below, then save it to `.ordewell/spec.md` in the project's working directory.
18
+
19
+ <spec-template>
20
+
21
+ ## Problem Statement
22
+
23
+ The problem that the user is facing, from the user's perspective.
24
+
25
+ ## Solution
26
+
27
+ The solution to the problem, from the user's perspective.
28
+
29
+ ## User Stories
30
+
31
+ A long, numbered list of user stories. Each user story should be in the format of:
32
+
33
+ 1. As an <actor>, I want <feature>, so that <benefit>
34
+
35
+ This list of user stories should be extremely extensive and cover all aspects of the feature.
36
+
37
+ ## Implementation Decisions
38
+
39
+ A list of implementation decisions that were made. This can include:
40
+ - The modules that will be built/modified
41
+ - The interfaces of those modules that will be modified
42
+ - Technical clarifications from the developer
43
+ - Architectural decisions
44
+ - Schema changes
45
+ - API contracts
46
+
47
+ Do NOT include specific file paths or code snippets. They end up being outdated very quickly.
48
+
49
+ ## Testing Decisions
50
+
51
+ A list of testing decisions that were made. Include:
52
+ - A description of what makes a good test (only test external behavior, not implementation details)
53
+ - Which modules will be tested
54
+ - Prior art for the tests (i.e. similar types of tests in the codebase)
55
+
56
+ ## Out of Scope
57
+
58
+ A description of the things that are out of scope for this spec.
59
+
60
+ ## Further Notes
61
+
62
+ Any further notes about the feature.
63
+
64
+ </spec-template>