stagent 0.9.5 → 0.10.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 (277) hide show
  1. package/README.md +5 -42
  2. package/dist/cli.js +42 -18
  3. package/docs/.coverage-gaps.json +13 -55
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/provider-runtimes.md +4 -0
  6. package/docs/features/schedules.md +32 -4
  7. package/docs/features/settings.md +28 -5
  8. package/docs/features/tables.md +9 -2
  9. package/docs/features/workflows.md +10 -4
  10. package/docs/journeys/developer.md +15 -1
  11. package/docs/journeys/personal-use.md +21 -4
  12. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  13. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  14. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  15. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  16. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  17. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  18. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  19. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  20. package/package.json +3 -2
  21. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  22. package/src/app/analytics/page.tsx +1 -21
  23. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  24. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  25. package/src/app/api/instance/config/route.ts +41 -0
  26. package/src/app/api/instance/init/route.ts +34 -0
  27. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  28. package/src/app/api/instance/upgrade/route.ts +96 -0
  29. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  30. package/src/app/api/memory/route.ts +0 -11
  31. package/src/app/api/notifications/route.ts +4 -2
  32. package/src/app/api/projects/[id]/route.ts +5 -155
  33. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  34. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  35. package/src/app/api/schedules/[id]/route.ts +9 -1
  36. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  37. package/src/app/api/schedules/route.ts +3 -12
  38. package/src/app/api/settings/openai/login/route.ts +22 -0
  39. package/src/app/api/settings/openai/logout/route.ts +7 -0
  40. package/src/app/api/settings/openai/route.ts +21 -1
  41. package/src/app/api/settings/providers/route.ts +35 -8
  42. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  43. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  44. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  45. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  46. package/src/app/api/tasks/[id]/execute/route.ts +0 -21
  47. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  48. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  49. package/src/app/api/workspace/context/route.ts +2 -0
  50. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  51. package/src/app/chat/page.tsx +11 -0
  52. package/src/app/inbox/page.tsx +12 -5
  53. package/src/app/layout.tsx +42 -21
  54. package/src/app/page.tsx +0 -2
  55. package/src/app/settings/page.tsx +6 -9
  56. package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
  57. package/src/components/chat/chat-command-popover.tsx +2 -2
  58. package/src/components/chat/chat-input.tsx +2 -3
  59. package/src/components/chat/chat-session-provider.tsx +720 -0
  60. package/src/components/chat/chat-shell.tsx +92 -401
  61. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  62. package/src/components/instance/instance-section.tsx +382 -0
  63. package/src/components/instance/upgrade-badge.tsx +219 -0
  64. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  65. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  66. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  67. package/src/components/notifications/inbox-list.tsx +11 -2
  68. package/src/components/notifications/notification-item.tsx +56 -2
  69. package/src/components/notifications/pending-approval-host.tsx +56 -37
  70. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  71. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  72. package/src/components/schedules/schedule-form.tsx +31 -0
  73. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  74. package/src/components/settings/auth-method-selector.tsx +19 -4
  75. package/src/components/settings/auth-status-badge.tsx +28 -3
  76. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  77. package/src/components/settings/openai-runtime-section.tsx +7 -1
  78. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  79. package/src/components/shared/app-sidebar.tsx +4 -3
  80. package/src/components/shared/command-palette.tsx +4 -5
  81. package/src/components/shared/theme-toggle.tsx +5 -24
  82. package/src/components/shared/workspace-indicator.tsx +61 -2
  83. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  84. package/src/components/tables/table-create-sheet.tsx +4 -0
  85. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  86. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  87. package/src/components/tables/table-spreadsheet.tsx +29 -5
  88. package/src/components/tables/table-toolbar.tsx +10 -1
  89. package/src/components/tasks/kanban-board.tsx +1 -0
  90. package/src/components/tasks/kanban-column.tsx +53 -14
  91. package/src/components/tasks/task-bento-grid.tsx +19 -0
  92. package/src/components/tasks/task-card.tsx +26 -3
  93. package/src/components/tasks/task-chip-bar.tsx +24 -0
  94. package/src/components/tasks/task-result-renderer.tsx +1 -1
  95. package/src/components/workflows/delay-step-body.tsx +109 -0
  96. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  97. package/src/components/workflows/loop-status-view.tsx +1 -1
  98. package/src/components/workflows/shared/step-result.tsx +78 -0
  99. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  100. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  101. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  102. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  103. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  104. package/src/components/workflows/workflow-form-view.tsx +133 -16
  105. package/src/components/workflows/workflow-status-view.tsx +30 -740
  106. package/src/instrumentation-node.ts +94 -0
  107. package/src/instrumentation.ts +4 -48
  108. package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
  109. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  110. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  111. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  112. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  113. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  114. package/src/lib/agents/claude-agent.ts +155 -18
  115. package/src/lib/agents/execution-manager.ts +0 -35
  116. package/src/lib/agents/learned-context.ts +0 -12
  117. package/src/lib/agents/learning-session.ts +18 -5
  118. package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
  119. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
  120. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
  121. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  122. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  123. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  124. package/src/lib/agents/runtime/openai-codex.ts +29 -60
  125. package/src/lib/agents/runtime/types.ts +8 -0
  126. package/src/lib/book/chapter-mapping.ts +11 -0
  127. package/src/lib/book/content.ts +10 -0
  128. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  129. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  130. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  131. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  132. package/src/lib/chat/active-streams.ts +27 -0
  133. package/src/lib/chat/codex-engine.ts +16 -17
  134. package/src/lib/chat/context-builder.ts +5 -3
  135. package/src/lib/chat/engine.ts +50 -3
  136. package/src/lib/chat/reconcile.ts +117 -0
  137. package/src/lib/chat/stagent-tools.ts +1 -0
  138. package/src/lib/chat/stream-telemetry.ts +132 -0
  139. package/src/lib/chat/suggested-prompts.ts +28 -1
  140. package/src/lib/chat/system-prompt.ts +26 -1
  141. package/src/lib/chat/tool-catalog.ts +2 -1
  142. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  143. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  144. package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
  145. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
  146. package/src/lib/chat/tools/document-tools.ts +29 -13
  147. package/src/lib/chat/tools/helpers.ts +39 -0
  148. package/src/lib/chat/tools/notification-tools.ts +9 -5
  149. package/src/lib/chat/tools/project-tools.ts +33 -0
  150. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  151. package/src/lib/chat/tools/table-tools.ts +71 -0
  152. package/src/lib/chat/tools/task-tools.ts +84 -20
  153. package/src/lib/chat/tools/workflow-tools.ts +234 -32
  154. package/src/lib/constants/settings.ts +8 -18
  155. package/src/lib/data/__tests__/clear.test.ts +56 -2
  156. package/src/lib/data/clear.ts +20 -15
  157. package/src/lib/data/delete-project.ts +171 -0
  158. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  159. package/src/lib/db/bootstrap.ts +45 -16
  160. package/src/lib/db/index.ts +5 -0
  161. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  162. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  163. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  164. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  165. package/src/lib/db/migrations/meta/_journal.json +21 -0
  166. package/src/lib/db/schema.ts +68 -23
  167. package/src/lib/environment/workspace-context.ts +13 -1
  168. package/src/lib/import/dedup.ts +4 -54
  169. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  170. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  171. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  172. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  173. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  174. package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
  175. package/src/lib/instance/bootstrap.ts +270 -0
  176. package/src/lib/instance/detect.ts +49 -0
  177. package/src/lib/instance/fingerprint.ts +78 -0
  178. package/src/lib/instance/git-ops.ts +95 -0
  179. package/src/lib/instance/settings.ts +61 -0
  180. package/src/lib/instance/types.ts +77 -0
  181. package/src/lib/instance/upgrade-poller.ts +153 -0
  182. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  183. package/src/lib/notifications/visibility.ts +33 -0
  184. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  185. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  186. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  187. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  188. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  189. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  190. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  191. package/src/lib/schedules/collision-check.ts +105 -0
  192. package/src/lib/schedules/config.ts +53 -0
  193. package/src/lib/schedules/scheduler.ts +232 -13
  194. package/src/lib/schedules/slot-claim.ts +105 -0
  195. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  196. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  197. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  198. package/src/lib/settings/openai-auth.ts +105 -10
  199. package/src/lib/settings/openai-login-manager.ts +260 -0
  200. package/src/lib/settings/runtime-setup.ts +14 -4
  201. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  202. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  203. package/src/lib/tables/enrichment-planner.ts +454 -0
  204. package/src/lib/tables/enrichment.ts +328 -0
  205. package/src/lib/tables/query-builder.ts +5 -2
  206. package/src/lib/tables/trigger-evaluator.ts +3 -2
  207. package/src/lib/theme.ts +71 -0
  208. package/src/lib/usage/ledger.ts +2 -18
  209. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  210. package/src/lib/util/similarity.ts +77 -0
  211. package/src/lib/utils/format-timestamp.ts +24 -0
  212. package/src/lib/utils/stagent-paths.ts +12 -0
  213. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  214. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  215. package/src/lib/validators/blueprint.ts +70 -9
  216. package/src/lib/validators/profile.ts +2 -2
  217. package/src/lib/validators/settings.ts +3 -1
  218. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  219. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  220. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  221. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  222. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  223. package/src/lib/workflows/blueprints/types.ts +10 -2
  224. package/src/lib/workflows/delay.ts +106 -0
  225. package/src/lib/workflows/engine.ts +207 -4
  226. package/src/lib/workflows/loop-executor.ts +349 -24
  227. package/src/lib/workflows/post-action.ts +91 -0
  228. package/src/lib/workflows/types.ts +166 -1
  229. package/src/app/api/license/checkout/route.ts +0 -28
  230. package/src/app/api/license/portal/route.ts +0 -26
  231. package/src/app/api/license/route.ts +0 -89
  232. package/src/app/api/license/usage/route.ts +0 -63
  233. package/src/app/api/marketplace/browse/route.ts +0 -15
  234. package/src/app/api/marketplace/import/route.ts +0 -28
  235. package/src/app/api/marketplace/publish/route.ts +0 -40
  236. package/src/app/api/onboarding/email/route.ts +0 -53
  237. package/src/app/api/settings/telemetry/route.ts +0 -14
  238. package/src/app/api/sync/export/route.ts +0 -54
  239. package/src/app/api/sync/restore/route.ts +0 -37
  240. package/src/app/api/sync/sessions/route.ts +0 -24
  241. package/src/app/auth/callback/route.ts +0 -73
  242. package/src/app/marketplace/page.tsx +0 -19
  243. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  244. package/src/components/marketplace/blueprint-card.tsx +0 -61
  245. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  246. package/src/components/onboarding/email-capture-card.tsx +0 -104
  247. package/src/components/settings/activation-form.tsx +0 -95
  248. package/src/components/settings/cloud-account-section.tsx +0 -147
  249. package/src/components/settings/cloud-sync-section.tsx +0 -155
  250. package/src/components/settings/subscription-section.tsx +0 -410
  251. package/src/components/settings/telemetry-section.tsx +0 -80
  252. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  253. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  254. package/src/components/shared/upgrade-banner.tsx +0 -112
  255. package/src/hooks/use-supabase-auth.ts +0 -79
  256. package/src/lib/billing/email.ts +0 -54
  257. package/src/lib/billing/products.ts +0 -80
  258. package/src/lib/billing/stripe.ts +0 -101
  259. package/src/lib/cloud/supabase-browser.ts +0 -32
  260. package/src/lib/cloud/supabase-client.ts +0 -56
  261. package/src/lib/license/__tests__/features.test.ts +0 -56
  262. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  263. package/src/lib/license/__tests__/manager.test.ts +0 -64
  264. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  265. package/src/lib/license/cloud-validation.ts +0 -60
  266. package/src/lib/license/features.ts +0 -44
  267. package/src/lib/license/key-format.ts +0 -101
  268. package/src/lib/license/limit-check.ts +0 -111
  269. package/src/lib/license/limit-queries.ts +0 -51
  270. package/src/lib/license/manager.ts +0 -345
  271. package/src/lib/license/notifications.ts +0 -59
  272. package/src/lib/license/tier-limits.ts +0 -71
  273. package/src/lib/marketplace/marketplace-client.ts +0 -107
  274. package/src/lib/sync/cloud-sync.ts +0 -235
  275. package/src/lib/telemetry/conversion-events.ts +0 -71
  276. package/src/lib/telemetry/queue.ts +0 -122
  277. package/src/lib/validators/license.ts +0 -33
