@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
@@ -149,48 +149,79 @@ sqlite3 "$GAIA_DB" <<'EOF'
149
149
  DELETE FROM agent_permissions WHERE agent_name = 'gaia-operator';
150
150
  EOF
151
151
 
152
- # === Section 3b: Seed schema_version baseline (v1) ===
152
+ # === Section 3b: Seed schema_version baseline (floor) ===
153
153
  #
154
- # La tabla schema_version se crea en schema.sql. Aquí insertamos SOLO la fila
155
- # v1 ("initial schema") como baseline del ledger. Idempotente vía INSERT OR
156
- # IGNORE -- reejecutar el bootstrap no duplica ni reescribe la fila.
154
+ # Modelo de FLOOR (piso de schema), reemplaza al viejo "seed v1 + camina
155
+ # v1..v17". Gaia es una herramienta personal de un solo usuario: nadie
156
+ # actualiza una DB más vieja que la versión actual, y las instalaciones
157
+ # nuevas construyen el schema directamente desde schema.sql (que ya produce
158
+ # la forma del FLOOR). Por eso colapsamos la historia v1->v17 a un baseline.
157
159
  #
158
- # Las versiones >= 2 NO se insertan aquí. Section 3c (abajo) aplica migraciones
159
- # en orden y emite la fila schema_version correspondiente sólo si la migración
160
- # concreta tuvo éxito. Diseño elegido para evitar el bug histórico de "ledger
161
- # miente": v2 era stampada incondicionalmente aunque CREATE TABLE IF NOT EXISTS
162
- # short-circuiteaba la DDL nueva sobre DBs preexistentes.
160
+ # SCHEMA_FLOOR es la versión mínima soportada in-place. schema.sql produce
161
+ # exactamente esta forma. Reglas:
162
+ #
163
+ # - DB nueva (sin filas en schema_version): schema.sql ya creó el estado
164
+ # FLOOR, así que sellamos (version=SCHEMA_FLOOR) directamente. No se
165
+ # siembra v1 ni se camina la cadena.
166
+ # - DB en o por encima del FLOOR: no se hace nada aquí (Section 3c decide
167
+ # si hay migraciones forward pendientes hacia EXPECTED).
168
+ # - DB por debajo del FLOOR (1 <= version < FLOOR): NO soportada para
169
+ # upgrade in-place. Abortamos con un mensaje claro pidiendo recrear la DB.
163
170
  #
164
171
  # `gaia doctor` lee MAX(version) y lo compara contra EXPECTED_SCHEMA_VERSION
165
172
  # baked in al CLI. Adicionalmente, check_schema_ddl_consistency compara el CHECK
166
173
  # constraint vivo contra el de schema.sql para cazar drift de ledger.
174
+ SCHEMA_FLOOR=18
175
+
167
176
  NOW_UTC="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
168
- sqlite3 "$GAIA_DB" <<EOF
177
+ EXISTING_VERSION="$(sqlite3 "$GAIA_DB" "SELECT COALESCE(MAX(version), 0) FROM schema_version;")"
178
+
179
+ if [ "$EXISTING_VERSION" -eq 0 ]; then
180
+ # Fresh install: schema.sql ya construyó el estado FLOOR. Sellamos el
181
+ # ledger directamente en el FLOOR. INSERT OR IGNORE mantiene idempotencia.
182
+ sqlite3 "$GAIA_DB" <<EOF
169
183
  INSERT OR IGNORE INTO schema_version (version, applied_at, description)
170
- VALUES (1, '${NOW_UTC}', 'initial schema');
184
+ VALUES (${SCHEMA_FLOOR}, '${NOW_UTC}', 'baseline floor: schema.sql at v${SCHEMA_FLOOR}');
171
185
  EOF
172
- echo "[bootstrap] schema_version baseline seeded (v1)"
186
+ echo "[bootstrap] schema_version baseline seeded at floor (v${SCHEMA_FLOOR})"
187
+ elif [ "$EXISTING_VERSION" -lt "$SCHEMA_FLOOR" ]; then
188
+ # DB por debajo del piso: ya no soportamos upgrade in-place desde la
189
+ # cadena histórica v1..v17. Fallamos claro (no en silencio).
190
+ echo "[bootstrap] ERROR: DB at schema_version=${EXISTING_VERSION} is below the supported floor v${SCHEMA_FLOOR}." >&2
191
+ echo "[bootstrap] In-place upgrade from pre-v${SCHEMA_FLOOR} databases is no longer supported." >&2
192
+ echo "[bootstrap] Recreate the DB: back up any data you need, delete ${GAIA_DB}, then re-run \`gaia install\`." >&2
193
+ exit 1
194
+ else
195
+ # DB en o por encima del piso: nada que sembrar aquí. Section 3c decide
196
+ # si hay migraciones forward pendientes hacia EXPECTED_SCHEMA_VERSION.
197
+ echo "[bootstrap] schema_version at v${EXISTING_VERSION} (>= floor v${SCHEMA_FLOOR}); no baseline seed needed"
198
+ fi
173
199
 
