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,48 @@
1
+ import type { CatalogHost } from './catalog.ts'
2
+ import { buildCatalog } from './catalog.ts'
3
+ import type { CatalogResult } from './catalog-types.ts'
4
+ import {
5
+ parseOverlay,
6
+ readOverlay,
7
+ validateOverlayAgainstCatalog,
8
+ writeOverlay,
9
+ type Overlay,
10
+ } from './overlay.ts'
11
+ import { resolveDshHome } from './home.ts'
12
+ import { dropUnselectableRoles } from './settings-draft.ts'
13
+
14
+ export interface SettingsSnapshot {
15
+ catalog: CatalogResult
16
+ overlay: Overlay
17
+ path: string
18
+ missing: boolean
19
+ droppedRoles: string[]
20
+ }
21
+
22
+ export async function loadSettingsSnapshot(host: CatalogHost): Promise<SettingsSnapshot> {
23
+ const dshHome = host.dshHome ?? resolveDshHome(host.env)
24
+ const catalog = await buildCatalog({ ...host, dshHome })
25
+ const current = await readOverlay(dshHome)
26
+ const { overlay, droppedRoles } = dropUnselectableRoles(current.overlay, catalog.routes)
27
+ return {
28
+ catalog,
29
+ overlay,
30
+ path: current.path,
31
+ missing: current.missing,
32
+ droppedRoles,
33
+ }
34
+ }
35
+
36
+ export async function saveSettingsOverlay(host: CatalogHost, raw: unknown): Promise<{ path: string; overlay: Overlay }> {
37
+ const dshHome = host.dshHome ?? resolveDshHome(host.env)
38
+ const overlay = typeof raw === 'string' ? parseOverlay(raw) : parseOverlay(JSON.stringify(raw))
39
+ const catalog = await buildCatalog({ ...host, dshHome })
40
+ const errors = validateOverlayAgainstCatalog(overlay, catalog.routes)
41
+ if (errors.length > 0) {
42
+ const error = new Error(`overlay rejected against live catalog:\n${errors.join('\n')}`)
43
+ ;(error as Error & { errors: string[] }).errors = errors
44
+ throw error
45
+ }
46
+ const path = await writeOverlay(overlay, dshHome)
47
+ return { path, overlay }
48
+ }
@@ -0,0 +1,144 @@
1
+ import { ALL_ROLES, isPanelRole, type PstackRole } from './roles.ts'
2
+ import type { LiveRoute } from './catalog-types.ts'
3
+ import {
4
+ emptyOverlay,
5
+ routeKey,
6
+ type Overlay,
7
+ type OverlayRoute,
8
+ } from './overlay-model.ts'
9
+
10
+ export type InheritChoice = 'inherit-parent' | 'auto'
11
+
12
+ export interface RoleDraft {
13
+ readonly role: PstackRole
14
+ readonly panel: boolean
15
+ inherit: boolean
16
+ inheritChoice: InheritChoice
17
+ routes: OverlayRoute[]
18
+ }
19
+
20
+ export function selectableRoutes(routes: readonly LiveRoute[]): LiveRoute[] {
21
+ return routes.filter(route => route.selectable)
22
+ }
23
+
24
+ export function overlayToDraft(overlay: Overlay): RoleDraft[] {
25
+ return ALL_ROLES.map(role => {
26
+ const assignment = overlay.roles[role] ?? { inherit: true, routes: [] }
27
+ const inherit = assignment.inherit || assignment.routes.length === 0
28
+ return {
29
+ role,
30
+ panel: isPanelRole(role),
31
+ inherit,
32
+ inheritChoice: 'inherit-parent' as InheritChoice,
33
+ routes: inherit ? [] : assignment.routes.map(route => ({ ...route })),
34
+ }
35
+ })
36
+ }
37
+
38
+ export function draftToOverlay(drafts: readonly RoleDraft[]): Overlay {
39
+ const overlay = emptyOverlay()
40
+ for (const draft of drafts) {
41
+ if (draft.inherit || draft.routes.length === 0) {
42
+ overlay.roles[draft.role] = { inherit: true, routes: [] }
43
+ continue
44
+ }
45
+ overlay.roles[draft.role] = {
46
+ inherit: false,
47
+ routes: draft.panel ? draft.routes.map(route => ({ ...route })) : [draft.routes[0]!],
48
+ }
49
+ }
50
+ return overlay
51
+ }
52
+
53
+ export function liveFor(routes: readonly LiveRoute[], provider: string, model: string): LiveRoute | undefined {
54
+ return routes.find(route => route.selectable && route.provider === provider && route.model === model)
55
+ }
56
+
57
+ export function stripIllegalEffort(route: OverlayRoute, live: readonly LiveRoute[]): OverlayRoute {
58
+ const found = liveFor(live, route.provider, route.model)
59
+ if (found === undefined) return { provider: route.provider, model: route.model }
60
+ if (route.reasoningEffort === undefined) return { provider: route.provider, model: route.model }
61
+ if (found.efforts.length === 0) return { provider: route.provider, model: route.model }
62
+ if (!found.efforts.some(effort => effort.id === route.reasoningEffort)) {
63
+ return { provider: route.provider, model: route.model }
64
+ }
65
+ return { provider: route.provider, model: route.model, reasoningEffort: route.reasoningEffort }
66
+ }
67
+
68
+ /** Stale overlay entries become inherit in the returned overlay; original file is not written. */
69
+ export function dropUnselectableRoles(overlay: Overlay, live: readonly LiveRoute[]): {
70
+ overlay: Overlay
71
+ droppedRoles: string[]
72
+ } {
73
+ const next = emptyOverlay()
74
+ const droppedRoles: string[] = []
75
+ const selectable = new Set(live.filter(route => route.selectable).map(route => routeKey(route.provider, route.model)))
76
+ for (const role of ALL_ROLES) {
77
+ const assignment = overlay.roles[role] ?? { inherit: true, routes: [] }
78
+ if (role === 'how-critics') {
79
+ next.roles[role] = { inherit: assignment.inherit, routes: assignment.routes.map(route => ({ ...route })) }
80
+ continue
81
+ }
82
+ if (assignment.inherit || assignment.routes.length === 0) {
83
+ next.roles[role] = { inherit: true, routes: [] }
84
+ continue
85
+ }
86
+ const kept = assignment.routes
87
+ .filter(route => selectable.has(routeKey(route.provider, route.model)))
88
+ .map(route => stripIllegalEffort(route, live))
89
+ if (kept.length === 0) {
90
+ droppedRoles.push(role)
91
+ next.roles[role] = { inherit: true, routes: [] }
92
+ continue
93
+ }
94
+ if (kept.length !== assignment.routes.length) droppedRoles.push(role)
95
+ next.roles[role] = {
96
+ inherit: false,
97
+ routes: isPanelRole(role) ? kept : [kept[0]!],
98
+ }
99
+ }
100
+ return { overlay: next, droppedRoles }
101
+ }
102
+
103
+ export function applyInheritAll(drafts: RoleDraft[], choice: InheritChoice = 'inherit-parent'): RoleDraft[] {
104
+ return drafts.map(draft => draft.role === 'how-critics' ? draft : ({
105
+ ...draft,
106
+ inherit: true,
107
+ inheritChoice: choice,
108
+ routes: [],
109
+ }))
110
+ }
111
+
112
+ export function applyRouteToAll(drafts: RoleDraft[], route: OverlayRoute, live: readonly LiveRoute[]): RoleDraft[] {
113
+ const cleaned = stripIllegalEffort(route, live)
114
+ if (liveFor(live, cleaned.provider, cleaned.model) === undefined) return drafts
115
+ return drafts.map(draft => draft.role === 'how-critics' ? draft : ({
116
+ ...draft,
117
+ inherit: false,
118
+ inheritChoice: 'inherit-parent',
119
+ routes: draft.panel ? [{ ...cleaned }] : [{ ...cleaned }],
120
+ }))
121
+ }
122
+
123
+ export function routeSelectValue(draft: RoleDraft, index = 0): string {
124
+ if (draft.inherit || draft.routes.length === 0) return draft.inheritChoice
125
+ const route = draft.routes[index]
126
+ if (route === undefined) return draft.inheritChoice
127
+ return routeKey(route.provider, route.model)
128
+ }
129
+
130
+ export function parseRouteSelectValue(
131
+ value: string,
132
+ live: readonly LiveRoute[],
133
+ ): { inherit: true; inheritChoice: InheritChoice } | { inherit: false; route: OverlayRoute } {
134
+ if (value === 'inherit-parent' || value === 'auto') {
135
+ return { inherit: true, inheritChoice: value }
136
+ }
137
+ const separator = value.indexOf('::')
138
+ if (separator <= 0) return { inherit: true, inheritChoice: 'inherit-parent' }
139
+ const provider = value.slice(0, separator)
140
+ const model = value.slice(separator + 2)
141
+ const found = liveFor(live, provider, model)
142
+ if (found === undefined) return { inherit: true, inheritChoice: 'inherit-parent' }
143
+ return { inherit: false, route: { provider: found.provider, model: found.model } }
144
+ }
@@ -0,0 +1,77 @@
1
+ /** Same-origin Settings snapshot/save. Overlay file is `$DSH_HOME/pstack-dsh.json`. */
2
+
3
+ import type { IncomingMessage, ServerResponse } from 'node:http'
4
+ import type { Context } from '@deepseek-ai/cordis'
5
+ import type {} from '@deepseek-ai/dsh-client-connection'
6
+ import type { CatalogHost } from './catalog.ts'
7
+ import { SETTINGS_SAVE_PATH, SETTINGS_SNAPSHOT_PATH } from './ids.ts'
8
+ import { json, readJson, trustedRequest } from './http-trust.ts'
9
+ import { loadSettingsSnapshot, saveSettingsOverlay } from './settings-api.ts'
10
+
11
+ function isRecord(value: unknown): value is Record<string, unknown> {
12
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
13
+ }
14
+
15
+ async function handleSnapshot(host: CatalogHost, req: IncomingMessage, res: ServerResponse): Promise<void> {
16
+ if (req.method !== 'GET') {
17
+ json(res, 405, { error: 'method not allowed' })
18
+ return
19
+ }
20
+ json(res, 200, await loadSettingsSnapshot(host))
21
+ }
22
+
23
+ async function handleSave(host: CatalogHost, req: IncomingMessage, res: ServerResponse): Promise<void> {
24
+ if (req.method !== 'PUT' && req.method !== 'POST') {
25
+ json(res, 405, { error: 'method not allowed' })
26
+ return
27
+ }
28
+ try {
29
+ const body = await readJson(req)
30
+ const overlay = isRecord(body) && 'overlay' in body ? body.overlay : body
31
+ const saved = await saveSettingsOverlay(host, overlay)
32
+ json(res, 200, saved)
33
+ } catch (error: unknown) {
34
+ const message = error instanceof Error ? error.message : 'save failed'
35
+ const errors = error instanceof Error && 'errors' in error && Array.isArray(error.errors)
36
+ ? error.errors as string[]
37
+ : undefined
38
+ json(res, 400, { error: message, ...errors === undefined ? {} : { errors } })
39
+ }
40
+ }
41
+
42
+ export function registerPstackSettingsRoutes(ctx: Context, host: CatalogHost): void {
43
+ const webServer = ctx.webServer
44
+ if (webServer === undefined) return
45
+ ctx.effect(() => {
46
+ const routes = [
47
+ webServer.register({
48
+ kind: 'exact',
49
+ path: SETTINGS_SNAPSHOT_PATH,
50
+ handler: async (req, res) => {
51
+ const rejection = ctx.connection.requestRejection(req)
52
+ if (rejection !== undefined) {
53
+ json(res, rejection, { error: rejection === 401 ? 'unauthorized' : 'forbidden' })
54
+ return
55
+ }
56
+ if (!trustedRequest(req)) {
57
+ json(res, 403, { error: 'forbidden' })
58
+ return
59
+ }
60
+ if (req.method === 'GET') {
61
+ await handleSnapshot(host, req, res)
62
+ return
63
+ }
64
+ await handleSave(host, req, res)
65
+ },
66
+ }),
67
+ ]
68
+ return () => {
69
+ for (const dispose of routes) dispose()
70
+ }
71
+ }, 'pstack-dsh: Settings overlay routes')
72
+ }
73
+
74
+ export const SETTINGS_HTTP = {
75
+ snapshot: SETTINGS_SNAPSHOT_PATH,
76
+ save: SETTINGS_SAVE_PATH,
77
+ } as const
@@ -0,0 +1,96 @@
1
+ import { readdir, readFile } from 'node:fs/promises'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { parseSkillMarkdown } from './frontmatter.ts'
5
+ import { SKILL_PROVIDER_NAME } from './ids.ts'
6
+
7
+ const BUNDLED_SKILL_RANK = 600
8
+
9
+ export function defaultSkillsRoot(): string {
10
+ return join(dirname(fileURLToPath(import.meta.url)), '..', 'skills')
11
+ }
12
+
13
+ interface SkillRecord {
14
+ name: string
15
+ description: string
16
+ whenToUse?: string
17
+ modelInvocable: boolean
18
+ userInvocable: boolean
19
+ content: string
20
+ dir: string
21
+ }
22
+
23
+ async function loadSkills(root: string): Promise<SkillRecord[]> {
24
+ let entries: string[] = []
25
+ try {
26
+ entries = await readdir(root)
27
+ } catch {
28
+ return []
29
+ }
30
+ const records: SkillRecord[] = []
31
+ for (const name of entries) {
32
+ const dir = join(root, name)
33
+ const path = join(dir, 'SKILL.md')
34
+ let text: string
35
+ try {
36
+ text = await readFile(path, 'utf8')
37
+ } catch {
38
+ continue
39
+ }
40
+ const parsed = parseSkillMarkdown(text, name)
41
+ if (parsed === undefined) continue
42
+ records.push({
43
+ name: parsed.frontmatter.name,
44
+ description: parsed.frontmatter.description,
45
+ ...parsed.frontmatter.whenToUse === undefined ? {} : { whenToUse: parsed.frontmatter.whenToUse },
46
+ modelInvocable: parsed.frontmatter.modelInvocable,
47
+ userInvocable: parsed.frontmatter.userInvocable,
48
+ content: parsed.content,
49
+ dir,
50
+ })
51
+ }
52
+ return records.sort((a, b) => a.name.localeCompare(b.name))
53
+ }
54
+
55
+ export function createSkillProvider(skillsRoot = defaultSkillsRoot()) {
56
+ return {
57
+ name: SKILL_PROVIDER_NAME,
58
+ async list() {
59
+ const skills = await loadSkills(skillsRoot)
60
+ return skills.map(skill => ({
61
+ name: skill.name,
62
+ description: skill.description,
63
+ ...skill.whenToUse === undefined ? {} : { whenToUse: skill.whenToUse },
64
+ invocation: {
65
+ modelInvocable: skill.modelInvocable,
66
+ userInvocable: skill.userInvocable,
67
+ },
68
+ provider: SKILL_PROVIDER_NAME,
69
+ source: 'bundled' as const,
70
+ resourceBase: { kind: 'directory' as const, path: skill.dir },
71
+ rank: BUNDLED_SKILL_RANK,
72
+ locator: skill.dir,
73
+ path: join(skill.dir, 'SKILL.md'),
74
+ }))
75
+ },
76
+ async get(candidate: { name: string; locator?: unknown }) {
77
+ const skills = await loadSkills(skillsRoot)
78
+ const skill = skills.find(entry => entry.name === candidate.name)
79
+ if (skill === undefined) return undefined
80
+ return {
81
+ name: skill.name,
82
+ description: skill.description,
83
+ ...skill.whenToUse === undefined ? {} : { whenToUse: skill.whenToUse },
84
+ invocation: {
85
+ modelInvocable: skill.modelInvocable,
86
+ userInvocable: skill.userInvocable,
87
+ },
88
+ provider: SKILL_PROVIDER_NAME,
89
+ source: 'bundled' as const,
90
+ resourceBase: { kind: 'directory' as const, path: skill.dir },
91
+ content: skill.content,
92
+ path: join(skill.dir, 'SKILL.md'),
93
+ }
94
+ },
95
+ }
96
+ }
package/src/spawn.ts ADDED
@@ -0,0 +1,52 @@
1
+ import type { Overlay, OverlayRoute } from './overlay.ts'
2
+ import { resolveRole } from './overlay.ts'
3
+ import { normalizeRole } from './roles.ts'
4
+
5
+ export interface SpawnRequest {
6
+ readonly role: string
7
+ readonly description: string
8
+ readonly prompt: string
9
+ readonly runInBackground: boolean
10
+ readonly routeIndex?: number
11
+ }
12
+
13
+ export interface AgentOptions {
14
+ provider?: string
15
+ model?: string
16
+ }
17
+
18
+ export interface ResolvedSpawn {
19
+ readonly role: string
20
+ readonly description: string
21
+ readonly prompt: string
22
+ readonly runInBackground: boolean
23
+ readonly inherit: boolean
24
+ readonly agentOptions?: AgentOptions
25
+ readonly reasoningEffort?: string
26
+ readonly route?: OverlayRoute
27
+ }
28
+
29
+ export function resolveSpawn(overlay: Overlay, request: SpawnRequest): ResolvedSpawn {
30
+ const role = normalizeRole(request.role)
31
+ const assignment = resolveRole(overlay, role)
32
+ const inherit = assignment.inherit || assignment.routes.length === 0
33
+ const index = request.routeIndex ?? 0
34
+ const route = inherit ? undefined : assignment.routes[index] ?? assignment.routes[0]
35
+ return {
36
+ role,
37
+ description: request.description,
38
+ prompt: request.prompt,
39
+ runInBackground: request.runInBackground,
40
+ inherit,
41
+ ...route === undefined
42
+ ? {}
43
+ : {
44
+ agentOptions: { provider: route.provider, model: route.model },
45
+ route,
46
+ ...route.reasoningEffort === undefined ? {} : { reasoningEffort: route.reasoningEffort },
47
+ },
48
+ }
49
+ }
50
+
51
+ /** Fields a skill may send on pstack_spawn. Official `subagent` has no model/effort. */
52
+ export const SPAWN_MODEL_FIELDS = ['description', 'prompt', 'role', 'run_in_background', 'route_index'] as const
package/src/tools.ts ADDED
@@ -0,0 +1,226 @@
1
+ import type { ParameterSchemaSpec } from '@deepseek-ai/dsh-tools'
2
+ import type { CatalogHost } from './catalog.ts'
3
+ import { buildCatalog } from './catalog.ts'
4
+ import {
5
+ parseOverlay,
6
+ readOverlay,
7
+ validateOverlayAgainstCatalog,
8
+ writeOverlay,
9
+ type Overlay,
10
+ } from './overlay.ts'
11
+ import { resolveDshHome } from './home.ts'
12
+ import { loadPersona } from './persona.ts'
13
+ import { applyRoleConfig, RoleEffortMap } from './request-overlay.ts'
14
+ import { resolveSpawn } from './spawn.ts'
15
+ import { isPstackRole, normalizeRole } from './roles.ts'
16
+ import {
17
+ SPAWN_PROVIDER,
18
+ TOOL_CATALOG,
19
+ TOOL_OVERLAY_READ,
20
+ TOOL_OVERLAY_WRITE,
21
+ TOOL_SPAWN,
22
+ } from './ids.ts'
23
+
24
+ export interface ToolDefinition {
25
+ name: string
26
+ description: string
27
+ parameters: ParameterSchemaSpec
28
+ execute(args: Record<string, unknown>, exec: { signal?: AbortSignal; agent?: { id: string } }): Promise<unknown>
29
+ }
30
+
31
+ export interface SubagentHandle {
32
+ start(provider: string, request: Record<string, unknown>): Promise<{ id: string; result: Promise<{ output?: unknown; stopReason?: string }>; dispose?: () => Promise<void> | void }>
33
+ startContinuable?(args: { provider: string; label?: string; request: Record<string, unknown>; signal?: AbortSignal }): Promise<{ childId: string }>
34
+ getProvider?(name: string): { prepareContinuable?: unknown; capabilities?: { persona?: boolean } } | undefined
35
+ }
36
+
37
+ export interface ToolHost {
38
+ llm?: CatalogHost['llm']
39
+ credentials?: CatalogHost['credentials']
40
+ settings?: CatalogHost['settings']
41
+ subagents?: SubagentHandle
42
+ dshHome?: string
43
+ env?: NodeJS.ProcessEnv
44
+ spawnProvider?: string
45
+ roles: RoleEffortMap
46
+ }
47
+
48
+ function textPrompt(prompt: string): unknown[] {
49
+ return [{ type: 'text', text: prompt }]
50
+ }
51
+
52
+ export function catalogTool(host: ToolHost): ToolDefinition {
53
+ return {
54
+ name: TOOL_CATALOG,
55
+ description:
56
+ 'List live DSH LLM routes pstack may use: registered adapters with a logged-in API key, plus OAuth store routes (dsh-oauth-login, dsh-antigravity-oauth) that are already signed in. Never a vendor catalog. Secrets are omitted.',
57
+ parameters: {},
58
+ async execute(_args, exec) {
59
+ return buildCatalog({
60
+ llm: host.llm,
61
+ credentials: host.credentials,
62
+ settings: host.settings,
63
+ dshHome: host.dshHome,
64
+ env: host.env,
65
+ }, exec.signal)
66
+ },
67
+ }
68
+ }
69
+
70
+ export function overlayReadTool(host: ToolHost): ToolDefinition {
71
+ return {
72
+ name: TOOL_OVERLAY_READ,
73
+ description: 'Read $DSH_HOME/pstack-dsh.json. Missing file means inherit the parent conversation for every role.',
74
+ parameters: {},
75
+ async execute() {
76
+ const current = await readOverlay(host.dshHome ?? resolveDshHome(host.env))
77
+ return {
78
+ path: current.path,
79
+ missing: current.missing,
80
+ overlay: current.overlay,
81
+ }
82
+ },
83
+ }
84
+ }
85
+
86
+ export function overlayWriteTool(host: ToolHost): ToolDefinition {
87
+ return {
88
+ name: TOOL_OVERLAY_WRITE,
89
+ description:
90
+ 'Write $DSH_HOME/pstack-dsh.json after validating every provider/model/effort against pstack_catalog. inherit-parent is stored as inherit: true with empty routes.',
91
+ parameters: {
92
+ overlay: {
93
+ type: 'json',
94
+ required: true,
95
+ description: 'Full overlay object { version: 1, roles: { <role>: { inherit, routes } } }',
96
+ },
97
+ },
98
+ async execute(args) {
99
+ const overlay = typeof args.overlay === 'string'
100
+ ? parseOverlay(args.overlay)
101
+ : parseOverlay(JSON.stringify(args.overlay))
102
+ const catalog = await buildCatalog({
103
+ llm: host.llm,
104
+ credentials: host.credentials,
105
+ settings: host.settings,
106
+ dshHome: host.dshHome,
107
+ env: host.env,
108
+ })
109
+ const errors = validateOverlayAgainstCatalog(overlay, catalog.routes)
110
+ if (errors.length > 0) {
111
+ throw new Error(`overlay rejected against live catalog:\n${errors.join('\n')}`)
112
+ }
113
+ const path = await writeOverlay(overlay, host.dshHome ?? resolveDshHome(host.env))
114
+ return { path, overlay }
115
+ },
116
+ }
117
+ }
118
+
119
+ export function spawnTool(host: ToolHost): ToolDefinition {
120
+ return {
121
+ name: TOOL_SPAWN,
122
+ description:
123
+ 'Delegate a pstack role to a DSH subagent via ctx.subagents (spawn provider). Send description, prompt, role, and optional run_in_background. Do not send model, provider, reasoning_effort, thinking, isolation, or subagent_type. Route and effort come from $DSH_HOME/pstack-dsh.json when that role is mapped to a live logged-in route; otherwise the child inherits this conversation.',
124
+ parameters: {
125
+ description: {
126
+ type: 'string',
127
+ required: true,
128
+ description: 'A short (3-5 word) description of the delegated task, for display.',
129
+ },
130
+ prompt: {
131
+ type: 'string',
132
+ required: true,
133
+ description: 'The complete, self-contained task. The child does not see this conversation.',
134
+ },
135
+ role: {
136
+ type: 'string',
137
+ required: true,
138
+ description: 'pstack role key (feature, how-explainer, poteto-agent, comment-sicko, …).',
139
+ },
140
+ run_in_background: {
141
+ type: 'boolean',
142
+ description: 'Default true. Continuable children return a subagent id; one-shot background returns a job-like start without waiting.',
143
+ },
144
+ route_index: {
145
+ type: 'number',
146
+ description: 'Which overlay route to use for a panel role. Default 0.',
147
+ },
148
+ },
149
+ async execute(args, exec) {
150
+ const parent = exec.agent
151
+ if (parent === undefined) throw new Error('pstack_spawn requires a calling agent')
152
+ const subagents = host.subagents
153
+ if (subagents === undefined) throw new Error('pstack_spawn requires ctx.subagents')
154
+ const role = normalizeRole(String(args.role ?? ''))
155
+ if (role === 'how-critics') {
156
+ return { kind: 'disabled', role, reason: 'how-critics was retired in pstack 0.15; no agent was started.' }
157
+ }
158
+ if (!isPstackRole(role) && role.length === 0) throw new Error('pstack_spawn role is required')
159
+ const overlay: Overlay = (await readOverlay(host.dshHome ?? resolveDshHome(host.env))).overlay
160
+ const resolved = resolveSpawn(overlay, {
161
+ role,
162
+ description: String(args.description ?? ''),
163
+ prompt: String(args.prompt ?? ''),
164
+ runInBackground: args.run_in_background !== false,
165
+ routeIndex: typeof args.route_index === 'number' ? args.route_index : undefined,
166
+ })
167
+ if (resolved.description.trim().length === 0) throw new Error('description is required')
168
+ if (resolved.prompt.trim().length === 0) throw new Error('prompt is required')
169
+
170
+ const persona = await loadPersona(resolved.role)
171
+ const providerName = host.spawnProvider ?? SPAWN_PROVIDER
172
+ const provider = subagents.getProvider?.(providerName)
173
+ const request: Record<string, unknown> = {
174
+ label: resolved.description,
175
+ prompt: textPrompt(resolved.prompt),
176
+ parent,
177
+ signal: exec.signal,
178
+ ...resolved.agentOptions ? { agentOptions: resolved.agentOptions } : {},
179
+ ...persona && provider?.capabilities?.persona !== false ? { persona } : {},
180
+ }
181
+
182
+ const remember = (sessionId: string) => {
183
+ host.roles.remember(sessionId, {
184
+ role: resolved.role,
185
+ ...resolved.reasoningEffort ? { reasoningEffort: resolved.reasoningEffort } : {},
186
+ ...resolved.agentOptions?.provider ? { provider: resolved.agentOptions.provider } : {},
187
+ ...resolved.agentOptions?.model ? { model: resolved.agentOptions.model } : {},
188
+ })
189
+ }
190
+
191
+ if (resolved.runInBackground && typeof subagents.startContinuable === 'function' && provider?.prepareContinuable !== undefined) {
192
+ const started = await subagents.startContinuable({
193
+ provider: providerName,
194
+ label: resolved.description,
195
+ request,
196
+ signal: exec.signal,
197
+ })
198
+ remember(started.childId)
199
+ return { kind: 'continuable', subagentId: started.childId, role: resolved.role, inherit: resolved.inherit }
200
+ }
201
+
202
+ const run = await subagents.start(providerName, request)
203
+ remember(run.id)
204
+ if (resolved.runInBackground) {
205
+ return { kind: 'foreground-detached', runId: run.id, role: resolved.role, inherit: resolved.inherit }
206
+ }
207
+ const result = await run.result
208
+ await Promise.resolve(run.dispose?.())
209
+ return {
210
+ kind: 'foreground',
211
+ runId: run.id,
212
+ role: resolved.role,
213
+ inherit: resolved.inherit,
214
+ output: result.output ?? null,
215
+ stopReason: result.stopReason ?? 'completed',
216
+ }
217
+ },
218
+ }
219
+ }
220
+
221
+ export function bindRequestOverlay(roles: RoleEffortMap) {
222
+ return (sessionId: string, config: Parameters<typeof applyRoleConfig>[0]) =>
223
+ applyRoleConfig(config, roles.lookup(sessionId))
224
+ }
225
+
226
+ export const ALL_TOOLS = [catalogTool, overlayReadTool, overlayWriteTool, spawnTool] as const