Skip to content

Commit e68586c

Browse files
suleimanshclaude
andcommitted
The greenfield flow is gone: a build always extends the existing codebase (#1683 review)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent c5f419e commit e68586c

13 files changed

Lines changed: 29 additions & 231 deletions

packages/framework/prompts/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ is written in TypeScript any more, so prompting can change without touching the
66
| file | what it is |
77
|---|---|
88
| `system_prompt.md` | The built-in system prompt (#326). Rom's doc. |
9-
| `build_prompt.md` / `extend_prompt.md` / `scaffold_prompt.md` | The prompts a build session opens with (#1347): greenfield, existing codebase (#185), and the scaffold retry (#182). `${{ tf.prompt }}` is the user's intent. |
9+
| `extend_prompt.md` | The prompt a build session opens with (#1347): deliver the intent within the existing codebase (#185). `${{ tf.prompt }}` is the user's intent. |
1010
| `protocols/await.md` | How to emit an awaited choice so the turn-boundary gate can detect it (#337/#339). |
1111
| `protocols/signal.md` | How to emit `setSessionName()` / `setReadyForMerge()` (#326). |
1212
| `presets/*.md` | One file per preset button: research (#331), readability (#360), maintainability (#361), security_audit (#461), ux (#472). |

packages/framework/prompts/SPEC.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Every prompt The Framework sends an agent lives here as markdown. Nothing agent-
1313
- **The data-branch protocol** - where the framework's own data lives and how to read and write it without putting it on a code branch.
1414
- **The protocols** - how an agent signals: awaited choices, session name and ready for merge, plus the sections added only when it has a browser, when it runs hands-off, and when nothing can answer its questions.
1515
- **The presets** - one file per launcher button and per routine prompt.
16-
- **The build-opening prompts** - the three framings a build agent can open with: the greenfield build, the existing-codebase variant, and the scaffold retry for a build that produced nothing.
16+
- **The build-opening prompt** - the one framing a build agent opens with: deliver the work within the existing codebase.
1717
- **The on-before-mergeable prompt** - the optional extra turn a finished agent gets, queueing quality follow-ups and folding what it learned into the knowledge base.
1818
- **Prompts are reviewed before they land** - a prompt change goes through review like any other change.
1919

@@ -47,7 +47,7 @@ See `## User story`.
4747
- **The triage scope rule** — the one-paragraph rule appended to both triage presets, that a triage only queues work and never implements it.
4848
- **The protocols** — how an agent signals to The Framework, and what this particular agent can do.
4949
- **The presets** — one file per preset: the launcher's buttons and the daemon's routine prompts.
50-
- **The build-opening prompts** — the greenfield build, the existing-codebase variant chosen when the workspace already holds source, and the scaffold retry sent when a build's opening turn left the workspace empty.
50+
- **The build-opening prompt** — the existing-codebase framing every build agent opens with: a project is a repo that already exists.
5151
- **The on-before-mergeable prompt** — the extra turn an agent gets when it signals ready for merge, if the user turned that on.
5252

5353
### Prompts are reviewed before they land

packages/framework/prompts/build_prompt.SPEC.md

Lines changed: 0 additions & 13 deletions
This file was deleted.

packages/framework/prompts/build_prompt.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/framework/prompts/scaffold_prompt.SPEC.md

Lines changed: 0 additions & 13 deletions
This file was deleted.

packages/framework/prompts/scaffold_prompt.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/framework/src/agent.test.ts

Lines changed: 7 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ test('a build turn that stops to ask fires a live gate and resumes on the pick (
267267
'```'
268268
const driver = new FakeDriver({
269269
respond: (prompt: string): string => {
270-
if (/Build this app end to end/.test(prompt)) return awaitBlock // the build stops to ask
270+
if (/Work within the existing codebase/.test(prompt)) return awaitBlock // the build stops to ask
271271
if (/You paused to ask/.test(prompt)) return 'Built it with Postgres. Done.' // the resume
272272
return 'done'
273273
},
@@ -302,7 +302,7 @@ test('a run with no preset and no serve config reviews nothing (#1372)', async (
302302
const driver = new FakeDriver({
303303
respond: (prompt: string): string => {
304304
prompts.push(prompt)
305-
if (/Build this app end to end/.test(prompt)) return 'Built it. Done.'
305+
if (/Work within the existing codebase/.test(prompt)) return 'Built it. Done.'
306306
return 'done'
307307
},
308308
sessionId: 'blackbox1372',
@@ -325,7 +325,7 @@ test('a build turn that stops to showMultiSelect fires a checklist gate and resu
325325
'```'
326326
const driver = new FakeDriver({
327327
respond: (prompt: string): string => {
328-
if (/Build this app end to end/.test(prompt)) return awaitBlock
328+
if (/Work within the existing codebase/.test(prompt)) return awaitBlock
329329
if (/You paused to ask/.test(prompt)) return 'Added the picks to TODO. Done.'
330330
return 'done'
331331
},
@@ -361,7 +361,7 @@ test('a build turn that stops for plan approval resumes on Approve (#358)', asyn
361361
'```'
362362
const driver = new FakeDriver({
363363
respond: (prompt: string): string => {
364-
if (/Build this app end to end/.test(prompt)) return awaitBlock
364+
if (/Work within the existing codebase/.test(prompt)) return awaitBlock
365365
if (/You paused to ask/.test(prompt)) return 'Built the plan out. Done.'
366366
return 'done'
367367
},
@@ -402,7 +402,7 @@ test('a declined plan stops the session cleanly instead of building on it (#358)
402402
let resumed = false
403403
const driver = new FakeDriver({
404404
respond: (prompt: string): string => {
405-
if (/Build this app end to end/.test(prompt)) return awaitBlock
405+
if (/Work within the existing codebase/.test(prompt)) return awaitBlock
406406
if (/You paused to ask/.test(prompt)) resumed = true
407407
return 'done'
408408
},
@@ -438,7 +438,7 @@ test('an unmarked option is an ordinary answer, whatever it is labelled (#358)',
438438
let resumedWith = ''
439439
const driver = new FakeDriver({
440440
respond: (prompt: string): string => {
441-
if (/Build this app end to end/.test(prompt)) return awaitBlock
441+
if (/Work within the existing codebase/.test(prompt)) return awaitBlock
442442
if (/You paused to ask/.test(prompt)) resumedWith = prompt
443443
return 'Understood — waiting for your instructions.'
444444
},
@@ -508,7 +508,7 @@ test('with nobody to ask, a session takes the recommended option and carries on
508508
let resumed = false
509509
const driver = new FakeDriver({
510510
respond: (prompt: string): string => {
511-
if (/Build this app end to end/.test(prompt)) return 'built it\n```await-choices\n{ "options": [{ "label": "A" }] }\n```'
511+
if (/Work within the existing codebase/.test(prompt)) return 'built it\n```await-choices\n{ "options": [{ "label": "A" }] }\n```'
512512
if (/You paused to ask/.test(prompt)) resumed = true
513513
return 'done'
514514
},
@@ -965,47 +965,6 @@ test('a build extends an existing project instead of rebuilding it (#185)', asyn
965965
}
966966
})
967967

968-
test('a build uses greenfield framing for an empty workspace (#185)', async () => {
969-
const cwd = await mkdtemp(join(tmpdir(), 'fw-greenfield-'))
970-
try {
971-
const { driver, prompts } = realNamedDriver([{ text: 'scaffolded it' }, { text: 'scaffolded it properly' }])
972-
await runAgent({ prompt: 'a blog', driver, cwd, todoLoop: false })
973-
assert.match(prompts[0]!, /Build this app end to end/i)
974-
assert.doesNotMatch(prompts[0]!, /existing codebase/i)
975-
} finally {
976-
await rm(cwd, { recursive: true, force: true })
977-
}
978-
})
979-
980-
test('a build re-prompts to scaffold from scratch when the workspace stays empty (#182)', async () => {
981-
const cwd = await mkdtemp(join(tmpdir(), 'fw-scaffold-'))
982-
try {
983-
const { driver, prompts } = realNamedDriver([
984-
{ text: 'thinking about the stack' },
985-
{ text: 'scaffolded the whole app' },
986-
])
987-
const { text } = await runAgent({ prompt: 'a blog', driver, cwd, todoLoop: false })
988-
// Two turns: the build, then the hard from-scratch retry once nothing landed on disk.
989-
assert.equal(prompts.length, 2)
990-
assert.match(prompts[1]!, /from scratch|empty/i)
991-
assert.equal(text, 'scaffolded the whole app')
992-
} finally {
993-
await rm(cwd, { recursive: true, force: true })
994-
}
995-
})
996-
997-
test('a build does not re-prompt when it produced files (#182)', async () => {
998-
const cwd = await mkdtemp(join(tmpdir(), 'fw-noscaffold-'))
999-
await writeFile(join(cwd, 'package.json'), '{}')
1000-
try {
1001-
const { driver, prompts } = realNamedDriver([{ text: 'built it' }])
1002-
await runAgent({ prompt: 'a blog', driver, cwd, todoLoop: false })
1003-
assert.equal(prompts.length, 1)
1004-
} finally {
1005-
await rm(cwd, { recursive: true, force: true })
1006-
}
1007-
})
1008-
1009968
test('a prompt session runs its text without build framing, and works no backlog (#353)', async () => {
1010969
const cwd = await mkdtemp(join(tmpdir(), 'fw-promptkind-'))
1011970
await writeFile(join(cwd, 'TODO_AGENTS.md'), '- [ ] leftover task\n')

packages/framework/src/agent.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { createAgentControls, emitSessionStart, endStopDetail } from './agent-te
44
import { createTurnSignalEmitter } from './turn-gate.js'
55
import { runAwaitRounds } from './await-gate.js'
66
import { runTodoLoop, type TodoLoopResult } from './todo-loop.js'
7-
import { buildPrompt, extendPrompt, isWorkspaceEmpty, scaffoldPrompt } from './steps.js'
7+
import { extendPrompt } from './steps.js'
88
import { type ChoicePick, type ChoiceRequest, type FrameworkEvent } from './events.js'
99
import type { AgentMessages } from './agent-messages.js'
1010
import { isHandsOff, type AgentLocation } from './agent-location.js'
@@ -195,7 +195,7 @@ export async function runAgent(opts: RunAgentOptions): Promise<RunAgentResult> {
195195
try {
196196
const rounds = await runAwaitRounds({
197197
session,
198-
prompt: openingPrompt(opts, kind, resuming, session.cwd),
198+
prompt: openingPrompt(opts, kind, resuming),
199199
emitTurnSignals,
200200
requestChoice: opts.requestChoice,
201201
emit,
@@ -215,16 +215,6 @@ export async function runAgent(opts: RunAgentOptions): Promise<RunAgentResult> {
215215
if (rounds.stopped) answerController.abort(new Error('[framework] stopped by your answer'))
216216
let text = rounds.text
217217

218-
// #182: a build must actually produce an app. If nothing landed on disk the agent stalled
219-
// (e.g. sanity-checking the stack), so re-prompt once with a hard "create it from scratch"
220-
// directive. Only for a real driver — the fake one writes nothing, so its workspace always
221-
// reads empty — and only when the agent is not mid-question, which the gates just drained.
222-
if (kind === 'build' && !resuming && opts.driver.id !== 'fake' && !rounds.stopped && isWorkspaceEmpty(opts.cwd)) {
223-
const scaffolded = await session.prompt(scaffoldPrompt(opts.prompt), { signal: agentSignal })
224-
emitTurnSignals(scaffolded.text)
225-
text = scaffolded.text
226-
}
227-
228218
// The session controls (a Stop, an answer that said stop #358) abort between turns, and the
229219
// opening rounds do not observe the abort themselves, so look before treating this as a
230220
// success — otherwise an aborted session settles as done.
@@ -286,17 +276,15 @@ export async function runAgent(opts: RunAgentOptions): Promise<RunAgentResult> {
286276
* A resumed session (#720/#1467) gets the text verbatim: the `--resume`d transcript already
287277
* carries the framing, so composing it again would stack a second preamble onto a conversation
288278
* that lived through the first. So does a transparent or prompt-less session, which is what
289-
* "raw `claude -p`" means. A build is framed for the workspace it lands in: an existing codebase
290-
* is *extended*, not rebuilt from scratch (#185).
279+
* "raw `claude -p`" means. A build extends the existing codebase (#185) — a project is a repo
280+
* that already exists, so the greenfield from-scratch framing is gone (#1683 review).
291281
*/
292-
function openingPrompt(opts: RunAgentOptions, kind: AgentKind, resuming: boolean, cwd: string): string {
282+
function openingPrompt(opts: RunAgentOptions, kind: AgentKind, resuming: boolean): string {
293283
if (resuming || opts.transparent) return opts.prompt
294284
if (kind === 'prompt') {
295285
return opts.vanilla ? opts.prompt : renderSystemPrompt({ prompt: opts.prompt }).user
296286
}
297-
// Gated on a real driver, so the fake one (which writes nothing, so its workspace always reads
298-
// empty) always takes the greenfield path and stays deterministic.
299-
return opts.driver.id !== 'fake' && !isWorkspaceEmpty(cwd) ? extendPrompt(opts.prompt) : buildPrompt(opts.prompt)
287+
return extendPrompt(opts.prompt)
300288
}
301289

302290
/** The live-chat phase a build reaches after its backlog, sharing the rounds' own loop. */

packages/framework/src/cli.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ test('runCli runs a whole session offline', async () => {
468468
assert.equal(code, 0)
469469
const text = out.join('\n')
470470
assert.match(text, / "a blog"/) // what it was asked for, said once as the session opens
471-
assert.match(text, /Build this app end to end/)
471+
assert.match(text, /Work within the existing codebase/)
472472
// #1372: nothing reviews the build, so there is no checklist pass — the build is the whole session.
473473
assert.doesNotMatch(text, /checklist pass/)
474474
assert.match(text, /\u2713 done/)
@@ -483,7 +483,6 @@ test('runCli --transparent runs a bare prompt raw, skipping the build flow + wra
483483
const text = out.join('\n')
484484
// Transparent = raw Claude Code: the build path's markers must NOT appear (contrast the test above).
485485
assert.doesNotMatch(text, /scope: full/) // no scope phase
486-
assert.doesNotMatch(text, /Build this app end to end/) // no buildPrompt wrapping
487486
assert.doesNotMatch(text, /Work within the existing codebase/) // no extendPrompt wrapping
488487
assert.doesNotMatch(text, /production-grade/) // no synthesize / production-grade pass
489488
} finally {

packages/framework/src/steps.SPEC.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
The prompts a build agent opens with, and the one check that decides between them: whether the workspace already holds an app.
1+
The opening prompt of a build agent, filled with the user's intent.
22

3-
## Business logic — TL;DR
3+
## Business logic
44

5-
- **Three build-opening prompts, filled with the user's intent** - the greenfield build prompt, the existing-codebase prompt (chosen when the workspace already holds source at build time), and the scaffold retry (sent when a build's opening turn left the workspace empty). Each prompt's text is authored as markdown in the prompts directory like every other agent-facing prompt; what happens here is only the choice between them and filling in the intent.
6-
- **Workspace-emptiness check** - a workspace counts as empty when it holds no source file the agent could have produced: lockfiles, dotfiles, and dependency/output directories (node_modules, .git, dist, build caches) do not count. Best-effort and cheap — it stops at the first real file, treats an unreadable or missing directory as empty, and never throws.
5+
A build agent's opening prompt is the existing-codebase framing — a project is a repo that already exists, so there is exactly one framing. Its text is authored as markdown in the prompts directory like every other agent-facing prompt; what happens here is only filling in the intent. The old greenfield and scaffold-retry framings (build an app in an empty folder) were removed: no product path leads a build agent into an empty workspace any more.
76

87
## Before modifying/creating SPEC.md files
98

0 commit comments

Comments
 (0)