stagent 0.9.6 → 0.11.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 (396) hide show
  1. package/README.md +20 -44
  2. package/dist/cli.js +66 -18
  3. package/docs/.coverage-gaps.json +144 -56
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +12 -2
  6. package/docs/features/chat.md +40 -5
  7. package/docs/features/cost-usage.md +1 -1
  8. package/docs/features/documents.md +5 -2
  9. package/docs/features/inbox-notifications.md +10 -2
  10. package/docs/features/keyboard-navigation.md +12 -3
  11. package/docs/features/provider-runtimes.md +20 -2
  12. package/docs/features/schedules.md +32 -4
  13. package/docs/features/settings.md +28 -5
  14. package/docs/features/shared-components.md +7 -3
  15. package/docs/features/tables.md +11 -2
  16. package/docs/features/tool-permissions.md +6 -2
  17. package/docs/features/workflows.md +14 -4
  18. package/docs/index.md +1 -1
  19. package/docs/journeys/developer.md +39 -2
  20. package/docs/journeys/personal-use.md +32 -8
  21. package/docs/journeys/power-user.md +45 -14
  22. package/docs/journeys/work-use.md +17 -8
  23. package/docs/manifest.json +15 -15
  24. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  25. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  26. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  27. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  28. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  29. package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
  30. package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
  31. package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
  32. package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
  33. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  34. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  35. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  36. package/next.config.mjs +1 -0
  37. package/package.json +3 -2
  38. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  39. package/src/app/analytics/page.tsx +1 -21
  40. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  41. package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
  42. package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
  43. package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
  44. package/src/app/api/chat/export/route.ts +52 -0
  45. package/src/app/api/chat/files/search/route.ts +50 -0
  46. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  47. package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
  48. package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
  49. package/src/app/api/environment/skills/route.ts +13 -0
  50. package/src/app/api/instance/config/route.ts +41 -0
  51. package/src/app/api/instance/init/route.ts +34 -0
  52. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  53. package/src/app/api/instance/upgrade/route.ts +96 -0
  54. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  55. package/src/app/api/memory/route.ts +0 -11
  56. package/src/app/api/notifications/route.ts +4 -2
  57. package/src/app/api/projects/[id]/route.ts +5 -155
  58. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  59. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  60. package/src/app/api/schedules/[id]/route.ts +9 -1
  61. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  62. package/src/app/api/schedules/route.ts +3 -12
  63. package/src/app/api/settings/chat/pins/route.ts +94 -0
  64. package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
  65. package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
  66. package/src/app/api/settings/environment/route.ts +26 -0
  67. package/src/app/api/settings/openai/login/route.ts +22 -0
  68. package/src/app/api/settings/openai/logout/route.ts +7 -0
  69. package/src/app/api/settings/openai/route.ts +21 -1
  70. package/src/app/api/settings/providers/route.ts +35 -8
  71. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  72. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  73. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  74. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  75. package/src/app/api/tasks/[id]/execute/route.ts +52 -33
  76. package/src/app/api/tasks/[id]/respond/route.ts +31 -15
  77. package/src/app/api/tasks/[id]/resume/route.ts +24 -3
  78. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  79. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  80. package/src/app/api/workspace/context/route.ts +2 -0
  81. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  82. package/src/app/chat/page.tsx +11 -0
  83. package/src/app/documents/page.tsx +4 -1
  84. package/src/app/inbox/page.tsx +12 -5
  85. package/src/app/layout.tsx +42 -21
  86. package/src/app/page.tsx +0 -2
  87. package/src/app/settings/page.tsx +8 -9
  88. package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
  89. package/src/components/chat/__tests__/chat-session-provider.test.tsx +573 -0
  90. package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
  91. package/src/components/chat/capability-banner.tsx +68 -0
  92. package/src/components/chat/chat-command-popover.tsx +670 -49
  93. package/src/components/chat/chat-input.tsx +104 -10
  94. package/src/components/chat/chat-message.tsx +12 -3
  95. package/src/components/chat/chat-session-provider.tsx +790 -0
  96. package/src/components/chat/chat-shell.tsx +151 -401
  97. package/src/components/chat/command-tab-bar.tsx +68 -0
  98. package/src/components/chat/conversation-template-picker.tsx +421 -0
  99. package/src/components/chat/help-dialog.tsx +39 -0
  100. package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
  101. package/src/components/chat/skill-row.tsx +147 -0
  102. package/src/components/documents/document-browser.tsx +37 -19
  103. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  104. package/src/components/instance/instance-section.tsx +382 -0
  105. package/src/components/instance/upgrade-badge.tsx +219 -0
  106. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  107. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  108. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
  109. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  110. package/src/components/notifications/inbox-list.tsx +11 -2
  111. package/src/components/notifications/notification-item.tsx +56 -2
  112. package/src/components/notifications/pending-approval-host.tsx +56 -37
  113. package/src/components/notifications/permission-response-actions.tsx +155 -1
  114. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  115. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  116. package/src/components/schedules/schedule-form.tsx +31 -0
  117. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  118. package/src/components/settings/auth-method-selector.tsx +19 -4
  119. package/src/components/settings/auth-status-badge.tsx +28 -3
  120. package/src/components/settings/environment-section.tsx +102 -0
  121. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  122. package/src/components/settings/openai-runtime-section.tsx +7 -1
  123. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  124. package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
  125. package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
  126. package/src/components/shared/app-sidebar.tsx +4 -3
  127. package/src/components/shared/command-palette.tsx +266 -7
  128. package/src/components/shared/filter-hint.tsx +70 -0
  129. package/src/components/shared/filter-input.tsx +59 -0
  130. package/src/components/shared/saved-searches-manager.tsx +199 -0
  131. package/src/components/shared/theme-toggle.tsx +5 -24
  132. package/src/components/shared/workspace-indicator.tsx +61 -2
  133. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  134. package/src/components/tables/table-create-sheet.tsx +4 -0
  135. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  136. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  137. package/src/components/tables/table-spreadsheet.tsx +29 -5
  138. package/src/components/tables/table-toolbar.tsx +10 -1
  139. package/src/components/tasks/kanban-board.tsx +1 -0
  140. package/src/components/tasks/kanban-column.tsx +53 -14
  141. package/src/components/tasks/task-bento-grid.tsx +31 -2
  142. package/src/components/tasks/task-card.tsx +29 -3
  143. package/src/components/tasks/task-chip-bar.tsx +54 -1
  144. package/src/components/tasks/task-result-renderer.tsx +1 -1
  145. package/src/components/workflows/delay-step-body.tsx +109 -0
  146. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  147. package/src/components/workflows/loop-status-view.tsx +1 -1
  148. package/src/components/workflows/shared/step-result.tsx +78 -0
  149. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  150. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  151. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  152. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  153. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  154. package/src/components/workflows/workflow-form-view.tsx +133 -16
  155. package/src/components/workflows/workflow-status-view.tsx +30 -740
  156. package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
  157. package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
  158. package/src/hooks/use-active-skills.ts +110 -0
  159. package/src/hooks/use-chat-autocomplete.ts +120 -7
  160. package/src/hooks/use-enriched-skills.ts +19 -0
  161. package/src/hooks/use-pinned-entries.ts +104 -0
  162. package/src/hooks/use-recent-user-messages.ts +19 -0
  163. package/src/hooks/use-saved-searches.ts +142 -0
  164. package/src/instrumentation-node.ts +94 -0
  165. package/src/instrumentation.ts +4 -48
  166. package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
  167. package/src/lib/agents/__tests__/claude-agent.test.ts +212 -0
  168. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  169. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  170. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  171. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  172. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  173. package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
  174. package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
  175. package/src/lib/agents/claude-agent.ts +217 -21
  176. package/src/lib/agents/execution-manager.ts +0 -35
  177. package/src/lib/agents/handoff/bus.ts +2 -2
  178. package/src/lib/agents/learned-context.ts +0 -12
  179. package/src/lib/agents/learning-session.ts +18 -5
  180. package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
  181. package/src/lib/agents/profiles/__tests__/registry.test.ts +53 -4
  182. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +97 -0
  183. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +36 -0
  184. package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
  185. package/src/lib/agents/profiles/registry.ts +18 -0
  186. package/src/lib/agents/profiles/types.ts +7 -1
  187. package/src/lib/agents/router.ts +3 -6
  188. package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
  189. package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
  190. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  191. package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
  192. package/src/lib/agents/runtime/catalog.ts +121 -0
  193. package/src/lib/agents/runtime/claude-sdk.ts +32 -0
  194. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  195. package/src/lib/agents/runtime/execution-target.ts +456 -0
  196. package/src/lib/agents/runtime/index.ts +4 -0
  197. package/src/lib/agents/runtime/launch-failure.ts +101 -0
  198. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  199. package/src/lib/agents/runtime/openai-codex.ts +64 -60
  200. package/src/lib/agents/runtime/openai-direct.ts +8 -0
  201. package/src/lib/agents/runtime/types.ts +8 -0
  202. package/src/lib/agents/task-dispatch.ts +220 -0
  203. package/src/lib/agents/tool-permissions.ts +16 -1
  204. package/src/lib/book/chapter-mapping.ts +11 -0
  205. package/src/lib/book/content.ts +10 -0
  206. package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
  207. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  208. package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
  209. package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
  210. package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
  211. package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
  212. package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
  213. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  214. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  215. package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
  216. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  217. package/src/lib/chat/__tests__/types.test.ts +28 -0
  218. package/src/lib/chat/active-skills.ts +31 -0
  219. package/src/lib/chat/active-streams.ts +27 -0
  220. package/src/lib/chat/clean-filter-input.ts +30 -0
  221. package/src/lib/chat/codex-engine.ts +46 -24
  222. package/src/lib/chat/command-tabs.ts +61 -0
  223. package/src/lib/chat/context-builder.ts +146 -4
  224. package/src/lib/chat/dismissals.ts +73 -0
  225. package/src/lib/chat/engine.ts +159 -18
  226. package/src/lib/chat/files/__tests__/search.test.ts +135 -0
  227. package/src/lib/chat/files/expand-mention.ts +76 -0
  228. package/src/lib/chat/files/search.ts +99 -0
  229. package/src/lib/chat/reconcile.ts +117 -0
  230. package/src/lib/chat/skill-composition.ts +210 -0
  231. package/src/lib/chat/skill-conflict.ts +105 -0
  232. package/src/lib/chat/stagent-tools.ts +7 -19
  233. package/src/lib/chat/stream-telemetry.ts +137 -0
  234. package/src/lib/chat/suggested-prompts.ts +28 -1
  235. package/src/lib/chat/system-prompt.ts +48 -1
  236. package/src/lib/chat/tool-catalog.ts +35 -4
  237. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  238. package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
  239. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  240. package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
  241. package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
  242. package/src/lib/chat/tools/__tests__/task-tools.test.ts +399 -0
  243. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +351 -0
  244. package/src/lib/chat/tools/blueprint-tools.ts +190 -0
  245. package/src/lib/chat/tools/document-tools.ts +29 -13
  246. package/src/lib/chat/tools/helpers.ts +41 -0
  247. package/src/lib/chat/tools/notification-tools.ts +9 -5
  248. package/src/lib/chat/tools/profile-tools.ts +120 -23
  249. package/src/lib/chat/tools/project-tools.ts +33 -0
  250. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  251. package/src/lib/chat/tools/skill-tools.ts +183 -0
  252. package/src/lib/chat/tools/table-tools.ts +71 -0
  253. package/src/lib/chat/tools/task-tools.ts +89 -21
  254. package/src/lib/chat/tools/workflow-tools.ts +275 -32
  255. package/src/lib/chat/types.ts +15 -0
  256. package/src/lib/constants/settings.ts +10 -18
  257. package/src/lib/data/__tests__/clear.test.ts +56 -2
  258. package/src/lib/data/clear.ts +17 -16
  259. package/src/lib/data/delete-project.ts +171 -0
  260. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  261. package/src/lib/db/bootstrap.ts +62 -16
  262. package/src/lib/db/index.ts +5 -0
  263. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  264. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  265. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  266. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  267. package/src/lib/db/migrations/meta/_journal.json +21 -0
  268. package/src/lib/db/schema.ts +94 -23
  269. package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
  270. package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
  271. package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
  272. package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
  273. package/src/lib/environment/data.ts +9 -0
  274. package/src/lib/environment/list-skills.ts +176 -0
  275. package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
  276. package/src/lib/environment/parsers/skill.ts +26 -5
  277. package/src/lib/environment/profile-generator.ts +54 -0
  278. package/src/lib/environment/skill-enrichment.ts +106 -0
  279. package/src/lib/environment/skill-recommendations.ts +66 -0
  280. package/src/lib/environment/workspace-context.ts +13 -1
  281. package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
  282. package/src/lib/filters/__tests__/parse.test.ts +135 -0
  283. package/src/lib/filters/parse.ts +86 -0
  284. package/src/lib/import/dedup.ts +4 -54
  285. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  286. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  287. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  288. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  289. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  290. package/src/lib/instance/__tests__/upgrade-poller.test.ts +181 -0
  291. package/src/lib/instance/bootstrap.ts +270 -0
  292. package/src/lib/instance/detect.ts +49 -0
  293. package/src/lib/instance/fingerprint.ts +76 -0
  294. package/src/lib/instance/git-ops.ts +95 -0
  295. package/src/lib/instance/settings.ts +61 -0
  296. package/src/lib/instance/types.ts +77 -0
  297. package/src/lib/instance/upgrade-poller.ts +205 -0
  298. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  299. package/src/lib/notifications/visibility.ts +33 -0
  300. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  301. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  302. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  303. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  304. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  305. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  306. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  307. package/src/lib/schedules/collision-check.ts +105 -0
  308. package/src/lib/schedules/config.ts +53 -0
  309. package/src/lib/schedules/scheduler.ts +236 -17
  310. package/src/lib/schedules/slot-claim.ts +105 -0
  311. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  312. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  313. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  314. package/src/lib/settings/openai-auth.ts +105 -10
  315. package/src/lib/settings/openai-login-manager.ts +260 -0
  316. package/src/lib/settings/runtime-setup.ts +14 -4
  317. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  318. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  319. package/src/lib/tables/enrichment-planner.ts +454 -0
  320. package/src/lib/tables/enrichment.ts +328 -0
  321. package/src/lib/tables/query-builder.ts +5 -2
  322. package/src/lib/tables/trigger-evaluator.ts +3 -2
  323. package/src/lib/theme.ts +71 -0
  324. package/src/lib/usage/ledger.ts +2 -18
  325. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  326. package/src/lib/util/similarity.ts +77 -0
  327. package/src/lib/utils/format-timestamp.ts +24 -0
  328. package/src/lib/utils/stagent-paths.ts +12 -0
  329. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  330. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  331. package/src/lib/validators/blueprint.ts +70 -9
  332. package/src/lib/validators/profile.ts +2 -2
  333. package/src/lib/validators/settings.ts +3 -1
  334. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  335. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  336. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  337. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  338. package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
  339. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  340. package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
  341. package/src/lib/workflows/blueprints/types.ts +16 -2
  342. package/src/lib/workflows/delay.ts +106 -0
  343. package/src/lib/workflows/engine.ts +212 -7
  344. package/src/lib/workflows/loop-executor.ts +349 -24
  345. package/src/lib/workflows/post-action.ts +91 -0
  346. package/src/lib/workflows/types.ts +166 -1
  347. package/src/test/setup.ts +10 -0
  348. package/src/app/api/license/checkout/route.ts +0 -28
  349. package/src/app/api/license/portal/route.ts +0 -26
  350. package/src/app/api/license/route.ts +0 -89
  351. package/src/app/api/license/usage/route.ts +0 -63
  352. package/src/app/api/marketplace/browse/route.ts +0 -15
  353. package/src/app/api/marketplace/import/route.ts +0 -28
  354. package/src/app/api/marketplace/publish/route.ts +0 -40
  355. package/src/app/api/onboarding/email/route.ts +0 -53
  356. package/src/app/api/settings/telemetry/route.ts +0 -14
  357. package/src/app/api/sync/export/route.ts +0 -54
  358. package/src/app/api/sync/restore/route.ts +0 -37
  359. package/src/app/api/sync/sessions/route.ts +0 -24
  360. package/src/app/auth/callback/route.ts +0 -73
  361. package/src/app/marketplace/page.tsx +0 -19
  362. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  363. package/src/components/marketplace/blueprint-card.tsx +0 -61
  364. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  365. package/src/components/onboarding/email-capture-card.tsx +0 -104
  366. package/src/components/settings/activation-form.tsx +0 -95
  367. package/src/components/settings/cloud-account-section.tsx +0 -147
  368. package/src/components/settings/cloud-sync-section.tsx +0 -155
  369. package/src/components/settings/subscription-section.tsx +0 -410
  370. package/src/components/settings/telemetry-section.tsx +0 -80
  371. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  372. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  373. package/src/components/shared/upgrade-banner.tsx +0 -112
  374. package/src/hooks/use-supabase-auth.ts +0 -79
  375. package/src/lib/billing/email.ts +0 -54
  376. package/src/lib/billing/products.ts +0 -80
  377. package/src/lib/billing/stripe.ts +0 -101
  378. package/src/lib/cloud/supabase-browser.ts +0 -32
  379. package/src/lib/cloud/supabase-client.ts +0 -56
  380. package/src/lib/license/__tests__/features.test.ts +0 -56
  381. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  382. package/src/lib/license/__tests__/manager.test.ts +0 -64
  383. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  384. package/src/lib/license/cloud-validation.ts +0 -60
  385. package/src/lib/license/features.ts +0 -44
  386. package/src/lib/license/key-format.ts +0 -101
  387. package/src/lib/license/limit-check.ts +0 -111
  388. package/src/lib/license/limit-queries.ts +0 -51
  389. package/src/lib/license/manager.ts +0 -345
  390. package/src/lib/license/notifications.ts +0 -59
  391. package/src/lib/license/tier-limits.ts +0 -71
  392. package/src/lib/marketplace/marketplace-client.ts +0 -107
  393. package/src/lib/sync/cloud-sync.ts +0 -235
  394. package/src/lib/telemetry/conversion-events.ts +0 -71
  395. package/src/lib/telemetry/queue.ts +0 -122
  396. package/src/lib/validators/license.ts +0 -33
