@kkelly-offical/kkcode 0.1.7 → 0.2.3-preview.1

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 (166) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +474 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +228 -220
  5. package/src/agent/custom-agent-loader.mjs +6 -3
  6. package/src/agent/generator.mjs +2 -2
  7. package/src/agent/prompt/assistant.txt +12 -0
  8. package/src/agent/prompt/bug-hunter.txt +89 -89
  9. package/src/agent/prompt/frontend-designer.txt +58 -58
  10. package/src/agent/prompt/guide.txt +1 -1
  11. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
  12. package/src/agent/prompt/longagent-coding-agent.txt +37 -37
  13. package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
  14. package/src/agent/prompt/longagent-preview-agent.txt +63 -63
  15. package/src/command/custom-commands.mjs +2 -2
  16. package/src/commands/agent.mjs +1 -1
  17. package/src/commands/background.mjs +145 -4
  18. package/src/commands/chat.mjs +117 -76
  19. package/src/commands/config.mjs +148 -1
  20. package/src/commands/doctor.mjs +30 -6
  21. package/src/commands/init.mjs +32 -6
  22. package/src/commands/longagent.mjs +117 -0
  23. package/src/commands/mcp.mjs +275 -43
  24. package/src/commands/permission.mjs +1 -1
  25. package/src/commands/session.mjs +195 -140
  26. package/src/commands/skill.mjs +63 -0
  27. package/src/commands/theme.mjs +1 -1
  28. package/src/commands/update.mjs +32 -0
  29. package/src/config/defaults.mjs +289 -260
  30. package/src/config/import-config.mjs +1 -1
  31. package/src/config/load-config.mjs +61 -4
  32. package/src/config/schema.mjs +604 -574
  33. package/src/context.mjs +4 -1
  34. package/src/core/constants.mjs +97 -91
  35. package/src/core/types.mjs +1 -1
  36. package/src/github/api.mjs +78 -78
  37. package/src/github/auth.mjs +294 -286
  38. package/src/github/flow.mjs +298 -298
  39. package/src/github/workspace.mjs +225 -212
  40. package/src/index.mjs +87 -82
  41. package/src/knowledge/frontend-aesthetics.txt +38 -38
  42. package/src/mcp/client-http.mjs +139 -141
  43. package/src/mcp/client-sse.mjs +297 -288
  44. package/src/mcp/client-stdio.mjs +534 -533
  45. package/src/mcp/constants.mjs +4 -2
  46. package/src/mcp/registry.mjs +498 -479
  47. package/src/mcp/stdio-framing.mjs +135 -133
  48. package/src/mcp/tool-result.mjs +24 -24
  49. package/src/observability/edit-diagnostics.mjs +449 -0
  50. package/src/observability/index.mjs +42 -42
  51. package/src/observability/metrics.mjs +165 -137
  52. package/src/observability/tracer.mjs +137 -137
  53. package/src/onboarding.mjs +209 -0
  54. package/src/orchestration/background-manager.mjs +567 -372
  55. package/src/orchestration/background-worker.mjs +419 -305
  56. package/src/orchestration/interruption-reason.mjs +21 -0
  57. package/src/orchestration/longagent-manager.mjs +197 -171
  58. package/src/orchestration/stage-scheduler.mjs +733 -728
  59. package/src/orchestration/subagent-router.mjs +7 -1
  60. package/src/orchestration/task-scheduler.mjs +219 -7
  61. package/src/permission/engine.mjs +1 -1
  62. package/src/permission/exec-policy.mjs +370 -370
  63. package/src/permission/file-edit-policy.mjs +108 -0
  64. package/src/permission/prompt.mjs +1 -1
  65. package/src/permission/rules.mjs +116 -7
  66. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  67. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  68. package/src/plugin/hook-bus.mjs +19 -5
  69. package/src/plugin/manifest-loader.mjs +222 -0
  70. package/src/provider/anthropic.mjs +396 -390
  71. package/src/provider/ollama.mjs +7 -1
  72. package/src/provider/openai.mjs +382 -340
  73. package/src/provider/retry-policy.mjs +74 -68
  74. package/src/provider/router.mjs +242 -241
  75. package/src/provider/sse.mjs +104 -104
  76. package/src/provider/wizard.mjs +556 -0
  77. package/src/repl/capability-facade.mjs +30 -0
  78. package/src/repl/command-surface.mjs +23 -0
  79. package/src/repl/controller-entry.mjs +40 -0
  80. package/src/repl/core-shell.mjs +208 -0
  81. package/src/repl/dialog-router.mjs +87 -0
  82. package/src/repl/input-engine.mjs +76 -0
  83. package/src/repl/keymap.mjs +7 -0
  84. package/src/repl/operator-surface.mjs +15 -0
  85. package/src/repl/permission-flow.mjs +49 -0
  86. package/src/repl/runtime-facade.mjs +36 -0
  87. package/src/repl/slash-router.mjs +62 -0
  88. package/src/repl/state-store.mjs +29 -0
  89. package/src/repl/turn-controller.mjs +58 -0
  90. package/src/repl/verification.mjs +23 -0
  91. package/src/repl.mjs +3371 -2981
  92. package/src/rules/load-rules.mjs +3 -3
  93. package/src/runtime.mjs +1 -1
  94. package/src/session/agent-transaction.mjs +86 -0
  95. package/src/session/checkpoint.mjs +302 -302
  96. package/src/session/compaction.mjs +298 -298
  97. package/src/session/engine.mjs +417 -232
  98. package/src/session/longagent-4stage.mjs +467 -460
  99. package/src/session/longagent-hybrid.mjs +1344 -1097
  100. package/src/session/longagent-plan.mjs +376 -365
  101. package/src/session/longagent-project-memory.mjs +53 -53
  102. package/src/session/longagent-scaffold.mjs +291 -291
  103. package/src/session/longagent-task-bus.mjs +138 -54
  104. package/src/session/longagent-utils.mjs +828 -472
  105. package/src/session/longagent.mjs +911 -900
  106. package/src/session/loop.mjs +1005 -930
  107. package/src/session/prompt/agent.txt +25 -25
  108. package/src/session/prompt/anthropic.txt +150 -150
  109. package/src/session/prompt/beast.txt +1 -1
  110. package/src/session/prompt/plan.txt +31 -31
  111. package/src/session/prompt/qwen.txt +46 -46
  112. package/src/session/recovery.mjs +21 -0
  113. package/src/session/rollback.mjs +196 -195
  114. package/src/session/routing-observability.mjs +72 -0
  115. package/src/session/runtime-state.mjs +47 -0
  116. package/src/session/store.mjs +523 -519
  117. package/src/session/system-prompt.mjs +308 -273
  118. package/src/session/task-validator.mjs +267 -267
  119. package/src/session/usability-gates.mjs +2 -2
  120. package/src/skill/builtin/commit.mjs +64 -64
  121. package/src/skill/builtin/design.mjs +76 -76
  122. package/src/skill/generator.mjs +18 -2
  123. package/src/skill/registry.mjs +642 -390
  124. package/src/storage/audit-store.mjs +18 -11
  125. package/src/storage/event-log.mjs +7 -1
  126. package/src/storage/ghost-commit-store.mjs +243 -245
  127. package/src/storage/paths.mjs +17 -0
  128. package/src/theme/default-theme.mjs +1 -1
  129. package/src/theme/markdown.mjs +4 -0
  130. package/src/theme/schema.mjs +1 -1
  131. package/src/theme/status-bar.mjs +162 -158
  132. package/src/tool/audit-wrapper.mjs +18 -2
  133. package/src/tool/edit-transaction.mjs +23 -0
  134. package/src/tool/executor.mjs +26 -1
  135. package/src/tool/file-read-state.mjs +65 -0
  136. package/src/tool/git-auto.mjs +526 -526
  137. package/src/tool/git-full-auto.mjs +487 -478
  138. package/src/tool/mutation-guard.mjs +54 -0
  139. package/src/tool/prompt/edit.txt +3 -3
  140. package/src/tool/prompt/multiedit.txt +1 -0
  141. package/src/tool/prompt/notebookedit.txt +2 -1
  142. package/src/tool/prompt/patch.txt +25 -24
  143. package/src/tool/prompt/read.txt +3 -3
  144. package/src/tool/prompt/sysinfo.txt +29 -0
  145. package/src/tool/prompt/task.txt +66 -4
  146. package/src/tool/prompt/write.txt +2 -2
  147. package/src/tool/question-prompt.mjs +99 -93
  148. package/src/tool/registry.mjs +1701 -1343
  149. package/src/tool/task-tool.mjs +14 -6
  150. package/src/ui/activity-renderer.mjs +667 -664
  151. package/src/ui/repl-background-panel.mjs +7 -0
  152. package/src/ui/repl-capability-panel.mjs +9 -0
  153. package/src/ui/repl-dashboard.mjs +54 -4
  154. package/src/ui/repl-help.mjs +110 -0
  155. package/src/ui/repl-operator-panel.mjs +12 -0
  156. package/src/ui/repl-route-feedback.mjs +35 -0
  157. package/src/ui/repl-status-view.mjs +76 -0
  158. package/src/ui/repl-task-panel.mjs +5 -0
  159. package/src/ui/repl-transcript-panel.mjs +56 -0
  160. package/src/ui/repl-turn-summary.mjs +135 -0
  161. package/src/update/checker.mjs +184 -0
  162. package/src/usage/pricing.mjs +122 -121
  163. package/src/usage/usage-meter.mjs +1 -0
  164. package/src/util/git.mjs +562 -519
  165. package/src/util/template.mjs +6 -1
  166. package/src/version.mjs +3 -0
