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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ test-results/

# VitePress dev cache (build output goes to .vitepress/dist, covered by dist/)
docs/.vitepress/cache/

# Generated from packages/framework/prompts/**/*.md by its gen-prompts script (#551).
packages/framework/src/prompts.generated.ts
11 changes: 6 additions & 5 deletions packages/framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@
}
},
"scripts": {
"build": "tsc -p tsconfig.build.json",
"dev": "tsc -p tsconfig.build.json --watch",
"typecheck": "tsc --noEmit",
"test": "tsc -p tsconfig.test.json && cd dist-test && node --test",
"build": "node scripts/gen-prompts.mjs && tsc -p tsconfig.build.json",
"dev": "node scripts/gen-prompts.mjs && tsc -p tsconfig.build.json --watch",
"typecheck": "node scripts/gen-prompts.mjs && tsc --noEmit",
"test": "node scripts/gen-prompts.mjs && tsc -p tsconfig.test.json && cd dist-test && node --test",
"bundle:dashboard": "node scripts/bundle-dashboard.mjs",
"clean": "rm -rf dist dist-test"
"clean": "rm -rf dist dist-test src/prompts.generated.ts",
"gen:prompts": "node scripts/gen-prompts.mjs"
},
"dependencies": {
"@gemstack/ai-autopilot": "workspace:^",
Expand Down
34 changes: 34 additions & 0 deletions packages/framework/prompts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Prompts

Every prompt the framework sends an agent lives here as markdown (#551). Nothing agent-facing
is written in TypeScript any more, so prompting can change without touching the code.

| file | what it is |
|---|---|
| `system_prompt.md` | The built-in system prompt (#326). Rom's doc. |
| `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). |

## Editing

Edit the markdown, then `pnpm build`. `scripts/gen-prompts.mjs` compiles this directory into
`src/prompts.generated.ts` (git-ignored, rebuilt by `build` / `test` / `typecheck`), which is
what the code imports. The markdown is the only source of truth.

Adding `foo/bar.md` exports `FOO_BAR`. A file's exact bytes become the string, minus one
trailing newline.

## Why generated instead of read from disk

`@gemstack/ai-autopilot` reads its `prompts/*.md` with `node:fs` at run time. That does not work
here: the system prompt and the presets are reachable from `src/client.ts`, which the dashboard
imports **in the browser** to show the user the prompt before a run (#520). A `node:fs` edge
there breaks the browser bundle, and `client.test.ts` fails the build over it. Generating a
module of plain strings crosses that boundary for free and keeps the package `files: ["dist"]`.

## Two rules

- **The system prompt is Rom's** (#500/#547). Change it on #326 first, then sync the markdown.
- **Prompts get a review round before they land in production** (#547). The point of this
directory is that a prompt change is a readable markdown diff.
2 changes: 2 additions & 0 deletions packages/framework/prompts/presets/maintainability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Refactor <PARAM:what> to make it as maintainable as possible:
- Look for maintainability red flags, and fix them.
16 changes: 16 additions & 0 deletions packages/framework/prompts/presets/readability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Refactor <PARAM:what> to make it as easy as possible for humans to read:
- Pinnacle architectural split
- Does each file and each <FUNCTION> represent a sensible and natural abstraction?
- Rate the *seams*, not just the boxes: for each call site, ask whether the responsibility sits on the right side of the boundary — should a caller's wrapper move down into the callee (or vice versa)? A <FUNCTION> can be clean, DRY and well-tested in isolation yet still be in the wrong place. "Well-factored" is not "well-located".
- Linearity (for humans)
- Put yourself in the shoes of a human reader who reads everything in a linear fashion
- Top to bottom: place callers above callees so readers encounter high-level logic before implementation details (humans think high-level first)
- Altitude pass: for each entry-point / orchestration <FUNCTION>, read it top-to-bottom as prose. Flag any line that drops the reader into lower-level mechanism (a flag, a thunk, a log verb, error plumbing) in the middle of what should be a high-level narrative. For each, ask: can that mechanism move down into the callee so the caller reads at one consistent altitude? Prioritize the reading path of all the <FUNCTION> a reader hits first.
- Before starting to work: list *ALL* files and *ALL* <FUNCTION> in this chat, rate them all (0: convoluted abstraction, hard to read, wrong place — 10: perfect), and give a reason for your rating
- DON'T skip any file nor any <FUNCTION> in your rating list — write an extra separated list in this chat of all files and all <FUNCTION> and put a ✅ tick to each entry to double check whether you forgot to rate something. So two lists: one list of ratings & explanation, and a second confirmation list.
- Separate commit for each refactor
- Work until it's exceptionally good. We as an expert team will check against every little detail.
- If we see that you gave mostly a 10/10 rating, that's a sign you've been lazy... so make sure you scrutinize everything and spend a substantial amount of time. We don't want to prompt you again and again to achieve quality — autonomously strive for quality on your own without us pushing you.
- Give summary of what you worked on: print the lists again with old rating => new rating with link to commit(s)

FUNCTION: an actual function or a class, procedure, etc. (anything that represents a unit of logic)
13 changes: 13 additions & 0 deletions packages/framework/prompts/presets/research.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Measure "problem variability" of <PARAM:what>
- List all high-level flows the code implements, i.e. the list of all "problems" the code solves
- Give a rating for each problem (from 0 to 10) following this criteria: does the code solves the problem in an obviously optimal way (10), or is it highly unclear whether the problem can be solved in a better way (0)?
- Write down the ratings in a new file <REVIEW_FILE>
- Show the list to the user and enable him to select problems via `showMultiSelect()`, <AWAIT>
- Set default to `true` for entries with low rating
- For all problems the user selected, add a new entry to <TODO_FILE>
- The entry: "Deep-dive research for alternative solutions, see <REVIEW_FILE>"

AWAIT: Stop, await user answer before resuming
REVIEW_FILE: `REVIEW-PROBLEMS_<SESSION_NAME>.agent.md`
TODO_FILE: `TODO_<SESSION_NAME>.agent.md`
SESSION_NAME: the name of the current Git branch — sanitize it to be a SLUG, if name is generic (e.g. `main`) then create a succinct SLUG
5 changes: 5 additions & 0 deletions packages/framework/prompts/presets/security_audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Security audit <PARAM:what>
- Scrutinize the entire code for potential security issues
- Make it exhaustive (100% coverage)
- List every aspect you considered and, for each aspect, include a verdict as well as an explanation for non-obvious verdicts
- If you see any security issue, fix each security issue in a separate commit
6 changes: 6 additions & 0 deletions packages/framework/prompts/presets/ux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Thoroughly review UX of <PARAM:what> and make proposals to improve. Focus your review from a usability perspective: is using the UI and all the functionalities a nice user experience?
1. Enumerate *all* your findings (in a sensible order and categorized), with reference numbers (so that it's easy to reference each point in follow up conversations), and make it a list of choices shown via `showChoices()`
2. <AWAIT>
3. Work on all accepted proposals

AWAIT: Stop, await user answer before resuming
24 changes: 24 additions & 0 deletions packages/framework/prompts/protocols/await.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## Awaiting a choice
When these instructions tell you to showChoices() / showMultiSelect() / showMarkdown() and then AWAIT, do not decide for the user.
End your turn with one fenced code block, then stop.
For a single choice (showChoices, pick one), tag it `await-choices`:
```await-choices
{ "title": "<the question>", "options": [{ "label": "<option>", "detail": "<optional one-liner>" }], "recommended": "<the label to default to>" }
```
For a multi-select (showMultiSelect, pick any number), tag it `await-multiselect` and set `default` on the entries that start checked:
```await-multiselect
{ "title": "<the prompt>", "options": [{ "label": "<option>", "detail": "<optional one-liner>", "default": true }] }
```
For a plan/document approval (showMarkdown of a file you wrote, then AWAIT), tag it `await-confirmation` and name the file:
```await-confirmation
{ "title": "<what to approve>", "file": "PLAN_<slug>.agent.md" }
```
The framework shows it, waits for the user, and re-prompts you with their answer. Do not continue past it on your own.

## Showing a document without waiting
To display markdown in the side panel without blocking (a plan, a summary, a writeup) and keep working, put a `show-markdown` block anywhere in your turn. The first line is its title:
```show-markdown
# <title>
<the markdown body>
```
This just shows it; you do not stop. Re-emit the same title to update that view in place.
11 changes: 11 additions & 0 deletions packages/framework/prompts/protocols/signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## Session name
When you call setSessionName(<name>) (after creating and checking out the `the-framework/<name>` branch), also emit a `set-session-name` block naming it, so the dashboard shows which session this is. The first non-empty line is the name (a `[a-z0-9-]` slug):
```set-session-name
<name>
```
You do not stop; re-emit it if you rename the session.

## Ready for merge
When you call setReadyForMerge() — you believe the work is complete and ready for human review — emit an empty `ready-for-merge` block. This flips the dashboard status from building to ready; it does not stop your turn.
```ready-for-merge
```
36 changes: 36 additions & 0 deletions packages/framework/prompts/system_prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# System prompt

SHOW_MD: Show it to the user via `showMarkdown()`
SHOW_CHOICES: Show it to the user via `showChoices()`
AWAIT: Stop, await user answer before resuming
SESSION_NAME: the name of the current Git branch — sanitize it to be a SLUG, if name is generic (e.g. `main`) then create a succinct SLUG
SLUG: [a-z0-9-]+
TODO_FILE: `TODO_<SESSION_NAME>.agent.md`

## Unclear scope

If it isn't clear what you should do (e.g. unclear scope, unclear user prompt), make a list of interpretations sorted by plausibility, <SHOW_CHOICES>, <AWAIT>

## Large scope

- If the scope of what you'll work on is *large*, create a `PLAN_<SESSION_NAME>.agent.md` of what you'll work on, <SHOW_MD>, <AWAIT>
- If the scope is potentially *very large* (e.g. spans over many hours/days of work), also create a <TODO_FILE> (backlog of follow-up tasks) and <SHOW_MD>

## Alternatives

Before starting to write code, measure "variability":
- List all high-level problems that need to be implemented
- Give a rating for each problem (from 0 to 10) following this criteria: is there an obviously optimal way to solve the problem (10), or is it highly unclear whether the problem can be solved in a better way (0)?
- Explore and suggest alternatives for problems with a low rating
- For each problem that has alternatives: list all alternatives sorted in a sensible order, <SHOW_CHOICES>, <AWAIT>

## Maintenance

- When making changes to existing code, ${{ tf.params.autopilot ? "you can prefer minimal changes (e.g. to postpone a deep refactor)" : "prefer minimal changes to make it easier for humans to read the changes" }}
- But your changes should still be the correct solution on a high-level, don't implement a bad solution for the sake of making minimal changes
- If your changes aren't trivial and leads to refactor potential, add a new entry to <TODO_FILE>
- The entry: "Look for refactoring opportunities arising from the <SESSION_NAME> merge"

# User prompt

${{tf.prompt}}
67 changes: 67 additions & 0 deletions packages/framework/scripts/gen-prompts.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { readdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, join, relative } from 'node:path'
import { fileURLToPath } from 'node:url'

// Compile `prompts/**/*.md` (#551) into `src/prompts.generated.ts` so the prompting is
// authored as markdown while the code keeps importing plain strings.
//
// Why generate instead of reading the .md at run time (the @gemstack/ai-autopilot pattern):
// the system prompt and every preset are reachable from `src/client.ts`, which the dashboard
// imports in the *browser* to show the prompt before a run (#520). A `node:fs` read there
// breaks the browser bundle, and `client.test.ts` fails the build over it. A generated module
// is just strings, so it crosses that boundary for free and the package stays `files: ["dist"]`.
//
// The .md is the only source of truth; the generated file is git-ignored and rebuilt by
// `build` / `test` / `typecheck`, so it cannot drift the way the hand-copied template did.

const here = dirname(fileURLToPath(import.meta.url))
const promptsDir = join(here, '..', 'prompts')
const outFile = join(here, '..', 'src', 'prompts.generated.ts')

/**
* Every prompt .md under prompts/, as absolute paths, sorted so the output is stable.
* README.md is documentation for humans, not a prompt.
*/
async function findMarkdown(dir) {
const found = []
for (const entry of await readdir(dir, { withFileTypes: true })) {
const path = join(dir, entry.name)
if (entry.isDirectory()) found.push(...(await findMarkdown(path)))
else if (entry.name.endsWith('.md') && entry.name !== 'README.md') found.push(path)
}
return found.sort()
}

/** `presets/security_audit.md` -> `PRESETS_SECURITY_AUDIT`. */
function constName(relPath) {
return relPath
.replace(/\.md$/, '')
.replace(/[^A-Za-z0-9]+/g, '_')
.toUpperCase()
}

const files = await findMarkdown(promptsDir)
const entries = await Promise.all(
files.map(async path => {
const relPath = relative(promptsDir, path).split('\\').join('/')
const raw = await readFile(path, 'utf8')
// Strip exactly one trailing newline: the files end with one so they are well-formed on
// disk, the prompts they carry do not.
return { relPath, name: constName(relPath), text: raw.replace(/\n$/, '') }
}),
)

const body = entries
// JSON.stringify, not a template literal: the prompts contain backticks and `${{ }}`
// fragments, and hand-rolled escaping is exactly the kind of thing that silently corrupts
// a prompt. Unreadable output is fine, nobody reads this file.
.map(e => `/** \`prompts/${e.relPath}\` */\nexport const ${e.name} = ${JSON.stringify(e.text)}\n`)
.join('\n')

const out = `// Generated from prompts/**/*.md by scripts/gen-prompts.mjs. Do not edit.
// Edit the markdown instead; this file is rebuilt on every build/test/typecheck.

${body}`

await writeFile(outFile, out)
console.log(`[gen-prompts] ${entries.length} prompts -> ${relative(join(here, '..'), outFile)}`)
6 changes: 3 additions & 3 deletions packages/framework/src/maintainability-preset.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { renderPresetPrompt, type PresetParam } from './preset-params.js'
import { PRESETS_MAINTAINABILITY } from './prompts.generated.js'

/**
* The [Maintainability] preset (#361): Rom's refactor-for-future-changes pass,
Expand All @@ -15,9 +16,8 @@ export const MAINTAINABILITY_PARAMS: readonly PresetParam[] = [
{ name: 'what', default: 'this PR', description: 'What to refactor for maintainability' },
]

/** The prompt template, verbatim from #361 (with `<PARAM:what>` as the blank). */
export const MAINTAINABILITY_PROMPT_TEMPLATE = `Refactor <PARAM:what> to make it as maintainable as possible:
- Look for maintainability red flags, and fix them.`
/** The prompt template, verbatim from #361, in `prompts/presets/maintainability.md` (#551). */
export const MAINTAINABILITY_PROMPT_TEMPLATE = PRESETS_MAINTAINABILITY

/**
* Render the Maintainability prompt for a target. A blank / omitted `what`
Expand Down
18 changes: 2 additions & 16 deletions packages/framework/src/readability-preset.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { renderPresetPrompt, type PresetParam } from './preset-params.js'
import { PRESETS_READABILITY } from './prompts.generated.js'

/**
* The [Readability] preset (#360): Rom's refactor-for-human-readers pass, shipped
Expand All @@ -17,22 +18,7 @@ export const READABILITY_PARAMS: readonly PresetParam[] = [
]

/** The prompt template, verbatim from #360 (with `<PARAM:what>` as the blank). */
export const READABILITY_PROMPT_TEMPLATE = `Refactor <PARAM:what> to make it as easy as possible for humans to read:
- Pinnacle architectural split
- Does each file and each <FUNCTION> represent a sensible and natural abstraction?
- Rate the *seams*, not just the boxes: for each call site, ask whether the responsibility sits on the right side of the boundary — should a caller's wrapper move down into the callee (or vice versa)? A <FUNCTION> can be clean, DRY and well-tested in isolation yet still be in the wrong place. "Well-factored" is not "well-located".
- Linearity (for humans)
- Put yourself in the shoes of a human reader who reads everything in a linear fashion
- Top to bottom: place callers above callees so readers encounter high-level logic before implementation details (humans think high-level first)
- Altitude pass: for each entry-point / orchestration <FUNCTION>, read it top-to-bottom as prose. Flag any line that drops the reader into lower-level mechanism (a flag, a thunk, a log verb, error plumbing) in the middle of what should be a high-level narrative. For each, ask: can that mechanism move down into the callee so the caller reads at one consistent altitude? Prioritize the reading path of all the <FUNCTION> a reader hits first.
- Before starting to work: list *ALL* files and *ALL* <FUNCTION> in this chat, rate them all (0: convoluted abstraction, hard to read, wrong place — 10: perfect), and give a reason for your rating
- DON'T skip any file nor any <FUNCTION> in your rating list — write an extra separated list in this chat of all files and all <FUNCTION> and put a ✅ tick to each entry to double check whether you forgot to rate something. So two lists: one list of ratings & explanation, and a second confirmation list.
- Separate commit for each refactor
- Work until it's exceptionally good. We as an expert team will check against every little detail.
- If we see that you gave mostly a 10/10 rating, that's a sign you've been lazy... so make sure you scrutinize everything and spend a substantial amount of time. We don't want to prompt you again and again to achieve quality — autonomously strive for quality on your own without us pushing you.
- Give summary of what you worked on: print the lists again with old rating => new rating with link to commit(s)

FUNCTION: an actual function or a class, procedure, etc. (anything that represents a unit of logic)`
export const READABILITY_PROMPT_TEMPLATE = PRESETS_READABILITY

/**
* Render the Readability prompt for a target. A blank / omitted `what` falls
Expand Down
15 changes: 2 additions & 13 deletions packages/framework/src/research-preset.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { renderPresetPrompt, type PresetParam } from './preset-params.js'
import { PRESETS_RESEARCH } from './prompts.generated.js'

/**
* The [Research] preset (#331): Rom's problem-variability review, shipped as a
Expand All @@ -19,19 +20,7 @@ export const RESEARCH_PARAMS: readonly PresetParam[] = [
]

/** The prompt template, verbatim from #331 (with `<PARAM:what>` as the blank). */
export const RESEARCH_PROMPT_TEMPLATE = `Measure "problem variability" of <PARAM:what>
- List all high-level flows the code implements, i.e. the list of all "problems" the code solves
- Give a rating for each problem (from 0 to 10) following this criteria: does the code solves the problem in an obviously optimal way (10), or is it highly unclear whether the problem can be solved in a better way (0)?
- Write down the ratings in a new file <REVIEW_FILE>
- Show the list to the user and enable him to select problems via \`showMultiSelect()\`, <AWAIT>
- Set default to \`true\` for entries with low rating
- For all problems the user selected, add a new entry to <TODO_FILE>
- The entry: "Deep-dive research for alternative solutions, see <REVIEW_FILE>"

AWAIT: Stop, await user answer before resuming
REVIEW_FILE: \`REVIEW-PROBLEMS_<SESSION_NAME>.agent.md\`
TODO_FILE: \`TODO_<SESSION_NAME>.agent.md\`
SESSION_NAME: the name of the current Git branch — sanitize it to be a SLUG, if name is generic (e.g. \`main\`) then create a succinct SLUG`
export const RESEARCH_PROMPT_TEMPLATE = PRESETS_RESEARCH

/**
* Render the Research prompt for a target. A blank / omitted `what` falls back
Expand Down
Loading
Loading