@raidou/pi-pm-subagents 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +7 -0
- package/AGENTS.md +1 -0
- package/README.md +85 -0
- package/README.zh-CN.md +85 -0
- package/agents/explorer.md +10 -0
- package/agents/planner.md +16 -0
- package/agents/researcher.md +22 -0
- package/agents/reviewer.md +10 -0
- package/eslint.config.mjs +14 -0
- package/example-prompts/coordinator.md +21 -0
- package/package.json +48 -0
- package/pm-subagents-prompts/coordinator.md +16 -0
- package/pnpm-workspace.yaml +5 -0
- package/src/bash-readonly.test.ts +331 -0
- package/src/bash-readonly.ts +205 -0
- package/src/coordinator/coordinator.test.ts +28 -0
- package/src/coordinator/coordinator.ts +275 -0
- package/src/custom-select.test.ts +91 -0
- package/src/custom-select.ts +209 -0
- package/src/index.ts +87 -0
- package/src/models-config/models-config.test.ts +88 -0
- package/src/models-config/models-config.ts +205 -0
- package/src/models-config/scoped-models-editor.test.ts +189 -0
- package/src/models-config/scoped-models-editor.ts +412 -0
- package/src/models-config/subagent-model-constants.ts +2 -0
- package/src/models-config/subagent-model-cycle.ts +53 -0
- package/src/models-config/subagent-model-utils.test.ts +250 -0
- package/src/models-config/subagent-model-utils.ts +52 -0
- package/src/pm-mode.test.ts +324 -0
- package/src/pm-mode.ts +142 -0
- package/src/prompts/mode.test.ts +289 -0
- package/src/prompts/mode.ts +31 -0
- package/src/prompts/roles.test.ts +724 -0
- package/src/prompts/roles.ts +119 -0
- package/src/subagent/activity.test.ts +230 -0
- package/src/subagent/activity.ts +60 -0
- package/src/subagent/batcher.test.ts +198 -0
- package/src/subagent/batcher.ts +51 -0
- package/src/subagent/consts.ts +1 -0
- package/src/subagent/demo.ts +773 -0
- package/src/subagent/fleet.test.ts +1758 -0
- package/src/subagent/fleet.ts +376 -0
- package/src/subagent/identity.test.ts +31 -0
- package/src/subagent/identity.ts +16 -0
- package/src/subagent/manager.test.ts +392 -0
- package/src/subagent/manager.ts +277 -0
- package/src/subagent/tools.ts +314 -0
- package/src/subagent/viewer.ts +305 -0
- package/src/types.ts +15 -0
- package/src/ui/border-view.ts +50 -0
- package/src/ui/review-pager.ts +146 -0
- package/src/ui/scroll-view.test.ts +190 -0
- package/src/ui/scroll-view.ts +155 -0
- package/src/utils/format.test.ts +76 -0
- package/src/utils/format.ts +67 -0
- package/src/utils/fs.ts +9 -0
- package/src/utils/markdown.test.ts +442 -0
- package/src/utils/markdown.ts +79 -0
- package/src/utils/messages.test.ts +436 -0
- package/src/utils/messages.ts +131 -0
- package/src/utils/model-ref.test.ts +42 -0
- package/src/utils/model-ref.ts +44 -0
- package/src/utils/state.test.ts +98 -0
- package/src/utils/state.ts +45 -0
- package/src/utils/tools.ts +48 -0
- package/src/utils/truncate.test.ts +41 -0
- package/src/utils/truncate.ts +59 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +8 -0
package/src/pm-mode.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ThemeColor,
|
|
5
|
+
} from '@earendil-works/pi-coding-agent'
|
|
6
|
+
|
|
7
|
+
import { BASH_READONLY_TOOL_NAME } from './bash-readonly.js'
|
|
8
|
+
import type { PmMode, PmSubagentState } from './types.js'
|
|
9
|
+
import type { PromptDefinition } from './utils/markdown.js'
|
|
10
|
+
import { modelRefOf, resolveModelRef } from './utils/model-ref.js'
|
|
11
|
+
import { persist } from './utils/state.js'
|
|
12
|
+
import { composeTools, type ToolConfig } from './utils/tools.js'
|
|
13
|
+
|
|
14
|
+
export type { ToolConfig } from './utils/tools.js'
|
|
15
|
+
|
|
16
|
+
/** Restore the model captured before entering a read-only mode. */
|
|
17
|
+
export async function restoreModel(
|
|
18
|
+
pi: ExtensionAPI,
|
|
19
|
+
state: PmSubagentState,
|
|
20
|
+
ctx: ExtensionContext,
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
const previousModel = state.previousModel
|
|
23
|
+
state.previousModel = undefined
|
|
24
|
+
if (!previousModel) return
|
|
25
|
+
|
|
26
|
+
const currentModel = ctx.model
|
|
27
|
+
if (currentModel) {
|
|
28
|
+
const currentRef = `${currentModel.provider}/${currentModel.id}`
|
|
29
|
+
if (currentRef === previousModel) return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const model = resolveModelRef(ctx, [previousModel])
|
|
33
|
+
if (model) await pi.setModel(model)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function baseToolsOf(
|
|
37
|
+
pi: ExtensionAPI,
|
|
38
|
+
state: PmSubagentState,
|
|
39
|
+
): string[] {
|
|
40
|
+
const diff = state.modeDiffTools
|
|
41
|
+
if (!diff) return pi.getActiveTools()
|
|
42
|
+
const current = pi.getActiveTools()
|
|
43
|
+
const added = new Set(diff.added)
|
|
44
|
+
const merged = [...new Set([...current, ...diff.removed])]
|
|
45
|
+
return merged.filter((name) => !added.has(name))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function restoreTools(pi: ExtensionAPI, state: PmSubagentState): void {
|
|
49
|
+
pi.setActiveTools(baseToolsOf(pi, state))
|
|
50
|
+
state.modeDiffTools = undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function assertModeIdle(state: PmSubagentState, entering: PmMode): void {
|
|
54
|
+
if (state.mode !== undefined) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Cannot enter ${entering} mode while in ${state.mode} mode — exit the current mode first.`,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const WRITE_TOOLS = new Set(['edit', 'write'])
|
|
62
|
+
|
|
63
|
+
const BASH_REPLACEMENT: ReadonlyMap<string, string> = new Map([
|
|
64
|
+
['bash', BASH_READONLY_TOOL_NAME],
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
export function calculateModeTools(
|
|
68
|
+
pi: ExtensionAPI,
|
|
69
|
+
state: PmSubagentState,
|
|
70
|
+
config: ToolConfig = {},
|
|
71
|
+
): string[] {
|
|
72
|
+
const base = pi.getActiveTools()
|
|
73
|
+
const applied = composeTools(base, config)
|
|
74
|
+
.filter((name) => !WRITE_TOOLS.has(name))
|
|
75
|
+
.map((name) => BASH_REPLACEMENT.get(name) ?? name)
|
|
76
|
+
|
|
77
|
+
if (!state.modeDiffTools) {
|
|
78
|
+
state.modeDiffTools = {
|
|
79
|
+
added: applied.filter((name) => !base.includes(name)),
|
|
80
|
+
removed: base.filter((name) => !applied.includes(name)),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return applied
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function exitModeFor(
|
|
87
|
+
pi: ExtensionAPI,
|
|
88
|
+
state: PmSubagentState,
|
|
89
|
+
ctx: ExtensionContext,
|
|
90
|
+
name: PmMode,
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
restoreTools(pi, state)
|
|
93
|
+
await restoreModel(pi, state, ctx)
|
|
94
|
+
ctx.ui.setStatus(name, undefined)
|
|
95
|
+
persist(pi, state)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function applyModeModel(
|
|
99
|
+
pi: ExtensionAPI,
|
|
100
|
+
state: PmSubagentState,
|
|
101
|
+
ctx: ExtensionContext,
|
|
102
|
+
modelRef: string | undefined,
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
if (!modelRef) return
|
|
105
|
+
|
|
106
|
+
const model = resolveModelRef(ctx, [modelRef])
|
|
107
|
+
if (!model) {
|
|
108
|
+
ctx.ui.notify(`Invalid model ref: ${modelRef}`, 'warning')
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!state.previousModel) {
|
|
113
|
+
const current = ctx.model
|
|
114
|
+
if (current) state.previousModel = modelRefOf(current)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const success = await pi.setModel(model)
|
|
118
|
+
if (!success) {
|
|
119
|
+
ctx.ui.notify('No API key for this model', 'error')
|
|
120
|
+
state.previousModel = undefined
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface ModeSetupOptions {
|
|
125
|
+
/** The mode's prompt definition. Used to derive the active tool set. */
|
|
126
|
+
promptDefinition: PromptDefinition
|
|
127
|
+
/** Theme color role used for the mode's status indicator. */
|
|
128
|
+
color: ThemeColor
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function applyModeFor(
|
|
132
|
+
pi: ExtensionAPI,
|
|
133
|
+
state: PmSubagentState,
|
|
134
|
+
pmMode: PmMode,
|
|
135
|
+
ctx: ExtensionContext,
|
|
136
|
+
options: ModeSetupOptions,
|
|
137
|
+
): Promise<void> {
|
|
138
|
+
pi.setActiveTools(calculateModeTools(pi, state, options.promptDefinition.fm))
|
|
139
|
+
await applyModeModel(pi, state, ctx, options.promptDefinition.fm.model)
|
|
140
|
+
ctx.ui.setStatus(pmMode, ctx.ui.theme.fg(options.color, pmMode))
|
|
141
|
+
persist(pi, state)
|
|
142
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
import { parseFrontmatter } from '@earendil-works/pi-coding-agent'
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
8
|
+
|
|
9
|
+
import { readModePrompt } from './mode.js'
|
|
10
|
+
|
|
11
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
|
|
13
|
+
const MOCK_AGENT_DIR_VAR = 'PI_CODING_AGENT_DIR'
|
|
14
|
+
|
|
15
|
+
describe('mode', () => {
|
|
16
|
+
let originalAgentDir: string | undefined
|
|
17
|
+
let tempDir: string
|
|
18
|
+
let modesDir: string
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
originalAgentDir = process.env[MOCK_AGENT_DIR_VAR]
|
|
22
|
+
tempDir = await mkdtemp(join(tmpdir(), 'pi-pm-subagents-test-mode-'))
|
|
23
|
+
modesDir = join(tempDir, 'pm-subagents-prompts')
|
|
24
|
+
await mkdir(modesDir, { recursive: true })
|
|
25
|
+
process.env[MOCK_AGENT_DIR_VAR] = tempDir
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
await rm(tempDir, { recursive: true, force: true })
|
|
30
|
+
process.env[MOCK_AGENT_DIR_VAR] = originalAgentDir
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('readModePrompt', () => {
|
|
34
|
+
it('reads mode prompt with override', async () => {
|
|
35
|
+
await writeFile(
|
|
36
|
+
join(modesDir, 'test-mode.md'),
|
|
37
|
+
`---
|
|
38
|
+
description: A test mode.
|
|
39
|
+
tools: [read, write]
|
|
40
|
+
---
|
|
41
|
+
This is a test mode prompt.`,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const result = await readModePrompt('test-mode')
|
|
45
|
+
expect(result.fm.description).toBe('A test mode.')
|
|
46
|
+
expect(result.fm.tools).toEqual(['read', 'write'])
|
|
47
|
+
expect(result.systemPrompt).toBe('This is a test mode prompt.')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('handles mode without frontmatter', async () => {
|
|
51
|
+
await writeFile(
|
|
52
|
+
join(modesDir, 'no-fm.md'),
|
|
53
|
+
'Plain prompt without frontmatter.',
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const result = await readModePrompt('no-fm')
|
|
57
|
+
expect(result.fm.description).toBeUndefined()
|
|
58
|
+
expect(result.fm.tools).toBeUndefined()
|
|
59
|
+
expect(result.systemPrompt).toBe('Plain prompt without frontmatter.')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('supports tools as array', async () => {
|
|
63
|
+
await writeFile(
|
|
64
|
+
join(modesDir, 'array-tools.md'),
|
|
65
|
+
`---
|
|
66
|
+
tools: [read, write, edit]
|
|
67
|
+
---
|
|
68
|
+
Test prompt.`,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const result = await readModePrompt('array-tools')
|
|
72
|
+
expect(result.fm.tools).toEqual(['read', 'write', 'edit'])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('supports extraTools as array', async () => {
|
|
76
|
+
await writeFile(
|
|
77
|
+
join(modesDir, 'extra-array.md'),
|
|
78
|
+
`---
|
|
79
|
+
extraTools: [bash, grep]
|
|
80
|
+
---
|
|
81
|
+
Test prompt.`,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const result = await readModePrompt('extra-array')
|
|
85
|
+
expect(result.fm.extraTools).toEqual(['bash', 'grep'])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('supports removeTools as array', async () => {
|
|
89
|
+
await writeFile(
|
|
90
|
+
join(modesDir, 'remove-array.md'),
|
|
91
|
+
`---
|
|
92
|
+
removeTools: [write, bash]
|
|
93
|
+
---
|
|
94
|
+
Test prompt.`,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const result = await readModePrompt('remove-array')
|
|
98
|
+
expect(result.fm.removeTools).toEqual(['write', 'bash'])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('supports model field', async () => {
|
|
102
|
+
await writeFile(
|
|
103
|
+
join(modesDir, 'model-test.md'),
|
|
104
|
+
`---
|
|
105
|
+
model: anthropic/claude-sonnet-4-5
|
|
106
|
+
---
|
|
107
|
+
Test prompt.`,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const result = await readModePrompt('model-test')
|
|
111
|
+
expect(result.fm.model).toBe('anthropic/claude-sonnet-4-5')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('appends extra content from -append.md', async () => {
|
|
115
|
+
await writeFile(
|
|
116
|
+
join(modesDir, 'with-append.md'),
|
|
117
|
+
`---
|
|
118
|
+
description: Base mode.
|
|
119
|
+
---
|
|
120
|
+
Base prompt content.`,
|
|
121
|
+
)
|
|
122
|
+
await writeFile(
|
|
123
|
+
join(modesDir, 'with-append-append.md'),
|
|
124
|
+
'Appended extra content.',
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const result = await readModePrompt('with-append')
|
|
128
|
+
expect(result.fm.description).toBe('Base mode.')
|
|
129
|
+
expect(result.systemPrompt).toBe(
|
|
130
|
+
'Base prompt content.\n\nAppended extra content.',
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('handles append without override', async () => {
|
|
135
|
+
await writeFile(
|
|
136
|
+
join(modesDir, 'coordinator-append.md'),
|
|
137
|
+
'Appended extra content.',
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const bundled = await readFile(
|
|
141
|
+
join(here, '../../pm-subagents-prompts/coordinator.md'),
|
|
142
|
+
'utf8',
|
|
143
|
+
)
|
|
144
|
+
const { body } = parseFrontmatter(bundled)
|
|
145
|
+
|
|
146
|
+
const result = await readModePrompt('coordinator')
|
|
147
|
+
// Intentionally structural assertions instead of hardcoding the full
|
|
148
|
+
// body: tolerate wording edits to the bundled prompt while guarding
|
|
149
|
+
// against sections being accidentally dropped from it.
|
|
150
|
+
expect(result.systemPrompt).toContain('You are a COORDINATOR agent')
|
|
151
|
+
expect(result.systemPrompt).toContain('## Responsibilities')
|
|
152
|
+
expect(result.systemPrompt).toContain(
|
|
153
|
+
'- Delegate tasks and wait for me to tell you subagent',
|
|
154
|
+
)
|
|
155
|
+
expect(result.systemPrompt).toContain(
|
|
156
|
+
`- Do not trust a subagent's self-reported result blindly.`,
|
|
157
|
+
)
|
|
158
|
+
expect(result.systemPrompt).toContain(body.trim())
|
|
159
|
+
expect(result.systemPrompt).toContain(
|
|
160
|
+
`${body.trim()}\n\nAppended extra content.`,
|
|
161
|
+
)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('handles empty tools array', async () => {
|
|
165
|
+
await writeFile(
|
|
166
|
+
join(modesDir, 'empty-tools.md'),
|
|
167
|
+
`---
|
|
168
|
+
tools: []
|
|
169
|
+
---
|
|
170
|
+
Test prompt.`,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
const result = await readModePrompt('empty-tools')
|
|
174
|
+
expect(result.fm.tools).toEqual([])
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('handles empty extraTools array', async () => {
|
|
178
|
+
await writeFile(
|
|
179
|
+
join(modesDir, 'empty-extra.md'),
|
|
180
|
+
`---
|
|
181
|
+
extraTools: []
|
|
182
|
+
---
|
|
183
|
+
Test prompt.`,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
const result = await readModePrompt('empty-extra')
|
|
187
|
+
expect(result.fm.extraTools).toEqual([])
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('handles empty removeTools array', async () => {
|
|
191
|
+
await writeFile(
|
|
192
|
+
join(modesDir, 'empty-remove.md'),
|
|
193
|
+
`---
|
|
194
|
+
removeTools: []
|
|
195
|
+
---
|
|
196
|
+
Test prompt.`,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
const result = await readModePrompt('empty-remove')
|
|
200
|
+
expect(result.fm.removeTools).toEqual([])
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('frontmatter from base is preserved when append exists', async () => {
|
|
204
|
+
await writeFile(
|
|
205
|
+
join(modesDir, 'fm-base.md'),
|
|
206
|
+
`---
|
|
207
|
+
description: Frontmatter from base.
|
|
208
|
+
tools: [read]
|
|
209
|
+
---
|
|
210
|
+
Base content.`,
|
|
211
|
+
)
|
|
212
|
+
await writeFile(join(modesDir, 'fm-base-append.md'), 'Append content.')
|
|
213
|
+
|
|
214
|
+
const result = await readModePrompt('fm-base')
|
|
215
|
+
expect(result.fm.description).toBe('Frontmatter from base.')
|
|
216
|
+
expect(result.fm.tools).toEqual(['read'])
|
|
217
|
+
expect(result.systemPrompt).toBe('Base content.\n\nAppend content.')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('append tools merge with base tools (union, deduped)', async () => {
|
|
221
|
+
await writeFile(
|
|
222
|
+
join(modesDir, 'tools-merge.md'),
|
|
223
|
+
`---
|
|
224
|
+
tools: [read, write]
|
|
225
|
+
---
|
|
226
|
+
Base content.`,
|
|
227
|
+
)
|
|
228
|
+
await writeFile(
|
|
229
|
+
join(modesDir, 'tools-merge-append.md'),
|
|
230
|
+
`---
|
|
231
|
+
tools: [write, grep]
|
|
232
|
+
---
|
|
233
|
+
`,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
const result = await readModePrompt('tools-merge')
|
|
237
|
+
expect(result.fm.tools).toEqual(['read', 'write', 'grep'])
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('merges description, model, tools, and systemPrompt from append', async () => {
|
|
241
|
+
await writeFile(
|
|
242
|
+
join(modesDir, 'all-merge.md'),
|
|
243
|
+
`---
|
|
244
|
+
description: Base description.
|
|
245
|
+
model: base/model
|
|
246
|
+
tools: [read, write]
|
|
247
|
+
---
|
|
248
|
+
Base content.`,
|
|
249
|
+
)
|
|
250
|
+
await writeFile(
|
|
251
|
+
join(modesDir, 'all-merge-append.md'),
|
|
252
|
+
`---
|
|
253
|
+
description: Append description.
|
|
254
|
+
model: append/model
|
|
255
|
+
tools: [grep]
|
|
256
|
+
---
|
|
257
|
+
Append body.`,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
const result = await readModePrompt('all-merge')
|
|
261
|
+
expect(result.fm.description).toBe('Append description.')
|
|
262
|
+
expect(result.fm.model).toBe('append/model')
|
|
263
|
+
expect(result.fm.tools).toEqual(['read', 'write', 'grep'])
|
|
264
|
+
expect(result.systemPrompt).toBe('Base content.\n\nAppend body.')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('append with frontmatter only and empty body merges cleanly', async () => {
|
|
268
|
+
await writeFile(
|
|
269
|
+
join(modesDir, 'empty-body.md'),
|
|
270
|
+
`---
|
|
271
|
+
description: Base description.
|
|
272
|
+
---
|
|
273
|
+
Base content.`,
|
|
274
|
+
)
|
|
275
|
+
await writeFile(
|
|
276
|
+
join(modesDir, 'empty-body-append.md'),
|
|
277
|
+
`---
|
|
278
|
+
model: append/model
|
|
279
|
+
---
|
|
280
|
+
`,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
const result = await readModePrompt('empty-body')
|
|
284
|
+
expect(result.fm.description).toBe('Base description.')
|
|
285
|
+
expect(result.fm.model).toBe('append/model')
|
|
286
|
+
expect(result.systemPrompt).toBe('Base content.')
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
|
|
4
|
+
import { getAgentDir } from '@earendil-works/pi-coding-agent'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
loadMarkdown,
|
|
8
|
+
mergePromptDefinitions,
|
|
9
|
+
type PromptDefinition,
|
|
10
|
+
} from '../utils/markdown.js'
|
|
11
|
+
|
|
12
|
+
export async function readModePrompt(name: string): Promise<PromptDefinition> {
|
|
13
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
14
|
+
const agentDir = getAgentDir()
|
|
15
|
+
const modesPromptsDir = join(agentDir, 'pm-subagents-prompts')
|
|
16
|
+
|
|
17
|
+
const overridePath = join(modesPromptsDir, `${name}.md`)
|
|
18
|
+
const bundledPath = join(here, '../..', 'pm-subagents-prompts', `${name}.md`)
|
|
19
|
+
const appendPath = join(modesPromptsDir, `${name}-append.md`)
|
|
20
|
+
|
|
21
|
+
const base =
|
|
22
|
+
(await loadMarkdown(overridePath)) ?? (await loadMarkdown(bundledPath))
|
|
23
|
+
|
|
24
|
+
const append = await loadMarkdown(appendPath)
|
|
25
|
+
|
|
26
|
+
if (!base) throw new Error(`Mode prompt "${name}" not found`)
|
|
27
|
+
|
|
28
|
+
if (!append) return base
|
|
29
|
+
|
|
30
|
+
return mergePromptDefinitions(base, append)
|
|
31
|
+
}
|