@kkelly-offical/kkcode 0.1.3 → 0.1.7

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 (66) hide show
  1. package/README.md +110 -172
  2. package/package.json +46 -46
  3. package/src/agent/agent.mjs +220 -170
  4. package/src/agent/prompt/bug-hunter.txt +90 -0
  5. package/src/agent/prompt/frontend-designer.txt +58 -0
  6. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
  7. package/src/agent/prompt/longagent-coding-agent.txt +37 -0
  8. package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
  9. package/src/agent/prompt/longagent-preview-agent.txt +63 -0
  10. package/src/config/defaults.mjs +260 -195
  11. package/src/config/schema.mjs +71 -6
  12. package/src/core/constants.mjs +91 -46
  13. package/src/index.mjs +1 -1
  14. package/src/knowledge/frontend-aesthetics.txt +39 -0
  15. package/src/knowledge/loader.mjs +2 -1
  16. package/src/knowledge/tailwind.txt +12 -3
  17. package/src/mcp/client-http.mjs +141 -157
  18. package/src/mcp/client-sse.mjs +288 -286
  19. package/src/mcp/client-stdio.mjs +533 -451
  20. package/src/mcp/constants.mjs +2 -0
  21. package/src/mcp/registry.mjs +479 -394
  22. package/src/mcp/stdio-framing.mjs +133 -127
  23. package/src/mcp/tool-result.mjs +24 -0
  24. package/src/observability/index.mjs +42 -0
  25. package/src/observability/metrics.mjs +137 -0
  26. package/src/observability/tracer.mjs +137 -0
  27. package/src/orchestration/background-manager.mjs +372 -358
  28. package/src/orchestration/background-worker.mjs +305 -245
  29. package/src/orchestration/longagent-manager.mjs +171 -116
  30. package/src/orchestration/stage-scheduler.mjs +728 -489
  31. package/src/permission/exec-policy.mjs +9 -11
  32. package/src/provider/anthropic.mjs +1 -0
  33. package/src/provider/openai.mjs +340 -339
  34. package/src/provider/retry-policy.mjs +68 -68
  35. package/src/provider/router.mjs +241 -228
  36. package/src/provider/sse.mjs +104 -91
  37. package/src/repl.mjs +59 -7
  38. package/src/session/checkpoint.mjs +66 -3
  39. package/src/session/compaction.mjs +298 -276
  40. package/src/session/engine.mjs +232 -225
  41. package/src/session/longagent-4stage.mjs +460 -0
  42. package/src/session/longagent-hybrid.mjs +1097 -0
  43. package/src/session/longagent-plan.mjs +365 -329
  44. package/src/session/longagent-project-memory.mjs +53 -0
  45. package/src/session/longagent-scaffold.mjs +291 -100
  46. package/src/session/longagent-task-bus.mjs +54 -0
  47. package/src/session/longagent-utils.mjs +472 -0
  48. package/src/session/longagent.mjs +900 -1462
  49. package/src/session/loop.mjs +65 -40
  50. package/src/session/project-context.mjs +30 -0
  51. package/src/session/prompt/agent.txt +25 -0
  52. package/src/session/prompt/plan.txt +31 -9
  53. package/src/session/rollback.mjs +196 -0
  54. package/src/session/store.mjs +519 -503
  55. package/src/session/system-prompt.mjs +273 -260
  56. package/src/session/task-validator.mjs +4 -3
  57. package/src/skill/builtin/design.mjs +76 -0
  58. package/src/skill/builtin/frontend.mjs +8 -0
  59. package/src/skill/registry.mjs +390 -336
  60. package/src/storage/ghost-commit-store.mjs +18 -8
  61. package/src/tool/executor.mjs +11 -0
  62. package/src/tool/git-auto.mjs +0 -19
  63. package/src/tool/question-prompt.mjs +93 -86
  64. package/src/tool/registry.mjs +71 -37
  65. package/src/ui/activity-renderer.mjs +664 -410
  66. package/src/util/git.mjs +23 -0
