@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +65 -0
- package/INSTALL.md +0 -2
- package/README.md +1 -6
- package/bin/README.md +0 -1
- package/bin/cli/_install_helpers.py +1 -1
- package/bin/cli/cleanup.py +0 -1
- package/bin/cli/doctor.py +2 -2
- package/bin/cli/memory.py +2 -0
- package/bin/cli/update.py +1 -1
- package/bin/pre-publish-validate.js +48 -5
- package/config/README.md +22 -44
- package/config/surface-routing.json +0 -1
- package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-ops/config/README.md +22 -44
- package/dist/gaia-ops/config/surface-routing.json +0 -1
- package/dist/gaia-ops/hooks/modules/agents/handoff_persister.py +2 -0
- package/dist/gaia-ops/hooks/modules/security/approval_grants.py +2 -0
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +2 -0
- package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +90 -55
- package/dist/gaia-ops/skills/README.md +1 -1
- package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +1 -1
- package/dist/gaia-ops/skills/gaia-patterns/reference.md +0 -1
- package/dist/gaia-ops/skills/gaia-release/SKILL.md +60 -24
- package/dist/gaia-ops/skills/gaia-release/reference.md +35 -11
- package/dist/gaia-ops/skills/git-conventions/SKILL.md +6 -2
- package/dist/gaia-ops/skills/orchestrator-present-approval/SKILL.md +10 -2
- package/dist/gaia-ops/skills/readme-writing/SKILL.md +1 -1
- package/dist/gaia-ops/skills/readme-writing/reference.md +0 -1
- package/dist/gaia-ops/tools/scan/ui.py +20 -4
- package/dist/gaia-ops/tools/scan/verify.py +3 -3
- package/dist/gaia-ops/tools/validation/README.md +15 -24
- package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-security/hooks/modules/agents/handoff_persister.py +2 -0
- package/dist/gaia-security/hooks/modules/security/approval_grants.py +2 -0
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +2 -0
- package/dist/gaia-security/hooks/modules/validation/commit_validator.py +90 -55
- package/hooks/modules/agents/handoff_persister.py +2 -0
- package/hooks/modules/security/approval_grants.py +2 -0
- package/hooks/modules/tools/bash_validator.py +2 -0
- package/hooks/modules/validation/commit_validator.py +90 -55
- package/index.js +2 -12
- package/package.json +4 -6
- package/pyproject.toml +3 -3
- package/scripts/bootstrap_database.sh +88 -439
- package/scripts/check_schema_drift.py +208 -0
- package/scripts/migrations/README.md +78 -28
- package/scripts/migrations/schema.checksum +8 -0
- package/scripts/release-prepare.mjs +199 -0
- package/skills/README.md +1 -1
- package/skills/gaia-patterns/SKILL.md +1 -1
- package/skills/gaia-patterns/reference.md +0 -1
- package/skills/gaia-release/SKILL.md +60 -24
- package/skills/gaia-release/reference.md +35 -11
- package/skills/git-conventions/SKILL.md +6 -2
- package/skills/orchestrator-present-approval/SKILL.md +10 -2
- package/skills/readme-writing/SKILL.md +1 -1
- package/skills/readme-writing/reference.md +0 -1
- package/tools/scan/ui.py +20 -4
- package/tools/scan/verify.py +3 -3
- package/tools/validation/README.md +15 -24
- package/commands/README.md +0 -64
- package/commands/gaia.md +0 -37
- package/commands/scan-project.md +0 -74
- package/config/crons-schema.md +0 -81
- package/config/git_standards.json +0 -72
- package/dist/gaia-ops/commands/gaia.md +0 -37
- package/dist/gaia-ops/config/crons-schema.md +0 -81
- package/dist/gaia-ops/config/git_standards.json +0 -72
- package/dist/gaia-ops/tools/agentic-loop/decide-status.py +0 -210
- package/dist/gaia-ops/tools/agentic-loop/parse-metric.py +0 -106
- package/dist/gaia-ops/tools/agentic-loop/record-iteration.py +0 -223
- package/git-hooks/commit-msg +0 -41
- package/scripts/migrations/v10_to_v11.sql +0 -170
- package/scripts/migrations/v10_to_v11_fresh.sql +0 -18
- package/scripts/migrations/v11_to_v12.sql +0 -195
- package/scripts/migrations/v11_to_v12_fresh.sql +0 -19
- package/scripts/migrations/v12_to_v13.sql +0 -48
- package/scripts/migrations/v12_to_v13_fresh.sql +0 -17
- package/scripts/migrations/v13_to_v14.sql +0 -44
- package/scripts/migrations/v13_to_v14_fresh.sql +0 -17
- package/scripts/migrations/v14_to_v15.sql +0 -71
- package/scripts/migrations/v14_to_v15_fresh.sql +0 -19
- package/scripts/migrations/v15_to_v16.sql +0 -57
- package/scripts/migrations/v15_to_v16_fresh.sql +0 -18
- package/scripts/migrations/v16_to_v17.sql +0 -51
- package/scripts/migrations/v16_to_v17_fresh.sql +0 -18
- package/scripts/migrations/v17_to_v18.sql +0 -66
- package/scripts/migrations/v17_to_v18_fresh.sql +0 -24
- package/scripts/migrations/v1_to_v2.sql +0 -97
- package/scripts/migrations/v2_to_v3.sql +0 -68
- package/scripts/migrations/v2_to_v3_merge.sql +0 -69
- package/scripts/migrations/v3_to_v4.sql +0 -67
- package/scripts/migrations/v3_to_v4_fresh.sql +0 -20
- package/scripts/migrations/v4_to_v5.sql +0 -55
- package/scripts/migrations/v4_to_v5_fresh.sql +0 -20
- package/scripts/migrations/v5_to_v6.sql +0 -48
- package/scripts/migrations/v5_to_v6_fresh.sql +0 -17
- package/scripts/migrations/v6_to_v7.sql +0 -26
- package/scripts/migrations/v6_to_v7_fresh.sql +0 -13
- package/scripts/migrations/v7_to_v8.sql +0 -44
- package/scripts/migrations/v7_to_v8_fresh.sql +0 -14
- package/scripts/migrations/v8_to_v9.sql +0 -87
- package/scripts/migrations/v8_to_v9_fresh.sql +0 -15
- package/scripts/migrations/v9_to_v10.sql +0 -109
- package/scripts/migrations/v9_to_v10_episodes_workspace.sql +0 -109
- package/scripts/migrations/v9_to_v10_fresh.sql +0 -18
- package/templates/README.md +0 -70
- package/templates/managed-settings.template.json +0 -43
- package/tools/agentic-loop/decide-status.py +0 -210
- package/tools/agentic-loop/parse-metric.py +0 -106
- 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
|
-
##
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 >=
|
|
78
|
+
assert schema_version >= 18
|
|
28
79
|
|
|
29
80
|
# Wrong -- breaks every time a new migration lands
|
|
30
|
-
assert schema_version ==
|
|
81
|
+
assert schema_version == 18
|
|
31
82
|
```
|
|
32
83
|
|
|
33
|
-
A floor assertion preserves test intent
|
|
34
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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:
|
|
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
|
|
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
|
|