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.
- package/HARNESS.md +109 -0
- package/LICENSE +25 -0
- package/README.md +168 -0
- package/TEST-PLAN.md +70 -0
- package/agents/comment-sicko.md +32 -0
- package/agents/poteto-agent.md +9 -0
- package/cordis.patch.yml +4 -0
- package/cordis.yml +4 -0
- package/docs/guide/01-setup.md +41 -0
- package/docs/guide/02-poteto-mode.md +98 -0
- package/docs/guide/03-understand.md +53 -0
- package/docs/guide/04-design.md +81 -0
- package/docs/guide/05-build-and-clean.md +75 -0
- package/docs/guide/06-verify-and-ship.md +87 -0
- package/docs/guide/07-overnight.md +81 -0
- package/docs/guide/08-principles.md +71 -0
- package/docs/guide/09-make-it-yours.md +67 -0
- package/docs/guide/10-recipes-and-pitfalls.md +96 -0
- package/docs/guide/README.md +30 -0
- package/dshx.yml +9 -0
- package/lib/client.js +981 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +34 -0
- package/lib/index.js +1120 -0
- package/package.json +140 -0
- package/skills/architect/SKILL.md +83 -0
- package/skills/architect/references/design-red-flags.md +33 -0
- package/skills/architect/references/rationale-template.md +35 -0
- package/skills/architect/references/runner-prompt.md +20 -0
- package/skills/arena/SKILL.md +73 -0
- package/skills/automate-me/SKILL.md +103 -0
- package/skills/blast-radius/SKILL.md +50 -0
- package/skills/bro/SKILL.md +7 -0
- package/skills/create-verification-skill/SKILL.md +44 -0
- package/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
- package/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
- package/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
- package/skills/figure-it-out/SKILL.md +53 -0
- package/skills/how/SKILL.md +45 -0
- package/skills/how/references/explainer-prompt.md +55 -0
- package/skills/how/references/explorer-prompt.md +52 -0
- package/skills/interrogate/SKILL.md +99 -0
- package/skills/interrogate/references/code-quality-review.md +47 -0
- package/skills/interrogate/references/lead-judgment.md +58 -0
- package/skills/interrogate/references/reviewer-prompt.md +72 -0
- package/skills/interrogate/references/rubric.md +77 -0
- package/skills/maintain-verification-skill/SKILL.md +39 -0
- package/skills/no-comments/SKILL.md +24 -0
- package/skills/poteto-mode/SKILL.md +149 -0
- package/skills/poteto-mode/playbooks/authoring-a-skill.md +12 -0
- package/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
- package/skills/poteto-mode/playbooks/autopilot-full.md +13 -0
- package/skills/poteto-mode/playbooks/autopilot-stack.md +16 -0
- package/skills/poteto-mode/playbooks/babysit.md +27 -0
- package/skills/poteto-mode/playbooks/bug-fix.md +17 -0
- package/skills/poteto-mode/playbooks/eval.md +25 -0
- package/skills/poteto-mode/playbooks/feature.md +21 -0
- package/skills/poteto-mode/playbooks/hillclimb.md +21 -0
- package/skills/poteto-mode/playbooks/investigation.md +14 -0
- package/skills/poteto-mode/playbooks/multi-phase-plan.md +156 -0
- package/skills/poteto-mode/playbooks/opening-a-pr.md +33 -0
- package/skills/poteto-mode/playbooks/orchestrate.md +113 -0
- package/skills/poteto-mode/playbooks/pause-safely.md +10 -0
- package/skills/poteto-mode/playbooks/perf-issue.md +24 -0
- package/skills/poteto-mode/playbooks/prototype.md +14 -0
- package/skills/poteto-mode/playbooks/refactoring.md +16 -0
- package/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
- package/skills/poteto-mode/playbooks/session-pickup.md +11 -0
- package/skills/poteto-mode/playbooks/shipping.md +17 -0
- package/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
- package/skills/poteto-mode/playbooks/visual-parity.md +11 -0
- package/skills/poteto-mode/playbooks/worktree-cleanup.md +14 -0
- package/skills/poteto-mode/references/bugbot-triage.md +142 -0
- package/skills/poteto-mode/scripts/bootstrap.ts +62 -0
- package/skills/poteto-mode/scripts/bun.lock +67 -0
- package/skills/poteto-mode/scripts/check-plan.mjs +186 -0
- package/skills/poteto-mode/scripts/orch/orch.test.ts +634 -0
- package/skills/poteto-mode/scripts/orch/orch.ts +578 -0
- package/skills/poteto-mode/scripts/orch/store.ts +1607 -0
- package/skills/poteto-mode/scripts/package.json +16 -0
- package/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
- package/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
- package/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +118 -0
- package/skills/poteto-mode/scripts/watch-pr/github.test.ts +306 -0
- package/skills/poteto-mode/scripts/watch-pr/github.ts +699 -0
- package/skills/poteto-mode/scripts/watch-pr/policy.test.ts +420 -0
- package/skills/poteto-mode/scripts/watch-pr/policy.ts +832 -0
- package/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
- package/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
- package/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
- package/skills/poteto-mode/scripts/watch-pr/types.ts +401 -0
- package/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
- package/skills/poteto-mode/scripts/worktree-audit.sh +86 -0
- package/skills/principle-attack-the-premise/SKILL.md +22 -0
- package/skills/principle-boundary-discipline/SKILL.md +33 -0
- package/skills/principle-build-the-lever/SKILL.md +22 -0
- package/skills/principle-encode-lessons-in-structure/SKILL.md +30 -0
- package/skills/principle-exhaust-the-design-space/SKILL.md +20 -0
- package/skills/principle-experience-first/SKILL.md +18 -0
- package/skills/principle-fix-root-causes/SKILL.md +22 -0
- package/skills/principle-foundational-thinking/SKILL.md +20 -0
- package/skills/principle-guard-the-context-window/SKILL.md +16 -0
- package/skills/principle-laziness-protocol/SKILL.md +17 -0
- package/skills/principle-make-operations-idempotent/SKILL.md +23 -0
- package/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +21 -0
- package/skills/principle-minimize-reader-load/SKILL.md +22 -0
- package/skills/principle-model-the-domain/SKILL.md +25 -0
- package/skills/principle-never-block-on-the-human/SKILL.md +21 -0
- package/skills/principle-outcome-oriented-execution/SKILL.md +21 -0
- package/skills/principle-prove-it-works/SKILL.md +32 -0
- package/skills/principle-redesign-from-first-principles/SKILL.md +15 -0
- package/skills/principle-separate-before-serializing-shared-state/SKILL.md +15 -0
- package/skills/principle-sequence-verifiable-units/SKILL.md +21 -0
- package/skills/principle-subtract-before-you-add/SKILL.md +20 -0
- package/skills/principle-test-behavior-not-implementation/SKILL.md +24 -0
- package/skills/principle-type-system-discipline/SKILL.md +30 -0
- package/skills/recall/SKILL.md +35 -0
- package/skills/reflect/SKILL.md +65 -0
- package/skills/reflect/references/divergent-reviewer.md +43 -0
- package/skills/reflect/references/judgment-reviewer.md +42 -0
- package/skills/reflect/references/synthesizer.md +56 -0
- package/skills/reflect/references/tooling-reviewer.md +55 -0
- package/skills/setup-pstack/SKILL.md +32 -0
- package/skills/setup-pstack/references/catalog.md +28 -0
- package/skills/setup-pstack/references/spawn.md +61 -0
- package/skills/show-me-your-work/SKILL.md +82 -0
- package/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
- package/skills/show-me-your-work/scripts/log.sh +40 -0
- package/skills/swarm/SKILL.md +44 -0
- package/skills/tdd/SKILL.md +44 -0
- package/skills/teach/SKILL.md +21 -0
- package/skills/technical-writing/SKILL.md +127 -0
- package/skills/typescript-best-practices/SKILL.md +30 -0
- package/skills/typescript-best-practices/references/patterns.md +313 -0
- package/skills/unslop/SKILL.md +67 -0
- package/skills/why/SKILL.md +146 -0
- package/skills/why/references/epistemics.md +144 -0
- package/skills/why/references/investigator-prompt.md +103 -0
- package/skills/why/references/source-playbook.md +17 -0
- package/skills/why/references/sources/code-archaeology.md +88 -0
- package/skills/why/references/sources/databricks.md +70 -0
- package/skills/why/references/sources/datadog.md +99 -0
- package/skills/why/references/sources/incident-postmortem.md +15 -0
- package/skills/why/references/sources/linear.md +48 -0
- package/skills/why/references/sources/notion.md +55 -0
- package/skills/why/references/sources/sentry.md +100 -0
- package/skills/why/references/sources/slack.md +54 -0
- package/skills/why/references/synthesizer-prompt.md +135 -0
- package/src/catalog-types.ts +54 -0
- package/src/catalog.ts +308 -0
- package/src/client/PstackSettings.tsx +519 -0
- package/src/client/api.ts +56 -0
- package/src/client/index.tsx +38 -0
- package/src/client/locales.ts +65 -0
- package/src/client/poteto-defaults.ts +37 -0
- package/src/frontmatter.ts +73 -0
- package/src/home.ts +13 -0
- package/src/http-trust.ts +38 -0
- package/src/ids.ts +39 -0
- package/src/index.ts +85 -0
- package/src/overlay-model.ts +139 -0
- package/src/overlay.ts +40 -0
- package/src/persona.ts +38 -0
- package/src/plugin-config.ts +29 -0
- package/src/request-overlay.ts +40 -0
- package/src/roles.ts +55 -0
- package/src/settings-api.ts +48 -0
- package/src/settings-draft.ts +144 -0
- package/src/settings-routes.ts +77 -0
- package/src/skills-provider.ts +96 -0
- package/src/spawn.ts +52 -0
- 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
|