pstack-dsh 0.2.3

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.
Files changed (172) hide show
  1. package/HARNESS.md +109 -0
  2. package/LICENSE +25 -0
  3. package/README.md +168 -0
  4. package/TEST-PLAN.md +70 -0
  5. package/agents/comment-sicko.md +32 -0
  6. package/agents/poteto-agent.md +9 -0
  7. package/cordis.patch.yml +4 -0
  8. package/cordis.yml +4 -0
  9. package/docs/guide/01-setup.md +41 -0
  10. package/docs/guide/02-poteto-mode.md +98 -0
  11. package/docs/guide/03-understand.md +53 -0
  12. package/docs/guide/04-design.md +81 -0
  13. package/docs/guide/05-build-and-clean.md +75 -0
  14. package/docs/guide/06-verify-and-ship.md +87 -0
  15. package/docs/guide/07-overnight.md +81 -0
  16. package/docs/guide/08-principles.md +71 -0
  17. package/docs/guide/09-make-it-yours.md +67 -0
  18. package/docs/guide/10-recipes-and-pitfalls.md +96 -0
  19. package/docs/guide/README.md +30 -0
  20. package/dshx.yml +9 -0
  21. package/lib/client.js +981 -0
  22. package/lib/client.js.map +1 -0
  23. package/lib/index.d.ts +34 -0
  24. package/lib/index.js +1120 -0
  25. package/package.json +140 -0
  26. package/skills/architect/SKILL.md +83 -0
  27. package/skills/architect/references/design-red-flags.md +33 -0
  28. package/skills/architect/references/rationale-template.md +35 -0
  29. package/skills/architect/references/runner-prompt.md +20 -0
  30. package/skills/arena/SKILL.md +73 -0
  31. package/skills/automate-me/SKILL.md +103 -0
  32. package/skills/blast-radius/SKILL.md +50 -0
  33. package/skills/bro/SKILL.md +7 -0
  34. package/skills/create-verification-skill/SKILL.md +44 -0
  35. package/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
  36. package/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
  37. package/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
  38. package/skills/figure-it-out/SKILL.md +53 -0
  39. package/skills/how/SKILL.md +45 -0
  40. package/skills/how/references/explainer-prompt.md +55 -0
  41. package/skills/how/references/explorer-prompt.md +52 -0
  42. package/skills/interrogate/SKILL.md +99 -0
  43. package/skills/interrogate/references/code-quality-review.md +47 -0
  44. package/skills/interrogate/references/lead-judgment.md +58 -0
  45. package/skills/interrogate/references/reviewer-prompt.md +72 -0
  46. package/skills/interrogate/references/rubric.md +77 -0
  47. package/skills/maintain-verification-skill/SKILL.md +39 -0
  48. package/skills/no-comments/SKILL.md +24 -0
  49. package/skills/poteto-mode/SKILL.md +149 -0
  50. package/skills/poteto-mode/playbooks/authoring-a-skill.md +12 -0
  51. package/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
  52. package/skills/poteto-mode/playbooks/autopilot-full.md +13 -0
  53. package/skills/poteto-mode/playbooks/autopilot-stack.md +16 -0
  54. package/skills/poteto-mode/playbooks/babysit.md +27 -0
  55. package/skills/poteto-mode/playbooks/bug-fix.md +17 -0
  56. package/skills/poteto-mode/playbooks/eval.md +25 -0
  57. package/skills/poteto-mode/playbooks/feature.md +21 -0
  58. package/skills/poteto-mode/playbooks/hillclimb.md +21 -0
  59. package/skills/poteto-mode/playbooks/investigation.md +14 -0
  60. package/skills/poteto-mode/playbooks/multi-phase-plan.md +156 -0
  61. package/skills/poteto-mode/playbooks/opening-a-pr.md +33 -0
  62. package/skills/poteto-mode/playbooks/orchestrate.md +113 -0
  63. package/skills/poteto-mode/playbooks/pause-safely.md +10 -0
  64. package/skills/poteto-mode/playbooks/perf-issue.md +24 -0
  65. package/skills/poteto-mode/playbooks/prototype.md +14 -0
  66. package/skills/poteto-mode/playbooks/refactoring.md +16 -0
  67. package/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
  68. package/skills/poteto-mode/playbooks/session-pickup.md +11 -0
  69. package/skills/poteto-mode/playbooks/shipping.md +17 -0
  70. package/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
  71. package/skills/poteto-mode/playbooks/visual-parity.md +11 -0
  72. package/skills/poteto-mode/playbooks/worktree-cleanup.md +14 -0
  73. package/skills/poteto-mode/references/bugbot-triage.md +142 -0
  74. package/skills/poteto-mode/scripts/bootstrap.ts +62 -0
  75. package/skills/poteto-mode/scripts/bun.lock +67 -0
  76. package/skills/poteto-mode/scripts/check-plan.mjs +186 -0
  77. package/skills/poteto-mode/scripts/orch/orch.test.ts +634 -0
  78. package/skills/poteto-mode/scripts/orch/orch.ts +578 -0
  79. package/skills/poteto-mode/scripts/orch/store.ts +1607 -0
  80. package/skills/poteto-mode/scripts/package.json +16 -0
  81. package/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
  82. package/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
  83. package/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +118 -0
  84. package/skills/poteto-mode/scripts/watch-pr/github.test.ts +306 -0
  85. package/skills/poteto-mode/scripts/watch-pr/github.ts +699 -0
  86. package/skills/poteto-mode/scripts/watch-pr/policy.test.ts +420 -0
  87. package/skills/poteto-mode/scripts/watch-pr/policy.ts +832 -0
  88. package/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
  89. package/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
  90. package/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
  91. package/skills/poteto-mode/scripts/watch-pr/types.ts +401 -0
  92. package/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
  93. package/skills/poteto-mode/scripts/worktree-audit.sh +86 -0
  94. package/skills/principle-attack-the-premise/SKILL.md +22 -0
  95. package/skills/principle-boundary-discipline/SKILL.md +33 -0
  96. package/skills/principle-build-the-lever/SKILL.md +22 -0
  97. package/skills/principle-encode-lessons-in-structure/SKILL.md +30 -0
  98. package/skills/principle-exhaust-the-design-space/SKILL.md +20 -0
  99. package/skills/principle-experience-first/SKILL.md +18 -0
  100. package/skills/principle-fix-root-causes/SKILL.md +22 -0
  101. package/skills/principle-foundational-thinking/SKILL.md +20 -0
  102. package/skills/principle-guard-the-context-window/SKILL.md +16 -0
  103. package/skills/principle-laziness-protocol/SKILL.md +17 -0
  104. package/skills/principle-make-operations-idempotent/SKILL.md +23 -0
  105. package/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +21 -0
  106. package/skills/principle-minimize-reader-load/SKILL.md +22 -0
  107. package/skills/principle-model-the-domain/SKILL.md +25 -0
  108. package/skills/principle-never-block-on-the-human/SKILL.md +21 -0
  109. package/skills/principle-outcome-oriented-execution/SKILL.md +21 -0
  110. package/skills/principle-prove-it-works/SKILL.md +32 -0
  111. package/skills/principle-redesign-from-first-principles/SKILL.md +15 -0
  112. package/skills/principle-separate-before-serializing-shared-state/SKILL.md +15 -0
  113. package/skills/principle-sequence-verifiable-units/SKILL.md +21 -0
  114. package/skills/principle-subtract-before-you-add/SKILL.md +20 -0
  115. package/skills/principle-test-behavior-not-implementation/SKILL.md +24 -0
  116. package/skills/principle-type-system-discipline/SKILL.md +30 -0
  117. package/skills/recall/SKILL.md +35 -0
  118. package/skills/reflect/SKILL.md +65 -0
  119. package/skills/reflect/references/divergent-reviewer.md +43 -0
  120. package/skills/reflect/references/judgment-reviewer.md +42 -0
  121. package/skills/reflect/references/synthesizer.md +56 -0
  122. package/skills/reflect/references/tooling-reviewer.md +55 -0
  123. package/skills/setup-pstack/SKILL.md +32 -0
  124. package/skills/setup-pstack/references/catalog.md +28 -0
  125. package/skills/setup-pstack/references/spawn.md +61 -0
  126. package/skills/show-me-your-work/SKILL.md +82 -0
  127. package/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
  128. package/skills/show-me-your-work/scripts/log.sh +40 -0
  129. package/skills/swarm/SKILL.md +44 -0
  130. package/skills/tdd/SKILL.md +44 -0
  131. package/skills/teach/SKILL.md +21 -0
  132. package/skills/technical-writing/SKILL.md +127 -0
  133. package/skills/typescript-best-practices/SKILL.md +30 -0
  134. package/skills/typescript-best-practices/references/patterns.md +313 -0
  135. package/skills/unslop/SKILL.md +67 -0
  136. package/skills/why/SKILL.md +146 -0
  137. package/skills/why/references/epistemics.md +144 -0
  138. package/skills/why/references/investigator-prompt.md +103 -0
  139. package/skills/why/references/source-playbook.md +17 -0
  140. package/skills/why/references/sources/code-archaeology.md +88 -0
  141. package/skills/why/references/sources/databricks.md +70 -0
  142. package/skills/why/references/sources/datadog.md +99 -0
  143. package/skills/why/references/sources/incident-postmortem.md +15 -0
  144. package/skills/why/references/sources/linear.md +48 -0
  145. package/skills/why/references/sources/notion.md +55 -0
  146. package/skills/why/references/sources/sentry.md +100 -0
  147. package/skills/why/references/sources/slack.md +54 -0
  148. package/skills/why/references/synthesizer-prompt.md +135 -0
  149. package/src/catalog-types.ts +54 -0
  150. package/src/catalog.ts +308 -0
  151. package/src/client/PstackSettings.tsx +519 -0
  152. package/src/client/api.ts +56 -0
  153. package/src/client/index.tsx +38 -0
  154. package/src/client/locales.ts +65 -0
  155. package/src/client/poteto-defaults.ts +37 -0
  156. package/src/frontmatter.ts +73 -0
  157. package/src/home.ts +13 -0
  158. package/src/http-trust.ts +38 -0
  159. package/src/ids.ts +39 -0
  160. package/src/index.ts +85 -0
  161. package/src/overlay-model.ts +139 -0
  162. package/src/overlay.ts +40 -0
  163. package/src/persona.ts +38 -0
  164. package/src/plugin-config.ts +29 -0
  165. package/src/request-overlay.ts +40 -0
  166. package/src/roles.ts +55 -0
  167. package/src/settings-api.ts +48 -0
  168. package/src/settings-draft.ts +144 -0
  169. package/src/settings-routes.ts +77 -0
  170. package/src/skills-provider.ts +96 -0
  171. package/src/spawn.ts +52 -0
  172. package/src/tools.ts +226 -0