174
- # === Section 3c: Apply pending schema migrations ===
200
+ # === Section 3c: Apply pending forward migrations (floor+1 .. EXPECTED) ===
175
201
  #
176
- # Itera desde MAX(version)+1 hasta EXPECTED_SCHEMA_VERSION (extraído de
177
- # bin/cli/doctor.py vía grep) y aplica scripts/migrations/v{N-1}_to_v{N}.sql
178
- # cuando son necesarios. Cada migración se aplica en su propia transacción
179
- # BEGIN/COMMIT con guard de pre-condición para soportar fresh installs donde
180
- # schema.sql ya creó la tabla en estado target.
202
+ # Modelo FLOOR forward-only. La cadena histórica v1..v17 fue eliminada; el
203
+ # baseline es el FLOOR (Section 3b). Esta sección aplica SÓLO migraciones
204
+ # forward que se agreguen en el futuro, una por bump:
181
205
  #
182
- # Lógica por versión N:
183
- # 1. ¿Existe el archivo de migración? Si no, abort.
184
- # 2. Guard probe: ¿la live DB ya está en estado target? Si sí, sólo stampa
185
- # el row del ledger y continúa (caso fresh install).
186
- # 3. Si no, ejecuta la migración dentro de BEGIN/COMMIT. Si la transacción
187
- # falla, abort -- el ledger NO se actualiza, el próximo bootstrap retry
188
- # ve la misma migración pendiente.
189
- # 4. Tras éxito, INSERT OR IGNORE en schema_version (version=N, ...).
206
+ # scripts/migrations/v{N-1}_to_v{N}.sql (N > SCHEMA_FLOOR)
207
+ #
208
+ # Convención forward-only (ver scripts/migrations/README.md):
209
+ # - El baseline es la versión actual (FLOOR). schema.sql produce esa forma.
210
+ # - Cada bump futuro agrega EXACTAMENTE un v{N-1}_to_v{N}.sql y sube
211
+ # EXPECTED_SCHEMA_VERSION en doctor.py en el mismo commit.
212
+ # - Para una DB en el FLOOR, esa migración corre directo (la DB está en el
213
+ # estado source de la migración). No se necesitan variantes _fresh: un
214
+ # fresh install ya está en EXPECTED tras schema.sql, así que el loop no
215
+ # entra (CURRENT == EXPECTED). El guard-probe por-versión del modelo viejo
216
+ # desaparece junto con la cadena histórica.
217
+ #
218
+ # Cada migración corre en su propia transacción BEGIN/COMMIT. Si falla, abort
219
+ # -- el ledger NO avanza y el próximo bootstrap retry ve la misma pendiente.
190
220
  #
191
221
  # EXPECTED_SCHEMA_VERSION se lee dinámicamente de doctor.py para mantener una
192
222
  # sola fuente de verdad. test_schema_version_lockstep garantiza que el número
193
- # en doctor.py concuerda con las migraciones disponibles.
223
+ # en doctor.py concuerda con las migraciones disponibles (== FLOOR cuando no
224
+ # hay migraciones forward todavía).
194
225
 
195
226
  DOCTOR_PY="${SCRIPT_DIR}/../bin/cli/doctor.py"
196
227
  if [ ! -f "$DOCTOR_PY" ]; then
@@ -215,6 +246,12 @@ echo "[bootstrap] schema_version: current=${CURRENT_VERSION}, expected=${EXPECTE
215
246
  MIG_DIR="${SCRIPT_DIR}/migrations"
216
247
 
217
248
  if [ "$CURRENT_VERSION" -lt "$EXPECTED_VERSION" ]; then
249
+ # Forward-only loop. Reaches here only when a FUTURE migration has been
250
+ # added (EXPECTED_SCHEMA_VERSION > FLOOR) and the live DB is behind it.
251
+ # On a fresh install the DB is already at EXPECTED (schema.sql produced the
252
+ # FLOOR == EXPECTED shape when no forward migrations exist), so this branch
253
+ # is skipped entirely. Any DB below the FLOOR was already rejected in
254
+ # Section 3b, so CURRENT_VERSION here is always >= FLOOR.
218
255
  for N in $(seq $((CURRENT_VERSION + 1)) "$EXPECTED_VERSION"); do
219
256
  PREV=$((N - 1))
220
257
  MIG_FILE="${MIG_DIR}/v${PREV}_to_v${N}.sql"
@@ -222,424 +259,35 @@ if [ "$CURRENT_VERSION" -lt "$EXPECTED_VERSION" ]; then
222
259
  if [ ! -f "$MIG_FILE" ]; then
223
260
  echo "[bootstrap] ERROR: missing migration file ${MIG_FILE}" >&2
224
261
  echo "[bootstrap] Cannot advance from v${PREV} to v${N}. The ledger will remain at v${CURRENT_VERSION}." >&2
262
+ echo "[bootstrap] When bumping EXPECTED_SCHEMA_VERSION to v${N}, add scripts/migrations/v${PREV}_to_v${N}.sql in the same commit." >&2
225
263
  exit 1
