@jaguilar87/gaia 5.0.4 → 5.0.6

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 +65 -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 +2 -2
  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
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env python3
2
+ """Build/pre-publish schema-drift guard.
3
+
4
+ Fails (non-zero) when gaia/store/schema.sql has changed but the schema version
5
+ was not bumped and no new migration was added for it. This catches at build
6
+ time the drift that `gaia doctor` only flags at runtime as a warning.
7
+
8
+ How it works
9
+ ------------
10
+ A committed fingerprint file (scripts/migrations/schema.checksum) pins the
11
+ sha256 of schema.sql to the EXPECTED_SCHEMA_VERSION it corresponds to:
12
+
13
+ version=18
14
+ sha256=<hex digest of gaia/store/schema.sql>
15
+
16
+ The guard reads EXPECTED_SCHEMA_VERSION from bin/cli/doctor.py and the live
17
+ sha256 of schema.sql, then:
18
+
19
+ 1. No fingerprint file yet (first run): record the current
20
+ (version, sha256) and pass. This is how the baseline for the current
21
+ floor (v18) is established.
22
+
23
+ 2. Recorded version == EXPECTED but sha256 differs: schema.sql changed
24
+ without a version bump. FAIL with a message telling the dev to bump
25
+ EXPECTED_SCHEMA_VERSION in doctor.py and add a migration file.
26
+
27
+ 3. Recorded version < EXPECTED (a bump happened): verify the corresponding
28
+ forward migration file scripts/migrations/v{EXPECTED-1}_to_v{EXPECTED}.sql
29
+ exists, then re-record the new (version, sha256). FAIL if the migration
30
+ file is missing.
31
+
32
+ 4. Recorded version == EXPECTED and sha256 matches: no drift, pass.
33
+
34
+ 5. Recorded version > EXPECTED: the fingerprint is ahead of the CLI -- this
35
+ is a misconfiguration (someone lowered EXPECTED). FAIL.
36
+
37
+ Usage:
38
+ python3 scripts/check_schema_drift.py # check (+ record on first run / after bump)
39
+ python3 scripts/check_schema_drift.py --record # force re-record for current EXPECTED (rare)
40
+
41
+ Exit codes:
42
+ 0 no drift (or baseline/bump recorded)
43
+ 1 drift detected, or missing migration after a bump, or misconfiguration
44
+ 2 internal error (file not found, parse failure)
45
+ """
46
+
47
+ from __future__ import annotations
48
+
49
+ import hashlib
50
+ import re
51
+ import sys
52
+ from pathlib import Path
53
+
54
+ _REPO_ROOT = Path(__file__).resolve().parents[1]
55
+ _SCHEMA_SQL = _REPO_ROOT / "gaia" / "store" / "schema.sql"
56
+ _DOCTOR_PY = _REPO_ROOT / "bin" / "cli" / "doctor.py"
57
+ _MIGRATIONS_DIR = _REPO_ROOT / "scripts" / "migrations"
58
+ _CHECKSUM_FILE = _MIGRATIONS_DIR / "schema.checksum"
59
+
60
+
61
+ def _fail(msg: str) -> None:
62
+ print(f"[schema-drift] FAIL: {msg}", file=sys.stderr)
63
+ sys.exit(1)
64
+
65
+
66
+ def _err(msg: str) -> None:
67
+ print(f"[schema-drift] ERROR: {msg}", file=sys.stderr)
68
+ sys.exit(2)
69
+
70
+
71
+ def _read_expected_version() -> int:
72
+ if not _DOCTOR_PY.is_file():
73
+ _err(f"doctor.py not found at {_DOCTOR_PY}")
74
+ text = _DOCTOR_PY.read_text()
75
+ m = re.search(r"^EXPECTED_SCHEMA_VERSION\s*=\s*(\d+)\s*$", text, re.MULTILINE)
76
+ if m is None:
77
+ _err(f"could not parse EXPECTED_SCHEMA_VERSION from {_DOCTOR_PY}")
78
+ return int(m.group(1))
79
+
80
+
81
+ def _schema_sha256() -> str:
82
+ if not _SCHEMA_SQL.is_file():
83
+ _err(f"schema.sql not found at {_SCHEMA_SQL}")
84
+ return hashlib.sha256(_SCHEMA_SQL.read_bytes()).hexdigest()
85
+
86
+
87
+ def _read_checksum_file() -> tuple[int, str] | None:
88
+ if not _CHECKSUM_FILE.is_file():
89
+ return None
90
+ version: int | None = None
91
+ sha: str | None = None
92
+ for raw in _CHECKSUM_FILE.read_text().splitlines():
93
+ line = raw.strip()
94
+ if not line or line.startswith("#"):
95
+ continue
96
+ if "=" not in line:
97
+ continue
98
+ key, _, val = line.partition("=")
99
+ key, val = key.strip(), val.strip()
100
+ if key == "version":
101
+ try:
102
+ version = int(val)
103
+ except ValueError:
104
+ _err(f"malformed version in {_CHECKSUM_FILE}: {val!r}")
105
+ elif key == "sha256":
106
+ sha = val
107
+ if version is None or sha is None:
108
+ _err(f"{_CHECKSUM_FILE} is missing version= or sha256=")
109
+ return version, sha
110
+
111
+
112
+ def _write_checksum_file(version: int, sha: str) -> None:
113
+ content = (
114
+ "# Schema fingerprint for the build/pre-publish drift guard.\n"
115
+ "# Generated and verified by scripts/check_schema_drift.py.\n"
116
+ "# Pins the sha256 of gaia/store/schema.sql to the schema version it\n"
117
+ "# corresponds to (EXPECTED_SCHEMA_VERSION in bin/cli/doctor.py).\n"
118
+ "# Do NOT edit by hand: bump EXPECTED_SCHEMA_VERSION + add a migration,\n"
119
+ "# then re-run the guard to refresh this file.\n"
120
+ f"version={version}\n"
121
+ f"sha256={sha}\n"
122
+ )
123
+ _CHECKSUM_FILE.write_text(content)
124
+
125
+
126
+ def _migration_for(version: int) -> Path:
127
+ return _MIGRATIONS_DIR / f"v{version - 1}_to_v{version}.sql"
128
+
129
+
130
+ def main() -> None:
131
+ force_record = "--record" in sys.argv[1:]
132
+
133
+ expected = _read_expected_version()
134
+ live_sha = _schema_sha256()
135
+ recorded = _read_checksum_file()
136
+
137
+ if force_record:
138
+ _write_checksum_file(expected, live_sha)
139
+ print(
140
+ f"[schema-drift] recorded fingerprint for v{expected} "
141
+ f"(sha256={live_sha[:12]}...) [--record]"
142
+ )
143
+ sys.exit(0)
144
+
145
+ # Case 1: first run -- establish the baseline for the current version.
146
+ if recorded is None:
147
+ _write_checksum_file(expected, live_sha)
148
+ print(
149
+ f"[schema-drift] no fingerprint on record; baseline recorded for "
150
+ f"v{expected} (sha256={live_sha[:12]}...)"
151
+ )
152
+ sys.exit(0)
153
+
154
+ rec_version, rec_sha = recorded
155
+
156
+ # Case 5: recorded ahead of the CLI -- misconfiguration.
157
+ if rec_version > expected:
158
+ _fail(
159
+ f"recorded schema fingerprint is for v{rec_version} but "
160
+ f"EXPECTED_SCHEMA_VERSION is v{expected} (lower). The CLI cannot "
161
+ f"expect a version older than the recorded fingerprint. Restore "
162
+ f"EXPECTED_SCHEMA_VERSION to >= v{rec_version}."
163
+ )
164
+
165
+ # Case 3: a version bump happened -- require the migration, then re-record.
166
+ if rec_version < expected:
167
+ mig = _migration_for(expected)
168
+ if not mig.is_file():
169
+ _fail(
170
+ f"EXPECTED_SCHEMA_VERSION was bumped to v{expected} "
171
+ f"(fingerprint was for v{rec_version}), but the forward "
172
+ f"migration {mig.relative_to(_REPO_ROOT)} is missing. Add the "
173
+ f"migration file in the same commit as the version bump."
174
+ )
175
+ _write_checksum_file(expected, live_sha)
176
+ print(
177
+ f"[schema-drift] version bump v{rec_version} -> v{expected} "
178
+ f"detected; migration {mig.name} present; fingerprint re-recorded "
179
+ f"(sha256={live_sha[:12]}...)"
180
+ )
181
+ sys.exit(0)
182
+
183
+ # rec_version == expected from here on.
184
+
185
+ # Case 4: fingerprint matches -- no drift.
186
+ if rec_sha == live_sha:
187
+ print(
188
+ f"[schema-drift] OK: schema.sql matches the recorded fingerprint "
189
+ f"for v{expected}"
190
+ )
191
+ sys.exit(0)
192
+
193
+ # Case 2: schema.sql changed without a version bump -- DRIFT.
194
+ _fail(
195
+ f"gaia/store/schema.sql changed but EXPECTED_SCHEMA_VERSION is still "
196
+ f"v{expected} and no new migration was added.\n"
197
+ f" recorded sha256: {rec_sha}\n"
198
+ f" current sha256: {live_sha}\n"
199
+ f" To fix: bump EXPECTED_SCHEMA_VERSION in bin/cli/doctor.py "
200
+ f"to v{expected + 1}, add scripts/migrations/v{expected}_to_v{expected + 1}.sql, "
201
+ f"then re-run this guard to refresh scripts/migrations/schema.checksum.\n"
202
+ f" (If the schema.sql edit is genuinely a no-op change that "
203
+ f"needs no migration, re-run with --record to re-pin the fingerprint.)"
204
+ )
205
+
206
+
207
+ if __name__ == "__main__":
208
+ main()
@@ -5,16 +5,67 @@ adding any new migration file.
5
5
 
