spine-framework 0.4.3 → 0.5.0-rc.4

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 (320) hide show
  1. package/.env.example +13 -3
  2. package/.framework/cli/commands/bootstrap.ts +392 -0
  3. package/.framework/cli/commands/dev.ts +91 -218
  4. package/.framework/cli/commands/doctor.ts +132 -26
  5. package/.framework/cli/commands/generate.ts +38 -298
  6. package/.framework/cli/commands/init.ts +20 -1
  7. package/.framework/cli/commands/migrate.ts +36 -30
  8. package/.framework/cli/commands/migrations.ts +16 -56
  9. package/.framework/cli/commands/upgrade.ts +35 -0
  10. package/.framework/cli/commands/validate-schema.ts +147 -0
  11. package/.framework/cli/context.ts +223 -110
  12. package/.framework/cli/index.ts +6 -0
  13. package/.framework/cli/lib/project-files.ts +241 -1
  14. package/.framework/functions/_shared/agent-runner.ts +1000 -81
  15. package/.framework/functions/_shared/cil.ts +2 -1
  16. package/.framework/functions/_shared/db.ts +3 -6
  17. package/.framework/functions/_shared/hash-api-key.ts +183 -79
  18. package/.framework/functions/_shared/kb.ts +61 -7
  19. package/.framework/functions/_shared/logging.ts +562 -224
  20. package/.framework/functions/_shared/message-sequence.ts +53 -0
  21. package/.framework/functions/_shared/middleware.ts +47 -44
  22. package/.framework/functions/_shared/pipeline-runner.ts +150 -37
  23. package/.framework/functions/_shared/principal.ts +9 -6
  24. package/.framework/functions/_shared/trigger-engine.ts +149 -40
  25. package/.framework/functions/admin-data.ts +48 -31
  26. package/.framework/functions/ai-agents.ts +103 -33
  27. package/.framework/functions/api-keys.ts +301 -122
  28. package/.framework/functions/apps.ts +43 -26
  29. package/.framework/functions/cil-ingest.ts +48 -15
  30. package/.framework/functions/debug-auth.ts +14 -8
  31. package/.framework/functions/deliveries.ts +39 -12
  32. package/.framework/functions/embeddings.ts +145 -76
  33. package/.framework/functions/integration-outbound.ts +67 -27
  34. package/.framework/functions/integrations.ts +254 -127
  35. package/.framework/functions/logs.ts +273 -133
  36. package/.framework/functions/observability.ts +277 -147
  37. package/.framework/functions/pipeline-executions.ts +1 -1
  38. package/.framework/functions/pipeline-runner-background.ts +120 -0
  39. package/.framework/functions/pipelines.ts +14 -9
  40. package/.framework/functions/prompt-configs.ts +7 -4
  41. package/.framework/functions/roles.ts +8 -5
  42. package/.framework/functions/security.ts +30 -17
  43. package/.framework/functions/system-cron.ts +250 -128
  44. package/.framework/functions/tests.ts +7 -6
  45. package/.framework/functions/timers.ts +310 -107
  46. package/.framework/functions/trigger-scheduler.ts +65 -5
  47. package/.framework/functions/triggers.ts +28 -13
  48. package/.framework/functions/types.ts +13 -10
  49. package/.framework/migrations/064_episode_leases.sql +196 -0
  50. package/.framework/migrations/065_execution_tables_updated_at.sql +20 -0
  51. package/.framework/migrations/066_episode_steal_thread_merge.sql +148 -0
  52. package/.framework/migrations/067_episode_lease_stale_steal.sql +93 -0
  53. package/.framework/migrations/068_cron_schedule_timer_rpcs.sql +251 -0
  54. package/.framework/migrations/069_log_stats_window.sql +54 -0
  55. package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
  56. package/.framework/migrations/071_timer_interval_keys.sql +96 -0
  57. package/.framework/migrations/072_slug_autofill.sql +77 -0
  58. package/.framework/migrations/073_seed_member_roles.sql +26 -0
  59. package/.framework/schemas/spine.config.schema.json +62 -0
  60. package/.framework/src/App.tsx +3 -1
  61. package/.framework/src/apps/admin/index.tsx +1141 -775
  62. package/.framework/src/components/ErrorBoundary.tsx +136 -0
  63. package/.framework/src/components/admin/AdminListPage.tsx +81 -73
  64. package/.framework/src/components/admin/AdminSidebar.tsx +92 -81
  65. package/.framework/src/components/admin/EntitySelect.tsx +181 -0
  66. package/.framework/src/components/admin/PageInstructions.tsx +86 -127
  67. package/.framework/src/components/app-shell/GenericAppShell.tsx +3 -1
  68. package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
  69. package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
  70. package/.framework/src/components/auth/TurnstileWidget.tsx +9 -6
  71. package/.framework/src/components/kb/KBArticleEditor.tsx +49 -6
  72. package/.framework/src/components/layout/Layout.tsx +4 -2
  73. package/.framework/src/components/layout/Sidebar.tsx +304 -179
  74. package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
  75. package/.framework/src/components/runtime/DataHeader.tsx +96 -48
  76. package/.framework/src/components/runtime/DataListPage.tsx +3 -26
  77. package/.framework/src/components/runtime/DataStats.tsx +10 -10
  78. package/.framework/src/components/runtime/DataTable.tsx +12 -10
  79. package/.framework/src/components/ui/sheet.tsx +24 -26
  80. package/.framework/src/components/ui/textarea.tsx +74 -48
  81. package/.framework/src/hooks/useBranding.ts +47 -0
  82. package/.framework/src/hooks/useEntityList.ts +12 -9
  83. package/.framework/src/hooks/useListSchema.ts +108 -58
  84. package/.framework/src/instructions/README.md +57 -0
  85. package/.framework/src/instructions/ai-agents.md +69 -0
  86. package/.framework/src/instructions/alerts.md +80 -0
  87. package/.framework/src/instructions/api-keys.md +87 -0
  88. package/.framework/src/instructions/apps.md +77 -0
  89. package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
  90. package/.framework/src/instructions/concepts/apps-and-renderers.md +162 -0
  91. package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
  92. package/.framework/src/instructions/concepts/automation.md +186 -0
  93. package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
  94. package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
  95. package/.framework/src/instructions/concepts/design-schema.md +152 -0
  96. package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
  97. package/.framework/src/instructions/concepts/observability.md +116 -0
  98. package/.framework/src/instructions/concepts/permissions.md +150 -0
  99. package/.framework/src/instructions/concepts/runtime-records.md +111 -0
  100. package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
  101. package/.framework/src/instructions/concepts/spine-way.md +172 -0
  102. package/.framework/src/instructions/deliveries.md +59 -0
  103. package/.framework/src/instructions/guide/00-orientation.md +106 -0
  104. package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
  105. package/.framework/src/instructions/guide/02-project-map.md +147 -0
  106. package/.framework/src/instructions/guide/03-domain-model.md +223 -0
  107. package/.framework/src/instructions/guide/04-first-app.md +227 -0
  108. package/.framework/src/instructions/guide/05-glossary.md +183 -0
  109. package/.framework/src/instructions/integrations.md +96 -0
  110. package/.framework/src/instructions/internal/cil-markers.md +83 -0
  111. package/.framework/src/instructions/invites.md +60 -0
  112. package/.framework/src/instructions/kb.md +80 -0
  113. package/.framework/src/instructions/logs.md +77 -0
  114. package/.framework/src/instructions/observability.md +61 -0
  115. package/.framework/src/instructions/operations/deployment.md +115 -0
  116. package/.framework/src/instructions/operations/migrations.md +125 -0
  117. package/.framework/src/instructions/operations/security-checklist.md +148 -0
  118. package/.framework/src/instructions/operations/troubleshooting.md +170 -0
  119. package/.framework/src/instructions/operations/upgrade.md +116 -0
  120. package/.framework/src/instructions/pipeline-executions.md +62 -0
  121. package/.framework/src/instructions/pipelines.md +78 -0
  122. package/.framework/src/instructions/prompts.md +95 -0
  123. package/.framework/src/instructions/reference/api/admin-data.md +162 -0
  124. package/.framework/src/instructions/reference/api/auth.md +113 -0
  125. package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
  126. package/.framework/src/instructions/reference/app-manifest.md +150 -0
  127. package/.framework/src/instructions/reference/cli.md +124 -0
  128. package/.framework/src/instructions/reference/database.md +132 -0
  129. package/.framework/src/instructions/reference/design-schema.md +280 -0
  130. package/.framework/src/instructions/reference/env-vars.md +133 -0
  131. package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
  132. package/.framework/src/instructions/reference/functions/admin-data.md +195 -0
  133. package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
  134. package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
  135. package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
  136. package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
  137. package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
  138. package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
  139. package/.framework/src/instructions/reference/functions/apps.md +140 -0
  140. package/.framework/src/instructions/reference/functions/auth.md +99 -0
  141. package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
  142. package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
  143. package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
  144. package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
  145. package/.framework/src/instructions/reference/functions/index.md +103 -0
  146. package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
  147. package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
  148. package/.framework/src/instructions/reference/functions/integrations.md +130 -0
  149. package/.framework/src/instructions/reference/functions/invites.md +182 -0
  150. package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
  151. package/.framework/src/instructions/reference/functions/kb.md +166 -0
  152. package/.framework/src/instructions/reference/functions/logs.md +141 -0
  153. package/.framework/src/instructions/reference/functions/observability.md +105 -0
  154. package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
  155. package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
  156. package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
  157. package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
  158. package/.framework/src/instructions/reference/functions/roles.md +121 -0
  159. package/.framework/src/instructions/reference/functions/security.md +140 -0
  160. package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
  161. package/.framework/src/instructions/reference/functions/system.md +106 -0
  162. package/.framework/src/instructions/reference/functions/tests.md +97 -0
  163. package/.framework/src/instructions/reference/functions/timers.md +117 -0
  164. package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
  165. package/.framework/src/instructions/reference/functions/triggers.md +140 -0
  166. package/.framework/src/instructions/reference/functions/types.md +146 -0
  167. package/.framework/src/instructions/reference/seeds.md +126 -0
  168. package/.framework/src/instructions/reference/shared-modules.md +127 -0
  169. package/.framework/src/instructions/reference/spine-config.md +114 -0
  170. package/.framework/src/instructions/roles.md +80 -0
  171. package/.framework/src/instructions/runtime-detail.md +70 -0
  172. package/.framework/src/instructions/runtime-list.md +77 -0
  173. package/.framework/src/instructions/security.md +67 -0
  174. package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
  175. package/.framework/src/instructions/tasks/add-a-role.md +96 -0
  176. package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
  177. package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
  178. package/.framework/src/instructions/tasks/add-a-type.md +151 -0
  179. package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
  180. package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
  181. package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
  182. package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
  183. package/.framework/src/instructions/tasks/package-and-install-an-app.md +127 -0
  184. package/.framework/src/instructions/tasks/test-your-code.md +108 -0
  185. package/.framework/src/instructions/tasks/write-a-function.md +132 -0
  186. package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
  187. package/.framework/src/instructions/testing.md +59 -0
  188. package/.framework/src/instructions/timers.md +82 -0
  189. package/.framework/src/instructions/triggers.md +69 -0
  190. package/.framework/src/instructions/types.md +93 -0
  191. package/.framework/src/lib/branding.ts +128 -0
  192. package/.framework/src/pages/NoAppPage.tsx +3 -1
  193. package/.framework/src/pages/NotFoundPage.tsx +62 -31
  194. package/.framework/src/pages/SetupPage.tsx +5 -2
  195. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
  196. package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
  197. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
  198. package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
  199. package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
  200. package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
  201. package/.framework/src/pages/admin/AppsPage.tsx +104 -66
  202. package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
  203. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
  204. package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
  205. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
  206. package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
  207. package/.framework/src/pages/admin/LogsPage.tsx +144 -76
  208. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
  209. package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
  210. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
  211. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
  212. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
  213. package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
  214. package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
  215. package/.framework/src/pages/admin/RolesPage.tsx +86 -43
  216. package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
  217. package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
  218. package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
  219. package/.framework/src/pages/admin/TimersPage.tsx +90 -62
  220. package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
  221. package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
  222. package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
  223. package/.framework/src/pages/admin/TypesPage.tsx +90 -48
  224. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
  225. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
  226. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
  227. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
  228. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
  229. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
  230. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
  231. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
  232. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
  233. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
  234. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
  235. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
  236. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
  237. package/.framework/src/pages/auth/AuthConfirmPage.tsx +3 -1
  238. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +3 -1
  239. package/.framework/src/pages/auth/LoginPage.tsx +4 -2
  240. package/.framework/src/pages/auth/RegisterPage.tsx +3 -1
  241. package/.framework/templates/agent/AGENTS.md +150 -0
  242. package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
  243. package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
  244. package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
  245. package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
  246. package/.framework/templates/agent/skills/spine-install-app/SKILL.md +72 -0
  247. package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
  248. package/.framework/templates/agent/skills/spine-new-app/SKILL.md +67 -0
  249. package/.framework/templates/agent/skills/spine-new-function/SKILL.md +65 -0
  250. package/.framework/templates/agent/skills/spine-new-type/SKILL.md +65 -0
  251. package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
  252. package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
  253. package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
  254. package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
  255. package/.framework/templates/project.json +5 -1
  256. package/.framework/tests/unit/account-scoping-gates.test.ts +392 -0
  257. package/.framework/tests/unit/agent-files.test.ts +355 -0
  258. package/.framework/tests/unit/agent-runner.test.ts +496 -47
  259. package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
  260. package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
  261. package/.framework/tests/unit/bootstrap.test.ts +242 -0
  262. package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
  263. package/.framework/tests/unit/debug-auth.test.ts +38 -0
  264. package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
  265. package/.framework/tests/unit/episode-lease.test.ts +695 -0
  266. package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
  267. package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
  268. package/.framework/tests/unit/kb-ingest-lease.test.ts +19 -0
  269. package/.framework/tests/unit/logging-app-id.test.ts +75 -0
  270. package/.framework/tests/unit/pipeline-runner-background.test.ts +120 -0
  271. package/.framework/tests/unit/pipeline-runner.test.ts +153 -1
  272. package/.framework/tests/unit/principal-cache.test.ts +15 -0
  273. package/.framework/tests/unit/system-context-denial.test.ts +145 -0
  274. package/.framework/tests/unit/system-cron.test.ts +240 -0
  275. package/.framework/tests/unit/timers.test.ts +159 -0
  276. package/.framework/tests/unit/trigger-engine.test.ts +278 -91
  277. package/.framework/tests/unit/trigger-scheduler.test.ts +78 -0
  278. package/README.md +125 -166
  279. package/config/tsconfig.cli.json +2 -2
  280. package/netlify.toml +5 -0
  281. package/package.json +15 -4
  282. package/templates/project/package-lock.json +2 -2
  283. package/.framework/docs/admin-routes.md +0 -76
  284. package/.framework/docs/api-docs-progress.md +0 -38
  285. package/.framework/docs/api-governance.md +0 -146
  286. package/.framework/docs/api-testing-results.md +0 -212
  287. package/.framework/docs/assembly-launch-guide.md +0 -138
  288. package/.framework/docs/audit-results.md +0 -590
  289. package/.framework/docs/db-api-inventory.md +0 -95
  290. package/.framework/docs/gap-remediation-backlog.md +0 -103
  291. package/.framework/docs/ui-adoption-verification.md +0 -111
  292. package/.framework/docs/ui-api-coverage.md +0 -84
  293. package/.framework/docs/v2-compatibility-audit.md +0 -228
  294. package/.framework/mcp/index.ts +0 -133
  295. package/.framework/mcp/load-env.ts +0 -60
  296. package/.framework/mcp/shared/app-manifest.ts +0 -275
  297. package/.framework/mcp/shared/db.ts +0 -477
  298. package/.framework/mcp/shared/kb.ts +0 -118
  299. package/.framework/mcp/tools/architect.ts +0 -431
  300. package/.framework/mcp/tools/cil/context.ts +0 -345
  301. package/.framework/mcp/tools/cil/graph.ts +0 -242
  302. package/.framework/mcp/tools/cil/ingest.ts +0 -1217
  303. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
  304. package/.framework/mcp/tools/cil/pipeline.ts +0 -767
  305. package/.framework/mcp/tools/cil/project.ts +0 -791
  306. package/.framework/mcp/tools/cil/retrieval.ts +0 -336
  307. package/.framework/mcp/tools/cil/search-core.ts +0 -454
  308. package/.framework/mcp/tools/cil/search.ts +0 -97
  309. package/.framework/mcp/tools/cil/suggest.ts +0 -186
  310. package/.framework/mcp/tools/cil/validate.ts +0 -1369
  311. package/.framework/mcp/tools/kb/delete.ts +0 -139
  312. package/.framework/mcp/tools/kb/index.ts +0 -52
  313. package/.framework/mcp/tools/kb/ingest.ts +0 -84
  314. package/.framework/mcp/tools/kb/list-types.ts +0 -102
  315. package/.framework/mcp/tools/kb/publish.ts +0 -100
  316. package/.framework/mcp/tools/kb/reindex.ts +0 -86
  317. package/.framework/mcp/tools/kb/revoke.ts +0 -85
  318. package/.framework/mcp/tools/kb/search.ts +0 -101
  319. package/.framework/mcp/tools/kb/upsert.ts +0 -125
  320. package/.framework/mcp/tools/observability.ts +0 -510
