@jaguilar87/gaia 5.0.4 → 5.0.5

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 (113) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +56 -0
  4. package/INSTALL.md +0 -2
  5. package/README.md +1 -6
  6. package/bin/README.md +0 -1
  7. package/bin/cli/_install_helpers.py +1 -1
  8. package/bin/cli/cleanup.py +0 -1
  9. package/bin/cli/doctor.py +1 -1
  10. package/bin/cli/memory.py +2 -0
  11. package/bin/cli/update.py +1 -1
  12. package/bin/pre-publish-validate.js +48 -5
  13. package/config/README.md +22 -44
  14. package/config/surface-routing.json +0 -1
  15. package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
  16. package/dist/gaia-ops/config/README.md +22 -44
  17. package/dist/gaia-ops/config/surface-routing.json +0 -1
  18. package/dist/gaia-ops/hooks/modules/agents/handoff_persister.py +2 -0
  19. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +2 -0
  20. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +2 -0
  21. package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +90 -55
  22. package/dist/gaia-ops/skills/README.md +1 -1
  23. package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +1 -1
  24. package/dist/gaia-ops/skills/gaia-patterns/reference.md +0 -1
  25. package/dist/gaia-ops/skills/gaia-release/SKILL.md +60 -24
  26. package/dist/gaia-ops/skills/gaia-release/reference.md +35 -11
  27. package/dist/gaia-ops/skills/git-conventions/SKILL.md +6 -2
  28. package/dist/gaia-ops/skills/orchestrator-present-approval/SKILL.md +10 -2
  29. package/dist/gaia-ops/skills/readme-writing/SKILL.md +1 -1
  30. package/dist/gaia-ops/skills/readme-writing/reference.md +0 -1
  31. package/dist/gaia-ops/tools/scan/ui.py +20 -4
  32. package/dist/gaia-ops/tools/scan/verify.py +3 -3
  33. package/dist/gaia-ops/tools/validation/README.md +15 -24
  34. package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
  35. package/dist/gaia-security/hooks/modules/agents/handoff_persister.py +2 -0
  36. package/dist/gaia-security/hooks/modules/security/approval_grants.py +2 -0
  37. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +2 -0
  38. package/dist/gaia-security/hooks/modules/validation/commit_validator.py +90 -55
  39. package/hooks/modules/agents/handoff_persister.py +2 -0
  40. package/hooks/modules/security/approval_grants.py +2 -0
  41. package/hooks/modules/tools/bash_validator.py +2 -0
  42. package/hooks/modules/validation/commit_validator.py +90 -55
  43. package/index.js +2 -12
  44. package/package.json +4 -6
  45. package/pyproject.toml +3 -3
  46. package/scripts/bootstrap_database.sh +88 -439
  47. package/scripts/check_schema_drift.py +208 -0
  48. package/scripts/migrations/README.md +78 -28
  49. package/scripts/migrations/schema.checksum +8 -0
  50. package/scripts/release-prepare.mjs +199 -0
  51. package/skills/README.md +1 -1
  52. package/skills/gaia-patterns/SKILL.md +1 -1
  53. package/skills/gaia-patterns/reference.md +0 -1
  54. package/skills/gaia-release/SKILL.md +60 -24
  55. package/skills/gaia-release/reference.md +35 -11
  56. package/skills/git-conventions/SKILL.md +6 -2
  57. package/skills/orchestrator-present-approval/SKILL.md +10 -2
  58. package/skills/readme-writing/SKILL.md +1 -1
  59. package/skills/readme-writing/reference.md +0 -1
  60. package/tools/scan/ui.py +20 -4
  61. package/tools/scan/verify.py +3 -3
  62. package/tools/validation/README.md +15 -24
  63. package/commands/README.md +0 -64
  64. package/commands/gaia.md +0 -37
  65. package/commands/scan-project.md +0 -74
  66. package/config/crons-schema.md +0 -81
  67. package/config/git_standards.json +0 -72
  68. package/dist/gaia-ops/commands/gaia.md +0 -37
  69. package/dist/gaia-ops/config/crons-schema.md +0 -81
  70. package/dist/gaia-ops/config/git_standards.json +0 -72
  71. package/dist/gaia-ops/tools/agentic-loop/decide-status.py +0 -210
  72. package/dist/gaia-ops/tools/agentic-loop/parse-metric.py +0 -106
  73. package/dist/gaia-ops/tools/agentic-loop/record-iteration.py +0 -223
  74. package/git-hooks/commit-msg +0 -41
  75. package/scripts/migrations/v10_to_v11.sql +0 -170
  76. package/scripts/migrations/v10_to_v11_fresh.sql +0 -18
  77. package/scripts/migrations/v11_to_v12.sql +0 -195
  78. package/scripts/migrations/v11_to_v12_fresh.sql +0 -19
  79. package/scripts/migrations/v12_to_v13.sql +0 -48
  80. package/scripts/migrations/v12_to_v13_fresh.sql +0 -17
  81. package/scripts/migrations/v13_to_v14.sql +0 -44
  82. package/scripts/migrations/v13_to_v14_fresh.sql +0 -17
  83. package/scripts/migrations/v14_to_v15.sql +0 -71
  84. package/scripts/migrations/v14_to_v15_fresh.sql +0 -19
  85. package/scripts/migrations/v15_to_v16.sql +0 -57
  86. package/scripts/migrations/v15_to_v16_fresh.sql +0 -18
  87. package/scripts/migrations/v16_to_v17.sql +0 -51
  88. package/scripts/migrations/v16_to_v17_fresh.sql +0 -18
  89. package/scripts/migrations/v17_to_v18.sql +0 -66
  90. package/scripts/migrations/v17_to_v18_fresh.sql +0 -24
  91. package/scripts/migrations/v1_to_v2.sql +0 -97
  92. package/scripts/migrations/v2_to_v3.sql +0 -68
  93. package/scripts/migrations/v2_to_v3_merge.sql +0 -69
  94. package/scripts/migrations/v3_to_v4.sql +0 -67
  95. package/scripts/migrations/v3_to_v4_fresh.sql +0 -20
  96. package/scripts/migrations/v4_to_v5.sql +0 -55
  97. package/scripts/migrations/v4_to_v5_fresh.sql +0 -20
  98. package/scripts/migrations/v5_to_v6.sql +0 -48
  99. package/scripts/migrations/v5_to_v6_fresh.sql +0 -17
  100. package/scripts/migrations/v6_to_v7.sql +0 -26
  101. package/scripts/migrations/v6_to_v7_fresh.sql +0 -13
  102. package/scripts/migrations/v7_to_v8.sql +0 -44
  103. package/scripts/migrations/v7_to_v8_fresh.sql +0 -14
  104. package/scripts/migrations/v8_to_v9.sql +0 -87
  105. package/scripts/migrations/v8_to_v9_fresh.sql +0 -15
  106. package/scripts/migrations/v9_to_v10.sql +0 -109
  107. package/scripts/migrations/v9_to_v10_episodes_workspace.sql +0 -109
  108. package/scripts/migrations/v9_to_v10_fresh.sql +0 -18
  109. package/templates/README.md +0 -70
  110. package/templates/managed-settings.template.json +0 -43
  111. package/tools/agentic-loop/decide-status.py +0 -210
  112. package/tools/agentic-loop/parse-metric.py +0 -106
  113. package/tools/agentic-loop/record-iteration.py +0 -223
