@stackbone/cli 0.1.0-alpha.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.
@@ -0,0 +1,150 @@
1
+ -- 0001_runs.sql — schema and tables for the run-level surface of Studio.
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.
4
+
5
+ CREATE SCHEMA IF NOT EXISTS stackbone_platform;
6
+
7
+ CREATE TABLE IF NOT EXISTS stackbone_platform.runs (
8
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
9
+ -- Nullable in local (single-agent emulator), required in cloud.
10
+ agent_id text,
11
+ workspace_id text,
12
+ trigger text NOT NULL DEFAULT 'manual'
13
+ CHECK (trigger IN ('manual', 'webhook', 'cron', 'event')),
14
+ status text NOT NULL DEFAULT 'running'
15
+ CHECK (status IN ('running', 'done', 'failed', 'interrupted')),
16
+ trace_id text,
17
+ is_playground boolean NOT NULL DEFAULT false,
18
+ input jsonb,
19
+ output jsonb,
20
+ error jsonb,
21
+ cost_estimated_usd numeric,
22
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
23
+ created_at timestamptz NOT NULL DEFAULT now(),
24
+ started_at timestamptz,
25
+ finished_at timestamptz,
26
+ duration_ms integer
27
+ );
28
+
29
+ CREATE INDEX IF NOT EXISTS runs_workspace_started_idx
30
+ ON stackbone_platform.runs (workspace_id, started_at DESC NULLS LAST);
31
+
32
+ -- Drives the cursor pagination from §5 of the protocol spec: order by
33
+ -- created_at desc with id as the tie-break for runs created within the same
34
+ -- millisecond.
35
+ CREATE INDEX IF NOT EXISTS runs_created_at_id_idx
36
+ ON stackbone_platform.runs (created_at DESC, id DESC);
37
+
38
+ CREATE TABLE IF NOT EXISTS stackbone_platform.run_steps (
39
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
40
+ run_id uuid NOT NULL
41
+ REFERENCES stackbone_platform.runs(id) ON DELETE CASCADE,
42
+ parent_step_id uuid REFERENCES stackbone_platform.run_steps(id) ON DELETE CASCADE,
43
+ type text NOT NULL
44
+ CHECK (type IN (
45
+ 'agent', 'llm_call', 'db_query', 'http_fetch',
46
+ 'queue_publish', 'hitl_pause', 'tool_call',
47
+ 'rag_query', 'storage_op'
48
+ )),
49
+ name text,
50
+ status text NOT NULL DEFAULT 'running'
51
+ CHECK (status IN ('ok', 'error', 'running')),
52
+ payload jsonb,
53
+ error jsonb,
54
+ started_at timestamptz NOT NULL DEFAULT now(),
55
+ finished_at timestamptz,
56
+ duration_ms integer
57
+ );
58
+
59
+ CREATE INDEX IF NOT EXISTS run_steps_run_started_idx
60
+ ON stackbone_platform.run_steps (run_id, started_at);
61
+ CREATE INDEX IF NOT EXISTS run_steps_parent_idx
62
+ ON stackbone_platform.run_steps (parent_step_id);
63
+
64
+ -- AFTER INSERT/UPDATE triggers fan-out into pg_notify('stackbone_platform_events',
65
+ -- ...). The `apps/api` SSE handler (D4) and the local emulator's bus (V2/V3)
66
+ -- LISTEN on this channel and forward each frame as the SSE payload defined in
67
+ -- the protocol spec §10.2.
68
+ CREATE OR REPLACE FUNCTION stackbone_platform.notify_run_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', 'run.created',
76
+ 'run_id', NEW.id,
77
+ 'trigger', NEW.trigger,
78
+ 'created_at', NEW.created_at
79
+ );
80
+ ELSIF TG_OP = 'UPDATE' THEN
81
+ IF (OLD.started_at IS NULL) AND (NEW.started_at IS NOT NULL) THEN
82
+ payload := jsonb_build_object(
83
+ 'type', 'run.started',
84
+ 'run_id', NEW.id,
85
+ 'started_at', NEW.started_at
86
+ );
87
+ ELSIF (OLD.finished_at IS NULL) AND (NEW.finished_at IS NOT NULL) THEN
88
+ payload := jsonb_build_object(
89
+ 'type', 'run.finished',
90
+ 'run_id', NEW.id,
91
+ 'status', NEW.status,
92
+ 'finished_at', NEW.finished_at,
93
+ 'duration_ms', NEW.duration_ms
94
+ );
95
+ ELSE
96
+ RETURN NULL;
97
+ END IF;
98
+ ELSE
99
+ RETURN NULL;
100
+ END IF;
101
+
102
+ PERFORM pg_notify('stackbone_platform_events', payload::text);
103
+ RETURN NULL;
104
+ END;
105
+ $fn$;
106
+
107
+ DROP TRIGGER IF EXISTS runs_notify_aiu ON stackbone_platform.runs;
108
+ CREATE TRIGGER runs_notify_aiu
109
+ AFTER INSERT OR UPDATE ON stackbone_platform.runs
110
+ FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_run_event();
111
+
112
+ CREATE OR REPLACE FUNCTION stackbone_platform.notify_run_step_event() RETURNS trigger
113
+ LANGUAGE plpgsql AS $fn$
114
+ DECLARE
115
+ payload jsonb;
116
+ BEGIN
117
+ IF TG_OP = 'INSERT' THEN
118
+ payload := jsonb_build_object(
119
+ 'type', 'run.step.created',
120
+ 'run_id', NEW.run_id,
121
+ 'step_id', NEW.id,
122
+ 'parent_step_id', NEW.parent_step_id,
123
+ 'step_type', NEW.type,
124
+ 'started_at', NEW.started_at
125
+ );
126
+ ELSIF TG_OP = 'UPDATE' THEN
127
+ IF (OLD.finished_at IS NULL) AND (NEW.finished_at IS NOT NULL) THEN
128
+ payload := jsonb_build_object(
129
+ 'type', 'run.step.finished',
130
+ 'run_id', NEW.run_id,
131
+ 'step_id', NEW.id,
132
+ 'status', NEW.status,
133
+ 'duration_ms', NEW.duration_ms
134
+ );
135
+ ELSE
136
+ RETURN NULL;
137
+ END IF;
138
+ ELSE
139
+ RETURN NULL;
140
+ END IF;
141
+
142
+ PERFORM pg_notify('stackbone_platform_events', payload::text);
143
+ RETURN NULL;
144
+ END;
145
+ $fn$;
146
+
147
+ DROP TRIGGER IF EXISTS run_steps_notify_aiu ON stackbone_platform.run_steps;
148
+ CREATE TRIGGER run_steps_notify_aiu
149
+ AFTER INSERT OR UPDATE ON stackbone_platform.run_steps
150
+ FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_run_step_event();
@@ -0,0 +1,110 @@
1
+ -- 0002_approvals.sql — HITL approvals queue and audit trail.
2
+ -- Spec: docs/arquitectura/specs/stackbone-agent-protocol-v1.md §6.6/§6.7.
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.
5
+
6
+ CREATE TABLE IF NOT EXISTS stackbone_platform.approvals (
7
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
8
+ -- Nullable in local (single-agent emulator), required in cloud.
9
+ agent_id text,
10
+ workspace_id text,
11
+ topic text NOT NULL,
12
+ status text NOT NULL DEFAULT 'pending'
13
+ CHECK (status IN (
14
+ 'pending', 'approved', 'rejected',
15
+ 'timed_out', 'cancelled'
16
+ )),
17
+ payload jsonb NOT NULL DEFAULT '{}'::jsonb,
18
+ schema jsonb,
19
+ run_id uuid REFERENCES stackbone_platform.runs(id) ON DELETE SET NULL,
20
+ requested_by_step_id uuid REFERENCES stackbone_platform.run_steps(id) ON DELETE SET NULL,
21
+ callback_url text,
22
+ idempotency_key text,
23
+ fallback text CHECK (fallback IS NULL OR fallback IN (
24
+ 'approve', 'reject', 'timeout'
25
+ )),
26
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
27
+ created_at timestamptz NOT NULL DEFAULT now(),
28
+ timeout_at timestamptz,
29
+ decided_at timestamptz
30
+ );
31
+
32
+ -- Idempotency: same (workspace, topic, idempotency_key) tuple cannot create
33
+ -- two approvals. Partial index keeps a NULL idempotency_key from blocking
34
+ -- everything; the 99% of HITL pauses that don't carry a key still work.
35
+ CREATE UNIQUE INDEX IF NOT EXISTS approvals_idempotency_uq
36
+ ON stackbone_platform.approvals (workspace_id, topic, idempotency_key)
37
+ WHERE idempotency_key IS NOT NULL;
38
+
39
+ CREATE INDEX IF NOT EXISTS approvals_workspace_status_idx
40
+ ON stackbone_platform.approvals (workspace_id, status, created_at DESC);
41
+
42
+ CREATE INDEX IF NOT EXISTS approvals_run_idx
43
+ ON stackbone_platform.approvals (run_id);
44
+
45
+ CREATE INDEX IF NOT EXISTS approvals_pending_timeout_idx
46
+ ON stackbone_platform.approvals (timeout_at)
47
+ WHERE status = 'pending' AND timeout_at IS NOT NULL;
48
+
49
+ CREATE TABLE IF NOT EXISTS stackbone_platform.approval_decisions (
50
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
51
+ approval_id uuid NOT NULL
52
+ REFERENCES stackbone_platform.approvals(id) ON DELETE CASCADE,
53
+ actor_user_id text,
54
+ actor_email text,
55
+ decision text NOT NULL CHECK (decision IN ('approve', 'reject')),
56
+ edited_payload jsonb,
57
+ comment text,
58
+ decided_at timestamptz NOT NULL DEFAULT now()
59
+ );
60
+
61
+ CREATE INDEX IF NOT EXISTS approval_decisions_approval_idx
62
+ ON stackbone_platform.approval_decisions (approval_id, decided_at);
63
+
64
+ -- Triggers fire on the same `stackbone_platform_events` channel as the runs
65
+ -- triggers, so the SSE handler in `apps/api` only LISTENs once.
66
+ CREATE OR REPLACE FUNCTION stackbone_platform.notify_approval_event() RETURNS trigger
67
+ LANGUAGE plpgsql AS $fn$
68
+ DECLARE
69
+ payload jsonb;
70
+ BEGIN
71
+ IF TG_OP = 'INSERT' THEN
72
+ payload := jsonb_build_object(
73
+ 'type', 'approval.created',
74
+ 'approval_id', NEW.id,
75
+ 'run_id', NEW.run_id,
76
+ 'topic', NEW.topic,
77
+ 'created_at', NEW.created_at,
78
+ 'timeout_at', NEW.timeout_at
79
+ );
80
+ ELSIF TG_OP = 'UPDATE' THEN
81
+ IF (OLD.decided_at IS NULL) AND (NEW.decided_at IS NOT NULL)
82
+ AND NEW.status IN ('approved', 'rejected') THEN
83
+ payload := jsonb_build_object(
84
+ 'type', 'approval.decided',
85
+ 'approval_id', NEW.id,
86
+ 'status', NEW.status,
87
+ 'decided_at', NEW.decided_at
88
+ );
89
+ ELSIF (OLD.status = 'pending') AND (NEW.status = 'timed_out') THEN
90
+ payload := jsonb_build_object(
91
+ 'type', 'approval.timed_out',
92
+ 'approval_id', NEW.id,
93
+ 'fallback', NEW.fallback
94
+ );
95
+ ELSE
96
+ RETURN NULL;
97
+ END IF;
98
+ ELSE
99
+ RETURN NULL;
100
+ END IF;
101
+
102
+ PERFORM pg_notify('stackbone_platform_events', payload::text);
103
+ RETURN NULL;
104
+ END;
105
+ $fn$;
106
+
107
+ DROP TRIGGER IF EXISTS approvals_notify_aiu ON stackbone_platform.approvals;
108
+ CREATE TRIGGER approvals_notify_aiu
109
+ AFTER INSERT OR UPDATE ON stackbone_platform.approvals
110
+ FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_approval_event();
@@ -0,0 +1,15 @@
1
+ -- 0003_runs_is_playground_index.sql — billing-friendly partial index over runs.
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.
4
+ --
5
+ -- The `is_playground` column itself already lives in 0001_runs.sql (F2 baked it
6
+ -- in defensively so playground writes had a place to land before V5 shipped).
7
+ -- This migration adds the partial index that the billing pipeline and the
8
+ -- workspace-level aggregations (V1+ Workspace Health, LLM Usage) need to scan
9
+ -- only the runs that count toward billing — i.e. `is_playground = false`.
10
+ -- Keeping it partial keeps the index small even when a workspace stress-tests
11
+ -- the agent from the Playground.
12
+
13
+ CREATE INDEX IF NOT EXISTS runs_billable_workspace_started_idx
14
+ ON stackbone_platform.runs (workspace_id, started_at DESC NULLS LAST)
15
+ WHERE is_playground = false;
@@ -0,0 +1,40 @@
1
+ -- 0004_playground_fixtures.sql — saved Playground inputs for V5 (Studio).
2
+ -- Spec: docs/arquitectura/specs/stackbone-agent-protocol-v1.md §6.10.
3
+ -- Task: docs/tasks/Studio V5 - Playground e Invoke.md (V5#03).
4
+ --
5
+ -- The table holds JSON inputs the creator pinned for repeated invocations
6
+ -- from the Studio Playground. Cloud-only — local fixtures live in
7
+ -- `~/.stackbone/dev/fixtures/<agentId>.jsonl` for filesystem parity with the
8
+ -- rest of the local datapath. Workspace-shared fixtures and version history
9
+ -- land in V1+ (today MVP only stores the owner's saved inputs).
10
+ --
11
+ -- The `(agent_id, name)` unique constraint backs the `409 fixture_name_conflict`
12
+ -- response defined in the protocol spec — the partial index keeps the
13
+ -- agentless rows (single-agent local emulator parity) from blocking each
14
+ -- other on `name`.
15
+
16
+ CREATE TABLE IF NOT EXISTS stackbone_platform.playground_fixtures (
17
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
18
+ -- Nullable in local (single-agent emulator). In cloud the saga sets it
19
+ -- to the install's agent_id so different agents in the same workspace
20
+ -- can reuse fixture names.
21
+ agent_id text,
22
+ name text NOT NULL,
23
+ input_json jsonb NOT NULL,
24
+ description text,
25
+ -- The owner's better-auth user id at save time. Kept text to match the
26
+ -- existing `workspaces.owner_user_id` column type.
27
+ created_by text,
28
+ created_at timestamptz NOT NULL DEFAULT now()
29
+ );
30
+
31
+ CREATE UNIQUE INDEX IF NOT EXISTS playground_fixtures_agent_name_uq
32
+ ON stackbone_platform.playground_fixtures (agent_id, name)
33
+ WHERE agent_id IS NOT NULL;
34
+
35
+ CREATE UNIQUE INDEX IF NOT EXISTS playground_fixtures_name_uq
36
+ ON stackbone_platform.playground_fixtures (name)
37
+ WHERE agent_id IS NULL;
38
+
39
+ CREATE INDEX IF NOT EXISTS playground_fixtures_created_at_idx
40
+ ON stackbone_platform.playground_fixtures (created_at DESC, id DESC);
@@ -0,0 +1,47 @@
1
+ -- 0005_stackbone_viewer.sql — read-only role for the Studio DB Explorer.
2
+ -- Spec: docs/arquitectura/componentes/11-stackbone-studio.md §"DB Explorer".
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.
5
+ --
6
+ -- The role is `NOLOGIN` because callers (apps/api, apps/cli emulator) reach it
7
+ -- via `SET ROLE stackbone_viewer` on a session opened with the regular owner
8
+ -- credentials, not by logging in as `stackbone_viewer` directly. This keeps the
9
+ -- Neon HTTP driver one-shot model (D3) untouched: the connection string still
10
+ -- belongs to the owner, the privilege drop happens per-statement.
11
+ --
12
+ -- DEFAULT PRIVILEGES are scoped to the current migration runner (`current_user`)
13
+ -- so any table the runner creates afterwards in `public`, `stackbone_platform`,
14
+ -- and the future `pgmq` / `rag_*` schemas auto-grants SELECT to the viewer
15
+ -- without re-issuing GRANTs from a follow-up migration.
16
+
17
+ DO $$
18
+ BEGIN
19
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'stackbone_viewer') THEN
20
+ CREATE ROLE stackbone_viewer NOLOGIN;
21
+ END IF;
22
+ END
23
+ $$;
24
+
25
+ -- Existing schemas at the time this migration runs.
26
+ GRANT USAGE ON SCHEMA public TO stackbone_viewer;
27
+ GRANT USAGE ON SCHEMA stackbone_platform TO stackbone_viewer;
28
+
29
+ GRANT SELECT ON ALL TABLES IN SCHEMA public TO stackbone_viewer;
30
+ GRANT SELECT ON ALL TABLES IN SCHEMA stackbone_platform TO stackbone_viewer;
31
+ GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO stackbone_viewer;
32
+ GRANT SELECT ON ALL SEQUENCES IN SCHEMA stackbone_platform TO stackbone_viewer;
33
+
34
+ -- Default privileges so any *future* table created by the migration runner in
35
+ -- these schemas auto-grants SELECT to `stackbone_viewer`. The Studio sidebar
36
+ -- promises visibility into `pgmq.*`, `_storage_objects`, `rag_*` and the
37
+ -- creator's own schemas — without these defaults, every new table created by
38
+ -- a downstream migration (or by the agent itself via Drizzle) would silently
39
+ -- be invisible to the DB Explorer.
40
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public
41
+ GRANT SELECT ON TABLES TO stackbone_viewer;
42
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public
43
+ GRANT SELECT ON SEQUENCES TO stackbone_viewer;
44
+ ALTER DEFAULT PRIVILEGES IN SCHEMA stackbone_platform
45
+ GRANT SELECT ON TABLES TO stackbone_viewer;
46
+ ALTER DEFAULT PRIVILEGES IN SCHEMA stackbone_platform
47
+ GRANT SELECT ON SEQUENCES TO stackbone_viewer;
@@ -0,0 +1,37 @@
1
+ -- 0006_secrets.sql — local mirror of `installation_secret` for the
2
+ -- `stackbone dev` emulator (Studio V10).
3
+ -- Spec: docs/arquitectura/componentes/11-stackbone-studio.md §"Secrets".
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
6
+ -- (decision V10#01 in the task itself: cloud secrets stay in
7
+ -- `installation_secret` of the control plane; local emulator stores
8
+ -- the same wire shape here so the Studio UI sees identical data.)
9
+ --
10
+ -- The emulator runs without a control plane. To keep the Studio Secrets
11
+ -- screen wire-compatible (one shape from `@stackbone/validators`) we mirror
12
+ -- the cloud table inside `stackbone_platform.secrets`. Encryption uses the
13
+ -- documented `dev-only-encryption-key` (see STUDIO_LOCAL_DEV_ENCRYPTION_KEY
14
+ -- in libs/validators) and never leaves the developer machine.
15
+ --
16
+ -- `is_system` carries the same semantics as in cloud — the saga injects
17
+ -- nothing here so the column stays at its default `false`, but we keep it
18
+ -- for parity in case future system-level injectors land in the emulator.
19
+
20
+ CREATE TABLE IF NOT EXISTS stackbone_platform.secrets (
21
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
22
+ name text NOT NULL,
23
+ version text NOT NULL DEFAULT 'v1' CHECK (version IN ('v1')),
24
+ nonce bytea NOT NULL,
25
+ ciphertext bytea NOT NULL,
26
+ is_system boolean NOT NULL DEFAULT false,
27
+ description text,
28
+ -- created_by_email is a free-text field in local: there is no `user`
29
+ -- table in the emulator's Postgres. The cloud surface backfills the
30
+ -- equivalent via better-auth user lookup.
31
+ created_by_email text,
32
+ created_at timestamptz NOT NULL DEFAULT now(),
33
+ updated_at timestamptz NOT NULL DEFAULT now()
34
+ );
35
+
36
+ CREATE UNIQUE INDEX IF NOT EXISTS secrets_name_idx
37
+ ON stackbone_platform.secrets (name);
@@ -0,0 +1,57 @@
1
+ -- 0007_agent_config_versions.sql — versioned history of `AGENT_CONFIG`
2
+ -- payloads for Studio's Config dinámica screen (V11).
3
+ -- Spec: docs/arquitectura/componentes/11-stackbone-studio.md §"Mapa de pantallas / C.Config dinámica".
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.
6
+ --
7
+ -- The current config is the row with the highest `version`. Older rows are
8
+ -- kept for the rollback action and the lateral history list. Hot-reload is
9
+ -- driven off `pg_notify('stackbone_platform_events', …)` from an INSERT trigger
10
+ -- — the local emulator orchestrator (V11#04) and `apps/api` SSE handlers
11
+ -- LISTEN on the same channel as the rest of the platform events.
12
+ --
13
+ -- `created_by_email` carries free text on the local emulator (no `user`
14
+ -- table inside the agent's Postgres) and the cloud control plane backfills
15
+ -- it via better-auth lookup. This mirrors `stackbone_platform.secrets` so the
16
+ -- audit shape stays uniform across vault screens.
17
+
18
+ CREATE TABLE IF NOT EXISTS stackbone_platform.agent_config_versions (
19
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
20
+ -- Monotonically increasing per install. The application layer assigns
21
+ -- `MAX(version) + 1` inside the same transaction as the INSERT so the
22
+ -- column doubles as a stable cursor for the lateral history list.
23
+ version integer NOT NULL CHECK (version >= 1),
24
+ value jsonb NOT NULL,
25
+ -- When a row originates from a rollback we record the source version
26
+ -- so the UI can render "rolled back from v7" without joining twice.
27
+ rolled_back_from integer,
28
+ created_by_email text,
29
+ created_at timestamptz NOT NULL DEFAULT now()
30
+ );
31
+
32
+ CREATE UNIQUE INDEX IF NOT EXISTS agent_config_versions_version_idx
33
+ ON stackbone_platform.agent_config_versions (version);
34
+
35
+ CREATE INDEX IF NOT EXISTS agent_config_versions_created_at_idx
36
+ ON stackbone_platform.agent_config_versions (created_at DESC, version DESC);
37
+
38
+ CREATE OR REPLACE FUNCTION stackbone_platform.notify_agent_config_event() RETURNS trigger
39
+ LANGUAGE plpgsql AS $fn$
40
+ DECLARE
41
+ payload jsonb;
42
+ BEGIN
43
+ payload := jsonb_build_object(
44
+ 'type', 'agent_config.changed',
45
+ 'version', NEW.version,
46
+ 'rolled_back_from', NEW.rolled_back_from,
47
+ 'created_at', NEW.created_at
48
+ );
49
+ PERFORM pg_notify('stackbone_platform_events', payload::text);
50
+ RETURN NULL;
51
+ END;
52
+ $fn$;
53
+
54
+ DROP TRIGGER IF EXISTS agent_config_versions_notify_aiu ON stackbone_platform.agent_config_versions;
55
+ CREATE TRIGGER agent_config_versions_notify_aiu
56
+ AFTER INSERT ON stackbone_platform.agent_config_versions
57
+ FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_agent_config_event();
@@ -0,0 +1,19 @@
1
+ -- 0008_approvals_idempotency_nulls_not_distinct.sql — fix the partial unique
2
+ -- index on `approvals` so the `ON CONFLICT (workspace_id, topic,
3
+ -- idempotency_key)` upsert in `apps/cli` reuses an existing row when
4
+ -- `workspace_id IS NULL` (the local emulator never populates it).
5
+ --
6
+ -- Postgres treats NULL as distinct in unique indexes by default, so two
7
+ -- inserts with the same (NULL, topic, idempotencyKey) tuple were both
8
+ -- accepted instead of round-tripping the original approvalId. `NULLS NOT
9
+ -- DISTINCT` (Postgres 15+) collapses NULLs into a single comparable value,
10
+ -- which preserves the cloud semantics (workspace_id NOT NULL) and unblocks
11
+ -- the local emulator path.
12
+ --
13
+ -- Spec: docs/arquitectura/specs/stackbone-agent-protocol-v1.md §6.6.
14
+
15
+ DROP INDEX IF EXISTS stackbone_platform.approvals_idempotency_uq;
16
+
17
+ CREATE UNIQUE INDEX IF NOT EXISTS approvals_idempotency_uq
18
+ ON stackbone_platform.approvals (workspace_id, topic, idempotency_key) NULLS NOT DISTINCT
19
+ WHERE idempotency_key IS NOT NULL;
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@stackbone/cli",
3
+ "version": "0.1.0-alpha.0",
4
+ "license": "UNLICENSED",
5
+ "type": "module",
6
+ "bin": {
7
+ "stackbone": "./main.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=20.18.0"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "dependencies": {
16
+ "@aws-sdk/client-s3": "^3.1038.0",
17
+ "@aws-sdk/s3-request-presigner": "^3.1038.0",
18
+ "@clack/prompts": "^1.2.0",
19
+ "@hono/node-server": "^2.0.0",
20
+ "ajv": "^8.20.0",
21
+ "citty": "^0.2.2",
22
+ "drizzle-kit": "^0.31.10",
23
+ "drizzle-orm": "^0.45.2",
24
+ "hono": "^4.12.15",
25
+ "open": "^11.0.0",
26
+ "pg": "^8.20.0",
27
+ "pg-query-emscripten": "^5.1.0",
28
+ "pino": "^10.3.1",
29
+ "pino-pretty": "^13.1.3",
30
+ "postgres": "^3.4.9",
31
+ "yaml": "^2.8.3",
32
+ "zod": "^4.3.6"
33
+ }
34
+ }
Binary file