stageflow 0.36.0 → 0.40.0

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 (288) hide show
  1. package/README.md +61 -26
  2. package/dist/agent/claudeAdapter.js +21 -1
  3. package/dist/agent/claudeTools.d.ts +13 -0
  4. package/dist/agent/claudeTools.js +98 -0
  5. package/dist/agent/piAdapter.d.ts +9 -1
  6. package/dist/agent/piAdapter.js +59 -18
  7. package/dist/agent/piIsolatedMcp.js +14 -2
  8. package/dist/agent/piIsolatedMcpProbe.d.ts +5 -1
  9. package/dist/agent/piIsolatedMcpProbe.js +16 -30
  10. package/dist/agent/port.d.ts +5 -0
  11. package/dist/agent/sandboxPiTools.d.ts +99 -0
  12. package/dist/agent/sandboxPiTools.js +109 -0
  13. package/dist/agents/agentId.d.ts +8 -0
  14. package/dist/agents/agentId.js +27 -0
  15. package/dist/agents/approvalBroker.d.ts +16 -0
  16. package/dist/agents/approvalBroker.js +45 -0
  17. package/dist/agents/authorChat.d.ts +19 -0
  18. package/dist/agents/authorChat.js +63 -0
  19. package/dist/agents/chatTurn.d.ts +23 -0
  20. package/dist/agents/chatTurn.js +77 -0
  21. package/dist/agents/communications/agentmail.d.ts +19 -0
  22. package/dist/agents/communications/agentmail.js +46 -0
  23. package/dist/agents/communications/agentmailListener.d.ts +59 -0
  24. package/dist/agents/communications/agentmailListener.js +183 -0
  25. package/dist/agents/communications/allowlist.d.ts +10 -0
  26. package/dist/agents/communications/allowlist.js +36 -0
  27. package/dist/agents/communications/channelConnections.d.ts +3 -0
  28. package/dist/agents/communications/channelConnections.js +51 -0
  29. package/dist/agents/communications/delivery.d.ts +53 -0
  30. package/dist/agents/communications/delivery.js +142 -0
  31. package/dist/agents/communications/email.d.ts +26 -0
  32. package/dist/agents/communications/email.js +20 -0
  33. package/dist/agents/communications/identity.d.ts +13 -0
  34. package/dist/agents/communications/identity.js +18 -0
  35. package/dist/agents/communications/imessageListener.d.ts +41 -0
  36. package/dist/agents/communications/imessageListener.js +92 -0
  37. package/dist/agents/communications/inbound.d.ts +63 -0
  38. package/dist/agents/communications/inbound.js +143 -0
  39. package/dist/agents/communications/listener.d.ts +36 -0
  40. package/dist/agents/communications/listener.js +38 -0
  41. package/dist/agents/communications/photon.d.ts +19 -0
  42. package/dist/agents/communications/photon.js +38 -0
  43. package/dist/agents/communications/port.d.ts +27 -0
  44. package/dist/agents/communications/port.js +6 -0
  45. package/dist/agents/communications/slack.d.ts +28 -0
  46. package/dist/agents/communications/slack.js +65 -0
  47. package/dist/agents/communications/supervisor.d.ts +36 -0
  48. package/dist/agents/communications/supervisor.js +96 -0
  49. package/dist/agents/communications/telegram.d.ts +19 -0
  50. package/dist/agents/communications/telegram.js +52 -0
  51. package/dist/agents/definitionSummary.d.ts +13 -0
  52. package/dist/agents/definitionSummary.js +91 -0
  53. package/dist/agents/delegate.d.ts +3 -0
  54. package/dist/agents/delegate.js +8 -0
  55. package/dist/agents/errors.d.ts +10 -0
  56. package/dist/agents/errors.js +32 -0
  57. package/dist/agents/guardrails.d.ts +24 -0
  58. package/dist/agents/guardrails.js +39 -0
  59. package/dist/agents/historyFit.d.ts +10 -0
  60. package/dist/agents/historyFit.js +26 -0
  61. package/dist/agents/hostContext.d.ts +54 -0
  62. package/dist/agents/hostContext.js +156 -0
  63. package/dist/agents/ledgers.d.ts +29 -0
  64. package/dist/agents/ledgers.js +31 -0
  65. package/dist/agents/lineDiff.d.ts +12 -0
  66. package/dist/agents/lineDiff.js +46 -0
  67. package/dist/agents/ndjson.d.ts +9 -0
  68. package/dist/agents/ndjson.js +47 -0
  69. package/dist/agents/otlpExport.d.ts +9 -0
  70. package/dist/agents/otlpExport.js +58 -0
  71. package/dist/agents/otlpSink.d.ts +7 -0
  72. package/dist/agents/otlpSink.js +23 -0
  73. package/dist/agents/policy.d.ts +31 -0
  74. package/dist/agents/policy.js +84 -0
  75. package/dist/agents/projectRoot.d.ts +5 -0
  76. package/dist/agents/projectRoot.js +15 -0
  77. package/dist/agents/runtime/piRuntime.d.ts +52 -0
  78. package/dist/agents/runtime/piRuntime.js +474 -0
  79. package/dist/agents/runtime/types.d.ts +77 -0
  80. package/dist/agents/runtime/types.js +26 -0
  81. package/dist/agents/sandbox.d.ts +17 -0
  82. package/dist/agents/sandbox.js +10 -0
  83. package/dist/agents/scheduleSource.d.ts +32 -0
  84. package/dist/agents/scheduleSource.js +100 -0
  85. package/dist/agents/service.d.ts +298 -0
  86. package/dist/agents/service.js +1105 -0
  87. package/dist/agents/sessions.d.ts +38 -0
  88. package/dist/agents/sessions.js +321 -0
  89. package/dist/agents/store.d.ts +277 -0
  90. package/dist/agents/store.js +643 -0
  91. package/dist/agents/trace.d.ts +27 -0
  92. package/dist/agents/trace.js +29 -0
  93. package/dist/agents/turnLifecycle.d.ts +72 -0
  94. package/dist/agents/turnLifecycle.js +157 -0
  95. package/dist/agents/webChat/publicChat.d.ts +36 -0
  96. package/dist/agents/webChat/publicChat.js +290 -0
  97. package/dist/agents/webChat/rateLimit.d.ts +15 -0
  98. package/dist/agents/webChat/rateLimit.js +33 -0
  99. package/dist/agents/webChat/slug.d.ts +2 -0
  100. package/dist/agents/webChat/slug.js +9 -0
  101. package/dist/channels/providers/agentmail.js +14 -1
  102. package/dist/channels/providers/photonImessage.js +36 -0
  103. package/dist/channels/providers/slack.js +2 -1
  104. package/dist/channels/providers/telegram.js +2 -1
  105. package/dist/channels/types.d.ts +27 -0
  106. package/dist/cli/agentsCommand.d.ts +14 -0
  107. package/dist/cli/agentsCommand.js +291 -0
  108. package/dist/cli.js +9 -0
  109. package/dist/config/browseCatalog.js +4 -11
  110. package/dist/config/draftPackage.d.ts +1 -0
  111. package/dist/config/draftPackage.js +4 -0
  112. package/dist/config/hostConfig.js +9 -0
  113. package/dist/config/loadAgent.d.ts +266 -0
  114. package/dist/config/loadAgent.js +374 -0
  115. package/dist/config/loadPipeline.js +5 -2
  116. package/dist/config/loadTask.js +34 -1
  117. package/dist/config/mcpHostCatalog.d.ts +50 -0
  118. package/dist/config/mcpHostCatalog.js +405 -0
  119. package/dist/config/mcpOAuth.d.ts +37 -0
  120. package/dist/config/mcpOAuth.js +263 -0
  121. package/dist/config/mcpRedaction.d.ts +42 -0
  122. package/dist/config/mcpRedaction.js +314 -0
  123. package/dist/config/mcpToolCache.d.ts +20 -0
  124. package/dist/config/mcpToolCache.js +43 -0
  125. package/dist/config/mergePipelineIncludes.d.ts +2 -0
  126. package/dist/config/mergePipelineIncludes.js +25 -0
  127. package/dist/config/resolveStageMcpServers.d.ts +35 -3
  128. package/dist/config/resolveStageMcpServers.js +124 -9
  129. package/dist/config/validateCatalog.d.ts +1 -1
  130. package/dist/config/validateCatalog.js +22 -1
  131. package/dist/mcp/agentTools.d.ts +8 -0
  132. package/dist/mcp/agentTools.js +132 -0
  133. package/dist/mcp/catalogTools.js +17 -10
  134. package/dist/mcp/deps.d.ts +6 -0
  135. package/dist/mcp/pipelineAuthorSupport.d.ts +19 -0
  136. package/dist/mcp/pipelineAuthorSupport.js +10 -0
  137. package/dist/mcp/pipelineAuthorTools.d.ts +8 -0
  138. package/dist/mcp/pipelineAuthorTools.js +109 -0
  139. package/dist/mcp/server.js +5 -0
  140. package/dist/mcp/tools.js +4 -0
  141. package/dist/mcp/triggerTools.js +32 -0
  142. package/dist/memory/access.d.ts +36 -0
  143. package/dist/memory/access.js +116 -0
  144. package/dist/memory/agentIssues.d.ts +3 -0
  145. package/dist/memory/agentIssues.js +23 -0
  146. package/dist/memory/bashDeny.d.ts +6 -0
  147. package/dist/memory/bashDeny.js +35 -0
  148. package/dist/memory/host.d.ts +51 -0
  149. package/dist/memory/host.js +394 -0
  150. package/dist/memory/ids.d.ts +15 -0
  151. package/dist/memory/ids.js +31 -0
  152. package/dist/memory/worker.d.ts +1 -0
  153. package/dist/memory/worker.js +84 -0
  154. package/dist/models/catalog.d.ts +113 -0
  155. package/dist/models/catalog.js +238 -0
  156. package/dist/models/response.d.ts +29 -0
  157. package/dist/models/response.js +25 -0
  158. package/dist/operatorAgent/agentAuthorContext.d.ts +17 -0
  159. package/dist/operatorAgent/agentAuthorContext.js +32 -0
  160. package/dist/operatorAgent/fakeHost.js +2 -0
  161. package/dist/operatorAgent/host.js +6 -3
  162. package/dist/operatorAgent/index.d.ts +4 -0
  163. package/dist/operatorAgent/index.js +4 -0
  164. package/dist/operatorAgent/piModel.js +2 -0
  165. package/dist/operatorAgent/profiles/agentAuthor.d.ts +11 -0
  166. package/dist/operatorAgent/profiles/agentAuthor.js +192 -0
  167. package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -0
  168. package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +29 -0
  169. package/dist/operatorAgent/profiles/workshopAuthor.js +2 -2
  170. package/dist/operatorAgent/profiles/workshopAuthorTools.js +3 -3
  171. package/dist/operatorAgent/proposals.d.ts +1 -0
  172. package/dist/operatorAgent/proposals.js +23 -2
  173. package/dist/package-meta.d.ts +1 -1
  174. package/dist/package-meta.js +1 -1
  175. package/dist/preflight/pipelinePreflight.js +1 -0
  176. package/dist/runstore/createStore.d.ts +2 -0
  177. package/dist/runstore/createStore.js +8 -0
  178. package/dist/runstore/overviewSummary.d.ts +56 -0
  179. package/dist/runstore/overviewSummary.js +134 -0
  180. package/dist/runstore/port.d.ts +3 -0
  181. package/dist/runstore/sqlite/SqliteRunStore.d.ts +2 -0
  182. package/dist/runstore/sqlite/SqliteRunStore.js +18 -0
  183. package/dist/runstore/sqlite/migrations/013-agents.d.ts +12 -0
  184. package/dist/runstore/sqlite/migrations/013-agents.js +156 -0
  185. package/dist/runstore/sqlite/migrations/014-agent-deliveries.d.ts +11 -0
  186. package/dist/runstore/sqlite/migrations/014-agent-deliveries.js +32 -0
  187. package/dist/runstore/sqlite/migrations/index.js +4 -0
  188. package/dist/runtime/gitCheckoutCapability.d.ts +1 -0
  189. package/dist/runtime/gitCheckoutCapability.js +1 -1
  190. package/dist/runtime/gitWorktreeCheckout.d.ts +22 -0
  191. package/dist/runtime/gitWorktreeCheckout.js +120 -0
  192. package/dist/runtime/sandboxContainer.d.ts +62 -0
  193. package/dist/runtime/sandboxContainer.js +151 -0
  194. package/dist/runtime/sandboxDesk.d.ts +28 -0
  195. package/dist/runtime/sandboxDesk.js +242 -0
  196. package/dist/runtime/sandboxFiles.d.ts +20 -0
  197. package/dist/runtime/sandboxFiles.js +150 -0
  198. package/dist/runtime/stageAttemptBootstrap.js +15 -1
  199. package/dist/runtime/stageEnvironment.d.ts +2 -0
  200. package/dist/runtime/stageEnvironment.js +8 -1
  201. package/dist/runtime/stageProcessLauncher.js +23 -0
  202. package/dist/runtime/stageRoots.d.ts +9 -0
  203. package/dist/runtime/stageRoots.js +14 -0
  204. package/dist/runtime/stageRunner.d.ts +5 -0
  205. package/dist/runtime/stageRunner.js +38 -4
  206. package/dist/runtime/stageSandbox.d.ts +16 -0
  207. package/dist/runtime/stageSandbox.js +54 -0
  208. package/dist/runtime/stageSecrets.d.ts +6 -1
  209. package/dist/runtime/stageSecrets.js +34 -1
  210. package/dist/runtime/stageWorker.js +138 -105
  211. package/dist/runtime/workspaceBinding.d.ts +2 -1
  212. package/dist/runtime/workspaceBinding.js +20 -4
  213. package/dist/server/agentAuthorRoutes.d.ts +5 -0
  214. package/dist/server/agentAuthorRoutes.js +64 -0
  215. package/dist/server/agentRoutes.d.ts +5 -0
  216. package/dist/server/agentRoutes.js +323 -0
  217. package/dist/server/bootstrap.d.ts +8 -0
  218. package/dist/server/bootstrap.js +36 -10
  219. package/dist/server/controlToken.js +8 -0
  220. package/dist/server/http.d.ts +8 -1
  221. package/dist/server/http.js +172 -20
  222. package/dist/server/mcpHost.js +1 -0
  223. package/dist/server/memoryRoutes.d.ts +5 -0
  224. package/dist/server/memoryRoutes.js +146 -0
  225. package/dist/server/projectMcpRoutes.d.ts +1 -0
  226. package/dist/server/projectMcpRoutes.js +241 -3
  227. package/dist/toolkit/guard.d.ts +27 -0
  228. package/dist/toolkit/guard.js +98 -0
  229. package/dist/toolkit/index.d.ts +48 -0
  230. package/dist/toolkit/index.js +82 -0
  231. package/dist/toolkit/meta.d.ts +36 -0
  232. package/dist/toolkit/meta.js +117 -0
  233. package/dist/toolkit/profiles.d.ts +13 -0
  234. package/dist/toolkit/profiles.js +25 -0
  235. package/dist/toolkit/record.d.ts +15 -0
  236. package/dist/toolkit/record.js +17 -0
  237. package/dist/tools/memoryTools.d.ts +95 -0
  238. package/dist/tools/memoryTools.js +179 -0
  239. package/dist/types/pipeline.d.ts +3 -0
  240. package/dist/types/stage.d.ts +2 -0
  241. package/dist/types/task.d.ts +11 -1
  242. package/dist/ui/assets/AssistantRuntimeProvider-CsiPFhCT.js +9 -0
  243. package/dist/ui/assets/MarkdownText-D7tYaIFO.js +29 -0
  244. package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +6 -0
  245. package/dist/ui/assets/Tooltip-DrpoRRNi.js +1 -0
  246. package/dist/ui/assets/chat-BbLukq9W.js +5 -0
  247. package/dist/ui/assets/chat-zIZU_bP1.css +2 -0
  248. package/dist/ui/assets/jsx-runtime-iVoFAvQ_.js +1 -0
  249. package/dist/ui/assets/{index-Clv4sJpU.css → main-BFo79krC.css} +1 -1
  250. package/dist/ui/assets/main-CijYMeM0.js +259 -0
  251. package/dist/ui/chat.html +25 -0
  252. package/dist/ui/index.html +6 -2
  253. package/dist/workshop/modelSettings.d.ts +2 -2
  254. package/dist/workshop/modelSettings.js +5 -11
  255. package/examples/README.md +1 -0
  256. package/examples/channels-smoke/README.md +12 -1
  257. package/examples/github-release/README.md +1 -1
  258. package/examples/memory/README.md +101 -0
  259. package/examples/memory/desk-outage.task.yaml +15 -0
  260. package/examples/memory/desk-refund.task.yaml +15 -0
  261. package/examples/memory/desk-revisit-outage.task.yaml +11 -0
  262. package/examples/memory/desk-revisit.pipeline.yaml +123 -0
  263. package/examples/memory/desk.pipeline.yaml +192 -0
  264. package/examples/memory/file-card.pipeline.yaml +39 -0
  265. package/examples/memory/file-ledger.task.yaml +22 -0
  266. package/examples/memory/file-release.task.yaml +24 -0
  267. package/examples/memory/file-rich.pipeline.yaml +40 -0
  268. package/examples/memory/file-ticket.task.yaml +7 -0
  269. package/examples/memory/file-timeline.task.yaml +28 -0
  270. package/examples/memory/find-card.pipeline.yaml +46 -0
  271. package/examples/memory/find-ticket.task.yaml +7 -0
  272. package/examples/memory/missing.pipeline.yaml +16 -0
  273. package/examples/memory/probe-bash.task.yaml +3 -0
  274. package/examples/memory/recall.pipeline.yaml +24 -0
  275. package/examples/memory/recall.task.yaml +5 -0
  276. package/examples/memory/remember-ada.task.yaml +6 -0
  277. package/examples/memory/remember-grace.task.yaml +6 -0
  278. package/examples/memory/remember.pipeline.yaml +58 -0
  279. package/examples/memory/sandbox-memory-bash.pipeline.yaml +28 -0
  280. package/examples/memory/stageflow.yaml +12 -0
  281. package/examples/memory/update-card.pipeline.yaml +43 -0
  282. package/examples/memory/update-ledger.task.yaml +23 -0
  283. package/examples/memory/update-rich.pipeline.yaml +44 -0
  284. package/examples/memory/update-ticket.task.yaml +7 -0
  285. package/examples/ship-feature/README.md +15 -0
  286. package/examples/stage-mcp/README.md +1 -1
  287. package/package.json +2 -1
  288. package/dist/ui/assets/index-BOhqnPNg.js +0 -260
