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
6 changes: 3 additions & 3 deletions .github/workflows/regenerate-prompts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ jobs:
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A prompts amp-prompts.md
git add -A README.md prompts subagents skills
if git diff --cached --quiet; then
echo "No prompt changes."
exit 0
fi
AMP_VERSION=$(bun -e "console.log(require('./node_modules/@sourcegraph/amp/package.json').version)")
git commit -m "Regenerate prompts for @sourcegraph/amp@${AMP_VERSION}"
AMP_VERSION=$(bun -e "console.log(require('./node_modules/@ampcode/cli/package.json').version)")
git commit -m "Regenerate prompts for @ampcode/cli@${AMP_VERSION}"
git push

- name: Auto-approve amp bump PR
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/update-amp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ jobs:
with:
bun-version: latest

- name: Update @sourcegraph/amp
- name: Update @ampcode/cli
id: amp
run: |
current=$(bun -e "console.log(require('./package.json').dependencies['@sourcegraph/amp'])")
latest=$(bun pm view @sourcegraph/amp version)
current=$(bun -e "console.log(require('./package.json').dependencies['@ampcode/cli'])")
latest=$(bun pm view @ampcode/cli version)

echo "current=${current}" >> "$GITHUB_OUTPUT"
echo "latest=${latest}" >> "$GITHUB_OUTPUT"
Expand All @@ -37,7 +37,7 @@ jobs:
exit 0
fi

bun add "@sourcegraph/amp@${latest}"
bun add "@ampcode/cli@${latest}"
bun run extract
echo "changed=true" >> "$GITHUB_OUTPUT"

Expand All @@ -49,10 +49,10 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
branch: bot/update-amp
delete-branch: true
title: "deps: update @sourcegraph/amp to ${{ steps.amp.outputs.latest }}"
commit-message: "deps: update @sourcegraph/amp to ${{ steps.amp.outputs.latest }}"
title: "deps: update @ampcode/cli to ${{ steps.amp.outputs.latest }}"
commit-message: "deps: update @ampcode/cli to ${{ steps.amp.outputs.latest }}"
body: |
Updates `@sourcegraph/amp` from `${{ steps.amp.outputs.current }}` to `${{ steps.amp.outputs.latest }}`.
Updates `@ampcode/cli` from `${{ steps.amp.outputs.current }}` to `${{ steps.amp.outputs.latest }}`.

This workflow also runs `bun run extract` so the generated prompt files stay in sync with the package bump.
labels: amp-bump
Expand Down
45 changes: 25 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
# amp-prompts

