spine-framework 0.4.2 → 0.4.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 (46) hide show
  1. package/.framework/functions/_shared/agent-runner.ts +1000 -81
  2. package/.framework/functions/_shared/cil.ts +2 -1
  3. package/.framework/functions/_shared/db.ts +3 -6
  4. package/.framework/functions/_shared/kb.ts +61 -7
  5. package/.framework/functions/_shared/message-sequence.ts +53 -0
  6. package/.framework/functions/_shared/pipeline-runner.ts +150 -37
  7. package/.framework/functions/_shared/principal.ts +9 -6
  8. package/.framework/functions/_shared/trigger-engine.ts +149 -40
  9. package/.framework/functions/admin-data.ts +9 -3
  10. package/.framework/functions/ai-agents.ts +93 -27
  11. package/.framework/functions/pipeline-runner-background.ts +120 -0
  12. package/.framework/functions/system-cron.ts +10 -1
  13. package/.framework/functions/trigger-scheduler.ts +65 -5
  14. package/.framework/migrations/064_episode_leases.sql +196 -0
  15. package/.framework/migrations/065_execution_tables_updated_at.sql +20 -0
  16. package/.framework/migrations/066_episode_steal_thread_merge.sql +148 -0
  17. package/.framework/migrations/067_episode_lease_stale_steal.sql +93 -0
  18. package/.framework/migrations/068_cron_schedule_timer_rpcs.sql +251 -0
  19. package/.framework/schemas/spine.config.schema.json +62 -0
  20. package/.framework/src/App.tsx +3 -1
  21. package/.framework/src/components/admin/AdminSidebar.tsx +4 -2
  22. package/.framework/src/components/app-shell/GenericAppShell.tsx +3 -1
  23. package/.framework/src/components/auth/TurnstileWidget.tsx +9 -6
  24. package/.framework/src/components/kb/KBArticleEditor.tsx +49 -6
  25. package/.framework/src/components/layout/Layout.tsx +4 -2
  26. package/.framework/src/components/layout/Sidebar.tsx +4 -2
  27. package/.framework/src/hooks/useBranding.ts +47 -0
  28. package/.framework/src/lib/branding.ts +128 -0
  29. package/.framework/src/pages/NoAppPage.tsx +3 -1
  30. package/.framework/src/pages/SetupPage.tsx +5 -2
  31. package/.framework/src/pages/auth/AuthConfirmPage.tsx +3 -1
  32. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +3 -1
  33. package/.framework/src/pages/auth/LoginPage.tsx +4 -2
  34. package/.framework/src/pages/auth/RegisterPage.tsx +3 -1
  35. package/.framework/templates/project.json +1 -0
  36. package/.framework/tests/unit/agent-runner.test.ts +17 -3
  37. package/.framework/tests/unit/episode-lease.test.ts +695 -0
  38. package/.framework/tests/unit/kb-ingest-lease.test.ts +19 -0
  39. package/.framework/tests/unit/pipeline-runner-background.test.ts +120 -0
  40. package/.framework/tests/unit/pipeline-runner.test.ts +153 -1
  41. package/.framework/tests/unit/principal-cache.test.ts +15 -0
  42. package/.framework/tests/unit/trigger-engine.test.ts +278 -91
  43. package/.framework/tests/unit/trigger-scheduler.test.ts +78 -0
  44. package/netlify.toml +5 -0
  45. package/package.json +1 -1
  46. package/templates/project/package-lock.json +2 -2
