@menghuan1918/dsh-apollo 0.0.0-stage → 0.3.0
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/LICENSE +674 -0
- package/README.md +232 -2
- package/cordis.patch.yml +78 -0
- package/docs/DEVELOPMENT.md +629 -0
- package/package.json +75 -4
- package/presets/eng/NOTICE +2 -0
- package/presets/eng/plugins/exit-guard/index.js +111 -0
- package/presets/eng/plugins/exit-guard/package.json +7 -0
- package/presets/eng/plugins/mode-instructions/index.js +48 -0
- package/presets/eng/plugins/mode-instructions/package.json +7 -0
- package/presets/eng/plugins/wait-subagent/index.js +228 -0
- package/presets/eng/plugins/wait-subagent/package.json +7 -0
- package/presets/eng-noask/NOTICE +2 -0
- package/presets/eng-noask/plugins/exit-guard/index.js +111 -0
- package/presets/eng-noask/plugins/exit-guard/package.json +7 -0
- package/presets/eng-noask/plugins/mode-instructions/index.js +48 -0
- package/presets/eng-noask/plugins/mode-instructions/package.json +7 -0
- package/presets/eng-noask/plugins/wait-subagent/index.js +228 -0
- package/presets/eng-noask/plugins/wait-subagent/package.json +7 -0
- package/presets/eng-noask.patch.yml +341 -0
- package/presets/eng.patch.yml +341 -0
- package/skills/brainstorming/SKILL.md +90 -0
- package/skills/brainstorming/spec-document-reviewer-prompt.md +50 -0
- package/skills/code-review/SKILL.md +90 -0
- package/skills/dev/SKILL.md +43 -0
- package/skills/diagnosing-bugs/SKILL.md +132 -0
- package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +55 -0
- package/skills/exec/SKILL.md +91 -0
- package/skills/handoff/SKILL.md +23 -0
- package/skills/main-agent-driven-development/SKILL.md +64 -0
- package/skills/tdd/SKILL.md +34 -0
- package/skills/tdd/mocking.md +59 -0
- package/skills/tdd/tests.md +77 -0
- package/src/cli/claude.js +166 -0
- package/src/cli/codex.js +164 -0
- package/src/cli/jsonl.js +216 -0
- package/src/cli-adapter.js +198 -0
- package/src/cli-sessions.js +69 -0
- package/src/client.js +815 -0
- package/src/config.js +301 -0
- package/src/dsh-home.js +38 -0
- package/src/index.js +181 -0
- package/src/panel.js +452 -0
- package/src/settings.js +191 -0
- package/src/subagents.js +199 -0
- package/third-party/LICENSE-MIT-dsh-standard-preset +21 -0
- package/third-party/NOTICE-mattpocock-skills +21 -0
package/src/subagents.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { join, isAbsolute, resolve } from 'node:path'
|
|
4
|
+
import { normalizeConfig } from './config.js'
|
|
5
|
+
import { CLI_ROUTES, createCliAdapter, enabledIn, selectCli } from './cli-adapter.js'
|
|
6
|
+
import { openCliSessions } from './cli-sessions.js'
|
|
7
|
+
|
|
8
|
+
const CLI_NAMES = Object.keys(CLI_ROUTES)
|
|
9
|
+
const cliName = (provider) => CLI_NAMES.includes(provider) ? provider : undefined
|
|
10
|
+
const internalRoute = (provider) => Object.values(CLI_ROUTES).includes(provider)
|
|
11
|
+
const success = (value) => ({ isError: false, value, content: [] })
|
|
12
|
+
|
|
13
|
+
export function cliCatalog(config, request = {}) {
|
|
14
|
+
const providers = CLI_NAMES.filter((cli) => normalizeConfig(config).subagents[cli].enabled)
|
|
15
|
+
if (request.provider === undefined) return providers.map((cli) => `${cli} — ${cli === 'codex' ? 'Codex' : 'Claude'} CLI (background continuable subagents)`).join('\n')
|
|
16
|
+
const cli = cliName(request.provider)
|
|
17
|
+
if (!cli) return ''
|
|
18
|
+
const backend = normalizeConfig(config).subagents[cli]
|
|
19
|
+
if (!backend.enabled) throw new Error(`Apollo ${cli} backend is disabled`)
|
|
20
|
+
if (request.model === undefined) return backend.models.map((model) => `${cli}/${model.id}${model.id === backend.defaultModel ? ' (default)' : ''}`).join('\n') || '(no allowed models)'
|
|
21
|
+
const model = backend.models.find((item) => item.id === request.model)
|
|
22
|
+
if (!model) throw new Error(`Apollo ${cli} model "${request.model}" is not allowed`)
|
|
23
|
+
return `${cli}/${model.id}\nReasoning efforts:\n${model.efforts.map((effort) => `${effort}${effort === model.defaultEffort ? ' (default)' : ''}`).join('\n')}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the child's initial absolute directory from the parent's effective one.
|
|
28
|
+
*
|
|
29
|
+
* The Session header keeps the original project directory, so it can be stale
|
|
30
|
+
* after a working-directory change; the working-directory service is the only
|
|
31
|
+
* authority for the parent's current directory. An absolute explicit `cwd`
|
|
32
|
+
* needs no such lookup, a relative one resolves against that current directory,
|
|
33
|
+
* and omission inherits it. Resolving here keeps the KV record and the
|
|
34
|
+
* manager's pinned initial directory identical.
|
|
35
|
+
* @param {object} ctx - runtime owning the working-directory service.
|
|
36
|
+
* @param {object} parent - calling Agent whose directory is inherited.
|
|
37
|
+
* @param {unknown} requested - explicit tool `cwd`, already schema-validated upstream.
|
|
38
|
+
* @param {AbortSignal} signal - cancellation for the directory inspection.
|
|
39
|
+
* @returns {Promise<string>} absolute child directory.
|
|
40
|
+
*/
|
|
41
|
+
async function childCwd(ctx, parent, requested, signal) {
|
|
42
|
+
if (requested !== undefined && typeof requested !== 'string') throw new Error('cwd must be a string')
|
|
43
|
+
signal.throwIfAborted()
|
|
44
|
+
if (requested !== undefined && isAbsolute(requested)) return requested
|
|
45
|
+
const current = await ctx.workingDirectory.ensure(parent, signal)
|
|
46
|
+
signal.throwIfAborted()
|
|
47
|
+
if (!current || !isAbsolute(current)) throw new Error('Apollo CLI subagent requires an absolute session working directory')
|
|
48
|
+
return requested === undefined ? current : resolve(current, requested)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function startCliSubagent(ctx, config, sessions, args, exec) {
|
|
52
|
+
const cli = cliName(args.provider)
|
|
53
|
+
if (!cli) throw new Error('Unknown Apollo CLI provider')
|
|
54
|
+
const parent = exec.agent
|
|
55
|
+
if (!parent) throw new Error('Apollo subagent requires a calling Agent')
|
|
56
|
+
if (!enabledIn(config, parent)) throw new Error('Apollo CLI subagents are disabled for this preset')
|
|
57
|
+
if (args.run_in_background !== undefined) throw new Error('run_in_background was removed for CLI subagents; they always run in the background and are continued with send_message')
|
|
58
|
+
if (args.agent_preset !== undefined) throw new Error('agent_preset is not supported for CLI subagents yet')
|
|
59
|
+
if (typeof args.prompt !== 'string' || typeof args.description !== 'string') throw new Error('subagent requires a prompt and description')
|
|
60
|
+
const selected = selectCli(config, cli, args.model, args.reasoning_effort)
|
|
61
|
+
const cwd = await childCwd(ctx, parent, args.cwd, exec.signal)
|
|
62
|
+
let executable
|
|
63
|
+
try {
|
|
64
|
+
const command = selected.executable.startsWith('~/') ? join(homedir(), selected.executable.slice(2)) : selected.executable
|
|
65
|
+
executable = await ctx.subprocess.resolveExecutable(command, undefined, exec.signal)
|
|
66
|
+
} catch (error) {
|
|
67
|
+
exec.signal.throwIfAborted()
|
|
68
|
+
throw new Error(`Apollo ${cli} executable is unavailable; set its absolute path in Plugins → Apollo`, { cause: error })
|
|
69
|
+
}
|
|
70
|
+
const id = randomUUID()
|
|
71
|
+
const record = {
|
|
72
|
+
version: 1, id, parentId: parent.id, cli, cwd,
|
|
73
|
+
executable, profile: cli === 'codex' ? selected.profile : '',
|
|
74
|
+
cliHome: cli === 'codex' ? process.env.CODEX_HOME ?? join(homedir(), '.codex')
|
|
75
|
+
: process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'),
|
|
76
|
+
// Explicit CLAUDE_CONFIG_DIR changes config and keychain lookup, even for ~/.claude.
|
|
77
|
+
...(cli === 'claude' && process.env.CLAUDE_CONFIG_DIR === undefined ? { claudeDefaultHome: true } : {}),
|
|
78
|
+
model: selected.model, effort: selected.effort, delivered: [], started: false,
|
|
79
|
+
}
|
|
80
|
+
await sessions.put(record)
|
|
81
|
+
try {
|
|
82
|
+
exec.signal.throwIfAborted()
|
|
83
|
+
const maxDepth = ctx.subagents.resolveMaxDepth()
|
|
84
|
+
const created = await ctx.subagents.startActivation({
|
|
85
|
+
provider: 'spawn', label: args.description, childId: id, signal: exec.signal,
|
|
86
|
+
delivery: 'parent',
|
|
87
|
+
request: {
|
|
88
|
+
parent, prompt: [{ type: 'text', text: args.prompt }], cwd,
|
|
89
|
+
agentOptions: { provider: CLI_ROUTES[cli], model: selected.model, reasoningEffort: selected.effort },
|
|
90
|
+
...(maxDepth === undefined ? {} : { maxDepth }),
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
exec.signal.throwIfAborted()
|
|
94
|
+
return { kind: 'activation', subagentId: created.childId }
|
|
95
|
+
} catch (error) {
|
|
96
|
+
// The manager rolls back pre-admission failure; drain also covers the final
|
|
97
|
+
// acceptance/cancellation race before this tool could hand the id back.
|
|
98
|
+
await ctx.subagents.drainChildren(parent, [id])
|
|
99
|
+
await sessions.delete(id)
|
|
100
|
+
throw error
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Host-level interception extends existing tools without replacing registrations. */
|
|
105
|
+
export function installCliRouting(ctx, config, sessions) {
|
|
106
|
+
const unpublished = new Map()
|
|
107
|
+
const cleanups = new Set()
|
|
108
|
+
const creating = new Set()
|
|
109
|
+
const lifetime = new AbortController()
|
|
110
|
+
const reclaim = (entry) => {
|
|
111
|
+
if (entry.cleanup) return entry.cleanup
|
|
112
|
+
const task = (async () => {
|
|
113
|
+
await ctx.subagents.drainChildren(entry.parent, [entry.id])
|
|
114
|
+
await sessions.delete(entry.id)
|
|
115
|
+
})()
|
|
116
|
+
entry.cleanup = task
|
|
117
|
+
cleanups.add(task)
|
|
118
|
+
task.catch((error) => ctx.logger?.warn?.(`Apollo could not reclaim unreported CLI child ${entry.id}: ${error.message}`))
|
|
119
|
+
.finally(() => cleanups.delete(task))
|
|
120
|
+
return task
|
|
121
|
+
}
|
|
122
|
+
ctx.on('tools/result', (exec, result) => {
|
|
123
|
+
const key = exec.token ?? exec
|
|
124
|
+
const entry = unpublished.get(key)
|
|
125
|
+
if (!entry) return
|
|
126
|
+
unpublished.delete(key)
|
|
127
|
+
if (!result.isError && result.value?.kind === 'activation' && result.value.subagentId === entry.id) return
|
|
128
|
+
void reclaim(entry)
|
|
129
|
+
})
|
|
130
|
+
ctx.tools.guard((exec) => {
|
|
131
|
+
const args = exec.arguments ?? {}
|
|
132
|
+
if (!['subagent', 'subagent_fork'].includes(exec.name)) return
|
|
133
|
+
if (internalRoute(args.provider)) return 'Use provider="codex" or provider="claude" with subagent; internal CLI routes are reserved'
|
|
134
|
+
const selected = cliName(args.provider)
|
|
135
|
+
if (selected && !enabledIn(config, exec.agent)) return 'Apollo CLI subagents are disabled for this preset'
|
|
136
|
+
if (exec.name === 'subagent_fork' && (selected || (args.provider === undefined && internalRoute(exec.agent?.options.provider)))) {
|
|
137
|
+
return 'Codex/Claude support background subagent creation only; subagent_fork cannot create a CLI child'
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
ctx.on('tools/execute', async (exec, next) => {
|
|
141
|
+
const args = exec.arguments ?? {}
|
|
142
|
+
if (!enabledIn(config, exec.agent)) return next()
|
|
143
|
+
if (exec.name === 'subagent' && cliName(args.provider)) {
|
|
144
|
+
const signal = AbortSignal.any([exec.signal, lifetime.signal])
|
|
145
|
+
const creation = (async () => {
|
|
146
|
+
const value = await startCliSubagent(ctx, config, sessions, args, { ...exec, signal })
|
|
147
|
+
unpublished.set(exec.token ?? exec, { parent: exec.agent, id: value.subagentId })
|
|
148
|
+
return success(value)
|
|
149
|
+
})()
|
|
150
|
+
creating.add(creation)
|
|
151
|
+
try { return await creation } finally { creating.delete(creation) }
|
|
152
|
+
}
|
|
153
|
+
if (exec.name !== 'list_subagent_models') return next()
|
|
154
|
+
if (cliName(args.provider)) return success(cliCatalog(config, args))
|
|
155
|
+
if (args.provider !== undefined || args.model !== undefined) return next()
|
|
156
|
+
const base = await next()
|
|
157
|
+
const extra = cliCatalog(config)
|
|
158
|
+
if (base.isError || !extra) return base
|
|
159
|
+
return success([base.value, extra].filter(Boolean).join('\n'))
|
|
160
|
+
})
|
|
161
|
+
ctx.systemPrompt.section({
|
|
162
|
+
name: 'apollo:cli-subagents', order: 80,
|
|
163
|
+
text: (context) => {
|
|
164
|
+
const agent = context.agent ?? ctx.agents.get(context.session?.id ?? context.scope?.id)
|
|
165
|
+
if (!enabledIn(config, agent)) return ''
|
|
166
|
+
const catalog = cliCatalog(config)
|
|
167
|
+
if (!catalog) return ''
|
|
168
|
+
return [
|
|
169
|
+
'Apollo CLI subagents: existing subagent also accepts provider="codex" or provider="claude".',
|
|
170
|
+
'Call list_subagent_models with that provider to inspect allowed models and reasoning efforts. Model and reasoning_effort may be omitted to use configured defaults.',
|
|
171
|
+
'CLI children always run in the background; run_in_background was removed. Pass cwd to choose their initial directory. subagent_fork and agent_preset are unsupported.',
|
|
172
|
+
'Use send_message, list_agents, interrupt_agent and wait_subagent when available to control their stable child ids. Their CLI permissions come from Apollo settings.',
|
|
173
|
+
catalog,
|
|
174
|
+
].join('\n')
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
return async () => {
|
|
178
|
+
lifetime.abort(new Error('Apollo CLI routing unloaded'))
|
|
179
|
+
await Promise.allSettled([...creating])
|
|
180
|
+
await Promise.allSettled([...unpublished.values()].map(reclaim))
|
|
181
|
+
unpublished.clear()
|
|
182
|
+
await Promise.allSettled([...cleanups])
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function mountSubagents(ctx, config) {
|
|
187
|
+
ctx.inject(['tools', 'subagents', 'agents', 'llm', 'storage', 'subprocess', 'systemPrompt', 'workingDirectory'], async (runtime) => {
|
|
188
|
+
const sessions = await openCliSessions(runtime)
|
|
189
|
+
const adapters = CLI_NAMES.map((cli) => createCliAdapter(runtime, config, sessions, cli))
|
|
190
|
+
let stopRouting = async () => {}
|
|
191
|
+
runtime.effect(() => async () => {
|
|
192
|
+
await stopRouting()
|
|
193
|
+
await Promise.all(adapters.map((adapter) => adapter.dispose()))
|
|
194
|
+
await sessions.close()
|
|
195
|
+
})
|
|
196
|
+
adapters.forEach((adapter, index) => runtime.llm.registerAdapter([CLI_ROUTES[CLI_NAMES[index]]], adapter))
|
|
197
|
+
stopRouting = installCliRouting(runtime, config, sessions)
|
|
198
|
+
})
|
|
199
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matt Pocock
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|