@@ -73,10 +73,16 @@ const {
73
73
  mockRunMetaCompletion,
74
74
  mockGetActiveLearnedContext,
75
75
  mockProposeContextAddition,
76
+ mockGetTaskWorkflowId,
77
+ mockHasLearningSession,
78
+ mockBufferProposal,
76
79
  } = vi.hoisted(() => ({
77
80
  mockRunMetaCompletion: vi.fn(),
78
81
  mockGetActiveLearnedContext: vi.fn().mockReturnValue(null),
79
82
  mockProposeContextAddition: vi.fn().mockResolvedValue("notif-123"),
83
+ mockGetTaskWorkflowId: vi.fn().mockReturnValue(null),
84
+ mockHasLearningSession: vi.fn().mockReturnValue(false),
85
+ mockBufferProposal: vi.fn(),
80
86
  }));
81
87
 
82
88
  vi.mock("../runtime/claude", () => ({
@@ -87,6 +93,11 @@ vi.mock("../learned-context", () => ({
87
93
  getActiveLearnedContext: mockGetActiveLearnedContext,
88
94
  proposeContextAddition: mockProposeContextAddition,
89
95
  }));
96
+ vi.mock("../learning-session", () => ({
97
+ getTaskWorkflowId: mockGetTaskWorkflowId,
98
+ hasLearningSession: mockHasLearningSession,
99
+ bufferProposal: mockBufferProposal,
100
+ }));
90
101
 
91
102
  import { analyzeForLearnedPatterns } from "../pattern-extractor";
92
103
 
@@ -103,6 +114,9 @@ beforeEach(() => {
103
114
  mockValues.mockResolvedValue(undefined);
104
115
  mockGetActiveLearnedContext.mockReturnValue(null);
105
116
  mockProposeContextAddition.mockResolvedValue("notif-123");
117
+ mockGetTaskWorkflowId.mockReturnValue(null);
118
+ mockHasLearningSession.mockReturnValue(false);
119
+ mockBufferProposal.mockReset();
106
120
  });
107
121
 
108
122
  // ═════════════════════════════════════════════════════════════════════
@@ -240,4 +254,38 @@ describe("analyzeForLearnedPatterns", () => {
240
254
  expect(additions).toContain("Description A");
241
255
  expect(additions).toContain("Description B");
242
256
  });
257
+
258
+ it("buffers workflow proposals into the active learning session", async () => {
259
+ mockWhere.mockResolvedValueOnce([
260
+ { title: "Task", description: "Desc", result: "Done" },
261
+ ]);
262
+ mockLimit.mockResolvedValueOnce([]);
263
+ mockRunMetaCompletion.mockResolvedValue({
264
+ text: JSON.stringify([
265
+ {
266
+ title: "Pattern A",
267
+ description: "Description A",
268
+ category: "best_practice",
269
+ },
270
+ ]),
271
+ usage: {},
272
+ });
273
+ mockGetTaskWorkflowId.mockReturnValue("workflow-1");
274
+ mockHasLearningSession.mockReturnValue(true);
275
+ mockProposeContextAddition.mockResolvedValue("proposal-row-1");
276
+
277
+ const result = await analyzeForLearnedPatterns("task-1", "general");
278
+
279
+ expect(result).toBe("proposal-row-1");
280
+ expect(mockProposeContextAddition).toHaveBeenCalledWith(
281
+ "general",
282
+ "task-1",
283
+ expect.stringContaining("Pattern A"),
284
+ { silent: true }
285
+ );
286
+ expect(mockBufferProposal).toHaveBeenCalledWith(
287
+ "workflow-1",
288
+ "proposal-row-1"
289
+ );
290
+ });
243
291
  });
@@ -35,6 +35,102 @@ import {
35
35
  clearPermissionCache,
36
36
  } from "./tool-permissions";
37
37
 
38
+ // ─── Stagent MCP injection helpers ──────────────────────────────────────
39
+ //
40
+ // Shared by executeClaudeTask and resumeClaudeTask so the two runtime entry
41
+ // points cannot drift apart. The drift between chat engine injection and
42
+ // claude-code runtime injection is what produced the P0 bug this feature
43
+ // fixes — do not duplicate these patterns inline.
44
+
45
+ /**
46
+ * Merge the in-process stagent MCP server into a profile/browser/external
47
+ * MCP server map. Stagent is spread LAST so no upstream source can shadow
48
+ * the `stagent` key with its own server.
49
+ *
50
+ * `@/lib/chat/stagent-tools` is loaded via dynamic `import()` to avoid a
51
+ * circular-dependency crash: that module transitively pulls in the chat
52
+ * tools registry, which imports the runtime registry (`runtime/catalog`,
53
+ * `runtime/index`), which statically references `claudeRuntimeAdapter` —
54
+ * the very module this file is defined in. A static import here would
55
+ * crash with "Cannot access 'claudeRuntimeAdapter' before initialization"
56
+ * at module-load time. The dynamic import defers the stagent-tools module
57
+ * until `executeClaudeTask` / `resumeClaudeTask` actually run, by which
58
+ * time every module in the graph has finished initializing.
59
+ */
60
+ async function withStagentMcpServer(
61
+ profileServers: Record<string, unknown>,
62
+ browserServers: Record<string, unknown>,
63
+ externalServers: Record<string, unknown>,
64
+ projectId?: string | null,
65
+ ): Promise<Record<string, unknown>> {
66
+ const { createToolServer } = await import("@/lib/chat/stagent-tools");
67
+ const stagentServer = createToolServer(projectId).asMcpServer();
68
+ return {
69
+ ...profileServers,
70
+ ...browserServers,
71
+ ...externalServers,
72
+ stagent: stagentServer,
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Prepend `mcp__stagent__*` to a profile's explicit allowedTools so the
78
+ * stagent tool registration survives the SDK preset filter. Returns
79
+ * `undefined` when the profile has no allowedTools — callers should spread
80
+ * the result conditionally so the SDK falls through to preset defaults in
81
+ * that case.
82
+ */
83
+ function withStagentAllowedTools(
84
+ profileAllowedTools: string[] | undefined,
85
+ ): string[] | undefined {
86
+ if (!profileAllowedTools) return undefined;
87
+ return Array.from(new Set(["mcp__stagent__*", ...profileAllowedTools]));
88
+ }
89
+
90
+ /**
91
+ * Classify an error into a machine-readable failure reason string.
92
+ * Used by writeTerminalFailureReason and handleExecutionError.
93
+ */
94
+ function classifyError(error: unknown): string {
95
+ if (!(error instanceof Error)) return "sdk_error";
96
+ if (error.name === "AbortError" || error.message.includes("aborted")) {
97
+ return "aborted";
98
+ }
99
+ const lower = error.message.toLowerCase();
100
+ if (
101
+ lower.includes("turn") &&
102
+ (lower.includes("limit") || lower.includes("exhausted") || lower.includes("max"))
103
+ ) {
104
+ return "turn_limit_exceeded";
105
+ }
106
+ if (lower.includes("timeout") || lower.includes("timed out")) return "timeout";
107
+ if (lower.includes("budget")) return "budget_exceeded";
108
+ if (lower.includes("authentication") || lower.includes("oauth")) {
109
+ return "auth_failed";
110
+ }
111
+ if (lower.includes("rate limit") || lower.includes("429")) {
112
+ return "rate_limited";
113
+ }
114
+ return "sdk_error";
115
+ }
116
+
117
+ /**
118
+ * Write an explicit failure_reason to tasks at terminal-state transitions.
119
+ * Called from handleExecutionError and the execute/resume functions on known
120
+ * error classes. Prefer this over reverse-engineering reasons from text via
121
+ * detectFailureReason in scheduler.ts, which is fragile to SDK message changes.
122
+ */
123
+ export async function writeTerminalFailureReason(
124
+ taskId: string,
125
+ error: unknown,
126
+ ): Promise<void> {
127
+ const reason = classifyError(error);
128
+ await db
129
+ .update(tasks)
130
+ .set({ failureReason: reason, updatedAt: new Date() })
131
+ .where(eq(tasks.id, taskId));
132
+ }
133
+
38
134
  /** Typed representation of messages from the Agent SDK stream */
39
135
  interface AgentStreamMessage {
40
136
  type?: string;
@@ -314,11 +410,14 @@ async function processAgentStream(
314
410
  ? `Agent exhausted its turn limit (${turnCount} turns used) without producing a final result. The task may need fewer sub-queries or a higher maxTurns setting.`
315
411
  : "Agent stream ended without producing a result";
316
412
 
413
+ const streamFailureReason = turnCount > 0 ? "turn_limit_exceeded" : "sdk_error";
414
+
317
415
  await db
318
416
  .update(tasks)
319
417
  .set({
320
418
  status: "failed",
321
419
  result: errorDetail,
420
+ failureReason: streamFailureReason,
322
421
  updatedAt: new Date(),
323
422
  })
324
423
  .where(eq(tasks.id, taskId));
@@ -432,13 +531,30 @@ export async function executeClaudeTask(taskId: string): Promise<void> {
432
531
  await prepareTaskOutputDirectory(taskId, { clearExisting: true });
433
532
  const ctx = await buildTaskQueryContext(task, agentProfileId);
434
533
 
435
- // Merge browser + external MCP servers when enabled globally
534
+ // Per-schedule override: if the task carries its own maxTurns (set by
535
+ // fireSchedule from schedules.maxTurns), it takes precedence over the
536
+ // profile default. This is the runtime-enforced budget cap.
537
+ const effectiveMaxTurns = task.maxTurns ?? ctx.maxTurns;
538
+
539
+ // Merge browser + external MCP servers, then inject the in-process
540
+ // stagent server via the shared helper (see withStagentMcpServer above).
541
+ // The helper is async because it dynamically imports @/lib/chat/stagent-tools
542
+ // to break a module-load cycle with the runtime registry.
436
543
  const [browserServers, externalServers] = await Promise.all([
437
544
  getBrowserMcpServers(),
438
545
  getExternalMcpServers(),
439
546
  ]);
440
- const profileMcpServers = ctx.payload?.mcpServers ?? {};
441
- const mergedMcpServers = { ...profileMcpServers, ...browserServers, ...externalServers };
547
+ const mergedMcpServers = await withStagentMcpServer(
548
+ ctx.payload?.mcpServers ?? {},
549
+ browserServers,
550
+ externalServers,
551
+ task.projectId,
552
+ );
553
+ // allowedTools prepended via shared helper (see withStagentAllowedTools).
554
+ // Computed once so the conditional spread below does not invoke the
555
+ // helper twice. Returns undefined when the profile has no allowlist so
556
+ // the SDK falls through to claude_code preset defaults.
557
+ const mergedAllowedTools = withStagentAllowedTools(ctx.payload?.allowedTools);
442
558
 
443
559
  const authEnv = await getAuthEnv();
444
560
  const response = query({
@@ -452,11 +568,11 @@ export async function executeClaudeTask(taskId: string): Promise<void> {
452
568
  systemPrompt: ctx.systemInstructions
453
569
  ? { type: "preset" as const, preset: "claude_code" as const, append: ctx.systemInstructions }
454
570
  : { type: "preset" as const, preset: "claude_code" as const },
455
- // F9: Bounded turn limit from profile or default
456
- maxTurns: ctx.maxTurns,
571
+ // F9: Bounded turn limit from profile or default; per-schedule override wins
572
+ maxTurns: effectiveMaxTurns,
457
573
  // F4: Per-execution budget cap — use task-specific override if set
458
574
  maxBudgetUsd: task.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD,
459
- ...(ctx.payload?.allowedTools && { allowedTools: ctx.payload.allowedTools }),
575
+ ...(mergedAllowedTools && { allowedTools: mergedAllowedTools }),
460
576
  ...(Object.keys(mergedMcpServers).length > 0 && {
461
577
  mcpServers: mergedMcpServers,
462
578
  }),
@@ -479,10 +595,11 @@ export async function executeClaudeTask(taskId: string): Promise<void> {
479
595
  usageState
480
596
  );
481
597
 
482
- // Fire-and-forget pattern extraction for self-improvement
483
- analyzeForLearnedPatterns(taskId, agentProfileId).catch((err) => {
598
+ try {
599
+ await analyzeForLearnedPatterns(taskId, agentProfileId);
600
+ } catch (err) {
484
601
  console.error("[self-improvement] pattern extraction failed:", err);
485
- });
602
+ }
486
603
  } catch (error: unknown) {
487
604
  await handleExecutionError(
488
605
  taskId,
@@ -545,13 +662,28 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
545
662
  await prepareTaskOutputDirectory(taskId);
546
663
  const ctx = await buildTaskQueryContext(task, profileId);
547
664
 
548
- // Merge browser + external MCP servers when enabled globally
665
+ // Per-schedule override: if the task carries its own maxTurns (set by
666
+ // fireSchedule from schedules.maxTurns), it takes precedence over the
667
+ // profile default. This is the runtime-enforced budget cap.
668
+ const effectiveMaxTurns = task.maxTurns ?? ctx.maxTurns;
669
+
670
+ // Merge browser + external MCP servers, then inject the in-process
671
+ // stagent server via the shared helper (see withStagentMcpServer).
672
+ // Async for the same cycle-breaking reason as executeClaudeTask above.
549
673
  const [browserServers, externalServers] = await Promise.all([
550
674
  getBrowserMcpServers(),
551
675
  getExternalMcpServers(),
552
676
  ]);
553
- const profileMcpServers = ctx.payload?.mcpServers ?? {};
554
- const mergedMcpServers = { ...profileMcpServers, ...browserServers, ...externalServers };
677
+ const mergedMcpServers = await withStagentMcpServer(
678
+ ctx.payload?.mcpServers ?? {},
679
+ browserServers,
680
+ externalServers,
681
+ task.projectId,
682
+ );
683
+ // allowedTools prepended via shared helper (see withStagentAllowedTools).
684
+ // Computed once so the conditional spread below does not invoke the
685
+ // helper twice.
686
+ const mergedAllowedTools = withStagentAllowedTools(ctx.payload?.allowedTools);
555
687
 
556
688
  const authEnv = await getAuthEnv();
557
689
  const response = query({
@@ -566,11 +698,11 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
566
698
  systemPrompt: ctx.systemInstructions
567
699
  ? { type: "preset" as const, preset: "claude_code" as const, append: ctx.systemInstructions }
568
700
  : { type: "preset" as const, preset: "claude_code" as const },
569
- // F9: Bounded turn limit from profile or default
570
- maxTurns: ctx.maxTurns,
701
+ // F9: Bounded turn limit from profile or default; per-schedule override wins
702
+ maxTurns: effectiveMaxTurns,
571
703
  // F4: Per-execution budget cap — use task-specific override if set
572
704
  maxBudgetUsd: task.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD,
573
- ...(ctx.payload?.allowedTools && { allowedTools: ctx.payload.allowedTools }),
705
+ ...(mergedAllowedTools && { allowedTools: mergedAllowedTools }),
574
706
  ...(Object.keys(mergedMcpServers).length > 0 && {
575
707
  mcpServers: mergedMcpServers,
576
708
  }),
@@ -593,10 +725,11 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
593
725
  usageState
594
726
  );
595
727
 
596
- // Fire-and-forget pattern extraction for self-improvement
597
- analyzeForLearnedPatterns(taskId, profileId).catch((err) => {
728
+ try {
729
+ await analyzeForLearnedPatterns(taskId, profileId);
730
+ } catch (err) {
598
731
  console.error("[self-improvement] pattern extraction failed:", err);
599
- });
732
+ }
600
733
  } catch (error: unknown) {
601
734
  const errorMessage =
602
735
  error instanceof Error ? error.message : String(error);
@@ -612,6 +745,7 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
612
745
  status: "failed",
613
746
  result: "Session expired — re-queue for fresh start",
614
747
  sessionId: null,
748
+ failureReason: "auth_failed",
615
749
  updatedAt: new Date(),
616
750
  })
617
751
  .where(eq(tasks.id, taskId));
@@ -667,11 +801,14 @@ async function handleExecutionError(
667
801
  return;
668
802
  }
669
803
 
804
+ const failureReason = classifyError(error);
805
+
670
806
  await db
671
807
  .update(tasks)
672
808
  .set({
673
809
  status: "failed",
674
810
  result: errorMessage,
811
+ failureReason,
675
812
  updatedAt: new Date(),
676
813
  })
677
814
  .where(eq(tasks.id, taskId));
@@ -1,6 +1,3 @@
1
- import { licenseManager } from "@/lib/license/manager";
2
- import { createTierLimitNotification } from "@/lib/license/notifications";
3
-
4
1
  interface RunningExecution {
5
2
  abortController: AbortController;
6
3
  sessionId: string | null;
@@ -17,42 +14,10 @@ export function getExecution(taskId: string): RunningExecution | undefined {
17
14
  return executions.get(taskId);
18
15
  }
19
16
 
20
- /**
21
- * Register a running execution. Checks the parallel workflow limit
22
- * for the current tier before allowing the execution to proceed.
23
- *
24
- * @throws {ParallelLimitError} if the concurrent execution limit is reached
25
- */
26
17
  export function setExecution(taskId: string, execution: RunningExecution): void {
27
- const limit = licenseManager.getLimit("parallelWorkflows");
28
- const currentCount = executions.size;
29
-
30
- if (Number.isFinite(limit) && currentCount >= limit) {
31
- const tier = licenseManager.getTier();
32
- // Fire-and-forget notification
33
- createTierLimitNotification("parallelWorkflows", currentCount, limit, taskId).catch(() => {});
34
- throw new ParallelLimitError(currentCount, limit, tier);
35
- }
36
-
37
18
  executions.set(taskId, execution);
38
19
  }
39
20
 
40
- export class ParallelLimitError extends Error {
41
- public readonly current: number;
42
- public readonly limit: number;
43
- public readonly tier: string;
44
-
45
- constructor(current: number, limit: number, tier: string) {
46
- super(
47
- `Parallel workflow limit reached (${current}/${limit}) on ${tier} tier. Wait for a running task to complete or upgrade.`
48
- );
49
- this.name = "ParallelLimitError";
50
- this.current = current;
51
- this.limit = limit;
52
- this.tier = tier;
53
- }
54
- }
55
-
56
21
  export function removeExecution(taskId: string): void {
57
22
  executions.delete(taskId);
58
23
  }
@@ -5,9 +5,6 @@ import type { LearnedContextRow } from "@/lib/db/schema";
5
5
  import { runMetaCompletion } from "./runtime/claude";
6
6
  import { getSettingSync } from "@/lib/settings/helpers";
7
7
  import { SETTINGS_KEYS } from "@/lib/constants/settings";
8
- import { checkLimit } from "@/lib/license/limit-check";
9
- import { getContextVersionCount } from "@/lib/license/limit-queries";
10
- import { createTierLimitNotification } from "@/lib/license/notifications";
11
8
 
12
9
  const DEFAULT_CONTEXT_CHAR_LIMIT = 8_000;
13
10
  const SUMMARIZATION_RATIO = 0.75;
@@ -95,15 +92,6 @@ export async function proposeContextAddition(
95
92
  additions: string,
96
93
  options?: { silent?: boolean }
97
94
  ): Promise<string> {
98
- // Tier limit check — context version cap per profile
99
- const versionCount = getContextVersionCount(profileId);
100
- const limitResult = checkLimit("contextVersions", versionCount);
101
- if (!limitResult.allowed) {
102
- createTierLimitNotification("contextVersions", versionCount, limitResult.limit, taskId).catch(() => {});
103
- throw new Error(
104
- `Context version limit reached (${versionCount}/${limitResult.limit}). Upgrade to unlock more capacity.`
105
- );
106
- }
107
95
 
108
96
  const version = getNextVersion(profileId);
109
97
  const notificationId = options?.silent ? null : crypto.randomUUID();
@@ -181,6 +181,7 @@ export async function batchApproveProposals(
181
181
  await import("./learned-context");
182
182
 
183
183
  let approved = 0;
184
+ const touchedProfileIds = new Set<string>();
184
185
  for (const rowId of proposalRowIds) {
185
186
  const [row] = db
186
187
  .select()
@@ -229,16 +230,28 @@ export async function batchApproveProposals(
229
230
  }
230
231
 
231
232
  approved++;
232
-
233
- const sizeInfo = checkContextSize(row.profileId);
234
- if (sizeInfo.needsSummarization) {
235
- await summarizeContext(row.profileId);
236
- }
233
+ touchedProfileIds.add(row.profileId);
237
234
  }
238
235
 
239
236
  // Mark the batch notification as responded
240
237
  await markBatchNotificationResponded(proposalRowIds, "approved");
241
238
 
239
+ const profilesNeedingSummarization = [...touchedProfileIds].filter(
240
+ (profileId) => checkContextSize(profileId).needsSummarization
241
+ );
242
+ void Promise.allSettled(
243
+ profilesNeedingSummarization.map(async (profileId) => {
244
+ try {
245
+ await summarizeContext(profileId);
246
+ } catch (error) {
247
+ console.error(
248
+ "[learning-session] Failed to summarize approved context batch:",
249
+ error
250
+ );
251
+ }
252
+ })
253
+ );
254
+
242
255
  return approved;
243
256
  }
244
257
 
@@ -49,7 +49,7 @@ describe("profile registry", () => {
49
49
  expect(general!.domain).toBe("work");
50
50
  });
51
51
 
52
- it("returns all 20 builtin profiles", () => {
52
+ it("returns all 21 builtin profiles", () => {
53
53
  const profiles = listProfiles().filter((p) => isBuiltin(p.id));
54
54
  const ids = profiles.map((p) => p.id);
55
55
 
@@ -68,14 +68,16 @@ describe("profile registry", () => {
68
68
  expect(ids).toContain("shopping-assistant");
69
69
  expect(ids).toContain("learning-coach");
70
70
  expect(ids).toContain("sweep");
71
- // 6 new business function profiles
71
+ // 6 business function profiles
72
72
  expect(ids).toContain("marketing-strategist");
73
73
  expect(ids).toContain("sales-researcher");
74
74
  expect(ids).toContain("customer-support-agent");
75
75
  expect(ids).toContain("financial-analyst");
76
76
  expect(ids).toContain("content-creator");
77
77
  expect(ids).toContain("operations-coordinator");
78
- expect(profiles.length).toBe(20);
78
+ // Clone lifecycle profile
79
+ expect(ids).toContain("upgrade-assistant");
80
+ expect(profiles.length).toBe(21);
79
81
  });
80
82
 
81
83
  it("getProfile returns undefined for unknown id", () => {
@@ -129,7 +131,7 @@ describe("profile registry", () => {
129
131
  (p) => p.domain === "personal"
130
132
  );
131
133
 
132
- expect(workProfiles.length).toBe(15); // general, code-reviewer, researcher, document-writer, project-manager, data-analyst, technical-writer, devops-engineer, sweep + 6 business profiles
134
+ expect(workProfiles.length).toBe(16); // general, code-reviewer, researcher, document-writer, project-manager, data-analyst, technical-writer, devops-engineer, sweep + 6 business profiles + upgrade-assistant
133
135
  expect(personalProfiles.length).toBe(5); // wealth-manager, travel-planner, health-fitness-coach, shopping-assistant, learning-coach
134
136
  });
135
137
 
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: upgrade-assistant
3
+ description: Guided interactive git merge of upstream stagent commits into the user's local instance branch
4
+ ---
5
+
6
+ You are the Upgrade Assistant for a stagent clone. Your job is to pull upstream commits from `origin/main` into the user's instance branch safely and interactively, surfacing merge conflicts in plain language so the user can decide how to resolve them.
7
+
8
+ ## Context for this upgrade
9
+
10
+ - **Instance branch:** `{{INSTANCE_BRANCH}}`
11
+ - **Upstream commits behind:** `{{COMMITS_BEHIND}}`
12
+ - **Data directory:** `{{DATA_DIR}}`
13
+ - **Working directory:** the current repo root
14
+
15
+ ## Crucial rules — read these before doing anything
16
+
17
+ 1. **Never modify `main` except by fast-forward.** After fetching, merge `origin/main` into local `main` with `--ff-only`. If that fast-forward fails, it means the user has local commits on `main` that aren't in `origin/main` — **stop and ask the user** whether to move them to `{{INSTANCE_BRANCH}}` or abort so they can review. Do not auto-resolve.
18
+
19
+ 2. **Never push any branch.** The pre-push hook blocks `{{INSTANCE_BRANCH}}` pushes, but you should not even attempt one. Your job ends at a local commit.
20
+
21
+ 3. **If any step fails, roll back.** On any error after the merge has begun, run `git merge --abort` and `git stash pop` (if you stashed earlier) before reporting the failure. Leave the working tree in the state the user started in.
22
+
23
+ 4. **Treat `local` identically to any named instance branch.** Users with a default single-clone setup have `{{INSTANCE_BRANCH}}=local`. Users running private domain clones have names like `wealth-mgr` or `investor-mgr`. The merge flow is identical in both cases.
24
+
25
+ 5. **Stop and ask the user on merge conflicts.** Do not guess. For each conflict, use the three canonical choices:
26
+ - **"Keep my version"** → `git checkout --ours <file>`
27
+ - **"Take main's version"** → `git checkout --theirs <file>`
28
+ - **"Show me the diff"** → `git diff <file>` and output the full conflict for manual review
29
+ After all conflicts are resolved, `git add` the files and continue the merge.
30
+
31
+ ## Standard merge flow
32
+
33
+ Execute these steps in order. Report progress as you go so the live log view shows the user what's happening.
34
+
35
+ 1. **Pre-flight check.** Run `git status` to confirm the working tree state. If there's uncommitted work, tell the user you'll stash it first.
36
+
37
+ 2. **Stash any work-in-progress.** If the working tree is dirty, run `git stash push -m "upgrade-session auto-stash"`. Record that you stashed — you need to pop it at the end.
38
+
39
+ 3. **Fetch origin.** Run `git fetch origin main`. This is the only network operation. If it fails, report the error and stop.
40
+
41
+ 4. **Fast-forward main.** Run `git checkout main` then `git merge --ff-only origin/main`. If `--ff-only` fails, stop and ask the user (see Rule 1).
42
+
43
+ 5. **Return to the instance branch.** Run `git checkout {{INSTANCE_BRANCH}}`.
44
+
45
+ 6. **Merge main into the instance branch.** Run `git merge main`. If there are conflicts, git will report them. For each conflicted file, use the three-choice flow (Rule 5).
46
+
47
+ 7. **Complete the merge commit.** After conflicts are resolved (or if there were none), run `git commit` to finalize the merge if git hasn't already done so automatically.
48
+
49
+ 8. **Reinstall dependencies if package-lock.json changed.** Check `git diff HEAD~1 HEAD -- package-lock.json`. If there are changes, run `npm install`.
50
+
51
+ 9. **Pop the stash.** If you stashed in step 2, run `git stash pop`. Then immediately run `git status` to check for conflicts. If any file shows "both modified" or "Unmerged", the stash pop conflicted — resolve each file using the same three-choice flow (Rule 5), then `git add` the resolved files. After all stash conflicts are resolved, run `git stash drop` to remove the stash entry (a conflicted pop does NOT auto-drop the stash). Finally, run `grep -rn "<<<<<<< \|>>>>>>>" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.json" .` to verify no conflict markers remain in the working tree.
52
+
53
+ 10. **Report completion.** Tell the user the merge is complete, how many new commits landed, whether dependencies were reinstalled, and that they should restart the dev server to apply changes.
54
+
55
+ ## Aborting
56
+
57
+ If the user clicks "Abort" during the session, or if any step fails irrecoverably, run:
58
+ - `git merge --abort` (safe to run even if no merge is in progress — it'll just exit 0)
59
+ - `git checkout {{INSTANCE_BRANCH}}` (return the user to where they started)
60
+ - If you stashed in step 2: run `git stash pop`. Then run `git status` — if there are conflicts from the pop, resolve them using the three-choice flow (Rule 5), `git add` the files, and `git stash drop`. Run `grep -rn "<<<<<<< \|>>>>>>>" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.json" .` to verify no markers remain
61
+
62
+ Then report the abort and what state the repo is in.
63
+
64
+ ## Guidelines
65
+
66
+ - Be concise but explanatory. The user is watching a live log — they need enough context to understand what's happening, not a novel.
67
+ - Always name the file path when asking about a conflict.
68
+ - If the user sends a natural-language question mid-merge ("what do these changes do?"), answer it based on the diff before proceeding.
69
+ - Never use `git push`. Never use `git rebase`. Never use `git reset --hard`. Never delete branches. Never touch the remote.
70
+ - If `git status` shows files you didn't touch and the user didn't mention, bring them up before assuming they're safe to stash.
@@ -0,0 +1,32 @@
1
+ id: upgrade-assistant
2
+ name: Upgrade Assistant
3
+ version: "1.0.0"
4
+ domain: work
5
+ tags: [upgrade, git, merge, maintenance, instance]
6
+ supportedRuntimes: [claude-code, anthropic-direct]
7
+ preferredRuntime: anthropic-direct
8
+
9
+ maxTurns: 40
10
+
11
+ author: stagent
12
+
13
+ # Bash tool allowlist — tight scope.
14
+ # The upgrade assistant should only ever run git and npm install, nothing else.
15
+ # Cross-reference: TDR-028 "self-upgrade via task pipeline, not chat tools"
16
+ # explains why this profile is the only safe surface for git shell access.
17
+ allowedTools:
18
+ - Bash(git fetch *)
19
+ - Bash(git status)
20
+ - Bash(git status *)
21
+ - Bash(git stash *)
22
+ - Bash(git checkout *)
23
+ - Bash(git merge *)
24
+ - Bash(git merge --abort)
25
+ - Bash(git commit *)
26
+ - Bash(git diff *)
27
+ - Bash(git rev-parse *)
28
+ - Bash(git log *)
29
+ - Bash(git branch *)
30
+ - Bash(npm install)
31
+ - Read
32
+ - Write