@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
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
import { normalizeConfig } from './config.js'
|
|
4
|
+
import * as codex from './cli/codex.js'
|
|
5
|
+
import * as claude from './cli/claude.js'
|
|
6
|
+
import { readJsonLines, sanitizeError } from './cli/jsonl.js'
|
|
7
|
+
|
|
8
|
+
export const CLI_ROUTES = Object.freeze({ codex: 'apollo-cli-codex', claude: 'apollo-cli-claude' })
|
|
9
|
+
const protocols = { codex, claude }
|
|
10
|
+
const NO_RETRIES = Object.freeze({ mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 500, maxDelayMs: 500, jitterRatio: 0 })
|
|
11
|
+
|
|
12
|
+
export function enabledIn(config, agent) {
|
|
13
|
+
const scope = normalizeConfig(config).subagents
|
|
14
|
+
let preset = agent?.session.header.agentPreset
|
|
15
|
+
try {
|
|
16
|
+
const projections = agent?.ctx?.get?.('sessionProjections')
|
|
17
|
+
if (projections) preset = projections.stateOf(agent.session, 'agentPreset') ?? preset
|
|
18
|
+
} catch { /* Compositions without the optional preset registry use the header. */ }
|
|
19
|
+
return scope.global || (scope.eng && ['eng', 'eng-noask'].includes(preset))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Exact checks are repeated before every CLI turn, including cold resume. */
|
|
23
|
+
export function selectCli(config, cli, model, effort) {
|
|
24
|
+
const backend = normalizeConfig(config).subagents[cli]
|
|
25
|
+
if (!backend?.enabled) throw new Error(`Apollo ${cli} backend is disabled; enable it in Plugins → Apollo`)
|
|
26
|
+
const modelId = model ?? backend.defaultModel
|
|
27
|
+
const selected = backend.models.find((candidate) => candidate.id === modelId)
|
|
28
|
+
if (!selected) throw new Error(`Apollo ${cli} model "${modelId}" is not allowed; check the backend model list`)
|
|
29
|
+
const selectedEffort = effort ?? selected.defaultEffort
|
|
30
|
+
if (!selectedEffort || !selected.efforts.includes(selectedEffort)) {
|
|
31
|
+
throw new Error(`Apollo ${cli} effort "${selectedEffort ?? ''}" is not allowed for model "${modelId}"`)
|
|
32
|
+
}
|
|
33
|
+
return { ...backend, model: modelId, effort: selectedEffort }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Only new user deliveries go to a native CLI that already owns its history. */
|
|
37
|
+
export function pendingMessages(messages, delivered) {
|
|
38
|
+
const seen = new Set(delivered)
|
|
39
|
+
const pending = []
|
|
40
|
+
for (const message of messages) {
|
|
41
|
+
if (message.role !== 'user') continue
|
|
42
|
+
const id = message.id ?? createHash('sha256').update(JSON.stringify(message.content)).digest('hex')
|
|
43
|
+
if (seen.has(id)) continue
|
|
44
|
+
const blocks = typeof message.content === 'string' ? [{ type: 'text', text: message.content }] : message.content
|
|
45
|
+
if (!Array.isArray(blocks) || blocks.some((block) => block.type !== 'text')) {
|
|
46
|
+
throw new Error('Apollo CLI subagents currently accept text messages only')
|
|
47
|
+
}
|
|
48
|
+
const text = blocks.map((block) => block.text).join('\n')
|
|
49
|
+
pending.push({ id, text })
|
|
50
|
+
seen.add(id)
|
|
51
|
+
}
|
|
52
|
+
return pending
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One adapter instance owns only one CLI; DSH owns the containing Agent loop. */
|
|
56
|
+
export function createCliAdapter(ctx, config, sessions, cli) {
|
|
57
|
+
const route = CLI_ROUTES[cli]
|
|
58
|
+
const active = new Set()
|
|
59
|
+
const running = new Set()
|
|
60
|
+
const settling = new Set()
|
|
61
|
+
const lifetime = new AbortController()
|
|
62
|
+
let disposed = false
|
|
63
|
+
const adapter = {
|
|
64
|
+
providerInfo: () => ({ id: route, name: `Apollo ${cli === 'codex' ? 'Codex' : 'Claude'} CLI (subagents)` }),
|
|
65
|
+
providerRetryPolicy: () => NO_RETRIES,
|
|
66
|
+
imageRequestPricing: () => undefined,
|
|
67
|
+
async listModels() {
|
|
68
|
+
const backend = normalizeConfig(config).subagents[cli]
|
|
69
|
+
return backend.enabled ? backend.models.map((model) => ({ provider: route, id: model.id, name: model.id, inputModalities: ['text'] })) : []
|
|
70
|
+
},
|
|
71
|
+
async resolveModel(provider, model) {
|
|
72
|
+
const backend = normalizeConfig(config).subagents[cli]
|
|
73
|
+
const selected = backend.models.find((candidate) => candidate.id === model)
|
|
74
|
+
if (provider !== route || !backend.enabled || !selected) throw new Error(`Apollo ${cli} model "${model}" is unavailable`)
|
|
75
|
+
if (selected.efforts.length === 0) throw new Error(`Apollo ${cli} model "${model}" has no allowed reasoning efforts; configure them in Plugins → Apollo`)
|
|
76
|
+
return {
|
|
77
|
+
provider: route, id: model, name: model, inputModalities: ['text'],
|
|
78
|
+
reasoning: { efforts: selected.efforts.map((id) => ({ id, name: id })), ...(selected.defaultEffort ? { defaultEffort: selected.defaultEffort } : {}) },
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
async prepareCall(provider, model, signal) {
|
|
82
|
+
signal?.throwIfAborted()
|
|
83
|
+
return { model: await adapter.resolveModel(provider, model), stream: (options) => adapter.stream(options) }
|
|
84
|
+
},
|
|
85
|
+
async *stream(options) {
|
|
86
|
+
if (disposed) throw new Error('Apollo CLI adapter has been unloaded')
|
|
87
|
+
const id = options.sessionId
|
|
88
|
+
const agent = id === undefined ? undefined : ctx.agents.get(id)
|
|
89
|
+
let state = id === undefined ? undefined : sessions.get(id)
|
|
90
|
+
if (!agent || !state || state.cli !== cli || agent.session.header.parentSession !== state.parentId
|
|
91
|
+
|| agent.session.header.origin !== 'subagent' || agent.session.header.isSeeded) {
|
|
92
|
+
throw new Error('Apollo CLI routes are reserved for subagent-created continuable sessions')
|
|
93
|
+
}
|
|
94
|
+
if (options.provider !== route || options.model !== state.model || options.reasoningEffort !== state.effort) {
|
|
95
|
+
throw new Error('Apollo CLI session model and effort are fixed at creation')
|
|
96
|
+
}
|
|
97
|
+
if (running.has(id)) throw new Error('Apollo CLI session already has an active turn')
|
|
98
|
+
const current = selectCli(config, cli, state.model, state.effort)
|
|
99
|
+
const pending = pendingMessages(options.messages, state.delivered)
|
|
100
|
+
if (pending.length === 0) throw new Error('Apollo CLI turn has no new user message to deliver')
|
|
101
|
+
if (state.started && !state.remoteId) throw new Error('Apollo CLI session creation was interrupted before its identity was saved; create a new subagent')
|
|
102
|
+
const signal = AbortSignal.any([lifetime.signal, ...options.signal ? [options.signal] : []])
|
|
103
|
+
signal.throwIfAborted()
|
|
104
|
+
running.add(id)
|
|
105
|
+
let settled
|
|
106
|
+
const completion = new Promise((resolve) => { settled = resolve })
|
|
107
|
+
settling.add(completion)
|
|
108
|
+
const prior = structuredClone(state)
|
|
109
|
+
let stateWritten = false
|
|
110
|
+
let handle
|
|
111
|
+
let done
|
|
112
|
+
try {
|
|
113
|
+
const prompt = [
|
|
114
|
+
...pending.map((message) => message.text),
|
|
115
|
+
`Apollo CLI bridge: return your answer as text; DSH forwards the final output automatically. Harness-only messaging tools are unavailable here. This turn uses ${cli} permission mode "${current.permissionMode}" from Apollo settings. Embedded DSH runtime snapshots describe the host tools; CLI permissions are controlled by these CLI settings.`,
|
|
116
|
+
].join('\n\n')
|
|
117
|
+
const { argv, input } = protocols[cli].buildCommand({
|
|
118
|
+
...state, profile: state.profile || undefined, remoteId: state.remoteId, prompt, permissionMode: current.permissionMode,
|
|
119
|
+
})
|
|
120
|
+
// Record attempted delivery before I/O. CLI turns are never retried automatically.
|
|
121
|
+
state = { ...state, started: true, delivered: [...state.delivered, ...pending.map((message) => message.id)] }
|
|
122
|
+
await sessions.put(state)
|
|
123
|
+
stateWritten = true
|
|
124
|
+
signal.throwIfAborted()
|
|
125
|
+
const subprocess = agent.ctx.get('subprocess')
|
|
126
|
+
if (!subprocess) throw new Error('Apollo CLI subagents require the managed subprocess service')
|
|
127
|
+
const env = state.cliHome ? { [cli === 'codex' ? 'CODEX_HOME' : 'CLAUDE_CONFIG_DIR']: state.cliHome } : undefined
|
|
128
|
+
if (cli === 'claude' && state.claudeDefaultHome && env) {
|
|
129
|
+
env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME'] = dirname(state.cliHome)
|
|
130
|
+
env.CLAUDE_CONFIG_DIR = undefined // Managed subprocess tombstone preserves native default/keychain lookup.
|
|
131
|
+
}
|
|
132
|
+
handle = subprocess.spawn({
|
|
133
|
+
argv, cwd: state.cwd,
|
|
134
|
+
stdio: { stdin: { data: input }, stdout: 'pipe', stderr: { maxBytes: 4096 } },
|
|
135
|
+
graceMs: 1500, signal,
|
|
136
|
+
...(env === undefined ? {} : { env }),
|
|
137
|
+
})
|
|
138
|
+
active.add(handle)
|
|
139
|
+
// Observe spawn errors immediately while stdout is being consumed.
|
|
140
|
+
done = handle.done.then((outcome) => ({ outcome }), (error) => ({ error }))
|
|
141
|
+
if (!handle.stdout) throw new Error('Apollo CLI did not expose its JSON event stream')
|
|
142
|
+
const parser = protocols[cli].createParser()
|
|
143
|
+
let protocolError
|
|
144
|
+
for await (const event of readJsonLines(handle.stdout)) {
|
|
145
|
+
const update = parser.push(event)
|
|
146
|
+
if (update?.error) protocolError = update.error
|
|
147
|
+
if (update?.remoteId) {
|
|
148
|
+
if (state.remoteId && state.remoteId !== update.remoteId) throw new Error('CLI resumed a different session; refusing to replace its identity')
|
|
149
|
+
if (!state.remoteId) {
|
|
150
|
+
state = { ...state, remoteId: update.remoteId }
|
|
151
|
+
await sessions.put(state)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
signal.throwIfAborted()
|
|
155
|
+
}
|
|
156
|
+
const result = await done
|
|
157
|
+
signal.throwIfAborted()
|
|
158
|
+
if (result.error) throw new Error(`Apollo ${cli} process could not start: ${result.error.code ?? result.error.name ?? 'spawn error'}`)
|
|
159
|
+
if (result.outcome.exitCode !== 0) {
|
|
160
|
+
const detail = protocolError || handle.collected?.stderr?.readFrom(0).text
|
|
161
|
+
throw new Error(`Apollo ${cli} exited with ${result.outcome.exitCode ?? result.outcome.signal}; check CLI authentication, profile and permissions${detail ? `: ${sanitizeError(detail)}` : ''}`)
|
|
162
|
+
}
|
|
163
|
+
const output = parser.finish()
|
|
164
|
+
if (!state.remoteId) throw new Error(`Apollo ${cli} completed without a resumable session id`)
|
|
165
|
+
if (output.text) {
|
|
166
|
+
yield { type: 'block-start', index: 0, blockType: 'text' }
|
|
167
|
+
yield { type: 'text-delta', index: 0, text: output.text }
|
|
168
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text: output.text } }
|
|
169
|
+
}
|
|
170
|
+
if (output.usage) yield { type: 'usage', usage: output.usage }
|
|
171
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
172
|
+
} finally {
|
|
173
|
+
try {
|
|
174
|
+
if (handle) {
|
|
175
|
+
handle.terminate()
|
|
176
|
+
await done
|
|
177
|
+
await handle.waitForExit()
|
|
178
|
+
} else if (stateWritten) {
|
|
179
|
+
// No process was published, so no native CLI consumed this delivery.
|
|
180
|
+
await sessions.put(prior)
|
|
181
|
+
}
|
|
182
|
+
} finally {
|
|
183
|
+
if (handle) active.delete(handle)
|
|
184
|
+
running.delete(id)
|
|
185
|
+
settling.delete(completion)
|
|
186
|
+
settled()
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
async dispose() {
|
|
191
|
+
disposed = true
|
|
192
|
+
lifetime.abort(new Error('Apollo CLI adapter unloaded'))
|
|
193
|
+
for (const handle of active) handle.terminate()
|
|
194
|
+
await Promise.allSettled([...settling])
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
return adapter
|
|
198
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
/** Durable CLI identities live outside the compactable conversation surface. */
|
|
4
|
+
export async function openCliSessions(ctx) {
|
|
5
|
+
const profile = ctx.get('profileContext')
|
|
6
|
+
const namespace = createHash('sha256').update(profile?.dir ?? 'default').digest('hex').slice(0, 16)
|
|
7
|
+
const backend = ctx.storage.backend.get('json')
|
|
8
|
+
if (!backend?.kv) throw new Error('Apollo CLI subagents require the JSON storage backend')
|
|
9
|
+
const unit = await backend.kv.open({
|
|
10
|
+
name: `apollo_cli_${namespace}`, version: 1, tables: ['sessions'], hasGlobal: false, layout: 'per-record',
|
|
11
|
+
})
|
|
12
|
+
try {
|
|
13
|
+
const snapshot = await unit.loadAll()
|
|
14
|
+
return new CliSessions(unit, snapshot.tables.sessions ?? {})
|
|
15
|
+
} catch (error) {
|
|
16
|
+
await unit.close()
|
|
17
|
+
throw error
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class CliSessions {
|
|
22
|
+
constructor(unit, records = {}) {
|
|
23
|
+
this.unit = unit
|
|
24
|
+
this.records = new Map(Object.entries(records))
|
|
25
|
+
this.writes = Promise.resolve()
|
|
26
|
+
this.closed = false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get(id) {
|
|
30
|
+
const value = this.records.get(id)
|
|
31
|
+
if (value === undefined) return undefined
|
|
32
|
+
if (value?.version !== 1 || !['codex', 'claude'].includes(value.cli) || value.id !== id
|
|
33
|
+
|| typeof value.parentId !== 'string' || typeof value.cwd !== 'string'
|
|
34
|
+
|| typeof value.model !== 'string' || typeof value.effort !== 'string'
|
|
35
|
+
|| typeof value.executable !== 'string' || !Array.isArray(value.delivered)) {
|
|
36
|
+
throw new Error(`Apollo CLI session ${id} has invalid stored state`)
|
|
37
|
+
}
|
|
38
|
+
return structuredClone(value)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async put(record) {
|
|
42
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(record.id)) throw new Error('Invalid Apollo CLI session id')
|
|
43
|
+
const detached = structuredClone(record)
|
|
44
|
+
await this.enqueue(async () => {
|
|
45
|
+
await this.unit.putRecord('sessions', detached.id, detached)
|
|
46
|
+
this.records.set(detached.id, detached)
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async delete(id) {
|
|
51
|
+
await this.enqueue(async () => {
|
|
52
|
+
await this.unit.deleteRecord('sessions', id)
|
|
53
|
+
this.records.delete(id)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
enqueue(write) {
|
|
58
|
+
if (this.closed) return Promise.reject(new Error('Apollo CLI session store is closed'))
|
|
59
|
+
const pending = this.writes.then(write)
|
|
60
|
+
this.writes = pending.catch(() => {})
|
|
61
|
+
return pending
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async close() {
|
|
65
|
+
this.closed = true
|
|
66
|
+
await this.writes
|
|
67
|
+
await this.unit.close()
|
|
68
|
+
}
|
|
69
|
+
}
|