Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/framework/prompts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ is written in TypeScript any more, so prompting can change without touching the
| file | what it is |
|---|---|
| `system_prompt.md` | The built-in system prompt (#326). Rom's doc. |
| `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. |
| `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. |
| `protocols/await.md` | How to emit an awaited choice so the turn-boundary gate can detect it (#337/#339). |
| `protocols/signal.md` | How to emit `setSessionName()` / `setReadyForMerge()` (#326). |
| `presets/*.md` | One file per preset button: research (#331), readability (#360), maintainability (#361), security_audit (#461), ux (#472). |
Expand Down
4 changes: 2 additions & 2 deletions packages/framework/prompts/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Every prompt The Framework sends an agent lives here as markdown. Nothing agent-
- **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.
- **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.
- **The presets** - one file per launcher button and per routine prompt.
- **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.
- **The build-opening prompt** - the one framing a build agent opens with: deliver the work within the existing codebase.
- **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.
- **Prompts are reviewed before they land** - a prompt change goes through review like any other change.

Expand Down Expand Up @@ -47,7 +47,7 @@ See `## User story`.
- **The triage scope rule** — the one-paragraph rule appended to both triage presets, that a triage only queues work and never implements it.
- **The protocols** — how an agent signals to The Framework, and what this particular agent can do.
- **The presets** — one file per preset: the launcher's buttons and the daemon's routine prompts.
- **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.
- **The build-opening prompt** — the existing-codebase framing every build agent opens with: a project is a repo that already exists.
- **The on-before-mergeable prompt** — the extra turn an agent gets when it signals ready for merge, if the user turned that on.

### Prompts are reviewed before they land
Expand Down
13 changes: 0 additions & 13 deletions packages/framework/prompts/build_prompt.SPEC.md

This file was deleted.

5 changes: 0 additions & 5 deletions packages/framework/prompts/build_prompt.md

This file was deleted.

13 changes: 0 additions & 13 deletions packages/framework/prompts/scaffold_prompt.SPEC.md

This file was deleted.

5 changes: 0 additions & 5 deletions packages/framework/prompts/scaffold_prompt.md

This file was deleted.

55 changes: 7 additions & 48 deletions packages/framework/src/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ test('a build turn that stops to ask fires a live gate and resumes on the pick (
'```'
const driver = new FakeDriver({
respond: (prompt: string): string => {
if (/Build this app end to end/.test(prompt)) return awaitBlock // the build stops to ask
if (/Work within the existing codebase/.test(prompt)) return awaitBlock // the build stops to ask
if (/You paused to ask/.test(prompt)) return 'Built it with Postgres. Done.' // the resume
return 'done'
},
Expand Down Expand Up @@ -302,7 +302,7 @@ test('a run with no preset and no serve config reviews nothing (#1372)', async (
const driver = new FakeDriver({
respond: (prompt: string): string => {
prompts.push(prompt)
if (/Build this app end to end/.test(prompt)) return 'Built it. Done.'
if (/Work within the existing codebase/.test(prompt)) return 'Built it. Done.'
return 'done'
},
sessionId: 'blackbox1372',
Expand All @@ -325,7 +325,7 @@ test('a build turn that stops to showMultiSelect fires a checklist gate and resu
'```'
const driver = new FakeDriver({
respond: (prompt: string): string => {
if (/Build this app end to end/.test(prompt)) return awaitBlock
if (/Work within the existing codebase/.test(prompt)) return awaitBlock
if (/You paused to ask/.test(prompt)) return 'Added the picks to TODO. Done.'
return 'done'
},
Expand Down Expand Up @@ -361,7 +361,7 @@ test('a build turn that stops for plan approval resumes on Approve (#358)', asyn
'```'
const driver = new FakeDriver({
respond: (prompt: string): string => {
if (/Build this app end to end/.test(prompt)) return awaitBlock
if (/Work within the existing codebase/.test(prompt)) return awaitBlock
if (/You paused to ask/.test(prompt)) return 'Built the plan out. Done.'
return 'done'
},
Expand Down Expand Up @@ -402,7 +402,7 @@ test('a declined plan stops the session cleanly instead of building on it (#358)
let resumed = false
const driver = new FakeDriver({
respond: (prompt: string): string => {
if (/Build this app end to end/.test(prompt)) return awaitBlock
if (/Work within the existing codebase/.test(prompt)) return awaitBlock
if (/You paused to ask/.test(prompt)) resumed = true
return 'done'
},
Expand Down Expand Up @@ -438,7 +438,7 @@ test('an unmarked option is an ordinary answer, whatever it is labelled (#358)',
let resumedWith = ''
const driver = new FakeDriver({
respond: (prompt: string): string => {
if (/Build this app end to end/.test(prompt)) return awaitBlock
if (/Work within the existing codebase/.test(prompt)) return awaitBlock
if (/You paused to ask/.test(prompt)) resumedWith = prompt
return 'Understood — waiting for your instructions.'
},
Expand Down Expand Up @@ -508,7 +508,7 @@ test('with nobody to ask, a session takes the recommended option and carries on
let resumed = false
const driver = new FakeDriver({
respond: (prompt: string): string => {
if (/Build this app end to end/.test(prompt)) return 'built it\n```await-choices\n{ "options": [{ "label": "A" }] }\n```'
if (/Work within the existing codebase/.test(prompt)) return 'built it\n```await-choices\n{ "options": [{ "label": "A" }] }\n```'
if (/You paused to ask/.test(prompt)) resumed = true
return 'done'
},
Expand Down Expand Up @@ -965,47 +965,6 @@ test('a build extends an existing project instead of rebuilding it (#185)', asyn
}
})

test('a build uses greenfield framing for an empty workspace (#185)', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'fw-greenfield-'))
try {
const { driver, prompts } = realNamedDriver([{ text: 'scaffolded it' }, { text: 'scaffolded it properly' }])
await runAgent({ prompt: 'a blog', driver, cwd, todoLoop: false })
assert.match(prompts[0]!, /Build this app end to end/i)
assert.doesNotMatch(prompts[0]!, /existing codebase/i)
} finally {
await rm(cwd, { recursive: true, force: true })
}
})

test('a build re-prompts to scaffold from scratch when the workspace stays empty (#182)', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'fw-scaffold-'))
try {
const { driver, prompts } = realNamedDriver([
{ text: 'thinking about the stack' },
{ text: 'scaffolded the whole app' },
])
const { text } = await runAgent({ prompt: 'a blog', driver, cwd, todoLoop: false })
// Two turns: the build, then the hard from-scratch retry once nothing landed on disk.
assert.equal(prompts.length, 2)
assert.match(prompts[1]!, /from scratch|empty/i)
assert.equal(text, 'scaffolded the whole app')
} finally {
await rm(cwd, { recursive: true, force: true })
}
})

test('a build does not re-prompt when it produced files (#182)', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'fw-noscaffold-'))
await writeFile(join(cwd, 'package.json'), '{}')
try {
const { driver, prompts } = realNamedDriver([{ text: 'built it' }])
await runAgent({ prompt: 'a blog', driver, cwd, todoLoop: false })
assert.equal(prompts.length, 1)
} finally {
await rm(cwd, { recursive: true, force: true })
}
})

test('a prompt session runs its text without build framing, and works no backlog (#353)', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'fw-promptkind-'))
await writeFile(join(cwd, 'TODO_AGENTS.md'), '- [ ] leftover task\n')
Expand Down
24 changes: 6 additions & 18 deletions packages/framework/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createAgentControls, emitSessionStart, endStopDetail } from './agent-te
import { createTurnSignalEmitter } from './turn-gate.js'
import { runAwaitRounds } from './await-gate.js'
import { runTodoLoop, type TodoLoopResult } from './todo-loop.js'
import { buildPrompt, extendPrompt, isWorkspaceEmpty, scaffoldPrompt } from './steps.js'
import { extendPrompt } from './steps.js'
import { type ChoicePick, type ChoiceRequest, type FrameworkEvent } from './events.js'
import type { AgentMessages } from './agent-messages.js'
import { isHandsOff, type AgentLocation } from './agent-location.js'
Expand Down Expand Up @@ -195,7 +195,7 @@ export async function runAgent(opts: RunAgentOptions): Promise<RunAgentResult> {
try {
const rounds = await runAwaitRounds({
session,
prompt: openingPrompt(opts, kind, resuming, session.cwd),
prompt: openingPrompt(opts, kind, resuming),
emitTurnSignals,
requestChoice: opts.requestChoice,
emit,
Expand All @@ -215,16 +215,6 @@ export async function runAgent(opts: RunAgentOptions): Promise<RunAgentResult> {
if (rounds.stopped) answerController.abort(new Error('[framework] stopped by your answer'))
let text = rounds.text

// #182: a build must actually produce an app. If nothing landed on disk the agent stalled
// (e.g. sanity-checking the stack), so re-prompt once with a hard "create it from scratch"
// directive. Only for a real driver — the fake one writes nothing, so its workspace always
// reads empty — and only when the agent is not mid-question, which the gates just drained.
if (kind === 'build' && !resuming && opts.driver.id !== 'fake' && !rounds.stopped && isWorkspaceEmpty(opts.cwd)) {
const scaffolded = await session.prompt(scaffoldPrompt(opts.prompt), { signal: agentSignal })
emitTurnSignals(scaffolded.text)
text = scaffolded.text
}

// The session controls (a Stop, an answer that said stop #358) abort between turns, and the
// opening rounds do not observe the abort themselves, so look before treating this as a
// success — otherwise an aborted session settles as done.
Expand Down Expand Up @@ -286,17 +276,15 @@ export async function runAgent(opts: RunAgentOptions): Promise<RunAgentResult> {
* A resumed session (#720/#1467) gets the text verbatim: the `--resume`d transcript already
* carries the framing, so composing it again would stack a second preamble onto a conversation
* that lived through the first. So does a transparent or prompt-less session, which is what
* "raw `claude -p`" means. A build is framed for the workspace it lands in: an existing codebase
* is *extended*, not rebuilt from scratch (#185).
* "raw `claude -p`" means. A build extends the existing codebase (#185) — a project is a repo
* that already exists, so the greenfield from-scratch framing is gone (#1683 review).
*/
function openingPrompt(opts: RunAgentOptions, kind: AgentKind, resuming: boolean, cwd: string): string {
function openingPrompt(opts: RunAgentOptions, kind: AgentKind, resuming: boolean): string {
if (resuming || opts.transparent) return opts.prompt
if (kind === 'prompt') {
return opts.vanilla ? opts.prompt : renderSystemPrompt({ prompt: opts.prompt }).user
}
// Gated on a real driver, so the fake one (which writes nothing, so its workspace always reads
// empty) always takes the greenfield path and stays deterministic.
return opts.driver.id !== 'fake' && !isWorkspaceEmpty(cwd) ? extendPrompt(opts.prompt) : buildPrompt(opts.prompt)
return extendPrompt(opts.prompt)
}

/** The live-chat phase a build reaches after its backlog, sharing the rounds' own loop. */
Expand Down
3 changes: 1 addition & 2 deletions packages/framework/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ test('runCli runs a whole session offline', async () => {
assert.equal(code, 0)
const text = out.join('\n')
assert.match(text, /▶ "a blog"/) // what it was asked for, said once as the session opens
assert.match(text, /Build this app end to end/)
assert.match(text, /Work within the existing codebase/)
// #1372: nothing reviews the build, so there is no checklist pass — the build is the whole session.
assert.doesNotMatch(text, /checklist pass/)
assert.match(text, /\u2713 done/)
Expand All @@ -483,7 +483,6 @@ test('runCli --transparent runs a bare prompt raw, skipping the build flow + wra
const text = out.join('\n')
// Transparent = raw Claude Code: the build path's markers must NOT appear (contrast the test above).
assert.doesNotMatch(text, /scope: full/) // no scope phase
assert.doesNotMatch(text, /Build this app end to end/) // no buildPrompt wrapping
assert.doesNotMatch(text, /Work within the existing codebase/) // no extendPrompt wrapping
assert.doesNotMatch(text, /production-grade/) // no synthesize / production-grade pass
} finally {
Expand Down
7 changes: 3 additions & 4 deletions packages/framework/src/steps.SPEC.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
The prompts a build agent opens with, and the one check that decides between them: whether the workspace already holds an app.
The opening prompt of a build agent, filled with the user's intent.

## Business logic — TL;DR
## Business logic

- **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.
- **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.
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.

## Before modifying/creating SPEC.md files

Expand Down
2 changes: 1 addition & 1 deletion packages/framework/src/steps.test.SPEC.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
What the tests cover: the workspace-emptiness check calls an empty directory, a directory holding only noise (lockfiles, dotfiles, node_modules), and a missing directory empty, and a directory with a real source file not empty; the greenfield build prompt names the intent, frames an end-to-end build, and warns the workspace may be empty; the existing-codebase prompt names the work and the existing codebase and nothing else — none of the retired behavior rules, no scope ask, and no claim the workspace might be empty; the scaffold retry prompt is the hard from-scratch directive and says an empty directory is expected rather than a reason to refuse.
What the tests cover: the existing-codebase opening prompt names the work and the existing codebase and nothing else — none of the retired behavior rules, no scope ask, and no claim the workspace might be empty.

## Before modifying/creating SPEC.md files

Expand Down
Loading
Loading