6
6
  ---
7
7
 
8
- ## 1. One feature per migration version
8
+ ## 0. The schema floor (baseline = current version)
9
+
10
+ Gaia is a single-user personal tool. Nobody upgrades a database older than the
11
+ current version, and fresh installs build the schema directly from
12
+ `gaia/store/schema.sql`. The full historical `v1 -> v17` migration chain was
13
+ therefore collapsed into a **schema floor**: the lowest schema version that is
14
+ supported for in-place use.
15
+
16
+ The floor is **v18**. It is declared in three places that must agree:
17
+
18
+ | Location | What it holds |
19
+ |----------|---------------|
20
+ | `gaia/store/schema.sql` | Produces the v18 shape directly (fresh installs land here). |
21
+ | `scripts/bootstrap_database.sh` Section 3b (`SCHEMA_FLOOR=18`) | Seeds/stamps the ledger at the floor; rejects DBs below it. |
22
+ | `bin/cli/doctor.py` (`EXPECTED_SCHEMA_VERSION`) | The version the CLI expects; equals the floor until a forward migration is added. |
23
+
24
+ How bootstrap treats each case:
25
+
26
+ * **Fresh install** (no `schema_version` rows): `schema.sql` already produced
27
+ the floor shape, so bootstrap stamps `(version=18, ...)` directly. It does
28
+ **not** seed v1 and walk the chain.
29
+ * **DB at or above the floor** (the common case, e.g. `~/.gaia/gaia.db`): no
30
+ migration needed. Section 3c only runs if a forward migration exists.
31
+ * **DB below the floor** (`1 <= version < 18`): **no longer supported** for
32
+ in-place upgrade. Bootstrap aborts with a clear message asking you to
33
+ recreate the DB (back up, delete `~/.gaia/gaia.db`, re-run `gaia install`).
34
+
35
+ There are no `_fresh` / `_merge` variants under the floor model. Those existed
36
+ only because the old baseline was v1 and the whole chain was walked on every
37
+ fresh install. With the floor, a fresh install is already at the expected
38
+ version after `schema.sql`, so the migration loop is skipped entirely.
9
39
 