@@ -0,0 +1,270 @@
1
+ import { randomUUID } from "crypto";
2
+ import { existsSync, readFileSync, writeFileSync, chmodSync, renameSync } from "fs";
3
+ import { join } from "path";
4
+ import type { EnsureStepResult, EnsureResult, GitOps, ConsentStatus } from "./types";
5
+ import { getInstanceConfig, setInstanceConfig, getGuardrails, setGuardrails } from "./settings";
6
+ import { isPrivateInstance, isDevMode, hasGitDir, detectRebaseInProgress } from "./detect";
7
+ import { createGitOps } from "./git-ops";
8
+
9
+ const DEFAULT_BRANCH_NAME = "local";
10
+
11
+ /**
12
+ * Phase A step 1: ensure the instance config row exists with a stable instanceId.
13
+ * Idempotent — returns early if config already exists.
14
+ */
15
+ export async function ensureInstanceConfig(): Promise<EnsureStepResult> {
16
+ const existing = getInstanceConfig();
17
+ if (existing) {
18
+ return { step: "instance-config", status: "skipped", reason: "already_exists" };
19
+ }
20
+ await setInstanceConfig({
21
+ instanceId: randomUUID(),
22
+ branchName: DEFAULT_BRANCH_NAME,
23
+ isPrivateInstance: isPrivateInstance(),
24
+ createdAt: Math.floor(Date.now() / 1000),
25
+ });
26
+ return { step: "instance-config", status: "ok" };
27
+ }
28
+
29
+ /**
30
+ * Phase A step 2: create the `local` branch at current HEAD if it doesn't exist.
31
+ * Non-destructive: `git checkout -b local` preserves whatever branch the user
32
+ * was on, including any local commits. Safe on drifted-main scenarios.
33
+ */
34
+ export function ensureLocalBranch(git: GitOps): EnsureStepResult {
35
+ if (git.branchExists(DEFAULT_BRANCH_NAME)) {
36
+ return { step: "local-branch", status: "skipped", reason: "branch_exists" };
37
+ }
38
+ try {
39
+ git.createAndCheckoutBranch(DEFAULT_BRANCH_NAME);
40
+ return { step: "local-branch", status: "ok" };
41
+ } catch (err) {
42
+ return {
43
+ step: "local-branch",
44
+ status: "failed",
45
+ reason: err instanceof Error ? err.message : String(err),
46
+ };
47
+ }
48
+ }
49
+
50
+ export const STAGENT_HOOK_VERSION = "1.0.0";
51
+
52
+ /**
53
+ * Pre-push hook template. Installed verbatim at .git/hooks/pre-push.
54
+ *
55
+ * Reads the blocked branch list from the stagent SQLite settings table
56
+ * via a bounded sqlite3 invocation. The query is hardcoded — no user
57
+ * input reaches the shell.
58
+ *
59
+ * Escape hatch: set ALLOW_PRIVATE_PUSH=1 in env to bypass the guardrail
60
+ * for legitimate cherry-pick pushes.
61
+ */
62
+ const PRE_PUSH_HOOK_TEMPLATE = `#!/bin/sh
63
+ # STAGENT_HOOK_VERSION=${STAGENT_HOOK_VERSION}
64
+ # Blocks pushes of private instance branches to origin.
65
+ # Escape hatch: ALLOW_PRIVATE_PUSH=1 git push ...
66
+ #
67
+ # Generated by src/lib/instance/bootstrap.ts — do not edit manually.
68
+
69
+ if [ "$ALLOW_PRIVATE_PUSH" = "1" ]; then
70
+ exit 0
71
+ fi
72
+
73
+ current_branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
74
+ if [ -z "$current_branch" ]; then
75
+ exit 0
76
+ fi
77
+
78
+ data_dir="\${STAGENT_DATA_DIR:-$HOME/.stagent}"
79
+ db_path="$data_dir/stagent.db"
80
+ if [ ! -f "$db_path" ] || ! command -v sqlite3 >/dev/null 2>&1; then
81
+ exit 0
82
+ fi
83
+
84
+ blocked_json=$(sqlite3 "$db_path" "SELECT value FROM settings WHERE key='instance.guardrails';" 2>/dev/null)
85
+ if [ -z "$blocked_json" ]; then
86
+ exit 0
87
+ fi
88
+
89
+ if echo "$blocked_json" | grep -q "\\"$current_branch\\""; then
90
+ echo "stagent: refusing to push private instance branch '$current_branch' to origin." >&2
91
+ echo "stagent: set ALLOW_PRIVATE_PUSH=1 to override (not recommended)." >&2
92
+ exit 1
93
+ fi
94
+
95
+ exit 0
96
+ `;
97
+
98
+ /**
99
+ * Phase B step 1: install the pre-push hook at .git/hooks/pre-push.
100
+ * Idempotent: checks version marker in existing file; backs up foreign hooks.
101
+ */
102
+ export function ensurePrePushHook(git: GitOps): EnsureStepResult {
103
+ const hookPath = join(git.getGitDir(), "hooks", "pre-push");
104
+ const markerLine = `STAGENT_HOOK_VERSION=${STAGENT_HOOK_VERSION}`;
105
+
106
+ if (existsSync(hookPath)) {
107
+ const existing = readFileSync(hookPath, "utf-8");
108
+ if (existing.includes(markerLine)) {
109
+ return { step: "pre-push-hook", status: "skipped", reason: "already_installed" };
110
+ }
111
+ if (existing.includes("STAGENT_HOOK_VERSION=")) {
112
+ try {
113
+ writeFileSync(hookPath, PRE_PUSH_HOOK_TEMPLATE, { mode: 0o755 });
114
+ return { step: "pre-push-hook", status: "ok", reason: "upgraded" };
115
+ } catch (err) {
116
+ return {
117
+ step: "pre-push-hook",
118
+ status: "failed",
119
+ reason: err instanceof Error ? err.message : String(err),
120
+ };
121
+ }
122
+ }
123
+ try {
124
+ renameSync(hookPath, `${hookPath}.stagent-backup`);
125
+ } catch (err) {
126
+ return {
127
+ step: "pre-push-hook",
128
+ status: "failed",
129
+ reason: `backup_failed: ${err instanceof Error ? err.message : String(err)}`,
130
+ };
131
+ }
132
+ }
133
+
134
+ try {
135
+ writeFileSync(hookPath, PRE_PUSH_HOOK_TEMPLATE, { mode: 0o755 });
136
+ chmodSync(hookPath, 0o755);
137
+ return { step: "pre-push-hook", status: "ok" };
138
+ } catch (err) {
139
+ return {
140
+ step: "pre-push-hook",
141
+ status: "failed",
142
+ reason: err instanceof Error ? err.message : String(err),
143
+ };
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Phase B step 2: set branch.<name>.pushRemote=no_push for each blocked branch.
149
+ * Idempotent via git config semantics (setting the same value is a no-op).
150
+ */
151
+ export function ensureBranchPushConfig(git: GitOps, branches: string[]): EnsureStepResult {
152
+ const failures: string[] = [];
153
+ for (const branch of branches) {
154
+ try {
155
+ git.setConfig(`branch.${branch}.pushRemote`, "no_push");
156
+ } catch (err) {
157
+ failures.push(`${branch}: ${err instanceof Error ? err.message : String(err)}`);
158
+ }
159
+ }
160
+ if (failures.length > 0) {
161
+ return {
162
+ step: "branch-push-config",
163
+ status: "failed",
164
+ reason: failures.join("; "),
165
+ };
166
+ }
167
+ return { step: "branch-push-config", status: "ok" };
168
+ }
169
+
170
+ export interface ConsentDecision {
171
+ shouldRunPhaseB: boolean;
172
+ reason: ConsentStatus;
173
+ }
174
+
175
+ /**
176
+ * Reads the current consent status from settings and returns a decision
177
+ * about whether Phase B (destructive guardrail installation) should run.
178
+ *
179
+ * On first call, stamps firstBootCompletedAt so the system has a record
180
+ * that bootstrap has run at least once. This enables the upgrade-session
181
+ * feature to distinguish "never booted" from "booted but consent not yet
182
+ * given" in its Settings → Instance UI.
183
+ *
184
+ * Does NOT create any UI artifact. The prompt surface is owned by
185
+ * upgrade-session, which renders a "Enable guardrails" action in the
186
+ * Settings → Instance section reading from settings.instance.guardrails.
187
+ */
188
+ export async function resolveConsentDecision(): Promise<ConsentDecision> {
189
+ const current = getGuardrails();
190
+
191
+ if (current.firstBootCompletedAt === null) {
192
+ await setGuardrails({
193
+ ...current,
194
+ firstBootCompletedAt: Math.floor(Date.now() / 1000),
195
+ });
196
+ }
197
+
198
+ return {
199
+ shouldRunPhaseB: current.consentStatus === "enabled",
200
+ reason: current.consentStatus,
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Main entry point called from src/instrumentation.ts.
206
+ * Idempotent — safe to run on every boot.
207
+ *
208
+ * Execution order:
209
+ * 1. Dev-mode gates (env + sentinel) — skip entirely if active
210
+ * 2. .git presence check — skip if absent (npx runtime)
211
+ * 3. Phase A: instanceId, local branch (non-destructive, always runs)
212
+ * 4. Consent: resolves consent decision (stamps firstBootCompletedAt on first call)
213
+ * 5. Phase B: pre-push hook, pushRemote config (only if consent=enabled)
214
+ */
215
+ export async function ensureInstance(cwd: string = process.cwd()): Promise<EnsureResult> {
216
+ if (isDevMode(cwd)) {
217
+ const reason = process.env.STAGENT_DEV_MODE === "true" ? "dev_mode_env" : "dev_mode_sentinel";
218
+ return { skipped: reason, steps: [] };
219
+ }
220
+
221
+ if (!hasGitDir(cwd)) {
222
+ return { skipped: "no_git", steps: [] };
223
+ }
224
+
225
+ const steps: EnsureStepResult[] = [];
226
+ const git = createGitOps(cwd);
227
+
228
+ // Phase A step 1: instance config
229
+ steps.push(await ensureInstanceConfig());
230
+
231
+ // Phase A step 2: local branch — skip if rebase in progress
232
+ if (detectRebaseInProgress(cwd)) {
233
+ steps.push({ step: "local-branch", status: "skipped", reason: "rebase_in_progress" });
234
+ } else {
235
+ steps.push(ensureLocalBranch(git));
236
+ }
237
+
238
+ // Resolve consent (stamps firstBootCompletedAt on first call, returns decision)
239
+ const decision = await resolveConsentDecision();
240
+
241
+ // Phase B — only if user has explicitly enabled guardrails
242
+ if (decision.shouldRunPhaseB) {
243
+ const hookResult = ensurePrePushHook(git);
244
+ steps.push(hookResult);
245
+
246
+ const config = getInstanceConfig();
247
+ const blockedBranches = config ? [config.branchName] : [];
248
+ if (blockedBranches.length > 0) {
249
+ const configResult = ensureBranchPushConfig(git, blockedBranches);
250
+ steps.push(configResult);
251
+
252
+ // Persist guardrail state back to settings so the pre-push hook can
253
+ // read the list of blocked branches at push time (the hook greps the
254
+ // serialized JSON of settings.instance.guardrails for the current
255
+ // branch name). Without this write, the hook would silently allow
256
+ // all pushes because pushRemoteBlocked would stay [].
257
+ if (hookResult.status !== "failed" && configResult.status !== "failed") {
258
+ const current = getGuardrails();
259
+ await setGuardrails({
260
+ ...current,
261
+ prePushHookInstalled: true,
262
+ prePushHookVersion: STAGENT_HOOK_VERSION,
263
+ pushRemoteBlocked: blockedBranches,
264
+ });
265
+ }
266
+ }
267
+ }
268
+
269
+ return { steps };
270
+ }
@@ -0,0 +1,49 @@
1
+ import { existsSync } from "fs";
2
+ import { join, resolve } from "path";
3
+ import { homedir } from "os";
4
+
5
+ /**
6
+ * Returns true if the current environment is the canonical stagent dev repo
7
+ * and should skip all instance bootstrap operations.
8
+ *
9
+ * Layered gates:
10
+ * 1. STAGENT_DEV_MODE=true env var (primary, per-developer)
11
+ * 2. .git/stagent-dev-mode sentinel file (secondary, git-dir-scoped)
12
+ *
13
+ * Override: STAGENT_INSTANCE_MODE=true forces bootstrap to run even in dev
14
+ * mode, so contributors can test the feature in the main repo.
15
+ */
16
+ export function isDevMode(cwd: string = process.cwd()): boolean {
17
+ if (process.env.STAGENT_INSTANCE_MODE === "true") return false;
18
+ if (process.env.STAGENT_DEV_MODE === "true") return true;
19
+ if (existsSync(join(cwd, ".git", "stagent-dev-mode"))) return true;
20
+ return false;
21
+ }
22
+
23
+ /** Returns true if a .git directory exists at the given path. */
24
+ export function hasGitDir(cwd: string = process.cwd()): boolean {
25
+ return existsSync(join(cwd, ".git"));
26
+ }
27
+
28
+ /**
29
+ * Returns true if STAGENT_DATA_DIR is set to a non-default path,
30
+ * indicating this clone is running as an isolated private instance.
31
+ */
32
+ export function isPrivateInstance(): boolean {
33
+ const override = process.env.STAGENT_DATA_DIR;
34
+ if (!override) return false;
35
+ const defaultDir = join(homedir(), ".stagent");
36
+ return resolve(override) !== resolve(defaultDir);
37
+ }
38
+
39
+ /**
40
+ * Returns true if a rebase is in progress in the current repo.
41
+ * Both rebase-merge (interactive) and rebase-apply (non-interactive) are detected.
42
+ */
43
+ export function detectRebaseInProgress(cwd: string = process.cwd()): boolean {
44
+ const gitDir = join(cwd, ".git");
45
+ return (
46
+ existsSync(join(gitDir, "rebase-merge")) ||
47
+ existsSync(join(gitDir, "rebase-apply"))
48
+ );
49
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Machine fingerprint generator.
3
+ *
4
+ * Produces a stable, non-identifying hash that uniquely identifies the machine
5
+ * this stagent instance is running on. Used to give each stagent instance a
6
+ * durable identity (e.g., for telemetry correlation and multi-instance
7
+ * disambiguation); no billing or cloud-metering dependency.
8
+ *
9
+ * The fingerprint is derived from:
10
+ * 1. os.hostname() — e.g., "macbook-pro.local"
11
+ * 2. os.userInfo().username — e.g., "navam"
12
+ * 3. SHA-256 of the first non-internal MAC address
13
+ *
14
+ * The MAC is hashed before it leaves the process so the raw network identifier
15
+ * never appears in logs or telemetry. The combined inputs are SHA-256'd
16
+ * together to produce a 64-character hex string.
17
+ *
18
+ * Stability: the fingerprint is stable across reboots and stagent restarts
19
+ * on the same machine. It changes if the user renames their account, renames
20
+ * their machine, or swaps network hardware.
21
+ */
22
+
23
+ import { createHash } from "crypto";
24
+ import { hostname, userInfo, networkInterfaces } from "os";
25
+
26
+ let cachedFingerprint: string | null = null;
27
+
28
+ export function getMachineFingerprint(): string {
29
+ if (cachedFingerprint !== null) return cachedFingerprint;
30
+
31
+ const host = safeHostname();
32
+ const user = safeUsername();
33
+ const macHash = hashPrimaryMac();
34
+
35
+ const combined = `${host}|${user}|${macHash}`;
36
+ cachedFingerprint = createHash("sha256").update(combined).digest("hex");
37
+ return cachedFingerprint;
38
+ }
39
+
40
+ /** Exposed for tests that need to reset the module-level cache. */
41
+ export function _resetFingerprintCache(): void {
42
+ cachedFingerprint = null;
43
+ }
44
+
45
+ function safeHostname(): string {
46
+ try {
47
+ return hostname();
48
+ } catch {
49
+ return "unknown-host";
50
+ }
51
+ }
52
+
53
+ function safeUsername(): string {
54
+ try {
55
+ return userInfo().username;
56
+ } catch {
57
+ return "unknown-user";
58
+ }
59
+ }
60
+
61
+ function hashPrimaryMac(): string {
62
+ try {
63
+ const interfaces = networkInterfaces();
64
+ for (const name of Object.keys(interfaces).sort()) {
65
+ const addrs = interfaces[name] ?? [];
66
+ for (const addr of addrs) {
67
+ if (addr.internal) continue;
68
+ if (!addr.mac || addr.mac === "00:00:00:00:00:00") continue;
69
+ return createHash("sha256").update(addr.mac).digest("hex");
70
+ }
71
+ }
72
+ } catch {
73
+ // fall through to the default below
74
+ }
75
+ return "no-mac-detected";
76
+ }
@@ -0,0 +1,95 @@
1
+ import { execFileSync } from "child_process";
2
+ import { join, resolve } from "path";
3
+ import type { GitOps } from "./types";
4
+
5
+ /**
6
+ * Real git operations wrapper. All commands use execFileSync with argv arrays —
7
+ * no shell interpolation, ever. File is the literal "git"; user-provided values
8
+ * flow through the args array which git parses without shell involvement.
9
+ *
10
+ * The cwd parameter is normalized to an absolute path at factory creation time
11
+ * so getGitDir() honors its interface contract of returning an absolute path.
12
+ */
13
+ export function createGitOps(cwd: string = process.cwd()): GitOps {
14
+ const absoluteCwd = resolve(cwd);
15
+ function run(args: string[]): string {
16
+ return execFileSync("git", args, {
17
+ cwd: absoluteCwd,
18
+ encoding: "utf-8",
19
+ stdio: ["ignore", "pipe", "pipe"],
20
+ }).trim();
21
+ }
22
+
23
+ return {
24
+ isGitRepo(): boolean {
25
+ try {
26
+ run(["rev-parse", "--is-inside-work-tree"]);
27
+ return true;
28
+ } catch {
29
+ return false;
30
+ }
31
+ },
32
+
33
+ getGitDir(): string {
34
+ return join(absoluteCwd, ".git");
35
+ },
36
+
37
+ getCurrentBranch(): string | null {
38
+ try {
39
+ const branch = run(["rev-parse", "--abbrev-ref", "HEAD"]);
40
+ return branch === "HEAD" ? null : branch;
41
+ } catch {
42
+ return null;
43
+ }
44
+ },
45
+
46
+ branchExists(name: string): boolean {
47
+ try {
48
+ run(["rev-parse", "--verify", `refs/heads/${name}`]);
49
+ return true;
50
+ } catch {
51
+ return false;
52
+ }
53
+ },
54
+
55
+ createAndCheckoutBranch(name: string): void {
56
+ run(["checkout", "-b", name]);
57
+ },
58
+
59
+ setConfig(key: string, value: string): void {
60
+ run(["config", key, value]);
61
+ },
62
+
63
+ fetchOrigin(): void {
64
+ run(["fetch", "origin", "main"]);
65
+ },
66
+
67
+ revParse(ref: string): string | null {
68
+ try {
69
+ return run(["rev-parse", ref]);
70
+ } catch {
71
+ return null;
72
+ }
73
+ },
74
+
75
+ countCommitsAhead(from: string, to: string): number {
76
+ try {
77
+ const out = run(["rev-list", "--count", `${from}..${to}`]);
78
+ const n = parseInt(out, 10);
79
+ return Number.isFinite(n) ? n : 0;
80
+ } catch {
81
+ return 0;
82
+ }
83
+ },
84
+ };
85
+ }
86
+
87
+ /** Test helper: detect if execFileSync would find git on this system. */
88
+ export function isGitAvailable(): boolean {
89
+ try {
90
+ execFileSync("git", ["--version"], { stdio: "ignore" });
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
@@ -0,0 +1,61 @@
1
+ import { getSettingSync, setSetting } from "@/lib/settings/helpers";
2
+ import type { InstanceConfig, Guardrails, UpgradeState } from "./types";
3
+
4
+ const INSTANCE_KEY = "instance";
5
+ const GUARDRAILS_KEY = "instance.guardrails";
6
+
7
+ const DEFAULT_GUARDRAILS: Guardrails = {
8
+ prePushHookInstalled: false,
9
+ prePushHookVersion: "",
10
+ pushRemoteBlocked: [],
11
+ consentStatus: "not_yet",
12
+ firstBootCompletedAt: null,
13
+ };
14
+
15
+ function readJson<T>(key: string): T | null {
16
+ const raw = getSettingSync(key);
17
+ if (raw === null) return null;
18
+ try {
19
+ return JSON.parse(raw) as T;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ export function getInstanceConfig(): InstanceConfig | null {
26
+ return readJson<InstanceConfig>(INSTANCE_KEY);
27
+ }
28
+
29
+ export async function setInstanceConfig(config: InstanceConfig): Promise<void> {
30
+ await setSetting(INSTANCE_KEY, JSON.stringify(config));
31
+ }
32
+
33
+ export function getGuardrails(): Guardrails {
34
+ return readJson<Guardrails>(GUARDRAILS_KEY) ?? { ...DEFAULT_GUARDRAILS };
35
+ }
36
+
37
+ export async function setGuardrails(guardrails: Guardrails): Promise<void> {
38
+ await setSetting(GUARDRAILS_KEY, JSON.stringify(guardrails));
39
+ }
40
+
41
+ const UPGRADE_KEY = "instance.upgrade";
42
+
43
+ const DEFAULT_UPGRADE_STATE: UpgradeState = {
44
+ lastPolledAt: null,
45
+ lastUpstreamSha: null,
46
+ localMainSha: null,
47
+ upgradeAvailable: false,
48
+ commitsBehind: 0,
49
+ lastSuccessfulUpgradeAt: null,
50
+ lastUpgradeTaskId: null,
51
+ pollFailureCount: 0,
52
+ lastPollError: null,
53
+ };
54
+
55
+ export function getUpgradeState(): UpgradeState {
56
+ return readJson<UpgradeState>(UPGRADE_KEY) ?? { ...DEFAULT_UPGRADE_STATE };
57
+ }
58
+
59
+ export async function setUpgradeState(state: UpgradeState): Promise<void> {
60
+ await setSetting(UPGRADE_KEY, JSON.stringify(state));
61
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Instance bootstrap shared types.
3
+ * See features/instance-bootstrap.md for full design rationale.
4
+ */
5
+
6
+ export interface InstanceConfig {
7
+ instanceId: string;
8
+ branchName: string;
9
+ isPrivateInstance: boolean;
10
+ createdAt: number;
11
+ }
12
+
13
+ export type ConsentStatus = "not_yet" | "enabled" | "declined_permanently";
14
+
15
+ export interface Guardrails {
16
+ prePushHookInstalled: boolean;
17
+ prePushHookVersion: string;
18
+ pushRemoteBlocked: string[];
19
+ consentStatus: ConsentStatus;
20
+ firstBootCompletedAt: number | null;
21
+ }
22
+
23
+ export interface UpgradeState {
24
+ lastPolledAt: number | null;
25
+ lastUpstreamSha: string | null;
26
+ localMainSha: string | null;
27
+ upgradeAvailable: boolean;
28
+ commitsBehind: number;
29
+ lastSuccessfulUpgradeAt: number | null;
30
+ lastUpgradeTaskId: string | null;
31
+ pollFailureCount: number;
32
+ lastPollError: string | null;
33
+ }
34
+
35
+ export type EnsureSkipReason =
36
+ | "dev_mode_env"
37
+ | "dev_mode_sentinel"
38
+ | "no_git";
39
+
40
+ export type EnsureStepStatus = "ok" | "skipped" | "failed";
41
+
42
+ export interface EnsureStepResult {
43
+ step: string;
44
+ status: EnsureStepStatus;
45
+ reason?: string;
46
+ }
47
+
48
+ export interface EnsureResult {
49
+ skipped?: EnsureSkipReason;
50
+ steps: EnsureStepResult[];
51
+ }
52
+
53
+ /**
54
+ * Injectable wrapper around git commands.
55
+ * Real implementation in git-ops.ts uses execFileSync.
56
+ * Tests provide a mock implementation.
57
+ */
58
+ export interface GitOps {
59
+ /** Returns true if the current working directory is inside a git repo (not a worktree of the main repo). */
60
+ isGitRepo(): boolean;
61
+ /** Returns the absolute path to the .git directory for the current repo. */
62
+ getGitDir(): string;
63
+ /** Returns the currently checked-out branch name, or null if detached HEAD. */
64
+ getCurrentBranch(): string | null;
65
+ /** Returns true if a branch with the given name exists locally. */
66
+ branchExists(name: string): boolean;
67
+ /** Creates a new branch at the current HEAD and checks it out. */
68
+ createAndCheckoutBranch(name: string): void;
69
+ /** Sets a git config value. Throws on failure. */
70
+ setConfig(key: string, value: string): void;
71
+ /** Fetches from origin. Throws on failure. */
72
+ fetchOrigin(): void;
73
+ /** Returns the SHA for a given ref (branch name or remote ref like "origin/main"). Returns null if the ref is unknown. */
74
+ revParse(ref: string): string | null;
75
+ /** Returns the count of commits reachable from `to` but not from `from`. Returns 0 if either ref is unknown. */
76
+ countCommitsAhead(from: string, to: string): number;
77
+ }