226
264
  fi
227
265
 
228
- # Per-version guard probe. Each migration has a fingerprint that
229
- # tells us whether the live DDL is already at the target state
230
- # (fresh install where schema.sql ran with the new DDL) or still
231
- # at the source state (existing DB where CREATE TABLE IF NOT EXISTS
232
- # short-circuited).
233
- #
234
- # A guard probe can also OVERRIDE which .sql file to run when the
235
- # entry state is mutative but distinct from the canonical source.
236
- # See v2->v3 below for the "both tables present" case, which needs
237
- # the merge variant rather than the rename variant.
238
- ALREADY_AT_TARGET=0
239
- OVERRIDE_MIG_FILE=""
240
- case "$N" in
241
- 2)
242
- # v1 -> v2: widen memory.type CHECK. Target state contains 'atom'.
243
- MEMORY_DDL="$(sqlite3 "$GAIA_DB" "SELECT sql FROM sqlite_master WHERE type='table' AND name='memory';")"
244
- if [[ "$MEMORY_DDL" == *"'atom'"* ]]; then
245
- ALREADY_AT_TARGET=1
246
- fi
247
- ;;
248
- 3)
249
- # v2 -> v3: rename context_contracts -> project_context_contracts
250
- # and add agent_contract_permissions. Three entry states:
251
- # state 1 (only old): rename via v2_to_v3.sql (the default file).
252
- # state 2 (only new + perms exist): "at target", stamp ledger.
253
- # state 3 (both tables): copy rows + drop old via v2_to_v3_merge.sql.
254
- #
255
- # The detection order is deliberate: we check for the legacy
256
- # table first because its presence is the disqualifying signal
257
- # for "already at target", regardless of what else exists.
258
- HAS_OLD="$(sqlite3 "$GAIA_DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='context_contracts';")"
259
- HAS_NEW="$(sqlite3 "$GAIA_DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='project_context_contracts';")"
260
- HAS_PERMS="$(sqlite3 "$GAIA_DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='agent_contract_permissions';")"
261
-
262
- if [ -z "$HAS_OLD" ] && [ "$HAS_NEW" = "project_context_contracts" ] && [ "$HAS_PERMS" = "agent_contract_permissions" ]; then
263
- # State 2: only new tables exist, fully migrated.
264
- ALREADY_AT_TARGET=1
265
- elif [ "$HAS_OLD" = "context_contracts" ] && [ "$HAS_NEW" = "project_context_contracts" ]; then
266
- # State 3: both tables exist -- run the merge variant.
267
- OVERRIDE_MIG_FILE="${MIG_DIR}/v2_to_v3_merge.sql"
268
- if [ ! -f "$OVERRIDE_MIG_FILE" ]; then
269
- echo "[bootstrap] ERROR: state-3 merge script missing at ${OVERRIDE_MIG_FILE}" >&2
270
- exit 1
271
- fi
272
- fi
273
- # Otherwise: state 1 (only old) -- fall through to default rename script.
274
- ;;
275
- 4)
276
- # v3 -> v4: add memory.class + memory.status columns plus the
277
- # memory_links table. Target state contains the `class` column
278
- # on the memory table. We probe pragma_table_info; presence of
279
- # 'class' is the fingerprint of v4 target state.
280
- #
281
- # Note: idx_memory_class_status is intentionally NOT declared
282
- # in schema.sql -- it references columns that ALTER TABLE adds
283
- # later, and CREATE INDEX in schema.sql would parse-fail on
284
- # v3 DBs. On fresh-install (ALREADY_AT_TARGET=1) we run the
285
- # migration script anyway because its statements are all
286
- # idempotent (`IF NOT EXISTS`) and the only operation that is
287
- # NOT a no-op on fresh install is the index creation.
288
- MEMORY_HAS_CLASS="$(sqlite3 "$GAIA_DB" "SELECT name FROM pragma_table_info('memory') WHERE name='class';")"
289
- MEMORY_LINKS_EXISTS="$(sqlite3 "$GAIA_DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='memory_links';")"
290
- if [ "$MEMORY_HAS_CLASS" = "class" ] && [ "$MEMORY_LINKS_EXISTS" = "memory_links" ]; then
291
- # Fresh install: schema.sql created the v4 columns and
292
- # memory_links table. Run the migration anyway -- the
293
- # ALTER TABLE statements need to be skipped because the
294
- # columns already exist. We branch to a fresh-install
295
- # variant that ONLY creates the index.
296
- OVERRIDE_MIG_FILE="${MIG_DIR}/v3_to_v4_fresh.sql"
297
- if [ ! -f "$OVERRIDE_MIG_FILE" ]; then
298
- echo "[bootstrap] ERROR: v3->v4 fresh-install variant missing at ${OVERRIDE_MIG_FILE}" >&2
299
- exit 1
300
- fi
301
- fi
302
- # Otherwise: existing v3 DB -- fall through to default v3_to_v4.sql.
303
- ;;
304
- 5)
305
- # v4 -> v5: add acceptance_criteria.status + milestones.status.
306
- # Target state: acceptance_criteria has 'status' column.
307
- AC_HAS_STATUS="$(sqlite3 "$GAIA_DB" "SELECT name FROM pragma_table_info('acceptance_criteria') WHERE name='status';")"
308
- MS_HAS_STATUS="$(sqlite3 "$GAIA_DB" "SELECT name FROM pragma_table_info('milestones') WHERE name='status';")"
309
- if [ "$AC_HAS_STATUS" = "status" ] && [ "$MS_HAS_STATUS" = "status" ]; then
310
- # Fresh install: schema.sql already created v5 columns.
311
- # Run the fresh-install variant that ONLY creates the indexes.
312
- OVERRIDE_MIG_FILE="${MIG_DIR}/v4_to_v5_fresh.sql"
313
- if [ ! -f "$OVERRIDE_MIG_FILE" ]; then
314
- echo "[bootstrap] ERROR: v4->v5 fresh-install variant missing at ${OVERRIDE_MIG_FILE}" >&2
315
- exit 1
316
- fi
317
- fi
318
- # Otherwise: existing v4 DB -- fall through to default v4_to_v5.sql.
319
- ;;
320
- 6)
321
- # v5 -> v6: add evidence table (three-tier storage model).
322
- # Target state: evidence table exists.
323
- EVIDENCE_EXISTS="$(sqlite3 "$GAIA_DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='evidence';")"
324
- if [ "$EVIDENCE_EXISTS" = "evidence" ]; then
325
- # Fresh install: schema.sql already created the evidence table.
326
- # Run the fresh-install variant that ONLY creates the indexes.
327
- OVERRIDE_MIG_FILE="${MIG_DIR}/v5_to_v6_fresh.sql"
328
- if [ ! -f "$OVERRIDE_MIG_FILE" ]; then
329
- echo "[bootstrap] ERROR: v5->v6 fresh-install variant missing at ${OVERRIDE_MIG_FILE}" >&2
330
- exit 1
331
- fi
332
- fi
333
- # Otherwise: existing v5 DB -- fall through to default v5_to_v6.sql.
334
- ;;
335
- 7)
336
- # v6 -> v7: add workspaces.last_scan_at column (agent-contract-handoff M1).
337
- # Target state: workspaces table has a 'last_scan_at' column.
338
- WS_HAS_LAST_SCAN="$(sqlite3 "$GAIA_DB" "SELECT name FROM pragma_table_info('workspaces') WHERE name='last_scan_at';")"
339
- if [ "$WS_HAS_LAST_SCAN" = "last_scan_at" ]; then
340
- # Fresh install: schema.sql already created the column.
341
- # Run the fresh-install variant (no-op SELECT) to stamp the ledger.
342
- OVERRIDE_MIG_FILE="${MIG_DIR}/v6_to_v7_fresh.sql"
343
- if [ ! -f "$OVERRIDE_MIG_FILE" ]; then
344
- echo "[bootstrap] ERROR: v6->v7 fresh-install variant missing at ${OVERRIDE_MIG_FILE}" >&2
345
- exit 1
346
- fi
347
- fi
348
- # Otherwise: existing v6 DB -- fall through to default v6_to_v7.sql.
349
- ;;
350
- 8)
351
- # v7 -> v8: add approval_grants table (agent-contract-handoff M3).
352
- # Target state: approval_grants table exists.
353
- GRANTS_EXISTS="$(sqlite3 "$GAIA_DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='approval_grants';")"
354
- if [ "$GRANTS_EXISTS" = "approval_grants" ]; then
355
- # Fresh install: schema.sql already created the approval_grants table.
356
- # Run the fresh-install variant (no-op SELECT) to stamp the ledger.
357
- OVERRIDE_MIG_FILE="${MIG_DIR}/v7_to_v8_fresh.sql"
358
- if [ ! -f "$OVERRIDE_MIG_FILE" ]; then
359
- echo "[bootstrap] ERROR: v7->v8 fresh-install variant missing at ${OVERRIDE_MIG_FILE}" >&2
360
- exit 1
361
- fi
362
- fi
363
- # Otherwise: existing v7 DB -- fall through to default v7_to_v8.sql.
364
- ;;
365
- 9)
366
- # v8 -> v9: add agent_contract_handoffs, agent_contract_handoff_approvals,
367
- # project_context_contracts_history tables + trg_pcc_history trigger
368
- # (agent-contract-handoff M4: handoff persistence).
369
- # Target state: agent_contract_handoffs table exists.
370
- HANDOFFS_EXISTS="$(sqlite3 "$GAIA_DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='agent_contract_handoffs';")"
371
- if [ "$HANDOFFS_EXISTS" = "agent_contract_handoffs" ]; then
372
- # Fresh install: schema.sql already created all v9 tables and trigger.
373
- # Run the fresh-install variant (no-op SELECT) to stamp the ledger.
374
- OVERRIDE_MIG_FILE="${MIG_DIR}/v8_to_v9_fresh.sql"
375
- if [ ! -f "$OVERRIDE_MIG_FILE" ]; then
376
- echo "[bootstrap] ERROR: v8->v9 fresh-install variant missing at ${OVERRIDE_MIG_FILE}" >&2
377
- exit 1
378
- fi
379
- fi
380
- # Otherwise: existing v8 DB -- fall through to default v8_to_v9.sql.
381
- ;;
382
- 10)
383
- # v9 -> v10: add episodes.tier column + idx_episodes_tier + idx_episodes_tier_outcome
384
- # + episode_anomalies table + its 3 indexes
385
- # (episodic-workflow-to-db AC-3: migration apply).
386
- #
387
- # Target state fingerprint: episodes.tier column exists.
388
- # We use PRAGMA table_info to check for the tier column.
389
- # This is the correct fingerprint because:
390
- # - Fresh install: schema.sql creates episodes WITH tier -> tier exists
391
- # - Existing v9 DB: schema.sql's CREATE TABLE IF NOT EXISTS is a no-op
392
- # -> tier does NOT exist -> falls through to the full migration
393
- # Note: episode_anomalies table is NOT a valid fingerprint because
394
- # schema.sql creates it via CREATE TABLE IF NOT EXISTS even on existing DBs.
395
- TIER_EXISTS="$(sqlite3 "$GAIA_DB" "SELECT name FROM pragma_table_info('episodes') WHERE name='tier';")"
396
- if [ "$TIER_EXISTS" = "tier" ]; then
397
- # Fresh install: schema.sql already created episodes with tier column.
398
- # Run the fresh-install variant (creates tier indexes) to stamp the ledger.
399
- OVERRIDE_MIG_FILE="${MIG_DIR}/v9_to_v10_fresh.sql"
400
- if [ ! -f "$OVERRIDE_MIG_FILE" ]; then
401
- echo "[bootstrap] ERROR: v9->v10 fresh-install variant missing at ${OVERRIDE_MIG_FILE}" >&2
402
- exit 1
403
- fi
404
- fi
405
- # Otherwise: existing v9 DB -- fall through to default v9_to_v10.sql.
406
- ;;
407
- 11)
408
- # v10 -> v11: memory.class NOT NULL + CHECK(anchor|thread|log)
409
- # + trg_pcc_history trigger column fix (contract_key->contract_name,
410
- # payload_json->payload). Closes ledger task #6.
411
- #
412
- # Target state fingerprint: memory.class column is NOT NULL.
413
- # We query pragma_table_info and check the notnull flag (column 3 in
414
- # the pragma output: 0=nullable, 1=NOT NULL). A fresh install creates
415
- # memory with NOT NULL class -> notnull=1. An existing v10 DB has
416
- # class as nullable -> notnull=0 -> falls through to the full migration.
417
- # Correct fingerprint because:
418
- # - Fresh install (schema.sql creates memory with NOT NULL class): notnull=1
419
- # - Existing v10 DB (CREATE TABLE IF NOT EXISTS is a no-op): notnull=0
420
- MEMORY_CLASS_NOTNULL="$(sqlite3 "$GAIA_DB" "SELECT \"notnull\" FROM pragma_table_info('memory') WHERE name='class';")"
421
- if [ "$MEMORY_CLASS_NOTNULL" = "1" ]; then
422
- # Fresh install: schema.sql already created memory with NOT NULL class.
423
- # Run the fresh-install variant (no-op SELECT) to stamp the ledger.
424
- OVERRIDE_MIG_FILE="${MIG_DIR}/v10_to_v11_fresh.sql"
425
- if [ ! -f "$OVERRIDE_MIG_FILE" ]; then
426
- echo "[bootstrap] ERROR: v10->v11 fresh-install variant missing at ${OVERRIDE_MIG_FILE}" >&2
427
- exit 1
428
- fi
429
- fi
430
- # Otherwise: existing v10 DB -- fall through to default v10_to_v11.sql.
431
- ;;
432
- 12)
433
- # v11 -> v12: add approvals + approval_events tables + three hash-chain triggers
434
- # (approval-model-redesign M1: user-in-loop, fingerprint-bound, hash-chained).
435
- #
436
- # Target state fingerprint: approvals table exists.
437
- # We probe sqlite_master for the table name.
438
- # This is the correct fingerprint because:
439
- # - Fresh install: schema.sql creates approvals table -> it exists
440
- # - Existing v11 DB: schema.sql's CREATE TABLE IF NOT EXISTS is a no-op
441
- # -> approvals does NOT exist -> falls through to the full migration
442
- # Note: approval_events triggers require the gaia_sha256 scalar function
443
- # to be registered on the connection before any INSERT fires them.
444
- # bootstrap_database.sh uses sqlite3 CLI which does NOT register Python
445
- # functions; the trigger DDL is stored but can only fire via gaia.store.
446
- # The migration SQL itself only defines the DDL (no INSERTs into
447
- # approval_events), so the migration applies cleanly via sqlite3 CLI.
448
- APPROVALS_EXISTS="$(sqlite3 "$GAIA_DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='approvals';")"
449
- if [ "$APPROVALS_EXISTS" = "approvals" ]; then
450
- # Fresh install: schema.sql already created the approvals table.
451
- # Run the fresh-install variant (no-op SELECT) to stamp the ledger.
452
- OVERRIDE_MIG_FILE="${MIG_DIR}/v11_to_v12_fresh.sql"
453
- if [ ! -f "$OVERRIDE_MIG_FILE" ]; then
454
- echo "[bootstrap] ERROR: v11->v12 fresh-install variant missing at ${OVERRIDE_MIG_FILE}" >&2
455
- exit 1
456
- fi
457
- fi
458
- # Otherwise: existing v11 DB -- fall through to default v11_to_v12.sql.
459
- ;;
460
- 13)
461
- # v12 -> v13: add group_name column to projects table
462
- # (gaia-scan-overhaul: workspace->group->repo model, AC-2).
463
- #
464
- # Target state fingerprint: projects.group_name column exists.
465
- # We probe pragma_table_info for the column name.
466
- # This is the correct fingerprint because:
467
- # - Fresh install: schema.sql creates projects WITH group_name -> it exists
468
- # - Existing v12 DB: schema.sql's CREATE TABLE IF NOT EXISTS is a no-op
469
- # -> group_name does NOT exist -> falls through to the full migration
470
- GROUP_NAME_EXISTS="$(sqlite3 "$GAIA_DB" "SELECT name FROM pragma_table_info('projects') WHERE name='group_name';")"
471
- if [ "$GROUP_NAME_EXISTS" = "group_name" ]; then
472
- # Fresh install: schema.sql already created projects with group_name column.
473
- # Run the fresh-install variant (no-op SELECT) to stamp the ledger.
474
- OVERRIDE_MIG_FILE="${MIG_DIR}/v12_to_v13_fresh.sql"
475
- if [ ! -f "$OVERRIDE_MIG_FILE" ]; then
476
- echo "[bootstrap] ERROR: v12->v13 fresh-install variant missing at ${OVERRIDE_MIG_FILE}" >&2
477
- exit 1
478
- fi
479
- fi
480
- # Otherwise: existing v12 DB -- fall through to default v12_to_v13.sql.
481
- ;;
482
- 14)
483
- # v13 -> v14: add path column to projects table
484
- # (gaia-scan-overhaul: findability, project -> path + workspace).
485
- #
486
- # Target state fingerprint: projects.path column exists.
487
- # We probe pragma_table_info for the column name.
488
- # This is the correct fingerprint because:
489
- # - Fresh install: schema.sql creates projects WITH path -> it exists
490
- # - Existing v13 DB: schema.sql's CREATE TABLE IF NOT EXISTS is a no-op
491
- # -> path does NOT exist -> falls through to the full migration
492
- PATH_EXISTS="$(sqlite3 "$GAIA_DB" "SELECT name FROM pragma_table_info('projects') WHERE name='path';")"
493
- if [ "$PATH_EXISTS" = "path" ]; then
494
- # Fresh install: schema.sql already created projects with path column.
495
- # Run the fresh-install variant (no-op SELECT) to stamp the ledger.
496
- OVERRIDE_MIG_FILE="${MIG_DIR}/v13_to_v14_fresh.sql"
497
- if [ ! -f "$OVERRIDE_MIG_FILE" ]; then
498
- echo "[bootstrap] ERROR: v13->v14 fresh-install variant missing at ${OVERRIDE_MIG_FILE}" >&2
499
- exit 1
500
- fi
501
- fi
502
- # Otherwise: existing v13 DB -- fall through to default v13_to_v14.sql.
503
- ;;
504
- 15)
505
- # v14 -> v15: rename the per-project child-table FK column
506
- # repo -> project on apps, libraries, services, features,
507
- # tf_modules, tf_live, releases, workloads, clusters_defined
508
- # (substrate rename catch-up; closes "no such column: project").
509
- #
510
- # Target state fingerprint: apps.project column exists.
511
- # We probe pragma_table_info for the column name on `apps`
512
- # (representative of all nine child tables, which are renamed
513
- # together in the same migration).
514
- # This is the correct fingerprint because:
515
- # - Fresh install: schema.sql creates apps WITH `project` -> it exists
516
- # - Existing v14 DB: schema.sql's CREATE TABLE IF NOT EXISTS is a no-op
517
- # so apps still has the legacy `repo` column -> `project` does
518
- # NOT exist -> falls through to the full rename migration.
519
- APPS_HAS_PROJECT="$(sqlite3 "$GAIA_DB" "SELECT name FROM pragma_table_info('apps') WHERE name='project';")"
520
- if [ "$APPS_HAS_PROJECT" = "project" ]; then
521
- # Fresh install: schema.sql already created child tables with
522
- # the `project` column. Run the fresh-install variant (no-op
523
- # SELECT) to stamp the ledger without attempting the rename.
524
- OVERRIDE_MIG_FILE="${MIG_DIR}/v14_to_v15_fresh.sql"
525
- if [ ! -f "$OVERRIDE_MIG_FILE" ]; then
526
- echo "[bootstrap] ERROR: v14->v15 fresh-install variant missing at ${OVERRIDE_MIG_FILE}" >&2
527
- exit 1
528
- fi
529
- fi
530
- # Otherwise: existing v14 DB -- fall through to default v14_to_v15.sql.
531
- ;;
532
- 16)
533
- # v15 -> v16: add status + missing_since columns to projects table
534
- # (gaia-scan-overhaul: soft-delete support for missing projects).
535
- #
536
- # Target state fingerprint: projects.status column exists.
537
- # We probe pragma_table_info for the column name.
538
- # This is the correct fingerprint because:
539
- # - Fresh install: schema.sql creates projects WITH status -> it exists
540
- # - Existing v15 DB: schema.sql's CREATE TABLE IF NOT EXISTS is a no-op
541
- # -> status does NOT exist -> falls through to the full migration
542
- PROJECTS_HAS_STATUS="$(sqlite3 "$GAIA_DB" "SELECT name FROM pragma_table_info('projects') WHERE name='status';")"
543
- if [ "$PROJECTS_HAS_STATUS" = "status" ]; then
544
- # Fresh install: schema.sql already created projects with status column.
545
- # Run the fresh-install variant (no-op SELECT) to stamp the ledger.
546
- OVERRIDE_MIG_FILE="${MIG_DIR}/v15_to_v16_fresh.sql"
547
- if [ ! -f "$OVERRIDE_MIG_FILE" ]; then
548
- echo "[bootstrap] ERROR: v15->v16 fresh-install variant missing at ${OVERRIDE_MIG_FILE}" >&2
549
- exit 1
550
- fi
551
- fi
552
- # Otherwise: existing v15 DB -- fall through to default v15_to_v16.sql.
553
- ;;
554
- 17)
555
- # v16 -> v17: add status + missing_since columns to workspaces
556
- # table (DEMOTE case: soft-delete support for demoted workspaces
557
- # whose Gaia install footprint disappeared).
558
- #
559
- # Target state fingerprint: workspaces.status column exists.
560
- # We probe pragma_table_info for the column name.
561
- # This is the correct fingerprint because:
562
- # - Fresh install: schema.sql creates workspaces WITH status -> it exists
563
- # - Existing v16 DB: schema.sql's CREATE TABLE IF NOT EXISTS is a no-op
564
- # -> status does NOT exist -> falls through to the full migration
565
- WORKSPACES_HAS_STATUS="$(sqlite3 "$GAIA_DB" "SELECT name FROM pragma_table_info('workspaces') WHERE name='status';")"
566
- if [ "$WORKSPACES_HAS_STATUS" = "status" ]; then
567
- # Fresh install: schema.sql already created workspaces with status column.
568
- # Run the fresh-install variant (no-op SELECT) to stamp the ledger.
569
- OVERRIDE_MIG_FILE="${MIG_DIR}/v16_to_v17_fresh.sql"
570
- if [ ! -f "$OVERRIDE_MIG_FILE" ]; then
571
- echo "[bootstrap] ERROR: v16->v17 fresh-install variant missing at ${OVERRIDE_MIG_FILE}" >&2
572
- exit 1
573
- fi
574
- fi
575
- # Otherwise: existing v16 DB -- fall through to default v16_to_v17.sql.
576
- ;;
577
- 18)
578
- # v17 -> v18: add project_identity column + partial unique index
579
- # to projects (stable, vantage-independent project identity that
580
- # collapses the same physical repo scanned from different roots
581
- # into one row).
582
- #
583
- # Target state fingerprint: projects.project_identity column exists.
584
- # We probe pragma_table_info for the column name.
585
- # This is the correct fingerprint because:
586
- # - Fresh install: schema.sql creates projects WITH project_identity -> it exists
587
- # - Existing v17 DB: schema.sql's CREATE TABLE IF NOT EXISTS is a no-op
588
- # -> project_identity does NOT exist -> falls through to the full migration
589
- PROJECTS_HAS_IDENTITY="$(sqlite3 "$GAIA_DB" "SELECT name FROM pragma_table_info('projects') WHERE name='project_identity';")"
590
- if [ "$PROJECTS_HAS_IDENTITY" = "project_identity" ]; then
591
- # Fresh install: schema.sql already created projects with the
592
- # project_identity column. Run the fresh-install variant
593
- # (no-op SELECT) to stamp the ledger.
594
- OVERRIDE_MIG_FILE="${MIG_DIR}/v17_to_v18_fresh.sql"
595
- if [ ! -f "$OVERRIDE_MIG_FILE" ]; then
596
- echo "[bootstrap] ERROR: v17->v18 fresh-install variant missing at ${OVERRIDE_MIG_FILE}" >&2
597
- exit 1
598
- fi
599
- fi
600
- # Otherwise: existing v17 DB -- fall through to default v17_to_v18.sql.
601
- ;;
602
- *)
603
- # Future migrations: each new N must add a case here with a
604
- # fingerprint of the post-migration state.
605
- echo "[bootstrap] ERROR: no guard probe registered for v${PREV}->v${N}." >&2
606
- echo "[bootstrap] Add a case to Section 3c when introducing migration v${N}." >&2
607
- exit 1
608
- ;;
609
- esac
610
-
611
- # Resolve which file actually runs: per-state override or default.
612
- EFFECTIVE_MIG_FILE="${OVERRIDE_MIG_FILE:-$MIG_FILE}"
613
-
614
- if [ "$ALREADY_AT_TARGET" = "1" ]; then
615
- echo "[bootstrap] migration v${PREV}->v${N}: live DDL already at target (fresh install), stamping ledger only"
616
- sqlite3 "$GAIA_DB" <<EOF
617
- INSERT OR IGNORE INTO schema_version (version, applied_at, description)
618
- VALUES (${N}, '${NOW_UTC}', 'auto-stamped: schema.sql created table at v${N} state');
619
- EOF
620
- else
621
- echo "[bootstrap] migration v${PREV}->v${N}: applying ${EFFECTIVE_MIG_FILE}"
622
- # Wrap the migration in an explicit transaction. The migration SQL
623
- # itself does NOT contain BEGIN/COMMIT so we control atomicity here.
624
- # Errors abort the script via set -e + sqlite3 exit code.
625
- MIG_SQL="$(cat "$EFFECTIVE_MIG_FILE")"
626
- if ! sqlite3 "$GAIA_DB" <<EOF
266
+ # Forward-only: a DB at the FLOOR (or any version below N) is in the
267
+ # source state of this migration, so we apply it directly inside an
268
+ # explicit transaction. No per-version guard probe and no _fresh
269
+ # variant are needed -- the historical "schema.sql already created the
270
+ # target table" case only existed because the baseline was v1 and the
271
+ # whole chain was walked on every fresh install. Under the FLOOR model
272
+ # a fresh install is already at EXPECTED, so it never enters this loop.
273
+ echo "[bootstrap] migration v${PREV}->v${N}: applying ${MIG_FILE}"
274
+ MIG_SQL="$(cat "$MIG_FILE")"
275
+ if ! sqlite3 "$GAIA_DB" <<EOF
627
276
  BEGIN;