10
- Each independent feature that introduces new DDL gets its own migration version.
11
- Do NOT extend a version that has already been stamped. The migration ledger is
12
- monotonic: once a version is written to `schema_migrations`, its content is
13
- frozen and `bootstrap_database.sh` will not re-run it.
40
+ ---
41
+
42
+ ## 1. Adding a future migration (one file per bump, forward-only)
43
+
44
+ Going forward the convention is **forward-only, one migration file per
45
+ version bump**. To raise the schema from the current floor (or any later
46
+ version) to `N`:
47
+
48
+ 1. Add the new DDL to `gaia/store/schema.sql` so fresh installs land in the
49
+ target shape.
50
+ 2. Create exactly one `scripts/migrations/v{N-1}_to_v{N}.sql` containing the
51
+ full DDL delta applied to a DB at version `N-1`.
52
+ 3. Bump `EXPECTED_SCHEMA_VERSION` to `N` in `bin/cli/doctor.py` **in the same
53
+ commit**.
14
54
 
15
- If you have two unrelated features ready at the same time, assign them
16
- consecutive versions (e.g., v6 and v7). Do not bundle them into v6 to save a
17
- number -- bundling violates atomicity and makes rollback reasoning impossible.
55
+ `bootstrap_database.sh` Section 3c then applies `v{N-1}_to_v{N}.sql` inside a
56
+ single `BEGIN/COMMIT` transaction for any DB behind `N`, and stamps the ledger
57
+ only on success. A fresh install is already at `N` after `schema.sql`, so it
58
+ never enters the loop -- no `_fresh` variant is required.
59
+
60
+ `tests/cli/test_schema_version_lockstep.py` enforces that
61
+ `EXPECTED_SCHEMA_VERSION` equals the floor when no forward migrations exist,
62
+ and equals the highest migration target once they do.
63
+
64
+ Each independent feature that introduces new DDL gets its own migration
65
+ version. Do NOT extend a version that has already been stamped: the ledger is
66
+ monotonic, and `bootstrap_database.sh` will not re-run a frozen version. Two
67
+ unrelated features ready at once get consecutive versions (e.g. v19 and v20),
68
+ never bundled.
18
69
 
