@stackbone/cli 0.1.0-alpha.8 → 0.1.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.
@@ -1,6 +1,6 @@
1
1
  -- 0001_runs.sql — schema and tables for the run-level surface of Studio.
2
2
  -- Spec: docs/arquitectura/specs/stackbone-agent-protocol-v1.md §6 (runs, run_steps).
3
- -- ADR: docs/arquitectura/decisiones/2026-05-03-stackbone-studio-datapath-y-scope-mvp.md §D2/D7/D9.
3
+ -- ADR: docs/arquitectura/decisiones/2026-05-30-stackbone-studio.md §D2/D7/D9.
4
4
 
5
5
  CREATE SCHEMA IF NOT EXISTS stackbone_platform;
6
6
 
@@ -1,7 +1,7 @@
1
1
  -- 0002_approvals.sql — HITL approvals queue and audit trail.
2
2
  -- Spec: docs/arquitectura/specs/stackbone-agent-protocol-v1.md §6.6/§6.7.
3
3
  -- Component: docs/arquitectura/componentes/08-hitl-inbox.md.
4
- -- ADR: docs/arquitectura/decisiones/2026-05-03-stackbone-studio-datapath-y-scope-mvp.md §D2.
4
+ -- ADR: docs/arquitectura/decisiones/2026-05-30-stackbone-studio.md §D2.
5
5
 
