@kkelly-offical/kkcode 0.1.2
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.
- package/LICENSE +674 -0
- package/README.md +445 -0
- package/package.json +46 -0
- package/src/agent/agent.mjs +170 -0
- package/src/agent/custom-agent-loader.mjs +158 -0
- package/src/agent/generator.mjs +115 -0
- package/src/agent/prompt/architect.txt +36 -0
- package/src/agent/prompt/build-fixer.txt +71 -0
- package/src/agent/prompt/build.txt +101 -0
- package/src/agent/prompt/compaction.txt +12 -0
- package/src/agent/prompt/explore.txt +29 -0
- package/src/agent/prompt/guide.txt +40 -0
- package/src/agent/prompt/longagent.txt +178 -0
- package/src/agent/prompt/plan.txt +50 -0
- package/src/agent/prompt/researcher.txt +23 -0
- package/src/agent/prompt/reviewer.txt +44 -0
- package/src/agent/prompt/security-reviewer.txt +62 -0
- package/src/agent/prompt/tdd-guide.txt +84 -0
- package/src/agent/prompt/title.txt +8 -0
- package/src/command/custom-commands.mjs +57 -0
- package/src/commands/agent.mjs +71 -0
- package/src/commands/audit.mjs +77 -0
- package/src/commands/background.mjs +86 -0
- package/src/commands/chat.mjs +114 -0
- package/src/commands/command.mjs +41 -0
- package/src/commands/config.mjs +44 -0
- package/src/commands/doctor.mjs +148 -0
- package/src/commands/hook.mjs +29 -0
- package/src/commands/init.mjs +141 -0
- package/src/commands/longagent.mjs +100 -0
- package/src/commands/mcp.mjs +89 -0
- package/src/commands/permission.mjs +36 -0
- package/src/commands/prompt.mjs +42 -0
- package/src/commands/review.mjs +266 -0
- package/src/commands/rule.mjs +34 -0
- package/src/commands/session.mjs +235 -0
- package/src/commands/theme.mjs +98 -0
- package/src/commands/usage.mjs +91 -0
- package/src/config/defaults.mjs +195 -0
- package/src/config/import-config.mjs +76 -0
- package/src/config/load-config.mjs +76 -0
- package/src/config/schema.mjs +509 -0
- package/src/context.mjs +40 -0
- package/src/core/constants.mjs +46 -0
- package/src/core/errors.mjs +57 -0
- package/src/core/events.mjs +29 -0
- package/src/core/types.mjs +57 -0
- package/src/github/api.mjs +78 -0
- package/src/github/auth.mjs +286 -0
- package/src/github/flow.mjs +298 -0
- package/src/github/workspace.mjs +212 -0
- package/src/index.mjs +82 -0
- package/src/knowledge/api-design.txt +9 -0
- package/src/knowledge/cpp.txt +10 -0
- package/src/knowledge/docker.txt +10 -0
- package/src/knowledge/dotnet.txt +9 -0
- package/src/knowledge/electron.txt +10 -0
- package/src/knowledge/flutter.txt +10 -0
- package/src/knowledge/go.txt +9 -0
- package/src/knowledge/graphql.txt +10 -0
- package/src/knowledge/java.txt +9 -0
- package/src/knowledge/kotlin.txt +10 -0
- package/src/knowledge/loader.mjs +125 -0
- package/src/knowledge/next.txt +8 -0
- package/src/knowledge/node.txt +8 -0
- package/src/knowledge/nuxt.txt +9 -0
- package/src/knowledge/php.txt +10 -0
- package/src/knowledge/python.txt +10 -0
- package/src/knowledge/react-native.txt +10 -0
- package/src/knowledge/react.txt +9 -0
- package/src/knowledge/ruby.txt +11 -0
- package/src/knowledge/rust.txt +9 -0
- package/src/knowledge/svelte.txt +9 -0
- package/src/knowledge/swift.txt +10 -0
- package/src/knowledge/tailwind.txt +10 -0
- package/src/knowledge/testing.txt +8 -0
- package/src/knowledge/typescript.txt +8 -0
- package/src/knowledge/vue.txt +9 -0
- package/src/mcp/client-http.mjs +157 -0
- package/src/mcp/client-sse.mjs +286 -0
- package/src/mcp/client-stdio.mjs +451 -0
- package/src/mcp/registry.mjs +394 -0
- package/src/mcp/stdio-framing.mjs +127 -0
- package/src/orchestration/background-manager.mjs +358 -0
- package/src/orchestration/background-worker.mjs +245 -0
- package/src/orchestration/longagent-manager.mjs +116 -0
- package/src/orchestration/stage-scheduler.mjs +489 -0
- package/src/orchestration/subagent-router.mjs +62 -0
- package/src/orchestration/task-scheduler.mjs +74 -0
- package/src/permission/engine.mjs +92 -0
- package/src/permission/exec-policy.mjs +372 -0
- package/src/permission/prompt.mjs +39 -0
- package/src/permission/rules.mjs +120 -0
- package/src/permission/workspace-trust.mjs +44 -0
- package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
- package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
- package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
- package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
- package/src/plugin/hook-bus.mjs +154 -0
- package/src/provider/anthropic.mjs +389 -0
- package/src/provider/ollama.mjs +236 -0
- package/src/provider/openai-compatible.mjs +1 -0
- package/src/provider/openai.mjs +339 -0
- package/src/provider/retry-policy.mjs +68 -0
- package/src/provider/router.mjs +228 -0
- package/src/provider/sse.mjs +91 -0
- package/src/repl.mjs +2929 -0
- package/src/review/diff-parser.mjs +36 -0
- package/src/review/rejection-queue.mjs +62 -0
- package/src/review/review-store.mjs +21 -0
- package/src/review/risk-score.mjs +61 -0
- package/src/rules/load-rules.mjs +64 -0
- package/src/runtime.mjs +1 -0
- package/src/session/checkpoint.mjs +239 -0
- package/src/session/compaction.mjs +276 -0
- package/src/session/engine.mjs +225 -0
- package/src/session/instinct-manager.mjs +172 -0
- package/src/session/instruction-loader.mjs +25 -0
- package/src/session/longagent-plan.mjs +329 -0
- package/src/session/longagent-scaffold.mjs +100 -0
- package/src/session/longagent.mjs +1462 -0
- package/src/session/loop.mjs +905 -0
- package/src/session/memory-loader.mjs +75 -0
- package/src/session/project-context.mjs +367 -0
- package/src/session/prompt/anthropic.txt +151 -0
- package/src/session/prompt/beast.txt +37 -0
- package/src/session/prompt/max-steps.txt +6 -0
- package/src/session/prompt/plan.txt +9 -0
- package/src/session/prompt/qwen.txt +46 -0
- package/src/session/prompt-loader.mjs +18 -0
- package/src/session/recovery.mjs +52 -0
- package/src/session/store.mjs +503 -0
- package/src/session/system-prompt.mjs +260 -0
- package/src/session/task-validator.mjs +266 -0
- package/src/session/usability-gates.mjs +379 -0
- package/src/skill/builtin/backend-patterns.mjs +123 -0
- package/src/skill/builtin/commit.mjs +64 -0
- package/src/skill/builtin/debug.mjs +45 -0
- package/src/skill/builtin/frontend-patterns.mjs +120 -0
- package/src/skill/builtin/frontend.mjs +188 -0
- package/src/skill/builtin/init.mjs +220 -0
- package/src/skill/builtin/review.mjs +49 -0
- package/src/skill/builtin/security-checklist.mjs +80 -0
- package/src/skill/builtin/tdd.mjs +54 -0
- package/src/skill/generator.mjs +113 -0
- package/src/skill/registry.mjs +336 -0
- package/src/storage/audit-store.mjs +83 -0
- package/src/storage/event-log.mjs +82 -0
- package/src/storage/ghost-commit-store.mjs +235 -0
- package/src/storage/json-store.mjs +53 -0
- package/src/storage/paths.mjs +148 -0
- package/src/theme/color.mjs +64 -0
- package/src/theme/default-theme.mjs +29 -0
- package/src/theme/load-theme.mjs +71 -0
- package/src/theme/markdown.mjs +135 -0
- package/src/theme/schema.mjs +45 -0
- package/src/theme/status-bar.mjs +158 -0
- package/src/tool/audit-wrapper.mjs +38 -0
- package/src/tool/edit-transaction.mjs +126 -0
- package/src/tool/executor.mjs +109 -0
- package/src/tool/file-lock-manager.mjs +85 -0
- package/src/tool/git-auto.mjs +545 -0
- package/src/tool/git-full-auto.mjs +478 -0
- package/src/tool/image-util.mjs +276 -0
- package/src/tool/prompt/background_cancel.txt +1 -0
- package/src/tool/prompt/background_output.txt +1 -0
- package/src/tool/prompt/bash.txt +71 -0
- package/src/tool/prompt/codesearch.txt +18 -0
- package/src/tool/prompt/edit.txt +27 -0
- package/src/tool/prompt/enter_plan.txt +74 -0
- package/src/tool/prompt/exit_plan.txt +62 -0
- package/src/tool/prompt/glob.txt +33 -0
- package/src/tool/prompt/grep.txt +43 -0
- package/src/tool/prompt/list.txt +8 -0
- package/src/tool/prompt/multiedit.txt +20 -0
- package/src/tool/prompt/notebookedit.txt +21 -0
- package/src/tool/prompt/patch.txt +24 -0
- package/src/tool/prompt/question.txt +44 -0
- package/src/tool/prompt/read.txt +40 -0
- package/src/tool/prompt/task.txt +83 -0
- package/src/tool/prompt/todowrite.txt +117 -0
- package/src/tool/prompt/webfetch.txt +38 -0
- package/src/tool/prompt/websearch.txt +43 -0
- package/src/tool/prompt/write.txt +38 -0
- package/src/tool/prompt-loader.mjs +18 -0
- package/src/tool/question-prompt.mjs +86 -0
- package/src/tool/registry.mjs +1309 -0
- package/src/tool/task-tool.mjs +28 -0
- package/src/ui/activity-renderer.mjs +410 -0
- package/src/ui/repl-dashboard.mjs +357 -0
- package/src/usage/pricing.mjs +121 -0
- package/src/usage/usage-meter.mjs +113 -0
- package/src/util/git.mjs +496 -0
- package/src/util/template.mjs +10 -0
- package/src/util/yaml.mjs +100 -0
|
@@ -0,0 +1,329 @@
|
|
|
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 check — detect 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
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { processTurnLoop } from "./loop.mjs"
|
|
2
|
+
import { EventBus } from "../core/events.mjs"
|
|
3
|
+
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
4
|
+
|
|
5
|
+
function buildScaffoldPrompt(objective, stagePlan) {
|
|
6
|
+
const allFiles = new Set()
|
|
7
|
+
const taskSpecs = []
|
|
8
|
+
|
|
9
|
+
for (const stage of stagePlan.stages || []) {
|
|
10
|
+
for (const task of stage.tasks || []) {
|
|
11
|
+
for (const file of task.plannedFiles || []) {
|
|
12
|
+
allFiles.add(file)
|
|
13
|
+
}
|
|
14
|
+
taskSpecs.push({
|
|
15
|
+
taskId: task.taskId,
|
|
16
|
+
prompt: task.prompt,
|
|
17
|
+
plannedFiles: task.plannedFiles,
|
|
18
|
+
acceptance: task.acceptance
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const fileList = [...allFiles].sort()
|
|
24
|
+
if (!fileList.length) return null
|
|
25
|
+
|
|
26
|
+
return [
|
|
27
|
+
"You are in SCAFFOLDING mode. Your job is to create stub files that define contracts for parallel implementation agents.",
|
|
28
|
+
"",
|
|
29
|
+
`## Objective: ${objective}`,
|
|
30
|
+
"",
|
|
31
|
+
"## Files to scaffold:",
|
|
32
|
+
...fileList.map((f) => `- ${f}`),
|
|
33
|
+
"",
|
|
34
|
+
"## Task specifications (for context):",
|
|
35
|
+
JSON.stringify(taskSpecs, null, 2),
|
|
36
|
+
"",
|
|
37
|
+
"## Scaffolding rules:",
|
|
38
|
+
"1. Create EVERY file listed above using the `write` tool.",
|
|
39
|
+
"2. For Python files: write function/class signatures with docstrings describing inputs, outputs, and behavior. Use `pass` or `...` as body.",
|
|
40
|
+
"3. For Vue/React components: write component stubs with props/events/slots documented in comments. Include the component shell structure.",
|
|
41
|
+
"4. For TypeScript/JavaScript: write export signatures with JSDoc or TSDoc describing the interface contract.",
|
|
42
|
+
"5. For config/data files: write the expected schema structure with placeholder values and comments.",
|
|
43
|
+
"6. For test files: write test suite structure with describe/it blocks and comments describing what each test should verify.",
|
|
44
|
+
"7. Each file MUST have a header comment explaining: purpose, dependencies, interfaces it implements/exports.",
|
|
45
|
+
"8. Do NOT implement business logic. Only define the contract (signatures, types, interfaces).",
|
|
46
|
+
"9. Include import statements that reference other scaffolded files so the dependency graph is explicit.",
|
|
47
|
+
"10. When done with ALL files, say [SCAFFOLD_COMPLETE].",
|
|
48
|
+
"",
|
|
49
|
+
"Start creating all stub files now."
|
|
50
|
+
].join("\n")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function runScaffoldPhase({
|
|
54
|
+
objective,
|
|
55
|
+
stagePlan,
|
|
56
|
+
model,
|
|
57
|
+
providerType,
|
|
58
|
+
sessionId,
|
|
59
|
+
configState,
|
|
60
|
+
baseUrl = null,
|
|
61
|
+
apiKeyEnv = null,
|
|
62
|
+
agent = null,
|
|
63
|
+
signal = null,
|
|
64
|
+
toolContext = {}
|
|
65
|
+
}) {
|
|
66
|
+
const prompt = buildScaffoldPrompt(objective, stagePlan)
|
|
67
|
+
if (!prompt) {
|
|
68
|
+
return { scaffolded: false, fileCount: 0, files: [], errors: [] }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const out = await processTurnLoop({
|
|
72
|
+
prompt,
|
|
73
|
+
mode: "agent",
|
|
74
|
+
model,
|
|
75
|
+
providerType,
|
|
76
|
+
sessionId,
|
|
77
|
+
configState,
|
|
78
|
+
baseUrl,
|
|
79
|
+
apiKeyEnv,
|
|
80
|
+
agent,
|
|
81
|
+
signal,
|
|
82
|
+
allowQuestion: false,
|
|
83
|
+
toolContext
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Extract files created from tool events
|
|
87
|
+
const createdFiles = (out.toolEvents || [])
|
|
88
|
+
.filter((e) => e.name === "write" && e.status === "completed")
|
|
89
|
+
.map((e) => e.args?.path)
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
scaffolded: true,
|
|
94
|
+
fileCount: createdFiles.length,
|
|
95
|
+
files: createdFiles,
|
|
96
|
+
usage: out.usage,
|
|
97
|
+
toolEvents: out.toolEvents,
|
|
98
|
+
errors: []
|
|
99
|
+
}
|
|
100
|
+
}
|