19
70
  ---
20
71
 
@@ -24,14 +75,14 @@ Tests that assert the schema version must use a floor check, not a point check:
24
75
 
25
76
  ```python
26
77
  # Correct -- survives future bumps without re-editing this test
27
- assert schema_version >= 5
78
+ assert schema_version >= 18
28
79
 
29
80
  # Wrong -- breaks every time a new migration lands
30
- assert schema_version == 5
81
+ assert schema_version == 18
31
82
  ```
32
83
 
33
- A floor assertion preserves test intent (the feature that introduced v5 is
34
- present) without becoming a maintenance burden as the ledger grows.
84
+ A floor assertion preserves test intent without becoming a maintenance burden
85
+ as the ledger grows.
35
86
 
36
87
  ---
37
88
 
@@ -39,25 +90,24 @@ present) without becoming a maintenance burden as the ledger grows.
39
90
 
40
91
  | Pattern | When to use |
41
92
  |---------|-------------|
42
- | `vN_to_vN+1.sql` | Applied to an existing DB at version N. Contains the full DDL delta. |
43
- | `vN_to_vN+1_fresh.sql` | Applied after a clean install where `schema.sql` already contains the target state. Usually carries only indexes and constraints that `schema.sql` cannot safely declare for older DBs. |
93
+ | `vN_to_vN+1.sql` | Applied to an existing DB at version N. Contains the full DDL delta, applied inside a `BEGIN/COMMIT` transaction by bootstrap Section 3c. |
44
94
 
45
- Both files are driven by `bootstrap_database.sh`. The `_fresh` variant runs
46
- when the bootstrap detects that the target objects already exist (Section 3c
47
- case logic). It must be idempotent (`CREATE INDEX IF NOT EXISTS`, etc.).
95
+ The historical `_fresh` and `_merge` variants are no longer used: under the
96
+ floor model a fresh install is already at the expected version after
97
+ `schema.sql`, so it never runs a migration script.
48
98
 
49
99
  ---
50
100
 
51
- ## 4. Lessons from Plan B closure
52
-
53
- Plan B originally added the `evidence` table DDL into `v4_to_v5.sql` after
54
- that version had already been stamped and executed on existing installs.
55
- Bootstrap skipped the modified file because the ledger row for v4->v5 was
56
- already present, so the new DDL never ran.
101
+ ## 4. Why the floor (lesson from the collapsed chain)
57
102
 
58
- The fix was to extract evidence DDL into its own `v5_to_v6.sql` (and a
59
- matching `v5_to_v6_fresh.sql` for clean installs). This is the canonical
60
- example of the one-feature-per-version rule in practice.
103
+ The pre-floor design seeded `(version=1)` then walked `v1 -> v2 -> ... -> v18`
104
+ on every fresh install, each step guarded by a per-version "is the live DDL
105
+ already at target?" probe in `bootstrap_database.sh`. That machinery existed
106
+ solely to make a fresh install (which `schema.sql` had already built to the
107
+ latest shape) walk the chain without re-running destructive DDL.
61
108
 
