@yuaone/core 0.1.0

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 (235) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +15 -0
  3. package/dist/__tests__/context-manager.test.d.ts +6 -0
  4. package/dist/__tests__/context-manager.test.d.ts.map +1 -0
  5. package/dist/__tests__/context-manager.test.js +220 -0
  6. package/dist/__tests__/context-manager.test.js.map +1 -0
  7. package/dist/__tests__/governor.test.d.ts +6 -0
  8. package/dist/__tests__/governor.test.d.ts.map +1 -0
  9. package/dist/__tests__/governor.test.js +210 -0
  10. package/dist/__tests__/governor.test.js.map +1 -0
  11. package/dist/__tests__/model-router.test.d.ts +6 -0
  12. package/dist/__tests__/model-router.test.d.ts.map +1 -0
  13. package/dist/__tests__/model-router.test.js +329 -0
  14. package/dist/__tests__/model-router.test.js.map +1 -0
  15. package/dist/agent-logger.d.ts +384 -0
  16. package/dist/agent-logger.d.ts.map +1 -0
  17. package/dist/agent-logger.js +820 -0
  18. package/dist/agent-logger.js.map +1 -0
  19. package/dist/agent-loop.d.ts +163 -0
  20. package/dist/agent-loop.d.ts.map +1 -0
  21. package/dist/agent-loop.js +609 -0
  22. package/dist/agent-loop.js.map +1 -0
  23. package/dist/agent-modes.d.ts +85 -0
  24. package/dist/agent-modes.d.ts.map +1 -0
  25. package/dist/agent-modes.js +418 -0
  26. package/dist/agent-modes.js.map +1 -0
  27. package/dist/approval.d.ts +137 -0
  28. package/dist/approval.d.ts.map +1 -0
  29. package/dist/approval.js +299 -0
  30. package/dist/approval.js.map +1 -0
  31. package/dist/async-completion-queue.d.ts +56 -0
  32. package/dist/async-completion-queue.d.ts.map +1 -0
  33. package/dist/async-completion-queue.js +77 -0
  34. package/dist/async-completion-queue.js.map +1 -0
  35. package/dist/auto-fix.d.ts +174 -0
  36. package/dist/auto-fix.d.ts.map +1 -0
  37. package/dist/auto-fix.js +319 -0
  38. package/dist/auto-fix.js.map +1 -0
  39. package/dist/codebase-context.d.ts +396 -0
  40. package/dist/codebase-context.d.ts.map +1 -0
  41. package/dist/codebase-context.js +1260 -0
  42. package/dist/codebase-context.js.map +1 -0
  43. package/dist/conflict-resolver.d.ts +191 -0
  44. package/dist/conflict-resolver.d.ts.map +1 -0
  45. package/dist/conflict-resolver.js +524 -0
  46. package/dist/conflict-resolver.js.map +1 -0
  47. package/dist/constants.d.ts +52 -0
  48. package/dist/constants.d.ts.map +1 -0
  49. package/dist/constants.js +141 -0
  50. package/dist/constants.js.map +1 -0
  51. package/dist/context-budget.d.ts +435 -0
  52. package/dist/context-budget.d.ts.map +1 -0
  53. package/dist/context-budget.js +903 -0
  54. package/dist/context-budget.js.map +1 -0
  55. package/dist/context-compressor.d.ts +143 -0
  56. package/dist/context-compressor.d.ts.map +1 -0
  57. package/dist/context-compressor.js +511 -0
  58. package/dist/context-compressor.js.map +1 -0
  59. package/dist/context-manager.d.ts +112 -0
  60. package/dist/context-manager.d.ts.map +1 -0
  61. package/dist/context-manager.js +247 -0
  62. package/dist/context-manager.js.map +1 -0
  63. package/dist/continuous-reflection.d.ts +267 -0
  64. package/dist/continuous-reflection.d.ts.map +1 -0
  65. package/dist/continuous-reflection.js +338 -0
  66. package/dist/continuous-reflection.js.map +1 -0
  67. package/dist/cross-file-refactor.d.ts +352 -0
  68. package/dist/cross-file-refactor.d.ts.map +1 -0
  69. package/dist/cross-file-refactor.js +1544 -0
  70. package/dist/cross-file-refactor.js.map +1 -0
  71. package/dist/dag-orchestrator.d.ts +138 -0
  72. package/dist/dag-orchestrator.d.ts.map +1 -0
  73. package/dist/dag-orchestrator.js +379 -0
  74. package/dist/dag-orchestrator.js.map +1 -0
  75. package/dist/debate-orchestrator.d.ts +301 -0
  76. package/dist/debate-orchestrator.d.ts.map +1 -0
  77. package/dist/debate-orchestrator.js +719 -0
  78. package/dist/debate-orchestrator.js.map +1 -0
  79. package/dist/dependency-analyzer.d.ts +113 -0
  80. package/dist/dependency-analyzer.d.ts.map +1 -0
  81. package/dist/dependency-analyzer.js +444 -0
  82. package/dist/dependency-analyzer.js.map +1 -0
  83. package/dist/design-loop.d.ts +59 -0
  84. package/dist/design-loop.d.ts.map +1 -0
  85. package/dist/design-loop.js +344 -0
  86. package/dist/design-loop.js.map +1 -0
  87. package/dist/doc-intelligence.d.ts +383 -0
  88. package/dist/doc-intelligence.d.ts.map +1 -0
  89. package/dist/doc-intelligence.js +1307 -0
  90. package/dist/doc-intelligence.js.map +1 -0
  91. package/dist/dynamic-role-generator.d.ts +76 -0
  92. package/dist/dynamic-role-generator.d.ts.map +1 -0
  93. package/dist/dynamic-role-generator.js +194 -0
  94. package/dist/dynamic-role-generator.js.map +1 -0
  95. package/dist/errors.d.ts +69 -0
  96. package/dist/errors.d.ts.map +1 -0
  97. package/dist/errors.js +102 -0
  98. package/dist/errors.js.map +1 -0
  99. package/dist/event-bus.d.ts +159 -0
  100. package/dist/event-bus.d.ts.map +1 -0
  101. package/dist/event-bus.js +305 -0
  102. package/dist/event-bus.js.map +1 -0
  103. package/dist/execution-engine.d.ts +425 -0
  104. package/dist/execution-engine.d.ts.map +1 -0
  105. package/dist/execution-engine.js +1555 -0
  106. package/dist/execution-engine.js.map +1 -0
  107. package/dist/git-intelligence.d.ts +306 -0
  108. package/dist/git-intelligence.d.ts.map +1 -0
  109. package/dist/git-intelligence.js +1099 -0
  110. package/dist/git-intelligence.js.map +1 -0
  111. package/dist/governor.d.ts +77 -0
  112. package/dist/governor.d.ts.map +1 -0
  113. package/dist/governor.js +161 -0
  114. package/dist/governor.js.map +1 -0
  115. package/dist/hierarchical-planner.d.ts +313 -0
  116. package/dist/hierarchical-planner.d.ts.map +1 -0
  117. package/dist/hierarchical-planner.js +981 -0
  118. package/dist/hierarchical-planner.js.map +1 -0
  119. package/dist/index.d.ts +121 -0
  120. package/dist/index.d.ts.map +1 -0
  121. package/dist/index.js +123 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/intent-inference.d.ts +103 -0
  124. package/dist/intent-inference.d.ts.map +1 -0
  125. package/dist/intent-inference.js +605 -0
  126. package/dist/intent-inference.js.map +1 -0
  127. package/dist/interrupt-manager.d.ts +143 -0
  128. package/dist/interrupt-manager.d.ts.map +1 -0
  129. package/dist/interrupt-manager.js +196 -0
  130. package/dist/interrupt-manager.js.map +1 -0
  131. package/dist/kernel.d.ts +564 -0
  132. package/dist/kernel.d.ts.map +1 -0
  133. package/dist/kernel.js +1419 -0
  134. package/dist/kernel.js.map +1 -0
  135. package/dist/language-support.d.ts +232 -0
  136. package/dist/language-support.d.ts.map +1 -0
  137. package/dist/language-support.js +1134 -0
  138. package/dist/language-support.js.map +1 -0
  139. package/dist/llm-client.d.ts +82 -0
  140. package/dist/llm-client.d.ts.map +1 -0
  141. package/dist/llm-client.js +475 -0
  142. package/dist/llm-client.js.map +1 -0
  143. package/dist/mcp-client.d.ts +232 -0
  144. package/dist/mcp-client.d.ts.map +1 -0
  145. package/dist/mcp-client.js +718 -0
  146. package/dist/mcp-client.js.map +1 -0
  147. package/dist/memory-manager.d.ts +200 -0
  148. package/dist/memory-manager.d.ts.map +1 -0
  149. package/dist/memory-manager.js +568 -0
  150. package/dist/memory-manager.js.map +1 -0
  151. package/dist/memory.d.ts +87 -0
  152. package/dist/memory.d.ts.map +1 -0
  153. package/dist/memory.js +341 -0
  154. package/dist/memory.js.map +1 -0
  155. package/dist/model-router.d.ts +245 -0
  156. package/dist/model-router.d.ts.map +1 -0
  157. package/dist/model-router.js +632 -0
  158. package/dist/model-router.js.map +1 -0
  159. package/dist/parallel-executor.d.ts +125 -0
  160. package/dist/parallel-executor.d.ts.map +1 -0
  161. package/dist/parallel-executor.js +201 -0
  162. package/dist/parallel-executor.js.map +1 -0
  163. package/dist/perf-optimizer.d.ts +212 -0
  164. package/dist/perf-optimizer.d.ts.map +1 -0
  165. package/dist/perf-optimizer.js +721 -0
  166. package/dist/perf-optimizer.js.map +1 -0
  167. package/dist/persona.d.ts +305 -0
  168. package/dist/persona.d.ts.map +1 -0
  169. package/dist/persona.js +887 -0
  170. package/dist/persona.js.map +1 -0
  171. package/dist/planner.d.ts +70 -0
  172. package/dist/planner.d.ts.map +1 -0
  173. package/dist/planner.js +264 -0
  174. package/dist/planner.js.map +1 -0
  175. package/dist/qa-pipeline.d.ts +365 -0
  176. package/dist/qa-pipeline.d.ts.map +1 -0
  177. package/dist/qa-pipeline.js +1352 -0
  178. package/dist/qa-pipeline.js.map +1 -0
  179. package/dist/reasoning-adapter.d.ts +116 -0
  180. package/dist/reasoning-adapter.d.ts.map +1 -0
  181. package/dist/reasoning-adapter.js +187 -0
  182. package/dist/reasoning-adapter.js.map +1 -0
  183. package/dist/role-registry.d.ts +55 -0
  184. package/dist/role-registry.d.ts.map +1 -0
  185. package/dist/role-registry.js +192 -0
  186. package/dist/role-registry.js.map +1 -0
  187. package/dist/sandbox-tiers.d.ts +327 -0
  188. package/dist/sandbox-tiers.d.ts.map +1 -0
  189. package/dist/sandbox-tiers.js +928 -0
  190. package/dist/sandbox-tiers.js.map +1 -0
  191. package/dist/security-scanner.d.ts +222 -0
  192. package/dist/security-scanner.d.ts.map +1 -0
  193. package/dist/security-scanner.js +1129 -0
  194. package/dist/security-scanner.js.map +1 -0
  195. package/dist/security.d.ts +93 -0
  196. package/dist/security.d.ts.map +1 -0
  197. package/dist/security.js +393 -0
  198. package/dist/security.js.map +1 -0
  199. package/dist/self-reflection.d.ts +397 -0
  200. package/dist/self-reflection.d.ts.map +1 -0
  201. package/dist/self-reflection.js +908 -0
  202. package/dist/self-reflection.js.map +1 -0
  203. package/dist/session-persistence.d.ts +191 -0
  204. package/dist/session-persistence.d.ts.map +1 -0
  205. package/dist/session-persistence.js +395 -0
  206. package/dist/session-persistence.js.map +1 -0
  207. package/dist/speculative-executor.d.ts +210 -0
  208. package/dist/speculative-executor.d.ts.map +1 -0
  209. package/dist/speculative-executor.js +618 -0
  210. package/dist/speculative-executor.js.map +1 -0
  211. package/dist/state-machine.d.ts +289 -0
  212. package/dist/state-machine.d.ts.map +1 -0
  213. package/dist/state-machine.js +695 -0
  214. package/dist/state-machine.js.map +1 -0
  215. package/dist/sub-agent.d.ts +177 -0
  216. package/dist/sub-agent.d.ts.map +1 -0
  217. package/dist/sub-agent.js +303 -0
  218. package/dist/sub-agent.js.map +1 -0
  219. package/dist/system-prompt.d.ts +26 -0
  220. package/dist/system-prompt.d.ts.map +1 -0
  221. package/dist/system-prompt.js +84 -0
  222. package/dist/system-prompt.js.map +1 -0
  223. package/dist/test-intelligence.d.ts +439 -0
  224. package/dist/test-intelligence.d.ts.map +1 -0
  225. package/dist/test-intelligence.js +1165 -0
  226. package/dist/test-intelligence.js.map +1 -0
  227. package/dist/types.d.ts +632 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +6 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/vector-index.d.ts +314 -0
  232. package/dist/vector-index.d.ts.map +1 -0
  233. package/dist/vector-index.js +618 -0
  234. package/dist/vector-index.js.map +1 -0
  235. package/package.json +41 -0