Tooling that extracts the system prompts baked into the
[`@sourcegraph/amp`](https://www.npmjs.com/package/@sourcegraph/amp) CLI bundle and
writes human-readable Markdown files under `prompts/` and `skills/`.

The Amp bundle ships as a single minified `dist/main.js`. Each prompt is a
template literal that often takes feature-flag arguments (e.g. `enableOracle`,
`enableDiagnostics`) and references other minified identifiers like `R4`, `V6`,
`AA`. This repo:

1. Pins `@sourcegraph/amp` as a normal dependency so Dependabot can open a PR
[`@ampcode/cli`](https://www.npmjs.com/package/@ampcode/cli) CLI and
writes human-readable Markdown files under `prompts/`, `subagents/`, and
`skills/`.

Older `@sourcegraph/amp` packages shipped a single minified `dist/main.js`.
Amp now ships through `@ampcode/cli`, whose platform packages contain a native
executable with the bundled JavaScript embedded inside it. The extractor
supports both layouts: it reads the legacy `dist/main.js` when present,
otherwise scans the installed native Amp binary for the embedded bundle. Each
prompt is a template literal that often takes feature-flag arguments (e.g.
`enableOracle`, `enableDiagnostics`) and references other minified identifiers
like `R4`, `V6`, `AA`. This repo:

1. Pins `@ampcode/cli` as a normal dependency so Dependabot can open a PR
each time a new bundle is published.
2. Parses the installed bundle with `@babel/parser`, locates every arrow
function or template literal whose body looks like a prompt, and
evaluates it inside a `node:vm` sandbox.
function or template literal whose body looks like a prompt, rejects
generated runtime bundles and trivial guard strings, and evaluates each
candidate inside a `node:vm` sandbox.
3. The sandbox's globals are a `Proxy` whose backing map is built from every
`name = "value"` assignment in the bundle, so a reference to `R4`
resolves to the string `"finder"`, `V6` to `"Read"`, and so on. Any
Expand Down Expand Up @@ -55,8 +61,8 @@ The script writes:

<!-- BEGIN GENERATED CATALOG -->

Source: node_modules/@sourcegraph/amp/dist/main.js
Package: @sourcegraph/amp@0.0.1781003299-g2fde29
Source: node_modules/@ampcode/cli-darwin-arm64/amp#embedded-js@62734374
Package: @ampcode/cli@0.0.1781102632-gaaab69

Notes:
- Extracted by parsing the bundle with `@babel/parser`, locating prompt-producing arrow functions and template literals, then evaluating each in a `node:vm` sandbox.
Expand All @@ -66,23 +72,22 @@ Notes:

## Prompts

- [prompt](prompts/prompt.md) — line 744
- [subagent-summary](prompts/subagent-summary.md) — line 1527
- [ai-assistant](prompts/ai-assistant.md) — line 2483
- [ai-assistant](prompts/ai-assistant.md) — line 2211
- [subagent-summary](prompts/subagent-summary.md) — line 4476

## Subagents

- [review](subagents/review.md) — line 1730
- [review](subagents/review.md) — line 1933

## Skills

- [code-review-skill](skills/code-review-skill.md) — line 1040
- [code-review-skill](skills/code-review-skill.md) — line 1804

<!-- END GENERATED CATALOG -->

## Bumping Amp

`@sourcegraph/amp` is pinned exactly in `package.json`. The workflow at
`@ampcode/cli` is pinned exactly in `package.json`. The workflow at
`.github/workflows/update-amp.yml` runs daily, can also be triggered manually,
asks the npm registry for the latest published version with `bun pm view`,
updates the exact pin with `bun add`, runs `bun run extract`, and opens a PR
Expand All @@ -99,7 +104,7 @@ also enables auto-merge for `amp-bump` PRs opened by `github-actions[bot]` or

```
extract-amp-prompts.mjs # the extractor
package.json # pins @sourcegraph/amp
package.json # pins @ampcode/cli
.github/workflows/update-amp.yml
.github/workflows/regenerate-prompts.yml
prompts/ # generated; refreshed by the workflow
Expand Down
17 changes: 7 additions & 10 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

136 changes: 122 additions & 14 deletions extract-amp-prompts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,112 @@ import vm from 'node:vm';
import { parse } from '@babel/parser';

const ROOT = path.dirname(url.fileURLToPath(import.meta.url));
const AMP_PKG_DIR = path.join(ROOT, 'node_modules', '@sourcegraph', 'amp');
const BUNDLE = path.join(AMP_PKG_DIR, 'dist', 'main.js');
const PKG = path.join(AMP_PKG_DIR, 'package.json');
const AMPCODE_CLI_DIR = path.join(ROOT, 'node_modules', '@ampcode', 'cli');
const AMPCODE_CLI_PKG = path.join(AMPCODE_CLI_DIR, 'package.json');
const LEGACY_SOURCEGRAPH_DIR = path.join(ROOT, 'node_modules', '@sourcegraph', 'amp');
const LEGACY_SOURCEGRAPH_PKG = path.join(LEGACY_SOURCEGRAPH_DIR, 'package.json');
const LEGACY_BUNDLE = path.join(LEGACY_SOURCEGRAPH_DIR, 'dist', 'main.js');
const PROMPTS_DIR = path.join(ROOT, 'prompts');
const SUBAGENTS_DIR = path.join(ROOT, 'subagents');
const SKILLS_DIR = path.join(ROOT, 'skills');
const README_FILE = path.join(ROOT, 'README.md');
const MAX_PROMPT_CHARS = 80_000;

const source = fs.readFileSync(BUNDLE, 'utf8');
const pkg = JSON.parse(fs.readFileSync(PKG, 'utf8'));
function relative(p) {
return path.relative(ROOT, p);
}

function resolveNativeAmpBinary() {
if (!fs.existsSync(AMPCODE_CLI_PKG)) {
throw new Error('Could not find @ampcode/cli. Run bun install first.');
}
const cliPkg = JSON.parse(fs.readFileSync(path.join(AMPCODE_CLI_DIR, 'package.json'), 'utf8'));
const platformPackageByKey = {
'darwin-arm64': '@ampcode/cli-darwin-arm64',
'darwin-x64': '@ampcode/cli-darwin-x64',
'linux-arm64': '@ampcode/cli-linux-arm64',
'linux-x64': '@ampcode/cli-linux-x64',
'win32-x64': '@ampcode/cli-win32-x64',
};
const key = `${process.platform}-${process.arch}`;
const platformPackage = platformPackageByKey[key];
if (platformPackage) {
const platformDir = path.join(ROOT, 'node_modules', ...platformPackage.split('/'));
const binary = path.join(platformDir, process.platform === 'win32' ? 'amp.exe' : 'amp');
if (fs.existsSync(binary)) return binary;
}

const binRel = cliPkg.bin?.amp;
if (binRel) {
const binary = path.join(AMPCODE_CLI_DIR, binRel);
if (fs.existsSync(binary) && fs.statSync(binary).size > 100_000) return binary;
}

console.log('Parsing bundle...');
throw new Error(`Could not find native Amp binary for ${key}. Install @ampcode/cli first.`);
}

function sourceLikeByte(byte) {
return byte === 0x09 || byte === 0x0a || byte === 0x0d || (byte >= 0x20 && byte <= 0x7e) || byte >= 0x80;
}

function extractEmbeddedBundle(binaryPath) {
const buffer = fs.readFileSync(binaryPath);
const bundleMarkers = [
'Code Review Skill',
'Additional instructions from the user',
'Please analyze this codebase',
'AGENTS.md guidance',
'promptFragments',
'systemPrompt',
];
const candidates = [];
let start = -1;
for (let i = 0; i <= buffer.length; i += 1) {
const isSourceLike = i < buffer.length && sourceLikeByte(buffer[i]);
if (isSourceLike && start === -1) start = i;
if ((!isSourceLike || i === buffer.length) && start !== -1) {
const length = i - start;
if (length >= 500_000) {
const text = buffer.subarray(start, i).toString('utf8');
const markerCount = bundleMarkers.filter((marker) => text.includes(marker)).length;
if (text.startsWith('// @bun') && markerCount > 0) {
candidates.push({ text, offset: start, length, markerCount });
}
}
start = -1;
}
}

candidates.sort((a, b) => b.markerCount - a.markerCount || b.length - a.length);
if (candidates.length === 0) {
throw new Error(`Could not find embedded Amp JS bundle in ${binaryPath}`);
}
return candidates[0];
}

function loadAmpSource() {
if (fs.existsSync(LEGACY_BUNDLE)) {
const pkg = JSON.parse(fs.readFileSync(LEGACY_SOURCEGRAPH_PKG, 'utf8'));
return {
source: fs.readFileSync(LEGACY_BUNDLE, 'utf8'),
sourceLabel: relative(LEGACY_BUNDLE),
pkg,
};
}

const pkg = JSON.parse(fs.readFileSync(AMPCODE_CLI_PKG, 'utf8'));
const binary = resolveNativeAmpBinary();
const embedded = extractEmbeddedBundle(binary);
return {
source: embedded.text,
sourceLabel: `${relative(binary)}#embedded-js@${embedded.offset}`,
pkg,
};
}