@@ -1,329 +1,365 @@
1
- import { newId } from "../core/types.mjs"
2
- import { processTurnLoop } from "./loop.mjs"
3
-
4
- function stripFence(text = "") {
5
- const raw = String(text || "").trim()
6
- const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)
7
- if (fenced) return fenced[1].trim()
8
- return raw
9
- }
10
-
11
- function parseJsonLoose(text = "") {
12
- const raw = stripFence(text)
13
- try {
14
- return JSON.parse(raw)
15
- } catch {
16
- const start = raw.indexOf("{")
17
- const end = raw.lastIndexOf("}")
18
- if (start >= 0 && end > start) {
19
- try {
20
- return JSON.parse(raw.slice(start, end + 1))
21
- } catch {
22
- return null
23
- }
24
- }
25
- return null
26
- }
27
- }
28
-
29
- function normalizeFileList(value) {
30
- if (!Array.isArray(value)) return []
31
- return [...new Set(value
32
- .map((v) => String(v || "").trim())
33
- .filter(Boolean)
34
- .slice(0, 80))]
35
- }
36
-
37
- function normalizeStringList(value) {
38
- if (!Array.isArray(value)) return []
39
- return value
40
- .map((v) => String(v || "").trim())
41
- .filter(Boolean)
42
- .slice(0, 50)
43
- }
44
-
45
- function normalizeTask(task, stageId, defaults = {}) {
46
- const baseId = String(task?.taskId || task?.id || "").trim()
47
- const taskId = baseId || `${stageId}_task_${newId("t").slice(-6)}`
48
- const prompt = String(task?.prompt || "").trim()
49
- if (!prompt) return null
50
- const timeoutMs = Number(task?.timeoutMs || defaults.timeoutMs || 600000)
51
- const maxRetries = Number(task?.maxRetries ?? defaults.maxRetries ?? 2)
52
- return {
53
- taskId,
54
- prompt,
55
- subagentType: task?.subagentType ? String(task.subagentType) : undefined,
56
- category: task?.category ? String(task.category) : undefined,
57
- plannedFiles: normalizeFileList(task?.plannedFiles),
58
- acceptance: normalizeStringList(task?.acceptance),
59
- timeoutMs: Number.isFinite(timeoutMs) && timeoutMs >= 1000 ? timeoutMs : 600000,
60
- maxRetries: Number.isFinite(maxRetries) && maxRetries >= 0 ? maxRetries : 2
61
- }
62
- }
63
-
64
- function normalizeStage(stage, defaults = {}, idx = 0) {
65
- const stageId = String(stage?.stageId || stage?.id || `stage_${idx + 1}`).trim() || `stage_${idx + 1}`
66
- const name = String(stage?.name || `Stage ${idx + 1}`).trim() || `Stage ${idx + 1}`
67
- const tasks = Array.isArray(stage?.tasks)
68
- ? stage.tasks.map((t) => normalizeTask(t, stageId, defaults)).filter(Boolean)
69
- : []
70
- return {
71
- stageId,
72
- name,
73
- passRule: "all_success",
74
- tasks
75
- }
76
- }
77
-
78
- export function defaultStagePlan(objective, defaults = {}) {
79
- const stageId = "stage_1"
80
- return {
81
- planId: newId("plan"),
82
- objective: String(objective || "").trim(),
83
- stages: [
84
- {
85
- stageId,
86
- name: "Execution",
87
- passRule: "all_success",
88
- tasks: [
89
- {
90
- taskId: `${stageId}_task_1`,
91
- prompt: String(objective || "").trim(),
92
- plannedFiles: [],
93
- acceptance: ["Task objective is fully usable"],
94
- timeoutMs: Number(defaults.timeoutMs || 600000),
95
- maxRetries: Number(defaults.maxRetries ?? 2)
96
- }
97
- ]
98
- }
99
- ]
100
- }
101
- }
102
-
103
- export function validateAndNormalizeStagePlan(input, { objective = "", defaults = {} } = {}) {
104
- if (!input || typeof input !== "object") {
105
- return { plan: defaultStagePlan(objective, defaults), errors: ["plan is not object"] }
106
- }
107
-
108
- const plan = {
109
- planId: String(input.planId || newId("plan")),
110
- objective: String(input.objective || objective || "").trim(),
111
- stages: Array.isArray(input.stages)
112
- ? input.stages.map((s, idx) => normalizeStage(s, defaults, idx))
113
- : []
114
- }
115
-
116
- const errors = []
117
- if (!plan.objective) errors.push("objective is empty")
118
- if (!plan.stages.length) errors.push("no stages")
119
- for (const stage of plan.stages) {
120
- if (!stage.tasks.length) errors.push(`stage "${stage.stageId}" has no tasks`)
121
- // Early file isolation checkdetect overlapping file ownership at plan time
122
- const ownership = new Map()
123
- for (const task of stage.tasks) {
124
- for (const file of task.plannedFiles || []) {
125
- if (ownership.has(file)) {
126
- errors.push(`stage "${stage.stageId}": file "${file}" claimed by "${ownership.get(file)}" and "${task.taskId}"`)
127
- } else {
128
- ownership.set(file, task.taskId)
129
- }
130
- }
131
- }
132
- }
133
-
134
- // Cross-stage dependency check: later stages should not own files already owned by earlier stages
135
- const globalOwnership = new Map()
136
- for (const stage of plan.stages) {
137
- for (const task of stage.tasks) {
138
- for (const file of task.plannedFiles || []) {
139
- if (globalOwnership.has(file)) {
140
- const prev = globalOwnership.get(file)
141
- errors.push(`file "${file}" appears in stage "${prev}" and "${stage.stageId}" — split into dependency chain or deduplicate`)
142
- } else {
143
- globalOwnership.set(file, stage.stageId)
144
- }
145
- }
146
- }
147
- }
148
-
149
- // Quality score: penalize tasks with no files or no acceptance criteria
150
- let qualityScore = 100
151
- let totalTasks = 0
152
- for (const stage of plan.stages) {
153
- for (const task of stage.tasks) {
154
- totalTasks += 1
155
- if (!task.plannedFiles.length) qualityScore -= 15
156
- if (!task.acceptance.length) qualityScore -= 10
157
- }
158
- }
159
- qualityScore = Math.max(0, Math.min(100, qualityScore))
160
-
161
- if (errors.length) {
162
- return {
163
- plan: defaultStagePlan(objective || plan.objective, defaults),
164
- errors,
165
- qualityScore: 0
166
- }
167
- }
168
- return { plan, errors: [], qualityScore }
169
- }
170
-
171
- export async function runIntakeDialogue({
172
- objective,
173
- model,
174
- providerType,
175
- sessionId,
176
- configState,
177
- baseUrl = null,
178
- apiKeyEnv = null,
179
- agent = null,
180
- signal = null,
181
- maxRounds = 6
182
- }) {
183
- const rounds = Math.max(1, Number(maxRounds || 6))
184
- const transcript = []
185
- let summary = ""
186
-
187
- for (let i = 1; i <= rounds; i++) {
188
- const prompt = [
189
- "You are performing pre-planning intake for a long-running coding task.",
190
- "Ask the most critical clarifying questions, then answer them with explicit assumptions.",
191
- "Return STRICT JSON:",
192
- `{"enough":boolean,"summary":"...","qa":[{"q":"...","a":"..."}]}`,
193
- "",
194
- `Round: ${i}/${rounds}`,
195
- `Objective: ${objective}`,
196
- summary ? `Previous summary: ${summary}` : ""
197
- ].filter(Boolean).join("\n")
198
-
199
- const out = await processTurnLoop({
200
- prompt,
201
- mode: "ask",
202
- model,
203
- providerType,
204
- sessionId,
205
- configState,
206
- baseUrl,
207
- apiKeyEnv,
208
- agent,
209
- signal,
210
- output: { write: () => {} },
211
- allowQuestion: true
212
- })
213
-
214
- const parsed = parseJsonLoose(out.reply)
215
- if (parsed && Array.isArray(parsed.qa)) {
216
- for (const item of parsed.qa.slice(0, 10)) {
217
- const q = String(item?.q || "").trim()
218
- const a = String(item?.a || "").trim()
219
- if (q || a) transcript.push({ q, a })
220
- }
221
- summary = String(parsed.summary || "").trim() || summary
222
- const enough = Boolean(parsed.enough)
223
- if (enough && i >= 2) break
224
- continue
225
- }
226
-
227
- const fallbackLine = String(out.reply || "").trim()
228
- if (fallbackLine) {
229
- transcript.push({ q: `Round ${i} synthesis`, a: fallbackLine })
230
- summary = fallbackLine
231
- }
232
- if (i >= 2) break
233
- }
234
-
235
- return {
236
- transcript,
237
- summary: summary || String(objective || "").trim()
238
- }
239
- }
240
-
241
- export async function buildStagePlan({
242
- objective,
243
- intakeSummary = "",
244
- model,
245
- providerType,
246
- sessionId,
247
- configState,
248
- baseUrl = null,
249
- apiKeyEnv = null,
250
- agent = null,
251
- signal = null,
252
- defaults = {}
253
- }) {
254
- const plannerPrompt = [
255
- "Generate a stage plan for parallel execution of a coding task.",
256
- "Return STRICT JSON ONLY with this schema:",
257
- '{"planId":"...","objective":"...","stages":[{"stageId":"...","name":"...","passRule":"all_success","tasks":[{"taskId":"...","prompt":"...","subagentType":"...","category":"...","plannedFiles":["..."],"acceptance":["..."],"timeoutMs":600000,"maxRetries":2}]}]}',
258
- "",
259
- "## Planning Rules",
260
- "",
261
- "### File Assignment (CRITICAL)",
262
- "- Files that import from each other MUST be in the same task",
263
- "- A module and its tests MUST be in the same task",
264
- "- A component and its type definitions MUST be in the same task",
265
- "- Each task should own 2-8 files. If only 1 file, merge with related task. If >10 files, split.",
266
- "- NO file may appear in multiple tasks within the same stage",
267
- "",
268
- "### Stage Ordering",
269
- "- Stages execute sequentially; tasks within a stage execute in parallel",
270
- "- Order: Infrastructure/utilities → Core logic → Integration/UI → Tests/Validation",
271
- "- If Task B depends on Task A's output (e.g. imports from files Task A creates), they MUST be in different stages (A's stage before B's stage)",
272
- "",
273
- "### Task Requirements",
274
- "- passRule must be all_success",
275
- "- each stage must have 1..8 tasks",
276
- "- each task MUST include: prompt (detailed instructions), plannedFiles (specific file paths), acceptance (machine-verifiable criteria)",
277
- "- keep task scope independent for parallel execution",
278
- "- task prompts should be self-contained — the sub-agent has no context beyond what you write",
279
- "",
280
- "### Acceptance Criteria Rules",
281
- "- MUST be machine-verifiable (e.g. 'node --check passes', 'npm test passes', 'function X is exported from Y')",
282
- "- NEVER use subjective criteria (e.g. 'code quality is good', 'implementation is clean')",
283
- "- Each task MUST have at least one acceptance criterion from these categories:",
284
- " 1. Syntax: 'node --check <file>' or 'python -m py_compile <file>'",
285
- " 2. Build: 'npm run build succeeds' (if applicable)",
286
- " 3. Functional: 'function X exists and handles Y' or 'API endpoint returns Z'",
287
- " 4. Test: 'npm test passes' or 'specific test file passes'",
288
- "- The FINAL stage should include a synthesis acceptance criterion: 'all modified files parse without errors AND project builds successfully'",
289
- "",
290
- "### Example (2-stage plan)",
291
- '{"planId":"plan_ex","objective":"Add user auth","stages":[',
292
- ' {"stageId":"s1","name":"Auth core","passRule":"all_success","tasks":[',
293
- ' {"taskId":"s1_auth","prompt":"Implement JWT auth middleware...","plannedFiles":["src/auth/jwt.mjs","src/auth/jwt.test.mjs"],"acceptance":["node --check src/auth/jwt.mjs passes","npm test -- auth passes"],"timeoutMs":600000,"maxRetries":2}',
294
- ' ]},{"stageId":"s2","name":"Auth routes","passRule":"all_success","tasks":[',
295
- ' {"taskId":"s2_routes","prompt":"Add login/register routes using auth from s1...","plannedFiles":["src/routes/auth.mjs","src/routes/auth.test.mjs"],"acceptance":["node --check passes","npm test passes"],"timeoutMs":600000,"maxRetries":2}',
296
- " ]}]}",
297
- "",
298
- `## Objective`,
299
- objective,
300
- intakeSummary ? `\n## Intake Summary\n${intakeSummary}` : ""
301
- ].filter(Boolean).join("\n")
302
-
303
- const out = await processTurnLoop({
304
- prompt: plannerPrompt,
305
- mode: "plan",
306
- model,
307
- providerType,
308
- sessionId,
309
- configState,
310
- baseUrl,
311
- apiKeyEnv,
312
- agent,
313
- signal,
314
- output: { write: () => {} },
315
- allowQuestion: true
316
- })
317
-
318
- const parsed = parseJsonLoose(out.reply)
319
- if (!parsed) {
320
- return {
321
- plan: defaultStagePlan(objective, defaults),
322
- errors: ["planner returned unparseable response falling back to single-stage plan"],
323
- qualityScore: 0,
324
- rawReply: out.reply
325
- }
326
- }
327
- const { plan, errors, qualityScore } = validateAndNormalizeStagePlan(parsed, { objective, defaults })
328
- return { plan, errors, qualityScore, rawReply: out.reply }
329
- }
1
+ import { newId } from "../core/types.mjs"
2
+ import { processTurnLoop } from "./loop.mjs"
3
+ import { stripFence, parseJsonLoose } from "./longagent-utils.mjs"
4
+
5
+ function normalizeFileList(value) {
6
+ if (!Array.isArray(value)) return []
7
+ return [...new Set(value
8
+ .map((v) => String(v || "").trim())
9
+ .filter(Boolean)
10
+ .slice(0, 80))]
11
+ }
12
+
13
+ function normalizeStringList(value) {
14
+ if (!Array.isArray(value)) return []
15
+ return value
16
+ .map((v) => String(v || "").trim())
17
+ .filter(Boolean)
18
+ .slice(0, 50)
19
+ }
20
+
21
+ function normalizeTask(task, stageId, defaults = {}) {
22
+ const baseId = String(task?.taskId || task?.id || "").trim()
23
+ const taskId = baseId || `${stageId}_task_${newId("t").slice(-6)}`
24
+ const prompt = String(task?.prompt || "").trim()
25
+ if (!prompt) return null
26
+ const timeoutMs = Number(task?.timeoutMs || defaults.timeoutMs || 600000)
27
+ const maxRetries = Number(task?.maxRetries ?? defaults.maxRetries ?? 2)
28
+ const complexity = ["low", "medium", "high"].includes(task?.complexity) ? task.complexity : "medium"
29
+ const dependsOn = normalizeStringList(task?.dependsOn || [])
30
+ return {
31
+ taskId,
32
+ prompt,
33
+ subagentType: task?.subagentType ? String(task.subagentType) : undefined,
34
+ category: task?.category ? String(task.category) : undefined,
35
+ plannedFiles: normalizeFileList(task?.plannedFiles),
36
+ acceptance: normalizeStringList(task?.acceptance),
37
+ dependsOn,
38
+ complexity,
39
+ timeoutMs: Number.isFinite(timeoutMs) && timeoutMs >= 1000 ? timeoutMs : 600000,
40
+ maxRetries: Number.isFinite(maxRetries) && maxRetries >= 0 ? maxRetries : 2
41
+ }
42
+ }
43
+
44
+ function normalizeStage(stage, defaults = {}, idx = 0) {
45
+ const stageId = String(stage?.stageId || stage?.id || `stage_${idx + 1}`).trim() || `stage_${idx + 1}`
46
+ const name = String(stage?.name || `Stage ${idx + 1}`).trim() || `Stage ${idx + 1}`
47
+ const tasks = Array.isArray(stage?.tasks)
48
+ ? stage.tasks.map((t) => normalizeTask(t, stageId, defaults)).filter(Boolean)
49
+ : []
50
+ return {
51
+ stageId,
52
+ name,
53
+ passRule: "all_success",
54
+ tasks
55
+ }
56
+ }
57
+
58
+ export function defaultStagePlan(objective, defaults = {}) {
59
+ const stageId = "stage_1"
60
+ return {
61
+ planId: newId("plan"),
62
+ objective: String(objective || "").trim(),
63
+ stages: [
64
+ {
65
+ stageId,
66
+ name: "Execution",
67
+ passRule: "all_success",
68
+ tasks: [
69
+ {
70
+ taskId: `${stageId}_task_1`,
71
+ prompt: String(objective || "").trim(),
72
+ plannedFiles: [],
73
+ acceptance: ["Task objective is fully usable"],
74
+ timeoutMs: Number(defaults.timeoutMs || 600000),
75
+ maxRetries: Number(defaults.maxRetries ?? 2)
76
+ }
77
+ ]
78
+ }
79
+ ]
80
+ }
81
+ }
82
+
83
+ export function validateAndNormalizeStagePlan(input, { objective = "", defaults = {} } = {}) {
84
+ if (!input || typeof input !== "object") {
85
+ return { plan: defaultStagePlan(objective, defaults), errors: ["plan is not object"] }
86
+ }
87
+
88
+ const plan = {
89
+ planId: String(input.planId || newId("plan")),
90
+ objective: String(input.objective || objective || "").trim(),
91
+ stages: Array.isArray(input.stages)
92
+ ? input.stages.map((s, idx) => normalizeStage(s, defaults, idx))
93
+ : []
94
+ }
95
+
96
+ const errors = []
97
+ if (!plan.objective) errors.push("objective is empty")
98
+ if (!plan.stages.length) errors.push("no stages")
99
+ for (const stage of plan.stages) {
100
+ if (!stage.tasks.length) errors.push(`stage "${stage.stageId}" has no tasks`)
101
+ // Early file isolation check — detect overlapping file ownership at plan time
102
+ const ownership = new Map()
103
+ for (const task of stage.tasks) {
104
+ for (const file of task.plannedFiles || []) {
105
+ if (ownership.has(file)) {
106
+ errors.push(`stage "${stage.stageId}": file "${file}" claimed by "${ownership.get(file)}" and "${task.taskId}"`)
107
+ } else {
108
+ ownership.set(file, task.taskId)
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ // Cross-stage dependency check: later stages should not own files already owned by earlier stages
115
+ const globalOwnership = new Map()
116
+ for (const stage of plan.stages) {
117
+ for (const task of stage.tasks) {
118
+ for (const file of task.plannedFiles || []) {
119
+ if (globalOwnership.has(file)) {
120
+ const prev = globalOwnership.get(file)
121
+ errors.push(`file "${file}" appears in stage "${prev}" and "${stage.stageId}" split into dependency chain or deduplicate`)
122
+ } else {
123
+ globalOwnership.set(file, stage.stageId)
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ // Quality score: penalize tasks with no files or no acceptance criteria
130
+ let qualityScore = 100
131
+ let totalTasks = 0
132
+ for (const stage of plan.stages) {
133
+ for (const task of stage.tasks) {
134
+ totalTasks += 1
135
+ if (!task.plannedFiles.length) qualityScore -= 15
136
+ if (!task.acceptance.length) qualityScore -= 10
137
+ }
138
+ }
139
+ qualityScore = Math.max(0, Math.min(100, qualityScore))
140
+
141
+ if (errors.length) {
142
+ return {
143
+ plan: defaultStagePlan(objective || plan.objective, defaults),
144
+ errors,
145
+ qualityScore: 0
146
+ }
147
+ }
148
+ return { plan, errors: [], qualityScore }
149
+ }
150
+
151
+ export async function runIntakeDialogue({
152
+ objective,
153
+ model,
154
+ providerType,
155
+ sessionId,
156
+ configState,
157
+ baseUrl = null,
158
+ apiKeyEnv = null,
159
+ agent = null,
160
+ signal = null,
161
+ maxRounds = 6
162
+ }) {
163
+ const rounds = Math.max(1, Number(maxRounds || 6))
164
+ const transcript = []
165
+ let summary = ""
166
+
167
+ for (let i = 1; i <= rounds; i++) {
168
+ const prompt = [
169
+ "You are the INTAKE ANALYST for a production-grade parallel coding pipeline.",
170
+ "Multiple independent sub-agents will execute tasks IN PARALLEL — they cannot communicate with each other during execution.",
171
+ "Your job: resolve ALL ambiguities NOW so that parallel agents produce compatible, integrable code.",
172
+ "",
173
+ "## Analysis Categories (address ALL that apply)",
174
+ "",
175
+ "### 1. SCOPE & BOUNDARIES",
176
+ "- What files/modules/directories are IN scope for modification?",
177
+ "- What existing code must NOT be changed (public APIs, shared interfaces, config schemas)?",
178
+ "- Are there related features that should be explicitly excluded to prevent scope creep?",
179
+ "",
180
+ "### 2. TECHNOLOGY & PATTERNS",
181
+ "- What language version, runtime, and framework constraints apply?",
182
+ "- What existing patterns in the codebase MUST be followed (error handling, logging, naming, async style)?",
183
+ "- What existing utilities/helpers MUST be reused instead of reimplemented?",
184
+ "- Are there specific libraries to use or avoid?",
185
+ "",
186
+ "### 3. INTERFACE CONTRACTS",
187
+ "- What are the exact function signatures, parameter types, and return types?",
188
+ "- What data schemas are involved (DB models, API payloads, config shapes)?",
189
+ "- What error types should be thrown/returned and how should callers handle them?",
190
+ "- What events/hooks/callbacks are part of the contract?",
191
+ "",
192
+ "### 4. QUALITY CONSTRAINTS",
193
+ "- What test coverage is expected (unit, integration, e2e)?",
194
+ "- What backward compatibility guarantees must be maintained?",
195
+ "- Are there performance budgets (latency, memory, bundle size)?",
196
+ "- What security considerations apply (input validation, auth, secrets)?",
197
+ "",
198
+ "### 5. DEPENDENCY ORDER",
199
+ "- Which components must exist before others can be built?",
200
+ "- Which components are independent and can be built in parallel?",
201
+ "- Are there shared types/interfaces that must be defined first?",
202
+ "",
203
+ "## Output Rules",
204
+ "- For each question, provide your BEST ASSUMPTION as the answer based on the objective and codebase context.",
205
+ "- Be specific: 'use HS256 JWT with 24h expiry' not 'implement auth'.",
206
+ "- Set enough=true ONLY when you have sufficient clarity to generate a concrete file-level execution plan.",
207
+ "",
208
+ "Return STRICT JSON (no markdown wrapping):",
209
+ `{"enough":boolean,"summary":"technical summary with ALL resolved assumptions and concrete decisions","qa":[{"q":"specific question","a":"concrete answer with implementation detail"}]}`,
210
+ "",
211
+ `Round: ${i}/${rounds}`,
212
+ `Objective: ${objective}`,
213
+ summary ? `Previous summary: ${summary}` : ""
214
+ ].filter(Boolean).join("\n")
215
+
216
+ const out = await processTurnLoop({
217
+ prompt,
218
+ mode: "ask",
219
+ model,
220
+ providerType,
221
+ sessionId,
222
+ configState,
223
+ baseUrl,
224
+ apiKeyEnv,
225
+ agent,
226
+ signal,
227
+ output: { write: () => {} },
228
+ allowQuestion: true
229
+ })
230
+
231
+ const parsed = parseJsonLoose(out.reply)
232
+ if (parsed && Array.isArray(parsed.qa)) {
233
+ for (const item of parsed.qa.slice(0, 10)) {
234
+ const q = String(item?.q || "").trim()
235
+ const a = String(item?.a || "").trim()
236
+ if (q || a) transcript.push({ q, a })
237
+ }
238
+ summary = String(parsed.summary || "").trim() || summary
239
+ const enough = Boolean(parsed.enough)
240
+ if (enough && i >= 2) break
241
+ continue
242
+ }
243
+
244
+ const fallbackLine = String(out.reply || "").trim()
245
+ if (fallbackLine) {
246
+ transcript.push({ q: `Round ${i} synthesis`, a: fallbackLine })
247
+ summary = fallbackLine
248
+ }
249
+ if (i >= 2) break
250
+ }
251
+
252
+ return {
253
+ transcript,
254
+ summary: summary || String(objective || "").trim()
255
+ }
256
+ }
257
+
258
+ export async function buildStagePlan({
259
+ objective,
260
+ intakeSummary = "",
261
+ model,
262
+ providerType,
263
+ sessionId,
264
+ configState,
265
+ baseUrl = null,
266
+ apiKeyEnv = null,
267
+ agent = null,
268
+ signal = null,
269
+ defaults = {}
270
+ }) {
271
+ const plannerPrompt = [
272
+ "You are the EXECUTION PLANNER for a production-grade parallel coding pipeline.",
273
+ "Generate a stage plan that will be executed by independent sub-agents running in parallel.",
274
+ "",
275
+ "Return STRICT JSON ONLY (no markdown wrapping, no explanation) with this schema:",
276
+ '{"planId":"...","objective":"...","stages":[{"stageId":"...","name":"...","passRule":"all_success","tasks":[{"taskId":"...","prompt":"...","plannedFiles":["..."],"acceptance":["..."],"timeoutMs":600000,"maxRetries":2,"complexity":"low|medium|high"}]}]}',
277
+ "",
278
+ "## Execution Model (understand this before planning)",
279
+ "",
280
+ "1. An ARCHITECT agent creates ALL plannedFiles as scaffolds with detailed inline comments (signatures + logic descriptions, no implementation)",
281
+ "2. Each task is assigned to an INDEPENDENT sub-agent that reads the scaffold and implements real code",
282
+ "3. Sub-agents within the same stage run IN PARALLEL they CANNOT see each other's work or communicate",
283
+ "4. Stages run SEQUENTIALLY stage N+1 starts only after ALL tasks in stage N succeed",
284
+ "5. If a task fails, it is retried up to maxRetries times. If all retries fail, the stage fails.",
285
+ "",
286
+ "## File Assignment Rules (violations cause runtime failures)",
287
+ "",
288
+ "- Files that import from each other MUST be in the SAME task (parallel agents cannot resolve cross-task imports)",
289
+ "- A module and its test file MUST be in the same task (tests need the implementation to exist)",
290
+ "- A component and its type definitions MUST be in the same task",
291
+ "- Each task should own 2-8 files. 1 file → merge with a related task. >10 files → split into smaller tasks",
292
+ "- NO file may appear in multiple tasks within a stage or across stages",
293
+ "- Shared types/interfaces that multiple tasks depend on put in stage 1 as a dedicated 'shared types' task",
294
+ "",
295
+ "## Stage Ordering Strategy",
296
+ "",
297
+ "- Stage 1: Shared types, interfaces, config schemas, utility functions (things others import from)",
298
+ "- Stage 2: Core business logic modules (independent of each other, depend on stage 1)",
299
+ "- Stage 3: Integration layer (routes, controllers, orchestrators that wire stage 2 modules together)",
300
+ "- Stage 4: Tests, validation, documentation (if not already co-located with their modules)",
301
+ "- Minimize stage count — only create a new stage when there is a REAL import dependency from a previous stage",
302
+ "- If all tasks are independent, use a SINGLE stage with multiple parallel tasks",
303
+ "",
304
+ "## Task Prompt Requirements (this is what the sub-agent sees)",
305
+ "",
306
+ "The sub-agent's ONLY context is: (1) your task prompt, (2) the scaffold file with inline comments, (3) the file ownership list.",
307
+ "The sub-agent has NO access to the original user objective, blueprint, or other tasks' prompts.",
308
+ "",
309
+ "Each task prompt MUST include:",
310
+ "1. WHAT to implement — specific behavior with concrete details, not vague goals",
311
+ "2. HOW to implement — key algorithms, data structures, patterns to use",
312
+ "3. INTEGRATION — what this task exports (function names, signatures), what format other tasks expect",
313
+ "4. ERROR HANDLING — what errors to throw/catch, how to handle edge cases (null, empty, invalid input)",
314
+ "5. TESTING — if test files are in this task, what test cases to write (happy path, error cases, edge cases)",
315
+ "",
316
+ "BAD prompt: 'implement the auth module'",
317
+ "GOOD prompt: 'Implement src/auth/jwt.mjs: generateToken(userId, role) returns signed JWT string using HS256 with 24h expiry, reading secret from process.env.JWT_SECRET. Throw AuthError if secret is missing. verifyToken(token) returns decoded payload or throws TokenExpiredError/InvalidTokenError. Export both functions as named exports. Test file: test happy path (valid token roundtrip), expired token, invalid signature, missing secret.'",
318
+ "",
319
+ "## Acceptance Criteria Rules",
320
+ "",
321
+ "- MUST be machine-verifiable commands or assertions:",
322
+ " - 'node --check src/auth/jwt.mjs' (syntax check)",
323
+ " - 'node --test test/auth.test.mjs' (test execution)",
324
+ " - 'grep -q \"export function generateToken\" src/auth/jwt.mjs' (API existence)",
325
+ "- NEVER subjective: 'code is clean', 'implementation is correct', 'works as expected'",
326
+ "- FINAL stage MUST include: 'all modified files parse without syntax errors' AND 'test suite passes'",
327
+ "",
328
+ "## Complexity Rating",
329
+ "",
330
+ "- low: simple CRUD, config changes, straightforward utility functions (< 100 lines total)",
331
+ "- medium: business logic with multiple code paths, moderate error handling (100-500 lines)",
332
+ "- high: complex algorithms, concurrent operations, extensive edge cases (> 500 lines)",
333
+ "",
334
+ `## Objective`,
335
+ objective,
336
+ intakeSummary ? `\n## Intake Summary\n${intakeSummary}` : ""
337
+ ].filter(Boolean).join("\n")
338
+
339
+ const out = await processTurnLoop({
340
+ prompt: plannerPrompt,
341
+ mode: "plan",
342
+ model,
343
+ providerType,
344
+ sessionId,
345
+ configState,
346
+ baseUrl,
347
+ apiKeyEnv,
348
+ agent,
349
+ signal,
350
+ output: { write: () => {} },
351
+ allowQuestion: true
352
+ })
353
+
354
+ const parsed = parseJsonLoose(out.reply)
355
+ if (!parsed) {
356
+ return {
357
+ plan: defaultStagePlan(objective, defaults),
358
+ errors: ["planner returned unparseable response — falling back to single-stage plan"],
359
+ qualityScore: 0,
360
+ rawReply: out.reply
361
+ }
362
+ }
363
+ const { plan, errors, qualityScore } = validateAndNormalizeStagePlan(parsed, { objective, defaults })
364
+ return { plan, errors, qualityScore, rawReply: out.reply }
365
+ }