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
package/lib/index.js ADDED
@@ -0,0 +1,1120 @@
1
+ import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
2
+ import { defineTool } from "@deepseek-ai/dsh-tools";
3
+ import { snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
4
+ import { readFile, readdir } from "node:fs/promises";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { homedir } from "node:os";
8
+ import { writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
9
+ //#region src/ids.ts
10
+ /** Cordis plugin / Loader row id. */
11
+ const PLUGIN_ID = "pstack-dsh";
12
+ /** Bundled skill provider name on `ctx.skills`. */
13
+ const SKILL_PROVIDER_NAME = "pstack-dsh";
14
+ /** Overlay file under `$DSH_HOME`. */
15
+ const OVERLAY_FILENAME = "pstack-dsh.json";
16
+ /** DSH-owned OAuth store from dsh-oauth-login. Never ~/.pi / ~/.codex / ~/.claude / grok CLI. */
17
+ const OAUTH_AUTH_FILENAME = ".dsh-oauth-auth.json";
18
+ const OAUTH_AUTH_LEGACY_FILENAME = ".pi-login-auth.json";
19
+ /** Antigravity OAuth store from dsh-antigravity-oauth. */
20
+ const ANTIGRAVITY_AUTH_FILENAME = ".dsh-antigravity-oauth.json";
21
+ /** Official DeepSeek adapter. `packages/llm/llm-deepseek/src/index.ts` `PROVIDER`, `DEFAULT_API_KEY_ENV`. */
22
+ const DEEPSEEK_API_KEY_ENV = "DEEPSEEK_API_KEY";
23
+ /** Model-facing tool names this plugin registers. */
24
+ const TOOL_CATALOG = "pstack_catalog";
25
+ const TOOL_SPAWN = "pstack_spawn";
26
+ const TOOL_OVERLAY_READ = "pstack_overlay_read";
27
+ const TOOL_OVERLAY_WRITE = "pstack_overlay_write";
28
+ /** Same-origin Web routes. Pattern: dsh-oauth-login `src/auth-routes.ts`. */
29
+ const SETTINGS_SNAPSHOT_PATH = "/plugins/pstack-dsh/settings";
30
+ //#endregion
31
+ //#region src/plugin-config.ts
32
+ const Config = { "~standard": {
33
+ version: 1,
34
+ vendor: "pstack-dsh",
35
+ validate(value) {
36
+ if (value === void 0 || value === null) return { value: { spawnProvider: "spawn" } };
37
+ if (typeof value !== "object" || Array.isArray(value)) return { issues: [{ message: "config must be an object" }] };
38
+ const spawnProvider = value.spawnProvider;
39
+ if (spawnProvider !== void 0 && (typeof spawnProvider !== "string" || spawnProvider.length === 0)) return { issues: [{ message: "spawnProvider must be a non-empty string" }] };
40
+ return { value: { spawnProvider: spawnProvider ?? "spawn" } };
41
+ }
42
+ } };
43
+ //#endregion
44
+ //#region src/frontmatter.ts
45
+ const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
46
+ function unquote(value) {
47
+ const trimmed = value.trim();
48
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1);
49
+ return trimmed;
50
+ }
51
+ function parseBool(value, fallback) {
52
+ if (value === void 0) return fallback;
53
+ const normalized = unquote(value).trim().toLowerCase();
54
+ if ([
55
+ "true",
56
+ "yes",
57
+ "on",
58
+ "1"
59
+ ].includes(normalized)) return true;
60
+ if ([
61
+ "false",
62
+ "no",
63
+ "off",
64
+ "0"
65
+ ].includes(normalized)) return false;
66
+ throw new Error(`invalid boolean "${value}"`);
67
+ }
68
+ /**
69
+ * Minimal YAML frontmatter reader for SKILL.md.
70
+ * Official keys: name, description, whenToUse, disable-model-invocation, user-invocable.
71
+ * `packages/skill/skill-filesystem/README.md`.
72
+ */
73
+ function parseSkillMarkdown(text, fallbackName) {
74
+ if (!text.startsWith("---")) return void 0;
75
+ const end = text.indexOf("\n---", 3);
76
+ if (end < 0) return void 0;
77
+ const raw = text.slice(4, end);
78
+ const body = text.slice(end + 4).replace(/^\s*\n/, "");
79
+ const fields = {};
80
+ for (const line of raw.split("\n")) {
81
+ const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
82
+ if (!match) continue;
83
+ fields[match[1]] = match[2] ?? "";
84
+ }
85
+ let name = unquote(fields.name ?? fallbackName);
86
+ if (name === "Poteto Mode") name = "poteto-mode";
87
+ if (!NAME_RE.test(name)) return void 0;
88
+ const description = unquote(fields.description ?? "");
89
+ if (description.length === 0) return void 0;
90
+ try {
91
+ const disableModel = parseBool(fields["disable-model-invocation"], false);
92
+ const userInvocable = parseBool(fields["user-invocable"], true);
93
+ return {
94
+ frontmatter: {
95
+ name,
96
+ description,
97
+ ...fields.whenToUse ? { whenToUse: unquote(fields.whenToUse) } : {},
98
+ modelInvocable: !disableModel,
99
+ userInvocable
100
+ },
101
+ content: body.trimStart()
102
+ };
103
+ } catch {
104
+ return;
105
+ }
106
+ }
107
+ //#endregion
108
+ //#region src/skills-provider.ts
109
+ const BUNDLED_SKILL_RANK = 600;
110
+ function defaultSkillsRoot() {
111
+ return join(dirname(fileURLToPath(import.meta.url)), "..", "skills");
112
+ }
113
+ async function loadSkills(root) {
114
+ let entries = [];
115
+ try {
116
+ entries = await readdir(root);
117
+ } catch {
118
+ return [];
119
+ }
120
+ const records = [];
121
+ for (const name of entries) {
122
+ const dir = join(root, name);
123
+ const path = join(dir, "SKILL.md");
124
+ let text;
125
+ try {
126
+ text = await readFile(path, "utf8");
127
+ } catch {
128
+ continue;
129
+ }
130
+ const parsed = parseSkillMarkdown(text, name);
131
+ if (parsed === void 0) continue;
132
+ records.push({
133
+ name: parsed.frontmatter.name,
134
+ description: parsed.frontmatter.description,
135
+ ...parsed.frontmatter.whenToUse === void 0 ? {} : { whenToUse: parsed.frontmatter.whenToUse },
136
+ modelInvocable: parsed.frontmatter.modelInvocable,
137
+ userInvocable: parsed.frontmatter.userInvocable,
138
+ content: parsed.content,
139
+ dir
140
+ });
141
+ }
142
+ return records.sort((a, b) => a.name.localeCompare(b.name));
143
+ }
144
+ function createSkillProvider(skillsRoot = defaultSkillsRoot()) {
145
+ return {
146
+ name: SKILL_PROVIDER_NAME,
147
+ async list() {
148
+ return (await loadSkills(skillsRoot)).map((skill) => ({
149
+ name: skill.name,
150
+ description: skill.description,
151
+ ...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
152
+ invocation: {
153
+ modelInvocable: skill.modelInvocable,
154
+ userInvocable: skill.userInvocable
155
+ },
156
+ provider: SKILL_PROVIDER_NAME,
157
+ source: "bundled",
158
+ resourceBase: {
159
+ kind: "directory",
160
+ path: skill.dir
161
+ },
162
+ rank: BUNDLED_SKILL_RANK,
163
+ locator: skill.dir,
164
+ path: join(skill.dir, "SKILL.md")
165
+ }));
166
+ },
167
+ async get(candidate) {
168
+ const skill = (await loadSkills(skillsRoot)).find((entry) => entry.name === candidate.name);
169
+ if (skill === void 0) return void 0;
170
+ return {
171
+ name: skill.name,
172
+ description: skill.description,
173
+ ...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
174
+ invocation: {
175
+ modelInvocable: skill.modelInvocable,
176
+ userInvocable: skill.userInvocable
177
+ },
178
+ provider: SKILL_PROVIDER_NAME,
179
+ source: "bundled",
180
+ resourceBase: {
181
+ kind: "directory",
182
+ path: skill.dir
183
+ },
184
+ content: skill.content,
185
+ path: join(skill.dir, "SKILL.md")
186
+ };
187
+ }
188
+ };
189
+ }
190
+ //#endregion
191
+ //#region src/request-overlay.ts
192
+ var RoleEffortMap = class {
193
+ bySession = /* @__PURE__ */ new Map();
194
+ remember(sessionId, binding) {
195
+ this.bySession.set(sessionId, binding);
196
+ }
197
+ forget(sessionId) {
198
+ this.bySession.delete(sessionId);
199
+ }
200
+ lookup(sessionId) {
201
+ return this.bySession.get(sessionId);
202
+ }
203
+ };
204
+ //#endregion
205
+ //#region src/home.ts
206
+ /** Official precedence: `$DSH_HOME`, then `~/.dsh`. `packages/util/home-paths/README.md`. */
207
+ function resolveDshHome(env = process.env) {
208
+ const fromEnv = env.DSH_HOME?.trim();
209
+ if (fromEnv) return resolve(fromEnv);
210
+ return join(homedir(), ".dsh");
211
+ }
212
+ //#endregion
213
+ //#region src/catalog-types.ts
214
+ /** Hints for known logins whose adapter is missing; not an allowlist for live adapters. */
215
+ const OAUTH_ROUTE_BY_STORE_ID = {
216
+ "openai-codex": "pi-openai-codex",
217
+ anthropic: "pi-anthropic",
218
+ xai: "pi-xai",
219
+ "github-copilot": "pi-github-copilot",
220
+ openrouter: "pi-openrouter",
221
+ "kimi-coding": "pi-kimi-coding",
222
+ antigravity: "agy-google-antigravity"
223
+ };
224
+ function storeIdForRoute(route) {
225
+ if (route.startsWith("pi-") && route.length > 3) return route.slice(3);
226
+ if (route === "agy-google-antigravity" || route.startsWith("agy-")) return "antigravity";
227
+ }
228
+ //#endregion
229
+ //#region src/roles.ts
230
+ /**
231
+ * pstack role keys used by playbooks and Settings → pstack.
232
+ * These are pstack names, not Cursor slugs and not grok-build `subagent_type` values.
233
+ */
234
+ const SCALAR_ROLES = [
235
+ "feature",
236
+ "refactoring",
237
+ "bug-fix",
238
+ "perf-issue",
239
+ "hillclimb",
240
+ "judgment-and-prose",
241
+ "hardest-tasks",
242
+ "how-explorer",
243
+ "how-explainer",
244
+ "why-investigators",
245
+ "why-synthesizer",
246
+ "reflect-tooling",
247
+ "reflect-judgment",
248
+ "swarm-workers",
249
+ "independent-verifier",
250
+ "poteto-agent",
251
+ "comment-sicko"
252
+ ];
253
+ const PANEL_ROLES = [
254
+ "how-critics",
255
+ "arena-runners",
256
+ "arena-cross-judge-pool",
257
+ "architect-runners",
258
+ "interrogate-reviewers"
259
+ ];
260
+ const ALL_ROLES = [...SCALAR_ROLES, ...PANEL_ROLES];
261
+ function isPstackRole(value) {
262
+ return ALL_ROLES.includes(value);
263
+ }
264
+ function isPanelRole(value) {
265
+ return PANEL_ROLES.includes(value);
266
+ }
267
+ /** Alias accepted from Cursor-era skills. */
268
+ function normalizeRole(raw) {
269
+ const trimmed = raw.trim();
270
+ if (trimmed === "Comment Sicko" || trimmed === "comment sicko") return "comment-sicko";
271
+ if (trimmed === "poteto-agent" || trimmed === "Poteto Agent") return "poteto-agent";
272
+ if (trimmed === "generalPurpose" || trimmed === "general-purpose") return "poteto-agent";
273
+ return trimmed;
274
+ }
275
+ function emptyOverlay() {
276
+ const roles = {};
277
+ for (const role of ALL_ROLES) roles[role] = {
278
+ inherit: true,
279
+ routes: []
280
+ };
281
+ return {
282
+ version: 1,
283
+ roles
284
+ };
285
+ }
286
+ function routeKey(provider, model) {
287
+ return `${provider}::${model}`;
288
+ }
289
+ function isRecord$2(value) {
290
+ return typeof value === "object" && value !== null && !Array.isArray(value);
291
+ }
292
+ function parseRoute(raw, path) {
293
+ if (!isRecord$2(raw)) throw new Error(`${path} must be an object`);
294
+ const provider = raw.provider;
295
+ const model = raw.model;
296
+ if (typeof provider !== "string" || provider.length === 0) throw new Error(`${path}.provider must be a non-empty string`);
297
+ if (typeof model !== "string" || model.length === 0) throw new Error(`${path}.model must be a non-empty string`);
298
+ const effort = raw.reasoningEffort;
299
+ if (effort !== void 0 && (typeof effort !== "string" || effort.length === 0)) throw new Error(`${path}.reasoningEffort must be a non-empty string when set`);
300
+ const extra = Object.keys(raw).filter((key) => key !== "provider" && key !== "model" && key !== "reasoningEffort");
301
+ if (extra.length > 0) throw new Error(`${path} has unknown fields: ${extra.join(", ")}`);
302
+ return effort === void 0 ? {
303
+ provider,
304
+ model
305
+ } : {
306
+ provider,
307
+ model,
308
+ reasoningEffort: effort
309
+ };
310
+ }
311
+ function parseRole(raw, role) {
312
+ if (!isRecord$2(raw)) throw new Error(`roles.${role} must be an object`);
313
+ const extra = Object.keys(raw).filter((key) => key !== "inherit" && key !== "routes");
314
+ if (extra.length > 0) throw new Error(`roles.${role} has unknown fields: ${extra.join(", ")}`);
315
+ const inherit = raw.inherit === true || raw.inherit === "true" || raw.inherit === "inherit-parent" || raw.inherit === "auto";
316
+ const routesRaw = raw.routes;
317
+ const routes = Array.isArray(routesRaw) ? routesRaw.map((entry, index) => parseRoute(entry, `roles.${role}.routes[${index}]`)) : [];
318
+ if (inherit) return {
319
+ inherit: true,
320
+ routes: []
321
+ };
322
+ if (routes.length === 0) return {
323
+ inherit: true,
324
+ routes: []
325
+ };
326
+ if (!isPanelRole(role) && routes.length !== 1) throw new Error(`roles.${role} is a scalar role and may have at most one route`);
327
+ return {
328
+ inherit: false,
329
+ routes
330
+ };
331
+ }
332
+ function parseOverlay(text) {
333
+ let value;
334
+ try {
335
+ value = JSON.parse(text);
336
+ } catch {
337
+ throw new Error("pstack-dsh overlay is not valid JSON");
338
+ }
339
+ if (!isRecord$2(value)) throw new Error("pstack-dsh overlay must be an object");
340
+ if (value.version !== 1) throw new Error(`pstack-dsh overlay version ${String(value.version)} is unsupported`);
341
+ const extra = Object.keys(value).filter((key) => key !== "version" && key !== "roles");
342
+ if (extra.length > 0) throw new Error(`pstack-dsh overlay has unknown fields: ${extra.join(", ")}`);
343
+ const rolesRaw = value.roles;
344
+ if (!isRecord$2(rolesRaw)) throw new Error("pstack-dsh overlay.roles must be an object");
345
+ const roles = {};
346
+ for (const role of ALL_ROLES) {
347
+ const entry = rolesRaw[role];
348
+ roles[role] = entry === void 0 ? {
349
+ inherit: true,
350
+ routes: []
351
+ } : parseRole(entry, role);
352
+ }
353
+ for (const key of Object.keys(rolesRaw)) if (!isPstackRole(key)) throw new Error(`unknown pstack role "${key}"`);
354
+ return {
355
+ version: 1,
356
+ roles
357
+ };
358
+ }
359
+ function validateOverlayAgainstCatalog(overlay, routes) {
360
+ const errors = [];
361
+ const live = new Map(routes.filter((route) => route.selectable).map((route) => [routeKey(route.provider, route.model), route]));
362
+ for (const role of ALL_ROLES) {
363
+ if (role === "how-critics") continue;
364
+ const assignment = overlay.roles[role] ?? {
365
+ inherit: true,
366
+ routes: []
367
+ };
368
+ if (assignment.inherit) continue;
369
+ for (const [index, entry] of assignment.routes.entries()) {
370
+ const found = live.get(routeKey(entry.provider, entry.model));
371
+ if (found === void 0) {
372
+ errors.push(`${role}[${index}]: ${entry.provider}/${entry.model} is not a logged-in live route`);
373
+ continue;
374
+ }
375
+ if (entry.reasoningEffort === void 0) continue;
376
+ if (found.efforts.length === 0) {
377
+ errors.push(`${role}[${index}]: ${entry.provider}/${entry.model} has no effort field; omit reasoningEffort`);
378
+ continue;
379
+ }
380
+ if (!found.efforts.some((effort) => effort.id === entry.reasoningEffort)) errors.push(`${role}[${index}]: effort "${entry.reasoningEffort}" is not accepted by ${entry.provider}/${entry.model} (live: ${found.efforts.map((effort) => effort.id).join(", ")})`);
381
+ }
382
+ }
383
+ return errors;
384
+ }
385
+ function resolveRole(overlay, role) {
386
+ const key = role;
387
+ return overlay.roles[key] ?? {
388
+ inherit: true,
389
+ routes: []
390
+ };
391
+ }
392
+ //#endregion
393
+ //#region src/overlay.ts
394
+ function overlayPath(dshHome = resolveDshHome()) {
395
+ return join(dshHome, OVERLAY_FILENAME);
396
+ }
397
+ async function readOverlay(dshHome = resolveDshHome()) {
398
+ const path = overlayPath(dshHome);
399
+ try {
400
+ return {
401
+ path,
402
+ overlay: parseOverlay(await readFile(path, "utf8")),
403
+ missing: false
404
+ };
405
+ } catch (error) {
406
+ if (error.code === "ENOENT") return {
407
+ path,
408
+ overlay: emptyOverlay(),
409
+ missing: true
410
+ };
411
+ throw error;
412
+ }
413
+ }
414
+ async function writeOverlay(overlay, dshHome = resolveDshHome()) {
415
+ const path = overlayPath(dshHome);
416
+ const body = `${JSON.stringify(overlay, null, 2)}\n`;
417
+ await writeFileAtomic(path, body, {
418
+ mode: 384,
419
+ dirMode: 448
420
+ });
421
+ return path;
422
+ }
423
+ //#endregion
424
+ //#region src/catalog.ts
425
+ const API_KEY_REF = /^[A-Za-z_][A-Za-z0-9_]*$/;
426
+ function isRecord$1(value) {
427
+ return typeof value === "object" && value !== null && !Array.isArray(value);
428
+ }
429
+ function collectApiKeyEnvRefs(value, out) {
430
+ if (Array.isArray(value)) {
431
+ for (const item of value) collectApiKeyEnvRefs(item, out);
432
+ return;
433
+ }
434
+ if (!isRecord$1(value)) return;
435
+ for (const [key, child] of Object.entries(value)) if (key === "apiKeyEnv" && typeof child === "string" && API_KEY_REF.test(child)) out.add(child);
436
+ else collectApiKeyEnvRefs(child, out);
437
+ }
438
+ function sliceAtPath(value, path) {
439
+ let current = value;
440
+ for (const segment of path) {
441
+ if (!isRecord$1(current)) return void 0;
442
+ current = current[segment];
443
+ }
444
+ return current;
445
+ }
446
+ function settingsValue(entry) {
447
+ if (!isRecord$1(entry)) return void 0;
448
+ return entry.value ?? entry.base ?? entry.user;
449
+ }
450
+ function settingsNs(entry) {
451
+ if (!isRecord$1(entry)) return void 0;
452
+ return typeof entry.ns === "string" ? entry.ns : void 0;
453
+ }
454
+ async function refConfigured(credentials, env, ref) {
455
+ if (credentials !== void 0) try {
456
+ return (await credentials.describe(ref)).configured === true;
457
+ } catch {
458
+ return false;
459
+ }
460
+ const ambient = env[ref];
461
+ return typeof ambient === "string" && ambient.trim().length > 0;
462
+ }
463
+ async function readOauthStoreIds(dshHome) {
464
+ const candidates = [join(dshHome, OAUTH_AUTH_FILENAME), join(dshHome, OAUTH_AUTH_LEGACY_FILENAME)];
465
+ let present = false;
466
+ const ids = /* @__PURE__ */ new Set();
467
+ for (const filename of candidates) try {
468
+ const text = await readFile(filename, "utf8");
469
+ const parsed = JSON.parse(text);
470
+ if (!isRecord$1(parsed) || !isRecord$1(parsed.credentials)) continue;
471
+ present = true;
472
+ for (const key of Object.keys(parsed.credentials)) ids.add(key);
473
+ break;
474
+ } catch (error) {
475
+ if (error.code === "ENOENT") continue;
476
+ present = true;
477
+ break;
478
+ }
479
+ try {
480
+ const text = await readFile(join(dshHome, ANTIGRAVITY_AUTH_FILENAME), "utf8");
481
+ const parsed = JSON.parse(text);
482
+ if (isRecord$1(parsed) && isRecord$1(parsed.credential)) {
483
+ present = true;
484
+ const cred = parsed.credential;
485
+ if (cred.type === "oauth" && typeof cred.projectId === "string" && cred.projectId.trim().length > 0 && typeof cred.expires === "number" && cred.expires > 0) ids.add("antigravity");
486
+ }
487
+ } catch (error) {
488
+ if (error.code !== "ENOENT") present = true;
489
+ }
490
+ return {
491
+ present,
492
+ ids: [...ids]
493
+ };
494
+ }
495
+ function effortsOf(info) {
496
+ const reasoning = info.reasoning;
497
+ if (reasoning === void 0 || !Array.isArray(reasoning.efforts)) return { efforts: [] };
498
+ return {
499
+ efforts: reasoning.efforts.map((effort) => ({
500
+ id: effort.id,
501
+ name: effort.name,
502
+ ...effort.description === void 0 ? {} : { description: effort.description }
503
+ })),
504
+ ...reasoning.defaultEffort === void 0 ? {} : { defaultEffort: reasoning.defaultEffort }
505
+ };
506
+ }
507
+ /**
508
+ * Live logged-in routes only. No vendor catalog. No invented slugs.
509
+ */
510
+ async function buildCatalog(host, signal) {
511
+ const dshHome = host.dshHome ?? resolveDshHome(host.env);
512
+ const env = host.env ?? process.env;
513
+ const overlay = await readOverlay(dshHome);
514
+ const oauth = await readOauthStoreIds(dshHome);
515
+ const llm = host.llm;
516
+ const providers = llm?.listProviders() ?? [];
517
+ const registered = new Set(providers.map((provider) => provider.id));
518
+ const oauthPluginPresent = [...registered].some((id) => id.startsWith("pi-") || id === "agy-google-antigravity" || id.startsWith("agy-"));
519
+ const recommendOauthLogin = oauth.ids.length === 0 && !oauthPluginPresent;
520
+ const refsByProvider = /* @__PURE__ */ new Map();
521
+ const remember = (provider, ref) => {
522
+ const set = refsByProvider.get(provider) ?? /* @__PURE__ */ new Set();
523
+ set.add(ref);
524
+ refsByProvider.set(provider, set);
525
+ };
526
+ if (host.settings !== void 0) {
527
+ let described = [];
528
+ try {
529
+ described = host.settings.describe({ redactSecrets: true });
530
+ } catch {
531
+ described = [];
532
+ }
533
+ const configurable = llm?.listConfigurableProviders?.() ?? [];
534
+ for (const entry of described) {
535
+ const ns = settingsNs(entry);
536
+ const value = settingsValue(entry);
537
+ for (const row of configurable) {
538
+ if (ns !== void 0 && row.settingsNs !== ns) continue;
539
+ const slice = row.settingsPath.length === 0 ? value : sliceAtPath(value, row.settingsPath);
540
+ const refs = /* @__PURE__ */ new Set();
541
+ collectApiKeyEnvRefs(slice ?? value, refs);
542
+ for (const ref of refs) remember(row.provider, ref);
543
+ }
544
+ }
545
+ }
546
+ for (const provider of providers) if (provider.id === "deepseek-official") remember(provider.id, DEEPSEEK_API_KEY_ENV);
547
+ const routes = [];
548
+ const seen = /* @__PURE__ */ new Set();
549
+ const push = (route) => {
550
+ const key = `${route.provider}::${route.model}`;
551
+ if (seen.has(key)) return;
552
+ seen.add(key);
553
+ routes.push(route);
554
+ };
555
+ if (llm !== void 0) for (const provider of providers) {
556
+ const storeId = storeIdForRoute(provider.id);
557
+ const oauthSignedIn = storeId !== void 0 && oauth.ids.includes(storeId);
558
+ let models = [];
559
+ try {
560
+ models = await llm.listModels(provider.id);
561
+ } catch {
562
+ models = [];
563
+ }
564
+ if (models.length === 0) continue;
565
+ const refs = [...refsByProvider.get(provider.id) ?? []];
566
+ let keyPresent = false;
567
+ for (const ref of refs) if (await refConfigured(host.credentials, env, ref)) {
568
+ keyPresent = true;
569
+ break;
570
+ }
571
+ const selectable = oauthSignedIn || keyPresent;
572
+ const source = oauthSignedIn ? "oauth" : "api-key";
573
+ if (!selectable) continue;
574
+ for (const model of models) {
575
+ let resolved;
576
+ try {
577
+ resolved = await llm.resolveModelInfo(provider.id, model.id, signal);
578
+ } catch {
579
+ resolved = {
580
+ id: model.id,
581
+ name: model.name
582
+ };
583
+ }
584
+ const { efforts, defaultEffort } = effortsOf(resolved);
585
+ push({
586
+ provider: provider.id,
587
+ providerName: provider.name,
588
+ model: model.id,
589
+ modelName: resolved.name || model.name,
590
+ selectable: true,
591
+ source,
592
+ ...oauthSignedIn ? { oauthSignedIn: true } : {},
593
+ routeRegistered: true,
594
+ efforts,
595
+ ...defaultEffort === void 0 ? {} : { defaultEffort }
596
+ });
597
+ }
598
+ }
599
+ for (const id of oauth.ids) {
600
+ const route = OAUTH_ROUTE_BY_STORE_ID[id];
601
+ if (route === void 0) continue;
602
+ if (registered.has(route)) continue;
603
+ const isAntigravity = id === "antigravity" || route === "agy-google-antigravity";
604
+ push({
605
+ provider: route,
606
+ providerName: isAntigravity ? "Google Antigravity" : id,
607
+ model: "*",
608
+ modelName: isAntigravity ? "(install dsh-antigravity-oauth to load this route)" : "(install dsh-oauth-login to load this route)",
609
+ selectable: false,
610
+ source: "oauth",
611
+ oauthSignedIn: true,
612
+ routeRegistered: false,
613
+ efforts: [],
614
+ hint: isAntigravity ? "Signed in at $DSH_HOME/.dsh-antigravity-oauth.json. Install dsh-antigravity-oauth so the route is live." : "Signed in at $DSH_HOME/.dsh-oauth-auth.json. Install https://github.com/aa2246740/dsh-oauth-login so the route is live."
615
+ });
616
+ }
617
+ const selectableCount = routes.filter((route) => route.selectable).length;
618
+ return {
619
+ routes,
620
+ selectableCount,
621
+ oauthPluginPresent,
622
+ oauthStorePresent: oauth.present,
623
+ oauthSignedInProviders: oauth.ids,
624
+ overlayPath: overlay.path,
625
+ overlayMissing: overlay.missing,
626
+ inheritParent: true,
627
+ recommendOauthLogin,
628
+ ...selectableCount === 0 ? { emptyReason: recommendOauthLogin ? "No logged-in API key and no dsh-oauth-login or dsh-antigravity-oauth store. Children inherit this conversation. Add a key in DSH, or install/login https://github.com/aa2246740/dsh-oauth-login / dsh-antigravity-oauth." : "No selectable live route. Children inherit this conversation." } : {}
629
+ };
630
+ }
631
+ //#endregion
632
+ //#region src/persona.ts
633
+ function defaultAgentsRoot() {
634
+ return join(dirname(fileURLToPath(import.meta.url)), "..", "agents");
635
+ }
636
+ function stripFrontmatter(text) {
637
+ if (!text.startsWith("---")) return text;
638
+ const end = text.indexOf("\n---", 3);
639
+ if (end < 0) return text;
640
+ return text.slice(end + 4).replace(/^\s*\n/, "");
641
+ }
642
+ /**
643
+ * Persona for a pstack role. Official spawn persona shadows deployment persona
644
+ * (`SubagentStartRequest.persona`, `packages/subagent/subagent/src/types.ts`).
645
+ */
646
+ async function loadPersona(role, agentsRoot = defaultAgentsRoot()) {
647
+ const key = normalizeRole(role);
648
+ const files = key === "comment-sicko" ? ["comment-sicko.md"] : key === "poteto-agent" ? ["poteto-agent.md"] : [`${key}.md`, "poteto-agent.md"];
649
+ for (const file of files) try {
650
+ const body = stripFrontmatter(await readFile(join(agentsRoot, file), "utf8")).trim();
651
+ if (body.length > 0) return body;
652
+ } catch {
653
+ continue;
654
+ }
655
+ }
656
+ //#endregion
657
+ //#region src/spawn.ts
658
+ function resolveSpawn(overlay, request) {
659
+ const role = normalizeRole(request.role);
660
+ const assignment = resolveRole(overlay, role);
661
+ const inherit = assignment.inherit || assignment.routes.length === 0;
662
+ const index = request.routeIndex ?? 0;
663
+ const route = inherit ? void 0 : assignment.routes[index] ?? assignment.routes[0];
664
+ return {
665
+ role,
666
+ description: request.description,
667
+ prompt: request.prompt,
668
+ runInBackground: request.runInBackground,
669
+ inherit,
670
+ ...route === void 0 ? {} : {
671
+ agentOptions: {
672
+ provider: route.provider,
673
+ model: route.model
674
+ },
675
+ route,
676
+ ...route.reasoningEffort === void 0 ? {} : { reasoningEffort: route.reasoningEffort }
677
+ }
678
+ };
679
+ }
680
+ //#endregion
681
+ //#region src/tools.ts
682
+ function textPrompt(prompt) {
683
+ return [{
684
+ type: "text",
685
+ text: prompt
686
+ }];
687
+ }
688
+ function catalogTool(host) {
689
+ return {
690
+ name: TOOL_CATALOG,
691
+ description: "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.",
692
+ parameters: {},
693
+ async execute(_args, exec) {
694
+ return buildCatalog({
695
+ llm: host.llm,
696
+ credentials: host.credentials,
697
+ settings: host.settings,
698
+ dshHome: host.dshHome,
699
+ env: host.env
700
+ }, exec.signal);
701
+ }
702
+ };
703
+ }
704
+ function overlayReadTool(host) {
705
+ return {
706
+ name: TOOL_OVERLAY_READ,
707
+ description: "Read $DSH_HOME/pstack-dsh.json. Missing file means inherit the parent conversation for every role.",
708
+ parameters: {},
709
+ async execute() {
710
+ const current = await readOverlay(host.dshHome ?? resolveDshHome(host.env));
711
+ return {
712
+ path: current.path,
713
+ missing: current.missing,
714
+ overlay: current.overlay
715
+ };
716
+ }
717
+ };
718
+ }
719
+ function overlayWriteTool(host) {
720
+ return {
721
+ name: TOOL_OVERLAY_WRITE,
722
+ description: "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.",
723
+ parameters: { overlay: {
724
+ type: "json",
725
+ required: true,
726
+ description: "Full overlay object { version: 1, roles: { <role>: { inherit, routes } } }"
727
+ } },
728
+ async execute(args) {
729
+ const overlay = typeof args.overlay === "string" ? parseOverlay(args.overlay) : parseOverlay(JSON.stringify(args.overlay));
730
+ const errors = validateOverlayAgainstCatalog(overlay, (await buildCatalog({
731
+ llm: host.llm,
732
+ credentials: host.credentials,
733
+ settings: host.settings,
734
+ dshHome: host.dshHome,
735
+ env: host.env
736
+ })).routes);
737
+ if (errors.length > 0) throw new Error(`overlay rejected against live catalog:\n${errors.join("\n")}`);
738
+ return {
739
+ path: await writeOverlay(overlay, host.dshHome ?? resolveDshHome(host.env)),
740
+ overlay
741
+ };
742
+ }
743
+ };
744
+ }
745
+ function spawnTool(host) {
746
+ return {
747
+ name: TOOL_SPAWN,
748
+ description: "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.",
749
+ parameters: {
750
+ description: {
751
+ type: "string",
752
+ required: true,
753
+ description: "A short (3-5 word) description of the delegated task, for display."
754
+ },
755
+ prompt: {
756
+ type: "string",
757
+ required: true,
758
+ description: "The complete, self-contained task. The child does not see this conversation."
759
+ },
760
+ role: {
761
+ type: "string",
762
+ required: true,
763
+ description: "pstack role key (feature, how-explainer, poteto-agent, comment-sicko, …)."
764
+ },
765
+ run_in_background: {
766
+ type: "boolean",
767
+ description: "Default true. Continuable children return a subagent id; one-shot background returns a job-like start without waiting."
768
+ },
769
+ route_index: {
770
+ type: "number",
771
+ description: "Which overlay route to use for a panel role. Default 0."
772
+ }
773
+ },
774
+ async execute(args, exec) {
775
+ const parent = exec.agent;
776
+ if (parent === void 0) throw new Error("pstack_spawn requires a calling agent");
777
+ const subagents = host.subagents;
778
+ if (subagents === void 0) throw new Error("pstack_spawn requires ctx.subagents");
779
+ const role = normalizeRole(String(args.role ?? ""));
780
+ if (role === "how-critics") return {
781
+ kind: "disabled",
782
+ role,
783
+ reason: "how-critics was retired in pstack 0.15; no agent was started."
784
+ };
785
+ if (!isPstackRole(role) && role.length === 0) throw new Error("pstack_spawn role is required");
786
+ const overlay = (await readOverlay(host.dshHome ?? resolveDshHome(host.env))).overlay;
787
+ const resolved = resolveSpawn(overlay, {
788
+ role,
789
+ description: String(args.description ?? ""),
790
+ prompt: String(args.prompt ?? ""),
791
+ runInBackground: args.run_in_background !== false,
792
+ routeIndex: typeof args.route_index === "number" ? args.route_index : void 0
793
+ });
794
+ if (resolved.description.trim().length === 0) throw new Error("description is required");
795
+ if (resolved.prompt.trim().length === 0) throw new Error("prompt is required");
796
+ const persona = await loadPersona(resolved.role);
797
+ const providerName = host.spawnProvider ?? "spawn";
798
+ const provider = subagents.getProvider?.(providerName);
799
+ const request = {
800
+ label: resolved.description,
801
+ prompt: textPrompt(resolved.prompt),
802
+ parent,
803
+ signal: exec.signal,
804
+ ...resolved.agentOptions ? { agentOptions: resolved.agentOptions } : {},
805
+ ...persona && provider?.capabilities?.persona !== false ? { persona } : {}
806
+ };
807
+ const remember = (sessionId) => {
808
+ host.roles.remember(sessionId, {
809
+ role: resolved.role,
810
+ ...resolved.reasoningEffort ? { reasoningEffort: resolved.reasoningEffort } : {},
811
+ ...resolved.agentOptions?.provider ? { provider: resolved.agentOptions.provider } : {},
812
+ ...resolved.agentOptions?.model ? { model: resolved.agentOptions.model } : {}
813
+ });
814
+ };
815
+ if (resolved.runInBackground && typeof subagents.startContinuable === "function" && provider?.prepareContinuable !== void 0) {
816
+ const started = await subagents.startContinuable({
817
+ provider: providerName,
818
+ label: resolved.description,
819
+ request,
820
+ signal: exec.signal
821
+ });
822
+ remember(started.childId);
823
+ return {
824
+ kind: "continuable",
825
+ subagentId: started.childId,
826
+ role: resolved.role,
827
+ inherit: resolved.inherit
828
+ };
829
+ }
830
+ const run = await subagents.start(providerName, request);
831
+ remember(run.id);
832
+ if (resolved.runInBackground) return {
833
+ kind: "foreground-detached",
834
+ runId: run.id,
835
+ role: resolved.role,
836
+ inherit: resolved.inherit
837
+ };
838
+ const result = await run.result;
839
+ await Promise.resolve(run.dispose?.());
840
+ return {
841
+ kind: "foreground",
842
+ runId: run.id,
843
+ role: resolved.role,
844
+ inherit: resolved.inherit,
845
+ output: result.output ?? null,
846
+ stopReason: result.stopReason ?? "completed"
847
+ };
848
+ }
849
+ };
850
+ }
851
+ //#endregion
852
+ //#region src/http-trust.ts
853
+ /** Loopback + same-origin. Copied shape from dsh-oauth-login `src/auth-routes.ts`. */
854
+ function trustedRequest(req) {
855
+ const remote = req.socket.remoteAddress;
856
+ if (remote !== "127.0.0.1" && remote !== "::1" && remote !== "::ffff:127.0.0.1") return false;
857
+ if (req.headers["sec-fetch-site"] === "cross-site") return false;
858
+ const host = req.headers.host;
859
+ if (host === void 0) return false;
860
+ const origin = req.headers.origin;
861
+ if (origin === void 0) return true;
862
+ try {
863
+ return new URL(origin).host === new URL(`http://${host}`).host;
864
+ } catch {
865
+ return false;
866
+ }
867
+ }
868
+ function json(res, status, value) {
869
+ res.writeHead(status, {
870
+ "content-type": "application/json; charset=utf-8",
871
+ "cache-control": "no-store",
872
+ "x-content-type-options": "nosniff"
873
+ });
874
+ res.end(JSON.stringify(value));
875
+ }
876
+ async function readJson(req, maxBytes = 262144) {
877
+ const chunks = [];
878
+ let size = 0;
879
+ for await (const chunk of req) {
880
+ size += chunk.length;
881
+ if (size > maxBytes) throw new Error("request body too large");
882
+ chunks.push(chunk);
883
+ }
884
+ if (chunks.length === 0) return {};
885
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
886
+ }
887
+ //#endregion
888
+ //#region src/settings-draft.ts
889
+ function liveFor(routes, provider, model) {
890
+ return routes.find((route) => route.selectable && route.provider === provider && route.model === model);
891
+ }
892
+ function stripIllegalEffort(route, live) {
893
+ const found = liveFor(live, route.provider, route.model);
894
+ if (found === void 0) return {
895
+ provider: route.provider,
896
+ model: route.model
897
+ };
898
+ if (route.reasoningEffort === void 0) return {
899
+ provider: route.provider,
900
+ model: route.model
901
+ };
902
+ if (found.efforts.length === 0) return {
903
+ provider: route.provider,
904
+ model: route.model
905
+ };
906
+ if (!found.efforts.some((effort) => effort.id === route.reasoningEffort)) return {
907
+ provider: route.provider,
908
+ model: route.model
909
+ };
910
+ return {
911
+ provider: route.provider,
912
+ model: route.model,
913
+ reasoningEffort: route.reasoningEffort
914
+ };
915
+ }
916
+ /** Stale overlay entries become inherit in the returned overlay; original file is not written. */
917
+ function dropUnselectableRoles(overlay, live) {
918
+ const next = emptyOverlay();
919
+ const droppedRoles = [];
920
+ const selectable = new Set(live.filter((route) => route.selectable).map((route) => routeKey(route.provider, route.model)));
921
+ for (const role of ALL_ROLES) {
922
+ const assignment = overlay.roles[role] ?? {
923
+ inherit: true,
924
+ routes: []
925
+ };
926
+ if (role === "how-critics") {
927
+ next.roles[role] = {
928
+ inherit: assignment.inherit,
929
+ routes: assignment.routes.map((route) => ({ ...route }))
930
+ };
931
+ continue;
932
+ }
933
+ if (assignment.inherit || assignment.routes.length === 0) {
934
+ next.roles[role] = {
935
+ inherit: true,
936
+ routes: []
937
+ };
938
+ continue;
939
+ }
940
+ const kept = assignment.routes.filter((route) => selectable.has(routeKey(route.provider, route.model))).map((route) => stripIllegalEffort(route, live));
941
+ if (kept.length === 0) {
942
+ droppedRoles.push(role);
943
+ next.roles[role] = {
944
+ inherit: true,
945
+ routes: []
946
+ };
947
+ continue;
948
+ }
949
+ if (kept.length !== assignment.routes.length) droppedRoles.push(role);
950
+ next.roles[role] = {
951
+ inherit: false,
952
+ routes: isPanelRole(role) ? kept : [kept[0]]
953
+ };
954
+ }
955
+ return {
956
+ overlay: next,
957
+ droppedRoles
958
+ };
959
+ }
960
+ //#endregion
961
+ //#region src/settings-api.ts
962
+ async function loadSettingsSnapshot(host) {
963
+ const dshHome = host.dshHome ?? resolveDshHome(host.env);
964
+ const catalog = await buildCatalog({
965
+ ...host,
966
+ dshHome
967
+ });
968
+ const current = await readOverlay(dshHome);
969
+ const { overlay, droppedRoles } = dropUnselectableRoles(current.overlay, catalog.routes);
970
+ return {
971
+ catalog,
972
+ overlay,
973
+ path: current.path,
974
+ missing: current.missing,
975
+ droppedRoles
976
+ };
977
+ }
978
+ async function saveSettingsOverlay(host, raw) {
979
+ const dshHome = host.dshHome ?? resolveDshHome(host.env);
980
+ const overlay = typeof raw === "string" ? parseOverlay(raw) : parseOverlay(JSON.stringify(raw));
981
+ const errors = validateOverlayAgainstCatalog(overlay, (await buildCatalog({
982
+ ...host,
983
+ dshHome
984
+ })).routes);
985
+ if (errors.length > 0) {
986
+ const error = /* @__PURE__ */ new Error(`overlay rejected against live catalog:\n${errors.join("\n")}`);
987
+ error.errors = errors;
988
+ throw error;
989
+ }
990
+ return {
991
+ path: await writeOverlay(overlay, dshHome),
992
+ overlay
993
+ };
994
+ }
995
+ //#endregion
996
+ //#region src/settings-routes.ts
997
+ function isRecord(value) {
998
+ return typeof value === "object" && value !== null && !Array.isArray(value);
999
+ }
1000
+ async function handleSnapshot(host, req, res) {
1001
+ if (req.method !== "GET") {
1002
+ json(res, 405, { error: "method not allowed" });
1003
+ return;
1004
+ }
1005
+ json(res, 200, await loadSettingsSnapshot(host));
1006
+ }
1007
+ async function handleSave(host, req, res) {
1008
+ if (req.method !== "PUT" && req.method !== "POST") {
1009
+ json(res, 405, { error: "method not allowed" });
1010
+ return;
1011
+ }
1012
+ try {
1013
+ const body = await readJson(req);
1014
+ json(res, 200, await saveSettingsOverlay(host, isRecord(body) && "overlay" in body ? body.overlay : body));
1015
+ } catch (error) {
1016
+ const message = error instanceof Error ? error.message : "save failed";
1017
+ const errors = error instanceof Error && "errors" in error && Array.isArray(error.errors) ? error.errors : void 0;
1018
+ json(res, 400, {
1019
+ error: message,
1020
+ ...errors === void 0 ? {} : { errors }
1021
+ });
1022
+ }
1023
+ }
1024
+ function registerPstackSettingsRoutes(ctx, host) {
1025
+ const webServer = ctx.webServer;
1026
+ if (webServer === void 0) return;
1027
+ ctx.effect(() => {
1028
+ const routes = [webServer.register({
1029
+ kind: "exact",
1030
+ path: SETTINGS_SNAPSHOT_PATH,
1031
+ handler: async (req, res) => {
1032
+ const rejection = ctx.connection.requestRejection(req);
1033
+ if (rejection !== void 0) {
1034
+ json(res, rejection, { error: rejection === 401 ? "unauthorized" : "forbidden" });
1035
+ return;
1036
+ }
1037
+ if (!trustedRequest(req)) {
1038
+ json(res, 403, { error: "forbidden" });
1039
+ return;
1040
+ }
1041
+ if (req.method === "GET") {
1042
+ await handleSnapshot(host, req, res);
1043
+ return;
1044
+ }
1045
+ await handleSave(host, req, res);
1046
+ }
1047
+ })];
1048
+ return () => {
1049
+ for (const dispose of routes) dispose();
1050
+ };
1051
+ }, "pstack-dsh: Settings overlay routes");
1052
+ }
1053
+ //#endregion
1054
+ //#region src/index.ts
1055
+ const name = PLUGIN_ID;
1056
+ const inject = ["tools", "skills"];
1057
+ const JSON_OUTPUT = {
1058
+ schema: { type: "json" },
1059
+ render: (_args, value) => [{
1060
+ type: "text",
1061
+ text: JSON.stringify(value, null, 2)
1062
+ }]
1063
+ };
1064
+ function asHost(ctx, config, roles) {
1065
+ return {
1066
+ get llm() {
1067
+ return ctx.get("llm");
1068
+ },
1069
+ get credentials() {
1070
+ return ctx.get("credentials");
1071
+ },
1072
+ get settings() {
1073
+ return ctx.get("settings");
1074
+ },
1075
+ get subagents() {
1076
+ return ctx.get("subagents");
1077
+ },
1078
+ spawnProvider: config?.spawnProvider ?? "spawn",
1079
+ roles
1080
+ };
1081
+ }
1082
+ function registerOne(ctx, tool) {
1083
+ ctx.tools.register(defineTool({
1084
+ name: tool.name,
1085
+ description: tool.description,
1086
+ parameters: tool.parameters,
1087
+ output: JSON_OUTPUT,
1088
+ async execute(args, exec) {
1089
+ const snapshot = snapshotJsonValue(await tool.execute(args, exec));
1090
+ if (snapshot === void 0) throw new TypeError(`pstack tool ${tool.name} returned a non-JSON value`);
1091
+ return snapshot;
1092
+ }
1093
+ }));
1094
+ }
1095
+ function apply(ctx, config) {
1096
+ console.log("[my-plugins/pstack-dsh] loaded");
1097
+ const roles = new RoleEffortMap();
1098
+ const host = asHost(ctx, config, roles);
1099
+ registerOne(ctx, catalogTool(host));
1100
+ registerOne(ctx, overlayReadTool(host));
1101
+ registerOne(ctx, overlayWriteTool(host));
1102
+ registerOne(ctx, spawnTool(host));
1103
+ ctx.skills.registerProvider(() => createSkillProvider());
1104
+ ctx.inject(["webServer", "connection"], (webCtx) => {
1105
+ registerPstackSettingsRoutes(webCtx, host);
1106
+ });
1107
+ ctx.on("agent/request", async (payload, next) => {
1108
+ const base = await next();
1109
+ const binding = roles.lookup(payload.agent.id);
1110
+ if (binding === void 0) return base;
1111
+ return {
1112
+ ...base,
1113
+ ...binding.provider ? { provider: binding.provider } : {},
1114
+ ...binding.model ? { model: binding.model } : {},
1115
+ ...binding.reasoningEffort ? { reasoningEffort: ReasoningEffortId(binding.reasoningEffort) } : {}
1116
+ };
1117
+ });
1118
+ }
1119
+ //#endregion
1120
+ export { Config, apply, inject, name };