@@ -0,0 +1,37 @@
1
+ import type { PstackRole } from '../roles.ts'
2
+
3
+ // Upstream 0.15 recommendations are display copy, never runtime route defaults.
4
+ const GROK = 'grok-4.6-fast-xhigh'
5
+ const SOL = 'gpt-5.6-sol-max'
6
+ const FABLE = 'claude-fable-5-1-thinking-max'
7
+ const PANEL = `${FABLE}, ${SOL}, ${GROK}, claude-opus-5-thinking-xhigh`
8
+
9
+ export const POTETO_DEFAULT_SLUGS = {
10
+ feature: GROK,
11
+ refactoring: GROK,
12
+ 'bug-fix': FABLE,
13
+ 'perf-issue': FABLE,
14
+ hillclimb: FABLE,
15
+ 'judgment-and-prose': FABLE,
16
+ 'hardest-tasks': FABLE,
17
+ 'how-explorer': GROK,
18
+ 'how-explainer': FABLE,
19
+ 'why-investigators': GROK,
20
+ 'why-synthesizer': FABLE,
21
+ 'reflect-tooling': SOL,
22
+ 'reflect-judgment': FABLE,
23
+ 'swarm-workers': GROK,
24
+ 'independent-verifier': '',
25
+ 'poteto-agent': '',
26
+ 'comment-sicko': '',
27
+ 'how-critics': '',
28
+ 'arena-runners': PANEL,
29
+ 'arena-cross-judge-pool': PANEL,
30
+ 'architect-runners': PANEL,
31
+ 'interrogate-reviewers': PANEL,
32
+ } as const satisfies Record<PstackRole, string>
33
+
34
+ export function potetoNoteCopy(role: PstackRole, prefix: string): string {
35
+ const slugs = POTETO_DEFAULT_SLUGS[role]
36
+ return slugs.length === 0 ? '' : `${prefix}${slugs}`
37
+ }
@@ -0,0 +1,73 @@
1
+ const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
2
+
3
+ export interface SkillFrontmatter {
4
+ name: string
5
+ description: string
6
+ whenToUse?: string
7
+ modelInvocable: boolean
8
+ userInvocable: boolean
9
+ }
10
+
11
+ export interface ParsedSkill {
12
+ frontmatter: SkillFrontmatter
13
+ content: string
14
+ }
15
+
16
+ function unquote(value: string): string {
17
+ const trimmed = value.trim()
18
+ if (
19
+ (trimmed.startsWith('"') && trimmed.endsWith('"'))
20
+ || (trimmed.startsWith("'") && trimmed.endsWith("'"))
21
+ ) {
22
+ return trimmed.slice(1, -1)
23
+ }
24
+ return trimmed
25
+ }
26
+
27
+ function parseBool(value: string | undefined, fallback: boolean): boolean {
28
+ if (value === undefined) return fallback
29
+ const normalized = unquote(value).trim().toLowerCase()
30
+ if (['true', 'yes', 'on', '1'].includes(normalized)) return true
31
+ if (['false', 'no', 'off', '0'].includes(normalized)) return false
32
+ throw new Error(`invalid boolean "${value}"`)
33
+ }
34
+
35
+ /**
36
+ * Minimal YAML frontmatter reader for SKILL.md.
37
+ * Official keys: name, description, whenToUse, disable-model-invocation, user-invocable.
38
+ * `packages/skill/skill-filesystem/README.md`.
39
+ */
40
+ export function parseSkillMarkdown(text: string, fallbackName: string): ParsedSkill | undefined {
41
+ if (!text.startsWith('---')) return undefined
42
+ const end = text.indexOf('\n---', 3)
43
+ if (end < 0) return undefined
44
+ const raw = text.slice(4, end)
45
+ const body = text.slice(end + 4).replace(/^\s*\n/, '')
46
+ const fields: Record<string, string> = {}
47
+ for (const line of raw.split('\n')) {
48
+ const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line)
49
+ if (!match) continue
50
+ fields[match[1]!] = match[2] ?? ''
51
+ }
52
+ let name = unquote(fields.name ?? fallbackName)
53
+ if (name === 'Poteto Mode') name = 'poteto-mode'
54
+ if (!NAME_RE.test(name)) return undefined
55
+ const description = unquote(fields.description ?? '')
56
+ if (description.length === 0) return undefined
57
+ try {
58
+ const disableModel = parseBool(fields['disable-model-invocation'], false)
59
+ const userInvocable = parseBool(fields['user-invocable'], true)
60
+ return {
61
+ frontmatter: {
62
+ name,
63
+ description,
64
+ ...fields.whenToUse ? { whenToUse: unquote(fields.whenToUse) } : {},
65
+ modelInvocable: !disableModel,
66
+ userInvocable,
67
+ },
68
+ content: body.trimStart(),
69
+ }
70
+ } catch {
71
+ return undefined
72
+ }
73
+ }
package/src/home.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { homedir } from 'node:os'
2
+ import { join, resolve } from 'node:path'
3
+
4
+ /** Official precedence: `$DSH_HOME`, then `~/.dsh`. `packages/util/home-paths/README.md`. */
5
+ export function resolveDshHome(env: NodeJS.ProcessEnv = process.env): string {
6
+ const fromEnv = env.DSH_HOME?.trim()
7
+ if (fromEnv) return resolve(fromEnv)
8
+ return join(homedir(), '.dsh')
9
+ }
10
+
11
+ export function dshHomeDisplay(env: NodeJS.ProcessEnv = process.env): string {
12
+ return env.DSH_HOME?.trim() ? '$DSH_HOME' : '~/.dsh'
13
+ }
@@ -0,0 +1,38 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http'
2
+
3
+ /** Loopback + same-origin. Copied shape from dsh-oauth-login `src/auth-routes.ts`. */
4
+ export function trustedRequest(req: IncomingMessage): boolean {
5
+ const remote = req.socket.remoteAddress
6
+ if (remote !== '127.0.0.1' && remote !== '::1' && remote !== '::ffff:127.0.0.1') return false
7
+ if (req.headers['sec-fetch-site'] === 'cross-site') return false
8
+ const host = req.headers.host
9
+ if (host === undefined) return false
10
+ const origin = req.headers.origin
11
+ if (origin === undefined) return true
12
+ try {
13
+ return new URL(origin).host === new URL(`http://${host}`).host
14
+ } catch {
15
+ return false
16
+ }
17
+ }
18
+
19
+ export function json(res: ServerResponse, status: number, value: unknown): void {
20
+ res.writeHead(status, {
21
+ 'content-type': 'application/json; charset=utf-8',
22
+ 'cache-control': 'no-store',
23
+ 'x-content-type-options': 'nosniff',
24
+ })
25
+ res.end(JSON.stringify(value))
26
+ }
27
+
28
+ export async function readJson(req: IncomingMessage, maxBytes = 262144): Promise<unknown> {
29
+ const chunks: Buffer[] = []
30
+ let size = 0
31
+ for await (const chunk of req) {
32
+ size += chunk.length
33
+ if (size > maxBytes) throw new Error('request body too large')
34
+ chunks.push(chunk as Buffer)
35
+ }
36
+ if (chunks.length === 0) return {}
37
+ return JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown
38
+ }
package/src/ids.ts ADDED
@@ -0,0 +1,39 @@
1
+ /** Startup line `dshx verify-boot` looks for. */
2
+ export const BOOT_MARKER = '[my-plugins/pstack-dsh] loaded'
3
+
4
+ /** Cordis plugin / Loader row id. */
5
+ export const PLUGIN_ID = 'pstack-dsh'
6
+
7
+ /** Bundled skill provider name on `ctx.skills`. */
8
+ export const SKILL_PROVIDER_NAME = 'pstack-dsh'
9
+
10
+ /** Overlay file under `$DSH_HOME`. */
11
+ export const OVERLAY_FILENAME = 'pstack-dsh.json'
12
+
13
+ /** DSH-owned OAuth store from dsh-oauth-login. Never ~/.pi / ~/.codex / ~/.claude / grok CLI. */
14
+ export const OAUTH_AUTH_FILENAME = '.dsh-oauth-auth.json'
15
+ export const OAUTH_AUTH_LEGACY_FILENAME = '.pi-login-auth.json'
16
+
17
+ /** Antigravity OAuth store from dsh-antigravity-oauth. */
18
+ export const ANTIGRAVITY_AUTH_FILENAME = '.dsh-antigravity-oauth.json'
19
+ export const ANTIGRAVITY_PROVIDER = 'agy-google-antigravity'
20
+
21
+ /** Official DeepSeek adapter. `packages/llm/llm-deepseek/src/index.ts` `PROVIDER`, `DEFAULT_API_KEY_ENV`. */
22
+ export const DEEPSEEK_API_KEY_ENV = 'DEEPSEEK_API_KEY'
23
+ export const DEEPSEEK_PROVIDER = 'deepseek-official'
24
+
25
+ /** Model-facing tool names this plugin registers. */
26
+ export const TOOL_CATALOG = 'pstack_catalog'
27
+ export const TOOL_SPAWN = 'pstack_spawn'
28
+ export const TOOL_OVERLAY_READ = 'pstack_overlay_read'
29
+ export const TOOL_OVERLAY_WRITE = 'pstack_overlay_write'
30
+
31
+ /** Default in-process spawn provider. `packages/bundle/base/cordis.patch.yml` `providerName: spawn`. */
32
+ export const SPAWN_PROVIDER = 'spawn'
33
+
34
+ /** Settings nav id. Official slot `settings.section` (`packages/client/ui-settings/src/client/contract/slots.ts`). */
35
+ export const SETTINGS_SECTION_ID = 'pstack'
36
+
37
+ /** Same-origin Web routes. Pattern: dsh-oauth-login `src/auth-routes.ts`. */
38
+ export const SETTINGS_SNAPSHOT_PATH = '/plugins/pstack-dsh/settings'
39
+ export const SETTINGS_SAVE_PATH = '/plugins/pstack-dsh/settings'
package/src/index.ts ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * pstack-dsh: DeepSeek Harness port of official pstack.
3
+ * Function/namespace plugin: named apply, no default export.
4
+ */
5
+
6
+ import type { Context } from '@deepseek-ai/cordis'
7
+ import type {} from '@deepseek-ai/dsh-agent'
8
+ import type {} from '@deepseek-ai/dsh-host-webserver'
9
+ import { ReasoningEffortId } from '@deepseek-ai/dsh-llm'
10
+ import type {} from '@deepseek-ai/dsh-skill'
11
+ import { defineTool } from '@deepseek-ai/dsh-tools'
12
+ import { snapshotJsonValue, type JsonValue } from '@deepseek-ai/dsh-util-values'
13
+ import { PLUGIN_ID, SPAWN_PROVIDER } from './ids.ts'
14
+ import { Config, type Config as PluginConfig } from './plugin-config.ts'
15
+ import { createSkillProvider } from './skills-provider.ts'
16
+ import { RoleEffortMap } from './request-overlay.ts'
17
+ import {
18
+ catalogTool,
19
+ overlayReadTool,
20
+ overlayWriteTool,
21
+ spawnTool,
22
+ type ToolHost,
23
+ } from './tools.ts'
24
+ import { registerPstackSettingsRoutes } from './settings-routes.ts'
25
+
26
+ export const name = PLUGIN_ID
27
+ export const inject = ['tools', 'skills']
28
+ export { Config }
29
+ export type { PluginConfig as ConfigType }
30
+
31
+ const JSON_OUTPUT = {
32
+ schema: { type: 'json' as const },
33
+ render: (_args: unknown, value: JsonValue) => [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }],
34
+ }
35
+
36
+ function asHost(ctx: Context, config: PluginConfig | undefined, roles: RoleEffortMap): ToolHost {
37
+ return {
38
+ get llm() { return ctx.get('llm') as ToolHost['llm'] },
39
+ get credentials() { return ctx.get('credentials') as ToolHost['credentials'] },
40
+ get settings() { return ctx.get('settings') as ToolHost['settings'] },
41
+ get subagents() { return ctx.get('subagents') as ToolHost['subagents'] },
42
+ spawnProvider: config?.spawnProvider ?? SPAWN_PROVIDER,
43
+ roles,
44
+ }
45
+ }
46
+
47
+ function registerOne(ctx: Context, tool: ReturnType<typeof catalogTool>): void {
48
+ ctx.tools.register(defineTool({
49
+ name: tool.name,
50
+ description: tool.description,
51
+ parameters: tool.parameters,
52
+ output: JSON_OUTPUT,
53
+ async execute(args: Record<string, unknown>, exec: { signal?: AbortSignal; agent?: { id: string } }): Promise<JsonValue> {
54
+ const snapshot = snapshotJsonValue(await tool.execute(args, exec))
55
+ if (snapshot === undefined) throw new TypeError(`pstack tool ${tool.name} returned a non-JSON value`)
56
+ return snapshot as JsonValue
57
+ },
58
+ }))
59
+ }
60
+
61
+ export function apply(ctx: Context, config?: PluginConfig): void {
62
+ console.log('[my-plugins/pstack-dsh] loaded')
63
+ const roles = new RoleEffortMap()
64
+ const host = asHost(ctx, config, roles)
65
+ registerOne(ctx, catalogTool(host))
66
+ registerOne(ctx, overlayReadTool(host))
67
+ registerOne(ctx, overlayWriteTool(host))
68
+ registerOne(ctx, spawnTool(host))
69
+ ctx.skills.registerProvider(() => createSkillProvider())
70
+ ctx.inject(['webServer', 'connection'], webCtx => {
71
+ registerPstackSettingsRoutes(webCtx, host)
72
+ })
73
+
74
+ ctx.on('agent/request', async (payload, next) => {
75
+ const base = await next()
76
+ const binding = roles.lookup(payload.agent.id)
77
+ if (binding === undefined) return base
78
+ return {
79
+ ...base,
80
+ ...binding.provider ? { provider: binding.provider } : {},
81
+ ...binding.model ? { model: binding.model } : {},
82
+ ...binding.reasoningEffort ? { reasoningEffort: ReasoningEffortId(binding.reasoningEffort) } : {},
83
+ }
84
+ })
85
+ }
@@ -0,0 +1,139 @@
1
+ import { ALL_ROLES, isPanelRole, isPstackRole, type PstackRole } from './roles.ts'
2
+ import type { LiveRoute } from './catalog-types.ts'
3
+
4
+ export const OVERLAY_VERSION = 1 as const
5
+
6
+ export interface OverlayRoute {
7
+ readonly provider: string
8
+ readonly model: string
9
+ /** Adapter-owned effort id for this exact route. Omit when the route has no effort field. */
10
+ readonly reasoningEffort?: string
11
+ }
12
+
13
+ export interface OverlayRole {
14
+ /** Child inherits the parent conversation route and effort. */
15
+ readonly inherit: boolean
16
+ /** One spawn per entry. Empty when inherit is true. */
17
+ readonly routes: OverlayRoute[]
18
+ }
19
+
20
+ export interface Overlay {
21
+ readonly version: typeof OVERLAY_VERSION
22
+ readonly roles: Record<string, OverlayRole>
23
+ }
24
+
25
+ export function emptyOverlay(): Overlay {
26
+ const roles: Record<string, OverlayRole> = {}
27
+ for (const role of ALL_ROLES) {
28
+ roles[role] = { inherit: true, routes: [] }
29
+ }
30
+ return { version: OVERLAY_VERSION, roles }
31
+ }
32
+
33
+ export function routeKey(provider: string, model: string): string {
34
+ return `${provider}::${model}`
35
+ }
36
+
37
+ function isRecord(value: unknown): value is Record<string, unknown> {
38
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
39
+ }
40
+
41
+ function parseRoute(raw: unknown, path: string): OverlayRoute {
42
+ if (!isRecord(raw)) throw new Error(`${path} must be an object`)
43
+ const provider = raw.provider
44
+ const model = raw.model
45
+ if (typeof provider !== 'string' || provider.length === 0) {
46
+ throw new Error(`${path}.provider must be a non-empty string`)
47
+ }
48
+ if (typeof model !== 'string' || model.length === 0) {
49
+ throw new Error(`${path}.model must be a non-empty string`)
50
+ }
51
+ const effort = raw.reasoningEffort
52
+ if (effort !== undefined && (typeof effort !== 'string' || effort.length === 0)) {
53
+ throw new Error(`${path}.reasoningEffort must be a non-empty string when set`)
54
+ }
55
+ const extra = Object.keys(raw).filter(key => key !== 'provider' && key !== 'model' && key !== 'reasoningEffort')
56
+ if (extra.length > 0) throw new Error(`${path} has unknown fields: ${extra.join(', ')}`)
57
+ return effort === undefined
58
+ ? { provider, model }
59
+ : { provider, model, reasoningEffort: effort }
60
+ }
61
+
62
+ function parseRole(raw: unknown, role: string): OverlayRole {
63
+ if (!isRecord(raw)) throw new Error(`roles.${role} must be an object`)
64
+ const extra = Object.keys(raw).filter(key => key !== 'inherit' && key !== 'routes')
65
+ if (extra.length > 0) throw new Error(`roles.${role} has unknown fields: ${extra.join(', ')}`)
66
+ const inherit = raw.inherit === true
67
+ || raw.inherit === 'true'
68
+ || raw.inherit === 'inherit-parent'
69
+ || raw.inherit === 'auto'
70
+ const routesRaw = raw.routes
71
+ const routes = Array.isArray(routesRaw)
72
+ ? routesRaw.map((entry, index) => parseRoute(entry, `roles.${role}.routes[${index}]`))
73
+ : []
74
+ if (inherit) return { inherit: true, routes: [] }
75
+ if (routes.length === 0) return { inherit: true, routes: [] }
76
+ if (!isPanelRole(role) && routes.length !== 1) {
77
+ throw new Error(`roles.${role} is a scalar role and may have at most one route`)
78
+ }
79
+ return { inherit: false, routes }
80
+ }
81
+
82
+ export function parseOverlay(text: string): Overlay {
83
+ let value: unknown
84
+ try {
85
+ value = JSON.parse(text) as unknown
86
+ } catch {
87
+ throw new Error('pstack-dsh overlay is not valid JSON')
88
+ }
89
+ if (!isRecord(value)) throw new Error('pstack-dsh overlay must be an object')
90
+ if (value.version !== OVERLAY_VERSION) {
91
+ throw new Error(`pstack-dsh overlay version ${String(value.version)} is unsupported`)
92
+ }
93
+ const extra = Object.keys(value).filter(key => key !== 'version' && key !== 'roles')
94
+ if (extra.length > 0) throw new Error(`pstack-dsh overlay has unknown fields: ${extra.join(', ')}`)
95
+ const rolesRaw = value.roles
96
+ if (!isRecord(rolesRaw)) throw new Error('pstack-dsh overlay.roles must be an object')
97
+ const roles: Record<string, OverlayRole> = {}
98
+ for (const role of ALL_ROLES) {
99
+ const entry = rolesRaw[role]
100
+ roles[role] = entry === undefined ? { inherit: true, routes: [] } : parseRole(entry, role)
101
+ }
102
+ for (const key of Object.keys(rolesRaw)) {
103
+ if (!isPstackRole(key)) throw new Error(`unknown pstack role "${key}"`)
104
+ }
105
+ return { version: OVERLAY_VERSION, roles }
106
+ }
107
+
108
+ export function validateOverlayAgainstCatalog(overlay: Overlay, routes: readonly LiveRoute[]): string[] {
109
+ const errors: string[] = []
110
+ const live = new Map(routes.filter(route => route.selectable).map(route => [routeKey(route.provider, route.model), route]))
111
+ for (const role of ALL_ROLES) {
112
+ if (role === 'how-critics') continue
113
+ const assignment = overlay.roles[role] ?? { inherit: true, routes: [] }
114
+ if (assignment.inherit) continue
115
+ for (const [index, entry] of assignment.routes.entries()) {
116
+ const found = live.get(routeKey(entry.provider, entry.model))
117
+ if (found === undefined) {
118
+ errors.push(`${role}[${index}]: ${entry.provider}/${entry.model} is not a logged-in live route`)
119
+ continue
120
+ }
121
+ if (entry.reasoningEffort === undefined) continue
122
+ if (found.efforts.length === 0) {
123
+ errors.push(`${role}[${index}]: ${entry.provider}/${entry.model} has no effort field; omit reasoningEffort`)
124
+ continue
125
+ }
126
+ if (!found.efforts.some(effort => effort.id === entry.reasoningEffort)) {
127
+ errors.push(
128
+ `${role}[${index}]: effort "${entry.reasoningEffort}" is not accepted by ${entry.provider}/${entry.model} (live: ${found.efforts.map(effort => effort.id).join(', ')})`,
129
+ )
130
+ }
131
+ }
132
+ }
133
+ return errors
134
+ }
135
+
136
+ export function resolveRole(overlay: Overlay, role: string): OverlayRole {
137
+ const key = role as PstackRole
138
+ return overlay.roles[key] ?? { inherit: true, routes: [] }
139
+ }
package/src/overlay.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
4
+ import { OVERLAY_FILENAME } from './ids.ts'
5
+ import { resolveDshHome } from './home.ts'
6
+ import { emptyOverlay, parseOverlay, type Overlay } from './overlay-model.ts'
7
+
8
+ export {
9
+ OVERLAY_VERSION,
10
+ emptyOverlay,
11
+ parseOverlay,
12
+ routeKey,
13
+ resolveRole,
14
+ validateOverlayAgainstCatalog,
15
+ } from './overlay-model.ts'
16
+ export type { Overlay, OverlayRole, OverlayRoute } from './overlay-model.ts'
17
+
18
+ export function overlayPath(dshHome = resolveDshHome()): string {
19
+ return join(dshHome, OVERLAY_FILENAME)
20
+ }
21
+
22
+ export async function readOverlay(dshHome = resolveDshHome()): Promise<{ path: string; overlay: Overlay; missing: boolean }> {
23
+ const path = overlayPath(dshHome)
24
+ try {
25
+ const text = await readFile(path, 'utf8')
26
+ return { path, overlay: parseOverlay(text), missing: false }
27
+ } catch (error) {
28
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
29
+ return { path, overlay: emptyOverlay(), missing: true }
30
+ }
31
+ throw error
32
+ }
33
+ }
34
+
35
+ export async function writeOverlay(overlay: Overlay, dshHome = resolveDshHome()): Promise<string> {
36
+ const path = overlayPath(dshHome)
37
+ const body = `${JSON.stringify(overlay, null, 2)}\n`
38
+ await writeFileAtomic(path, body, { mode: 0o600, dirMode: 0o700 })
39
+ return path
40
+ }
package/src/persona.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { normalizeRole } from './roles.ts'
5
+
6
+ export function defaultAgentsRoot(): string {
7
+ return join(dirname(fileURLToPath(import.meta.url)), '..', 'agents')
8
+ }
9
+
10
+ function stripFrontmatter(text: string): string {
11
+ if (!text.startsWith('---')) return text
12
+ const end = text.indexOf('\n---', 3)
13
+ if (end < 0) return text
14
+ return text.slice(end + 4).replace(/^\s*\n/, '')
15
+ }
16
+
17
+ /**
18
+ * Persona for a pstack role. Official spawn persona shadows deployment persona
19
+ * (`SubagentStartRequest.persona`, `packages/subagent/subagent/src/types.ts`).
20
+ */
21
+ export async function loadPersona(role: string, agentsRoot = defaultAgentsRoot()): Promise<string | undefined> {
22
+ const key = normalizeRole(role)
23
+ const files = key === 'comment-sicko'
24
+ ? ['comment-sicko.md']
25
+ : key === 'poteto-agent'
26
+ ? ['poteto-agent.md']
27
+ : [`${key}.md`, 'poteto-agent.md']
28
+ for (const file of files) {
29
+ try {
30
+ const text = await readFile(join(agentsRoot, file), 'utf8')
31
+ const body = stripFrontmatter(text).trim()
32
+ if (body.length > 0) return body
33
+ } catch {
34
+ continue
35
+ }
36
+ }
37
+ return undefined
38
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Standard Schema v1 Config. Loader validates before apply.
3
+ * Avoids a hard schemastery import so the package still typechecks without DSH.
4
+ */
5
+
6
+ export interface Config {
7
+ /** In-process subagent provider name. Default `spawn`. */
8
+ spawnProvider?: string
9
+ }
10
+
11
+ export const Config = {
12
+ '~standard': {
13
+ version: 1 as const,
14
+ vendor: 'pstack-dsh',
15
+ validate(value: unknown) {
16
+ if (value === undefined || value === null) {
17
+ return { value: { spawnProvider: 'spawn' } }
18
+ }
19
+ if (typeof value !== 'object' || Array.isArray(value)) {
20
+ return { issues: [{ message: 'config must be an object' }] }
21
+ }
22
+ const spawnProvider = (value as { spawnProvider?: unknown }).spawnProvider
23
+ if (spawnProvider !== undefined && (typeof spawnProvider !== 'string' || spawnProvider.length === 0)) {
24
+ return { issues: [{ message: 'spawnProvider must be a non-empty string' }] }
25
+ }
26
+ return { value: { spawnProvider: spawnProvider ?? 'spawn' } }
27
+ },
28
+ },
29
+ }
@@ -0,0 +1,40 @@
1
+ export class RoleEffortMap {
2
+ private readonly bySession = new Map<string, { role: string; reasoningEffort?: string; provider?: string; model?: string }>()
3
+
4
+ remember(sessionId: string, binding: { role: string; reasoningEffort?: string; provider?: string; model?: string }): void {
5
+ this.bySession.set(sessionId, binding)
6
+ }
7
+
8
+ forget(sessionId: string): void {
9
+ this.bySession.delete(sessionId)
10
+ }
11
+
12
+ lookup(sessionId: string) {
13
+ return this.bySession.get(sessionId)
14
+ }
15
+ }
16
+
17
+ export interface CallConfig {
18
+ provider: string
19
+ model: string
20
+ reasoningEffort?: string
21
+ temperature?: number
22
+ maxTokens?: number
23
+ stop?: string[]
24
+ }
25
+
26
+ /**
27
+ * Apply overlay effort/route on the child request waterfall.
28
+ * Official home is `LlmCallConfig.reasoningEffort` (`packages/llm/llm/src/call-config.ts`),
29
+ * not a spawn-tool field. `agent/request` is the replacement waterfall
30
+ * (`packages/core/agent/src/runtime-types.ts`).
31
+ */
32
+ export function applyRoleConfig(base: CallConfig, binding: { reasoningEffort?: string; provider?: string; model?: string } | undefined): CallConfig {
33
+ if (binding === undefined) return base
34
+ return {
35
+ ...base,
36
+ ...binding.provider ? { provider: binding.provider } : {},
37
+ ...binding.model ? { model: binding.model } : {},
38
+ ...binding.reasoningEffort ? { reasoningEffort: binding.reasoningEffort } : {},
39
+ }
40
+ }
package/src/roles.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * pstack role keys used by playbooks and Settings → pstack.
3
+ * These are pstack names, not Cursor slugs and not grok-build `subagent_type` values.
4
+ */
5
+
6
+ export const SCALAR_ROLES = [
7
+ 'feature',
8
+ 'refactoring',
9
+ 'bug-fix',
10
+ 'perf-issue',
11
+ 'hillclimb',
12
+ 'judgment-and-prose',
13
+ 'hardest-tasks',
14
+ 'how-explorer',
15
+ 'how-explainer',
16
+ 'why-investigators',
17
+ 'why-synthesizer',
18
+ 'reflect-tooling',
19
+ 'reflect-judgment',
20
+ 'swarm-workers',
21
+ 'independent-verifier',
22
+ 'poteto-agent',
23
+ 'comment-sicko',
24
+ ] as const
25
+
26
+ export const PANEL_ROLES = [
27
+ 'how-critics',
28
+ 'arena-runners',
29
+ 'arena-cross-judge-pool',
30
+ 'architect-runners',
31
+ 'interrogate-reviewers',
32
+ ] as const
33
+
34
+ export const ALL_ROLES = [...SCALAR_ROLES, ...PANEL_ROLES] as const
35
+
36
+ export type ScalarRole = (typeof SCALAR_ROLES)[number]
37
+ export type PanelRole = (typeof PANEL_ROLES)[number]
38
+ export type PstackRole = (typeof ALL_ROLES)[number]
39
+
40
+ export function isPstackRole(value: string): value is PstackRole {
41
+ return (ALL_ROLES as readonly string[]).includes(value)
42
+ }
43
+
44
+ export function isPanelRole(value: string): value is PanelRole {
45
+ return (PANEL_ROLES as readonly string[]).includes(value)
46
+ }
47
+
48
+ /** Alias accepted from Cursor-era skills. */
49
+ export function normalizeRole(raw: string): string {
50
+ const trimmed = raw.trim()
51
+ if (trimmed === 'Comment Sicko' || trimmed === 'comment sicko') return 'comment-sicko'
52
+ if (trimmed === 'poteto-agent' || trimmed === 'Poteto Agent') return 'poteto-agent'
53
+ if (trimmed === 'generalPurpose' || trimmed === 'general-purpose') return 'poteto-agent'
54
+ return trimmed
55
+ }