@jaguilar87/gaia 5.0.2 → 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 (154) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/ARCHITECTURE.md +0 -1
  4. package/CHANGELOG.md +110 -0
  5. package/INSTALL.md +0 -2
  6. package/README.md +1 -6
  7. package/bin/README.md +0 -1
  8. package/bin/cli/_install_helpers.py +1 -1
  9. package/bin/cli/approvals.py +23 -21
  10. package/bin/cli/cleanup.py +0 -1
  11. package/bin/cli/doctor.py +1 -1
  12. package/bin/cli/memory.py +2 -0
  13. package/bin/cli/update.py +1 -1
  14. package/bin/pre-publish-validate.js +48 -5
  15. package/config/README.md +22 -44
  16. package/config/surface-routing.json +0 -2
  17. package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
  18. package/dist/gaia-ops/config/README.md +22 -44
  19. package/dist/gaia-ops/config/surface-routing.json +0 -2
  20. package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +18 -0
  21. package/dist/gaia-ops/hooks/modules/agents/handoff_persister.py +214 -2
  22. package/dist/gaia-ops/hooks/modules/agents/response_contract.py +26 -0
  23. package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +15 -0
  24. package/dist/gaia-ops/hooks/modules/security/__init__.py +0 -5
  25. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +124 -19
  26. package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +99 -7
  27. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +127 -24
  28. package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +90 -55
  29. package/dist/gaia-ops/skills/README.md +1 -1
  30. package/dist/gaia-ops/skills/agent-contract-handoff/SKILL.md +3 -0
  31. package/dist/gaia-ops/skills/agent-response/SKILL.md +4 -2
  32. package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +1 -1
  33. package/dist/gaia-ops/skills/gaia-patterns/reference.md +2 -3
  34. package/dist/gaia-ops/skills/gaia-release/SKILL.md +60 -24
  35. package/dist/gaia-ops/skills/gaia-release/reference.md +35 -11
  36. package/dist/gaia-ops/skills/git-conventions/SKILL.md +6 -2
  37. package/dist/gaia-ops/skills/orchestrator-present-approval/SKILL.md +30 -7
  38. package/dist/gaia-ops/skills/orchestrator-present-approval/reference.md +32 -15
  39. package/dist/gaia-ops/skills/readme-writing/SKILL.md +1 -1
  40. package/dist/gaia-ops/skills/readme-writing/reference.md +0 -1
  41. package/dist/gaia-ops/skills/security-tiers/SKILL.md +5 -1
  42. package/dist/gaia-ops/skills/security-tiers/reference.md +3 -1
  43. package/dist/gaia-ops/skills/subagent-request-approval/SKILL.md +43 -6
  44. package/dist/gaia-ops/skills/subagent-request-approval/reference.md +66 -16
  45. package/dist/gaia-ops/tools/context/README.md +1 -1
  46. package/dist/gaia-ops/tools/gaia_simulator/extractor.py +0 -1
  47. package/dist/gaia-ops/tools/scan/ui.py +20 -4
  48. package/dist/gaia-ops/tools/scan/verify.py +3 -3
  49. package/dist/gaia-ops/tools/validation/README.md +15 -24
  50. package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
  51. package/dist/gaia-security/hooks/modules/agents/contract_validator.py +18 -0
  52. package/dist/gaia-security/hooks/modules/agents/handoff_persister.py +214 -2
  53. package/dist/gaia-security/hooks/modules/agents/response_contract.py +26 -0
  54. package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +15 -0
  55. package/dist/gaia-security/hooks/modules/security/__init__.py +0 -5
  56. package/dist/gaia-security/hooks/modules/security/approval_grants.py +124 -19
  57. package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +99 -7
  58. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +127 -24
  59. package/dist/gaia-security/hooks/modules/validation/commit_validator.py +90 -55
  60. package/gaia/state/transitions.py +4 -4
  61. package/gaia/store/writer.py +56 -0
  62. package/hooks/modules/README.md +2 -4
  63. package/hooks/modules/agents/contract_validator.py +18 -0
  64. package/hooks/modules/agents/handoff_persister.py +214 -2
  65. package/hooks/modules/agents/response_contract.py +26 -0
  66. package/hooks/modules/agents/transcript_reader.py +15 -0
  67. package/hooks/modules/security/__init__.py +0 -5
  68. package/hooks/modules/security/approval_grants.py +124 -19
  69. package/hooks/modules/security/mutative_verbs.py +99 -7
  70. package/hooks/modules/tools/bash_validator.py +127 -24
  71. package/hooks/modules/validation/commit_validator.py +90 -55
  72. package/index.js +2 -12
  73. package/package.json +4 -6
  74. package/pyproject.toml +3 -3
  75. package/scripts/bootstrap_database.sh +88 -439
  76. package/scripts/check_schema_drift.py +208 -0
  77. package/scripts/migrations/README.md +78 -28
  78. package/scripts/migrations/schema.checksum +8 -0
  79. package/scripts/release-prepare.mjs +199 -0
  80. package/skills/README.md +1 -1
  81. package/skills/agent-contract-handoff/SKILL.md +3 -0
  82. package/skills/agent-response/SKILL.md +4 -2
  83. package/skills/gaia-patterns/SKILL.md +1 -1
  84. package/skills/gaia-patterns/reference.md +2 -3
  85. package/skills/gaia-release/SKILL.md +60 -24
  86. package/skills/gaia-release/reference.md +35 -11
  87. package/skills/git-conventions/SKILL.md +6 -2
  88. package/skills/orchestrator-present-approval/SKILL.md +30 -7
  89. package/skills/orchestrator-present-approval/reference.md +32 -15
  90. package/skills/readme-writing/SKILL.md +1 -1
  91. package/skills/readme-writing/reference.md +0 -1
  92. package/skills/security-tiers/SKILL.md +5 -1
  93. package/skills/security-tiers/reference.md +3 -1
  94. package/skills/subagent-request-approval/SKILL.md +43 -6
  95. package/skills/subagent-request-approval/reference.md +66 -16
  96. package/tools/context/README.md +1 -1
  97. package/tools/gaia_simulator/extractor.py +0 -1
  98. package/tools/scan/ui.py +20 -4
  99. package/tools/scan/verify.py +3 -3
  100. package/tools/validation/README.md +15 -24
  101. package/commands/README.md +0 -64
  102. package/commands/gaia.md +0 -37
  103. package/commands/scan-project.md +0 -74
  104. package/config/crons-schema.md +0 -81
  105. package/config/git_standards.json +0 -72
  106. package/dist/gaia-ops/commands/gaia.md +0 -37
  107. package/dist/gaia-ops/config/crons-schema.md +0 -81
  108. package/dist/gaia-ops/config/git_standards.json +0 -72
  109. package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +0 -179
  110. package/dist/gaia-ops/tools/agentic-loop/decide-status.py +0 -210
  111. package/dist/gaia-ops/tools/agentic-loop/parse-metric.py +0 -106
  112. package/dist/gaia-ops/tools/agentic-loop/record-iteration.py +0 -223
  113. package/dist/gaia-security/hooks/modules/security/gitops_validator.py +0 -179
  114. package/git-hooks/commit-msg +0 -41
  115. package/hooks/modules/security/gitops_validator.py +0 -179
  116. package/scripts/migrations/v10_to_v11.sql +0 -170
  117. package/scripts/migrations/v10_to_v11_fresh.sql +0 -18
  118. package/scripts/migrations/v11_to_v12.sql +0 -195
  119. package/scripts/migrations/v11_to_v12_fresh.sql +0 -19
  120. package/scripts/migrations/v12_to_v13.sql +0 -48
  121. package/scripts/migrations/v12_to_v13_fresh.sql +0 -17
  122. package/scripts/migrations/v13_to_v14.sql +0 -44
  123. package/scripts/migrations/v13_to_v14_fresh.sql +0 -17
  124. package/scripts/migrations/v14_to_v15.sql +0 -71
  125. package/scripts/migrations/v14_to_v15_fresh.sql +0 -19
  126. package/scripts/migrations/v15_to_v16.sql +0 -57
  127. package/scripts/migrations/v15_to_v16_fresh.sql +0 -18
  128. package/scripts/migrations/v16_to_v17.sql +0 -51
  129. package/scripts/migrations/v16_to_v17_fresh.sql +0 -18
  130. package/scripts/migrations/v17_to_v18.sql +0 -66
  131. package/scripts/migrations/v17_to_v18_fresh.sql +0 -24
  132. package/scripts/migrations/v1_to_v2.sql +0 -97
  133. package/scripts/migrations/v2_to_v3.sql +0 -68
  134. package/scripts/migrations/v2_to_v3_merge.sql +0 -69
  135. package/scripts/migrations/v3_to_v4.sql +0 -67
  136. package/scripts/migrations/v3_to_v4_fresh.sql +0 -20
  137. package/scripts/migrations/v4_to_v5.sql +0 -55
  138. package/scripts/migrations/v4_to_v5_fresh.sql +0 -20
  139. package/scripts/migrations/v5_to_v6.sql +0 -48
  140. package/scripts/migrations/v5_to_v6_fresh.sql +0 -17
  141. package/scripts/migrations/v6_to_v7.sql +0 -26
  142. package/scripts/migrations/v6_to_v7_fresh.sql +0 -13
  143. package/scripts/migrations/v7_to_v8.sql +0 -44
  144. package/scripts/migrations/v7_to_v8_fresh.sql +0 -14
  145. package/scripts/migrations/v8_to_v9.sql +0 -87
  146. package/scripts/migrations/v8_to_v9_fresh.sql +0 -15
  147. package/scripts/migrations/v9_to_v10.sql +0 -109
  148. package/scripts/migrations/v9_to_v10_episodes_workspace.sql +0 -109
  149. package/scripts/migrations/v9_to_v10_fresh.sql +0 -18
  150. package/templates/README.md +0 -70
  151. package/templates/managed-settings.template.json +0 -43
  152. package/tools/agentic-loop/decide-status.py +0 -210
  153. package/tools/agentic-loop/parse-metric.py +0 -106
  154. 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)