628
277
  ${MIG_SQL}
629
278
  COMMIT;
630
279
  EOF
631
- then
632
- echo "[bootstrap] ERROR: migration v${PREV}->v${N} failed. Transaction rolled back." >&2
633
- echo "[bootstrap] schema_version ledger remains at v${CURRENT_VERSION} -- not stamping v${N}." >&2
634
- exit 1
635
- fi
636
- echo "[bootstrap] migration v${PREV}->v${N}: applied successfully"
637
- MIG_DESC="applied migration $(basename "$EFFECTIVE_MIG_FILE")"
638
- sqlite3 "$GAIA_DB" <<EOF
280
+ then
281
+ echo "[bootstrap] ERROR: migration v${PREV}->v${N} failed. Transaction rolled back." >&2
282
+ echo "[bootstrap] schema_version ledger remains at v${CURRENT_VERSION} -- not stamping v${N}." >&2
283
+ exit 1
284
+ fi
285
+ echo "[bootstrap] migration v${PREV}->v${N}: applied successfully"
286
+ MIG_DESC="applied migration $(basename "$MIG_FILE")"
287
+ sqlite3 "$GAIA_DB" <<EOF
639
288
  INSERT OR IGNORE INTO schema_version (version, applied_at, description)
640
289
  VALUES (${N}, '${NOW_UTC}', '${MIG_DESC}');