6
6
  CREATE TABLE IF NOT EXISTS stackbone_platform.approvals (
7
7
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -1,6 +1,6 @@
1
1
  -- 0003_runs_is_playground_index.sql — billing-friendly partial index over runs.
2
2
  -- Spec: docs/tasks/Studio V5 - Playground e Invoke.md (V5#01).
3
- -- ADR: docs/arquitectura/decisiones/2026-05-03-stackbone-studio-datapath-y-scope-mvp.md §D5/D9.
3
+ -- ADR: docs/arquitectura/decisiones/2026-05-30-stackbone-studio.md §D5/D9.
4
4
  --
5
5
  -- The `is_playground` column itself already lives in 0001_runs.sql (F2 baked it
6
6
  -- in defensively so playground writes had a place to land before V5 shipped).
@@ -1,7 +1,7 @@
1
1
  -- 0005_stackbone_viewer.sql — read-only role for the Studio DB Explorer.
2
2
  -- Spec: docs/arquitectura/componentes/11-stackbone-studio.md §"DB Explorer".
3
3
  -- Task: docs/tasks/Studio V6 - DB Explorer read-only.md (V6#01).
4
- -- ADR: docs/arquitectura/decisiones/2026-05-03-stackbone-studio-datapath-y-scope-mvp.md §D6.
4
+ -- ADR: docs/arquitectura/decisiones/2026-05-30-stackbone-studio.md §D6.
5
5
  --
6
6
  -- The role is `NOLOGIN` because callers (apps/api, apps/cli emulator) reach it
7
7
  -- via `SET ROLE stackbone_viewer` on a session opened with the regular owner
@@ -2,7 +2,7 @@
2
2
  -- `stackbone dev` emulator (Studio V10).
3
3
  -- Spec: docs/arquitectura/componentes/11-stackbone-studio.md §"Secrets".
4
4
  -- Task: docs/tasks/Studio V10 - Secrets.md (V10#02).
5
- -- ADR: docs/arquitectura/decisiones/2026-05-03-stackbone-studio-datapath-y-scope-mvp.md
5
+ -- ADR: docs/arquitectura/decisiones/2026-05-30-stackbone-studio.md
6
6
  -- (decision V10#01 in the task itself: cloud secrets stay in
7
7
  -- `installation_secret` of the control plane; local emulator stores
8
8
  -- the same wire shape here so the Studio UI sees identical data.)
@@ -2,7 +2,7 @@
2
2
  -- payloads for Studio's Config dinámica screen (V11).
3
3
  -- Spec: docs/arquitectura/componentes/11-stackbone-studio.md §"Mapa de pantallas / C.Config dinámica".
4
4
  -- Task: docs/tasks/Studio V11 - Config dinámica.md (V11#01).
5
- -- ADR: docs/arquitectura/decisiones/2026-05-03-stackbone-studio-datapath-y-scope-mvp.md §D2/D7.
5
+ -- ADR: docs/arquitectura/decisiones/2026-05-30-stackbone-studio.md §D2/D7.
6
6
  --
7
7
  -- The current config is the row with the highest `version`. Older rows are
8
8
  -- kept for the rollback action and the lateral history list. Hot-reload is
@@ -1,6 +1,6 @@
1
1
  -- 0014_rag.sql — the canonical RAG store (collections / documents / chunks /
2
2
  -- jobs), moved into the platform schema.
3
- -- ADR: docs/adr/2026-06-05-rag-tables-in-platform-schema.md
3
+ -- ADR: docs/adr/2026-06-05-rag.md
4
4
  --
5
5
  -- Previously these four tables lived in `public` and were installed by the
6
6
  -- creator's own migrator (`stackbone db migrate add-rag`). They are a
@@ -0,0 +1,60 @@
1
+ -- 0015_queue_jobs_lease.sql — lease columns + a terminal NOTIFY on the agent's
2
+ -- `_queue_jobs` journal (P1 "async jobs").
3
+ --
4
+ -- The wrapper around a dispatched handler now leases the in-flight row so the
5
+ -- control-plane reaper can tell a still-running job from a crashed one:
6
+ --
7
+ -- heartbeat_at — touched by the harness roughly every 15s while a dispatched
8
+ -- handler is running. A stale heartbeat is what lets the
9
+ -- reaper DERIVE the `lost` state (never written as a column —
10
+ -- the status CHECK stays `done` / `failed`).
11
+ -- deadline_at — started_at + the effective handler timeout; the hard ceiling
12
+ -- past which the reaper treats the lease as expired.
13
+ -- run_id — correlation key with the `runs` timeline so Studio can join
14
+ -- a journal row to its run.
15
+ --
16
+ -- All three are nullable and added IF NOT EXISTS: the install saga may re-apply
17
+ -- migrations on a redeploy, and a job journaled before this migration simply
18
+ -- carries NULL leases.
19
+ --
20
+ -- A terminal NOTIFY fans out the NULL→terminal transition on
21
+ -- `stackbone_platform_events` so the dispatcher's tracker can resolve a job the
22
+ -- instant its journal row flips, instead of polling. It mirrors the trigger
23
+ -- style of `0001_runs.sql`.
24
+
25
+ ALTER TABLE stackbone_platform._queue_jobs
26
+ ADD COLUMN IF NOT EXISTS heartbeat_at timestamptz;
27
+ ALTER TABLE stackbone_platform._queue_jobs
28
+ ADD COLUMN IF NOT EXISTS deadline_at timestamptz;
29
+ ALTER TABLE stackbone_platform._queue_jobs
30
+ ADD COLUMN IF NOT EXISTS run_id text;
31
+
32
+ -- AFTER UPDATE trigger fans out into pg_notify('stackbone_platform_events', ...)
33
+ -- ONLY on the NULL→terminal status transition (the job just finished). The
34
+ -- dispatcher's tracker LISTENs on this channel and resolves the in-flight job
35
+ -- the moment its journal row flips to `done` / `failed`.
36
+ CREATE OR REPLACE FUNCTION stackbone_platform.notify_queue_job_event() RETURNS trigger
37
+ LANGUAGE plpgsql AS $fn$
38
+ DECLARE
39
+ payload jsonb;
40
+ BEGIN
41
+ IF (OLD.status IS NULL) AND (NEW.status IS NOT NULL) THEN
42
+ payload := jsonb_build_object(
43
+ 'type', 'queue_job.finished',
44
+ 'dispatch_id', NEW.dispatch_id,
45
+ 'run_id', NEW.run_id,
46
+ 'status', NEW.status
47
+ );
48
+ ELSE
49
+ RETURN NULL;
50
+ END IF;
51
+
52
+ PERFORM pg_notify('stackbone_platform_events', payload::text);
53
+ RETURN NULL;
54
+ END;
55
+ $fn$;
56
+
57
+ DROP TRIGGER IF EXISTS queue_jobs_notify_au ON stackbone_platform._queue_jobs;
58
+ CREATE TRIGGER queue_jobs_notify_au
59
+ AFTER UPDATE ON stackbone_platform._queue_jobs
60
+ FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_queue_job_event();
@@ -0,0 +1,15 @@
1
+ -- 0016_runs_job_trigger.sql — allow `trigger = 'job'` on stackbone_platform.runs.
2
+ --
3
+ -- Dispatched queue jobs (ADR 2026-05-30) are first-class runs, but until now a
4
+ -- queue dispatch only wrote the `_queue_jobs` journal — it never inserted a
5
+ -- `runs` row. Two consequences: the dispatched run was INVISIBLE in the Studio
6
+ -- runs timeline, and `ctx.progress` / `ctx.step` inside a job handler hit the
7
+ -- `run_steps.run_id REFERENCES runs(id)` FK with no parent row, so the progress
8
+ -- note was silently dropped. The wrapper now materialises a `trigger='job'` run
9
+ -- row when it journals the job's started lifecycle, which fixes both — but the
10
+ -- original `0001_runs.sql` CHECK predates the `job` trigger and would reject the
11
+ -- insert. Widen it to match `runTriggerSchema` (which already carries `job`).
12
+ ALTER TABLE stackbone_platform.runs DROP CONSTRAINT IF EXISTS runs_trigger_check;
13
+ ALTER TABLE stackbone_platform.runs
14
+ ADD CONSTRAINT runs_trigger_check
15
+ CHECK (trigger IN ('manual', 'webhook', 'cron', 'event', 'job'));
@@ -0,0 +1,25 @@
1
+ -- 0017_runs_workflow_chat_triggers.sql — widen the runs/run_steps CHECKs for
2
+ -- the eve durable runtime (feature 67).
3
+ --
4
+ -- The Workflow World → runs projection (F5) materialises each durable workflow
5
+ -- as a first-class run so it shows up in the Studio timeline; it tags those rows
6
+ -- with `trigger='workflow'` and records each workflow step as a generic
7
+ -- `run_steps.type='workflow_step'`. The chat recorder (F6) persists every eve
8
+ -- chat turn as a run tagged `trigger='chat'` (the chat session id rides the
9
+ -- free-form `runs.metadata` column — no new column is added). The earlier
10
+ -- CHECKs (0001 listed `manual/webhook/cron/event`, 0016 added `job`) predate all
11
+ -- three values and would reject the inserts. Widen `runs_trigger_check` to match
12
+ -- `runTriggerSchema` and `run_steps_type_check` to match the SDK `RunStepType`.
13
+ ALTER TABLE stackbone_platform.runs DROP CONSTRAINT IF EXISTS runs_trigger_check;
14
+ ALTER TABLE stackbone_platform.runs
15
+ ADD CONSTRAINT runs_trigger_check
16
+ CHECK (trigger IN ('manual', 'webhook', 'cron', 'event', 'job', 'workflow', 'chat'));
17
+
18
+ ALTER TABLE stackbone_platform.run_steps DROP CONSTRAINT IF EXISTS run_steps_type_check;
19
+ ALTER TABLE stackbone_platform.run_steps
20
+ ADD CONSTRAINT run_steps_type_check
21
+ CHECK (type IN (
22
+ 'agent', 'llm_call', 'db_query', 'http_fetch',
23
+ 'queue_publish', 'hitl_pause', 'tool_call',
24
+ 'rag_query', 'storage_op', 'workflow_step'
25
+ ));
@@ -0,0 +1,167 @@
1
+ -- 0018_run_steps_v2.sql — demolish the legacy run_steps surface and rebuild it
2
+ -- around the eve + Workflow World step telemetry (feature 68, Fase 2 / A4).
3
+ --
4
+ -- The old run_steps table (0001) modelled hand-written SDK steps (`ctx.step`
5
+ -- with types like `llm_call`/`db_query`/…), carried no token columns and was
6
+ -- driven by the now-removed span processor / `ctx.step` writers. Feature 67 only
7
+ -- widened its type CHECK to admit `workflow_step`; feature 68 drops the whole
8
+ -- table and recreates it v2.
9
+ --
10
+ -- v2 projects the durable Workflow World step timeline (one row per World step,
11
+ -- `type='workflow_step'`) plus, for the fast-follow, per-model-call rows
12
+ -- (`type='model_call'`). Each row carries the four eve token counters
13
+ -- (input/output/cache-read/cache-write); the total is derived at read time, not
14
+ -- stored. `world_step_id` is the World correlation/idempotency key the projector
15
+ -- upserts on. No cost column (D5 — tokens only, no $).
16
+ --
17
+ -- This migration also drops `runs.cost_estimated_usd` (the demolished cost
18
+ -- accumulator) and recreates the `approvals.requested_by_step_id` FK against
19
+ -- run_steps v2 (A6), plus the `notify_run_step_event` pg_notify trigger that the
20
+ -- table drop took down with it.
21
+
22
+ -- --------------------------------------------------------------------------
23
+ -- Demolition (A3): drop the legacy table, the cost column and the dependent FK.
24
+ -- --------------------------------------------------------------------------
25
+
26
+ -- The FK lives on `approvals` and references the old run_steps; drop it before
27
+ -- the table so the table drop does not have to CASCADE through it. Recreated
28
+ -- against v2 at the end of this migration.
29
+ ALTER TABLE stackbone_platform.approvals
30
+ DROP CONSTRAINT IF EXISTS approvals_requested_by_step_id_fkey;
31
+ ALTER TABLE stackbone_platform.approvals
32
+ DROP COLUMN IF EXISTS requested_by_step_id;
33
+
34
+ -- The AFTER INSERT/UPDATE trigger and its function are owned by the old table;
35
+ -- drop both so the recreate below is unambiguous.
36
+ DROP TRIGGER IF EXISTS run_steps_notify_aiu ON stackbone_platform.run_steps;
37
+ DROP FUNCTION IF EXISTS stackbone_platform.notify_run_step_event();
38
+
39
+ -- Self-referential parent FK and run FK go away with the table. CASCADE clears
40
+ -- the dependent self-FK in one shot.
41
+ DROP TABLE IF EXISTS stackbone_platform.run_steps CASCADE;
42
+
43
+ -- The per-run cost accumulator is demolished (no $ cost anywhere in this
44
+ -- feature).
45
+ ALTER TABLE stackbone_platform.runs
46
+ DROP COLUMN IF EXISTS cost_estimated_usd;
47
+
48
+ -- Per-turn token counters on `runs` (v1, A5). An eve agent CHAT turn = one
49
+ -- `runs` row (trigger='chat'); the chat recorder stamps the four eve
50
+ -- `data.usage` counters onto that row (the per-model-call granularity stays in
51
+ -- run_steps, used by workflows). All nullable; the total is `input + output`,
52
+ -- derived at read, never stored (D4). The runs timeline and feature 69's
53
+ -- session aggregate read these directly off `runs`.
54
+ ALTER TABLE stackbone_platform.runs
55
+ ADD COLUMN IF NOT EXISTS input_tokens integer,
56
+ ADD COLUMN IF NOT EXISTS output_tokens integer,
57
+ ADD COLUMN IF NOT EXISTS cache_read_tokens integer,
58
+ ADD COLUMN IF NOT EXISTS cache_write_tokens integer;
59
+
60
+ -- --------------------------------------------------------------------------
61
+ -- Recreate run_steps v2.
62
+ -- --------------------------------------------------------------------------
63
+
64
+ CREATE TABLE stackbone_platform.run_steps (
65
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
66
+ run_id uuid NOT NULL
67
+ REFERENCES stackbone_platform.runs(id) ON DELETE CASCADE,
68
+ -- Nullable self-FK. In practice null for flat "use step" workflows (the
69
+ -- World fork does not populate step-parent provenance for them), but kept
70
+ -- for the nested-step tree the projector derives from the event-log writer.
71
+ parent_step_id uuid REFERENCES stackbone_platform.run_steps(id) ON DELETE CASCADE,
72
+ type text NOT NULL
73
+ CHECK (type IN ('workflow_step', 'model_call')),
74
+ name text,
75
+ status text NOT NULL DEFAULT 'running'
76
+ CHECK (status IN (
77
+ 'pending', 'running', 'completed',
78
+ 'failed', 'cancelled'
79
+ )),
80
+ started_at timestamptz,
81
+ completed_at timestamptz,
82
+ duration_ms integer,
83
+ input jsonb,
84
+ output jsonb,
85
+ error jsonb,
86
+ -- The four eve token counters (data.usage on `step.completed`). All
87
+ -- nullable: workflow steps carry none (the World has no tokens) and any
88
+ -- counter may be absent. The total is `input + output`, derived at read
89
+ -- time — never persisted (D4).
90
+ input_tokens integer,
91
+ output_tokens integer,
92
+ cache_read_tokens integer,
93
+ cache_write_tokens integer,
94
+ -- World correlation / idempotency key (= the World `stepId`). The
95
+ -- event-driven projector upserts on this so a replayed/retried step updates
96
+ -- the same row instead of inserting a duplicate.
97
+ world_step_id text,
98
+ created_at timestamptz NOT NULL DEFAULT now()
99
+ );
100
+
101
+ CREATE INDEX IF NOT EXISTS run_steps_run_started_idx
102
+ ON stackbone_platform.run_steps (run_id, started_at);
103
+ CREATE INDEX IF NOT EXISTS run_steps_parent_idx
104
+ ON stackbone_platform.run_steps (parent_step_id);
105
+
106
+ -- Idempotent upsert target for the projector: one row per (run, World step).
107
+ -- Partial so rows without a World correlation (e.g. future synthetic rows)
108
+ -- never block one another on a NULL key.
109
+ CREATE UNIQUE INDEX IF NOT EXISTS run_steps_world_step_uq
110
+ ON stackbone_platform.run_steps (run_id, world_step_id)
111
+ WHERE world_step_id IS NOT NULL;
112
+
113
+ -- --------------------------------------------------------------------------
114
+ -- Recreate the pg_notify trigger on the same `stackbone_platform_events`
115
+ -- channel the SSE handler / emulator bus already LISTEN on. `created` fires on
116
+ -- INSERT; `finished` fires when completed_at transitions from null to a value.
117
+ -- --------------------------------------------------------------------------
118
+
119
+ CREATE OR REPLACE FUNCTION stackbone_platform.notify_run_step_event() RETURNS trigger
120
+ LANGUAGE plpgsql AS $fn$
121
+ DECLARE
122
+ payload jsonb;
123
+ BEGIN
124
+ IF TG_OP = 'INSERT' THEN
125
+ payload := jsonb_build_object(
126
+ 'type', 'run.step.created',
127
+ 'run_id', NEW.run_id,
128
+ 'step_id', NEW.id,
129
+ 'parent_step_id', NEW.parent_step_id,
130
+ 'step_type', NEW.type,
131
+ 'started_at', NEW.started_at
132
+ );
133
+ ELSIF TG_OP = 'UPDATE' THEN
134
+ IF (OLD.completed_at IS NULL) AND (NEW.completed_at IS NOT NULL) THEN
135
+ payload := jsonb_build_object(
136
+ 'type', 'run.step.finished',
137
+ 'run_id', NEW.run_id,
138
+ 'step_id', NEW.id,
139
+ 'status', NEW.status,
140
+ 'duration_ms', NEW.duration_ms
141
+ );
142
+ ELSE
143
+ RETURN NULL;
144
+ END IF;
145
+ ELSE
146
+ RETURN NULL;
147
+ END IF;
148
+
149
+ PERFORM pg_notify('stackbone_platform_events', payload::text);
150
+ RETURN NULL;
151
+ END;
152
+ $fn$;
153
+
154
+ DROP TRIGGER IF EXISTS run_steps_notify_aiu ON stackbone_platform.run_steps;
155
+ CREATE TRIGGER run_steps_notify_aiu
156
+ AFTER INSERT OR UPDATE ON stackbone_platform.run_steps
157
+ FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_run_step_event();
158
+
159
+ -- --------------------------------------------------------------------------
160
+ -- Recreate the approvals → run_steps FK against v2 (A6). `requestApproval`
161
+ -- stamps the id of the workflow step that asked for the approval so the inbox
162
+ -- can anchor it to the exact step.
163
+ -- --------------------------------------------------------------------------
164
+
165
+ ALTER TABLE stackbone_platform.approvals
166
+ ADD COLUMN IF NOT EXISTS requested_by_step_id uuid
167
+ REFERENCES stackbone_platform.run_steps(id) ON DELETE SET NULL;
@@ -0,0 +1,106 @@
1
+ -- 0019_sessions.sql — project eve agent conversations as a first-class entity
2
+ -- (feature 69, Fase 1).
3
+ --
4
+ -- An eve agent conversation is durable in the agent's Workflow World (one
5
+ -- long-lived `workflowEntry` run per session). The control plane never read the
6
+ -- World back, so a conversation had no row/endpoint/screen — only the loose
7
+ -- `runs.metadata.sessionId` jsonb the chat proxy stamped on each turn. This
8
+ -- migration adds the SESSION as a projected entity: a lean `sessions` table fed
9
+ -- by the World→sessions projector (Fase 2), plus a real `runs.session_id` column
10
+ -- so a session's turns join by an indexed key instead of the jsonb.
11
+ --
12
+ -- Lean by design (D6): `sessions` stores ONLY lifecycle (status + timestamps).
13
+ -- The read-time aggregates (turn_count, last_activity_at, preview, tokens) are a
14
+ -- GROUP BY over `runs` keyed on `session_id`; nothing here couples to feature
15
+ -- 68's token columns.
16
+
17
+ -- --------------------------------------------------------------------------
18
+ -- The sessions table. `id` is the deterministic UUIDv5 the projector derives
19
+ -- from the eve sessionId (= the `workflowEntry` World runId) under its own
20
+ -- namespace — always caller-provided, so no gen_random_uuid default. The
21
+ -- `status` CHECK is intentionally EXTENSIBLE (D5): v1 models only the three
22
+ -- lifecycle states; the later HITL-in-chat feature widens it with
23
+ -- `awaiting_input` via a non-breaking ALTER (the 0017 pattern), not a rewrite.
24
+ -- --------------------------------------------------------------------------
25
+ CREATE TABLE IF NOT EXISTS stackbone_platform.sessions (
26
+ id uuid PRIMARY KEY,
27
+ -- The eve agent this conversation targets. Nullable in local (the emulator
28
+ -- is single-tenant), required in cloud — mirrors `runs.agent_id`.
29
+ agent_id text,
30
+ status text NOT NULL DEFAULT 'open'
31
+ CHECK (status IN ('open', 'completed', 'failed')),
32
+ started_at timestamptz,
33
+ ended_at timestamptz,
34
+ created_at timestamptz NOT NULL DEFAULT now()
35
+ );
36
+
37
+ -- Cursor pagination over (created_at, id), the same keyset shape the runs list
38
+ -- uses, so the Studio Sessions list reuses one decoder.
39
+ CREATE INDEX IF NOT EXISTS sessions_created_at_id_idx
40
+ ON stackbone_platform.sessions (created_at DESC, id DESC);
41
+
42
+ CREATE INDEX IF NOT EXISTS sessions_agent_status_idx
43
+ ON stackbone_platform.sessions (agent_id, status, created_at DESC);
44
+
45
+ -- --------------------------------------------------------------------------
46
+ -- `runs.session_id` — the real join key (D4). Stamped by the chat recorder and
47
+ -- the projector with the SAME deterministic UUIDv5 as `sessions.id`. A LOGICAL
48
+ -- FK only (NOT enforced): a turn can persist before the session row exists
49
+ -- (eventual consistency; the projector and recorder race), so a real FK would
50
+ -- reject the turn. `runs.metadata.sessionId` stays for back-compat but the join
51
+ -- moves to this column. Indexed for the `WHERE session_id = :id` turn lookup.
52
+ -- --------------------------------------------------------------------------
53
+ ALTER TABLE stackbone_platform.runs
54
+ ADD COLUMN IF NOT EXISTS session_id uuid;
55
+
56
+ CREATE INDEX IF NOT EXISTS runs_session_id_idx
57
+ ON stackbone_platform.runs (session_id, created_at)
58
+ WHERE session_id IS NOT NULL;
59
+
60
+ -- --------------------------------------------------------------------------
61
+ -- pg_notify trigger on the SAME `stackbone_platform_events` channel the
62
+ -- runs/run_steps/approvals triggers use, so the cloud `StudioStreamService` (one
63
+ -- LISTEN) and the emulator's run-steps-bridge forward `session.*` with no new
64
+ -- infra. `created` fires on INSERT; `updated` fires when the lifecycle actually
65
+ -- moved (status or ended_at changed) so an idempotent re-upsert of the same
66
+ -- state does not spam the stream.
67
+ -- --------------------------------------------------------------------------
68
+ CREATE OR REPLACE FUNCTION stackbone_platform.notify_session_event() RETURNS trigger
69
+ LANGUAGE plpgsql AS $fn$
70
+ DECLARE
71
+ payload jsonb;
72
+ BEGIN
73
+ IF TG_OP = 'INSERT' THEN
74
+ payload := jsonb_build_object(
75
+ 'type', 'session.created',
76
+ 'session_id', NEW.id,
77
+ 'agent_id', NEW.agent_id,
78
+ 'status', NEW.status,
79
+ 'started_at', NEW.started_at,
80
+ 'created_at', NEW.created_at
81
+ );
82
+ ELSIF TG_OP = 'UPDATE' THEN
83
+ IF (OLD.status IS DISTINCT FROM NEW.status)
84
+ OR (OLD.ended_at IS DISTINCT FROM NEW.ended_at) THEN
85
+ payload := jsonb_build_object(
86
+ 'type', 'session.updated',
87
+ 'session_id', NEW.id,
88
+ 'status', NEW.status,
89
+ 'ended_at', NEW.ended_at
90
+ );
91
+ ELSE
92
+ RETURN NULL;
93
+ END IF;
94
+ ELSE
95
+ RETURN NULL;
96
+ END IF;
97
+
98
+ PERFORM pg_notify('stackbone_platform_events', payload::text);
99
+ RETURN NULL;
100
+ END;
101
+ $fn$;
102
+
103
+ DROP TRIGGER IF EXISTS sessions_notify_aiu ON stackbone_platform.sessions;
104
+ CREATE TRIGGER sessions_notify_aiu
105
+ AFTER INSERT OR UPDATE ON stackbone_platform.sessions
106
+ FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_session_event();
@@ -0,0 +1,23 @@
1
+ -- 0020_runs_entity_name.sql — record which agent/workflow a run belongs to.
2
+ --
3
+ -- The Studio Runs screen lists a run's id, trigger and status but never the
4
+ -- NAME of the agent or workflow that produced it, so a workspace running several
5
+ -- eve agents + workflows could not tell whose run a row was, nor filter by it.
6
+ -- The name is known at run-creation time (the chat proxy has the agent `:name`;
7
+ -- the workflow start has the workflow `:name`) but was dropped.
8
+ --
9
+ -- `entity_name` denormalises that name onto the run so the list renders it as a
10
+ -- column and filters by it without a join. It is the agent name for `chat` runs
11
+ -- and the workflow name for `workflow` runs; combined with the existing
12
+ -- `trigger` the UI knows the kind. Nullable: classic/manual cloud runs (eve is
13
+ -- dev-only) leave it NULL. Distinct from `agent_id`, which in cloud is the agent
14
+ -- TEMPLATE id (a stable identifier, not a display name) and is null for workflows.
15
+
16
+ ALTER TABLE stackbone_platform.runs
17
+ ADD COLUMN IF NOT EXISTS entity_name text;
18
+
19
+ -- Backs both the exact-match filter (`WHERE entity_name = $1`) and the distinct
20
+ -- list that populates the Runs screen's name dropdown.
21
+ CREATE INDEX IF NOT EXISTS runs_entity_name_idx
22
+ ON stackbone_platform.runs (entity_name)
23
+ WHERE entity_name IS NOT NULL;
@@ -0,0 +1,67 @@
1
+ -- 0021_trigger_subscriptions.sql — the inbound side of Stackbone Connect
2
+ -- (feature 89): an operator links a connector polling TRIGGER to a World
3
+ -- workflow, and a polling intake engine starts that workflow for each new item.
4
+ --
5
+ -- This is the emulator's OWN inbound state — deliberately NOT the classic
6
+ -- `automation*` tables (which live in cloud and feed the deprecated `/invoke`
7
+ -- pipeline). The model mirrors that pipeline's SHAPE (config + cursor state +
8
+ -- a delivery journal) without coupling to it: the classic engine stays running
9
+ -- and untouched; its retirement is tied to the World runtime's port to prod.
10
+ --
11
+ -- `trigger_subscriptions` holds the operator-owned link plus its polling state
12
+ -- (the opaque cursor + last-poll marker, owned by the platform; the
13
+ -- remote-subscription id is reserved for a future webhook/push source).
14
+ -- `trigger_deliveries` is the at-least-once dedup journal: one row per
15
+ -- (subscription, provider item) so two overlapping ticks or a mid-restart
16
+ -- re-poll never start the same item's workflow twice.
17
+
18
+ CREATE TABLE IF NOT EXISTS stackbone_platform.trigger_subscriptions (
19
+ id uuid PRIMARY KEY,
20
+ -- Integration id — the SAME id resolves the credential (broker) and the
21
+ -- trigger capability (connector kit). e.g. 'gmail'.
22
+ connector text NOT NULL,
23
+ -- Capability trigger id, e.g. 'message-received'. Not the reserved word
24
+ -- `trigger`, so the column is `trigger_id`.
25
+ trigger_id text NOT NULL,
26
+ -- Which broker grant to poll with: 'app' | 'user:<issuer>:<id>'.
27
+ principal_key text NOT NULL,
28
+ -- The World workflow this link starts.
29
+ workflow_name text NOT NULL,
30
+ -- Optional JSONata event→input mapping; identity when null.
31
+ mapping text,
32
+ enabled boolean NOT NULL DEFAULT true,
33
+ -- Opaque polling cursor (Gmail historyId, etc), platform-owned. Advanced
34
+ -- atomically at the END of a tick.
35
+ poll_cursor jsonb,
36
+ -- Reserved for a future push source (a provider-side subscription id); the
37
+ -- polling path never reads it.
38
+ remote_subscription_id text,
39
+ last_polled_at timestamptz,
40
+ created_at timestamptz NOT NULL DEFAULT now(),
41
+ updated_at timestamptz NOT NULL DEFAULT now()
42
+ );
43
+
44
+ -- The reconcile sweep loads ACTIVE links to arm one repeatable each.
45
+ CREATE INDEX IF NOT EXISTS trigger_subscriptions_enabled_idx
46
+ ON stackbone_platform.trigger_subscriptions (enabled, created_at)
47
+ WHERE enabled;
48
+
49
+ CREATE TABLE IF NOT EXISTS stackbone_platform.trigger_deliveries (
50
+ id uuid PRIMARY KEY,
51
+ subscription_id uuid NOT NULL,
52
+ -- The connector's stable provider item id (Gmail messageId) — the dedup key.
53
+ provider_item_id text NOT NULL,
54
+ status text NOT NULL DEFAULT 'claimed'
55
+ CHECK (status IN ('claimed', 'started', 'rejected')),
56
+ -- The projected World run id once the workflow started.
57
+ run_id text,
58
+ -- Validation/mapping issues when the delivery was rejected.
59
+ issues jsonb,
60
+ created_at timestamptz NOT NULL DEFAULT now(),
61
+ updated_at timestamptz NOT NULL DEFAULT now()
62
+ );
63
+
64
+ -- The at-least-once dedup guarantee: claiming a delivery is an INSERT that
65
+ -- collapses on this unique pair, so a re-polled item is a no-op, not a re-run.
66
+ CREATE UNIQUE INDEX IF NOT EXISTS trigger_deliveries_dedup_idx
67
+ ON stackbone_platform.trigger_deliveries (subscription_id, provider_item_id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackbone/cli",
3
- "version": "0.1.0-alpha.8",
3
+ "version": "0.1.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,21 +18,24 @@
18
18
  "@clack/prompts": "^1.2.0",
19
19
  "@hono/node-server": "^2.0.0",
20
20
  "ajv": "^8.20.0",
21
+ "ajv-formats": "^3.0.1",
21
22
  "bullmq": "^5.76.6",
22
23
  "citty": "^0.2.2",
24
+ "cron-parser": "^4.9.0",
23
25
  "drizzle-kit": "^0.31.10",
24
26
  "drizzle-orm": "^0.45.2",
25
27
  "esbuild": "^0.27.0",
26
28
  "hono": "^4.12.15",
27
29
  "libsodium-wrappers": "^0.8.4",
28
30
  "open": "^11.0.0",
29
- "openai": "^4.104.0",
31
+ "openai": "^6.42.0",
30
32
  "pg": "^8.20.0",
31
33
  "pg-query-emscripten": "^5.1.0",
32
34
  "pino": "^10.3.1",
33
35
  "pino-pretty": "^13.1.3",
34
36
  "postgres": "^3.4.9",
35
- "unpdf": "^0.12.1",
37
+ "semver": "^7.7.4",
38
+ "tar": "^7.5.13",
36
39
  "yaml": "^2.8.3",
37
40
  "zod": "^4.3.6"
38
41
  }