File size: 4,199 Bytes
bd7e398
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
-- KanBot v0 schema — column names quoted.
-- `column` is a reserved keyword in PostgreSQL, so every reference to the
-- cards.column / trigger NEW.column / OLD.column must be double-quoted.
-- This file is the version used by the HF Space Docker image.
-- Idempotent: IF NOT EXISTS / DROP TRIGGER IF EXISTS / DO $$ ... EXCEPTION.

BEGIN;

-- --------------------------------------------------------------------------- --
-- enums mirror core/contract.py exactly. Keep them in sync.
-- --------------------------------------------------------------------------- --
DO $$ BEGIN
  CREATE TYPE kb_column AS ENUM
    ('backlog','ready','in-flight','review','blocked','done');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;

DO $$ BEGIN
  CREATE TYPE kb_role AS ENUM ('planner','executor','critic','human');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;

-- --------------------------------------------------------------------------- --
-- cards = the atoms. thread holds inter-agent comms as an ordered JSONB array.
-- --------------------------------------------------------------------------- --
CREATE TABLE IF NOT EXISTS cards (
  id                   TEXT PRIMARY KEY,             -- CARD-42
  title                TEXT NOT NULL,
  "column"             kb_column NOT NULL DEFAULT 'backlog',
  agent_role           kb_role  NOT NULL DEFAULT 'executor',
  spec                 TEXT NOT NULL DEFAULT '',
  acceptance_criteria  JSONB NOT NULL DEFAULT '[]'::jsonb,
  assigned_worker      TEXT,
  parent               TEXT REFERENCES cards(id) ON DELETE SET NULL,
  children             JSONB NOT NULL DEFAULT '[]'::jsonb,
  thread               JSONB NOT NULL DEFAULT '[]'::jsonb,
  lease_expires_at     TIMESTAMPTZ,                  -- drives the reaper
  attempt              INT  NOT NULL DEFAULT 0,
  requires_human       BOOLEAN NOT NULL DEFAULT FALSE,
  tags                 JSONB NOT NULL DEFAULT '[]'::jsonb,  -- ["heavy"] -> cloud
  created_at           TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at           TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_cards_column ON cards("column");
CREATE INDEX IF NOT EXISTS idx_cards_lease  ON cards(lease_expires_at)
  WHERE "column" = 'in-flight';

-- --------------------------------------------------------------------------- --
-- board_events = the append-only transition log. Also the idempotency ledger.
-- --------------------------------------------------------------------------- --
CREATE TABLE IF NOT EXISTS board_events (
  id         BIGSERIAL PRIMARY KEY,
  card_id    TEXT NOT NULL REFERENCES cards(id) ON DELETE CASCADE,
  from_col   kb_column,
  to_col     kb_column NOT NULL,
  attempt    INT NOT NULL DEFAULT 0,
  actor      TEXT NOT NULL,       -- worker id / "human:remy" / "system:reaper"
  reason     TEXT,
  ts         TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Replays of the same transition are no-ops (ON CONFLICT DO NOTHING upstream).
CREATE UNIQUE INDEX IF NOT EXISTS uq_transition
  ON board_events(card_id, from_col, to_col, attempt);

-- --------------------------------------------------------------------------- --
-- NOTIFY trigger: any column change publishes a JSON payload on 'kb_events'.
-- The Conductor LISTENs on this channel and dispatches an agent per event.
-- --------------------------------------------------------------------------- --
CREATE OR REPLACE FUNCTION notify_card_transition() RETURNS trigger AS $$
BEGIN
  IF (TG_OP = 'UPDATE' AND NEW."column" IS DISTINCT FROM OLD."column")
     OR TG_OP = 'INSERT' THEN
    PERFORM pg_notify(
      'kb_events',
      json_build_object(
        'card_id',   NEW.id,
        'from',      CASE WHEN TG_OP='INSERT' THEN NULL ELSE OLD."column" END,
        'to',        NEW."column",
        'role',      NEW.agent_role,
        'attempt',   NEW.attempt,
        'tags',      NEW.tags,
        'ts',        extract(epoch from now())
      )::text
    );
  END IF;
  NEW.updated_at := now();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS trg_card_transition ON cards;
CREATE TRIGGER trg_card_transition
  BEFORE INSERT OR UPDATE ON cards
  FOR EACH ROW EXECUTE FUNCTION notify_card_transition();

COMMIT;