@cratis/pi 0.0.1 → 2.0.2

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 (234) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -37
  3. package/package/corpus/agents/backend-developer.md +125 -0
  4. package/package/corpus/agents/code-reviewer.md +165 -0
  5. package/package/corpus/agents/coordinator.md +163 -0
  6. package/package/corpus/agents/frontend-developer.md +246 -0
  7. package/package/corpus/agents/orchestrator.md +196 -0
  8. package/package/corpus/agents/performance-reviewer.md +109 -0
  9. package/package/corpus/agents/planner.md +145 -0
  10. package/package/corpus/agents/repository-investigation-reviewer.md +45 -0
  11. package/package/corpus/agents/repository-investigator.md +50 -0
  12. package/package/corpus/agents/security-reviewer.md +118 -0
  13. package/package/corpus/agents/slice-implementer.md +59 -0
  14. package/package/corpus/agents/spec-writer.md +149 -0
  15. package/package/corpus/harnesses/pi/extensions/cratis-hooks/index.ts +213 -0
  16. package/package/corpus/harnesses/pi/extensions/cratis-rules/index.ts +27 -0
  17. package/package/corpus/harnesses/pi/extensions/package.json +4 -0
  18. package/package/corpus/harnesses/pi/extensions/subagent/agents.ts +167 -0
  19. package/package/corpus/harnesses/pi/extensions/subagent/index.ts +352 -0
  20. package/package/corpus/hooks/README.md +434 -0
  21. package/package/corpus/hooks/agent-stop.md +49 -0
  22. package/package/corpus/hooks/pre-commit.md +47 -0
  23. package/package/corpus/hooks/scripts/cratis-guard-writes.sh +87 -0
  24. package/package/corpus/hooks/scripts/cratis-nuget-pins.txt +11 -0
  25. package/package/corpus/hooks/scripts/cratis-pattern-scan.sh +197 -0
  26. package/package/corpus/hooks/scripts/cratis-patterns.json +84 -0
  27. package/package/corpus/hooks/scripts/cratis-quality-gate.sh +219 -0
  28. package/package/corpus/hooks/scripts/hook-lib.sh +152 -0
  29. package/package/corpus/hooks/scripts/quality-gates.json +261 -0
  30. package/package/corpus/hooks/scripts/type-references-allowlist.txt +71 -0
  31. package/package/corpus/hooks/scripts/validate-package-imports.sh +166 -0
  32. package/package/corpus/hooks/scripts/validate-package-subpaths.sh +120 -0
  33. package/package/corpus/hooks/scripts/validate-type-references.sh +308 -0
  34. package/package/corpus/hooks/settings.template.json +40 -0
  35. package/package/corpus/prompts/add-business-rule.prompt.md +22 -0
  36. package/package/corpus/prompts/add-concept.prompt.md +17 -0
  37. package/package/corpus/prompts/add-ef-migration.prompt.md +24 -0
  38. package/package/corpus/prompts/add-projection.prompt.md +20 -0
  39. package/package/corpus/prompts/add-reactor.prompt.md +22 -0
  40. package/package/corpus/prompts/add-reducer.prompt.md +20 -0
  41. package/package/corpus/prompts/audit-hooks.prompt.md +15 -0
  42. package/package/corpus/prompts/check-doc-drift.prompt.md +21 -0
  43. package/package/corpus/prompts/code-review.prompt.md +9 -0
  44. package/package/corpus/prompts/new-feature.prompt.md +9 -0
  45. package/package/corpus/prompts/new-vertical-slice.prompt.md +18 -0
  46. package/package/corpus/prompts/review-pr.prompt.md +35 -0
  47. package/package/corpus/prompts/review-skill.prompt.md +16 -0
  48. package/package/corpus/prompts/scaffold-feature.prompt.md +16 -0
  49. package/package/corpus/prompts/ship-changes.prompt.md +20 -0
  50. package/package/corpus/prompts/verify-ai-setup.prompt.md +19 -0
  51. package/package/corpus/prompts/write-documentation.prompt.md +21 -0
  52. package/package/corpus/prompts/write-specs.prompt.md +22 -0
  53. package/package/corpus/rules/capability-is-not-authority.md +31 -0
  54. package/package/corpus/rules/code-quality.csharp.md +91 -0
  55. package/package/corpus/rules/code-quality.md +82 -0
  56. package/package/corpus/rules/code-quality.typescript.md +89 -0
  57. package/package/corpus/rules/components.md +207 -0
  58. package/package/corpus/rules/concepts.md +115 -0
  59. package/package/corpus/rules/csharp.md +269 -0
  60. package/package/corpus/rules/dialogs.md +264 -0
  61. package/package/corpus/rules/documentation-structure-and-formatting.md +148 -0
  62. package/package/corpus/rules/documentation.md +90 -0
  63. package/package/corpus/rules/editing-cratis-docs.md +69 -0
  64. package/package/corpus/rules/efcore.md +235 -0
  65. package/package/corpus/rules/efcore.specs.md +44 -0
  66. package/package/corpus/rules/exit-codes-and-wrappers.md +33 -0
  67. package/package/corpus/rules/framework.md +52 -0
  68. package/package/corpus/rules/frontend-quality.md +59 -0
  69. package/package/corpus/rules/frontend-testing.md +126 -0
  70. package/package/corpus/rules/general.md +305 -0
  71. package/package/corpus/rules/git-commits.md +138 -0
  72. package/package/corpus/rules/github-actions.md +92 -0
  73. package/package/corpus/rules/glossary.md +61 -0
  74. package/package/corpus/rules/guards-and-fuses.md +45 -0
  75. package/package/corpus/rules/local-work-artifacts.md +33 -0
  76. package/package/corpus/rules/managing-ai-rules.md +40 -0
  77. package/package/corpus/rules/orleans.md +50 -0
  78. package/package/corpus/rules/pull-requests.md +78 -0
  79. package/package/corpus/rules/react.md +195 -0
  80. package/package/corpus/rules/reactors.md +238 -0
  81. package/package/corpus/rules/rtk.md +37 -0
  82. package/package/corpus/rules/specs.csharp.md +139 -0
  83. package/package/corpus/rules/specs.md +132 -0
  84. package/package/corpus/rules/specs.scenarios.csharp.md +172 -0
  85. package/package/corpus/rules/specs.typescript.md +139 -0
  86. package/package/corpus/rules/storybook.md +84 -0
  87. package/package/corpus/rules/terminal-commands.md +19 -0
  88. package/package/corpus/rules/typescript.md +149 -0
  89. package/package/corpus/rules/verification-discipline.md +21 -0
  90. package/package/corpus/rules/vertical-slices.md +338 -0
  91. package/package/corpus/rules/web-fetching.md +11 -0
  92. package/package/corpus/rules/writing-correct-examples.md +35 -0
  93. package/package/corpus/rules/writing-cratis-docs.md +70 -0
  94. package/package/corpus/skills/cratis-engineering-csharp-conventions/LICENSE +2 -0
  95. package/package/corpus/skills/cratis-engineering-csharp-conventions/SKILL.md +131 -0
  96. package/package/corpus/skills/cratis-engineering-csharp-conventions/references/code-style.md +187 -0
  97. package/package/corpus/skills/cratis-engineering-csharp-conventions/references/domain-philosophy.md +91 -0
  98. package/package/corpus/skills/cratis-engineering-csharp-conventions/references/exceptions-logging-and-di.md +223 -0
  99. package/package/corpus/skills/cratis-engineering-decision-record/LICENSE +2 -0
  100. package/package/corpus/skills/cratis-engineering-decision-record/SKILL.md +133 -0
  101. package/package/corpus/skills/cratis-engineering-decision-record/references/record-format.md +107 -0
  102. package/package/corpus/skills/cratis-engineering-docs-authoring/LICENSE +2 -0
  103. package/package/corpus/skills/cratis-engineering-docs-authoring/SKILL.md +86 -0
  104. package/package/corpus/skills/cratis-engineering-docs-authoring/references/site-format.md +46 -0
  105. package/package/corpus/skills/cratis-engineering-effect-boundaries/LICENSE +2 -0
  106. package/package/corpus/skills/cratis-engineering-effect-boundaries/SKILL.md +130 -0
  107. package/package/corpus/skills/cratis-engineering-effect-boundaries/references/failure-archetypes.md +133 -0
  108. package/package/corpus/skills/cratis-fundamentals-concept/verification.json +8 -0
  109. package/package/profile-catalog.json +821 -0
  110. package/package.json +35 -13
  111. package/src/index.ts +80 -0
  112. /package/{skills → package/corpus/skills}/cratis-application-react-specifications/LICENSE +0 -0
  113. /package/{skills → package/corpus/skills}/cratis-application-react-specifications/SKILL.md +0 -0
  114. /package/{skills → package/corpus/skills}/cratis-application-slice-specifications/LICENSE +0 -0
  115. /package/{skills → package/corpus/skills}/cratis-application-slice-specifications/SKILL.md +0 -0
  116. /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/LICENSE +0 -0
  117. /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/SKILL.md +0 -0
  118. /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/authentication.md +0 -0
  119. /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/authorization.md +0 -0
  120. /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/frontend.md +0 -0
  121. /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/local-development.md +0 -0
  122. /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +0 -0
  123. /package/{skills → package/corpus/skills}/cratis-arc-command/LICENSE +0 -0
  124. /package/{skills → package/corpus/skills}/cratis-arc-command/SKILL.md +0 -0
  125. /package/{skills → package/corpus/skills}/cratis-arc-command/references/command-result.md +0 -0
  126. /package/{skills → package/corpus/skills}/cratis-arc-command/references/handler-shapes.md +0 -0
  127. /package/{skills → package/corpus/skills}/cratis-arc-command/references/proxy-generation.md +0 -0
  128. /package/{skills → package/corpus/skills}/cratis-arc-command/references/read-model-injection.md +0 -0
  129. /package/{skills → package/corpus/skills}/cratis-arc-command-execution/LICENSE +0 -0
  130. /package/{skills → package/corpus/skills}/cratis-arc-command-execution/SKILL.md +0 -0
  131. /package/{skills → package/corpus/skills}/cratis-arc-command-validation/LICENSE +0 -0
  132. /package/{skills → package/corpus/skills}/cratis-arc-command-validation/SKILL.md +0 -0
  133. /package/{skills → package/corpus/skills}/cratis-arc-ef-core-migration/LICENSE +0 -0
  134. /package/{skills → package/corpus/skills}/cratis-arc-ef-core-migration/SKILL.md +0 -0
  135. /package/{skills → package/corpus/skills}/cratis-arc-observable-query-http/LICENSE +0 -0
  136. /package/{skills → package/corpus/skills}/cratis-arc-observable-query-http/SKILL.md +0 -0
  137. /package/{skills → package/corpus/skills}/cratis-arc-query-paging/LICENSE +0 -0
  138. /package/{skills → package/corpus/skills}/cratis-arc-query-paging/SKILL.md +0 -0
  139. /package/{skills → package/corpus/skills}/cratis-arc-react-page/LICENSE +0 -0
  140. /package/{skills → package/corpus/skills}/cratis-arc-react-page/SKILL.md +0 -0
  141. /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/data-page.md +0 -0
  142. /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/data-tables.md +0 -0
  143. /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/dialogs.md +0 -0
  144. /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/mvvm.md +0 -0
  145. /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/queries-and-commands.md +0 -0
  146. /package/{skills → package/corpus/skills}/cratis-chronicle-cli-operations/LICENSE +0 -0
  147. /package/{skills → package/corpus/skills}/cratis-chronicle-cli-operations/SKILL.md +0 -0
  148. /package/{skills → package/corpus/skills}/cratis-chronicle-client-dotnet/LICENSE +0 -0
  149. /package/{skills → package/corpus/skills}/cratis-chronicle-client-dotnet/SKILL.md +0 -0
  150. /package/{skills → package/corpus/skills}/cratis-chronicle-client-elixir/LICENSE +0 -0
  151. /package/{skills → package/corpus/skills}/cratis-chronicle-client-elixir/SKILL.md +0 -0
  152. /package/{skills → package/corpus/skills}/cratis-chronicle-client-kotlin/LICENSE +0 -0
  153. /package/{skills → package/corpus/skills}/cratis-chronicle-client-kotlin/SKILL.md +0 -0
  154. /package/{skills → package/corpus/skills}/cratis-chronicle-client-typescript/LICENSE +0 -0
  155. /package/{skills → package/corpus/skills}/cratis-chronicle-client-typescript/SKILL.md +0 -0
  156. /package/{skills → package/corpus/skills}/cratis-chronicle-compliance/LICENSE +0 -0
  157. /package/{skills → package/corpus/skills}/cratis-chronicle-compliance/SKILL.md +0 -0
  158. /package/{skills → package/corpus/skills}/cratis-chronicle-event-constraints/LICENSE +0 -0
  159. /package/{skills → package/corpus/skills}/cratis-chronicle-event-constraints/SKILL.md +0 -0
  160. /package/{skills → package/corpus/skills}/cratis-chronicle-event-modeling/LICENSE +0 -0
  161. /package/{skills → package/corpus/skills}/cratis-chronicle-event-modeling/SKILL.md +0 -0
  162. /package/{skills → package/corpus/skills}/cratis-chronicle-event-specifications/LICENSE +0 -0
  163. /package/{skills → package/corpus/skills}/cratis-chronicle-event-specifications/SKILL.md +0 -0
  164. /package/{skills → package/corpus/skills}/cratis-chronicle-event-type-migration/LICENSE +0 -0
  165. /package/{skills → package/corpus/skills}/cratis-chronicle-event-type-migration/SKILL.md +0 -0
  166. /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/LICENSE +0 -0
  167. /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/SKILL.md +0 -0
  168. /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/references/blocked-tools.md +0 -0
  169. /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/references/observational-tools.md +0 -0
  170. /package/{skills → package/corpus/skills}/cratis-chronicle-multi-tenancy/LICENSE +0 -0
  171. /package/{skills → package/corpus/skills}/cratis-chronicle-multi-tenancy/SKILL.md +0 -0
  172. /package/{skills → package/corpus/skills}/cratis-chronicle-projection/LICENSE +0 -0
  173. /package/{skills → package/corpus/skills}/cratis-chronicle-projection/SKILL.md +0 -0
  174. /package/{skills → package/corpus/skills}/cratis-chronicle-projection/references/fluent-builder.md +0 -0
  175. /package/{skills → package/corpus/skills}/cratis-chronicle-projection/references/model-bound-attributes.md +0 -0
  176. /package/{skills → package/corpus/skills}/cratis-chronicle-reactor/LICENSE +0 -0
  177. /package/{skills → package/corpus/skills}/cratis-chronicle-reactor/SKILL.md +0 -0
  178. /package/{skills → package/corpus/skills}/cratis-chronicle-read-model/LICENSE +0 -0
  179. /package/{skills → package/corpus/skills}/cratis-chronicle-read-model/SKILL.md +0 -0
  180. /package/{skills → package/corpus/skills}/cratis-chronicle-read-model/references/queries.md +0 -0
  181. /package/{skills → package/corpus/skills}/cratis-chronicle-read-model-specifications/LICENSE +0 -0
  182. /package/{skills → package/corpus/skills}/cratis-chronicle-read-model-specifications/SKILL.md +0 -0
  183. /package/{skills → package/corpus/skills}/cratis-chronicle-reducer/LICENSE +0 -0
  184. /package/{skills → package/corpus/skills}/cratis-chronicle-reducer/SKILL.md +0 -0
  185. /package/{skills → package/corpus/skills}/cratis-chronicle-web-workbench/LICENSE +0 -0
  186. /package/{skills → package/corpus/skills}/cratis-chronicle-web-workbench/SKILL.md +0 -0
  187. /package/{skills → package/corpus/skills}/cratis-cli-terminal-workbench/LICENSE +0 -0
  188. /package/{skills → package/corpus/skills}/cratis-cli-terminal-workbench/SKILL.md +0 -0
  189. /package/{skills → package/corpus/skills}/cratis-code-review/LICENSE +0 -0
  190. /package/{skills → package/corpus/skills}/cratis-code-review/SKILL.md +0 -0
  191. /package/{skills → package/corpus/skills}/cratis-components-accessibility/LICENSE +0 -0
  192. /package/{skills → package/corpus/skills}/cratis-components-accessibility/SKILL.md +0 -0
  193. /package/{skills → package/corpus/skills}/cratis-components-schema-editor/LICENSE +0 -0
  194. /package/{skills → package/corpus/skills}/cratis-components-schema-editor/SKILL.md +0 -0
  195. /package/{skills → package/corpus/skills}/cratis-components-stepper-command-dialog/LICENSE +0 -0
  196. /package/{skills → package/corpus/skills}/cratis-components-stepper-command-dialog/SKILL.md +0 -0
  197. /package/{skills → package/corpus/skills}/cratis-components-styling/LICENSE +0 -0
  198. /package/{skills → package/corpus/skills}/cratis-components-styling/SKILL.md +0 -0
  199. /package/{skills → package/corpus/skills}/cratis-components-toolbar/LICENSE +0 -0
  200. /package/{skills → package/corpus/skills}/cratis-components-toolbar/SKILL.md +0 -0
  201. /package/{skills → package/corpus/skills}/cratis-documentation-writing/LICENSE +0 -0
  202. /package/{skills → package/corpus/skills}/cratis-documentation-writing/SKILL.md +0 -0
  203. /package/{skills → package/corpus/skills}/cratis-event-model-diagram/LICENSE +0 -0
  204. /package/{skills → package/corpus/skills}/cratis-event-model-diagram/SKILL.md +0 -0
  205. /package/{skills → package/corpus/skills}/cratis-fundamentals-concept/LICENSE +0 -0
  206. /package/{skills → package/corpus/skills}/cratis-fundamentals-concept/SKILL.md +0 -0
  207. /package/{skills → package/corpus/skills}/cratis-fundamentals-type-discovery/LICENSE +0 -0
  208. /package/{skills → package/corpus/skills}/cratis-fundamentals-type-discovery/SKILL.md +0 -0
  209. /package/{skills → package/corpus/skills}/cratis-governed-release-methodology/LICENSE +0 -0
  210. /package/{skills → package/corpus/skills}/cratis-governed-release-methodology/SKILL.md +0 -0
  211. /package/{skills → package/corpus/skills}/cratis-lens-browser-extension/LICENSE +0 -0
  212. /package/{skills → package/corpus/skills}/cratis-lens-browser-extension/SKILL.md +0 -0
  213. /package/{skills → package/corpus/skills}/cratis-performance-review/LICENSE +0 -0
  214. /package/{skills → package/corpus/skills}/cratis-performance-review/SKILL.md +0 -0
  215. /package/{skills → package/corpus/skills}/cratis-screenplay-model-authoring/LICENSE +0 -0
  216. /package/{skills → package/corpus/skills}/cratis-screenplay-model-authoring/SKILL.md +0 -0
  217. /package/{skills → package/corpus/skills}/cratis-security-review/LICENSE +0 -0
  218. /package/{skills → package/corpus/skills}/cratis-security-review/SKILL.md +0 -0
  219. /package/{skills → package/corpus/skills}/cratis-specification-by-example/LICENSE +0 -0
  220. /package/{skills → package/corpus/skills}/cratis-specification-by-example/SKILL.md +0 -0
  221. /package/{skills → package/corpus/skills}/cratis-specifications-csharp/LICENSE +0 -0
  222. /package/{skills → package/corpus/skills}/cratis-specifications-csharp/SKILL.md +0 -0
  223. /package/{skills → package/corpus/skills}/cratis-specifications-csharp/references/application-scenarios.md +0 -0
  224. /package/{skills → package/corpus/skills}/cratis-specifications-csharp/references/csharp-patterns.md +0 -0
  225. /package/{skills → package/corpus/skills}/cratis-specifications-csharp/references/integration-specs.md +0 -0
  226. /package/{skills → package/corpus/skills}/cratis-specifications-typescript/LICENSE +0 -0
  227. /package/{skills → package/corpus/skills}/cratis-specifications-typescript/SKILL.md +0 -0
  228. /package/{skills → package/corpus/skills}/cratis-specifications-typescript/references/typescript-patterns.md +0 -0
  229. /package/{skills → package/corpus/skills}/cratis-stage-rendering-and-sandbox/LICENSE +0 -0
  230. /package/{skills → package/corpus/skills}/cratis-stage-rendering-and-sandbox/SKILL.md +0 -0
  231. /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/LICENSE +0 -0
  232. /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/SKILL.md +0 -0
  233. /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/references/blocked-tools.md +0 -0
  234. /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/references/observational-tools.md +0 -0
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Cratis Subagent tool for the Pi coding agent.
3
+ *
4
+ * Pi ships no built-in subagents by design — they are an extension pattern. This tool
5
+ * delegates a task to one of the corpus agents (defined once in `.cratis/ai/agents/*.md`, surfaced
6
+ * to Pi via `.pi/agents/*.md` symlinks) by spawning a fresh `pi` subprocess per agent, giving
7
+ * each an isolated context window. It mirrors Pi's official subagent example, trimmed to the
8
+ * essentials and taught to consume the corpus's Claude-shaped agent files through
9
+ * `./agents.ts` (which normalizes tool names and keeps model ids as-is).
10
+ *
11
+ * Modes:
12
+ * - single: { agent, task }
13
+ * - parallel: { tasks: [{ agent, task }, ...] } (max 8, 4 concurrent)
14
+ * - chain: { chain: [{ agent, task }, ...] } (sequential, {previous} placeholder)
15
+ */
16
+
17
+ import { spawn } from "node:child_process";
18
+ import * as fs from "node:fs";
19
+ import * as os from "node:os";
20
+ import * as path from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import { CONFIG_DIR_NAME, type ExtensionAPI, getAgentDir } from "@earendil-works/pi-coding-agent";
23
+ import { StringEnum } from "@earendil-works/pi-ai";
24
+ import { Type } from "typebox";
25
+ import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.ts";
26
+
27
+ const extensionPath = fileURLToPath(import.meta.url);
28
+ const isPackagedExtension = extensionPath.includes(`${path.sep}package${path.sep}corpus${path.sep}`);
29
+ const MAX_PARALLEL_TASKS = 8;
30
+ const MAX_CONCURRENCY = 4;
31
+ const PER_TASK_OUTPUT_CAP = 50 * 1024;
32
+
33
+ interface SingleResult {
34
+ agent: string;
35
+ agentSource: "package" | "user" | "project" | "unknown";
36
+ task: string;
37
+ exitCode: number;
38
+ finalText: string;
39
+ stderr: string;
40
+ model?: string;
41
+ stopReason?: string;
42
+ errorMessage?: string;
43
+ step?: number;
44
+ }
45
+
46
+ function isFailed(r: SingleResult): boolean {
47
+ return r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
48
+ }
49
+
50
+ function resultOutput(r: SingleResult): string {
51
+ if (isFailed(r)) return r.errorMessage || r.stderr || r.finalText || "(no output)";
52
+ return r.finalText || "(no output)";
53
+ }
54
+
55
+ function capOutput(output: string): string {
56
+ if (Buffer.byteLength(output, "utf8") <= PER_TASK_OUTPUT_CAP) return output;
57
+ let truncated = output.slice(0, PER_TASK_OUTPUT_CAP);
58
+ while (Buffer.byteLength(truncated, "utf8") > PER_TASK_OUTPUT_CAP) truncated = truncated.slice(0, -1);
59
+ return `${truncated}\n\n[Output truncated. Full output preserved in tool details.]`;
60
+ }
61
+
62
+ /** Resolve how to re-invoke this same Pi build for a child process. */
63
+ function piInvocation(args: string[]): { command: string; args: string[] } {
64
+ const currentScript = process.argv[1];
65
+ const isBunVirtual = currentScript?.startsWith("/$bunfs/root/");
66
+ if (currentScript && !isBunVirtual && fs.existsSync(currentScript)) {
67
+ return { command: process.execPath, args: [currentScript, ...args] };
68
+ }
69
+ const execName = path.basename(process.execPath).toLowerCase();
70
+ if (!/^(node|bun)(\.exe)?$/.test(execName)) return { command: process.execPath, args };
71
+ return { command: "pi", args };
72
+ }
73
+
74
+ async function runSingleAgent(
75
+ defaultCwd: string,
76
+ dispatch: { model?: string; thinkingLevel?: string },
77
+ agents: AgentConfig[],
78
+ agentName: string,
79
+ task: string,
80
+ cwd: string | undefined,
81
+ step: number | undefined,
82
+ signal: AbortSignal | undefined,
83
+ onText: ((text: string) => void) | undefined,
84
+ ): Promise<SingleResult> {
85
+ const agent = agents.find((a) => a.name === agentName);
86
+ if (!agent) {
87
+ const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
88
+ return {
89
+ agent: agentName,
90
+ agentSource: "unknown",
91
+ task,
92
+ exitCode: 1,
93
+ finalText: "",
94
+ stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
95
+ step,
96
+ };
97
+ }
98
+
99
+ // An agent that pins no model inherits the dispatching session's model + thinking level.
100
+ const model = agent.model ?? dispatch.model;
101
+ const args: string[] = ["--mode", "json", "-p", "--no-session"];
102
+ if (model) args.push("--model", model);
103
+ if (!agent.model && dispatch.thinkingLevel) args.push("--thinking", dispatch.thinkingLevel);
104
+ if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
105
+
106
+ const result: SingleResult = {
107
+ agent: agentName,
108
+ agentSource: agent.source,
109
+ task,
110
+ exitCode: 0,
111
+ finalText: "",
112
+ stderr: "",
113
+ model,
114
+ step,
115
+ };
116
+
117
+ let tmpDir: string | null = null;
118
+ let tmpFile: string | null = null;
119
+ try {
120
+ if (agent.systemPrompt.trim()) {
121
+ tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-subagent-"));
122
+ tmpFile = path.join(tmpDir, `prompt-${agent.name.replace(/[^\w.-]+/g, "_")}.md`);
123
+ await fs.promises.writeFile(tmpFile, agent.systemPrompt, { encoding: "utf-8", mode: 0o600 });
124
+ args.push("--append-system-prompt", tmpFile);
125
+ }
126
+ args.push(`Task: ${task}`);
127
+
128
+ let aborted = false;
129
+ const exitCode = await new Promise<number>((resolve) => {
130
+ const inv = piInvocation(args);
131
+ const proc = spawn(inv.command, inv.args, {
132
+ cwd: cwd ?? defaultCwd,
133
+ shell: false,
134
+ stdio: ["ignore", "pipe", "pipe"],
135
+ });
136
+ let buffer = "";
137
+ const processLine = (line: string) => {
138
+ if (!line.trim()) return;
139
+ let event: any;
140
+ try {
141
+ event = JSON.parse(line);
142
+ } catch {
143
+ return;
144
+ }
145
+ if (event.type === "message_end" && event.message?.role === "assistant") {
146
+ const msg = event.message;
147
+ for (const part of msg.content ?? []) {
148
+ if (part.type === "text" && typeof part.text === "string") {
149
+ result.finalText = part.text;
150
+ onText?.(part.text);
151
+ }
152
+ }
153
+ if (!result.model && msg.model) result.model = msg.model;
154
+ if (msg.stopReason) result.stopReason = msg.stopReason;
155
+ if (msg.errorMessage) result.errorMessage = msg.errorMessage;
156
+ }
157
+ };
158
+ proc.stdout.on("data", (data) => {
159
+ buffer += data.toString();
160
+ const lines = buffer.split("\n");
161
+ buffer = lines.pop() || "";
162
+ for (const line of lines) processLine(line);
163
+ });
164
+ proc.stderr.on("data", (data) => {
165
+ result.stderr += data.toString();
166
+ });
167
+ proc.on("close", (code) => {
168
+ if (buffer.trim()) processLine(buffer);
169
+ resolve(code ?? 0);
170
+ });
171
+ proc.on("error", () => resolve(1));
172
+ if (signal) {
173
+ const kill = () => {
174
+ aborted = true;
175
+ proc.kill("SIGTERM");
176
+ setTimeout(() => {
177
+ if (!proc.killed) proc.kill("SIGKILL");
178
+ }, 5000);
179
+ };
180
+ if (signal.aborted) kill();
181
+ else signal.addEventListener("abort", kill, { once: true });
182
+ }
183
+ });
184
+
185
+ result.exitCode = exitCode;
186
+ if (aborted) result.stopReason = "aborted";
187
+ return result;
188
+ } finally {
189
+ if (tmpFile) try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
190
+ if (tmpDir) try { fs.rmdirSync(tmpDir); } catch { /* ignore */ }
191
+ }
192
+ }
193
+
194
+ async function mapLimit<TIn, TOut>(items: TIn[], limit: number, fn: (item: TIn, i: number) => Promise<TOut>): Promise<TOut[]> {
195
+ if (items.length === 0) return [];
196
+ const width = Math.max(1, Math.min(limit, items.length));
197
+ const results: TOut[] = new Array(items.length);
198
+ let next = 0;
199
+ await Promise.all(
200
+ new Array(width).fill(null).map(async () => {
201
+ while (true) {
202
+ const i = next++;
203
+ if (i >= items.length) return;
204
+ results[i] = await fn(items[i], i);
205
+ }
206
+ }),
207
+ );
208
+ return results;
209
+ }
210
+
211
+ const TaskItem = Type.Object({
212
+ agent: Type.String({ description: "Name of the agent to invoke" }),
213
+ task: Type.String({ description: "Task to delegate to the agent" }),
214
+ cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
215
+ });
216
+
217
+ const SubagentParams = Type.Object({
218
+ agent: Type.Optional(Type.String({ description: "Agent name (single mode)" })),
219
+ task: Type.Optional(Type.String({ description: "Task to delegate (single mode)" })),
220
+ tasks: Type.Optional(Type.Array(TaskItem, { description: "{agent, task} items to run in parallel" })),
221
+ chain: Type.Optional(Type.Array(TaskItem, { description: "{agent, task} items run in order; use {previous} for prior output" })),
222
+ agentScope: Type.Optional(
223
+ StringEnum(["user", "project", "both"] as const, {
224
+ description: 'Which agent dirs to use. Default "both" (project .pi/agents override ~/.pi/agent/agents).',
225
+ default: "both",
226
+ }),
227
+ ),
228
+ confirmProjectAgents: Type.Optional(
229
+ Type.Boolean({ description: "Prompt before running repo-controlled project agents. Default true.", default: true }),
230
+ ),
231
+ cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
232
+ });
233
+
234
+ export default function (pi: ExtensionAPI) {
235
+ if (isPackagedExtension && fs.existsSync(path.join(process.cwd(), ".cratis", "ai.manifest.json"))) return;
236
+ // Discover once at registration so the tool description can name the valid agents — the model
237
+ // otherwise has to guess an `agent` value. Per-call execution rediscovers, so editing an agent
238
+ // mid-session still takes effect; only this hint list is fixed until the next /reload.
239
+ let knownAgents = "";
240
+ try {
241
+ const names = discoverAgents(process.cwd(), "both").agents.map((a) => a.name);
242
+ if (names.length > 0) knownAgents = ` Available agents: ${names.map((n) => `"${n}"`).join(", ")}.`;
243
+ } catch {
244
+ knownAgents = "";
245
+ }
246
+ pi.registerTool({
247
+ name: "subagent",
248
+ label: "Subagent",
249
+ description: [
250
+ "Delegate a task to a specialized Cratis agent in an isolated context (separate pi process).",
251
+ "Modes: single (agent + task), parallel (tasks array, max 8), chain (sequential, {previous} placeholder).",
252
+ `Agents come from the packaged Cratis corpus, ${path.join(CONFIG_DIR_NAME, "agents")} (project), and ${path.join(getAgentDir(), "agents")} (user).`,
253
+ knownAgents,
254
+ ].join(" ").trim(),
255
+ parameters: SubagentParams,
256
+
257
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
258
+ const agentScope: AgentScope = (params.agentScope as AgentScope) ?? "both";
259
+ const dispatch = {
260
+ model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined,
261
+ thinkingLevel: ctx.thinkingLevel,
262
+ };
263
+ const { agents, projectAgentsDir } = discoverAgents(ctx.cwd, agentScope);
264
+
265
+ const hasChain = (params.chain?.length ?? 0) > 0;
266
+ const hasTasks = (params.tasks?.length ?? 0) > 0;
267
+ const hasSingle = Boolean(params.agent && params.task);
268
+ if (Number(hasChain) + Number(hasTasks) + Number(hasSingle) !== 1) {
269
+ const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
270
+ return {
271
+ content: [{ type: "text", text: `Provide exactly one mode (single, parallel, or chain).\nAvailable agents: ${available}` }],
272
+ details: { agents: agents.map((a) => a.name) },
273
+ };
274
+ }
275
+
276
+ // Security gate: project agents are repo-controlled prompts.
277
+ if ((agentScope === "project" || agentScope === "both") && (params.confirmProjectAgents ?? true) && ctx.hasUI) {
278
+ const requested = new Set<string>();
279
+ for (const s of params.chain ?? []) requested.add(s.agent);
280
+ for (const t of params.tasks ?? []) requested.add(t.agent);
281
+ if (params.agent) requested.add(params.agent);
282
+ const projectRequested = Array.from(requested)
283
+ .map((n) => agents.find((a) => a.name === n))
284
+ .filter((a): a is AgentConfig => a?.source === "project");
285
+ if (projectRequested.length > 0) {
286
+ const ok = await ctx.ui.confirm(
287
+ "Run project-local agents?",
288
+ `Agents: ${projectRequested.map((a) => a.name).join(", ")}\nSource: ${projectAgentsDir ?? "(unknown)"}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
289
+ );
290
+ if (!ok) return { content: [{ type: "text", text: "Canceled: project-local agents not approved." }], details: {} };
291
+ }
292
+ }
293
+
294
+ // ── chain ──
295
+ if (params.chain && params.chain.length > 0) {
296
+ const results: SingleResult[] = [];
297
+ let previous = "";
298
+ for (let i = 0; i < params.chain.length; i++) {
299
+ const stepDef = params.chain[i];
300
+ const task = stepDef.task.replace(/\{previous\}/g, previous);
301
+ const r = await runSingleAgent(ctx.cwd, dispatch, agents, stepDef.agent, task, stepDef.cwd, i + 1, signal, undefined);
302
+ results.push(r);
303
+ if (isFailed(r)) {
304
+ return {
305
+ content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${stepDef.agent}): ${resultOutput(r)}` }],
306
+ details: { mode: "chain", results },
307
+ isError: true,
308
+ };
309
+ }
310
+ previous = r.finalText;
311
+ }
312
+ return {
313
+ content: [{ type: "text", text: resultOutput(results[results.length - 1]) }],
314
+ details: { mode: "chain", results },
315
+ };
316
+ }
317
+
318
+ // ── parallel ──
319
+ if (params.tasks && params.tasks.length > 0) {
320
+ if (params.tasks.length > MAX_PARALLEL_TASKS) {
321
+ return {
322
+ content: [{ type: "text", text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.` }],
323
+ details: {},
324
+ };
325
+ }
326
+ const results = await mapLimit(params.tasks, MAX_CONCURRENCY, (t) =>
327
+ runSingleAgent(ctx.cwd, dispatch, agents, t.agent, t.task, t.cwd, undefined, signal, undefined),
328
+ );
329
+ const ok = results.filter((r) => !isFailed(r)).length;
330
+ const summaries = results.map((r) => {
331
+ const status = isFailed(r) ? `failed${r.stopReason && r.stopReason !== "end" ? ` (${r.stopReason})` : ""}` : "completed";
332
+ return `### [${r.agent}] ${status}\n\n${capOutput(resultOutput(r))}`;
333
+ });
334
+ return {
335
+ content: [{ type: "text", text: `Parallel: ${ok}/${results.length} succeeded\n\n${summaries.join("\n\n---\n\n")}` }],
336
+ details: { mode: "parallel", results },
337
+ };
338
+ }
339
+
340
+ // ── single ──
341
+ const r = await runSingleAgent(ctx.cwd, dispatch, agents, params.agent!, params.task!, params.cwd, undefined, signal, undefined);
342
+ if (isFailed(r)) {
343
+ return {
344
+ content: [{ type: "text", text: `Agent ${r.stopReason || "failed"}: ${resultOutput(r)}` }],
345
+ details: { mode: "single", results: [r] },
346
+ isError: true,
347
+ };
348
+ }
349
+ return { content: [{ type: "text", text: resultOutput(r) }], details: { mode: "single", results: [r] } };
350
+ },
351
+ });
352
+ }