@@ -1,223 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- record-iteration.py
4
-
5
- Atomically update state.json and append to worklog.md after each iteration.
6
- The LLM never writes state.json directly — this script is the only writer.
7
-
8
- Usage:
9
- python3 record-iteration.py \
10
- --state-file state.json \
11
- --worklog worklog.md \
12
- --iteration 5 \
13
- --metric-value 94.5 \
14
- --status keep \
15
- --description "Handle hyphenated verbs" \
16
- --insight "delete-objects splits correctly" \
17
- --next "Check camelCase+hyphen combined"
18
-
19
- Optional flags:
20
- --changed TEXT What was modified (default: same as description)
21
- --metric-name TEXT Name of the metric recorded (default: "metric")
22
-
23
- Atomic write guarantee: state.json is written to a .tmp sibling, fsynced,
24
- then renamed over the original. Either the full write lands or the original
25
- is untouched.
26
- """
27
-
28
- from __future__ import annotations
29
-
30
- import argparse
31
- import json
32
- import os
33
- import sys
34
- import tempfile
35
- from datetime import datetime, timezone
36
-
37
-
38
- def load_state(path: str) -> dict:
39
- """Load existing state.json or return an empty skeleton."""
40
- if not os.path.exists(path):
41
- return {
42
- "iteration": 0,
43
- "current_metric": None,
44
- "best_metric": None,
45
- "consecutive_discards": 0,
46
- "pivot_count": 0,
47
- "timestamp": None,
48
- "status": None,
49
- }
50
- try:
51
- with open(path, "r") as fh:
52
- data = json.load(fh)
53
- return data
54
- except (OSError, json.JSONDecodeError) as exc:
55
- print(f"error: cannot read state file '{path}': {exc}", file=sys.stderr)
56
- sys.exit(1)
57
-
58
-
59
- def atomic_write_json(path: str, data: dict) -> None:
60
- """Write *data* to *path* atomically using write-fsync-rename."""
61
- dir_name = os.path.dirname(os.path.abspath(path))
62
- # Use a temp file in the same directory so rename is on the same filesystem.
63
- try:
64
- fd, tmp_path = tempfile.mkstemp(dir=dir_name, suffix=".tmp")
65
- try:
66
- with os.fdopen(fd, "w") as fh:
67
- json.dump(data, fh, indent=2)
68
- fh.write("\n")
69
- fh.flush()
70
- os.fsync(fh.fileno())
71
- os.replace(tmp_path, path)
72
- except Exception:
73
- # Clean up orphaned temp file on failure.
74
- try:
75
- os.unlink(tmp_path)
76
- except OSError:
77
- pass
78
- raise
79
- except OSError as exc:
80
- print(f"error: atomic write to '{path}' failed: {exc}", file=sys.stderr)
81
- sys.exit(1)
82
-
83
-
84
- def append_worklog(
85
- path: str,
86
- iteration: int,
87
- description: str,
88
- metric_name: str,
89
- metric_value: float,
90
- status: str,
91
- changed: str,
92
- insight: str,
93
- next_step: str,
94
- best_metric: float | None,
95
- ) -> None:
96
- """Append a structured run entry to worklog.md."""
97
- status_upper = status.upper()
98
-
99
- # Build result sentence
100
- if best_metric is None:
101
- result_text = f"{metric_name}={metric_value} (first run, no prior best)"
102
- else:
103
- comparison = (
104
- f"improved from {best_metric}"
105
- if metric_value > best_metric
106
- else (
107
- f"unchanged from {best_metric}"
108
- if metric_value == best_metric
109
- else f"regressed from {best_metric}"
110
- )
111
- )
112
- result_text = f"{metric_name}={metric_value} ({comparison})"
113
-
114
- entry = (
115
- f"\n### Run {iteration}: {description} — {metric_name}={metric_value} ({status_upper})\n"
116
- f"- **Changed:** {changed}\n"
117
- f"- **Result:** {result_text}\n"
118
- f"- **Insight:** {insight}\n"
119
- f"- **Next:** {next_step}\n"
120
- )
121
-
122
- try:
123
- with open(path, "a") as fh:
124
- fh.write(entry)
125
- except OSError as exc:
126
- print(f"error: cannot append to worklog '{path}': {exc}", file=sys.stderr)
127
- sys.exit(1)
128
-
129
-
130
- def main() -> None:
131
- parser = argparse.ArgumentParser(
132
- description="Atomically record an agentic-loop iteration into state.json and worklog.md.",
133
- formatter_class=argparse.RawDescriptionHelpFormatter,
134
- epilog="""
135
- Status values:
136
- keep — metric improved; best is updated, consecutive_discards reset to 0
137
- discard — metric did not improve; consecutive_discards incremented
138
- pivot — forced strategy change (also increments pivot_count)
139
- stop — terminal state; loop should halt
140
-
141
- Exit codes:
142
- 0 success
143
- 1 error (message on stderr)
144
- """,
145
- )
146
- parser.add_argument("--state-file", required=True, metavar="PATH", help="Path to state.json")
147
- parser.add_argument("--worklog", required=True, metavar="PATH", help="Path to worklog.md (append-only)")
148
- parser.add_argument("--iteration", required=True, type=int, help="Current iteration number (1-based)")
149
- parser.add_argument("--metric-value", required=True, type=float, metavar="NUM", help="Numeric metric value this run")
150
- parser.add_argument(
151
- "--status",
152
- required=True,
153
- choices=["keep", "discard", "pivot", "stop"],
154
- help="Outcome classification for this iteration",
155
- )
156
- parser.add_argument("--description", required=True, help="Short description of what changed this run")
157
- parser.add_argument("--insight", required=True, help="What was learned from this run")
158
- parser.add_argument("--next", required=True, dest="next_step", help="What to try in the next iteration")
159
- parser.add_argument(
160
- "--changed",
161
- default=None,
162
- metavar="TEXT",
163
- help="What was specifically modified (defaults to --description)",
164
- )
165
- parser.add_argument(
166
- "--metric-name",
167
- default="metric",
168
- metavar="NAME",
169
- help="Name label for the metric (default: metric)",
170
- )
171
- args = parser.parse_args()
172
-
173
- changed = args.changed if args.changed is not None else args.description
174
-
175
- # --- Load current state ---
176
- state = load_state(args.state_file)
177
-
178
- prev_best: float | None = state.get("best_metric")
179
-
180
- # --- Compute new state values ---
181
- state["iteration"] = args.iteration
182
- state["current_metric"] = args.metric_value
183
- state["status"] = args.status
184
- state["timestamp"] = datetime.now(tz=timezone.utc).isoformat()
185
-
186
- if args.status == "keep":
187
- # Keep: this run is better; promote to best.
188
- state["best_metric"] = args.metric_value
189
- state["consecutive_discards"] = 0
190
- elif args.status == "discard":
191
- # Do not update best; increment discard counter.
192
- state["consecutive_discards"] = int(state.get("consecutive_discards") or 0) + 1
193
- elif args.status == "pivot":
194
- # Pivot: counts as a discard for streak purposes, but also advances pivot_count.
195
- state["consecutive_discards"] = int(state.get("consecutive_discards") or 0) + 1
196
- state["pivot_count"] = int(state.get("pivot_count") or 0) + 1
197
- elif args.status == "stop":
198
- # Terminal — no counter changes needed beyond recording.
199
- pass
200
-
201
- # --- Atomic write ---
202
- atomic_write_json(args.state_file, state)
203
-
204
- # --- Append worklog ---
205
- append_worklog(
206
- path=args.worklog,
207
- iteration=args.iteration,
208
- description=args.description,
209
- metric_name=args.metric_name,
210
- metric_value=args.metric_value,
211
- status=args.status,
212
- changed=changed,
213
- insight=args.insight,
214
- next_step=args.next_step,
215
- best_metric=prev_best,
216
- )
217
-
218
- # Emit updated state summary for easy inspection.
219
- print(json.dumps(state, indent=2))
220
-
221
-
222
- if __name__ == "__main__":
223
- main()
@@ -1,41 +0,0 @@
1
- #!/bin/sh
2
- #
3
- # commit-msg hook: strip Claude Code attribution footers from commit messages.
4
- #
5
- # This hook runs at the git level, catching ALL commits regardless of whether
6
- # they originate from Claude Code's Bash tool, a subagent, or the user's terminal.
7
- #
8
- # Patterns match those in hooks/modules/tools/bash_validator.py _strip_claude_footers()
9
- # to maintain a single source of truth for what constitutes a forbidden footer.
10
- #
11
- # Installation:
12
- # cp git-hooks/commit-msg .git/hooks/commit-msg
13
- # chmod +x .git/hooks/commit-msg
14
- #
15
- # Or via gaia-init (automatic).
16
-
17
- COMMIT_MSG_FILE="$1"
18
-
19
- if [ ! -f "${COMMIT_MSG_FILE}" ]; then
20
- exit 0
21
- fi
22
-
23
- # Create a temp file for the cleaned message
24
- TEMP_FILE=$(mktemp)
25
- trap 'rm -f "${TEMP_FILE}"' EXIT
26
-
27
- # Read the commit message and strip forbidden footer lines:
28
- # - Co-Authored-By: containing "Claude" (any case)
29
- # - "Generated with Claude Code" or "[Claude Code]" (any case)
30
- # - Emoji-prefixed "Generated with" lines
31
- sed -E \
32
- -e '/^[[:space:]]*[Cc][Oo]-[Aa][Uu][Tt][Hh][Oo][Rr][Ee][Dd]-[Bb][Yy]:.*[Cc][Ll][Aa][Uu][Dd][Ee]/d' \
33
- -e '/^[[:space:]]*[Gg][Ee][Nn][Ee][Rr][Aa][Tt][Ee][Dd] [Ww][Ii][Tt][Hh].*[Cc][Ll][Aa][Uu][Dd][Ee] [Cc][Oo][Dd][Ee]/d' \
34
- -e '/^[[:space:]]*..?[[:space:]]*[Gg][Ee][Nn][Ee][Rr][Aa][Tt][Ee][Dd] [Ww][Ii][Tt][Hh]/d' \
35
- "${COMMIT_MSG_FILE}" > "${TEMP_FILE}"
36
-
37
- # Remove trailing blank lines (collapse to single trailing newline)
38
- # This prevents empty trailers after stripping
39
- sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' "${TEMP_FILE}" > "${COMMIT_MSG_FILE}"
40
-
41
- exit 0
@@ -1,170 +0,0 @@
1
- -- Migration v10 -> v11
2
- --
3
- -- Two structural changes in a single migration:
4
- --
5
- -- Part A: memory.class NOT NULL + CHECK constraint enforcement
6
- -- Part B: trg_pcc_history trigger — fix column references (contract_key ->
7
- -- contract_name, payload_json -> payload) that caused runtime errors
8
- -- in v1->v2 migration path.
9
- --
10
- -- Background (Part A)
11
- -- -------------------
12
- -- v4 introduced memory.class as a NULLABLE column with enum enforcement only
13
- -- at the writer layer (not in DDL). The explicit decision documented in
14
- -- schema.sql was:
15
- -- "adding a CHECK at ALTER TIME forces a table-rebuild that would risk
16
- -- FTS trigger drift on the live DB"
17
- -- Task #2 reclassified all pre-v4 NULL rows, so the precondition for a safe
18
- -- rebuild is satisfied (0 rows with class IS NULL).
19
- --
20
- -- The rebuild follows the same rename-create-copy-drop pattern as v1_to_v2.sql:
21
- -- 1. Guard: fail fast if any NULL class rows exist.
22
- -- 2. Drop FTS5 mirror triggers (avoid double-writes during bulk copy).
23
- -- 3. Rename old table out of the way.
24
- -- 4. Create new table with NOT NULL CHECK(class IN ('anchor','thread','log')).
25
- -- 5. Copy rows preserving rowid.
26
- -- 6. Drop renamed old table.
27
- -- 7. Recreate indexes (workspace, type, class+status).
28
- -- 8. Recreate FTS5 mirror triggers verbatim.
29
- -- 9. Rebuild memory_fts.
30
- --
31
- -- Background (Part B)
32
- -- -------------------
33
- -- v8_to_v9.sql created trg_pcc_history referencing:
34
- -- OLD.contract_key -- but project_context_contracts has column contract_name
35
- -- OLD.payload_json -- but project_context_contracts has column payload
36
- -- NEW.payload_json -- same
37
- -- The trigger DDL was stored in sqlite_master with the wrong column names. SQLite
38
- -- defers column resolution to execution time, so the trigger was silently
39
- -- accepted at CREATE time but blew up whenever it fired, AND when any
40
- -- DDL mutation (like ALTER TABLE in v1_to_v2 migration) caused SQLite to
41
- -- validate all live triggers -- aborting the v1->v2 migration transaction.
42
- --
43
- -- Precondition guards
44
- -- -------------------
45
- -- We assert 0 NULL class rows before starting the rebuild. If any exist, the
46
- -- migration aborts and the caller must run `gaia memory reclassify` first.
47
- -- SQLite does not have native ASSERT, so we use a CREATE TABLE trick: if the
48
- -- subquery returns any rows the INSERT will succeed but we check with a
49
- -- NOT EXISTS guard pattern via CASE WHEN inside a temporary trigger.
50
- -- Simpler: we use a CHECK on a temp row that fails if the count > 0.
51
- --
52
- -- Atomicity: bootstrap_database.sh wraps this script in BEGIN/COMMIT.
53
- -- A failure mid-flight rolls back to v10 state; the ledger row is NOT
54
- -- inserted, so the next bootstrap retry sees the same pending migration.
55
- -- Closes ledger task #6.
56
-
57
- -- ============================================================
58
- -- PRE-CONDITION: coalesce any remaining NULL class values to 'log'
59
- -- ============================================================
60
- -- On the live DB, task #2 reclassified all NULL rows before this migration
61
- -- runs, so this UPDATE is a no-op. In test/CI scenarios where a synthetic
62
- -- DB is built from v1 and migrated through all versions, legacy rows that
63
- -- were never reclassified get a safe default of 'log'. Using COALESCE in
64
- -- the INSERT SELECT below (Step A4) achieves the same result without a
65
- -- separate UPDATE pass, but an explicit UPDATE here makes the intent clear
66
- -- and ensures the NOT NULL CHECK in the new table never fires unexpectedly.
67
- UPDATE memory SET class = 'log' WHERE class IS NULL;
68
-
69
- -- ============================================================
70
- -- PART A: memory table rebuild (NOT NULL + CHECK on class)
71
- -- ============================================================
72
-
73
- -- Step A1: Drop the FTS5 trigger trio before the rename. They will be
74
- -- recreated verbatim in Step A8. Dropping them prevents double-writes
75
- -- and avoids referencing the renamed table during the bulk copy.
76
- DROP TRIGGER IF EXISTS memory_ai;
77
- DROP TRIGGER IF EXISTS memory_ad;
78
- DROP TRIGGER IF EXISTS memory_au;
79
-
80
- -- Step A2: Rename old table out of the way. SQLite carries indexes along.
81
- ALTER TABLE memory RENAME TO memory_v10_old;
82
-
83
- -- Step A3: Create the new memory table with NOT NULL DEFAULT + CHECK on class.
84
- -- Schema matches schema.sql exactly: class is NOT NULL with DEFAULT 'log' and
85
- -- enum CHECK. The DEFAULT ensures that callers who do not supply class (e.g.
86
- -- upsert_memory in writer.py) get a sensible default rather than a hard failure.
87
- -- Explicit NULL is still rejected by NOT NULL.
88
- CREATE TABLE memory (
89
- workspace TEXT NOT NULL,
90
- name TEXT NOT NULL,
91
- type TEXT NOT NULL CHECK (type IN ('project', 'user', 'feedback', 'atom', 'decision', 'negative')),
92
- description TEXT,
93
- body TEXT NOT NULL,
94
- origin_session_id TEXT,
95
- updated_at TEXT,
96
- class TEXT NOT NULL DEFAULT 'log' CHECK (class IN ('anchor', 'thread', 'log')),
97
- status TEXT,
98
- PRIMARY KEY (workspace, name),
99
- FOREIGN KEY (workspace) REFERENCES workspaces(name) ON DELETE CASCADE
100
- );
101
-
102
- -- Step A4: Copy all rows preserving rowid. memory_fts joins on rowid;
103
- -- changing them would invalidate the FTS5 index. class is guaranteed
104
- -- non-NULL by the UPDATE in the pre-condition step above.
105
- INSERT INTO memory (rowid, workspace, name, type, description, body, origin_session_id, updated_at, class, status)
106
- SELECT rowid, workspace, name, type, description, body, origin_session_id, updated_at, class, status
107
- FROM memory_v10_old;
108
-
109
- -- Step A5: Drop the renamed old table. Its indexes go with it.
110
- DROP TABLE memory_v10_old;
111
-
112
- -- Step A6: Recreate the standard indexes on the new table.
113
- CREATE INDEX IF NOT EXISTS idx_memory_workspace ON memory(workspace);
114
- CREATE INDEX IF NOT EXISTS idx_memory_type ON memory(type);
115
- -- idx_memory_class_status was created by v3_to_v4.sql; must be recreated here
116
- -- because the underlying table was dropped and recreated above.
117
- CREATE INDEX IF NOT EXISTS idx_memory_class_status ON memory(workspace, class, status);
118
-
119
- -- Step A7: Recreate the three FTS5 mirror triggers verbatim from schema.sql.
120
- CREATE TRIGGER memory_ai AFTER INSERT ON memory BEGIN
121
- INSERT INTO memory_fts(rowid, workspace, name, description, body)
122
- VALUES (new.rowid, new.workspace, new.name, new.description, new.body);
123
- END;
124
-
125
- CREATE TRIGGER memory_ad AFTER DELETE ON memory BEGIN
126
- INSERT INTO memory_fts(memory_fts, rowid, workspace, name, description, body)
127
- VALUES ('delete', old.rowid, old.workspace, old.name, old.description, old.body);
128
- END;
129
-
130
- CREATE TRIGGER memory_au AFTER UPDATE ON memory BEGIN
131
- INSERT INTO memory_fts(memory_fts, rowid, workspace, name, description, body)
132
- VALUES ('delete', old.rowid, old.workspace, old.name, old.description, old.body);
133
- INSERT INTO memory_fts(rowid, workspace, name, description, body)
134
- VALUES (new.rowid, new.workspace, new.name, new.description, new.body);
135
- END;
136
-
137
- -- Step A8: Rebuild memory_fts from the new memory table.
138
- INSERT INTO memory_fts(memory_fts) VALUES('rebuild');
139
-
140
- -- ============================================================
141
- -- PART B: trg_pcc_history trigger fix
142
- -- ============================================================
143
- -- Drop the broken trigger (created by v8_to_v9.sql with wrong column refs).
144
- DROP TRIGGER IF EXISTS trg_pcc_history;
145
-
146
- -- Recreate with correct column references:
147
- -- OLD.contract_name (project_context_contracts PK component)
148
- -- OLD.payload (project_context_contracts payload column)
149
- -- NEW.payload (project_context_contracts payload column)
150
- -- The INSERT target history table still uses column `contract_key` (unchanged).
151
- CREATE TRIGGER trg_pcc_history
152
- AFTER UPDATE ON project_context_contracts
153
- BEGIN
154
- INSERT INTO project_context_contracts_history (
155
- contract_key, workspace, before_payload_json, after_payload_json, changed_at
156
- ) VALUES (
157
- OLD.contract_name,
158
- OLD.workspace,
159
- OLD.payload,
160
- NEW.payload,
161
- strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
162
- );
163
- END;
164
-
165
- -- ============================================================
166
- -- LEDGER BUMP
167
- -- ============================================================
168
- INSERT OR IGNORE INTO schema_version (version, applied_at, description)
169
- VALUES (11, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
170
- 'memory.class NOT NULL + CHECK; trg_pcc_history column fix (task #6)');
@@ -1,18 +0,0 @@
1
- -- Migration v10 -> v11 fresh-install variant
2
- --
3
- -- Used by bootstrap_database.sh when the live DB was created directly from
4
- -- schema.sql at v11 state (i.e. schema.sql already declares memory.class as
5
- -- NOT NULL CHECK and trg_pcc_history with correct column references).
6
- --
7
- -- On a fresh install:
8
- -- - schema.sql creates memory with class NOT NULL CHECK -> no rebuild needed
9
- -- - schema.sql creates trg_pcc_history with correct column refs -> no fix needed
10
- --
11
- -- This variant is a no-op; it only exists so the bootstrap guard-probe branch
12
- -- can select it and stamp the ledger without applying DDL.
13
- --
14
- -- Atomicity: bootstrap_database.sh wraps this script in BEGIN/COMMIT.
15
- -- No DDL is executed; the COMMIT is harmless.
16
-
17
- -- No-op: fresh install already at v11 state (schema.sql created all objects).
18
- SELECT 1;
@@ -1,195 +0,0 @@
1
- -- Migration v11 -> v12 (approval-model-redesign: user-in-loop, fingerprint-bound, hash-chained)
2
- --
3
- -- Background
4
- -- ----------
5
- -- v11 schema has all the episodic/memory/handoff tables from prior migrations.
6
- -- v12 adds two tables for the new approval model:
7
- -- approvals -- durable record per approval lifecycle, P-{id} prefixed
8
- -- approval_events -- append-only hash-chained audit log per lifecycle event
9
- --
10
- -- Three trigger families:
11
- -- ai_approval_events_hash -- AFTER INSERT: computes this_hash via gaia_sha256()
12
- -- bu_approval_events_immutable -- BEFORE UPDATE: raises (append-only invariant)
13
- -- bd_approval_events_immutable -- BEFORE DELETE: raises (append-only invariant)
14
- --
15
- -- Design decisions (from plan D15 and brief approach)
16
- -- ----------------------------------------------------
17
- -- D1: approvals.id carries P-{uuid4} prefix (TEXT PK, not AUTOINCREMENT INTEGER).
18
- -- Rationale: the prefix is readable in denial messages and debug output without
19
- -- a JOIN. UUIDs avoid collisions without a central counter. The hook generates
20
- -- the id and embeds it in the denial message so subagents can reference it.
21
- --
22
- -- D2: approval_events.this_hash = SHA-256(prev_hash || fingerprint)
23
- -- Computed by the AFTER INSERT trigger via SQLite scalar function `gaia_sha256`
24
- -- registered in gaia.store.writer._connect(). SQLite's built-in functions
25
- -- do not include SHA-256 so we inject a Python function at connection time.
26
- -- The trigger runs deterministically with the connection's registered function.
27
- --
28
- -- D3: Genesis row bootstrapping
29
- -- For the first event row of any approval chain (row 0), prev_hash IS NULL.
30
- -- this_hash = SHA-256("" || fingerprint) where the null is treated as an
31
- -- empty string by the trigger expression COALESCE(prev_hash, '').
32
- -- This is the documented canonical treatment -- callers should not assume a
33
- -- sentinel hash (like all-zeros); they must use COALESCE('', prev_hash) when
34
- -- walking the chain. The chain_walk validator in gaia/approvals/chain.py
35
- -- implements this correctly.
36
- --
37
- -- D4: Append-only invariant
38
- -- BEFORE UPDATE and BEFORE DELETE triggers raise an error with the literal
39
- -- message "approval_events is append-only" so any accidental mutative SQL
40
- -- gets a clear, actionable error rather than a silent no-op or wrong-table
41
- -- write. These triggers are part of the security contract: a tampered row
42
- -- breaks hash-chain validation *and* prevents direct mutation at the SQL layer.
43
- --
44
- -- D5: event_type CHECK constraint
45
- -- Nine valid event types from the plan spec. `EXPIRED` is intentionally
46
- -- excluded (no TTL-based expiry in this brief). `ESCALATED` is excluded
47
- -- (no multi-level approval chain in this brief). The CHECK is on the
48
- -- approval_events table so the constraint is enforced at the DB layer.
49
- --
50
- -- Atomicity: bootstrap_database.sh wraps this script in BEGIN/COMMIT.
51
- -- A failure mid-flight rolls back to v11 state; the ledger row is NOT
52
- -- inserted, so the next bootstrap retry sees the same pending migration.
53
- -- Closes M1 (Wave 1) of brief approval-model-redesign-user-in-loop (brief_id=71).
54
-
55
- -- ---------------------------------------------------------------------------
56
- -- Step 1: Create approvals table (durable lifecycle record)
57
- -- ---------------------------------------------------------------------------
58
- CREATE TABLE IF NOT EXISTS approvals (
59
- id TEXT PRIMARY KEY, -- P-{uuid4} prefixed identifier
60
- agent_id TEXT, -- agent that initiated the request
61
- session_id TEXT, -- CLAUDE_SESSION_ID at request time
62
- status TEXT NOT NULL DEFAULT 'pending'
63
- CHECK (status IN ('pending', 'approved', 'rejected', 'revoked', 'expired')),
64
- fingerprint TEXT, -- SHA-256 hex of canonical sealed_payload_json
65
- payload_json TEXT, -- canonical-JSON sealed_payload at REQUESTED time
66
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
67
- decided_at TEXT -- ISO-8601 UTC when approved/rejected/revoked
68
- );
69
-
70
- -- Indexes for the common query patterns:
71
- -- (a) All pending approvals regardless of session (cross-session recovery)
72
- -- (b) All approvals for a specific agent
73
- -- (c) All approvals for a specific session
74
- CREATE INDEX IF NOT EXISTS idx_approvals_status ON approvals(status);
75
- CREATE INDEX IF NOT EXISTS idx_approvals_agent ON approvals(agent_id);
76
- CREATE INDEX IF NOT EXISTS idx_approvals_session ON approvals(session_id);
77
-
78
- -- ---------------------------------------------------------------------------
79
- -- Step 2: Create approval_events table (append-only hash-chained audit log)
80
- --
81
- -- Column inventory from plan D15 (verbatim):
82
- -- id, approval_id (FK), event_type, agent_id, session_id,
83
- -- payload_json, fingerprint, prev_hash, this_hash, metadata_json, created_at
84
- -- ---------------------------------------------------------------------------
85
- CREATE TABLE IF NOT EXISTS approval_events (
86
- id INTEGER PRIMARY KEY AUTOINCREMENT,
87
- approval_id TEXT NOT NULL, -- FK -> approvals.id (P-{uuid4})
88
- event_type TEXT NOT NULL CHECK (event_type IN (
89
- 'REQUESTED',
90
- 'SHOWN',
91
- 'APPROVED',
92
- 'REJECTED',
93
- 'EXECUTED',
94
- 'FAILED',
95
- 'NOOP',
96
- 'REVOKED',
97
- 'REVERTED'
98
- )),
99
- agent_id TEXT, -- agent that triggered this event
100
- session_id TEXT, -- session that created this event row
101
- payload_json TEXT, -- canonical-JSON sealed_payload at this event
102
- fingerprint TEXT, -- SHA-256 hex of canonical payload_json
103
- prev_hash TEXT, -- this_hash of the immediately preceding row
104
- -- NULL for the genesis row (row 0 in the chain)
105
- this_hash TEXT, -- SHA-256(COALESCE(prev_hash,'') || COALESCE(fingerprint,''))
106
- -- computed by ai_approval_events_hash AFTER INSERT trigger
107
- metadata_json TEXT, -- free-form JSON for event-specific extras
108
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
109
- FOREIGN KEY (approval_id) REFERENCES approvals(id)
110
- );
111
-
112
- -- Indexes for the common query patterns:
113
- -- (a) All events for a specific approval (primary chain-walk pattern)
114
- -- (b) All events of a specific type across all approvals (audit dashboard)
115
- -- (c) All events for a specific session
116
- CREATE INDEX IF NOT EXISTS idx_approval_events_approval ON approval_events(approval_id, id);
117
- CREATE INDEX IF NOT EXISTS idx_approval_events_type ON approval_events(event_type);
118
- CREATE INDEX IF NOT EXISTS idx_approval_events_session ON approval_events(session_id);
119
-
120
- -- ---------------------------------------------------------------------------
121
- -- Step 3: this_hash computation -- application layer, not a trigger
122
- --
123
- -- The AFTER INSERT + BEFORE UPDATE combination is architecturally conflicted
124
- -- in SQLite: an AFTER INSERT trigger that calls UPDATE on the same row would
125
- -- fire the BEFORE UPDATE immutability trigger, blocking the computation.
126
- --
127
- -- Resolution: this_hash is computed by the application layer before each
128
- -- INSERT via gaia.approvals.chain._compute_this_hash(prev_hash, fingerprint).
129
- -- All INSERTs into approval_events MUST supply a computed this_hash value.
130
- -- The Python helper gaia.approvals.chain.insert_event() enforces this at
131
- -- the API boundary.
132
- --
133
- -- The gaia_sha256 scalar function is still registered on connections by
134
- -- gaia.store.writer._connect() for ad-hoc queries, chain-walk re-validation,
135
- -- and future trigger uses that do not conflict with the immutability triggers.
136
- --
137
- -- Named placeholder trigger (for schema consistency and test assertions about
138
- -- trigger count): We create a named view-only trigger that does nothing, so
139
- -- that `gaia doctor` can assert "ai_approval_events_hash trigger exists" and
140
- -- the schema introspection is consistent with the migration spec.
141
- -- This is intentionally a no-op SELECT that documents the design decision.
142
- -- ---------------------------------------------------------------------------
143
- CREATE TRIGGER IF NOT EXISTS ai_approval_events_hash
144
- AFTER INSERT ON approval_events
145
- BEGIN
146
- -- this_hash is computed by the application layer before INSERT.
147
- -- See gaia.approvals.chain._compute_this_hash() and insert_event().
148
- -- This trigger is a named placeholder for schema introspection consistency.
149
- SELECT 1;
150
- END;
151
-
152
- -- ---------------------------------------------------------------------------
153
- -- Step 4: BEFORE UPDATE trigger -- enforce append-only invariant
154
- --
155
- -- Trigger name: bu_approval_events_immutable
156
- -- bu_ prefix = BEFORE UPDATE
157
- --
158
- -- Raises with a clear message so accidental UPDATEs surface immediately
159
- -- rather than silently corrupting the audit chain.
160
- -- ---------------------------------------------------------------------------
161
- CREATE TRIGGER IF NOT EXISTS bu_approval_events_immutable
162
- BEFORE UPDATE ON approval_events
163
- BEGIN
164
- SELECT RAISE(ABORT, 'approval_events is append-only');
165
- END;
166
-
167
- -- ---------------------------------------------------------------------------
168
- -- Step 5: BEFORE DELETE trigger -- enforce append-only invariant
169
- --
170
- -- Trigger name: bd_approval_events_immutable
171
- -- bd_ prefix = BEFORE DELETE
172
- -- ---------------------------------------------------------------------------
173
- CREATE TRIGGER IF NOT EXISTS bd_approval_events_immutable
174
- BEFORE DELETE ON approval_events
175
- BEGIN
176
- SELECT RAISE(ABORT, 'approval_events is append-only');
177
- END;
178
-
179
- -- ---------------------------------------------------------------------------
180
- -- Step 6: Bump schema_version to 12
181
- -- ---------------------------------------------------------------------------
182
- INSERT OR IGNORE INTO schema_version (version, applied_at, description)
183
- VALUES (12, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
184
- 'approvals + approval_events tables + hash-chain triggers (approval-model-redesign M1)');
185
-
186
- -- Verification queries (run after applying):
187
- -- SELECT MAX(version) FROM schema_version; -- expect: 12
188
- -- SELECT name FROM sqlite_master WHERE type='table'
189
- -- AND name IN ('approvals','approval_events'); -- expect: 2 rows
190
- -- SELECT name FROM sqlite_master WHERE type='trigger'
191
- -- AND name IN ('ai_approval_events_hash',
192
- -- 'bu_approval_events_immutable',
193
- -- 'bd_approval_events_immutable'); -- expect: 3 rows
194
- -- PRAGMA table_info(approvals); -- expect: 7 columns
195
- -- PRAGMA table_info(approval_events); -- expect: 11 columns
@@ -1,19 +0,0 @@
1
- -- Migration v11 -> v12 fresh-install variant
2
- --
3
- -- Used by bootstrap_database.sh when the live DB was created directly from
4
- -- schema.sql at v12 state (i.e. schema.sql already declares the approvals
5
- -- and approval_events tables plus the trigger family).
6
- --
7
- -- On a fresh install:
8
- -- - schema.sql creates approvals with all columns -> no DDL needed
9
- -- - schema.sql creates approval_events with all columns -> no DDL needed
10
- -- - schema.sql creates all three triggers -> no DDL needed
11
- --
12
- -- This variant is a no-op; it only exists so the bootstrap guard-probe branch
13
- -- can select it and stamp the ledger without applying DDL.
14
- --
15
- -- Atomicity: bootstrap_database.sh wraps this script in BEGIN/COMMIT.
16
- -- No DDL is executed; the COMMIT is harmless.
17
-
18
- -- No-op: fresh install already at v12 state (schema.sql created all objects).
19
- SELECT 1;