Merge branch 'stack/agent-profiles-3-wire' into stack/agent-profiles-5-web-ui

This commit is contained in:
Yichen Jiang
2026-08-08 22:51:17 +08:00
910 changed files with 17011 additions and 6270 deletions

View File

@@ -21,12 +21,16 @@ function exitCode(argv: string[]): number {
afterEach(() => { vi.restoreAllMocks() })
describe('parseDshArgs', () => {
it('routes profile boots, one-shot tasks, and the web alias', () => {
it('routes profile boots, one-shot runs, and the web alias', () => {
expect(parse(['--profile', 'tui'])).toEqual({ mode: 'profile', profile: 'tui', patches: [] })
expect(parse(['--profile', 'headless', 'run', 'the', 'tests']))
.toEqual({ mode: 'profile', profile: 'headless', patches: [], task: 'run the tests' })
expect(parse(['--profile', 'tui', '--patch', 'a.yml', '--patch', 'b.yml']))
.toEqual({ mode: 'profile', profile: 'tui', patches: ['a.yml', 'b.yml'] })
expect(parse(['run', 'run', 'the', 'tests']))
.toEqual({ mode: 'run', profile: 'headless', patches: [], task: 'run the tests' })
expect(parse(['run', '--profile', 'custom', '--patch', 'a.yml', '--patch', 'b.yml', 'run', 'the', 'tests']))
.toEqual({ mode: 'run', profile: 'custom', patches: ['a.yml', 'b.yml'], task: 'run the tests' })
expect(parse(['run', '--', '--profile', 'is', 'task', 'text']))
.toEqual({ mode: 'run', profile: 'headless', patches: [], task: '--profile is task text' })
expect(parse(['web'])).toEqual({ mode: 'web', dev: false, patches: [] })
expect(parse(['web', '--patch', 'web.yml'])).toEqual({ mode: 'web', dev: false, patches: ['web.yml'] })
expect(parse(['web', '--host', '0.0.0.0', '--port', '8080', '--dev', '--workspace-root', '/w']))
@@ -65,6 +69,13 @@ describe('parseDshArgs', () => {
expect(exitCode(['tui'])).toBe(1) // a bare word is a task without --profile
expect(exitCode(['--config', 'c.yml'])).toBe(1) // removed
expect(exitCode(['-p', 'task'])).toBe(1) // removed
expect(exitCode(['--profile', 'headless', 'task'])).toBe(1) // tasks belong to `run`
expect(exitCode(['run'])).toBe(1)
expect(exitCode(['run', ''])).toBe(1)
expect(exitCode(['run', '--profile', '', 'task'])).toBe(1)
expect(exitCode(['run', '--patch=', 'task'])).toBe(1)
expect(exitCode(['--profile', 'headless', 'run', 'task'])).toBe(1)
expect(exitCode(['--patch', 'parent.yml', 'run', 'task'])).toBe(1)
expect(exitCode(['--profile', ''])).toBe(1)
expect(exitCode(['--profile', 'x', '--patch='])).toBe(1)
expect(exitCode(['--dump-config'])).toBe(1)
@@ -90,6 +101,7 @@ describe('parseDshArgs', () => {
it('exits 0 for help and version', () => {
expect(exitCode(['--help'])).toBe(0)
expect(exitCode(['run', '--help'])).toBe(0)
expect(exitCode(['--version'])).toBe(0)
})
})

View File

@@ -182,14 +182,56 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
const help = await runBuiltBin(['--help'])
expect(help.code).toBe(0)
expect(help.stdout).toContain('dsh --profile web')
expect(help.stdout).toContain('dsh run "run the tests"')
expect(help.stdout).toContain('dsh plugin --profile')
expect(help.stdout).not.toMatch(/^\s+(?:tui|meta|upgrade)\b/mu)
for (const removed of [['tui'], ['--config', 'x.yml'], ['-p', 'task']]) {
for (const removed of [['tui'], ['--config', 'x.yml'], ['-p', 'task'], ['--profile', 'headless', 'task']]) {
const result = await runBuiltBin(removed)
expect(result.code).toBe(1)
}
}, 30_000)
it('prints run help without initializing the selected profile', async () => {
const parent = mkdtempSync(join(tmpdir(), 'dsh-run-help-'))
const home = join(parent, 'not-created')
try {
const result = await runBuiltBin(['run', '--help'], { DSH_HOME: home })
expect(result.code).toBe(0)
expect(result.stderr).toBe('')
expect(result.stdout).toContain('Usage: dsh run [options] <task...>')
expect(existsSync(home)).toBe(false)
} finally {
rmSync(parent, { recursive: true, force: true })
}
})
it('runs the default headless profile through the published run command', async () => {
const apiKey = 'built-dsh-run-key'
const server = await startMockLlmServer({
sequence: ['success'],
apiKey,
successText: 'published dsh run reached the mock',
})
const home = mkdtempSync(join(tmpdir(), 'dsh-built-run-'))
try {
const result = await runBuiltBin(['run', 'answer', 'from', 'the', 'published', 'entry'], {
DSH_HOME: home,
DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: apiKey,
DEEPSEEK_BASE_URL: server.baseURL,
})
expect(result.code, result.stderr).toBe(0)
expect(result.stdout).toBe('published dsh run reached the mock')
expect(result.stderr).toMatch(/^dsh: observing at http:\/\/127\.0\.0\.1:\d+$/u)
expect(server.requests.length).toBeGreaterThan(0)
expect(server.requests.every(request => request.path === '/chat/completions')).toBe(true)
expect(JSON.stringify(server.requests.map(request => request.body))).toContain('answer from the published entry')
} finally {
await server.close()
rmSync(home, { recursive: true, force: true })
}
}, 30_000)
it('does not load a project environment for --version', async () => {
const project = mkdtempSync(join(tmpdir(), 'dsh-version-project-'))
writeFileSync(join(project, '.env'), 'PATH=/project-only-path\n')

View File

@@ -0,0 +1,176 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
const binScript = fileURLToPath(new URL('./fixtures/dsh-badge/snapshot.ts', import.meta.url))
const configPath = fileURLToPath(new URL('./fixtures/dsh-badge/cordis.yml', import.meta.url))
const defaultConfigPath = fileURLToPath(new URL('./fixtures/dsh-badge/default.cordis.yml', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const badgeAssetsPath = fileURLToPath(new URL('../../../packages/skill/skill-badge/assets/', import.meta.url))
describe('dsh badge assembled snapshot', () => {
it('advertises and loads the opt-in bundled skill through the shipped app', async () => {
const disabled = await runLoaderSmoke({
label: 'disabled dsh badge skill snapshot',
tempDirPrefix: 'headless-snapshot-dsh-badge-disabled-',
binScript,
libBinScript: binScript,
configPath: defaultConfigPath,
tsconfigPath,
})
const enabled = await runLoaderSmoke({
label: 'dsh badge skill snapshot',
tempDirPrefix: 'headless-snapshot-dsh-badge-',
binScript,
libBinScript: binScript,
configPath,
tsconfigPath,
})
const disabledSnapshot = JSON.parse(disabled.stdout) as unknown
const enabledSnapshot = JSON.parse(
enabled.stdout.replaceAll(badgeAssetsPath, '{{badgeAssetsPath}}'),
) as unknown
expect(disabled.stderr).toBe('')
expect(enabled.stderr).toBe('')
expect(disabledSnapshot).toMatchInlineSnapshot(`
{
"catalog": null,
"result": {
"content": [
{
"text": "Error: skill "dsh-badge" is unknown or no longer available",
"type": "text",
},
],
"error": {
"message": "skill "dsh-badge" is unknown or no longer available",
},
"isError": true,
},
"summary": null,
}
`)
expect(enabledSnapshot).toMatchInlineSnapshot(`
{
"catalog": [
{
"text": "<system-reminder>
A skill is a reusable set of task-specific instructions. The following skills are available in this session:
<available_skills>
- \`dsh-badge\`: Add the official “powered by dsh” badge to documents, pull requests, merge requests, and other content produced with DeepSeek Harness. Use whenever creating a pull request or merge request. Also use when the user asks for a dsh badge, powered-by-dsh attribution, or a reusable dsh badge asset or snippet.
</available_skills>
If the user names a skill, or the task clearly matches a skill's description, call the \`skill\` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.
A user may also invoke a skill directly; its <skill_content> block then appears in this conversation. Follow it, and do not call the \`skill\` tool again for that skill.
</system-reminder>",
"type": "text",
},
],
"result": {
"content": [
{
"text": "<skill_content name="dsh-badge">
<skill_resources>
Base directory for this skill: {{badgeAssetsPath}}
Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.
</skill_resources>
<skill_instructions>
# dsh Badge
Add the official “powered by dsh” badge without recreating or restyling it.
## Assets
- Local PNG: [\`dsh-badge.png\`](dsh-badge.png), 726×120 source image; render at 121×20
- Shields.io image URL: \`https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white\`
- Project URL: \`https://github.com/deepseek-ai/deepseek-harness-sdk\`
## Markdown
Use this linked badge in Markdown:
\`\`\`markdown
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
\`\`\`
If attribution should not be linked, use:
\`\`\`markdown
![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)
\`\`\`
## Usage rules
- For GitHub or GitLab Markdown, use the Shields.io URL and link it to the project URL unless the user asks for an unlinked image.
- For Feishu and other systems that import remote images unreliably, upload \`dsh-badge.png\` from this skill directory instead of generating another badge.
- Preserve the badge's 121×20 dimensions and aspect ratio.
- Place the badge at the end of the attributed document or section unless the user specifies another position.
- Do not substitute another color, logo, label, or project URL.
</skill_instructions>
</skill_content>",
"type": "text",
},
],
"isError": false,
"value": {
"content": "# dsh Badge
Add the official “powered by dsh” badge without recreating or restyling it.
## Assets
- Local PNG: [\`dsh-badge.png\`](dsh-badge.png), 726×120 source image; render at 121×20
- Shields.io image URL: \`https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white\`
- Project URL: \`https://github.com/deepseek-ai/deepseek-harness-sdk\`
## Markdown
Use this linked badge in Markdown:
\`\`\`markdown
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
\`\`\`
If attribution should not be linked, use:
\`\`\`markdown
![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)
\`\`\`
## Usage rules
- For GitHub or GitLab Markdown, use the Shields.io URL and link it to the project URL unless the user asks for an unlinked image.
- For Feishu and other systems that import remote images unreliably, upload \`dsh-badge.png\` from this skill directory instead of generating another badge.
- Preserve the badge's 121×20 dimensions and aspect ratio.
- Place the badge at the end of the attributed document or section unless the user specifies another position.
- Do not substitute another color, logo, label, or project URL.
",
"name": "dsh-badge",
"provider": "dsh-badge",
"resourceBase": {
"kind": "directory",
"path": "{{badgeAssetsPath}}",
},
},
},
"summary": {
"description": "Add the official “powered by dsh” badge to documents, pull requests, merge requests, and other content produced with DeepSeek Harness. Use whenever creating a pull request or merge request. Also use when the user asks for a dsh badge, powered-by-dsh attribution, or a reusable dsh badge asset or snippet.",
"invocation": {
"modelInvocable": true,
"userInvocable": true,
},
"name": "dsh-badge",
"provider": "dsh-badge",
"resourceBase": {
"kind": "directory",
"path": "{{badgeAssetsPath}}",
},
"source": "bundled",
},
}
`)
}, LOADER_SMOKE_TEST_TIMEOUT_MS * 2)
})

View File

@@ -0,0 +1,9 @@
- id: skill-badge
disabled: false
- id: skill-local
config:
watch: false
- id: telemetry-otel
disabled: true

View File

@@ -0,0 +1,6 @@
- id: skill-local
config:
watch: false
- id: telemetry-otel
disabled: true

View File

@@ -0,0 +1,56 @@
import { fileURLToPath } from 'node:url'
import { Context } from 'cordis'
import { agentEvents, Inbox, type Agent } from '@deepseek-ai/dsh-agent'
import { CallId } from '@deepseek-ai/dsh-llm'
import { boot, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
import { SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-skill'
import type {} from '@deepseek-ai/dsh-tools'
const overlayPath = process.argv[2]
if (overlayPath === undefined) throw new Error('dsh-badge snapshot requires an overlay path')
const rootConfigPath = fileURLToPath(new URL('../../../../../packages/bundle/base/tests/fixtures/root.cordis.yml', import.meta.url))
const basePatchPath = fileURLToPath(new URL('../../../../../packages/bundle/base/cordis.patch.yml', import.meta.url))
const ctx = await boot('dsh-badge-snapshot', rootConfigPath, [
...loadOverlayPatches('dsh-badge-snapshot', basePatchPath),
...loadOverlayPatches('dsh-badge-snapshot', overlayPath),
])
try {
const agentId = SessionId('dsh-badge-snapshot')
const session = ctx.sessions.create(agentId, { meta: { cwd: process.cwd() } })
const agent: Agent = {
ctx: new Context(),
id: agentId,
options: {},
session,
inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
status: 'idle',
send: () => {},
followup: () => {},
steer: () => {},
inject: () => { throw new Error('dsh-badge snapshot must receive the catalog at the step boundary') },
cancel: () => {},
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
const decision = await agentEvents(ctx, agent).waterfall(
'agent/pre-step',
{ messages: [], turn: 1, step: 1, signal: new AbortController().signal },
() => Promise.resolve({ kind: 'enter' as const, messages: [] }),
)
const catalog = decision.kind === 'enter'
? decision.messages.find(message => message.role === 'user'
&& message.source.kind === 'skill-catalog')?.content
: undefined
const summary = (await ctx.skills.list()).find(skill => skill.name === 'dsh-badge')
const result = await ctx.tools.execute({
callId: CallId('dsh-badge-snapshot'),
name: 'skill',
arguments: { name: 'dsh-badge' },
signal: new AbortController().signal,
})
process.stdout.write(`${JSON.stringify({ catalog: catalog ?? null, summary: summary ?? null, result })}\n`)
} finally {
await ctx.fiber.dispose()
}

View File

@@ -66,7 +66,7 @@ async function runHeadlessPtySmoke(): Promise<string> {
try {
const home = join(cwd, '.dsh')
// Pre-initialize the headless profile with the never-dispose row in its
// user patch layer (the same file `dsh --profile headless` hot-reloads).
// user patch layer (the same file a long-lived profile boot hot-reloads).
const profileDir = join(home, 'profiles', 'headless')
await mkdir(profileDir, { recursive: true })
await writeFile(join(profileDir, 'package.json'), JSON.stringify({
@@ -83,7 +83,7 @@ async function runHeadlessPtySmoke(): Promise<string> {
].join('\n'))
const launch = resolveExampleLaunch({
srcBin: dshBinScript,
configArgs: ['--profile', 'headless', 'never complete'],
configArgs: ['run', 'never complete'],
tsconfigPath,
env: {
DSH_HOME: home,