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,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
|
+
}
|