@@ -0,0 +1,238 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { ModelRuntime } from "@earendil-works/pi-coding-agent";
4
+ import { getCursorHostStatus } from "../agent/cursorProvider.js";
5
+ import { globalModelFromManifest } from "../config/resolveModel.js";
6
+ import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
7
+ import { resolveCredentialBinding } from "../runtime/credentialBinding.js";
8
+ import { readFactorySettings } from "../runtime/settingsFile.js";
9
+ /** Last resort when nothing is connected and nothing is configured. Pi resolves unknown OpenRouter ids. */
10
+ export const LAST_RESORT_MODEL = "openrouter/nvidia/nemotron-3.5-lightning:free";
11
+ const DEFAULT_TTL_MS = 30_000;
12
+ const DEFAULT_REFRESH_TIMEOUT_MS = 10_000;
13
+ const NOTHING_CONNECTED = {
14
+ getProviders: () => [],
15
+ getProviderAuthStatus: () => ({ configured: false }),
16
+ listCredentials: async () => [],
17
+ getModels: () => [],
18
+ refresh: async () => undefined,
19
+ };
20
+ // ModelRuntime.create writes auth.json when it is missing; listing models must not, so no file means nothing connected.
21
+ const defaultCreateRuntime = async (authPath) => !existsSync(authPath)
22
+ ? NOTHING_CONNECTED
23
+ : ModelRuntime.create({
24
+ authPath,
25
+ // Same layout as the provider routes: models.json and models-store.json sit beside auth.json.
26
+ modelsPath: path.join(path.dirname(authPath), "models.json"),
27
+ refreshOnCreate: false,
28
+ });
29
+ /** Cursor models come from the extension's cached list; they only count once Cursor is connected. */
30
+ function defaultCursorSource(authPath) {
31
+ return () => {
32
+ const status = getCursorHostStatus();
33
+ const connected = status.cursorApiKeyConfigured && status.cursorSdkReady;
34
+ if (!connected)
35
+ return { connected, modelIds: [] };
36
+ const file = path.join(path.dirname(authPath), "cursor-sdk-model-list.json");
37
+ try {
38
+ if (!existsSync(file))
39
+ return { connected, modelIds: [] };
40
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
41
+ const ids = (parsed.models ?? [])
42
+ .map((entry) => (typeof entry.id === "string" ? entry.id : ""))
43
+ .filter((id) => id.length > 0)
44
+ .map((id) => `cursor/${id === "default" ? "auto" : id}`);
45
+ return { connected, modelIds: ids };
46
+ }
47
+ catch {
48
+ return { connected, modelIds: [] };
49
+ }
50
+ };
51
+ }
52
+ function errorMessage(error) {
53
+ return error instanceof Error ? error.message : String(error);
54
+ }
55
+ function isFree(model) {
56
+ if (model.id.endsWith(":free"))
57
+ return true;
58
+ return model.cost !== undefined && model.cost.input === 0 && model.cost.output === 0;
59
+ }
60
+ /**
61
+ * The one place model lists and the default model come from. Models are those of connected
62
+ * providers, asked of Pi's registry (live where the provider lists models, built-in otherwise).
63
+ */
64
+ export class ModelCatalog {
65
+ options;
66
+ cached;
67
+ inflight;
68
+ lastDefault;
69
+ constructor(options) {
70
+ this.options = options;
71
+ }
72
+ now() {
73
+ return (this.options.now ?? Date.now)();
74
+ }
75
+ /** Cached list; rebuilt when older than the TTL or when `refresh` forces Pi to refetch provider catalogs. */
76
+ async snapshot(options = {}) {
77
+ const ttl = this.options.ttlMs ?? DEFAULT_TTL_MS;
78
+ if (!options.refresh && this.cached && this.now() - this.cached.builtAt < ttl)
79
+ return this.cached.snapshot;
80
+ if (this.inflight && !options.refresh)
81
+ return this.inflight;
82
+ const run = this.build(options.refresh === true)
83
+ .then((snapshot) => {
84
+ this.cached = { snapshot, builtAt: this.now() };
85
+ return snapshot;
86
+ })
87
+ .catch((error) => {
88
+ // Keep serving the last good list when a rebuild fails outright.
89
+ if (this.cached)
90
+ return this.cached.snapshot;
91
+ throw error;
92
+ })
93
+ .finally(() => {
94
+ if (this.inflight === run)
95
+ this.inflight = undefined;
96
+ });
97
+ this.inflight = run;
98
+ return run;
99
+ }
100
+ refresh() {
101
+ return this.snapshot({ refresh: true });
102
+ }
103
+ /** True when the id belongs to a connected provider. Custom ids still run; this only drives warnings. */
104
+ async isKnown(id) {
105
+ const wanted = id.trim();
106
+ if (wanted.length === 0)
107
+ return false;
108
+ return (await this.snapshot()).models.includes(wanted);
109
+ }
110
+ /**
111
+ * stageflow.yaml `model`, then the Settings workshop model, then the catalog's pick: the last-resort
112
+ * model when connected, else the first free model, else the first model, else the last resort.
113
+ */
114
+ async defaultModel() {
115
+ const configured = await this.configuredDefault();
116
+ if (configured)
117
+ return this.remember(configured);
118
+ return this.remember(this.pick(await this.snapshot().catch(() => undefined)));
119
+ }
120
+ /** Synchronous default for call sites that cannot await: the last resolved default, else the last resort. */
121
+ peekDefault() {
122
+ return this.lastDefault ?? this.configuredDefaultSync() ?? this.pick(this.cached?.snapshot);
123
+ }
124
+ remember(id) {
125
+ this.lastDefault = id;
126
+ lastKnownDefault = id;
127
+ return id;
128
+ }
129
+ async configuredDefault() {
130
+ const ctx = await resolveStageflowContext(this.options.cwd);
131
+ const model = globalModelFromManifest(ctx.manifest) ?? readFactorySettings(this.options.cwd).workshopModel;
132
+ const trimmed = model?.trim();
133
+ return trimmed ? trimmed : undefined;
134
+ }
135
+ configuredDefaultSync() {
136
+ const trimmed = readFactorySettings(this.options.cwd).workshopModel?.trim();
137
+ return trimmed ? trimmed : undefined;
138
+ }
139
+ pick(snapshot) {
140
+ const all = snapshot?.providers.flatMap((provider) => provider.models) ?? [];
141
+ if (all.some((model) => model.id === LAST_RESORT_MODEL))
142
+ return LAST_RESORT_MODEL;
143
+ return all.find((model) => model.free && !model.nativeTools)?.id ?? all.find((model) => !model.nativeTools)?.id ?? all[0]?.id ?? LAST_RESORT_MODEL;
144
+ }
145
+ async build(force) {
146
+ const authPath = resolveCredentialBinding(this.options.cwd).authPath;
147
+ const runtime = await (this.options.createRuntime ?? defaultCreateRuntime)(authPath);
148
+ const credentials = new Set((await runtime.listCredentials().catch(() => [])).map((entry) => entry.providerId));
149
+ const connected = runtime
150
+ .getProviders()
151
+ .filter((provider) => credentials.has(provider.id) || runtime.getProviderAuthStatus(provider.id).configured);
152
+ const ids = connected.map((provider) => provider.id);
153
+ // Restore the cached catalogs first, then ask the providers; a failure leaves the cached or built-in list.
154
+ const errors = new Map();
155
+ if (ids.length > 0) {
156
+ await runtime.refresh({ allowNetwork: false, providers: ids }).catch(() => undefined);
157
+ const signal = AbortSignal.timeout(this.options.refreshTimeoutMs ?? DEFAULT_REFRESH_TIMEOUT_MS);
158
+ try {
159
+ const result = await runtime.refresh({ allowNetwork: true, providers: ids, force, signal });
160
+ if (result)
161
+ for (const [id, error] of result.errors)
162
+ errors.set(id, error);
163
+ }
164
+ catch (error) {
165
+ for (const id of ids)
166
+ errors.set(id, error instanceof Error ? error : new Error(errorMessage(error)));
167
+ }
168
+ }
169
+ const providers = connected.map((provider) => {
170
+ const failure = errors.get(provider.id);
171
+ return {
172
+ id: provider.id,
173
+ name: provider.name ?? provider.id,
174
+ source: failure ? "fallback" : "live",
175
+ ...(failure ? { error: errorMessage(failure) } : {}),
176
+ models: runtime
177
+ .getModels(provider.id)
178
+ .map((model) => ({
179
+ id: `${model.provider}/${model.id}`,
180
+ provider: model.provider,
181
+ name: model.name ?? model.id,
182
+ free: isFree(model),
183
+ ...(typeof model.contextWindow === "number" ? { contextWindow: model.contextWindow } : {}),
184
+ ...(typeof model.reasoning === "boolean" ? { reasoning: model.reasoning } : {}),
185
+ nativeTools: false,
186
+ }))
187
+ .sort((a, b) => a.id.localeCompare(b.id)),
188
+ };
189
+ });
190
+ const cursor = (this.options.cursor ?? defaultCursorSource(authPath))();
191
+ if (cursor.connected && cursor.modelIds.length > 0) {
192
+ providers.push({
193
+ id: "cursor",
194
+ name: "Cursor",
195
+ source: "live",
196
+ models: [...new Set(cursor.modelIds)].sort().map((id) => ({
197
+ id,
198
+ provider: "cursor",
199
+ name: id.slice("cursor/".length),
200
+ free: false,
201
+ nativeTools: true,
202
+ })),
203
+ });
204
+ }
205
+ providers.sort((a, b) => a.name.localeCompare(b.name));
206
+ return {
207
+ providers,
208
+ models: providers.flatMap((provider) => provider.models.map((model) => model.id)).sort((a, b) => a.localeCompare(b)),
209
+ fetchedAt: new Date(this.now()).toISOString(),
210
+ };
211
+ }
212
+ }
213
+ const catalogs = new Map();
214
+ let lastKnownDefault;
215
+ /**
216
+ * The most recently resolved default, for call sites that cannot await. Hosts resolve it at boot,
217
+ * so this is the catalog's answer, not a second source.
218
+ */
219
+ export function peekDefaultModel() {
220
+ return lastKnownDefault ?? LAST_RESORT_MODEL;
221
+ }
222
+ /** The Host's catalog for a working directory, shared by every consumer in the process. */
223
+ export function getModelCatalog(cwd) {
224
+ const key = path.resolve(cwd);
225
+ let catalog = catalogs.get(key);
226
+ if (!catalog) {
227
+ catalog = new ModelCatalog({ cwd: key });
228
+ catalogs.set(key, catalog);
229
+ }
230
+ return catalog;
231
+ }
232
+ export function resetModelCatalogsForTests() {
233
+ catalogs.clear();
234
+ lastKnownDefault = undefined;
235
+ }
236
+ export function setModelCatalogForTests(cwd, catalog) {
237
+ catalogs.set(path.resolve(cwd), catalog);
238
+ }
@@ -0,0 +1,29 @@
1
+ import type { RunStore } from "../runstore/port.js";
2
+ import { listModelsMultiProject } from "../config/multiProjectCatalog.js";
3
+ import { type CatalogProvider } from "./catalog.js";
4
+ export type ModelsResponse = {
5
+ /** Catalog ids plus ids pipelines already use, sorted. Back-compatible with the old flat list. */
6
+ models: string[];
7
+ /** Connected providers with their models. */
8
+ providers: CatalogProvider[];
9
+ /** The model a new chat, stage or agent starts on. */
10
+ default: string;
11
+ fetchedAt: string;
12
+ /** Ids pipelines use that no connected provider lists. They still run; pickers warn. */
13
+ unlisted: string[];
14
+ entries: Array<{
15
+ id: string;
16
+ project_root: string;
17
+ }>;
18
+ root_errors: Awaited<ReturnType<typeof listModelsMultiProject>>["root_errors"];
19
+ };
20
+ /** The shared answer for GET /api/models, the MCP `list_models` tool and every picker. */
21
+ export declare function buildModelsResponse(options: {
22
+ store: RunStore;
23
+ cwd: string;
24
+ projectRootFilter?: string | undefined;
25
+ refresh?: boolean;
26
+ }): Promise<{
27
+ response: ModelsResponse;
28
+ used: Awaited<ReturnType<typeof listModelsMultiProject>>;
29
+ }>;
@@ -0,0 +1,25 @@
1
+ import { listModelsMultiProject } from "../config/multiProjectCatalog.js";
2
+ import { getModelCatalog } from "./catalog.js";
3
+ /** The shared answer for GET /api/models, the MCP `list_models` tool and every picker. */
4
+ export async function buildModelsResponse(options) {
5
+ const catalog = getModelCatalog(options.cwd);
6
+ const [snapshot, used, defaultModel] = await Promise.all([
7
+ catalog.snapshot(options.refresh ? { refresh: true } : {}),
8
+ listModelsMultiProject({ store: options.store, bootCwd: options.cwd, projectRootFilter: options.projectRootFilter }),
9
+ catalog.defaultModel(),
10
+ ]);
11
+ const listed = new Set(snapshot.models);
12
+ const unlisted = used.models.filter((id) => !listed.has(id));
13
+ return {
14
+ used,
15
+ response: {
16
+ models: [...new Set([...snapshot.models, ...used.models])].sort((a, b) => a.localeCompare(b)),
17
+ providers: snapshot.providers,
18
+ default: defaultModel,
19
+ fetchedAt: snapshot.fetchedAt,
20
+ unlisted,
21
+ entries: used.items,
22
+ root_errors: used.root_errors,
23
+ },
24
+ };
25
+ }
@@ -0,0 +1,17 @@
1
+ import type { OperatorAgentContextAdapter } from "./types.js";
2
+ /**
3
+ * The unsaved agent definition the author edits: any subset of the v2 agent
4
+ * schema (src/config/loadAgent.ts). It may be incomplete or invalid while the
5
+ * operator is still describing the agent; Save validates it.
6
+ */
7
+ export type AgentAuthorDraft = Record<string, unknown>;
8
+ export type AgentAuthorContext = {
9
+ kind: "agent_author";
10
+ projectRoot?: string;
11
+ draft: AgentAuthorDraft;
12
+ };
13
+ export declare const AGENT_AUTHOR_GREETING = "What should this agent do? Tell me the job, the tools it needs, and anything it must never do without asking.";
14
+ export declare function emptyAgentAuthorDraft(): AgentAuthorDraft;
15
+ export declare function isAgentAuthorContext(value: unknown): value is AgentAuthorContext;
16
+ export declare function readAgentAuthorDraft(context: unknown): AgentAuthorDraft;
17
+ export declare function agentAuthorContextAdapter(): OperatorAgentContextAdapter;
@@ -0,0 +1,32 @@
1
+ export const AGENT_AUTHOR_GREETING = "What should this agent do? Tell me the job, the tools it needs, and anything it must never do without asking.";
2
+ export function emptyAgentAuthorDraft() {
3
+ return {};
4
+ }
5
+ export function isAgentAuthorContext(value) {
6
+ if (value === null || typeof value !== "object")
7
+ return false;
8
+ const record = value;
9
+ return (record.kind === "agent_author" &&
10
+ record.draft !== null &&
11
+ typeof record.draft === "object" &&
12
+ !Array.isArray(record.draft));
13
+ }
14
+ export function readAgentAuthorDraft(context) {
15
+ return isAgentAuthorContext(context) ? structuredClone(context.draft) : emptyAgentAuthorDraft();
16
+ }
17
+ export function agentAuthorContextAdapter() {
18
+ return {
19
+ serialize: (context) => context ?? { kind: "agent_author", draft: emptyAgentAuthorDraft() },
20
+ applyProposal: (context, proposal) => {
21
+ if (!isAgentAuthorContext(proposal.nextContext))
22
+ return context;
23
+ const current = isAgentAuthorContext(context) ? context : undefined;
24
+ const projectRoot = proposal.nextContext.projectRoot ?? current?.projectRoot;
25
+ return {
26
+ kind: "agent_author",
27
+ ...(projectRoot ? { projectRoot } : {}),
28
+ draft: structuredClone(proposal.nextContext.draft),
29
+ };
30
+ },
31
+ };
32
+ }
@@ -1,4 +1,5 @@
1
1
  import { createOperatorAgentHost, invokeProfileTool, } from "./host.js";