@@ -1,365 +1,376 @@
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. INTEGRATIONwhat 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
- }
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
+ // Downgraded to warnings — cross-stage overlap is common (e.g. shared config) and should not block the plan
116
+ const warnings = []
117
+ const globalOwnership = new Map()
118
+ for (const stage of plan.stages) {
119
+ for (const task of stage.tasks) {
120
+ for (const file of task.plannedFiles || []) {
121
+ if (globalOwnership.has(file)) {
122
+ const prev = globalOwnership.get(file)
123
+ warnings.push(`file "${file}" appears in stage "${prev}" and "${stage.stageId}"`)
124
+ } else {
125
+ globalOwnership.set(file, stage.stageId)
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ // Quality score: penalize tasks with no files or no acceptance criteria
132
+ let qualityScore = 100
133
+ let totalTasks = 0
134
+ for (const stage of plan.stages) {
135
+ for (const task of stage.tasks) {
136
+ totalTasks += 1
137
+ if (!task.plannedFiles.length) qualityScore -= 15
138
+ if (!task.acceptance.length) qualityScore -= 10
139
+ }
140
+ }
141
+ // Cross-stage file overlap: penalize quality but don't block
142
+ qualityScore -= warnings.length * 5
143
+ qualityScore = Math.max(0, Math.min(100, qualityScore))
144
+
145
+ if (errors.length) {
146
+ // If the parsed plan has at least one valid stage with tasks, keep it despite warnings
147
+ const hasValidStages = plan.stages.some(s => s.tasks.length > 0)
148
+ if (hasValidStages) {
149
+ // Filter out empty stages but preserve valid ones
150
+ plan.stages = plan.stages.filter(s => s.tasks.length > 0)
151
+ return { plan, errors, warnings, qualityScore: Math.max(0, qualityScore - errors.length * 10) }
152
+ }
153
+ return {
154
+ plan: defaultStagePlan(objective || plan.objective, defaults),
155
+ errors, warnings,
156
+ qualityScore: 0
157
+ }
158
+ }
159
+ return { plan, errors: [], warnings, qualityScore }
160
+ }
161
+
162
+ export async function runIntakeDialogue({
163
+ objective,
164
+ model,
165
+ providerType,
166
+ sessionId,
167
+ configState,
168
+ baseUrl = null,
169
+ apiKeyEnv = null,
170
+ agent = null,
171
+ signal = null,
172
+ maxRounds = 6
173
+ }) {
174
+ const rounds = Math.max(1, Number(maxRounds || 6))
175
+ const transcript = []
176
+ let summary = ""
177
+
178
+ for (let i = 1; i <= rounds; i++) {
179
+ const prompt = [
180
+ "You are the INTAKE ANALYST for a production-grade parallel coding pipeline.",
181
+ "Multiple independent sub-agents will execute tasks IN PARALLEL they cannot communicate with each other during execution.",
182
+ "Your job: resolve ALL ambiguities NOW so that parallel agents produce compatible, integrable code.",
183
+ "",
184
+ "## Analysis Categories (address ALL that apply)",
185
+ "",
186
+ "### 1. SCOPE & BOUNDARIES",
187
+ "- What files/modules/directories are IN scope for modification?",
188
+ "- What existing code must NOT be changed (public APIs, shared interfaces, config schemas)?",
189
+ "- Are there related features that should be explicitly excluded to prevent scope creep?",
190
+ "",
191
+ "### 2. TECHNOLOGY & PATTERNS",
192
+ "- What language version, runtime, and framework constraints apply?",
193
+ "- What existing patterns in the codebase MUST be followed (error handling, logging, naming, async style)?",
194
+ "- What existing utilities/helpers MUST be reused instead of reimplemented?",
195
+ "- Are there specific libraries to use or avoid?",
196
+ "",
197
+ "### 3. INTERFACE CONTRACTS",
198
+ "- What are the exact function signatures, parameter types, and return types?",
199
+ "- What data schemas are involved (DB models, API payloads, config shapes)?",
200
+ "- What error types should be thrown/returned and how should callers handle them?",
201
+ "- What events/hooks/callbacks are part of the contract?",
202
+ "",
203
+ "### 4. QUALITY CONSTRAINTS",
204
+ "- What test coverage is expected (unit, integration, e2e)?",
205
+ "- What backward compatibility guarantees must be maintained?",
206
+ "- Are there performance budgets (latency, memory, bundle size)?",
207
+ "- What security considerations apply (input validation, auth, secrets)?",
208
+ "",
209
+ "### 5. DEPENDENCY ORDER",
210
+ "- Which components must exist before others can be built?",
211
+ "- Which components are independent and can be built in parallel?",
212
+ "- Are there shared types/interfaces that must be defined first?",
213
+ "",
214
+ "## Output Rules",
215
+ "- For each question, provide your BEST ASSUMPTION as the answer based on the objective and codebase context.",
216
+ "- Be specific: 'use HS256 JWT with 24h expiry' not 'implement auth'.",
217
+ "- Set enough=true ONLY when you have sufficient clarity to generate a concrete file-level execution plan.",
218
+ "",
219
+ "Return STRICT JSON (no markdown wrapping):",
220
+ `{"enough":boolean,"summary":"technical summary with ALL resolved assumptions and concrete decisions","qa":[{"q":"specific question","a":"concrete answer with implementation detail"}]}`,
221
+ "",
222
+ `Round: ${i}/${rounds}`,
223
+ `Objective: ${objective}`,
224
+ summary ? `Previous summary: ${summary}` : ""
225
+ ].filter(Boolean).join("\n")
226
+
227
+ const out = await processTurnLoop({
228
+ prompt,
229
+ mode: "assistant",
230
+ model,
231
+ providerType,
232
+ sessionId,
233
+ configState,
234
+ baseUrl,
235
+ apiKeyEnv,
236
+ agent,
237
+ signal,
238
+ output: { write: () => {} },
239
+ allowQuestion: true
240
+ })
241
+
242
+ const parsed = parseJsonLoose(out.reply)
243
+ if (parsed && Array.isArray(parsed.qa)) {
244
+ for (const item of parsed.qa.slice(0, 10)) {
245
+ const q = String(item?.q || "").trim()
246
+ const a = String(item?.a || "").trim()
247
+ if (q || a) transcript.push({ q, a })
248
+ }
249
+ summary = String(parsed.summary || "").trim() || summary
250
+ const enough = Boolean(parsed.enough)
251
+ if (enough && i >= 2) break
252
+ continue
253
+ }
254
+
255
+ const fallbackLine = String(out.reply || "").trim()
256
+ if (fallbackLine) {
257
+ transcript.push({ q: `Round ${i} synthesis`, a: fallbackLine })
258
+ summary = fallbackLine
259
+ }
260
+ if (i >= 2) break
261
+ }
262
+
263
+ return {
264
+ transcript,
265
+ summary: summary || String(objective || "").trim()
266
+ }
267
+ }
268
+
269
+ export async function buildStagePlan({
270
+ objective,
271
+ intakeSummary = "",
272
+ model,
273
+ providerType,
274
+ sessionId,
275
+ configState,
276
+ baseUrl = null,
277
+ apiKeyEnv = null,
278
+ agent = null,
279
+ signal = null,
280
+ defaults = {}
281
+ }) {
282
+ const plannerPrompt = [
283
+ "You are the EXECUTION PLANNER for a production-grade parallel coding pipeline.",
284
+ "Generate a stage plan that will be executed by independent sub-agents running in parallel.",
285
+ "",
286
+ "Return STRICT JSON ONLY (no markdown wrapping, no explanation) with this schema:",
287
+ '{"planId":"...","objective":"...","stages":[{"stageId":"...","name":"...","passRule":"all_success","tasks":[{"taskId":"...","prompt":"...","plannedFiles":["..."],"acceptance":["..."],"timeoutMs":600000,"maxRetries":2,"complexity":"low|medium|high"}]}]}',
288
+ "",
289
+ "## Execution Model (understand this before planning)",
290
+ "",
291
+ "1. An ARCHITECT agent creates ALL plannedFiles as scaffolds with detailed inline comments (signatures + logic descriptions, no implementation)",
292
+ "2. Each task is assigned to an INDEPENDENT sub-agent that reads the scaffold and implements real code",
293
+ "3. Sub-agents within the same stage run IN PARALLEL they CANNOT see each other's work or communicate",
294
+ "4. Stages run SEQUENTIALLY — stage N+1 starts only after ALL tasks in stage N succeed",
295
+ "5. If a task fails, it is retried up to maxRetries times. If all retries fail, the stage fails.",
296
+ "",
297
+ "## File Assignment Rules (violations cause runtime failures)",
298
+ "",
299
+ "- Files that import from each other MUST be in the SAME task (parallel agents cannot resolve cross-task imports)",
300
+ "- A module and its test file MUST be in the same task (tests need the implementation to exist)",
301
+ "- A component and its type definitions MUST be in the same task",
302
+ "- Each task should own 2-8 files. 1 file merge with a related task. >10 files → split into smaller tasks",
303
+ "- NO file may appear in multiple tasks within a stage or across stages",
304
+ "- Shared types/interfaces that multiple tasks depend on put in stage 1 as a dedicated 'shared types' task",
305
+ "",
306
+ "## Stage Ordering Strategy",
307
+ "",
308
+ "- Stage 1: Shared types, interfaces, config schemas, utility functions (things others import from)",
309
+ "- Stage 2: Core business logic modules (independent of each other, depend on stage 1)",
310
+ "- Stage 3: Integration layer (routes, controllers, orchestrators that wire stage 2 modules together)",
311
+ "- Stage 4: Tests, validation, documentation (if not already co-located with their modules)",
312
+ "- Minimize stage count only create a new stage when there is a REAL import dependency from a previous stage",
313
+ "- If all tasks are independent, use a SINGLE stage with multiple parallel tasks",
314
+ "",
315
+ "## Task Prompt Requirements (this is what the sub-agent sees)",
316
+ "",
317
+ "The sub-agent's ONLY context is: (1) your task prompt, (2) the scaffold file with inline comments, (3) the file ownership list.",
318
+ "The sub-agent has NO access to the original user objective, blueprint, or other tasks' prompts.",
319
+ "",
320
+ "Each task prompt MUST include:",
321
+ "1. WHAT to implement specific behavior with concrete details, not vague goals",
322
+ "2. HOW to implement key algorithms, data structures, patterns to use",
323
+ "3. INTEGRATION what this task exports (function names, signatures), what format other tasks expect",
324
+ "4. ERROR HANDLING what errors to throw/catch, how to handle edge cases (null, empty, invalid input)",
325
+ "5. TESTING if test files are in this task, what test cases to write (happy path, error cases, edge cases)",
326
+ "",
327
+ "BAD prompt: 'implement the auth module'",
328
+ "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.'",
329
+ "",
330
+ "## Acceptance Criteria Rules",
331
+ "",
332
+ "- MUST be machine-verifiable commands or assertions:",
333
+ " - 'node --check src/auth/jwt.mjs' (syntax check)",
334
+ " - 'node --test test/auth.test.mjs' (test execution)",
335
+ " - 'grep -q \"export function generateToken\" src/auth/jwt.mjs' (API existence)",
336
+ "- NEVER subjective: 'code is clean', 'implementation is correct', 'works as expected'",
337
+ "- FINAL stage MUST include: 'all modified files parse without syntax errors' AND 'test suite passes'",
338
+ "",
339
+ "## Complexity Rating",
340
+ "",
341
+ "- low: simple CRUD, config changes, straightforward utility functions (< 100 lines total)",
342
+ "- medium: business logic with multiple code paths, moderate error handling (100-500 lines)",
343
+ "- high: complex algorithms, concurrent operations, extensive edge cases (> 500 lines)",
344
+ "",
345
+ `## Objective`,
346
+ objective,
347
+ intakeSummary ? `\n## Intake Summary\n${intakeSummary}` : ""
348
+ ].filter(Boolean).join("\n")
349
+
350
+ const out = await processTurnLoop({
351
+ prompt: plannerPrompt,
352
+ mode: "plan",
353
+ model,
354
+ providerType,
355
+ sessionId,
356
+ configState,
357
+ baseUrl,
358
+ apiKeyEnv,
359
+ agent,
360
+ signal,
361
+ output: { write: () => {} },
362
+ allowQuestion: true
363
+ })
364
+
365
+ const parsed = parseJsonLoose(out.reply)
366
+ if (!parsed) {
367
+ return {
368
+ plan: defaultStagePlan(objective, defaults),
369
+ errors: ["planner returned unparseable response — falling back to single-stage plan"],
370
+ qualityScore: 0,
371
+ rawReply: out.reply
372
+ }
373
+ }
374
+ const { plan, errors, qualityScore } = validateAndNormalizeStagePlan(parsed, { objective, defaults })
375
+ return { plan, errors, qualityScore, rawReply: out.reply }
376
+ }