@@ -0,0 +1,981 @@
1
+ /**
2
+ * @module hierarchical-planner
3
+ * @description 3-Level Hierarchical Planner — Strategic / Tactical / Operational.
4
+ *
5
+ * 사용자의 고수준 목표를 3단계 계획으로 분해한다:
6
+ * - L1 Strategic: 목표 → 서브골 분해 (Flagship 모델)
7
+ * - L2 Tactical: 서브골 → 파일별 태스크 (Premium 모델)
8
+ * - L3 Operational: 태스크 → 구체적 도구 호출 (Standard 모델)
9
+ *
10
+ * 기존 Planner(단일 레벨)와 호환되는 toExecutionPlan() 변환을 지원한다.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const planner = new HierarchicalPlanner({ projectPath: "/app" });
15
+ * const plan = await planner.createHierarchicalPlan("Add auth to the API", llmClient);
16
+ * const execPlan = planner.toExecutionPlan(plan);
17
+ * ```
18
+ */
19
+ import { randomUUID } from "node:crypto";
20
+ import { readFile, readdir } from "node:fs/promises";
21
+ import { join } from "node:path";
22
+ // ─── Internal Defaults ───
23
+ const DEFAULTS = {
24
+ strategicModel: "flagship",
25
+ tacticalModel: "premium",
26
+ operationalModel: "standard",
27
+ maxSubGoals: 10,
28
+ maxTasksPerGoal: 20,
29
+ maxActionsPerTask: 50,
30
+ enableOperationalPrePlanning: false,
31
+ };
32
+ /** Tokens per iteration estimate for budget calculation */
33
+ const TOKENS_PER_ITERATION = 3_000;
34
+ /** High-risk file patterns that trigger elevated risk assessment */
35
+ const HIGH_RISK_PATTERNS = [
36
+ /\.env/i,
37
+ /secret/i,
38
+ /credential/i,
39
+ /auth/i,
40
+ /password/i,
41
+ /token/i,
42
+ /config\.(ts|js|json)$/i,
43
+ /package\.json$/,
44
+ /tsconfig/i,
45
+ /docker/i,
46
+ /\.ya?ml$/i,
47
+ /migration/i,
48
+ /schema\.(prisma|sql)$/i,
49
+ ];
50
+ /** Destructive operations that elevate risk */
51
+ const DESTRUCTIVE_OPS = [
52
+ "file_write",
53
+ "shell_exec",
54
+ "git_ops",
55
+ ];
56
+ // ─── HierarchicalPlanner ───
57
+ /**
58
+ * HierarchicalPlanner — 3-level planning hierarchy for the YUAN coding agent.
59
+ *
60
+ * Decomposes a high-level user goal into:
61
+ * 1. **Strategic** — what sub-goals to pursue, risk assessment, capability analysis
62
+ * 2. **Tactical** — which files to touch, which tools to use, dependency ordering
63
+ * 3. **Operational** — exact tool calls with pre-planned arguments
64
+ *
65
+ * Supports re-planning when execution encounters errors or new information.
66
+ * Backward-compatible with the existing `ExecutionPlan` format via `toExecutionPlan()`.
67
+ */
68
+ export class HierarchicalPlanner {
69
+ config;
70
+ projectContext;
71
+ constructor(config) {
72
+ this.config = {
73
+ projectPath: config.projectPath,
74
+ strategicModel: config.strategicModel ?? DEFAULTS.strategicModel,
75
+ tacticalModel: config.tacticalModel ?? DEFAULTS.tacticalModel,
76
+ operationalModel: config.operationalModel ?? DEFAULTS.operationalModel,
77
+ maxSubGoals: config.maxSubGoals ?? DEFAULTS.maxSubGoals,
78
+ maxTasksPerGoal: config.maxTasksPerGoal ?? DEFAULTS.maxTasksPerGoal,
79
+ maxActionsPerTask: config.maxActionsPerTask ?? DEFAULTS.maxActionsPerTask,
80
+ enableOperationalPrePlanning: config.enableOperationalPrePlanning ?? DEFAULTS.enableOperationalPrePlanning,
81
+ };
82
+ this.projectContext = "";
83
+ }
84
+ // ─── Public API ───
85
+ /**
86
+ * L1: Strategic Planning — decompose a high-level goal into sub-goals.
87
+ * Uses a flagship model for best reasoning quality.
88
+ *
89
+ * @param goal - The user's high-level goal
90
+ * @param llmClient - BYOK LLM client for the strategic model
91
+ * @param projectContext - Optional pre-gathered project context
92
+ * @returns Strategic goal with sub-goals, complexity, and risk assessment
93
+ */
94
+ async planStrategic(goal, llmClient, projectContext) {
95
+ const ctx = projectContext ?? await this.gatherProjectContext();
96
+ this.projectContext = ctx;
97
+ const prompt = this.buildStrategicPrompt(goal, ctx);
98
+ const messages = [
99
+ { role: "system", content: prompt },
100
+ { role: "user", content: goal },
101
+ ];
102
+ const response = await llmClient.chat(messages);
103
+ const parsed = this.parseStrategicResponse(response.content ?? "", goal);
104
+ // Enforce sub-goal limit
105
+ if (parsed.subGoals.length > this.config.maxSubGoals) {
106
+ parsed.subGoals = parsed.subGoals.slice(0, this.config.maxSubGoals);
107
+ }
108
+ // Enrich with risk assessment if not provided by LLM
109
+ if (!parsed.riskAssessment || !parsed.riskAssessment.level) {
110
+ const allFiles = parsed.subGoals.flatMap((sg) => sg.targetFiles);
111
+ parsed.riskAssessment = this.assessRisk(goal, allFiles);
112
+ }
113
+ return parsed;
114
+ }
115
+ /**
116
+ * L2: Tactical Planning — plan file-level tasks for each sub-goal.
117
+ * Uses a premium model for good coding analysis.
118
+ *
119
+ * @param strategicGoal - The strategic goal to decompose
120
+ * @param llmClient - BYOK LLM client for the tactical model
121
+ * @param fileContext - Optional map of file path → content for context
122
+ * @returns Array of tactical tasks with dependencies and ordering
123
+ */
124
+ async planTactical(strategicGoal, llmClient, fileContext) {
125
+ const allTasks = [];
126
+ for (const subGoal of strategicGoal.subGoals) {
127
+ const prompt = this.buildTacticalPrompt(subGoal, fileContext);
128
+ const messages = [
129
+ { role: "system", content: prompt },
130
+ {
131
+ role: "user",
132
+ content: `Plan implementation for: ${subGoal.description}\nTarget files: ${subGoal.targetFiles.join(", ")}`,
133
+ },
134
+ ];
135
+ const response = await llmClient.chat(messages);
136
+ const tasks = this.parseTacticalResponse(response.content ?? "", strategicGoal.id, subGoal);
137
+ // Enforce task limit per goal
138
+ const limited = tasks.slice(0, this.config.maxTasksPerGoal);
139
+ allTasks.push(...limited);
140
+ }
141
+ // Assign global ordering based on dependencies
142
+ return this.buildDependencyChain(allTasks);
143
+ }
144
+ /**
145
+ * L3: Operational Planning — plan exact tool calls for a task.
146
+ * Uses a standard model for cost efficiency on detailed step planning.
147
+ *
148
+ * @param task - The tactical task to detail
149
+ * @param llmClient - BYOK LLM client for the operational model
150
+ * @param currentState - Optional current execution state for context
151
+ * @returns Array of operational actions with tool inputs
152
+ */
153
+ async planOperational(task, llmClient, currentState) {
154
+ const prompt = this.buildOperationalPrompt(task, currentState);
155
+ const messages = [
156
+ { role: "system", content: prompt },
157
+ {
158
+ role: "user",
159
+ content: `Plan exact actions for: ${task.description}\nTools: ${task.toolStrategy.join(", ")}\nFiles: ${task.targetFiles.join(", ")}`,
160
+ },
161
+ ];
162
+ const response = await llmClient.chat(messages);
163
+ const actions = this.parseOperationalResponse(response.content ?? "", task.id);
164
+ // Enforce action limit
165
+ return actions.slice(0, this.config.maxActionsPerTask);
166
+ }
167
+ /**
168
+ * Create a full hierarchical plan from a user goal.
169
+ * Runs L1 → L2 sequentially; L3 is conditional on config.
170
+ *
171
+ * @param goal - The user's high-level goal
172
+ * @param llmClient - BYOK LLM client (used for all levels)
173
+ * @returns Complete hierarchical plan
174
+ */
175
+ async createHierarchicalPlan(goal, llmClient) {
176
+ // L1: Strategic
177
+ const strategic = await this.planStrategic(goal, llmClient);
178
+ // L2: Tactical
179
+ const tactical = await this.planTactical(strategic, llmClient);
180
+ // L3: Operational (optional pre-planning)
181
+ const operational = new Map();
182
+ if (this.config.enableOperationalPrePlanning) {
183
+ for (const task of tactical) {
184
+ const actions = await this.planOperational(task, llmClient);
185
+ operational.set(task.id, actions);
186
+ }
187
+ }
188
+ // Compute metadata
189
+ const parallelizableGroups = this.findParallelGroups(tactical);
190
+ const criticalPath = this.findCriticalPath(tactical);
191
+ const totalEstimatedIterations = tactical.reduce((sum, t) => sum + t.estimatedIterations, 0);
192
+ const plan = {
193
+ id: randomUUID(),
194
+ goal,
195
+ createdAt: Date.now(),
196
+ strategic,
197
+ tactical,
198
+ operational,
199
+ totalEstimatedIterations,
200
+ parallelizableGroups,
201
+ criticalPath,
202
+ estimatedTokenBudget: 0,
203
+ };
204
+ plan.estimatedTokenBudget = this.estimateTokenBudget(plan);
205
+ return plan;
206
+ }
207
+ /**
208
+ * Re-plan after encountering an error, new information, or other trigger.
209
+ * Decides the best recovery strategy and returns modified tasks.
210
+ *
211
+ * @param currentPlan - The current hierarchical plan
212
+ * @param trigger - What triggered the re-plan
213
+ * @param llmClient - BYOK LLM client for re-planning
214
+ * @returns Re-plan result with strategy and modified tasks
215
+ */
216
+ async replan(currentPlan, trigger, llmClient) {
217
+ const prompt = this.buildReplanPrompt(currentPlan, trigger);
218
+ const messages = [
219
+ { role: "system", content: prompt },
220
+ {
221
+ role: "user",
222
+ content: `Re-plan needed: ${trigger.description}\nType: ${trigger.type}\nSeverity: ${trigger.severity}\nAffected tasks: ${trigger.affectedTaskIds.join(", ")}`,
223
+ },
224
+ ];
225
+ const response = await llmClient.chat(messages);
226
+ return this.parseReplanResponse(response.content ?? "", currentPlan, trigger);
227
+ }
228
+ /**
229
+ * Convert a HierarchicalPlan to the existing ExecutionPlan format.
230
+ * Provides backward compatibility with code that uses the single-level Planner.
231
+ *
232
+ * @param plan - Hierarchical plan to convert
233
+ * @returns ExecutionPlan compatible with existing agent infrastructure
234
+ */
235
+ toExecutionPlan(plan) {
236
+ const steps = plan.tactical.map((task) => ({
237
+ id: task.id,
238
+ goal: task.description,
239
+ targetFiles: task.targetFiles,
240
+ readFiles: task.readFiles,
241
+ tools: task.toolStrategy,
242
+ estimatedIterations: task.estimatedIterations,
243
+ dependsOn: task.dependsOn,
244
+ }));
245
+ return {
246
+ goal: plan.goal,
247
+ steps,
248
+ estimatedTokens: plan.estimatedTokenBudget,
249
+ };
250
+ }
251
+ /**
252
+ * Find groups of tasks that can execute in parallel.
253
+ * Tasks with no mutual dependencies can run concurrently.
254
+ *
255
+ * @param tasks - Tactical tasks to analyze
256
+ * @returns Array of groups, each group is an array of task IDs
257
+ */
258
+ findParallelGroups(tasks) {
259
+ if (tasks.length === 0)
260
+ return [];
261
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
262
+ const completed = new Set();
263
+ const groups = [];
264
+ // Iterative topological grouping
265
+ let remaining = tasks.length;
266
+ const maxIterations = tasks.length + 1; // safety bound
267
+ let iteration = 0;
268
+ while (remaining > 0 && iteration < maxIterations) {
269
+ iteration++;
270
+ const group = [];
271
+ for (const task of tasks) {
272
+ if (completed.has(task.id))
273
+ continue;
274
+ // Check if all dependencies are completed
275
+ const depsReady = task.dependsOn.every((dep) => completed.has(dep) || !taskMap.has(dep));
276
+ if (depsReady) {
277
+ group.push(task.id);
278
+ }
279
+ }
280
+ if (group.length === 0) {
281
+ // Circular dependency detected — break remaining tasks into single group
282
+ const stuck = tasks
283
+ .filter((t) => !completed.has(t.id))
284
+ .map((t) => t.id);
285
+ groups.push(stuck);
286
+ break;
287
+ }
288
+ groups.push(group);
289
+ for (const id of group) {
290
+ completed.add(id);
291
+ remaining--;
292
+ }
293
+ }
294
+ return groups;
295
+ }
296
+ /**
297
+ * Find the critical path — the longest sequential dependency chain.
298
+ * Uses DFS with memoization for path length calculation.
299
+ *
300
+ * @param tasks - Tactical tasks to analyze
301
+ * @returns Array of task IDs forming the critical path
302
+ */
303
+ findCriticalPath(tasks) {
304
+ if (tasks.length === 0)
305
+ return [];
306
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
307
+ const memo = new Map();
308
+ /**
309
+ * Recursively find the longest path starting from a task.
310
+ */
311
+ const longestFrom = (taskId, visited) => {
312
+ if (memo.has(taskId))
313
+ return memo.get(taskId);
314
+ if (visited.has(taskId))
315
+ return [taskId]; // cycle — stop
316
+ visited.add(taskId);
317
+ const task = taskMap.get(taskId);
318
+ if (!task)
319
+ return [taskId];
320
+ // Find dependents (tasks that depend on this one)
321
+ const dependents = tasks.filter((t) => t.dependsOn.includes(taskId));
322
+ if (dependents.length === 0) {
323
+ const path = [taskId];
324
+ memo.set(taskId, path);
325
+ visited.delete(taskId);
326
+ return path;
327
+ }
328
+ let longestChild = [];
329
+ for (const dep of dependents) {
330
+ const childPath = longestFrom(dep.id, visited);
331
+ if (childPath.length > longestChild.length) {
332
+ longestChild = childPath;
333
+ }
334
+ }
335
+ const path = [taskId, ...longestChild];
336
+ memo.set(taskId, path);
337
+ visited.delete(taskId);
338
+ return path;
339
+ };
340
+ // Find roots (tasks with no dependencies or only external deps)
341
+ const roots = tasks.filter((t) => t.dependsOn.length === 0 || t.dependsOn.every((d) => !taskMap.has(d)));
342
+ let criticalPath = [];
343
+ for (const root of roots) {
344
+ const path = longestFrom(root.id, new Set());
345
+ if (path.length > criticalPath.length) {
346
+ criticalPath = path;
347
+ }
348
+ }
349
+ // If no roots found (all circular), just return all task IDs
350
+ if (criticalPath.length === 0 && tasks.length > 0) {
351
+ criticalPath = tasks.map((t) => t.id);
352
+ }
353
+ return criticalPath;
354
+ }
355
+ /**
356
+ * Estimate the total token budget for a hierarchical plan.
357
+ * Based on iteration count, planning overhead, and operational detail.
358
+ *
359
+ * @param plan - The hierarchical plan
360
+ * @returns Estimated token budget
361
+ */
362
+ estimateTokenBudget(plan) {
363
+ // Base: iterations * tokens per iteration
364
+ const iterationTokens = plan.totalEstimatedIterations * TOKENS_PER_ITERATION;
365
+ // Planning overhead: ~2K tokens per tactical task for planning
366
+ const planningOverhead = plan.tactical.length * 2_000;
367
+ // Operational overhead: if pre-planned, count actions
368
+ let operationalTokens = 0;
369
+ if (plan.operational.size > 0) {
370
+ for (const [, actions] of plan.operational) {
371
+ operationalTokens += actions.length * 500;
372
+ }
373
+ }
374
+ // Strategic overhead: one-time cost for L1 planning
375
+ const strategicOverhead = 5_000;
376
+ return iterationTokens + planningOverhead + operationalTokens + strategicOverhead;
377
+ }
378
+ // ─── Private: Project Context ───
379
+ /**
380
+ * Gather project context by reading package.json and directory listing.
381
+ * @returns Concatenated project context string
382
+ */
383
+ async gatherProjectContext() {
384
+ const parts = [];
385
+ // package.json
386
+ try {
387
+ const pkgContent = await readFile(join(this.config.projectPath, "package.json"), "utf-8");
388
+ parts.push(`package.json:\n${pkgContent}`);
389
+ }
390
+ catch {
391
+ // no package.json
392
+ }
393
+ // tsconfig.json
394
+ try {
395
+ const tsContent = await readFile(join(this.config.projectPath, "tsconfig.json"), "utf-8");
396
+ parts.push(`tsconfig.json:\n${tsContent}`);
397
+ }
398
+ catch {
399
+ // no tsconfig
400
+ }
401
+ // Directory listing (shallow)
402
+ try {
403
+ const entries = await readdir(this.config.projectPath, {
404
+ withFileTypes: true,
405
+ });
406
+ const filtered = entries
407
+ .filter((e) => !e.name.startsWith(".") &&
408
+ e.name !== "node_modules" &&
409
+ e.name !== "dist")
410
+ .map((e) => (e.isDirectory() ? `${e.name}/` : e.name));
411
+ parts.push(`Project files:\n${filtered.join("\n")}`);
412
+ }
413
+ catch {
414
+ // can't list
415
+ }
416
+ return parts.join("\n\n");
417
+ }
418
+ // ─── Private: Prompt Builders ───
419
+ /**
420
+ * Build the system prompt for L1 strategic planning.
421
+ * Instructs the LLM to decompose the goal into sub-goals with JSON output.
422
+ */
423
+ buildStrategicPrompt(goal, context) {
424
+ return `You are a senior software architect. Decompose the user's goal into strategic sub-goals.
425
+
426
+ ## Project Context
427
+ ${context}
428
+
429
+ ## Goal
430
+ ${goal}
431
+
432
+ ## Instructions
433
+ - Break the goal into 1-${this.config.maxSubGoals} sub-goals
434
+ - Each sub-goal should be independently achievable
435
+ - Identify required capabilities (tools needed)
436
+ - Assess overall complexity and risk
437
+ - Consider file dependencies and ordering
438
+
439
+ ## Output Format
440
+ Respond with ONLY a JSON object (no markdown fences):
441
+ {
442
+ "description": "overall strategic description",
443
+ "subGoals": [
444
+ {
445
+ "description": "what this sub-goal achieves",
446
+ "targetFiles": ["files to modify"],
447
+ "readFiles": ["files to read for context"],
448
+ "toolStrategy": ["tool names"],
449
+ "dependsOn": [],
450
+ "estimatedIterations": 3,
451
+ "modelPreference": "premium"
452
+ }
453
+ ],
454
+ "complexity": "moderate",
455
+ "requiredCapabilities": ["file_write", "shell_exec"],
456
+ "riskAssessment": {
457
+ "level": "low",
458
+ "factors": [],
459
+ "mitigations": [],
460
+ "requiresApproval": false
461
+ }
462
+ }
463
+
464
+ Available tools: file_read, file_write, file_edit, shell_exec, grep, glob, git_ops, test_run, security_scan
465
+
466
+ Complexity levels: trivial, simple, moderate, complex, massive
467
+ Risk levels: low, medium, high, critical`;
468
+ }
469
+ /**
470
+ * Build the system prompt for L2 tactical planning.
471
+ * Instructs the LLM to plan file-level tasks for a sub-goal.
472
+ */
473
+ buildTacticalPrompt(subGoal, fileContext) {
474
+ let contextSection = "";
475
+ if (fileContext && fileContext.size > 0) {
476
+ const entries = [];
477
+ for (const [path, content] of fileContext) {
478
+ // Truncate large files
479
+ const truncated = content.length > 2_000
480
+ ? content.slice(0, 2_000) + "\n... (truncated)"
481
+ : content;
482
+ entries.push(`--- ${path} ---\n${truncated}`);
483
+ }
484
+ contextSection = `\n## File Context\n${entries.join("\n\n")}`;
485
+ }
486
+ return `You are a tech lead planning implementation tasks. For this sub-goal, plan specific file-level tasks.
487
+
488
+ ## Sub-Goal
489
+ ${subGoal.description}
490
+
491
+ ## Target Files
492
+ ${subGoal.targetFiles.join(", ") || "TBD"}
493
+ ${contextSection}
494
+
495
+ ## Instructions
496
+ - Plan concrete tasks that modify specific files
497
+ - Identify read-only dependencies (files to read but not modify)
498
+ - Choose the right tools for each task
499
+ - Estimate iterations conservatively (2-10 per file)
500
+ - Mark dependencies between tasks (by task ID)
501
+ - Suggest model preference: "flagship" for complex logic, "premium" for standard coding, "standard" for simple edits
502
+
503
+ ## Output Format
504
+ Respond with ONLY a JSON object (no markdown fences):
505
+ {
506
+ "tasks": [
507
+ {
508
+ "description": "what this task does",
509
+ "targetFiles": ["src/foo.ts"],
510
+ "readFiles": ["src/types.ts"],
511
+ "toolStrategy": ["file_read", "file_edit"],
512
+ "dependsOn": [],
513
+ "estimatedIterations": 3,
514
+ "modelPreference": "premium"
515
+ }
516
+ ]
517
+ }
518
+
519
+ Available tools: file_read, file_write, file_edit, shell_exec, grep, glob, git_ops, test_run, security_scan`;
520
+ }
521
+ /**
522
+ * Build the system prompt for L3 operational planning.
523
+ * Instructs the LLM to plan exact tool invocations for a task.
524
+ */
525
+ buildOperationalPrompt(task, currentState) {
526
+ const stateSection = currentState
527
+ ? `\n## Current State\n${JSON.stringify(currentState, null, 2)}`
528
+ : "";
529
+ return `You are a developer writing step-by-step tool actions. Plan exact tool calls for this task.
530
+
531
+ ## Task
532
+ ${task.description}
533
+
534
+ ## Target Files
535
+ ${task.targetFiles.join(", ")}
536
+
537
+ ## Read Files
538
+ ${task.readFiles.join(", ")}
539
+
540
+ ## Available Tools
541
+ ${task.toolStrategy.join(", ")}
542
+ ${stateSection}
543
+
544
+ ## Instructions
545
+ - Plan each action as a specific tool call with arguments
546
+ - Include expected outcomes for verification
547
+ - Add fallback actions for risky operations
548
+ - Keep actions atomic and verifiable
549
+ - Maximum ${this.config.maxActionsPerTask} actions
550
+
551
+ ## Output Format
552
+ Respond with ONLY a JSON object (no markdown fences):
553
+ {
554
+ "actions": [
555
+ {
556
+ "type": "read",
557
+ "description": "Read current file contents",
558
+ "tool": "file_read",
559
+ "toolInput": { "path": "src/foo.ts" },
560
+ "expectedOutcome": "File contents loaded for analysis"
561
+ },
562
+ {
563
+ "type": "edit",
564
+ "description": "Add import statement",
565
+ "tool": "file_edit",
566
+ "toolInput": { "path": "src/foo.ts", "old_string": "...", "new_string": "..." },
567
+ "expectedOutcome": "Import added successfully",
568
+ "fallbackAction": {
569
+ "type": "write",
570
+ "description": "Rewrite file if edit fails",
571
+ "tool": "file_write",
572
+ "toolInput": { "path": "src/foo.ts", "content": "..." },
573
+ "expectedOutcome": "File rewritten"
574
+ }
575
+ }
576
+ ]
577
+ }
578
+
579
+ Action types: read, analyze, write, edit, execute, test, verify`;
580
+ }
581
+ /**
582
+ * Build the system prompt for re-planning after failure.
583
+ */
584
+ buildReplanPrompt(plan, trigger) {
585
+ const affectedTasks = plan.tactical
586
+ .filter((t) => trigger.affectedTaskIds.includes(t.id))
587
+ .map((t) => ` - ${t.id}: ${t.description}`)
588
+ .join("\n");
589
+ const completedTasks = plan.tactical
590
+ .filter((t) => !trigger.affectedTaskIds.includes(t.id))
591
+ .map((t) => ` - ${t.id}: ${t.description}`)
592
+ .join("\n");
593
+ return `You are an engineering manager deciding how to recover from a problem during task execution.
594
+
595
+ ## Original Goal
596
+ ${plan.goal}
597
+
598
+ ## Problem
599
+ Type: ${trigger.type}
600
+ Severity: ${trigger.severity}
601
+ Description: ${trigger.description}
602
+
603
+ ## Affected Tasks
604
+ ${affectedTasks || " (none)"}
605
+
606
+ ## Completed Tasks
607
+ ${completedTasks || " (none)"}
608
+
609
+ ## Instructions
610
+ Decide the best recovery strategy:
611
+ - "retry_with_fix": retry the failed task with modifications
612
+ - "alternative_approach": try a completely different approach
613
+ - "skip_and_continue": skip the failed task and continue with remaining
614
+ - "escalate": the problem requires user intervention
615
+ - "full_replan": discard remaining plan and create a new one
616
+
617
+ ## Output Format
618
+ Respond with ONLY a JSON object (no markdown fences):
619
+ {
620
+ "strategy": "retry_with_fix",
621
+ "reason": "explanation of the decision",
622
+ "modifiedTasks": [
623
+ {
624
+ "description": "modified task description",
625
+ "targetFiles": ["..."],
626
+ "readFiles": ["..."],
627
+ "toolStrategy": ["..."],
628
+ "dependsOn": [],
629
+ "estimatedIterations": 3,
630
+ "modelPreference": "premium"
631
+ }
632
+ ]
633
+ }`;
634
+ }
635
+ // ─── Private: Response Parsers ───
636
+ /**
637
+ * Parse LLM JSON response into a StrategicGoal.
638
+ * Falls back to a minimal goal on parse failure.
639
+ */
640
+ parseStrategicResponse(content, fallbackGoal) {
641
+ const goalId = `goal-${randomUUID().slice(0, 8)}`;
642
+ const json = this.extractJson(content);
643
+ if (json) {
644
+ try {
645
+ const parsed = JSON.parse(json);
646
+ const rawSubGoals = (parsed.subGoals ?? parsed.sub_goals ?? []);
647
+ const subGoals = rawSubGoals.map((sg, i) => ({
648
+ id: `task-${randomUUID().slice(0, 8)}`,
649
+ goalId,
650
+ description: sg.description ?? "",
651
+ targetFiles: sg.targetFiles ?? [],
652
+ readFiles: sg.readFiles ?? [],
653
+ toolStrategy: sg.toolStrategy ?? ["file_read", "file_edit"],
654
+ order: i,
655
+ dependsOn: sg.dependsOn ?? [],
656
+ estimatedIterations: sg.estimatedIterations ?? 5,
657
+ modelPreference: sg.modelPreference,
658
+ }));
659
+ const rawRisk = (parsed.riskAssessment ?? {});
660
+ return {
661
+ id: goalId,
662
+ description: parsed.description ?? fallbackGoal,
663
+ subGoals,
664
+ estimatedComplexity: this.normalizeComplexity(parsed.complexity ?? "moderate"),
665
+ requiredCapabilities: parsed.requiredCapabilities ?? [],
666
+ riskAssessment: {
667
+ level: this.normalizeRiskLevel(rawRisk.level ?? "low"),
668
+ factors: rawRisk.factors ?? [],
669
+ mitigations: rawRisk.mitigations ?? [],
670
+ requiresApproval: rawRisk.requiresApproval ?? false,
671
+ },
672
+ };
673
+ }
674
+ catch {
675
+ // fall through to default
676
+ }
677
+ }
678
+ // Default: single sub-goal wrapping the entire request
679
+ return {
680
+ id: goalId,
681
+ description: fallbackGoal,
682
+ subGoals: [
683
+ {
684
+ id: `task-${randomUUID().slice(0, 8)}`,
685
+ goalId,
686
+ description: fallbackGoal,
687
+ targetFiles: [],
688
+ readFiles: [],
689
+ toolStrategy: ["file_read", "file_edit"],
690
+ order: 0,
691
+ dependsOn: [],
692
+ estimatedIterations: 5,
693
+ },
694
+ ],
695
+ estimatedComplexity: "moderate",
696
+ requiredCapabilities: ["file_read", "file_edit"],
697
+ riskAssessment: {
698
+ level: "low",
699
+ factors: [],
700
+ mitigations: [],
701
+ requiresApproval: false,
702
+ },
703
+ };
704
+ }
705
+ /**
706
+ * Parse LLM JSON response into TacticalTask[].
707
+ */
708
+ parseTacticalResponse(content, goalId, parentSubGoal) {
709
+ const json = this.extractJson(content);
710
+ if (json) {
711
+ try {
712
+ const parsed = JSON.parse(json);
713
+ const rawTasks = (parsed.tasks ?? []);
714
+ return rawTasks.map((t, i) => ({
715
+ id: `task-${randomUUID().slice(0, 8)}`,
716
+ goalId,
717
+ description: t.description ?? "",
718
+ targetFiles: t.targetFiles ?? [],
719
+ readFiles: t.readFiles ?? [],
720
+ toolStrategy: t.toolStrategy ?? ["file_read", "file_edit"],
721
+ order: i,
722
+ dependsOn: t.dependsOn ?? [],
723
+ estimatedIterations: t.estimatedIterations ?? 5,
724
+ modelPreference: t.modelPreference,
725
+ }));
726
+ }
727
+ catch {
728
+ // fall through
729
+ }
730
+ }
731
+ // Default: return the parent sub-goal as a single task
732
+ return [
733
+ {
734
+ ...parentSubGoal,
735
+ id: `task-${randomUUID().slice(0, 8)}`,
736
+ goalId,
737
+ },
738
+ ];
739
+ }
740
+ /**
741
+ * Parse LLM JSON response into OperationalAction[].
742
+ */
743
+ parseOperationalResponse(content, taskId) {
744
+ const json = this.extractJson(content);
745
+ if (json) {
746
+ try {
747
+ const parsed = JSON.parse(json);
748
+ const rawActions = (parsed.actions ?? []);
749
+ return rawActions.map((a) => {
750
+ const action = {
751
+ id: `action-${randomUUID().slice(0, 8)}`,
752
+ taskId,
753
+ type: this.normalizeActionType(a.type ?? "read"),
754
+ description: a.description ?? "",
755
+ tool: a.tool ?? "file_read",
756
+ toolInput: a.toolInput ?? {},
757
+ expectedOutcome: a.expectedOutcome ?? "",
758
+ };
759
+ // Parse fallback action if present
760
+ if (a.fallbackAction) {
761
+ const fb = a.fallbackAction;
762
+ action.fallbackAction = {
763
+ id: `action-${randomUUID().slice(0, 8)}`,
764
+ taskId,
765
+ type: this.normalizeActionType(fb.type ?? "write"),
766
+ description: fb.description ?? "",
767
+ tool: fb.tool ?? "file_write",
768
+ toolInput: fb.toolInput ?? {},
769
+ expectedOutcome: fb.expectedOutcome ?? "",
770
+ };
771
+ }
772
+ return action;
773
+ });
774
+ }
775
+ catch {
776
+ // fall through
777
+ }
778
+ }
779
+ // Default: single read action
780
+ return [
781
+ {
782
+ id: `action-${randomUUID().slice(0, 8)}`,
783
+ taskId,
784
+ type: "read",
785
+ description: "Read target files",
786
+ tool: "file_read",
787
+ toolInput: {},
788
+ expectedOutcome: "Files loaded for analysis",
789
+ },
790
+ ];
791
+ }
792
+ /**
793
+ * Parse re-plan LLM response into RePlanResult.
794
+ */
795
+ parseReplanResponse(content, currentPlan, trigger) {
796
+ const json = this.extractJson(content);
797
+ if (json) {
798
+ try {
799
+ const parsed = JSON.parse(json);
800
+ const rawTasks = (parsed.modifiedTasks ?? []);
801
+ const modifiedTasks = rawTasks.map((t, i) => ({
802
+ id: `task-${randomUUID().slice(0, 8)}`,
803
+ goalId: currentPlan.strategic.id,
804
+ description: t.description ?? "",
805
+ targetFiles: t.targetFiles ?? [],
806
+ readFiles: t.readFiles ?? [],
807
+ toolStrategy: t.toolStrategy ?? ["file_read", "file_edit"],
808
+ order: i,
809
+ dependsOn: t.dependsOn ?? [],
810
+ estimatedIterations: t.estimatedIterations ?? 5,
811
+ modelPreference: t.modelPreference,
812
+ }));
813
+ return {
814
+ strategy: this.normalizeReplanStrategy(parsed.strategy ?? "retry_with_fix"),
815
+ modifiedTasks,
816
+ reason: parsed.reason ?? "Re-plan generated by LLM",
817
+ };
818
+ }
819
+ catch {
820
+ // fall through
821
+ }
822
+ }
823
+ // Default: escalate on parse failure
824
+ return {
825
+ strategy: trigger.severity === "critical" ? "escalate" : "retry_with_fix",
826
+ modifiedTasks: currentPlan.tactical.filter((t) => trigger.affectedTaskIds.includes(t.id)),
827
+ reason: "Could not parse re-plan response; defaulting to safe strategy",
828
+ };
829
+ }
830
+ // ─── Private: Risk Assessment ───
831
+ /**
832
+ * Assess risk based on the goal description and target files.
833
+ */
834
+ assessRisk(goal, files) {
835
+ const factors = [];
836
+ const mitigations = [];
837
+ let riskScore = 0; // 0=low, 1=medium, 2=high, 3=critical
838
+ // Check files against high-risk patterns
839
+ for (const file of files) {
840
+ for (const pattern of HIGH_RISK_PATTERNS) {
841
+ if (pattern.test(file)) {
842
+ factors.push(`Modifies sensitive file: ${file}`);
843
+ riskScore = Math.max(riskScore, 1);
844
+ break;
845
+ }
846
+ }
847
+ }
848
+ // Check goal for destructive keywords
849
+ const destructiveKeywords = /\b(delete|remove|drop|reset|force|overwrite|wipe|migrate)\b/i;
850
+ if (destructiveKeywords.test(goal)) {
851
+ factors.push("Goal contains destructive operation keywords");
852
+ riskScore = Math.min(riskScore + 1, 3);
853
+ mitigations.push("Create backup before executing");
854
+ }
855
+ // Check for destructive tool usage
856
+ const goalLower = goal.toLowerCase();
857
+ if (DESTRUCTIVE_OPS.some((op) => goalLower.includes(op))) {
858
+ factors.push("Requires write/execute operations");
859
+ riskScore = Math.max(riskScore, 1);
860
+ mitigations.push("Verify changes with tests after modification");
861
+ }
862
+ const riskLevels = ["low", "medium", "high", "critical"];
863
+ const maxRisk = riskLevels[Math.min(riskScore, 3)];
864
+ // Determine approval requirement
865
+ const requiresApproval = maxRisk === "high" || maxRisk === "critical";
866
+ if (requiresApproval) {
867
+ mitigations.push("Request user approval before executing");
868
+ }
869
+ return { level: maxRisk, factors, mitigations, requiresApproval };
870
+ }
871
+ // ─── Private: Dependency Analysis ───
872
+ /**
873
+ * Sort tasks by dependency order and assign sequential order values.
874
+ * Uses Kahn's algorithm for topological sorting.
875
+ */
876
+ buildDependencyChain(tasks) {
877
+ if (tasks.length === 0)
878
+ return [];
879
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
880
+ // Compute in-degree for each task (only count internal deps)
881
+ const inDegree = new Map();
882
+ for (const task of tasks) {
883
+ if (!inDegree.has(task.id))
884
+ inDegree.set(task.id, 0);
885
+ for (const dep of task.dependsOn) {
886
+ if (taskMap.has(dep)) {
887
+ inDegree.set(task.id, (inDegree.get(task.id) ?? 0) + 1);
888
+ }
889
+ }
890
+ }
891
+ // Kahn's algorithm
892
+ const queue = [];
893
+ for (const [id, degree] of inDegree) {
894
+ if (degree === 0)
895
+ queue.push(id);
896
+ }
897
+ const sorted = [];
898
+ let order = 0;
899
+ while (queue.length > 0) {
900
+ const current = queue.shift();
901
+ const task = taskMap.get(current);
902
+ if (!task)
903
+ continue;
904
+ task.order = order++;
905
+ sorted.push(task);
906
+ // Reduce in-degree for dependents
907
+ for (const other of tasks) {
908
+ if (other.dependsOn.includes(current) && taskMap.has(other.id)) {
909
+ const newDegree = (inDegree.get(other.id) ?? 1) - 1;
910
+ inDegree.set(other.id, newDegree);
911
+ if (newDegree === 0) {
912
+ queue.push(other.id);
913
+ }
914
+ }
915
+ }
916
+ }
917
+ // Append any remaining tasks (cycle members) at the end
918
+ for (const task of tasks) {
919
+ if (!sorted.includes(task)) {
920
+ task.order = order++;
921
+ sorted.push(task);
922
+ }
923
+ }
924
+ return sorted;
925
+ }
926
+ // ─── Private: Helpers ───
927
+ /**
928
+ * Extract the first JSON object or array from a response string.
929
+ * Handles common LLM output patterns (markdown fences, preamble text).
930
+ */
931
+ extractJson(content) {
932
+ // Strip markdown code fences if present
933
+ const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/);
934
+ if (fenced) {
935
+ return fenced[1].trim();
936
+ }
937
+ // Find first { ... } block
938
+ const objectMatch = content.match(/\{[\s\S]*\}/);
939
+ if (objectMatch) {
940
+ return objectMatch[0];
941
+ }
942
+ return null;
943
+ }
944
+ /** Normalize complexity string to valid union type */
945
+ normalizeComplexity(value) {
946
+ const valid = new Set(["trivial", "simple", "moderate", "complex", "massive"]);
947
+ return valid.has(value)
948
+ ? value
949
+ : "moderate";
950
+ }
951
+ /** Normalize risk level string to valid union type */
952
+ normalizeRiskLevel(value) {
953
+ const valid = new Set(["low", "medium", "high", "critical"]);
954
+ return valid.has(value) ? value : "low";
955
+ }
956
+ /** Normalize action type string to valid union type */
957
+ normalizeActionType(value) {
958
+ const valid = new Set([
959
+ "read",
960
+ "analyze",
961
+ "write",
962
+ "edit",
963
+ "execute",
964
+ "test",
965
+ "verify",
966
+ ]);
967
+ return valid.has(value) ? value : "read";
968
+ }
969
+ /** Normalize re-plan strategy string to valid union type */
970
+ normalizeReplanStrategy(value) {
971
+ const valid = new Set([
972
+ "retry_with_fix",
973
+ "alternative_approach",
974
+ "skip_and_continue",
975
+ "escalate",
976
+ "full_replan",
977
+ ]);
978
+ return valid.has(value) ? value : "retry_with_fix";
979
+ }
980
+ }
981
+ //# sourceMappingURL=hierarchical-planner.js.map