const { source, sourceLabel, pkg } = loadAmpSource();

console.log(`Parsing ${sourceLabel}...`);
const ast = parse(source, {
sourceType: 'unambiguous',
errorRecovery: true,
Expand Down Expand Up @@ -70,7 +164,21 @@ console.log(`Collected ${identMap.size} identifier candidates.`);
// 1. Arrow/Function expressions whose body is (or returns) a template literal containing prompt-y text.
// 2. Top-level template literals containing prompt-y text (already-rendered prompts).
const PROMPT_RE = /You are|Please analyze|AGENTS\.md guidance|MUST answer|fast, parallel code search agent|Librarian|REPL operator|code review|Additional instructions from the user/i;
const STARTS_LIKE_PROMPT = /^(#|You are|Please analyze|AGENTS\.md guidance|MUST answer|The following|Run comprehensive|Remember:|Additional instructions from the user)/i;
const STARTS_LIKE_PROMPT = /^(# Code Review Skill\b|#{1,3}\s+.+\bSkill\b|You are|Please analyze|AGENTS\.md guidance|MUST answer|The following|Run comprehensive|Remember:|Additional instructions from the user)/i;

function isBundleLikeText(text) {
const trimmed = text.trimStart();
if (trimmed.length > MAX_PROMPT_CHARS) return true;
if (trimmed.startsWith('#!/usr/bin/env bun')) return true;
if (trimmed.startsWith('// @bun\nvar ')) return true;
if (/^(var|function)\s+[A-Za-z_$][\w$]*=.*Object\.create/.test(trimmed.slice(0, 300))) return true;
return false;
}

function looksLikePromptText(text) {
const trimmed = text.trim();
return !isBundleLikeText(trimmed) && PROMPT_RE.test(trimmed) && STARTS_LIKE_PROMPT.test(trimmed);
}

function lineOf(node) {
return node.loc?.start?.line ?? 0;
Expand Down Expand Up @@ -104,7 +212,7 @@ function walkWithParent(node, parent) {
}
if (tpl) {
const sample = templateLiteralText(tpl);
if (PROMPT_RE.test(sample) && STARTS_LIKE_PROMPT.test(sample.trim())) {
if (looksLikePromptText(sample)) {
candidates.push({
kind: 'function',
node,
Expand All @@ -121,7 +229,7 @@ function walkWithParent(node, parent) {
// Case B: a bare TemplateLiteral that looks like a prompt.
if (node.type === 'TemplateLiteral' && (parent?.type !== 'ReturnStatement' && parent?.type !== 'ArrowFunctionExpression')) {
const sample = templateLiteralText(node);
if (PROMPT_RE.test(sample) && STARTS_LIKE_PROMPT.test(sample.trim())) {
if (looksLikePromptText(sample)) {
candidates.push({
kind: 'template',
node,
Expand Down Expand Up @@ -154,11 +262,11 @@ const PUBLIC_BASE_PROMPT_NAMES = new Map([
['frontier', null],
]);
const basePromptNameBySymbol = new Map();
for (const match of source.matchAll(/case"([^"]+)":I=([A-Za-z_$][\w$]*)\(/g)) {
for (const match of source.matchAll(/case"([^"]+)":[A-Za-z_$][\w$]*=([A-Za-z_$][\w$]*)\(/g)) {
const name = PUBLIC_BASE_PROMPT_NAMES.has(match[1]) ? PUBLIC_BASE_PROMPT_NAMES.get(match[1]) : match[1];
if (name) basePromptNameBySymbol.set(match[2], name);
}
const defaultPrompt = source.match(/default:I=([A-Za-z_$][\w$]*)\(\);break/);
const defaultPrompt = source.match(/default:[A-Za-z_$][\w$]*=([A-Za-z_$][\w$]*)\(\);break/);
if (defaultPrompt) basePromptNameBySymbol.set(defaultPrompt[1], 'smart');

// Evaluate each candidate by extracting its source slice and running it in a fresh vm
Expand Down Expand Up @@ -285,7 +393,7 @@ console.log(`Rendered ${rendered.length} prompts.`);
const MIN_BODY_CHARS = 200;
const substantial = rendered.filter((r) => {
const nonEmptyLines = r.text.split('\n').filter((l) => l.trim().length > 0);
return r.text.length >= MIN_BODY_CHARS && nonEmptyLines.length >= 2;
return r.text.length >= MIN_BODY_CHARS && r.text.length <= MAX_PROMPT_CHARS && nonEmptyLines.length >= 2 && !isBundleLikeText(r.text);
});
console.log(`Dropped ${rendered.length - substantial.length} trivial prompts.`);

Expand Down Expand Up @@ -462,7 +570,7 @@ named.forEach((block) => {
const body = [
`# ${block.derivedName}`,
'',
`_Source: \`dist/main.js:${block.line}\` (symbol \`${block.name}\`)_`,
`_Source: \`${sourceLabel}:${block.line}\` (symbol \`${block.name}\`)_`,
'',
block.text,
'',
Expand All @@ -489,7 +597,7 @@ function replaceGeneratedCatalog(catalog) {
}

const catalog = [
`Source: ${path.relative(ROOT, BUNDLE)}`,
`Source: ${sourceLabel}`,
`Package: ${pkg.name}@${pkg.version}`,
'',
'Notes:',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
"dependencies": {
"@babel/parser": "^7.29.3",
"@babel/types": "^7.29.0",
"@sourcegraph/amp": "0.0.1781003299-g2fde29"
"@ampcode/cli": "0.0.1781102632-gaaab69"
}
}
Loading