62
- See `v5_to_v6.sql` and `v5_to_v6_fresh.sql` in this directory for the
63
- resulting migration files.
109
+ Since fresh installs build straight from `schema.sql` and no one runs a DB
110
+ older than the current version, the entire chain plus its guard probes were
111
+ dead weight. Collapsing to a floor removes ~35 migration files and the
112
+ per-version `case` block, leaving a single forward-only loop for genuine future
113
+ bumps.
@@ -0,0 +1,8 @@
1
+ # Schema fingerprint for the build/pre-publish drift guard.
2
+ # Generated and verified by scripts/check_schema_drift.py.
3
+ # Pins the sha256 of gaia/store/schema.sql to the schema version it
4
+ # corresponds to (EXPECTED_SCHEMA_VERSION in bin/cli/doctor.py).
5
+ # Do NOT edit by hand: bump EXPECTED_SCHEMA_VERSION + add a migration,
6
+ # then re-run the guard to refresh this file.
7
+ version=18
8
+ sha256=6f728de0625d5011b86eaf536c21785d19cfa08592ecaf086ab46f9b0d0ebda0
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * release-prepare -- atomically bump every version source, rebuild dist/, and
4
+ * validate, in one command. Invoked by the gaia-release skill's "release" flow
5
+ * (step b), NOT run by hand: the whole point is that no human has to remember
6
+ * the five files that must agree or the order they move in.
7
+ *
8
+ * The saga this prevents: bumping package.json but forgetting pyproject.toml
9
+ * (which then ships stale and fails CI's validate-manifests leg AFTER the tag
10
+ * is pushed -- a 5.0.3 -> 5.0.4 re-release). pre-publish:validate is the gate
11
+ * that catches drift; this script makes drift impossible to introduce by hand
12
+ * by writing all sources from a single target version, then running that same
13
+ * gate locally so a drift fails here, loudly, before any tag exists.
14
+ *
15
+ * Steps (atomic -- a failure leaves the working tree for inspection, never
16
+ * half-published):
17
+ * 1. Bump ALL version sources to <version>:
18
+ * - package.json
19
+ * - pyproject.toml ([project].version)
20
+ * - .claude-plugin/plugin.json
21
+ * - .claude-plugin/marketplace.json (every plugin entry)
22
+ * - CHANGELOG.md (top versioned header; inserts a stub if absent)
23
+ * 2. npm run build:plugins (regenerates dist/, including the per-plugin
24
+ * manifests that carry the version)
25
+ * 3. npm run pre-publish:validate (the drift gate -- fails loud on any
26
+ * source that did not move)
27
+ *
28
+ * Idempotent: re-running with the same version is a no-op bump (sources already
29
+ * agree) and re-validates. Usage:
30
+ * node scripts/release-prepare.mjs <version>
31
+ * npm run release:prepare <version>
32
+ *
33
+ * <version> is a bare semver, e.g. 5.0.5 or 5.1.0-rc.1 (no leading "v").
34
+ */
35
+
36
+ import fs from 'fs';
37
+ import path from 'path';
38
+ import { execSync } from 'child_process';
39
+ import { fileURLToPath } from 'url';
40
+ import chalk from 'chalk';
41
+
42
+ const __filename = fileURLToPath(import.meta.url);
43
+ const REPO_ROOT = path.resolve(path.dirname(__filename), '..');
44
+
45
+ const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
46
+
47
+ function log(msg, level = 'info') {
48
+ const ts = new Date().toLocaleTimeString();
49
+ const p = `[${ts}]`;
50
+ switch (level) {
51
+ case 'error': console.error(chalk.red(`${p} ✗ ${msg}`)); break;
52
+ case 'success': console.log(chalk.green(`${p} ✓ ${msg}`)); break;
53
+ case 'warning': console.warn(chalk.yellow(`${p} ⚠️ ${msg}`)); break;
54
+ case 'step': console.log(chalk.bold.cyan(`\n${p} ${msg}`)); break;
55
+ default: console.log(chalk.blue(`${p} ℹ️ ${msg}`));
56
+ }
57
+ }
58
+
59
+ function fail(msg) {
60
+ log(msg, 'error');
61
+ process.exit(1);
62
+ }
63
+
64
+ function readText(rel) {
65
+ return fs.readFileSync(path.join(REPO_ROOT, rel), 'utf-8');
66
+ }
67
+
68
+ function writeText(rel, content) {
69
+ fs.writeFileSync(path.join(REPO_ROOT, rel), content);
70
+ }
71
+
72
+ function exists(rel) {
73
+ return fs.existsSync(path.join(REPO_ROOT, rel));
74
+ }
75
+
76
+ // --- version-source bumpers ------------------------------------------------
77
+ // Each returns a short status string describing what it did, or throws.
78
+
79
+ function bumpJsonVersionField(rel, version) {
80
+ const data = JSON.parse(readText(rel));
81
+ const before = data.version;
82
+ data.version = version;
83
+ // Preserve npm's 2-space style + trailing newline (matches pre-publish-validate.js).
84
+ writeText(rel, JSON.stringify(data, null, 2) + '\n');
85
+ return `${rel}: ${before} -> ${version}`;
86
+ }
87
+
88
+ function bumpMarketplace(rel, version) {
89
+ const data = JSON.parse(readText(rel));
90
+ const plugins = data.plugins || [];
91
+ if (plugins.length === 0) throw new Error(`${rel}: no plugins[] to bump`);
92
+ const befores = plugins.map((p) => `${p.name}=${p.version}`);
93
+ for (const plugin of plugins) plugin.version = version;
94
+ writeText(rel, JSON.stringify(data, null, 2) + '\n');
95
+ return `${rel}: [${befores.join(', ')}] -> all ${version}`;
96
+ }
97
+
98
+ function bumpPyproject(rel, version) {
99
+ const text = readText(rel);
100
+ // Bump only the version line inside the [project] table, not [tool.*] tables.
101
+ const projectMatch = text.match(/(\[project\][\s\S]*?)(?=\n\[|$)/);
102
+ if (!projectMatch) throw new Error(`${rel}: [project] section not found`);
103
+ const block = projectMatch[1];
104
+ const verLine = block.match(/^(\s*version\s*=\s*)["']([^"']+)["']/m);
105
+ if (!verLine) throw new Error(`${rel}: [project].version not found`);
106
+ const before = verLine[2];
107
+ const newBlock = block.replace(
108
+ /^(\s*version\s*=\s*)["'][^"']+["']/m,
109
+ `$1"${version}"`,
110
+ );
111
+ writeText(rel, text.replace(block, newBlock));
112
+ return `${rel}: ${before} -> ${version}`;
113
+ }
114
+
115
+ function bumpChangelog(rel, version) {
116
+ const text = readText(rel);
117
+ // Find the first real versioned header (skip "## [Unreleased]").
118
+ const headerRe = /^##\s*\[([^\]]+)\](.*)$/gm;
119
+ let m;
120
+ while ((m = headerRe.exec(text)) !== null) {
121
+ if (m[1].trim().toLowerCase() === 'unreleased') continue;
122
+ if (m[1].trim() === version) {
123
+ return `${rel}: top header already [${version}] (no change)`;
124
+ }
125
+ // Insert a new dated stub entry above the current top version, right after
126
+ // the "## [Unreleased]" line if present, else above the first version header.
127
+ const today = new Date().toISOString().slice(0, 10);
128
+ const stub = `## [${version}] - ${today}\n\n`;
129
+ const insertAt = m.index;
130
+ const updated = text.slice(0, insertAt) + stub + text.slice(insertAt);
131
+ writeText(rel, updated);
132
+ return `${rel}: inserted stub [${version}] above [${m[1].trim()}] ` +
133
+ `(EDIT the body before release)`;
134
+ }
135
+ throw new Error(`${rel}: no versioned header found to anchor the new entry`);
136
+ }
137
+
138
+ // --- main ------------------------------------------------------------------
139
+
140
+ function run(cmd) {
141
+ log(`Running: ${cmd}`, 'info');
142
+ execSync(cmd, { cwd: REPO_ROOT, stdio: 'inherit' });
143
+ }
144
+
145
+ function main() {
146
+ const version = process.argv[2];
147
+ if (!version) {
148
+ fail('Usage: node scripts/release-prepare.mjs <version> (e.g. 5.0.5 or 5.1.0-rc.1)');
149
+ }
150
+ if (version.startsWith('v')) {
151
+ fail(`Pass a bare semver without the leading "v" (got "${version}"). The tag adds the v; the sources do not carry it.`);
152
+ }
153
+ if (!SEMVER_RE.test(version)) {
154
+ fail(`"${version}" is not a valid semver. Expected MAJOR.MINOR.PATCH with optional -prerelease.`);
155
+ }
156
+
157
+ log(`Target version: ${version}`, 'step');
158
+
159
+ // Step 1 -- atomic bump of every version source.
160
+ log('Step 1: Bumping all version sources atomically...', 'step');
161
+ const results = [];
162
+ try {
163
+ results.push(bumpJsonVersionField('package.json', version));
164
+ results.push(bumpPyproject('pyproject.toml', version));
165
+ if (exists('.claude-plugin/plugin.json')) {
166
+ results.push(bumpJsonVersionField('.claude-plugin/plugin.json', version));
167
+ }
168
+ if (exists('.claude-plugin/marketplace.json')) {
169
+ results.push(bumpMarketplace('.claude-plugin/marketplace.json', version));
170
+ }
171
+ results.push(bumpChangelog('CHANGELOG.md', version));
172
+ } catch (err) {
173
+ fail(`Version bump failed (working tree left for inspection): ${err.message}`);
174
+ }
175
+ for (const r of results) log(` ${r}`, 'success');
176
+
177
+ // Step 2 -- rebuild dist/ so the per-plugin manifests carry the new version.
178
+ log('Step 2: Rebuilding plugins (npm run build:plugins)...', 'step');
179
+ try {
180
+ run('npm run build:plugins');
181
+ } catch {
182
+ fail('build:plugins failed -- dist/ is not regenerated. Fix the build, then re-run release:prepare.');
183
+ }
184
+ log('dist/ regenerated', 'success');
185
+
186
+ // Step 3 -- the drift gate. Fails loud if any source did not move.
187
+ log('Step 3: Validating version sync (npm run pre-publish:validate)...', 'step');
188
+ try {
189
+ run('npm run pre-publish:validate');
190
+ } catch {
191
+ fail('pre-publish:validate FAILED -- version drift or a manifest problem remains. ' +
192
+ 'This is the gate that protects the release; do NOT tag until it is green.');
193
+ }
194
+
195
+ log(`release:prepare complete -- all sources at ${version}, dist/ rebuilt, validation green.`, 'success');
196
+ log('Next (driven by the gaia-release "release" flow, not by hand): pre-flight (Python 3.11/3.12 + tests), commit, tag, push, gh release.', 'info');
197
+ }
198
+
199
+ main();
package/skills/README.md CHANGED
@@ -64,7 +64,7 @@ skills/
64
64
  ├── gaia-patterns/ # Gaia component patterns: hooks, agents, routing, CLI
65
65
  │ └── reference.md
66
66
  ├── gaia-planner/ # Feature planning, briefs, task decomposition
67
- ├── gaia-release/ # Gaia release pipeline: live, dry-run, beta, stable
67
+ ├── gaia-release/ # Gaia release pipeline: install local, dry-run, release
68
68
  ├── gaia-audit/ # Audit one component (agent or skill) against its standard + live implementation
69
69
  ├── gaia-verify/ # Verify a Gaia installation across delivery surfaces
70
70
  ├── git-conventions/ # Conventional Commits (on-demand workflow skill)
@@ -91,7 +91,7 @@ When you modify any Gaia component (hook, skill, agent definition, routing confi
91
91
  - Changed `_is_protected()` paths in `adapters/claude_code.py` → check `security-tiers/SKILL.md` for path documentation
92
92
  - Added a new agent definition → check `gaia-patterns/reference.md` for agents table
93
93
  - Modified hook enforcement logic → check `security-tiers` and `agent-protocol` references
94
- - When adding or modifying files in agents/, skills/, hooks/, commands/, config/, bin/, tests/, build/, templates/ or the repo root, load Skill('readme-writing') to update the relevant README.md
94
+ - When adding or modifying files in agents/, skills/, hooks/, commands/, config/, bin/, tests/, build/ or the repo root, load Skill('readme-writing') to update the relevant README.md
95
95
 
96
96
  **Format:** In `cross_layer_impacts`, list the doc file and the behavior change, e.g.:
97
97
  ```
@@ -130,7 +130,6 @@ The package ships a single `gaia` binary (`bin/gaia.js`) that dispatches to Pyth
130
130
  |------|---------|
131
131
  | `config/context-contracts.json` | Seeding source for per-agent context contracts (applied to gaia.db on install; runtime SSOT is DB) |
132
132
  | `config/surface-routing.json` | Surface routing table (intent to agent mapping) |
133
- | `config/git_standards.json` | Git commit and branch standards |
134
133
  | `config/cloud/aws.json` | AWS service patterns and commands |
135
134
  | `config/cloud/gcp.json` | GCP service patterns and commands |
136
135