Skip to content

Commit 7d361ce

Browse files
authored
fix(bootstrap): stabilize system prompt prefix (#329)
1 parent ac8c336 commit 7d361ce

4 files changed

Lines changed: 68 additions & 7 deletions

File tree

src/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ const getPackageVersion = (): string => {
4343

4444
const SystematicPlugin: Plugin = async ({ client, directory }) => {
4545
const config = loadConfig(directory)
46+
// Snapshot bootstrap once per plugin init so the cached system prefix stays
47+
// stable across requests. Custom bootstrap file edits take effect on restart.
48+
const bootstrapContent = getBootstrapContent(config, { bundledSkillsDir })
4649

4750
const configHandler = createConfigHandler({
4851
directory,
@@ -106,9 +109,8 @@ const SystematicPlugin: Plugin = async ({ client, directory }) => {
106109
return
107110
}
108111

109-
const content = getBootstrapContent(config, { bundledSkillsDir })
110-
if (content) {
111-
applyBootstrapContent(output, content)
112+
if (bootstrapContent) {
113+
applyBootstrapContent(output, bootstrapContent)
112114
}
113115
},
114116
}

src/lib/bootstrap.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface BootstrapDeps {
1919
bundledSkillsDir: string
2020
}
2121

22-
function getToolMappingTemplate(bundledSkillsDir: string): string {
22+
function getToolMappingTemplate(): string {
2323
return `**Tool Mapping for OpenCode:**
2424
When skills reference tools you don't have, substitute OpenCode equivalents:
2525
- \`TodoWrite\` → \`todowrite\`
@@ -37,7 +37,7 @@ When skills reference tools you don't have, substitute OpenCode equivalents:
3737
- Use the native \`skill\` tool for non-Systematic skills
3838
3939
**Skills location:**
40-
Bundled skills are in \`${bundledSkillsDir}/\``
40+
Bundled skills ship with the Systematic plugin and are discoverable via \`systematic_skill\`.`
4141
}
4242

4343
export function getBootstrapContent(
@@ -66,7 +66,7 @@ export function getBootstrapContent(
6666
const fullContent = fs.readFileSync(usingSystematicPath, 'utf8')
6767
const { body } = parseFrontmatter(fullContent)
6868
const content = body.trim()
69-
const toolMapping = getToolMappingTemplate(bundledSkillsDir)
69+
const toolMapping = getToolMappingTemplate()
7070

7171
return `<SYSTEMATIC_WORKFLOWS>
7272
You have access to structured engineering workflows via the systematic plugin.

tests/unit/bootstrap.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,10 @@ describe('getBootstrapContent', () => {
194194
const content = getBootstrapContent(config, { bundledSkillsDir })
195195

196196
expect(content).not.toBeNull()
197-
expect(content).toContain(bundledSkillsDir)
197+
expect(content).not.toContain(bundledSkillsDir)
198+
expect(content).toContain(
199+
'Bundled skills ship with the Systematic plugin and are discoverable via `systematic_skill`.',
200+
)
198201
})
199202
})
200203

tests/unit/plugin.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, test } from 'bun:test'
22
import fs from 'node:fs'
3+
import os from 'node:os'
34
import path from 'node:path'
45
import { pathToFileURL } from 'node:url'
56

@@ -24,6 +25,61 @@ describe('plugin loading', () => {
2425
expect(pluginModule.SystematicPlugin).toBeUndefined()
2526
})
2627

28+
test('plugin snapshots bootstrap content at init instead of re-reading files per transform', async () => {
29+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'systematic-plugin-'))
30+
const opencodeDir = path.join(tempDir, '.opencode')
31+
const bootstrapPath = path.join(tempDir, 'bootstrap.md')
32+
const configPath = path.join(opencodeDir, 'systematic.json')
33+
34+
fs.mkdirSync(opencodeDir, { recursive: true })
35+
fs.writeFileSync(bootstrapPath, 'INITIAL bootstrap content')
36+
fs.writeFileSync(
37+
configPath,
38+
JSON.stringify({
39+
bootstrap: {
40+
enabled: true,
41+
file: bootstrapPath,
42+
},
43+
}),
44+
)
45+
46+
try {
47+
const pluginPath = path.join(SRC_DIR, 'index.ts')
48+
const pluginModule = (await import(pathToFileURL(pluginPath).href)) as {
49+
default: (args: {
50+
client: { app: { log: (entry: unknown) => Promise<void> } }
51+
directory: string
52+
}) => Promise<{
53+
'experimental.chat.system.transform': (
54+
input: unknown,
55+
output: { system: string[] },
56+
) => Promise<void>
57+
}>
58+
}
59+
60+
const plugin = await pluginModule.default({
61+
client: {
62+
app: {
63+
log: async () => {},
64+
},
65+
},
66+
directory: tempDir,
67+
})
68+
69+
fs.writeFileSync(bootstrapPath, 'UPDATED bootstrap content')
70+
71+
const output = { system: ['Existing system prompt'] }
72+
await plugin['experimental.chat.system.transform']({}, output)
73+
74+
expect(output.system).toEqual([
75+
'Existing system prompt\n\nINITIAL bootstrap content',
76+
])
77+
expect(output.system[0]).not.toContain('UPDATED bootstrap content')
78+
} finally {
79+
fs.rmSync(tempDir, { recursive: true, force: true })
80+
}
81+
})
82+
2783
test('CI smoke test validates the workflow plugin export and registry drift contract', () => {
2884
const workflowPath = path.join(ROOT_DIR, '.github/workflows/main.yaml')
2985
const workflow = fs.readFileSync(workflowPath, 'utf8')

0 commit comments

Comments
 (0)