641
290
  EOF
642
- fi
643
291
  done
644
292
  else
645
293
  echo "[bootstrap] schema_version up-to-date (no migrations pending)"
@@ -831,12 +479,13 @@ else
831
479
  fi
832
480
 
833
481
  # Check 5: schema_version. La tabla se crea en schema.sql y la Section 3b
834
- # inserta la fila (version=1). Verificamos que MAX(version) >= 1.
482
+ # sella la fila baseline en el FLOOR (v${SCHEMA_FLOOR}). Verificamos que
483
+ # MAX(version) >= FLOOR -- por debajo del piso ya habríamos abortado en 3b.
835
484
  SCHEMA_VER="$(sqlite3 "$GAIA_DB" "SELECT COALESCE(MAX(version), 0) FROM schema_version;")"
836
- if [ "$SCHEMA_VER" -ge 1 ]; then
837
- echo "[bootstrap] check: schema_version >= 1 (got ${SCHEMA_VER}) -- PASS"
485
+ if [ "$SCHEMA_VER" -ge "$SCHEMA_FLOOR" ]; then
486
+ echo "[bootstrap] check: schema_version >= floor v${SCHEMA_FLOOR} (got ${SCHEMA_VER}) -- PASS"
838
487
  else
839
- echo "[bootstrap] check: schema_version >= 1 (got ${SCHEMA_VER}) -- FAIL"
488
+ echo "[bootstrap] check: schema_version >= floor v${SCHEMA_FLOOR} (got ${SCHEMA_VER}) -- FAIL"
840
489
  ALL_OK=0
841
490
  fi
842
491