@@ -0,0 +1,128 @@
1
+ // CIL[BEGIN stable_id="~/.framework/src/lib/branding.ts:module_header:module_header" type="module_header" identity="module_header" parent="~/.framework/src/lib/branding.ts"]
2
+ // CIL[DOC-MACRO purpose="Instance branding resolved from spine.config.json — app name, logo, favicon, document title"]
3
+ // CIL[DOC-RESPONSIBILITY responsibility="Load the optional branding block from /spine.config.json once per session, merge it over Spine defaults, and apply favicon/title to the document"]
4
+ // CIL[DOC-CONTEXT module="framework-api" layer="lib" related_files=".framework/src/hooks/useBranding.ts,spine.config.json,.framework/index.html"]
5
+ // CIL[DOC-CONTRACT inputs="none" outputs="SpineBranding resolved object; applyBrandingToDocument performs DOM mutation" errors="fetch or parse failures fall back to DEFAULT_BRANDING" side_effects="HTTP GET /spine.config.json (single cached request); document.title and link[rel=icon] mutation"]
6
+ // CIL[DOC-MICRO logic_summary="Module-level cache plus single inflight fetch; resolveBranding fills missing keys with defaults; applyBrandingToDocument sets document.title and swaps the favicon link href" notable_decisions="Branding lives in instance-owned spine.config.json so framework upgrades can never overwrite app branding; index.html keeps the Spine default and the runtime swap handles app-branded instances"]
7
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
8
+ // CIL[DOC-STABILITY stability="experimental"]
9
+ // CIL[DOC-AUDIENCE audience="framework developers"]
10
+ // CIL[DOC-NOTE type="contract" content="branding is a top-level sibling of registration in spine.config.json. All keys optional; absent block yields DEFAULT_BRANDING so bare framework installs stay Spine-branded."]
11
+ // CIL[END stable_id="~/.framework/src/lib/branding.ts:module_header:module_header"]
12
+
13
+ // CIL[BEGIN stable_id="~/.framework/src/lib/branding.ts:spinebranding:interface" type="interface" identity="SpineBranding" parent="~/.framework/src/lib/branding.ts"]
14
+ // CIL[DOC-MACRO purpose="Resolved instance branding shape"]
15
+ // CIL[DOC-RESPONSIBILITY responsibility="Type for the branding block after defaults are applied"]
16
+ // CIL[DOC-CONTEXT module="framework-api" layer="lib" related_files=".framework/src/lib/branding.ts"]
17
+ // CIL[DOC-CONTRACT inputs="none" outputs="SpineBranding object" errors="none" side_effects="none"]
18
+ // CIL[DOC-MICRO logic_summary="app_name drives headings and logo alt; title drives document.title and wordmark text; logo_url and favicon_url are public/ asset paths" notable_decisions="title falls back to app_name, then to 'Spine Framework' when no branding block exists"]
19
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
20
+ // CIL[DOC-STABILITY stability="experimental"]
21
+ export interface SpineBranding {
22
+ app_name: string
23
+ title: string
24
+ logo_url: string
25
+ favicon_url: string
26
+ }
27
+ // CIL[END stable_id="~/.framework/src/lib/branding.ts:spinebranding:interface"]
28
+
29
+ // CIL[BEGIN stable_id="~/.framework/src/lib/branding.ts:default_branding:constant" type="constant" identity="DEFAULT_BRANDING" parent="~/.framework/src/lib/branding.ts"]
30
+ // CIL[DOC-MACRO purpose="Spine Framework default branding used when spine.config.json has no branding block"]
31
+ // CIL[DOC-RESPONSIBILITY responsibility="Provide the fallback brand so the framework is self-branded out of the box"]
32
+ // CIL[DOC-CONTEXT module="framework-api" layer="lib" related_files=".framework/src/lib/branding.ts,.framework/index.html"]
33
+ // CIL[DOC-CONTRACT inputs="none" outputs="SpineBranding constant" errors="none" side_effects="none"]
34
+ // CIL[DOC-MICRO logic_summary="Matches the static values in .framework/index.html (title 'Spine Framework', favicon /spine-logo.svg)" notable_decisions="[]"]
35
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
36
+ // CIL[DOC-STABILITY stability="experimental"]
37
+ export const DEFAULT_BRANDING: SpineBranding = {
38
+ app_name: 'Spine',
39
+ title: 'Spine Framework',
40
+ logo_url: '/spine-logo.svg',
41
+ favicon_url: '/spine-logo.svg',
42
+ }
43
+ // CIL[END stable_id="~/.framework/src/lib/branding.ts:default_branding:constant"]
44
+
45
+ let cached: SpineBranding | null = null
46
+ let inflight: Promise<SpineBranding> | null = null
47
+
48
+ // CIL[BEGIN stable_id="~/.framework/src/lib/branding.ts:resolvebranding:function" type="function" identity="resolveBranding" parent="~/.framework/src/lib/branding.ts"]
49
+ // CIL[DOC-MACRO purpose="Merge a raw branding block over defaults"]
50
+ // CIL[DOC-RESPONSIBILITY responsibility="Produce a complete SpineBranding from a possibly partial branding config value"]
51
+ // CIL[DOC-CONTEXT module="framework-api" layer="lib" related_files=".framework/src/lib/branding.ts"]
52
+ // CIL[DOC-CONTRACT inputs="raw: unknown (config.branding)" outputs="SpineBranding" errors="none" side_effects="none"]
53
+ // CIL[DOC-MICRO logic_summary="Each key falls back independently; title falls back to the resolved app_name so app-branded instances get a sane tab title without duplicating config" notable_decisions="non-string values treated as absent"]
54
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
55
+ // CIL[DOC-STABILITY stability="experimental"]
56
+ function resolveBranding(raw: unknown): SpineBranding {
57
+ const b = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>
58
+ const appName = typeof b.app_name === 'string' && b.app_name ? b.app_name : DEFAULT_BRANDING.app_name
59
+ const configuredTitle = typeof b.title === 'string' && b.title ? b.title : null
60
+ return {
61
+ app_name: appName,
62
+ title: configuredTitle ?? (b.app_name ? appName : DEFAULT_BRANDING.title),
63
+ logo_url: typeof b.logo_url === 'string' && b.logo_url ? b.logo_url : DEFAULT_BRANDING.logo_url,
64
+ favicon_url:
65
+ typeof b.favicon_url === 'string' && b.favicon_url ? b.favicon_url : DEFAULT_BRANDING.favicon_url,
66
+ }
67
+ }
68
+ // CIL[END stable_id="~/.framework/src/lib/branding.ts:resolvebranding:function"]
69
+
70
+ // CIL[BEGIN stable_id="~/.framework/src/lib/branding.ts:loadbranding:function" type="function" identity="loadBranding" parent="~/.framework/src/lib/branding.ts"]
71
+ // CIL[DOC-MACRO purpose="Fetch and cache the instance branding from /spine.config.json"]
72
+ // CIL[DOC-RESPONSIBILITY responsibility="Perform a single fetch per session and memoize the resolved branding"]
73
+ // CIL[DOC-CONTEXT module="framework-api" layer="lib" related_files=".framework/src/lib/branding.ts,.framework/src/hooks/useBranding.ts"]
74
+ // CIL[DOC-CONTRACT inputs="none" outputs="Promise<SpineBranding>" errors="network/parse failures resolve to DEFAULT_BRANDING" side_effects="one HTTP GET /spine.config.json per session"]
75
+ // CIL[DOC-MICRO logic_summary="Returns cached value if present; otherwise shares one inflight promise across callers; any failure caches defaults so a missing config never retries per component" notable_decisions="[]"]
76
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
77
+ // CIL[DOC-STABILITY stability="experimental"]
78
+ export function loadBranding(): Promise<SpineBranding> {
79
+ if (cached) return Promise.resolve(cached)
80
+ if (!inflight) {
81
+ inflight = fetch('/spine.config.json')
82
+ .then(res => (res.ok ? res.json() : null))
83
+ .then(config => {
84
+ cached = resolveBranding(config?.branding)
85
+ return cached
86
+ })
87
+ .catch(() => {
88
+ cached = DEFAULT_BRANDING
89
+ return cached
90
+ })
91
+ }
92
+ return inflight
93
+ }
94
+ // CIL[END stable_id="~/.framework/src/lib/branding.ts:loadbranding:function"]
95
+
96
+ // CIL[BEGIN stable_id="~/.framework/src/lib/branding.ts:getcachedbranding:function" type="function" identity="getCachedBranding" parent="~/.framework/src/lib/branding.ts"]
97
+ // CIL[DOC-MACRO purpose="Synchronous read of resolved branding, or defaults before the fetch lands"]
98
+ // CIL[DOC-RESPONSIBILITY responsibility="Give render paths a branding value without awaiting"]
99
+ // CIL[DOC-CONTEXT module="framework-api" layer="lib" related_files=".framework/src/lib/branding.ts"]
100
+ // CIL[DOC-CONTRACT inputs="none" outputs="SpineBranding" errors="none" side_effects="none"]
101
+ // CIL[DOC-MICRO logic_summary="cached ?? DEFAULT_BRANDING" notable_decisions="[]"]
102
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
103
+ // CIL[DOC-STABILITY stability="experimental"]
104
+ export function getCachedBranding(): SpineBranding {
105
+ return cached ?? DEFAULT_BRANDING
106
+ }
107
+ // CIL[END stable_id="~/.framework/src/lib/branding.ts:getcachedbranding:function"]
108
+
109
+ // CIL[BEGIN stable_id="~/.framework/src/lib/branding.ts:applybrandingtodocument:function" type="function" identity="applyBrandingToDocument" parent="~/.framework/src/lib/branding.ts"]
110
+ // CIL[DOC-MACRO purpose="Apply resolved branding to document.title and the favicon link"]
111
+ // CIL[DOC-RESPONSIBILITY responsibility="Swap the framework-shipped favicon/title for instance branding at runtime"]
112
+ // CIL[DOC-CONTEXT module="framework-api" layer="lib" related_files=".framework/src/lib/branding.ts,.framework/index.html"]
113
+ // CIL[DOC-CONTRACT inputs="branding: SpineBranding" outputs="void" errors="none" side_effects="sets document.title; rewrites or creates link[rel~=icon] in document.head"]
114
+ // CIL[DOC-MICRO logic_summary="Idempotent DOM mutation — safe to call on every branding state change from every consumer" notable_decisions="runtime swap keeps .framework/index.html a static framework-owned file that upgrades can replace freely"]
115
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
116
+ // CIL[DOC-STABILITY stability="experimental"]
117
+ export function applyBrandingToDocument(branding: SpineBranding): void {
118
+ if (branding.title) document.title = branding.title
119
+ if (!branding.favicon_url) return
120
+ let link = document.querySelector<HTMLLinkElement>('link[rel~="icon"]')
121
+ if (!link) {
122
+ link = document.createElement('link')
123
+ link.rel = 'icon'
124
+ document.head.appendChild(link)
125
+ }
126
+ link.href = branding.favicon_url
127
+ }
128
+ // CIL[END stable_id="~/.framework/src/lib/branding.ts:applybrandingtodocument:function"]
@@ -10,6 +10,7 @@
10
10
 
