diff --git a/.changeset/post-merge-todo-entries-556.md b/.changeset/post-merge-todo-entries-556.md new file mode 100644 index 00000000..60901470 --- /dev/null +++ b/.changeset/post-merge-todo-entries-556.md @@ -0,0 +1,13 @@ +--- +'@gemstack/framework': minor +--- + +Post-merge now queues the quality follow-ups instead of running them (#556), which is what the #326 doc says and what composes with the backlog loop (#323/#538). On `setReadyForMerge()`, a `--post-merge` run used to fire maintainability, readability and security-audit as three child `framework prompt` runs back to back, on the spot. It now fires one short turn that appends "Apply preset X on the changes introduced by " entries to the session's TODO file, and lets the loop pick them up. Cheaper by a lot: a few TODO lines rather than three full preset passes serialized on the same git index. + +The readability entry is gated on Technical control, per the doc. That setting already existed as a preference, a `--technical` flag and a Global options toggle; it just never reached the prompts, so `TfContext` gains `settings.technical_control` and `session_name`. The session name is carried on run state: the agent sets it before its first change and the post-merge prompt reads it afterwards. + +`--eco-auto-maintenance` does something again. #326 moved the maintenance section out of the system prompt, which left the flag inert (#555); the post-merge prompt is exactly that section, so the flag now skips it. + +The post-merge prompt is flattened rather than verbatim from the doc, and this is the one place the prompts depart from it. The doc nests `${{ tf.session_name }}` inside the outer `${{ ... }}` and puts backticks inside a backtick template literal; the fragment regex is non-greedy, so the outer fragment closes on the inner `}}` and what is left is not valid JS. It throws `TemplateFragmentError` today, with or without the new context fields. Same branch, same output, one fragment, and a test now rejects any nested fragment so the prompt cannot regress into a shape that will not render. + +`POST_MERGE_PASSES` and `runPostMergeSuite` are replaced by `runPostMerge` and `renderPostMergePrompt`. The three preset modules are untouched: the dashboard buttons still use them. diff --git a/packages/framework/prompts/post_merge_prompt.md b/packages/framework/prompts/post_merge_prompt.md new file mode 100644 index 00000000..2015c98b --- /dev/null +++ b/packages/framework/prompts/post_merge_prompt.md @@ -0,0 +1,9 @@ +TODO_FILE: `TODO_.agent.md` + +## Maintenance + +If the changes introduced by ${{ tf.session_name }} aren't trivial and have refactor potential, add the following to +- "Apply preset `maintainability` on the changes introduced by ${{ tf.session_name }}" +${{ tf.settings.technical_control ? '- "Apply preset `readability` on the changes introduced by ' + tf.session_name + '"\n' : '' }} +If the changes introduced by ${{ tf.session_name }} can potentially lead to security issues, add the following to +- "Apply preset `security_audit` on the changes introduced by ${{ tf.session_name }}" diff --git a/packages/framework/src/cli.test.ts b/packages/framework/src/cli.test.ts index 9ad037b2..bad5a109 100644 --- a/packages/framework/src/cli.test.ts +++ b/packages/framework/src/cli.test.ts @@ -22,8 +22,7 @@ import { runCli, runLogEntry, runLogKind, - runPostMergeSuite, - POST_MERGE_PASSES, + runPostMerge, unguardedNotices, withBrowser, BROWSER_MCP_SERVERS, @@ -83,31 +82,58 @@ test('withBrowser folds chrome-devtools-mcp into driver options only when enable test('promptRunArgs runs a headless prompt and carries NO --post-merge (recursion guard, #326)', () => { const args = promptRunArgs('audit this', '/work/app', '/bin/framework', 3) assert.deepEqual(args, ['/bin/framework', 'prompt', 'audit this', '--no-dashboard', '--cwd', '/work/app', '--max-cost', '3']) - // The guard: a quality pass must not trigger its own post-merge suite. + // The guard: a queued pass must not trigger its own post-merge prompt. assert.equal(args.includes('--post-merge'), false) // maxCost is optional. assert.equal(promptRunArgs('x', '/w', '/bin/f').includes('--max-cost'), false) }) -test('runPostMergeSuite runs maintainability -> readability -> security-audit in order, best-effort (#326)', async () => { - const { io, out } = capture() +test('runPostMerge queues the follow-ups in ONE run instead of running the presets (#326/#556)', async () => { + const { io } = capture() const seen: string[] = [] - // Middle pass "fails" (resolves false): the suite must continue past it. const run = (prompt: string) => { seen.push(prompt) - return Promise.resolve(!/humans to read/.test(prompt)) // the readability pass "fails" + return Promise.resolve(true) + } + await runPostMerge('/work/app', '/bin/framework', io, { session_name: 'add-oauth' }, undefined, run) + // One child run, not three: it asks for TODO entries rather than doing the passes. + assert.equal(seen.length, 1) + const prompt = seen[0]! + assert.match(prompt, /add the following to /) + assert.match(prompt, /Apply preset `maintainability` on the changes introduced by add-oauth/) + assert.match(prompt, /Apply preset `security_audit` on the changes introduced by add-oauth/) + // The preset prompts themselves are not what gets sent any more. + assert.doesNotMatch(prompt, /easy as possible for humans to read/) + assert.ok(!prompt.includes('${{'), 'fully rendered') +}) + +test('runPostMerge gates the readability entry on technical_control (#326)', async () => { + const { io } = capture() + const render = async (technical_control: boolean) => { + const seen: string[] = [] + await runPostMerge( + '/work/app', + '/bin/framework', + io, + { session_name: 'add-oauth', settings: { technical_control } }, + undefined, + p => { + seen.push(p) + return Promise.resolve(true) + }, + ) + return seen[0]! } - await runPostMergeSuite('/work/app', '/bin/framework', io, undefined, run) - assert.equal(seen.length, 3) - assert.match(seen[0]!, /maintainable/) - assert.match(seen[1]!, /easy as possible for humans to read/) - assert.match(seen[2]!, /^Security audit/) - assert.deepEqual( - POST_MERGE_PASSES.map(p => p.label), - ['maintainability', 'readability', 'security-audit'], + assert.match(await render(true), /Apply preset `readability` on the changes introduced by add-oauth/) + assert.doesNotMatch(await render(false), /readability/) +}) + +test('runPostMerge is best-effort: a failed queueing run is reported, never thrown (#326)', async () => { + const { io, out } = capture() + await runPostMerge('/work/app', '/bin/framework', io, { session_name: 'add-oauth' }, undefined, () => + Promise.resolve(false), ) - // The failed middle pass is reported, and the last pass still ran. - assert.ok(out.some(l => /readability did not complete/.test(l))) + assert.ok(out.some(l => /post-merge queueing did not complete/.test(l))) }) test('parseArgs reads the maintain subcommand + its bounds (#298)', () => { diff --git a/packages/framework/src/cli.ts b/packages/framework/src/cli.ts index c6579766..137896ef 100644 --- a/packages/framework/src/cli.ts +++ b/packages/framework/src/cli.ts @@ -48,8 +48,7 @@ import { type RepoReview, } from './maintenance.js' import { renderMaintainabilityPrompt } from './maintainability-preset.js' -import { renderReadabilityPrompt } from './readability-preset.js' -import { renderSecurityAuditPrompt } from './security-audit-preset.js' +import { renderPostMergePrompt, type PostMergeContext } from './post-merge-prompt.js' import { runPrompt } from './prompt-run.js' import { renderResearchPrompt } from './research-preset.js' @@ -153,8 +152,9 @@ Options: "Context: " line to the system prompt; the agent can still reach every repo, this just narrows where it looks. --post-merge When the run signals setReadyForMerge(), fire the post-merge - quality suite: maintainability, readability, and security-audit - prompts, one after another (#326). + prompt: queue the maintainability and security-audit follow-ups + (plus readability under --technical) as TODO entries for the + backlog loop to pick up (#326). --browser Give the agent a real browser during the run via chrome-devtools-mcp: navigate pages, read console + network, inspect the DOM, and screenshot. Off by default (#452). @@ -227,7 +227,7 @@ export interface CliOptions { eco: Required /** `--context ` (repeatable): in-context directories added as one `Context:` line (#439). */ context: string[] - /** `--post-merge`: fire the #326 post-merge quality suite (maintainability/readability/security-audit) when the run signals setReadyForMerge(). */ + /** `--post-merge`: fire the #326 post-merge prompt when the run signals setReadyForMerge(), queueing the quality follow-ups as TODO entries. */ postMerge: boolean /** `--browser`: give the agent a real browser via chrome-devtools-mcp (navigate, console, network, DOM, screenshot) during the run (#452). */ browser: boolean @@ -914,8 +914,12 @@ export async function runCli(argv: string[], io: CliIO = defaultIO): Promise => { if (!opts.postMerge || !sawReadyForMerge || stoppedCleanly || fake) return + // The post-merge prompt is exactly the maintenance section, so --eco-auto-maintenance + // (#314) leaves nothing to queue. This is the flag's target now that #326 moved that + // section out of the system prompt, where it had gone inert (#555). + if (opts.eco.autoMaintenance) return + // Every line of the prompt names the session, so there is nothing to queue without one. + // An agent that made changes has one; this is the agent that ignored the instruction. + if (!sessionName) { + io.out(' ! post-merge skipped: the run never called setSessionName().') + return + } const binPath = process.argv[1] if (!binPath) return - await runPostMergeSuite(cwd, binPath, io, opts.maxCost) + await runPostMerge( + cwd, + binPath, + io, + { session_name: sessionName, settings: { technical_control: opts.technical } }, + opts.maxCost, + ) } // A mode/kind given with no preset in effect has nothing to act on: note it. @@ -1358,7 +1379,7 @@ function spawnMaintenanceRun(review: RepoReview, binPath: string, maxCost?: numb * Run one direct prompt by spawning `framework prompt "" --cwd --no-dashboard`, * reusing the whole run path (preflight, driver, budget cap, LOGS.md). The child inherits * stdio so its run streams to the terminal. Note it carries no `--post-merge`, so a quality - * pass never triggers its own post-merge suite (the recursion guard). Resolves true on a + * pass never triggers its own post-merge prompt (the recursion guard). Resolves true on a * clean exit (0). Never re-execs a test entry (fork-bomb guard). */ /** @@ -1384,37 +1405,30 @@ function spawnPromptRun(prompt: string, cwd: string, binPath: string, maxCost?: }) } -/** How the suite spawns one pass; injectable so tests observe order without spawning. */ +/** How the post-merge prompt is spawned; injectable so tests observe it without spawning. */ export type PromptRunner = (prompt: string, cwd: string, binPath: string, maxCost?: number) => Promise -/** The post-merge quality passes (#326), in the order they run. */ -export const POST_MERGE_PASSES: readonly { label: string; render: () => string }[] = [ - { label: 'maintainability', render: () => renderMaintainabilityPrompt() }, - { label: 'readability', render: () => renderReadabilityPrompt() }, - { label: 'security-audit', render: () => renderSecurityAuditPrompt() }, -] - /** - * Fire the #326 post-merge quality suite after a run signalled setReadyForMerge(): the - * maintainability, readability, and security-audit prompts, each a `framework prompt` child - * on the same workspace. Runs them **one after another** (not in parallel): the three passes - * edit and commit the same git tree, so concurrent writers would race on the index lock — - * sequential is the safe MVP (worktree-isolated parallelism is a follow-up). Best-effort: - * a failed pass is logged and the suite continues. + * Fire the #326 post-merge prompt after a run signalled setReadyForMerge(): one + * `framework prompt` child on the same workspace that appends the quality follow-ups to the + * session's TODO file, for the backlog loop (#323/#538) to pick up. + * + * It used to run maintainability, readability and security-audit inline instead, as three + * child runs back to back (#556). Queueing is both what the doc says and the cheaper thing: + * one short turn that writes a few TODO lines, rather than three full preset passes serialized + * on the same git index. Best-effort, like the suite was: a failure is logged, never thrown. */ -export async function runPostMergeSuite( +export async function runPostMerge( cwd: string, binPath: string, io: CliIO, + tf: PostMergeContext, maxCost?: number, run: PromptRunner = spawnPromptRun, ): Promise { - io.out(`\n◆ post-merge quality suite: ${POST_MERGE_PASSES.map(p => p.label).join(' → ')}`) - for (const pass of POST_MERGE_PASSES) { - io.out(`\n◆ post-merge: ${pass.label}`) - const ok = await run(pass.render(), cwd, binPath, maxCost) - if (!ok) io.out(` ! post-merge ${pass.label} did not complete cleanly; continuing.`) - } + io.out(`\n◆ post-merge: queueing quality follow-ups for ${tf.session_name}`) + const ok = await run(renderPostMergePrompt(tf), cwd, binPath, maxCost) + if (!ok) io.out(` ! post-merge queueing did not complete cleanly.`) } async function runRelayServer(opts: CliOptions, io: CliIO): Promise { diff --git a/packages/framework/src/daemon.test.ts b/packages/framework/src/daemon.test.ts index 7f098b46..8b2ed812 100644 --- a/packages/framework/src/daemon.test.ts +++ b/packages/framework/src/daemon.test.ts @@ -56,7 +56,7 @@ test('startOptionFlags maps only enabled Global options to CLI flags (#314)', () '--context', '/work/ui', ]) - // Post-merge quality suite (#326): maps to --post-merge. + // Post-merge prompt (#326): maps to --post-merge. assert.deepEqual(startOptionFlags({ postMerge: true }), ['--post-merge']) // Browser via chrome-devtools-mcp (#452): maps to --browser. assert.deepEqual(startOptionFlags({ browser: true }), ['--browser']) diff --git a/packages/framework/src/dashboard/server.ts b/packages/framework/src/dashboard/server.ts index ee961437..9877441a 100644 --- a/packages/framework/src/dashboard/server.ts +++ b/packages/framework/src/dashboard/server.ts @@ -93,7 +93,7 @@ export interface StartRunOptions { eco?: EcoOptions /** In-context directories (#439): each becomes a `--context ` flag on the spawned run. */ context?: string[] - /** Post-merge quality suite (#326): on setReadyForMerge(), fire maintainability/readability/security-audit; maps to `--post-merge`. */ + /** Post-merge prompt (#326): on setReadyForMerge(), queue the quality follow-ups as TODO entries; maps to `--post-merge`. */ postMerge?: boolean /** Give the agent a real browser via chrome-devtools-mcp during the run (#452); maps to `--browser`. */ browser?: boolean diff --git a/packages/framework/src/index.ts b/packages/framework/src/index.ts index d089b939..9d103fb8 100644 --- a/packages/framework/src/index.ts +++ b/packages/framework/src/index.ts @@ -212,7 +212,8 @@ export { type VersionFetcher, type UpdateStatus, } from './update-check.js' -export { runCli, parseArgs, buildDeployTarget, runPostMergeSuite, promptRunArgs, POST_MERGE_PASSES, type PromptRunner, type CliIO, type CliOptions } from './cli.js' +export { runCli, parseArgs, buildDeployTarget, runPostMerge, promptRunArgs, type PromptRunner, type CliIO, type CliOptions } from './cli.js' +export { renderPostMergePrompt, POST_MERGE_PROMPT_TEMPLATE, type PostMergeContext } from './post-merge-prompt.js' export { loadFrameworkConfig, parseFrameworkConfig, diff --git a/packages/framework/src/post-merge-prompt.test.ts b/packages/framework/src/post-merge-prompt.test.ts new file mode 100644 index 00000000..ff2d9ff8 --- /dev/null +++ b/packages/framework/src/post-merge-prompt.test.ts @@ -0,0 +1,58 @@ +import { strict as assert } from 'node:assert' +import { test } from 'node:test' +import { renderPostMergePrompt, POST_MERGE_PROMPT_TEMPLATE } from './post-merge-prompt.js' +import { TemplateFragmentError } from './prompt-template.js' + +test('POST_MERGE_PROMPT_TEMPLATE carries the #326 post-merge block', () => { + assert.ok(POST_MERGE_PROMPT_TEMPLATE.includes('TODO_FILE: `TODO_.agent.md`')) + assert.ok(POST_MERGE_PROMPT_TEMPLATE.includes('## Maintenance')) + for (const preset of ['maintainability', 'readability', 'security_audit']) { + assert.ok(POST_MERGE_PROMPT_TEMPLATE.includes(`Apply preset \`${preset}\``), `missing ${preset}`) + } +}) + +test('the template never nests a fragment inside another (#556)', () => { + // The one place this prompt departs from the doc, and the reason why. `renderTemplate`'s + // fragment regex is non-greedy, so an inner `${{ ... }}` closes the outer fragment early and + // the leftover is not valid JS. The doc's version throws "Unexpected identifier". Pin it: + // every fragment must be flat, or the whole prompt stops rendering at run time. + for (const fragment of POST_MERGE_PROMPT_TEMPLATE.match(/\$\{\{[\s\S]*?\}\}/g) ?? []) { + assert.ok(!fragment.slice(3).includes('${{'), `nested fragment: ${fragment}`) + } +}) + +test('renderPostMergePrompt names the session on every entry', () => { + const prompt = renderPostMergePrompt({ session_name: 'add-oauth' }) + assert.ok(!prompt.includes('${{'), 'fully rendered') + assert.match(prompt, /Apply preset `maintainability` on the changes introduced by add-oauth/) + assert.match(prompt, /Apply preset `security_audit` on the changes introduced by add-oauth/) +}) + +test('renderPostMergePrompt adds the readability entry only under technical control (#326)', () => { + const on = renderPostMergePrompt({ session_name: 'add-oauth', settings: { technical_control: true } }) + const off = renderPostMergePrompt({ session_name: 'add-oauth', settings: { technical_control: false } }) + assert.match(on, /Apply preset `readability` on the changes introduced by add-oauth/) + assert.doesNotMatch(off, /readability/) + // The other two entries are unconditional either way. + for (const prompt of [on, off]) { + assert.match(prompt, /`maintainability`/) + assert.match(prompt, /`security_audit`/) + } +}) + +test('renderPostMergePrompt defaults absent settings to off rather than throwing (#556)', () => { + // The template reads `tf.settings.technical_control`, so an absent `settings` would throw + // on the property access rather than read as off. + const prompt = renderPostMergePrompt({ session_name: 'add-oauth' }) + assert.doesNotMatch(prompt, /readability/) + assert.equal(prompt, renderPostMergePrompt({ session_name: 'add-oauth', settings: {} })) +}) + +test('renderPostMergePrompt throws a useful error when the session name is missing (#556)', () => { + // Rather than queueing "changes introduced by undefined". The CLI checks first; this is the + // backstop for any other caller. + assert.throws( + () => renderPostMergePrompt({ session_name: undefined as unknown as string }), + (err: unknown) => err instanceof TemplateFragmentError && /session_name/.test(err.message), + ) +}) diff --git a/packages/framework/src/post-merge-prompt.ts b/packages/framework/src/post-merge-prompt.ts new file mode 100644 index 00000000..f110eb78 --- /dev/null +++ b/packages/framework/src/post-merge-prompt.ts @@ -0,0 +1,37 @@ +import { renderTemplate } from './prompt-template.js' +import { POST_MERGE_PROMPT } from './prompts.generated.js' +import type { TfContext } from './system-prompt.js' + +/** + * The post-merge prompt (#326), in `prompts/post_merge_prompt.md` (#551). + * + * It does not *run* the quality presets, it *queues* them: one agent turn that appends + * "Apply preset X on the changes introduced by " entries to the session's TODO + * file, which the backlog loop (#323/#538) picks up later. That is the whole point of + * #556 — the previous suite executed maintainability, readability and security-audit as + * three child runs on the spot, which does not compose with the queue. + * + * Flattened rather than verbatim, which is the one place this departs from the doc: the + * doc nests `${{ tf.session_name }}` inside the outer `${{ ... }}` and puts backticks + * inside a backtick template literal. {@link renderTemplate}'s fragment regex is + * non-greedy, so the outer fragment closes on the inner `}}` and the remainder is not + * valid JS. Same branch, same output, one fragment. + */ +export const POST_MERGE_PROMPT_TEMPLATE = POST_MERGE_PROMPT + +/** What the post-merge prompt's fragments read. A subset of {@link TfContext}. */ +export interface PostMergeContext { + /** The session the finished run named via setSessionName(). Every line of the prompt names it. */ + session_name: string + /** The user's settings; `technical_control` gates the readability entry. Absent means off. */ + settings?: TfContext['settings'] +} + +/** + * Render the post-merge prompt for a finished session. `settings` is defaulted rather than + * left absent: the template reads `tf.settings.technical_control`, so a missing `settings` + * throws a {@link TemplateFragmentError} instead of reading as off. + */ +export function renderPostMergePrompt(tf: PostMergeContext): string { + return renderTemplate(POST_MERGE_PROMPT_TEMPLATE, { tf: { ...tf, settings: tf.settings ?? {} } }) +} diff --git a/packages/framework/src/preset-run.test.ts b/packages/framework/src/preset-run.test.ts index f7659162..a0d918c2 100644 --- a/packages/framework/src/preset-run.test.ts +++ b/packages/framework/src/preset-run.test.ts @@ -119,7 +119,7 @@ test('the #326 action layer is injected even with the built-in prompt off (#500) // The built-in prompt is gone, but the emit protocols still ride the system channel — // else the agent never learns how to signal set-session-name / ready-for-merge, so - // setReadyForMerge() and the --post-merge suite silently never fire (the build path used + // setReadyForMerge() and the --post-merge prompt silently never fire (the build path used // to nest these inside the promptBlock branch; the direct-prompt path always kept them). assert.ok(!system().includes('# System prompt'), 'built-in #326 prompt is off') assert.match(system(), /## Ready for merge/) // SIGNAL_PROTOCOL (#326) diff --git a/packages/framework/src/registry.ts b/packages/framework/src/registry.ts index f2f9a3c1..37895f04 100644 --- a/packages/framework/src/registry.ts +++ b/packages/framework/src/registry.ts @@ -33,7 +33,7 @@ export interface Preferences { ecoPlanning?: boolean ecoResearch?: boolean ecoMaintenance?: boolean - /** Post-merge quality suite (#326): fire maintainability/readability/security-audit on setReadyForMerge(). */ + /** Post-merge prompt (#326): on setReadyForMerge(), queue the quality follow-ups as TODO entries. */ postMergeQuality?: boolean /** Give the agent a real browser via chrome-devtools-mcp during the run (#452); maps to `--browser`. */ browser?: boolean diff --git a/packages/framework/src/system-prompt.test.ts b/packages/framework/src/system-prompt.test.ts index e15a8c99..63bc4479 100644 --- a/packages/framework/src/system-prompt.test.ts +++ b/packages/framework/src/system-prompt.test.ts @@ -159,10 +159,10 @@ test('eco.autoResearch drops only the Alternatives section (#314)', () => { assert.ok(system.includes('### Scope')) }) -test('eco.autoMaintenance drops nothing: #326 moved the section to the post-merge prompt (#555/#556)', () => { - // Not a silent breakage but a deliberate no-op: the maintenance text left the system - // prompt, so there is nothing here to trim and the tokens are already saved for everyone. - // The flag stays parsed and persisted, and re-points at the post-merge prompt in #556. +test('eco.autoMaintenance drops nothing here: #326 moved the section to the post-merge prompt (#555/#556)', () => { + // Not a silent breakage but a deliberate no-op on *this* prompt: the maintenance text left + // the system prompt, so the tokens are already saved for everyone. The flag acts on the + // post-merge prompt instead, where the CLI skips it (#556). const { system, user } = renderSystemPrompt({ prompt: 'ship it', params: { eco: { autoMaintenance: true } } }) assert.equal(system, renderSystemPrompt({ prompt: 'ship it', params: {} }).system) assert.equal(user, 'ship it') // the user half is untouched by eco diff --git a/packages/framework/src/system-prompt.ts b/packages/framework/src/system-prompt.ts index e94913e8..b7d808b3 100644 --- a/packages/framework/src/system-prompt.ts +++ b/packages/framework/src/system-prompt.ts @@ -42,10 +42,10 @@ export interface EcoOptions { /** Drop `### Alternatives` (the variability-rating research section). */ autoResearch?: boolean | undefined /** - * Drop the maintenance section. #326 moved it out of the system prompt and into the - * post-merge prompt, so there is nothing left here to trim and this flag currently - * drops nothing. It re-points at that prompt when it lands (#556); until then it stays - * parsed and persisted so the CLI flag and the dashboard toggle keep working. + * Drop the maintenance section. Nothing to drop *here*: #326 moved that section out of + * the system prompt and into the post-merge prompt, so this flag acts on that prompt + * instead (#556) and the CLI skips it wholesale, the post-merge prompt being exactly the + * maintenance section. Listed here because it is still an {@link EcoOptions} flag. */ autoMaintenance?: boolean | undefined } @@ -65,12 +65,24 @@ const ECO_SECTION_HEADINGS: Partial> = { autoResearch: '### Alternatives', } -/** The `tf` context the template's `${{...}}` fragments read (#326/#350). */ +/** + * The `tf` context the templates' `${{...}}` fragments read (#326/#350). One shape across + * the prompts; each reads the subset it needs. `session_name` and `settings` are the + * post-merge prompt's (#556), and are snake_case because the doc writes them that way. + */ export interface TfContext { /** The user's prompt (the run intent, or the typed prompt): fills `${{tf.prompt}}`. */ prompt: string /** Run parameters the template branches on (e.g. `autopilot`, #325's mode sense; `eco`, #314). */ params: { autopilot?: boolean; eco?: EcoOptions | undefined } & Record + /** + * The session name the agent set via setSessionName(), carried on run state. Only the + * post-merge prompt reads it, never the system prompt: it is set before the agent makes + * changes and read afterwards, so it is not chicken-and-egg. + */ + session_name?: string | undefined + /** The user's persisted settings the prompts branch on (the #314 Global options). */ + settings?: { technical_control?: boolean | undefined } | undefined } /** The neutral context used when a caller has none: empty prompt, no modes. */