2
+ import { createAgentAuthorProfile } from "./profiles/agentAuthor.js";
2
3
  import { createWorkshopAuthorProfile, isTaskProposalIntent, proposeStageFromUserMessage, proposeTaskFromUserMessage, WORKSHOP_AUTHOR_PROFILE_ID, } from "./profiles/workshopAuthor.js";
3
4
  /**
4
5
  * Deterministic Operator Agent Host for Workshop and tests.
@@ -86,6 +87,7 @@ export function createWorkshopOperatorHost(scriptOrOptions) {
86
87
  retriever: options.retriever,
87
88
  projectRoot: options.projectRoot,
88
89
  }),
90
+ createAgentAuthorProfile(),
89
91
  ]);
90
92
  return host;
91
93
  }
@@ -1,5 +1,5 @@
1
- import { readDraftFromContext } from "./draftContext.js";
2
- import { draftFingerprint, enrichProposal, isMutationConflict, UNDO_MUTATION_CONFLICT_NOTICE, } from "./proposals.js";
1
+ import { profileMayUse } from "../toolkit/profiles.js";
2
+ import { enrichProposal, isMutationConflict, operatorContextFingerprint, UNDO_MUTATION_CONFLICT_NOTICE, } from "./proposals.js";
3
3
  import { rejectUnlinkedWorkshopDraftTool } from "../workshop/workshopPin.js";
4
4
  function createSession(profile, initialContext, model) {
5
5
  let context = initialContext;
@@ -17,7 +17,7 @@ function createSession(profile, initialContext, model) {
17
17
  context = profile.contextAdapter.applyProposal(context, enriched);
18
18
  const applied = {
19
19
  ...enriched,
20
- appliedFingerprint: draftFingerprint(readDraftFromContext(context)),
20
+ appliedFingerprint: operatorContextFingerprint(context),
21
21
  };
22
22
  mutations.set(applied.id, applied);
23
23
  lastMutationId = applied.id;
@@ -172,6 +172,9 @@ export async function invokeProfileTool(profile, name, args, ctx) {
172
172
  if (!tool) {
173
173
  return { ok: false, content: null, error: `Unknown tool: ${name}` };
174
174
  }
175
+ if (!profileMayUse(profile.id, name)) {
176
+ return { ok: false, content: null, error: `${profile.id} is not granted ${name}` };
177
+ }
175
178
  const blocked = rejectUnlinkedWorkshopDraftTool(ctx.getContext(), name);
176
179
  if (blocked)
177
180
  return blocked;
@@ -4,6 +4,10 @@ export { createFakeOperatorAgentModel, createWorkshopOperatorHost, type FakeOper
4
4
  export { AGENT_PORT_BACKENDS, createPiAgentPort, resolveAgentPort, type AgentPortBackendId, type AgentPort, type AgentPortOptions, } from "./agentPort.js";
5
5
  export { createLiveWorkshopOperatorHost, createPiOperatorAgentModel, type LiveWorkshopOperatorHostOptions, type PiOperatorAgentModel, type PiOperatorAgentModelOptions, type PiOperatorOpenSessionInput, type PiOperatorSessionHandle, type WorkshopTranscriptSeedMessage, } from "./piModel.js";
6
6
  export { createFailingDocsRetriever, createFilesystemDocsRetriever, createStubDocsRetriever, resolveWorkshopCatalogRoot, type DocsRetrievalHit, type DocsRetrievalResult, type DocsRetriever, } from "./docsRetrieval.js";
7
+ export { agentAuthorContextAdapter, emptyAgentAuthorDraft, isAgentAuthorContext, readAgentAuthorDraft, AGENT_AUTHOR_GREETING, type AgentAuthorContext, type AgentAuthorDraft, } from "./agentAuthorContext.js";
8
+ export { AGENT_AUTHOR_PROFILE_ID, AGENT_AUTHOR_TOOL_NAMES, createAgentAuthorContext, createAgentAuthorProfile, } from "./profiles/agentAuthor.js";
9
+ export { AGENT_AUTHOR_PLAYBOOK } from "./profiles/agentAuthorPlaybook.js";
10
+ export { operatorContextFingerprint } from "./proposals.js";
7
11
  export { createWorkshopDraftContext, draftTrackStages, emptyDraftPackage, isWorkshopDraftContext, readDraftFromContext, withDestination, withDraft, withProjectRoot, workshopDraftContextAdapter, WORKSHOP_AUTHOR_GREETING, } from "./draftContext.js";
8
12
  export { affectedStageIds, diffDraftPackages, draftFingerprint, enrichProposal, isMutationConflict, STALE_PROPOSAL_NOTICE, UNDO_MUTATION_CONFLICT_NOTICE, } from "./proposals.js";
9
13
  export { assertWorkshopAuthorToolsExcludeDiskShell, buildStageAddProposal, buildTaskAddProposal, createWorkshopAuthorProfile, createWorkshopAuthorTools, isTaskProposalIntent, proposeStageFromUserMessage, proposeTaskFromUserMessage, WORKSHOP_AUTHOR_PLAYBOOK, WORKSHOP_AUTHOR_PROFILE_ID, WORKSHOP_AUTHOR_TOOL_NAMES, WORKSHOP_FORBIDDEN_AGENT_TOOLS, workshopAuthorTools, type WorkshopAuthorProfileOptions, } from "./profiles/workshopAuthor.js";
@@ -3,6 +3,10 @@ export { createFakeOperatorAgentModel, createWorkshopOperatorHost, } from "./fak
3
3
  export { AGENT_PORT_BACKENDS, createPiAgentPort, resolveAgentPort, } from "./agentPort.js";
4
4
  export { createLiveWorkshopOperatorHost, createPiOperatorAgentModel, } from "./piModel.js";
5
5
  export { createFailingDocsRetriever, createFilesystemDocsRetriever, createStubDocsRetriever, resolveWorkshopCatalogRoot, } from "./docsRetrieval.js";
6
+ export { agentAuthorContextAdapter, emptyAgentAuthorDraft, isAgentAuthorContext, readAgentAuthorDraft, AGENT_AUTHOR_GREETING, } from "./agentAuthorContext.js";
7
+ export { AGENT_AUTHOR_PROFILE_ID, AGENT_AUTHOR_TOOL_NAMES, createAgentAuthorContext, createAgentAuthorProfile, } from "./profiles/agentAuthor.js";
8
+ export { AGENT_AUTHOR_PLAYBOOK } from "./profiles/agentAuthorPlaybook.js";
9
+ export { operatorContextFingerprint } from "./proposals.js";
6
10
  export { createWorkshopDraftContext, draftTrackStages, emptyDraftPackage, isWorkshopDraftContext, readDraftFromContext, withDestination, withDraft, withProjectRoot, workshopDraftContextAdapter, WORKSHOP_AUTHOR_GREETING, } from "./draftContext.js";
7
11
  export { affectedStageIds, diffDraftPackages, draftFingerprint, enrichProposal, isMutationConflict, STALE_PROPOSAL_NOTICE, UNDO_MUTATION_CONFLICT_NOTICE, } from "./proposals.js";
8
12
  export { assertWorkshopAuthorToolsExcludeDiskShell, buildStageAddProposal, buildTaskAddProposal, createWorkshopAuthorProfile, createWorkshopAuthorTools, isTaskProposalIntent, proposeStageFromUserMessage, proposeTaskFromUserMessage, WORKSHOP_AUTHOR_PLAYBOOK, WORKSHOP_AUTHOR_PROFILE_ID, WORKSHOP_AUTHOR_TOOL_NAMES, WORKSHOP_FORBIDDEN_AGENT_TOOLS, workshopAuthorTools, } from "./profiles/workshopAuthor.js";
@@ -19,6 +19,7 @@ import { globalStageflowHome } from "../project/globalHome.js";
19
19
  import { isUsableAuthFile, resolveCredentialBinding, } from "../runtime/credentialBinding.js";
20
20
  import { resolveWorkshopModel } from "../workshop/modelSettings.js";
21
21
  import { createOperatorAgentHost, invokeProfileTool, } from "./host.js";
22
+ import { createAgentAuthorProfile } from "./profiles/agentAuthor.js";
22
23
  import { createWorkshopAuthorProfile, } from "./profiles/workshopAuthor.js";
23
24
  import { readDraftFromContext } from "./draftContext.js";
24
25
  export function sessionNeedsExtensionReload(loaded, needed) {
@@ -544,5 +545,6 @@ export function createLiveWorkshopOperatorHost(options) {
544
545
  retriever,
545
546
  ...(projectRoot !== undefined ? { projectRoot } : {}),
546
547
  }),
548
+ createAgentAuthorProfile(),
547
549
  ]);
548
550
  }
@@ -0,0 +1,11 @@
1
+ import { type AgentAuthorContext, type AgentAuthorDraft } from "../agentAuthorContext.js";
2
+ import type { OperatorAgentProfile } from "../types.js";
3
+ export declare const AGENT_AUTHOR_PROFILE_ID = "agent-author";
4
+ /** Objects merge key by key; arrays and scalars replace; `null` removes the key. */
5
+ export declare function mergeDraftPatch(draft: AgentAuthorDraft, patch: Record<string, unknown>): AgentAuthorDraft;
6
+ export declare const AGENT_AUTHOR_TOOL_NAMES: readonly ["read_project_file", "list_project_files", "list_mcp_servers", "update_agent_draft"];
7
+ export declare function createAgentAuthorProfile(): OperatorAgentProfile;
8
+ export declare function createAgentAuthorContext(input: {
9
+ projectRoot: string;
10
+ draft?: AgentAuthorDraft;
11
+ }): AgentAuthorContext;
@@ -0,0 +1,192 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { agentAuthorContextAdapter, emptyAgentAuthorDraft, isAgentAuthorContext, readAgentAuthorDraft, } from "../agentAuthorContext.js";
4
+ import { AGENT_ID_PATTERN, parseAgentDefinition } from "../../config/loadAgent.js";
5
+ import { listProjectMcpCatalog } from "../../config/resolveStageMcpServers.js";
6
+ import { AGENT_AUTHOR_PLAYBOOK } from "./agentAuthorPlaybook.js";
7
+ export const AGENT_AUTHOR_PROFILE_ID = "agent-author";
8
+ const MAX_READ_CHARS = 32_000;
9
+ let proposalSeq = 0;
10
+ function nextProposalId() {
11
+ proposalSeq += 1;
12
+ return `agent-author-${proposalSeq}`;
13
+ }
14
+ function projectRootOf(ctx) {
15
+ const context = ctx.getContext();
16
+ if (!isAgentAuthorContext(context))
17
+ return undefined;
18
+ const root = context.projectRoot?.trim();
19
+ return root || undefined;
20
+ }
21
+ function rejectEscape(root, rel) {
22
+ if (!rel || path.isAbsolute(rel) || rel.includes("\0"))
23
+ return "path must be a relative project path";
24
+ const parts = rel.split(/[/\\]/);
25
+ if (parts.some((part) => part === ".." || part === ".git" || part === "node_modules")) {
26
+ return "path is outside the readable project surface";
27
+ }
28
+ const target = path.resolve(root, rel);
29
+ const base = path.resolve(root);
30
+ if (target !== base && !target.startsWith(base + path.sep))
31
+ return "path escapes the project";
32
+ return null;
33
+ }
34
+ function stringArg(args, key) {
35
+ const value = args[key];
36
+ return typeof value === "string" ? value : undefined;
37
+ }
38
+ function isPlainObject(value) {
39
+ return value !== null && typeof value === "object" && !Array.isArray(value);
40
+ }
41
+ /** Objects merge key by key; arrays and scalars replace; `null` removes the key. */
42
+ export function mergeDraftPatch(draft, patch) {
43
+ const next = structuredClone(draft);
44
+ for (const [key, value] of Object.entries(patch)) {
45
+ if (value === null)
46
+ delete next[key];
47
+ else if (isPlainObject(value) && isPlainObject(next[key])) {
48
+ next[key] = mergeDraftPatch(next[key], value);
49
+ }
50
+ else
51
+ next[key] = structuredClone(value);
52
+ }
53
+ return next;
54
+ }
55
+ function slugId(name) {
56
+ const slug = name
57
+ .trim()
58
+ .toLowerCase()
59
+ .replace(/[^a-z0-9]+/g, "_")
60
+ .replace(/^_+|_+$/g, "")
61
+ .replace(/^[^a-z]+/, "")
62
+ .slice(0, 64);
63
+ return AGENT_ID_PATTERN.test(slug) ? slug : "";
64
+ }
65
+ const readProjectFile = {
66
+ name: "read_project_file",
67
+ description: "Read a text file inside the operator project. Relative path only. Refuses .git, node_modules, and paths outside the project. Does not write.",
68
+ async handler(args, ctx) {
69
+ const root = projectRootOf(ctx);
70
+ const rel = stringArg(args, "path")?.trim();
71
+ if (!root)
72
+ return { ok: false, content: null, error: "project root is not set" };
73
+ if (!rel)
74
+ return { ok: false, content: null, error: "path is required" };
75
+ const rejected = rejectEscape(root, rel);
76
+ if (rejected)
77
+ return { ok: false, content: null, error: rejected };
78
+ try {
79
+ const bytes = await readFile(path.resolve(root, rel));
80
+ if (bytes.includes(0))
81
+ return { ok: false, content: null, error: "file is not text" };
82
+ const text = bytes.toString("utf8");
83
+ return {
84
+ ok: true,
85
+ content: {
86
+ path: rel,
87
+ text: text.length > MAX_READ_CHARS ? `${text.slice(0, MAX_READ_CHARS)}\n…truncated` : text,
88
+ },
89
+ };
90
+ }
91
+ catch {
92
+ return { ok: false, content: null, error: "file is not readable" };
93
+ }
94
+ },
95
+ };
96
+ const listProjectFiles = {
97
+ name: "list_project_files",
98
+ description: "List readable project files: AGENTS.md when present, and YAML files under agents/. Does not write.",
99
+ async handler(_args, ctx) {
100
+ const root = projectRootOf(ctx);
101
+ if (!root)
102
+ return { ok: false, content: null, error: "project root is not set" };
103
+ const files = [];
104
+ try {
105
+ await readFile(path.join(root, "AGENTS.md"));
106
+ files.push("AGENTS.md");
107
+ }
108
+ catch {
109
+ /* absent */
110
+ }
111
+ try {
112
+ const entries = await readdir(path.join(root, "agents"), { withFileTypes: true });
113
+ for (const entry of entries) {
114
+ if (entry.isFile() && /\.ya?ml$/i.test(entry.name))
115
+ files.push(`agents/${entry.name}`);
116
+ }
117
+ }
118
+ catch {
119
+ /* no agents dir */
120
+ }
121
+ return { ok: true, content: { files } };
122
+ },
123
+ };
124
+ const listMcpServers = {
125
+ name: "list_mcp_servers",
126
+ description: "List MCP server names declared in the project's .mcp.json. Use these names under tools.mcp. Does not connect.",
127
+ async handler(_args, ctx) {
128
+ const root = projectRootOf(ctx);
129
+ if (!root)
130
+ return { ok: false, content: null, error: "project root is not set" };
131
+ const catalog = await listProjectMcpCatalog(root);
132
+ return { ok: true, content: { status: catalog.status, servers: catalog.servers.map((server) => server.name) } };
133
+ },
134
+ };
135
+ const updateAgentDraft = {
136
+ name: "update_agent_draft",
137
+ description: [
138
+ "Change the unsaved agent draft. Arguments: patch (object, required) and summary (one short sentence).",
139
+ "patch uses the agent YAML schema: objects merge key by key, arrays replace, null removes a key.",
140
+ "The result lists validation issues; fix them in a follow-up patch. Does not write YAML.",
141
+ ].join(" "),
142
+ handler(args, ctx) {
143
+ const patch = args.patch;
144
+ if (!isPlainObject(patch))
145
+ return { ok: false, content: null, error: "patch must be an object" };
146
+ const currentContext = ctx.getContext();
147
+ const current = readAgentAuthorDraft(currentContext);
148
+ const next = mergeDraftPatch(current, patch);
149
+ if (typeof next.name === "string" && next.name.trim() && !(typeof next.id === "string" && next.id.trim())) {
150
+ const id = slugId(next.name);
151
+ if (id)
152
+ next.id = id;
153
+ }
154
+ const parsed = parseAgentDefinition(next);
155
+ const summary = stringArg(args, "summary")?.trim() || "Updated the agent draft";
156
+ const proposal = {
157
+ id: nextProposalId(),
158
+ summary,
159
+ baseContext: currentContext,
160
+ nextContext: {
161
+ kind: "agent_author",
162
+ ...(isAgentAuthorContext(currentContext) && currentContext.projectRoot
163
+ ? { projectRoot: currentContext.projectRoot }
164
+ : {}),
165
+ draft: next,
166
+ },
167
+ };
168
+ ctx.emitProposal(proposal);
169
+ return {
170
+ ok: true,
171
+ content: { summary, valid: parsed.ok, issues: parsed.ok ? [] : parsed.issues.map((issue) => `${issue.path || "(draft)"}: ${issue.message}`) },
172
+ };
173
+ },
174
+ };
175
+ export const AGENT_AUTHOR_TOOL_NAMES = ["read_project_file", "list_project_files", "list_mcp_servers", "update_agent_draft"];
176
+ export function createAgentAuthorProfile() {
177
+ return {
178
+ id: AGENT_AUTHOR_PROFILE_ID,
179
+ title: "Agent author",
180
+ playbook: AGENT_AUTHOR_PLAYBOOK,
181
+ greeting: "What should this agent do?",
182
+ tools: [readProjectFile, listProjectFiles, listMcpServers, updateAgentDraft],
183
+ contextAdapter: agentAuthorContextAdapter(),
184
+ };
185
+ }
186
+ export function createAgentAuthorContext(input) {
187
+ return {
188
+ kind: "agent_author",
189
+ projectRoot: input.projectRoot,
190
+ draft: { ...emptyAgentAuthorDraft(), ...structuredClone(input.draft ?? {}) },
191
+ };
192
+ }
@@ -0,0 +1 @@
1
+ export declare const AGENT_AUTHOR_PLAYBOOK = "You are the Stageflow Agent author. You draft one sealed project agent with the operator. You are not a task runner, and you do not write files.\n\n## How you talk\n- Short turns, plain words, present tense. One to four sentences.\n- Do not narrate tool names. Say what changed in the config.\n- The config panel shows the draft. The operator can edit it directly; your next patch builds on what they changed.\n\n## Tools\n- read_project_file and list_project_files are read-only. Use them when the operator points at the project, an existing agent file, or AGENTS.md.\n- list_mcp_servers names the servers in .mcp.json. Only grant MCP servers that exist there; if the operator wants one that isn't there, say so.\n- update_agent_draft applies a patch to the draft immediately. Call it as soon as you know enough. Do not wait for Keep. If it reports issues, fix them with another patch.\n- Never use bash, write, or edit. Save is the operator's button, not a tool.\n\n## The draft (agents/<id>.agent.yaml schema)\n- id (snake_case, starts with a letter; derived from name when omitted), name, description, greeting, instructions (required).\n- model: { backend: pi | claude, id?: \"provider/model\", thinking: low | medium | high }. Leave id out to use the project default unless they name one, and say so.\n- tools: { builtin: [read | write | shell] (whole groups), builtin_tools: [read | grep | find | ls | write | edit | bash] (single tools), stageflow: { groups: [...], tools: [...] }, mcp: { <server>: [tool names] or [\"*\"] }, skills: [names], browser: bool }.\n- approvals: { ask_first: [tool ids], approvers: { operators: bool, channel_ids: [] }, timeout_minutes, on_timeout: skip | deny }.\n Tool ids: builtin names (read, grep, find, ls, write, edit, bash), \"<server>.<tool>\" for MCP, \"agent:<id>\" for sub-agents. ask_first may only name granted tools.\n- sub_agents: [agent ids], observability: { capture: off | metadata | full }, limits: { session_minutes, max_turns, cost_usd | null, long_context: summarize | truncate }, guardrails: { block_patterns: [JavaScript regex] }, schedule: { cron, timezone?, message? }.\n\n## Defaults\n- A new agent saves disabled and starts sealed: read only. Write and shell stay off unless the job needs them.\n- Anything that sends, posts, writes, merges or deletes goes in approvals.ask_first.\n- Capture stays metadata unless they ask for full content.\n\n## After an edit\n- One sentence on what the agent will do now. Mention any assumption or open question. Do not paste YAML unless they ask.\n";