11
11
  import React from 'react'
12
12
  import { useAuth } from '../contexts/AuthContext'
13
+ import { useBranding } from '../hooks/useBranding'
13
14
  // CIL[END stable_id="~/.framework/src/pages/NoAppPage.tsx:module_header:module_header"]
14
15
 
15
16
  // CIL[BEGIN stable_id="~/.framework/src/pages/NoAppPage.tsx:noapppage:component" type="component" identity="NoAppPage" parent="~/.framework/src/pages/NoAppPage.tsx"]
@@ -22,12 +23,13 @@ import { useAuth } from '../contexts/AuthContext'
22
23
  // CIL[DOC-STABILITY stability="experimental"]
23
24
  export function NoAppPage() {
24
25
  const { logout } = useAuth()
26
+ const branding = useBranding()
25
27
 
26
28
  return (
27
29
  <div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
28
30
  <div className="max-w-md w-full text-center space-y-6">
29
31
  <div className="mx-auto flex items-center justify-center">
30
- <img src="/spine-logo.svg" alt="Spine Framework" className="h-14 w-auto" />
32
+ <img src={branding.logo_url} alt={branding.app_name} className="h-14 w-auto" />
31
33
  </div>
32
34
  <div className="space-y-2">
33
35
  <h1 className="text-2xl font-semibold text-slate-900">No apps yet</h1>
@@ -10,6 +10,7 @@
10
10
 
11
11
  import React from 'react'
12
12
  import { Link } from 'react-router-dom'
13
+ import { useBranding } from '../hooks/useBranding'
13
14
  // CIL[DOC-VECTOR type="context" content="purpose='Declare module-level imports for the Setup page'; No verified external callers; imports: none"]
14
15
  // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
15
16
  // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
@@ -28,15 +29,17 @@ import { Link } from 'react-router-dom'
28
29
  // CIL[DOC-STABILITY stable]
29
30
 
30
31
  export function SetupPage() {
32
+ const branding = useBranding()
33
+
31
34
  return (
32
35
  <div className="min-h-screen flex items-center justify-center bg-slate-50 py-12 px-4 sm:px-6 lg:px-8">
33
36
  <div className="max-w-lg w-full space-y-8">
34
37
  <div className="text-center">
35
38
  <div className="mx-auto flex items-center justify-center">
36
- <img src="/spine-logo.svg" alt="Spine Framework" className="h-14 w-auto" />
39
+ <img src={branding.logo_url} alt={branding.app_name} className="h-14 w-auto" />
37
40
  </div>
38
41
  <h2 className="mt-6 text-3xl font-extrabold text-slate-900">
39
- Welcome to Spine
42
+ Welcome to {branding.app_name}
40
43
  </h2>
41
44
  <p className="mt-2 text-sm text-slate-600">
42
45
  Your project is running but no apps are installed yet.
@@ -17,6 +17,7 @@ import {
17
17
  finalizeRegistration,
18
18
  readPendingRegistration,
19
19
  } from '../../lib/registration'
20
+ import { useBranding } from '../../hooks/useBranding'
20
21
  // CIL[DOC-VECTOR type="context" content="purpose='Defines the AuthConfirmPage React component for confirming email signup and password reset tokens.'; No verified external callers; imports: .framework/src/lib/supabase.ts,.framework/src/components/ui/LoadingSpinner.tsx"]
21
22
  // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
22
23
  // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
@@ -59,6 +60,7 @@ export function AuthConfirmPage() {
59
60
  const [searchParams] = useSearchParams()
60
61
  const navigate = useNavigate()
61
62
  const { refreshUser } = useAuth()
63
+ const branding = useBranding()
62
64
 
63
65
  const tokenHash = searchParams.get('token_hash')
64
66
  const type = searchParams.get('type') as 'signup' | 'recovery' | string | null
@@ -275,7 +277,7 @@ export function AuthConfirmPage() {
275
277
  <div className="max-w-md w-full space-y-8">
276
278
  <div>
277
279
  <div className="mx-auto flex items-center justify-center">
278
- <img src="/spine-logo.svg" alt="Spine Framework" className="h-14 w-auto" />
280
+ <img src={branding.logo_url} alt={branding.app_name} className="h-14 w-auto" />
279
281
  </div>
280
282
  <h2 className="mt-6 text-center text-3xl font-extrabold text-slate-900">
281
283
  Set a new password
@@ -13,6 +13,7 @@ import { Link } from 'react-router-dom'
13
13
  import { supabase } from '../../lib/supabase'
14
14
  import { LoadingSpinner } from '../../components/ui/LoadingSpinner'
15
15
  import { TurnstileWidget, turnstileEnabled } from '../../components/auth/TurnstileWidget'
16
+ import { useBranding } from '../../hooks/useBranding'
16
17
 
17
18
  // CIL[DOC-VECTOR type="context" content="purpose='Provides imports and entry-point metadata for the ForgotPasswordPage component.'; No verified external callers; imports: .framework/src/lib/supabase.ts,.framework/src/components/ui/LoadingSpinner.tsx"]
18
19
  // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
@@ -36,6 +37,7 @@ export function ForgotPasswordPage() {
36
37
  const [isLoading, setIsLoading] = useState(false)
37
38
  const [submitted, setSubmitted] = useState(false)
38
39
  const [error, setError] = useState('')
40
+ const branding = useBranding()
39
41
 
40
42
  const handleSubmit = async (e: React.FormEvent) => {
41
43
  e.preventDefault()
@@ -91,7 +93,7 @@ export function ForgotPasswordPage() {
91
93
  <div className="max-w-md w-full space-y-8">
92
94
  <div>
93
95
  <div className="mx-auto flex items-center justify-center">
94
- <img src="/spine-logo.svg" alt="Spine Framework" className="h-14 w-auto" />
96
+ <img src={branding.logo_url} alt={branding.app_name} className="h-14 w-auto" />
95
97
  </div>
96
98
  <h2 className="mt-6 text-center text-3xl font-extrabold text-slate-900">
97
99
  Reset your password
@@ -13,6 +13,7 @@ import { Link, useNavigate } from 'react-router-dom'
13
13
  import { useAuth } from '../../contexts/AuthContext'
14
14
  import { LoadingSpinner } from '../../components/ui/LoadingSpinner'
15
15
  import { TurnstileWidget, turnstileEnabled } from '../../components/auth/TurnstileWidget'
16
+ import { useBranding } from '../../hooks/useBranding'
16
17
 
17
18
  // CIL[DOC-VECTOR type="context" content="purpose='Module header for the LoginPage component: imports, dependencies, and module-level context.'; No verified external callers; imports: .framework/src/contexts/AuthContext.tsx,.framework/src/components/ui/LoadingSpinner.tsx"]
18
19
  // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
@@ -37,6 +38,7 @@ export function LoginPage() {
37
38
  const [isLoading, setIsLoading] = useState(false)
38
39
 
39
40
  const { login } = useAuth()
41
+ const branding = useBranding()
40
42
  const navigate = useNavigate()
41
43
 
42
44
  const handleSubmit = async (e: React.FormEvent) => {
@@ -74,10 +76,10 @@ export function LoginPage() {
74
76
  <div className="max-w-md w-full space-y-8">
75
77
  <div>
76
78
  <div className="mx-auto flex items-center justify-center">
77
- <img src="/spine-logo.svg" alt="Spine Framework" className="h-14 w-auto" />
79
+ <img src={branding.logo_url} alt={branding.app_name} className="h-14 w-auto" />
78
80
  </div>
79
81
  <h2 className="mt-6 text-center text-3xl font-extrabold text-slate-900">
80
- Sign in to Spine
82
+ Sign in to {branding.app_name}
81
83
  </h2>
82
84
  <p className="mt-2 text-center text-sm text-slate-600">
83
85
  Or{' '}
@@ -23,6 +23,7 @@ import {
23
23
  PendingRegistrationIntent,
24
24
  } from '../../lib/registration'
25
25
  import { TurnstileWidget, turnstileEnabled } from '../../components/auth/TurnstileWidget'
26
+ import { useBranding } from '../../hooks/useBranding'
26
27
  // CIL[DOC-NOTE type="change" content="Batch4b: POST invites calls go through apiFetch (authenticated); pending-registration intent helpers imported for confirm-email resume"]
27
28
  // CIL[END stable_id="~/.framework/src/pages/auth/RegisterPage.tsx:module_header:module_header"]
28
29
  // CIL[BEGIN stable_id="~/.framework/src/pages/auth/RegisterPage.tsx:usetype:type" type="type" identity="UseType" parent="~/.framework/src/pages/auth/RegisterPage.tsx"]
@@ -270,6 +271,7 @@ export function RegisterPage() {
270
271
  const [searchParams] = useSearchParams()
271
272
  const navigate = useNavigate()
272
273
  const { user, refreshUser, logout } = useAuth()
274
+ const branding = useBranding()
273
275
  const inviteToken = searchParams.get('t')
274
276
  const resumeRequested = searchParams.get('resume') === '1'
275
277
 
@@ -652,7 +654,7 @@ export function RegisterPage() {
652
654
  <div className="max-w-md w-full space-y-8">
653
655
  <div>
654
656
  <div className="mx-auto flex items-center justify-center">
655
- <img src="/spine-logo.svg" alt="Spine Framework" className="h-14 w-auto" />
657
+ <img src={branding.logo_url} alt={branding.app_name} className="h-14 w-auto" />
656
658
  </div>
657
659
  <h2 className="mt-6 text-center text-3xl font-extrabold text-slate-900">
658
660
  {isInvite ? 'Complete your registration' : 'Create your account'}
@@ -4,6 +4,7 @@
4
4
  ".framework/functions",
5
5
  ".framework/migrations",
6
6
  ".framework/public",
7
+ ".framework/schemas",
7
8
  ".framework/scripts",
8
9
  ".framework/seeds",
9
10
  ".framework/src",
@@ -1370,7 +1370,7 @@ describe('executeToolAction — cil_chunk_lookup code content search', () => {
1370
1370
  describe('executeToolAction — escalate_to_human', () => {
1371
1371
  beforeEach(() => vi.clearAllMocks())
1372
1372
 
1373
- function mockEscalationFlow(opts: { escalationStatus?: string } = {}) {
1373
+ function mockEscalationFlow(opts: { escalationStatus?: string; owner?: string } = {}) {
1374
1374
  const updates: { table: string; payload: any }[] = []
1375
1375
  vi.mocked(adminDb.from).mockImplementation((table: string) => {
1376
1376
  const q: any = {
@@ -1381,7 +1381,7 @@ describe('executeToolAction — escalate_to_human', () => {
1381
1381
  update: vi.fn((payload: any) => { updates.push({ table, payload }); return q }),
1382
1382
  single: vi.fn(() => {
1383
1383
  if (table === 'threads' && q._cols === 'data') {
1384
- return Promise.resolve({ data: { data: { escalation_status: opts.escalationStatus } }, error: null })
1384
+ return Promise.resolve({ data: { data: { owner: opts.owner, escalation_status: opts.escalationStatus } }, error: null })
1385
1385
  }
1386
1386
  if (table === 'threads' && q._cols === 'target_type, target_id') {
1387
1387
  return Promise.resolve({ data: { target_type: 'items', target_id: 'item-1' }, error: null })
@@ -1419,7 +1419,7 @@ describe('executeToolAction — escalate_to_human', () => {
1419
1419
  expect(res).toEqual({ escalated: true, already_escalated: false, reason: 'no relevant docs' })
1420
1420
  expect(updates).toContainEqual({
1421
1421
  table: 'threads',
1422
- payload: { data: { escalation_status: 'pending' } }
1422
+ payload: { data: { owner: 'human' } }
1423
1423
  })
1424
1424
  expect(updates).toContainEqual({
1425
1425
  table: 'items',
@@ -1440,6 +1440,20 @@ describe('executeToolAction — escalate_to_human', () => {
1440
1440
  expect(res.already_escalated).toBe(true)
1441
1441
  expect(updates).toHaveLength(0)
1442
1442
  })
1443
+
1444
+ it('dedupes when the thread is already human-owned', async () => {
1445
+ const updates = mockEscalationFlow({ owner: 'human' })
1446
+
1447
+ const res = await executeToolAction(
1448
+ { handler: 'escalate_to_human' },
1449
+ { reason: 'still stuck' },
1450
+ makeCtx(),
1451
+ { threadId: THREAD_ID, config: escConfig }
1452
+ )
1453
+
1454
+ expect(res.already_escalated).toBe(true)
1455
+ expect(updates).toHaveLength(0)
1456
+ })
1443
1457
  })
1444
1458
  // CIL[DOC-VECTOR type="context" content="purpose='Test executeToolAction escalate_to_human handler'; No verified external callers; imports: .framework/functions/_shared/middleware.ts,.framework/functions/_shared/db.ts,.framework/functions/_shared/audit.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/outcomes.ts,.framework/functions/admin-data.ts,.framework/functions/integration-outbound.ts,.framework/functions/_shared/agent-runner.ts,.framework/functions/_shared/cil.ts,.framework/functions/_shared/pipeline-runner.ts"]
1445
1459
  // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]