@@ -262,6 +262,47 @@ interface PipelineStage {
262
262
  * @errorHandling Handles errors at each stage and decides whether to continue based on stage configuration.
263
263
  * @cil-generated
264
264
  */
265
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/pipeline-runner.ts:persistExecutionResult:function" type="function" identity="persistExecutionResult" parent="~/.framework/functions/_shared/pipeline-runner.ts"]
266
+ /**
267
+ * Writes the terminal status of a pipeline execution. The update result
268
+ * is checked and retried once — an unchecked failure is what previously
269
+ * left rows stuck 'running' forever while the audit span still claimed
270
+ * 'pipeline.completed'. A retry failure is logged loudly so the row can
271
+ * be found and reconciled.
272
+ *
273
+ * @inputSpec
274
+ * ctx: CoreContext (required) — context for request-id logging.
275
+ * executionId: string (required) — the pipeline_executions row to update.
276
+ * patch: Record<string, any> (required) — terminal status payload.
277
+ * @outputSpec Promise<void>
278
+ * @sideEffects Updates public.pipeline_executions; logs on failure.
279
+ * @stability stable
280
+ * @calls adminDb.from('pipeline_executions').update
281
+ * @cil-generated
282
+ */
283
+ async function persistExecutionResult(
284
+ ctx: CoreContext,
285
+ executionId: string,
286
+ patch: Record<string, any>
287
+ ): Promise<void> {
288
+ let { error } = await adminDb
289
+ .from('pipeline_executions')
290
+ .update(patch)
291
+ .eq('id', executionId)
292
+ if (error) {
293
+ console.error(`[${ctx.requestId}] pipeline_executions final update failed for ${executionId}: ${error.message} — retrying`)
294
+ const retry = await adminDb
295
+ .from('pipeline_executions')
296
+ .update(patch)
297
+ .eq('id', executionId)
298
+ error = retry.error
299
+ if (error) {
300
+ console.error(`[${ctx.requestId}] pipeline_executions final update retry failed for ${executionId}: ${error.message}`)
301
+ }
302
+ }
303
+ }
304
+ // CIL[END stable_id="~/.framework/functions/_shared/pipeline-runner.ts:persistExecutionResult:function"]
305
+
265
306
  // CIL[BEGIN stable_id="~/.framework/functions/_shared/pipeline-runner.ts:runpipeline:function" type="function" identity="runPipeline" parent="~/.framework/functions/_shared/pipeline-runner.ts"]
266
307
  // CIL[DOC-MACRO purpose="runPipeline function"]
267
308
  // CIL[DOC-RESPONSIBILITY responsibility="Implements the runPipeline function."]
@@ -270,14 +311,40 @@ interface PipelineStage {
270
311
  // CIL[DOC-MICRO logic_summary="Implements the runPipeline function." notable_decisions="[]"]
271
312
  // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
272
313
  // CIL[DOC-STABILITY stability="stable"]
314
+ // An execution row whose runner died mid-flight (detached process killed,
315
+ // function timeout) can never write its own terminal status — it wedges
316
+ // 'running' forever. Sweep rows that have not been touched within the
317
+ // staleness window whenever a new pipeline run starts. Fire-and-forget:
318
+ // the sweep is bookkeeping, never on the critical path, and a failed
319
+ // sweep retries on the next run.
320
+ const STALE_EXECUTION_MS = 10 * 60 * 1000
321
+
322
+ export async function sweepStaleExecutions(): Promise<void> {
323
+ const cutoff = new Date(Date.now() - STALE_EXECUTION_MS).toISOString()
324
+ const { error } = await adminDb
325
+ .from('pipeline_executions')
326
+ .update({
327
+ status: 'failed',
328
+ error_message: 'stale execution: runner died before terminal write (swept)',
329
+ completed_at: new Date().toISOString(),
330
+ updated_at: new Date().toISOString()
331
+ })
332
+ .in('status', ['running', 'pending', 'queued'])
333
+ .lt('updated_at', cutoff)
334
+ if (error) console.warn('pipeline_executions stale sweep failed:', error.message)
335
+ }
336
+
273
337
  export async function runPipeline(
274
338
  pipelineId: string,
275
339
  triggerData: any,
276
- ctx: CoreContext
340
+ ctx: CoreContext,
341
+ opts?: { executionId?: string }
277
342
  ): Promise<ExecutionResult> {
278
343
  return withSpan(ctx, 'pipeline.run', async (ctx) => {
279
344
  const startTime = Date.now()
280
345
 
346
+ void sweepStaleExecutions().catch(() => {})
347
+
281
348
  const { data: pipeline, error: pipelineError } = await adminDb
282
349
  .from('pipelines')
283
350
  .select('*')
@@ -289,24 +356,76 @@ export async function runPipeline(
289
356
  throw new Error(`Pipeline not found or inactive: ${pipelineId}`)
290
357
  }
291
358
 
292
- const { data: execution, error: execError } = await adminDb
293
- .from('pipeline_executions')
294
- .insert({
295
- pipeline_id: pipelineId,
296
- status: 'running',
297
- trigger_data: triggerData || {},
298
- account_id: ctx.accountId,
299
- created_by: isUuid(ctx.principal?.id) ? ctx.principal?.id : null,
300
- started_at: new Date().toISOString()
301
- })
302
- .select()
303
- .single()
359
+ // When a pre-created execution ID is supplied (dispatched trigger
360
+ // pipelines), claim that row — transitioning queued → running — rather
361
+ // than inserting a duplicate. The row was created by checkAndFireTriggers
362
+ // with status='queued' immediately before dispatch.
363
+ let executionId: string
364
+ if (opts?.executionId) {
365
+ const { data: claimed, error: claimError } = await adminDb
366
+ .from('pipeline_executions')
367
+ .update({
368
+ status: 'running',
369
+ trigger_data: triggerData || {},
370
+ started_at: new Date().toISOString(),
371
+ updated_at: new Date().toISOString()
372
+ })
373
+ .eq('id', opts.executionId)
374
+ .eq('status', 'queued')
375
+ .select('id')
304
376
 
305
- if (execError || !execution) {
306
- throw new Error(`Failed to create execution: ${execError?.message}`)
307
- }
377
+ if (claimError) {
378
+ throw new Error(`Failed to claim queued execution ${opts.executionId}: ${claimError.message}`)
379
+ }
380
+ if (claimed && claimed.length > 0) {
381
+ executionId = opts.executionId
382
+ } else {
383
+ // Claimed nothing — either the queued insert never landed (caller's
384
+ // fallback UUID) or a duplicate/swept dispatch arrived. Try inserting
385
+ // the row ourselves with the same ID: succeeds only when the row is
386
+ // genuinely absent, so duplicate deliveries die on the PK conflict
387
+ // instead of running the pipeline twice.
388
+ const { data: execution, error: execError } = await adminDb
389
+ .from('pipeline_executions')
390
+ .insert({
391
+ id: opts.executionId,
392
+ pipeline_id: pipelineId,
393
+ status: 'running',
394
+ trigger_data: triggerData || {},
395
+ account_id: ctx.accountId,
396
+ created_by: isUuid(ctx.principal?.id) ? ctx.principal?.id : null,
397
+ started_at: new Date().toISOString(),
398
+ updated_at: new Date().toISOString()
399
+ })
400
+ .select()
401
+ .single()
402
+ if (execError || !execution) {
403
+ throw new Error(`Failed to create execution: ${execError?.message}`)
404
+ }
405
+ executionId = execution.id
406
+ }
407
+ } else {
408
+ const { data: execution, error: execError } = await adminDb
409
+ .from('pipeline_executions')
410
+ .insert({
411
+ pipeline_id: pipelineId,
412
+ status: 'running',
413
+ trigger_data: triggerData || {},
414
+ account_id: ctx.accountId,
415
+ created_by: isUuid(ctx.principal?.id) ? ctx.principal?.id : null,
416
+ started_at: new Date().toISOString(),
417
+ // Seed updated_at so the stale-execution sweep has a baseline from
418
+ // t=0 — the row is 'running' the moment it exists.
419
+ updated_at: new Date().toISOString()
420
+ })
421
+ .select()
422
+ .single()
308
423
 
309
- const executionId = execution.id
424
+ if (execError || !execution) {
425
+ throw new Error(`Failed to create execution: ${execError?.message}`)
426
+ }
427
+ executionId = execution.id
428
+ }
310
429
  const stageResults: StageResult[] = []
311
430
  const stages: PipelineStage[] = pipeline.stages || []
312
431
 
@@ -365,15 +484,12 @@ export async function runPipeline(
365
484
 
366
485
  const durationMs = Date.now() - startTime
367
486
 
368
- await adminDb
369
- .from('pipeline_executions')
370
- .update({
371
- status: 'completed',
372
- result: { stages: stageResults },
373
- completed_at: new Date().toISOString(),
374
- duration_ms: durationMs
375
- })
376
- .eq('id', executionId)
487
+ await persistExecutionResult(ctx, executionId, {
488
+ status: 'completed',
489
+ result: { stages: stageResults },
490
+ completed_at: new Date().toISOString(),
491
+ duration_ms: durationMs
492
+ })
377
493
 
378
494
  await emitAudit(ctx, 'pipeline.completed', {
379
495
  type: 'pipeline_execution',
@@ -402,16 +518,13 @@ export async function runPipeline(
402
518
  } catch (error: any) {
403
519
  const durationMs = Date.now() - startTime
404
520
 
405
- await adminDb
406
- .from('pipeline_executions')
407
- .update({
408
- status: 'failed',
409
- result: { stages: stageResults },
410
- error_message: error.message,
411
- completed_at: new Date().toISOString(),
412
- duration_ms: durationMs
413
- })
414
- .eq('id', executionId)
521
+ await persistExecutionResult(ctx, executionId, {
522
+ status: 'failed',
523
+ result: { stages: stageResults },
524
+ error_message: error.message,
525
+ completed_at: new Date().toISOString(),
526
+ duration_ms: durationMs
527
+ })
415
528
 
416
529
  await emitAudit(ctx, 'pipeline.failed', {
417
530
  type: 'pipeline_execution',
@@ -1192,7 +1305,7 @@ const stageHandlers: Record<string, Function> = {
1192
1305
  if (!itemId) throw new Error('kb_ingest: could not resolve item_id from trigger data')
1193
1306
 
1194
1307
  const { kbIngest } = await import('./kb')
1195
- return await kbIngest(ctx, { id: itemId })
1308
+ return await kbIngest(ctx, { id: itemId, account_id: triggerData?.entity?.data?.account_id })
1196
1309
  },
1197
1310
 
1198
1311
  'kb_revoke': async (ctx: CoreContext, config: any) => {
@@ -27,7 +27,7 @@
27
27
 
28
28
  import { getUserDb, adminDb } from './db'
29
29
  import { createClient } from '@supabase/supabase-js'
30
- import WebSocket from 'ws'
30
+
31
31
  import { timingSafeEqual, createHash } from 'crypto'
32
32
  // CIL[DOC-VECTOR type="context" content="purpose='Shared principal and authorization module for Spine functions'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
33
33
  // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
@@ -96,10 +96,10 @@ const env = (globalThis as any).process?.env || {}
96
96
 
97
97
  // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:supabase:constant" type="constant" identity="supabase" parent="~/.framework/functions/_shared/principal.ts"]
98
98
  // CIL[DOC-MACRO purpose="Create a Supabase client configured for function use"]
99
- // CIL[DOC-RESPONSIBILITY responsibility="Provide an authenticated Supabase client with realtime transport"]
99
+ // CIL[DOC-RESPONSIBILITY responsibility="Provide an anon-key Supabase client for auth.getUser JWT verification"]
100
100
  // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
101
101
  // CIL[DOC-CONTRACT inputs="none" outputs="Supabase client" errors="none" side_effects="none"]
102
- // CIL[DOC-MICRO logic_summary="Instantiate createClient with URL, anon key and auth/realtime options" notable_decisions="[]"]
102
+ // CIL[DOC-MICRO logic_summary="Instantiate createClient with URL, anon key, and auth options. Realtime transport omitted — this client is used only for supabase.auth.getUser; no realtime channels are opened server-side." notable_decisions="ws realtime transport removed to reduce bundle size and eliminate unnecessary module-level init overhead across every function"]
103
103
  // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
104
104
  // CIL[DOC-STABILITY stability="stable"]
105
105
  const supabase = createClient(
@@ -107,10 +107,7 @@ const supabase = createClient(
107
107
  env.SUPABASE_ANON_KEY || 'placeholder',
108
108
  {
109
109
  auth: { autoRefreshToken: false, persistSession: false },
110
- realtime: { transport: WebSocket as any }
111
110
  }
112
-
113
-
114
111
  )
115
112
  // CIL[DOC-VECTOR type="context" content="purpose='Create a Supabase client configured for function use'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
116
113
  // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
@@ -269,6 +266,12 @@ export async function resolvePrincipal(event: any): Promise<Principal> {
269
266
  if (triggerId) {
270
267
  return resolveTriggerPrincipal(triggerId, event)
271
268
  }
269
+
270
+ // A bare internal-invoke secret is itself a first-class credential —
271
+ // function-to-function calls without a cron/trigger principal to
272
+ // impersonate (e.g. manual system-cron runs) get the privileged
273
+ // system principal rather than falling through to anonymous.
274
+ return SYSTEM_PRINCIPAL
272
275
  }
273
276
 
274
277
  const authHeader = event.headers?.authorization || event.headers?.Authorization
@@ -37,7 +37,6 @@
37
37
  import { CoreContext } from './middleware'
38
38
  import { adminDb } from './db'
39
39
  import { emitAudit } from './audit'
40
- import { runPipeline } from './pipeline-runner'
41
40
  // CIL[END stable_id="~/.framework/functions/_shared/trigger-engine.ts:module_header:module_header"]
42
41
  // CIL[DOC-VECTOR type="context" content="purpose='This module serves as the trigger engine for processing events within the system, coordinating various actions based on incoming triggers.'; No verified external callers" status="ready"]
43
42
  // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
@@ -203,43 +202,102 @@ interface Trigger {
203
202
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
204
203
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
205
204
 
206
- // CIL[DOC-MACRO purpose="This function checks for active triggers based on the specified event type and fires them if their conditions are met."]
207
- // CIL[DOC-RESPONSIBILITY responsibility="This function checks for active triggers based on the specified event type and fires them if their conditions are met. It exists to facilitate the execution of defined actions in response to specific events within the system."]
208
- // CIL[DOC-CONTEXT module="trigger-engine" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/pipeline-runner"]
209
- // CIL[DOC-CONTRACT inputs="- eventType: EventType (required) — the type of event that triggers the action.\\n- entityType: string (required) — the type of entity associated with the event.\\n- entityId: string (required) — the unique identifier of the entity.\\n- entityData: any (required) — the data associated with the entity.\\n- ctx: CoreContext (required) — the context for the current operation." outputs="Promise<void>" errors="none" side_effects="Database writes to update trigger last triggered time and count, RPC calls to increment trigger count, and audit log emissions. dependencies="adminDb.from('triggers'), adminDb.from('types'), runPipeline, emitAudit, adminDb.from('triggers').update"]
210
- // CIL[DOC-MICRO logic_summary="This function checks for active triggers based on the specified event type and fires them if their conditions are met. It exists to facilitate the execution of defined actions in response to specific events within the system." notable_decisions="Logs errors to the console and emits audit logs for failures."]
211
- // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
205
+ // CIL[DOC-MACRO purpose="Dispatch a trigger pipeline to the background pipeline-runner function, returning immediately without awaiting execution"]
206
+ // CIL[DOC-RESPONSIBILITY responsibility="POST the payload to /.netlify/functions/pipeline-runner-background with the internal invoke secret so the pipeline runs in an isolated process with its own connection budget"]
207
+ // CIL[DOC-CONTEXT module="trigger-engine" layer="shared-core" related_files=".framework/functions/pipeline-runner-background.ts"]
208
+ // CIL[DOC-CONTRACT inputs="pipelineId, triggerData, ctx, opts { executionId, triggerId }" outputs="Promise<void>" errors="Logs dispatch failure to console; caller's queued execution row is left for the sweeper" side_effects="HTTP POST to internal function"]
209
+ // CIL[DOC-MICRO logic_summary="Builds the dispatch URL from SITE_URL/URL env (falling back to localhost for netlify dev), sends X-Internal-Secret for machine auth, and awaits only the 202 ack bounded by a 5s AbortSignal timeout — the caller never waits for pipeline completion" notable_decisions="Does NOT await runPipeline — that was the connection-pool starvation bug; moving execution to a separate function invocation is the fix. The 5s timeout bounds the caller when Netlify's queue ack stalls; an aborted dispatch may still deliver server-side, and the pre-created 'queued' row is the sweeper's safety net either way"]
210
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
211
+ // CIL[DOC-STABILITY stability="stable"]
212
+ /**
213
+ * POST a pipeline dispatch to the internal background function.
214
+ * Returns immediately; the pipeline runs in a separate process.
215
+ * Any dispatch failure leaves the pre-created execution row 'queued' —
216
+ * the sweeper in pipeline-runner.ts marks it 'failed' after 10 minutes.
217
+ */
218
+ export async function dispatchTriggerPipeline(
219
+ pipelineId: string,
220
+ triggerData: any,
221
+ ctx: CoreContext,
222
+ opts: { executionId: string; triggerId: string }
223
+ ): Promise<void> {
224
+ const secret = (globalThis as any).process?.env?.INTERNAL_INVOKE_SECRET
225
+ if (!secret) {
226
+ console.error(`[${ctx.requestId}] INTERNAL_INVOKE_SECRET not set — cannot dispatch trigger pipeline ${pipelineId}`)
227
+ return
228
+ }
229
+
230
+ const siteUrl = (globalThis as any).process?.env?.SITE_URL ||
231
+ (globalThis as any).process?.env?.URL ||
232
+ 'http://localhost:8888'
233
+ const url = `${siteUrl.replace(/\/$/, '')}/.netlify/functions/pipeline-runner-background`
234
+
235
+ try {
236
+ const response = await fetch(url, {
237
+ method: 'POST',
238
+ headers: {
239
+ 'Content-Type': 'application/json',
240
+ 'X-Internal-Secret': secret,
241
+ },
242
+ body: JSON.stringify({
243
+ pipeline_id: pipelineId,
244
+ trigger_data: triggerData,
245
+ account_id: ctx.accountId,
246
+ execution_id: opts.executionId,
247
+ trigger_id: opts.triggerId,
248
+ }),
249
+ // Bound the caller's wait: the background function normally answers
250
+ // 202 in well under a second, but a stuck connection (deploy rollout,
251
+ // slow Netlify queue ack) once held a caller for ~34s. Aborting only
252
+ // releases this caller — if Netlify already received the POST the
253
+ // queued invocation still runs; if not, the pre-created 'queued'
254
+ // execution row is left for the sweeper.
255
+ signal: AbortSignal.timeout(5000),
256
+ })
257
+
258
+ if (!response.ok) {
259
+ const errText = await response.text().catch(() => `HTTP ${response.status}`)
260
+ console.error(`[${ctx.requestId}] trigger dispatch failed for pipeline ${pipelineId} (execution ${opts.executionId}): HTTP ${response.status} — ${errText}`)
261
+ }
262
+ } catch (err: any) {
263
+ const timedOut = err?.name === 'AbortError' || err?.name === 'TimeoutError'
264
+ console.error(`[${ctx.requestId}] trigger dispatch ${timedOut ? 'timed out after 5s' : 'error'} for pipeline ${pipelineId} (execution ${opts.executionId}):`, err.message || err)
265
+ }
266
+ }
267
+ // CIL[END stable_id="~/.framework/functions/_shared/trigger-engine.ts:dispatchtriggerpipeline:function"]
268
+
269
+
270
+ // CIL[DOC-MACRO purpose="Check for active triggers matching the event type, pre-create a queued pipeline_executions row, and dispatch to the background function"]
271
+ // CIL[DOC-RESPONSIBILITY responsibility="Evaluate trigger conditions, reserve a queued execution row, dispatch to pipeline-runner-background, record trigger fire, and emit audit — returns without waiting for pipeline completion"]
272
+ // CIL[DOC-CONTEXT module="trigger-engine" layer="shared-core" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/pipeline-runner-background.ts"]
273
+ // CIL[DOC-CONTRACT inputs="eventType, entityType, entityId, entityData, ctx, previousEntityData? (pre-update record snapshot for transition checks)" outputs="Promise<void>" errors="Logs errors; emits trigger.failed audit" side_effects="Inserts pipeline_executions row (status: queued), RPC record_trigger_fire, emits trigger.fired audit"]
274
+ // CIL[DOC-MICRO logic_summary="Queries active triggers for the event type, resolves type_slug when needed for trigger filters, evaluates each trigger's conditions, then for matches: INSERTs a queued pipeline_executions row so the execution ID exists before dispatch, calls dispatchTriggerPipeline (which returns immediately), records the trigger fire, and emits the trigger.fired audit with execution_status='queued'" notable_decisions="Pre-creating the execution row is the key to preserving execution_id in the audit trail without awaiting pipeline completion; 'queued' rows left orphaned are swept by the stale-execution reaper"]
275
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
212
276
  // CIL[DOC-STABILITY stability="stable"]
213
277
  /**
214
- * This function checks for active triggers based on the specified event type and fires them if their conditions are met. It exists to facilitate the execution of defined actions in response to specific events within the system.
278
+ * Check for active triggers matching the event type and dispatch each to the
279
+ * background pipeline-runner function. Returns immediately — pipeline
280
+ * completion is tracked in pipeline_executions independently.
215
281
  *
216
282
  * @inputSpec
217
- * - eventType: EventType (required) — the type of event that triggers the action.
218
- * - entityType: string (required) — the type of entity associated with the event.
219
- * - entityId: string (required) — the unique identifier of the entity.
220
- * - entityData: any (required) — the data associated with the entity.
221
- * - ctx: CoreContext (required) — the context for the current operation.
283
+ * - eventType: EventType — the event that occurred.
284
+ * - entityType: string — the entity type (plural runtime table name).
285
+ * - entityId: string — the affected entity ID.
286
+ * - entityData: any — the entity snapshot at the time of the event.
287
+ * - ctx: CoreContext — request context (account scope, request ID, principal).
222
288
  * @outputSpec Promise<void>
223
- * @sideEffects Database writes to update trigger last triggered time and count, RPC calls to increment trigger count, and audit log emissions.
289
+ * @sideEffects INSERTs pipeline_executions rows (status: queued),
290
+ * RPC record_trigger_fire per matching trigger,
291
+ * emits trigger.fired / trigger.failed audit events.
224
292
  * @stability stable
225
- * @calls adminDb.from('triggers'), adminDb.from('types'), runPipeline, emitAudit, adminDb.from('triggers').update
226
- * @errorHandling Logs errors to the console and emits audit logs for failures.
227
- * @cil-generated
228
293
  */
229
- // CIL[BEGIN stable_id="~/.framework/functions/_shared/trigger-engine.ts:checkandfiretriggers:function" type="function" identity="checkAndFireTriggers" parent="~/.framework/functions/_shared/trigger-engine.ts"]
230
- // CIL[DOC-MACRO purpose="checkAndFireTriggers function"]
231
- // CIL[DOC-RESPONSIBILITY responsibility="Implements the checkAndFireTriggers function."]
232
- // CIL[DOC-CONTEXT module=".framework/functions/_shared/trigger-engine.ts" layer="app-specific" related_files=""]
233
- // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
234
- // CIL[DOC-MICRO logic_summary="Implements the checkAndFireTriggers function." notable_decisions="[]"]
235
- // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
236
- // CIL[DOC-STABILITY stability="stable"]
237
294
  export async function checkAndFireTriggers(
238
295
  eventType: EventType,
239
296
  entityType: string,
240
297
  entityId: string,
241
298
  entityData: any,
242
- ctx: CoreContext
299
+ ctx: CoreContext,
300
+ previousEntityData?: any
243
301
  ): Promise<void> {
244
302
  const { data: triggers, error } = await adminDb
245
303
  .from('triggers')
@@ -270,11 +328,30 @@ export async function checkAndFireTriggers(
270
328
  for (const trigger of triggers) {
271
329
  try {
272
330
  const shouldFire = evaluateTriggerConditions(trigger, entityType, entityData)
273
-
331
+
274
332
  if (!shouldFire) {
275
333
  continue
276
334
  }
277
335
 
336
+ // Transition-only triggers (config.fire_on_transition): fire only when
337
+ // the record moves INTO the matching state. Without this, every
338
+ // update to an already-matching record re-fires the pipeline — e.g.
339
+ // each write to a published KB article re-dispatched auto_embed (3×
340
+ // per publish) and revoke fired on articles that never left
341
+ // 'published'. When no previous snapshot is available the trigger
342
+ // fires normally (conservative: matches pre-flag behavior).
343
+ if (trigger.config?.fire_on_transition && previousEntityData) {
344
+ const prevEntityData = {
345
+ ...previousEntityData,
346
+ // Same record → same type; the slug was resolved on the new
347
+ // snapshot only, so carry it over for config.type_slug checks.
348
+ type_slug: previousEntityData.type_slug ?? entityData?.type_slug
349
+ }
350
+ if (evaluateTriggerConditions(trigger, entityType, prevEntityData)) {
351
+ continue
352
+ }
353
+ }
354
+
278
355
  const triggerData = {
279
356
  event: eventType,
280
357
  entity_id: entityId,
@@ -290,15 +367,46 @@ export async function checkAndFireTriggers(
290
367
  fired_at: new Date().toISOString()
291
368
  }
292
369
 
293
- const result = await runPipeline(trigger.pipeline_id, triggerData, ctx)
294
-
295
- await adminDb
296
- .from('triggers')
297
- .update({
298
- last_triggered: new Date().toISOString(),
299
- trigger_count: adminDb.rpc('increment_trigger_count', { p_trigger_id: trigger.id })
370
+ // Reserve the execution row before dispatch so the audit trail has a
371
+ // stable execution_id even though the pipeline has not started yet.
372
+ // The background function claims this row (queued → running) via
373
+ // runPipeline's opts.executionId parameter. If dispatch fails, this
374
+ // row stays 'queued' and the sweeper marks it 'failed' after 10 min.
375
+ const { data: execRow } = await adminDb
376
+ .from('pipeline_executions')
377
+ .insert({
378
+ pipeline_id: trigger.pipeline_id,
379
+ status: 'queued',
380
+ trigger_data: triggerData,
381
+ account_id: ctx.accountId,
382
+ // Same guard as runPipeline's own insert: created_by is uuid-typed,
383
+ // non-UUID principals (machine ids like 'system', 'timer:*') → null.
384
+ created_by: ctx.principal?.id &&
385
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(ctx.principal.id)
386
+ ? ctx.principal.id : null,
387
+ started_at: new Date().toISOString(),
388
+ updated_at: new Date().toISOString()
300
389
  })
301
- .eq('id', trigger.id)
390
+ .select('id')
391
+ .single()
392
+
393
+ const executionId = execRow?.id ?? crypto.randomUUID()
394
+
395
+ await dispatchTriggerPipeline(trigger.pipeline_id, triggerData, ctx, {
396
+ executionId,
397
+ triggerId: trigger.id,
398
+ })
399
+
400
+ // Atomic bookkeeping: the previous update embedded an
401
+ // adminDb.rpc() Promise as a column value, so the whole write
402
+ // failed silently — trigger_count stayed 0 and last_triggered
403
+ // stayed null on every fire.
404
+ const { error: fireError } = await adminDb.rpc('record_trigger_fire', {
405
+ p_trigger_id: trigger.id
406
+ })
407
+ if (fireError) {
408
+ console.error(`[${ctx.requestId}] record_trigger_fire failed for ${trigger.id}:`, fireError.message)
409
+ }
302
410
 
303
411
  await emitAudit(ctx, 'trigger.fired', {
304
412
  type: 'trigger',
@@ -309,13 +417,13 @@ export async function checkAndFireTriggers(
309
417
  entity_type: entityType,
310
418
  entity_id: entityId,
311
419
  pipeline_id: trigger.pipeline_id,
312
- execution_id: result.executionId,
313
- execution_status: result.status
420
+ execution_id: executionId,
421
+ execution_status: 'queued'
314
422
  })
315
423
 
316
424
  } catch (error: any) {
317
425
  console.error(`[${ctx.requestId}] Trigger ${trigger.id} failed:`, error)
318
-
426
+
319
427
  await emitAudit(ctx, 'trigger.failed', {
320
428
  type: 'trigger',
321
429
  id: trigger.id,
@@ -602,10 +710,11 @@ export async function fireUpdateTriggers(
602
710
  entityType: string,
603
711
  entityId: string,
604
712
  entityData: any,
605
- ctx: CoreContext
713
+ ctx: CoreContext,
714
+ previousEntityData?: any
606
715
  ): Promise<void> {
607
716
  const eventType = `${eventNameFor(entityType)}_updated` as EventType
608
- await checkAndFireTriggers(eventType, entityType, entityId, entityData, ctx)
717
+ await checkAndFireTriggers(eventType, entityType, entityId, entityData, ctx, previousEntityData)
609
718
  }
610
719
  // CIL[END stable_id="~/.framework/functions/_shared/trigger-engine.ts:fireupdatetriggers:function"]
611
720
  // CIL[DOC-VECTOR type="context" content="purpose='Triggers an update event for a specified entity type and ID, allowing for associated actions to be executed.'; No verified external callers" status="ready"]