@@ -43,6 +43,7 @@ The fenced `agent_contract_handoff` block. Parsed by `parse_contract` (regex `_R
43
43
  | `consolidation_report` | Conditional | required when INPUT set `consolidation_required` / `cross_check_required` / `surface_routing.multi_surface` (`requires_consolidation_report`); else may be `null` |
44
44
  | `approval_request` | Conditional | required when `plan_status` is `APPROVAL_REQUEST`; see sub-field table |
45
45
  | `loop_state` | Conditional | agentic-loop turns only; `_check_loop_state_blocking` blocks `COMPLETE` when `iteration < max_iterations AND metric < threshold` |
46
+ | `user_facing_summary` | Optional | a brief prose summary written ONCE for the human reader; `parse_user_facing_summary`. The only human-audience field in the contract -- every other field is machine-audience for the orchestrator. On a single-agent `COMPLETE` (N=1) the orchestrator relays it near-verbatim (adapted to the user's language) instead of re-synthesizing `key_outputs`. Absent, or N>1 (multi-agent), the orchestrator falls back to synthesizing `key_outputs`. Purely additive: never required, never rejected. |
46
47
  | `memorialize_suggestions` | Optional | structured memory candidates for the user to triage; `parse_memorialize_suggestions` |
47
48
  | `memory_suggestions` | Optional | advisory text-only notes (array of strings); `parse_memory_suggestions` |
48
49
  | `update_contracts` | Optional | array of `{contract, payload}` for project-context writes; `parse_update_contracts`; see sub-field table |
@@ -67,6 +68,8 @@ The required keys are EXACTLY 7 (`_EVIDENCE_REQUIRED_FIELDS` in `contract_valida
67
68
 
68
69
  `verification` is a SEPARATE field, NOT one of the 7. It is required ONLY when `plan_status` is `COMPLETE`: it must be a dict and `verification.result` must equal `"pass"`. Missing -> `VERIFICATION_RESULT_REQUIRED_FOR_COMPLETE`; non-pass -> `VERIFICATION_RESULT_MUST_BE_PASS`. For non-COMPLETE statuses `verification` may be absent.
69
70
 
71
+ **Audience boundary.** `key_outputs` and every other `evidence_report` key are written for the **orchestrator** -- distilled findings it reasons over to route the next turn. The optional top-level `user_facing_summary` is the **single** field written for the **human**. Keeping the two distinct is what lets the orchestrator relay a human-shaped summary on N=1 without re-synthesizing machine-shaped evidence, and lets it still synthesize from `key_outputs` when the summary is absent or when multiple agents must be consolidated.
72
+
70
73
  ### consolidation_report
71
74
 
72
75
  Required keys when present (`_CONSOLIDATION_REQUIRED_FIELDS`):
@@ -16,7 +16,7 @@ The orchestrator loads this to interpret a returned `agent_contract_handoff` and
16
16
 
17
17
  ```
18
18
  parse_contract(agent_output) -> read agent_status.plan_status
19
- |- COMPLETE -> summarize key_outputs + surface verification, then close
19
+ |- COMPLETE -> relay user_facing_summary if present & N=1, else summarize key_outputs; surface verification, then close
20
20
  |- APPROVAL_REQUEST -> split on approval_id (present: present-approval; absent: plan options)
21
21
  |- NEEDS_INPUT -> AskUserQuestion, then SendMessage the answer
22
22
  |- BLOCKED -> present open_gaps; new dispatch or accept the limitation
@@ -29,7 +29,7 @@ Before any branch runs, the contract must parse. A block that fails `parse_contr
29
29
 
30
30
  | `plan_status` | Action |
31
31
  |---|---|
32
- | `COMPLETE` | Summarize `key_outputs` in 3-5 bullets AND surface `verification.result` / `verification.details` -- that block is the proof the work landed, and relaying it is what lets the user trust the increment rather than take "done" on faith. Mention `cross_layer_impacts` and `open_gaps` when non-empty. |
32
+ | `COMPLETE` | If `user_facing_summary` is present AND this is a single-agent turn (N=1), relay it near-verbatim -- adapt only to the user's language, do not re-synthesize -- because the subagent already wrote the human-shaped summary and re-summarizing its `key_outputs` only spends tokens to restate what it said. If the field is absent, or N>1 (multiple agents being consolidated), summarize `key_outputs` in 3-5 bullets as before. Either way, surface `verification.result` / `verification.details` -- that block is the proof the work landed, and relaying it is what lets the user trust the increment rather than take "done" on faith. Mention `cross_layer_impacts` and `open_gaps` when non-empty. |
33
33
  | `APPROVAL_REQUEST` | Split on `approval_request.approval_id`: present -> load `Skill('orchestrator-present-approval')`; absent -> present the plan with options (execute / modify / cancel) and on execute/modify resume the SAME agent via `SendMessage`. It splits because a hook-issued `approval_id` carries a pending T3 grant that needs the structured consent flow, while a plan-first request only needs direction (`agent-approval-protocol`, combo decision 2). |
34
34
  | `NEEDS_INPUT` | `AskUserQuestion` with the options in `next_action`, then `SendMessage` the answer back to resume. |
35
35
  | `BLOCKED` | Present `open_gaps` to the user. If they give direction, dispatch a NEW agent addressing the blocker; if they accept the limitation, close the task as incomplete and move on. |
@@ -41,6 +41,8 @@ These ride alongside `plan_status` and carry signal the orchestrator loses if it
41
41
 
42
42
  **`verification`** -- covered in COMPLETE above. It is required only on `COMPLETE` and its `result` must equal `"pass"` (`VERIFICATION_RESULT_MUST_BE_PASS`, `contract_validator.py`); surface `result` and `details` so the user sees the proof, never just the word "done."
43
43
 
44
+ **`user_facing_summary`** -- the one human-audience field (every other field is machine-audience for the orchestrator). On a single-agent `COMPLETE` it is what you relay to the user, near-verbatim and language-adapted, *instead of* re-synthesizing `key_outputs`; that is the whole point -- the subagent wrote the summary once, so re-summarizing duplicates work the user never sees value in. It is optional and additive: when absent, fall back to `key_outputs`; when multiple agents are in flight (N>1), ignore it and synthesize across them, because no single agent's summary speaks for the consolidated result.
45
+
44
46
  **`memorialize_suggestions` / `memory_suggestions`** -- present each entry to the user before closing the turn and persist ONLY on consent. The orchestrator is the sole memory writer; subagents are blocked from curated writes by design so each entry enters the substrate as a named choice. For the curation mechanics -- how to triage, slug, and persist -- load `Skill('memory')` (combo decision 1: the HOW lives in `memory`).
45
47
 
46
48
  **`ownership_assessment`** (in `consolidation_report`, enum `VALID_OWNERSHIP_ASSESSMENTS`) -- a ROUTING INPUT the orchestrator acts on silently, not a user-facing field. `owned_here` means the output is authoritative; `cross_surface_dependency` or `not_my_surface` means another dispatch may be needed to close the gap. Route on it; do not narrate it (combo decision 4).
@@ -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
  ```
@@ -29,7 +29,7 @@ SessionStart emits a one-shot `hookSpecificOutput.additionalContext` manifest (E
29
29
  | Package | Files | Purpose |
30
30
  |---------|-------|---------|
31
31
  | `core/` | `hook_entry`, `paths`, `plugin_mode`, `plugin_setup`, `state`, `stdin` | Entry dispatch, path resolution, mode detection, shared state |
32
- | `security/` | `blocked_commands`, `mutative_verbs`, `tiers`, `gitops_validator`, `command_semantics`, `approval_grants`, `approval_scopes`, `approval_cleanup`, `approval_constants`, `approval_messages`, `blocked_message_formatter`, `prompt_validator` | T3 gate, blocked commands, approval nonce lifecycle |
32
+ | `security/` | `blocked_commands`, `mutative_verbs`, `tiers`, `command_semantics`, `approval_grants`, `approval_scopes`, `approval_cleanup`, `approval_constants`, `approval_messages`, `blocked_message_formatter`, `prompt_validator` | T3 gate, blocked commands, approval nonce lifecycle |
33
33
  | `audit/` | `logger`, `metrics`, `event_detector`, `workflow_auditor`, `workflow_recorder` | Structured logging, metrics collection, workflow audit trail |
34
34
  | `tools/` | `bash_validator`, `cloud_pipe_validator`, `shell_parser`, `task_validator`, `hook_response` | Command validation, pipe detection, shell parsing |
35
35
  | `context/` | `context_injector`, `context_writer`, `context_freshness`, `contracts_loader`, `compact_context_builder`, `anchor_tracker` | Project-context injection, freshness checks, contract loading |
@@ -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
 
@@ -254,7 +253,7 @@ The hook invoker is `python3 <script>` rather than executing the script directly
254
253
  | Category | Directory | What it tests |
255
254
  |----------|-----------|---------------|
256
255
  | Prompt regression | `tests/layer1_prompt_regression/` | Routing table, skill content rules, agent frontmatter, agent prompts, security tier consistency, skills cross-reference, context contracts |
257
- | Hooks | `tests/hooks/modules/` | Security modules (mutative_verbs, blocked_commands, tiers, gitops_validator, approval_grants, approval_scopes, command_semantics), tools (bash_validator, shell_parser, cloud_pipe_validator, task_validator), core (paths, state), context (context_writer) |
256
+ | Hooks | `tests/hooks/modules/` | Security modules (mutative_verbs, blocked_commands, tiers, approval_grants, approval_scopes, command_semantics), tools (bash_validator, shell_parser, cloud_pipe_validator, task_validator), core (paths, state), context (context_writer) |
258
257
  | System | `tests/system/` | Directory structure, permissions, agent definitions, configuration, schema compatibility |
259
258
  | Tools | `tests/tools/` | context_provider, episodic, pending_updates, deep_merge, review_engine, surface_router |
260
259
  | Integration | `tests/integration/` | Context enrichment, subagent lifecycle, subagent stop, nonce approval relay |