@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
@@ -23,11 +23,75 @@ Usage:
23
23
  import json
24
24
  import os
25
25
  import re
26
- from typing import Dict, List, Any, Optional
26
+ from typing import Dict, List, Optional
27
27
  from datetime import datetime
28
28
  from dataclasses import dataclass
29
29
 
30
30
 
31
+ # ---------------------------------------------------------------------------
32
+ # Git commit standards -- inlined constants.
33
+ #
34
+ # These were previously loaded from config/git_standards.json. They are now
35
+ # module-level constants: commit_validator.py is the single runtime consumer
36
+ # of these format/subject/body rules, so the JSON indirection added drift risk
37
+ # without any benefit. Footer detection/stripping is NOT here -- it lives,
38
+ # hardcoded, in bash_validator (footers are bash_validator's responsibility).
39
+ # ---------------------------------------------------------------------------
40
+
41
+ FORMAT = "conventional_commits"
42
+
43
+ TYPE_ALLOWED = (
44
+ "feat",
45
+ "fix",
46
+ "refactor",
47
+ "docs",
48
+ "test",
49
+ "chore",
50
+ "ci",
51
+ "perf",
52
+ "style",
53
+ "build",
54
+ )
55
+
56
+ SCOPE_REQUIRED = False
57
+ SCOPE_EXAMPLES = ("helmrelease", "terraform", "pg-non-prod", "infrastructure")
58
+
59
+ SUBJECT_MAX_LENGTH = 72
60
+ SUBJECT_RULES = {
61
+ "capitalize_first_letter": False,
62
+ "no_period_at_end": True,
63
+ "imperative_mood": True,
64
+ "no_emoji": True,
65
+ }
66
+
67
+ BODY_MAX_LINE_LENGTH = 72
68
+ BODY_REQUIRED = False
69
+
70
+ EXAMPLES_VALID = (
71
+ "feat(helmrelease): add Phase 3.3 services",
72
+ "fix(pg-non-prod): correct API key environment variable mappings",
73
+ "refactor: simplify context provider logic",
74
+ "docs: update README with new workflow",
75
+ "chore(deps): update terraform to v1.6.0",
76
+ )
77
+
78
+ EXAMPLES_INVALID = (
79
+ "Added new feature",
80
+ "Fixed bugs",
81
+ "Updates",
82
+ "feat: add feature\n\n🤖 Generated with Claude Code",
83
+ "feat: add new feature 🚀",
84
+ "fix: 🐛 correct bug",
85
+ )
86
+
87
+ ENFORCEMENT = {
88
+ "enabled": True,
89
+ "block_on_failure": True,
90
+ "log_violations": True,
91
+ "log_path": ".claude/logs/commit-violations.jsonl",
92
+ }
93
+
94
+
31
95
  @dataclass
32
96
  class ValidationResult:
33
97
  """Result of commit message validation."""
@@ -44,43 +108,30 @@ class CommitMessageValidator:
44
108
  """
45
109
  Validates git commit messages against project standards.
46
110
 
47
- Standards are defined in .claude/config/git_standards.json
111
+ Standards are inlined as module-level constants (TYPE_ALLOWED,
112
+ SUBJECT_MAX_LENGTH, SUBJECT_RULES, etc.). Footer detection is not handled
113
+ here -- that is bash_validator's responsibility.
48
114
  """
49
115
 
50
116
  def __init__(self, config_path: Optional[str] = None):
51
117
  """
52
- Initialize validator with configuration.
118
+ Initialize validator.
53
119
 
54
120
  Args:
55
- config_path: Optional path to git_standards.json
56
- If None, uses default location
121
+ config_path: Accepted for backward compatibility only. When given,
122
+ it anchors base_path (used to resolve the relative
123
+ violation log path); the rules themselves come from
124
+ the module-level constants, not from any file.
57
125
  """
58
126
  if config_path is None:
59
- # Default path relative to this file
60
- # From hooks/modules/validation/ go up to gaia-ops root
127
+ # base_path -> gaia-ops root, used to resolve the violation log.
61
128
  # __file__ -> hooks/modules/validation/commit_validator.py
62
- # dirname(dirname(dirname(dirname(__file__)))) -> gaia-ops root
63
129
  base_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
64
- config_path = os.path.join(base_path, 'config', 'git_standards.json')
65
130
  else:
66
- # If config_path provided, derive base_path from it
67
131
  base_path = os.path.dirname(os.path.dirname(config_path))
68
132
 
69
133
  self.base_path = base_path
70
- self.config_path = config_path
71
- self.config = self._load_config()
72
- self.standards = self.config.get('commit_message', {})
73
- self.enforcement = self.config.get('enforcement', {})
74
-
75
- def _load_config(self) -> Dict[str, Any]:
76
- """Load git standards configuration from JSON file."""
77
- if not os.path.exists(self.config_path):
78
- raise FileNotFoundError(
79
- f"Git standards configuration not found at: {self.config_path}"
80
- )
81
-
82
- with open(self.config_path, 'r') as f:
83
- return json.load(f)
134
+ self.enforcement = ENFORCEMENT
84
135
 
85
136
  def validate(self, message: str) -> ValidationResult:
86
137
  """
@@ -95,19 +146,19 @@ class CommitMessageValidator:
95
146
  errors = []
96
147
  warnings = []
97
148
 
98
- # 1. Check for forbidden footers (CRITICAL)
99
- footer_errors = self._check_forbidden_footers(message)
100
- errors.extend(footer_errors)
149
+ # Note: forbidden-footer detection is intentionally NOT done here.
150
+ # Footers are bash_validator's responsibility (stripping/detection
151
+ # is hardcoded there).
101
152
 
102
- # 2. Check conventional commits format
153
+ # 1. Check conventional commits format
103
154
  format_errors = self._check_conventional_format(message)
104
155
  errors.extend(format_errors)
105
156
 
106
- # 3. Check subject line rules
157
+ # 2. Check subject line rules
107
158
  subject_errors = self._check_subject_rules(message)
108
159
  errors.extend(subject_errors)
109
160
 
110
- # 4. Check body rules (warnings only)
161
+ # 3. Check body rules (warnings only)
111
162
  body_warnings = self._check_body_rules(message)
112
163
  warnings.extend(body_warnings)
113
164
 
@@ -121,22 +172,6 @@ class CommitMessageValidator:
121
172
  warnings=warnings
122
173
  )
123
174
 
124
- def _check_forbidden_footers(self, message: str) -> List[Dict[str, str]]:
125
- """Check for forbidden footers in commit message."""
126
- errors = []
127
- forbidden = self.standards.get('footer_forbidden', [])
128
-
129
- for forbidden_text in forbidden:
130
- if forbidden_text.lower() in message.lower():
131
- errors.append({
132
- 'type': 'FORBIDDEN_FOOTER',
133
- 'message': f"Commit message contains forbidden footer: '{forbidden_text}'",
134
- 'fix': f"Remove all occurrences of '{forbidden_text}'",
135
- 'severity': 'error'
136
- })
137
-
138
- return errors
139
-
140
175
  def _check_conventional_format(self, message: str) -> List[Dict[str, str]]:
141
176
  """Check if message follows Conventional Commits format."""
142
177
  errors = []
@@ -147,16 +182,16 @@ class CommitMessageValidator:
147
182
 
148
183
  # Pattern: type(scope)?: description
149
184
  # Examples: feat: add feature, fix(api): correct bug
150
- allowed_types = '|'.join(self.standards.get('type_allowed', []))
185
+ allowed_types = '|'.join(TYPE_ALLOWED)
151
186
  pattern = rf'^({allowed_types})(\(.+?\))?: .+$'
152
187
 
153
188
  if not re.match(pattern, subject):
154
189
  errors.append({
155
190
  'type': 'INVALID_FORMAT',
156
191
  'message': 'Commit message does not follow Conventional Commits format',
157
- 'fix': f"Use format: type(scope): description\nAllowed types: {', '.join(self.standards.get('type_allowed', []))}",
192
+ 'fix': f"Use format: type(scope): description\nAllowed types: {', '.join(TYPE_ALLOWED)}",
158
193
  'severity': 'error',
159
- 'examples': self.standards.get('examples_valid', [])
194
+ 'examples': list(EXAMPLES_VALID)
160
195
  })
161
196
 
162
197
  return errors
@@ -175,7 +210,7 @@ class CommitMessageValidator:
175
210
  description = match.group(2)
176
211
 
177
212
  # Check max length
178
- max_length = self.standards.get('subject_max_length', 72)
213
+ max_length = SUBJECT_MAX_LENGTH
179
214
  if len(subject) > max_length:
180
215
  errors.append({
181
216
  'type': 'SUBJECT_TOO_LONG',
@@ -185,7 +220,7 @@ class CommitMessageValidator:
185
220
  })
186
221
 
187
222
  # Check for period at end
188
- rules = self.standards.get('subject_rules', {})
223
+ rules = SUBJECT_RULES
189
224
  if rules.get('no_period_at_end', True) and description.endswith('.'):
190
225
  errors.append({
191
226
  'type': 'SUBJECT_ENDS_WITH_PERIOD',
@@ -242,7 +277,7 @@ class CommitMessageValidator:
242
277
  })
243
278
 
244
279
  # Check body line length
245
- max_length = self.standards.get('body_max_line_length', 72)
280
+ max_length = BODY_MAX_LINE_LENGTH
246
281
  for i, line in enumerate(lines[2:], start=3): # Skip subject and blank line
247
282
  if len(line) > max_length and not line.startswith('http'):
248
283
  warnings.append({
@@ -285,13 +320,13 @@ class CommitMessageValidator:
285
320
  def get_examples(self) -> Dict[str, List[str]]:
286
321
  """Get example commit messages (valid and invalid)."""
287
322
  return {
288
- 'valid': self.standards.get('examples_valid', []),
289
- 'invalid': self.standards.get('examples_invalid', [])
323
+ 'valid': list(EXAMPLES_VALID),
324
+ 'invalid': list(EXAMPLES_INVALID)
290
325
  }
291
326
 
292
327
  def get_allowed_types(self) -> List[str]:
293
328
  """Get list of allowed commit types."""
294
- return self.standards.get('type_allowed', [])
329
+ return list(TYPE_ALLOWED)
295
330
 
296
331
  def format_error_message(self, validation: ValidationResult) -> str:
297
332
  """
@@ -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 |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gaia-release
3
- description: Use when testing, validating, or publishing Gaia releases -- live install, dry-run, RC, or stable
3
+ description: Use when testing, validating, or publishing Gaia releases -- "install local", "dry-run", "release", live install, sandbox verify, RC, or stable
4
4
  metadata:
5
5
  user-invocable: false
6
6
  type: technique
@@ -8,24 +8,52 @@ metadata:
8
8
 
9
9
  # Gaia Release
10
10
 
11
- Single source of truth for install modes. Each mode exercises a different surface: live tests the working tree against a real workspace, dry-run tests the full install pipeline in an ephemeral sandbox, and the published channels (rc / latest) test npm registry delivery. Skipping a layer means discovering its bugs in production -- a clean dry-run does not prove that a freshly published tarball is what consumers actually receive, and a live install over an existing workspace does not predict a missing file on a clean project.
11
+ The norm for getting Gaia onto a machine and into the registry. The user expresses exactly one of three intentions -- **install local**, **dry-run**, or **release** -- and each maps to a complete, automated sequence the orchestrator runs end-to-end. The user never recalls a sub-step and never runs a release script by hand: the script is a tool the flow invokes, not a command the human must remember. This is the lesson of the sagas that shipped broken: a release failed because a version source was bumped one file at a time and a forgotten `pyproject.toml` drifted; another needed a force-push to reconcile a tag. Every one of those was a manual step a human was trusted to remember and didn't. The fix is to norm the sequence so the steps cannot be forgotten -- they are the flow, not a checklist beside it.
12
12
 
13
- ## Install Modes
13
+ ## The three intentions
14
14
 
15
- | Mode | Command | When to use |
16
- |------|---------|-------------|
17
- | **live self** | `cd /path/to/gaia && npm run gaia:install-local` | Re-install Gaia in the same workspace where Claude Code is running (e.g. `me/`). Validates working-tree changes against your dev environment. |
18
- | **live external** | `cd /path/to/gaia && bash bin/validate-sandbox.sh --tarball ./jaguilar87-gaia-*.tgz --target local --workspace /path/to/target` | Install the working tree into a different workspace (e.g. `qxo/`). Validates consumer-real conditions without touching your dev environment. |
19
- | **live fresh** | Add `--fresh` to either of the above | Wipes `node_modules/`, `package.json`, and `package-lock.json` from the target before install. Forces a clean postinstall run. |
20
- | **dry-run** | `npm run gaia:verify-install:local` | Pack + install into `/tmp/gaia-sandbox-<ts>/` + run the 8-check harness. Validates exactly what `npm publish` would ship. |
21
- | **RC** | Version bump to `X.Y.Z-rc.N` + GitHub Release | Pipeline publishes to npm with `--tag rc`. Consumers opt-in: `npm install @jaguilar87/gaia@rc`. |
22
- | **stable** | Version bump to `X.Y.Z` + GitHub Release | Pipeline publishes to npm with `--tag latest`. Default install: `npm install @jaguilar87/gaia`. |
15
+ When the user says one of these, run the *whole* sequence. Do not stop after the first command and wait to be told the next one -- the sequence below IS the intention.
23
16
 
24
- For step-by-step commands per mode (including version-bump syntax, `--stay` for interactive inspection, and how to test both `ops` and `security` plugin modes), see `reference.md` -> "Mode runbooks".
17
+ ### "install local" -- put the working tree into a real workspace
25
18
 
26
- ## Wire-up Verification Checklist
19
+ ```
20
+ npm run gaia:install-local
21
+ ```
22
+ Then, without being asked:
23
+ 1. Run the **Wire-up verification checklist** (below). If any check fails, jump to `reference.md` -> "Diagnostic guide".
24
+ 2. **Remind the user to restart `claude`** -- skills, hooks, and agents cache at startup, so a fresh install is invisible until restart.
27
25
 
28
- After any install (live, dry-run, RC, stable), the same checklist applies. If any check fails, jump to `reference.md` -> "Diagnostic guide".
26
+ Installing into a *different* workspace (e.g. `qxo/`) or wiping install metadata first is the same intention with a different target -- see `reference.md` -> "Mode runbooks" for the `--workspace` and `--fresh` forms. Always pass `--workspace` explicitly when invoking from inside the gaia repo (the self-referencing `node_modules/@jaguilar87/gaia/` tricks auto-detect; guarded by `is_gaia_repo_root()` in `validate-sandbox.sh`).
27
+
28
+ ### "dry-run" -- prove a clean install works, reproducing CI locally
29
+
30
+ This is not just the sandbox harness -- it is the **local stand-in for CI**, so it must run the same gates CI runs (see the pre-flight principle below). Run, in order:
31
+ 1. `npm run pre-publish:validate` -- the version-drift gate (`validate-manifests` in `ci.yml`).
32
+ 2. `npm run gaia:verify-install:local` -- packs, installs into `/tmp/gaia-sandbox-<ts>/`, runs the 8-check harness. Validates exactly what `npm publish` would ship.
33
+ 3. `npm test` -- the L1 suite (the harness/tests CI runs that reasonably reproduce locally).
34
+
35
+ A green dry-run that skips step 1 is a *subset* of CI, not a stand-in for it -- the gap surfaces only after publish, when the fix costs another release.
36
+
37
+ ### "release [version]" -- end-to-end publish, fully automated
38
+
39
+ The orchestrator runs every step below in order. The user supplies (or confirms) the version and approves the T3 operations; the orchestrator does the rest. **The user does not run `release:prepare` -- step (b) invokes it.**
40
+
41
+ | Step | Action | Notes |
42
+ |------|--------|-------|
43
+ | **(a)** | Determine the version | Default to the next **patch**. If the change is major/minor, **confirm with the user** (`NEEDS_INPUT`) before proceeding -- never silently pick major/minor. |
44
+ | **(b)** | `npm run release:prepare <version>` | The atomic core: bumps ALL version sources at once (`package.json`, `pyproject.toml`, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `CHANGELOG.md`), runs `build:plugins`, then `pre-publish:validate`. Fails loud on any drift. This is `scripts/release-prepare.mjs` -- invoked by the flow, never by the user. |
45
+ | **(c)** | Pre-flight that reproduces CI | `pre-publish:validate` already ran inside (b). Now run `npm test` (plus any harness that applies) so the local gate matches CI before the tag exists. |
46
+ | **(d)** | Commit | `git add` + `git commit` -- local-only, not T3. |
47
+ | **(e)** | Tag, **force-free** | A *new* tag (`v<version>`); never move an existing one. If the remote diverged, reconcile with **merge, not rebase** (rebase forces a tag move, hard-denied locally). See `reference.md` -> "Reconciling a diverged remote". |
48
+ | **(f)** | Push | `git push` (T3). If diverged, the merge from (e) makes this force-free. |
49
+ | **(g)** | `gh release create v<version>` | Triggers `publish.yml`, which builds, validates, and publishes to npm with the auto-detected tag (`-rc.` -> rc, else latest). Mark RC as pre-release. |
50
+ | **(h)** | Monitor to the outcome | Watch the workflow run to its desenlace, then verify the package landed on npm (`npm run gaia:verify-install:rc` / `:latest`). The release is not done when the tag is pushed -- it is done when npm serves the new version. |
51
+
52
+ For the full command forms, the schema-migration lockstep, and the diverged-remote reconciliation, see `reference.md`.
53
+
54
+ ## Wire-up verification checklist
55
+
56
+ After any install (install local, dry-run sandbox, RC, stable), the same checklist applies. If any check fails, jump to `reference.md` -> "Diagnostic guide".
29
57
 
30
58
  1. `ls -la <workspace>/.claude/` -- 7 symlinks (agents, hooks, skills, commands, config, templates, tools) + `logs/`, `approvals/`, `plugin-registry.json`, `settings.local.json`.
31
59
  2. `cat <workspace>/.claude/plugin-registry.json` -- `installed[].name` includes `gaia-ops` (or `gaia-security`) at the expected version.
@@ -36,25 +64,33 @@ After any install (live, dry-run, RC, stable), the same checklist applies. If an
36
64
 
37
65
  These six checks are not redundant with `gaia doctor`. Steps 1-5 catch what doctor cannot reach when the wire-up is so broken that doctor itself walks up to the user `.claude/` instead of the workspace.
38
66
 
39
- ## Release Checklist
40
-
41
- Pre-publish, publish, and post-publish steps -- plus the schema migration protocol when `EXPECTED_SCHEMA_VERSION` changes -- live in `reference.md` -> "Release checklist" and "Schema migration protocol". Both are read on-demand from the SKILL when actually doing a release; they are not in this file because they would dominate the line budget without informing the day-to-day mode decision.
42
-
43
67
  ## CI/CD
44
68
 
45
69
  | Workflow | File | Triggers |
46
70
  |----------|------|----------|
47
- | CI | `.github/workflows/ci.yml` | Push / PR -- runs pytest (Python 3.9/3.11/3.12), Node tests, and plugin build verification |
71
+ | CI | `.github/workflows/ci.yml` | Push / PR -- runs pytest (Python 3.11/3.12), Node tests, plugin build verification, and `validate-manifests` |
48
72
  | Publish | `.github/workflows/publish.yml` | GitHub Release event -- builds plugins, validates artifacts, auto-detects npm tag from version (`-rc.` -> rc, `-beta.` -> beta, else -> latest), and publishes |
49
73
 
50
74
  `NPM_TOKEN` lives in GitHub Secrets; local `npm publish` bypasses build verification and is not the supported path.
51
75
 
76
+ ## Principles -- why the sequence is normed, not optional
77
+
78
+ - **The pre-flight reproduces what CI validates, not a subset of it.** When the local check skips a gate CI runs (`pre-publish:validate`), that gate's failures surface only *after* publishing, on the published tarball, where the only remedy is another release. That is exactly how a `pyproject.toml` drift shipped green-local and red-CI. The "dry-run" intention and step (c) of "release" close the gap -- `pre-publish:validate` for drift. See `reference.md` -> "The pre-flight reproduces what CI validates".
79
+ - **Bump every version source in one step, never one at a time.** `pre-publish:validate` requires `package.json`, `pyproject.toml`, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, and the `CHANGELOG.md` top header to agree. A partial bump leaves the tree in a state the validator rejects and lets a stale source ship. `release:prepare` writes all of them from one target version, so a hand-desync is impossible. See `scripts/release-prepare.mjs`.
80
+ - **Tag force-free; reconcile with merge, never rebase.** `publish.yml` commits built artifacts back to `main`, so the remote leads after every release. Rebasing rewrites hashes and forces a tag move (`git tag -f` / `--force`), hard-denied by local hooks (`git_destructive` in `blocked_commands.py`, exit 2, not approvable). Merge preserves hashes and tags; a new release gets a *new* tag, never a moved one. See `reference.md` -> "Reconciling a diverged remote".
81
+ - **A release ends at npm, not at the tag.** Pushing the tag only starts `publish.yml`. The intention is not satisfied until the workflow reaches its outcome and npm serves the new version -- step (h) is part of the sequence, not a follow-up.
82
+
52
83
  ## Anti-Patterns
53
84
 
54
- - **Live-only testing** -- live install runs against your accumulated workspace state; only dry-run proves a clean-install works.
85
+ - **Stopping after the first command of an intention** -- "install local" is not just `gaia:install-local`; "release" is not just `release:prepare`. Each intention is the *whole* sequence. Running one command and waiting to be told the next reintroduces the forgettable manual step the norm exists to remove.
86
+ - **Asking the user to run `release:prepare`** -- it is a tool the "release" flow invokes at step (b), not a command the human runs. Surfacing it as a manual step is the same failure mode (a step someone must remember) wearing a new script.
87
+ - **Pre-flight that is a subset of CI** -- skipping `pre-publish:validate` locally means the version drift surfaces after publish. Reproduce CI; do not approximate it.
88
+ - **Bumping version sources one at a time** -- desyncs a source by hand; `pre-publish:validate` rejects the tree and a forgotten file ships if the check is skipped. Always go through `release:prepare`.
89
+ - **Rebase to reconcile a diverged remote** -- forces a tag move, hard-denied locally. Merge instead.
90
+ - **Live-only testing** -- live install runs against accumulated workspace state; only dry-run proves a clean install works.
55
91
  - **Local npm publish** -- bypasses the pipeline's build verification step.
56
92
  - **Single-mode testing** -- `ops` and `security` load different skill sets and hook configurations; one can break while the other passes.
57
- - **Stale dist/** -- forgetting `npm run build:plugins` before pack means validating old code.
58
- - **Missing restart** -- the process caches skills, hooks, and agents at startup; mode switches and fresh installs require restarting `claude`.
59
- - **Ignoring `~/.gaia/last-install-error.json`** -- when postinstall fails silently, this is the marker that says so. Treat its presence as a hard failure regardless of what `gaia doctor` reports.
60
- - **Relying on auto-detect when cwd is inside the gaia repo** -- the repo has a self-referencing `node_modules/@jaguilar87/gaia/` entry that can trick the workspace detector. Always pass `--workspace /home/jorge/ws/me` explicitly when running installs from within the gaia repo. Verify with `readlink /home/jorge/ws/me/.claude/hooks` post-install -- it must point to the consumer workspace's `node_modules`, not the repo's.
93
+ - **Stale dist/** -- forgetting `npm run build:plugins` before pack means validating old code. `release:prepare` and `build:plugins` regenerate it; dry-run packs fresh.
94
+ - **Missing restart** -- the process caches skills, hooks, and agents at startup; installs and mode switches require restarting `claude`.
95
+ - **Ignoring `~/.gaia/last-install-error.json`** -- when postinstall fails silently, this is the marker. Treat its presence as a hard failure regardless of what `gaia doctor` reports.
96
+ - **Relying on auto-detect when cwd is inside the gaia repo** -- the self-referencing `node_modules/@jaguilar87/gaia/` entry tricks the workspace detector. Pass `--workspace /home/jorge/ws/me` explicitly; verify with `readlink /home/jorge/ws/me/.claude/hooks`.
@@ -44,7 +44,7 @@ Append `--fresh` to either form. The harness will delete `node_modules/`, `packa
44
44
  **What postinstall does:**
45
45
  1. Ships `scripts/` (bootstrap_database.sh) -- failed silently in pre-rc.4 builds; verified in `npm pack --dry-run`.
46
46
  2. Creates `.claude/` if missing.
47
- 3. Runs `bootstrap_database.sh` -- seeds the schema (v17), agent rows, and `schema_version`. Fails loud on any error (writes `~/.gaia/last-install-error.json` and exits non-zero).
47
+ 3. Runs `bootstrap_database.sh` -- seeds the schema (current floor v18), agent rows, and `schema_version`. Fails loud on any error (writes `~/.gaia/last-install-error.json` and exits non-zero).
48
48
  4. Merges hooks into `settings.local.json` via the consolidated `merge_hooks` step.
49
49
  5. Creates 7 symlinks under `.claude/` to `node_modules/@jaguilar87/gaia/<dir>/`.
50
50
  6. Writes `plugin-registry.json` with `installed[].name == "gaia-ops"`.
@@ -87,20 +87,32 @@ A change that works in one mode can break the other because they load different
87
87
 
88
88
  ### RC and stable (pipeline)
89
89
 
90
- Both modes share the same pipeline. The pipeline auto-detects the npm tag from the version string.
90
+ Both modes share the same pipeline. The pipeline auto-detects the npm tag from the version string. These steps are the expansion of the "release" intention in `SKILL.md`; the orchestrator runs them, the user supplies/confirms the version.
91
91
 
92
92
  1. Dry-run must pass locally first.
93
- 2. Version bump:
94
- - RC: edit `package.json` to `X.Y.Z-rc.N` (the tooling does not provide a single-shot `npm version` for RC; bump manually + rebuild dist/).
95
- - Stable: `npm version minor` (or `major` / `patch` as appropriate).
96
- 3. Rebuild `dist/`: `npm run build:plugins`.
97
- 4. Update `dist/*/plugin.json` and `marketplace.json` to match the new version.
98
- 5. Commit + push (PR or direct to main).
93
+ 2. **`npm run release:prepare <version>`** -- the atomic bump. This is `scripts/release-prepare.mjs`, invoked by the flow, **never run by the user by hand**. In one command it:
94
+ - writes `<version>` to ALL sources at once -- `package.json`, `pyproject.toml`, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json` (every plugin entry), and the `CHANGELOG.md` top header (inserts a dated stub above the current top if absent -- edit its body before release);
95
+ - runs `npm run build:plugins` to regenerate `dist/` (including the per-plugin manifests that carry the version);
96
+ - runs `npm run pre-publish:validate` and fails loud on any drift.
97
+
98
+ This replaces hand-bumping one file at a time. `pre-publish:validate` fails the release unless every version source agrees, and the two real escapes a hand-bump leaves are a `pyproject.toml` left behind on a prior version (caught only by `pre-publish:validate`) and a `marketplace.json` that still advertises the old tag. `release:prepare` makes the desync impossible because all sources are written from one target version. For a bare semver: `5.0.5` for stable, `5.1.0-rc.1` for RC (no leading `v` -- the tag adds it). The script is idempotent: re-running with the same version is a no-op bump that re-validates.
99
+ 3. Pre-flight that reproduces CI (steps already partly done inside `release:prepare`): `npm test`. `pre-publish:validate` ran in step 2.
100
+ 4. Commit (`git add` + `git commit` -- local-only, not T3). **If the remote diverged, reconcile with MERGE, never rebase** (see "Reconciling a diverged remote" below).
101
+ 5. Tag (force-free -- a *new* `v<version>`, never moved) + push (`git push`, T3). The merge in step 4 keeps the push force-free.
99
102
  6. Create a GitHub Release:
100
103
  - Tag: the version from `package.json` (e.g., `v5.0.0-rc.4` or `v5.3.0`).
101
104
  - Title: the version.
102
105
  - Mark RC releases as pre-release.
103
106
  7. `publish.yml` triggers automatically and publishes with `--tag <auto-detected>`.
107
+ 8. Monitor the workflow run to its outcome, then verify npm serves the new version (`npm run gaia:verify-install:rc` / `:latest`). The release is done at npm, not at the tag.
108
+
109
+ ### Reconciling a diverged remote -- merge, never rebase; never move a tag
110
+
111
+ `publish.yml` commits built artifacts back to `main` and pushes (the "Commit built plugins" step), so after a release the remote `main` is *ahead* of your local. When you next go to release and find the remote diverged, the reconciliation choice is forced by local policy:
112
+
113
+ - **Reconcile with merge, not rebase.** Rebase rewrites your local commit hashes. If a tag already pointed at one of those commits, you would have to re-point it -- which means `git tag -f` or a force-push of the tag. Both match the `git_destructive` pattern in `hooks/modules/security/blocked_commands.py` and are **hard-denied locally** (exit 2, not approvable) -- there is no `approval_id` that unblocks them. Merge preserves the existing hashes, so existing tags stay valid and no force is ever needed.
114
+ - **Tags are create-only -- never move one.** A published tag is immutable history; a new release gets a *new* tag (`-rc.N+1`, next patch/minor), it does not re-point an old one. Moving a tag requires the same force path that local hooks deny.
115
+ - **The force-deny is a local hooks policy, not a CI one.** `publish.yml` itself runs `git tag -f` and `git push --force` for the tag after committing `dist/` -- that is the pipeline operating under its own permissions, outside the local hook layer. Do not read the pipeline's force-push as license to force locally; the local deny stands regardless of what CI does.
104
116
 
105
117
  **Verify from npm** (registry round-trip):
106
118
  - RC: `npm run gaia:verify-install:rc`
@@ -134,17 +146,29 @@ When you bump `EXPECTED_SCHEMA_VERSION` in `bin/cli/doctor.py`, the four steps b
134
146
  3. Add migration SQL (`UPDATE` / `ALTER TABLE`) when existing DBs need to upgrade in place; otherwise old workspaces stay below the expected version and `gaia doctor` fails.
135
147
  4. Run `pytest tests/cli/test_schema_version_lockstep.py` -- it cross-references the constant, the bootstrap insert, and the migration SQL to confirm they all agree.
136
148
 
149
+ ### Build/pre-publish Schema-Drift Guard
150
+
151
+ `bin/pre-publish-validate.js` Step 5c runs `scripts/check_schema_drift.py`, which sha256-fingerprints `gaia/store/schema.sql` and compares it against `scripts/migrations/schema.checksum` (pinned to `EXPECTED_SCHEMA_VERSION`). If the schema has changed but the version was not bumped and no migration file added, the guard fails the build.
152
+
153
+ **Consequence:** if you edit `schema.sql` you MUST either (a) bump `EXPECTED_SCHEMA_VERSION` + add the migration file (following the lockstep above), OR (b) re-pin the checksum with `python3 scripts/check_schema_drift.py --record` (the escape hatch for pure-comment or non-semantic edits). Without one of these, `npm run pre-publish:validate` — and therefore `release:prepare` — will FAIL.
154
+
137
155
  ## Release Checklist
138
156
 
157
+ ### The pre-flight reproduces what CI validates, not a subset of it
158
+
159
+ A green pre-flight only protects the release if it runs the same gates CI runs. When the local check is a *subset* of CI, the gaps CI covers are discovered after publishing -- on the published tarball, where the only remedy is another release. `npm run gaia:verify-install:local` packs and installs into a sandbox, but it does **not** run `pre-publish:validate`. CI (`.github/workflows/ci.yml`) runs it separately. A real failure escaped exactly through that gap: a `pyproject.toml` version drift that only `pre-publish:validate` catches, which was green locally and red in CI *after* the tag was pushed.
160
+
161
+ So the pre-flight must close that gap before any tag or push:
162
+
139
163
  **Pre-publish:**
140
164
  - `pytest tests/` green (or `npm test` for the L1 subset).
165
+ - **`npm run pre-publish:validate` green locally** -- this is the version-drift gate (`validate-manifests` job in `ci.yml`). Run it before tag/push, not only in CI. It is what catches a `pyproject.toml` / `package.json` / `plugin.json` / `marketplace.json` desync before it ships.
141
166
  - `npm pack --dry-run | grep scripts/` confirms `scripts/bootstrap_database.sh` is included in the tarball.
142
167
  - `bash bin/validate-sandbox.sh --tarball ./jaguilar87-gaia-*.tgz --target sandbox --fresh` green (or `npm run gaia:verify-install:local`).
143
168
  - Optional smoke: `npm run gaia:install-local -- --workspace /tmp/test-install --fresh`.
144
169
 
145
170
  **Publish:**
146
- - Bump version in `package.json`, `dist/*/plugin.json`, and `marketplace.json`.
147
- - `npm run build:plugins` regenerates `dist/`.
171
+ - Run `npm run release:prepare <version>` -- atomically bumps all version sources (`package.json`, `pyproject.toml`, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`), regenerates `dist/` via `build:plugins`, and runs `pre-publish:validate` (including the schema-drift guard). Nothing else should write a version.
148
172
  - Commit + push.
149
173
  - Create GitHub Release with the version tag.
150
174
  - Pipeline publishes (`publish.yml` triggers on Release event):
@@ -165,7 +189,7 @@ The workflow at `.github/workflows/publish.yml` runs on every GitHub Release eve
165
189
  - Installs deps with `npm ci`.
166
190
  - Builds plugins with `npm run build:plugins`.
167
191
  - Verifies all expected artifacts in `dist/`.
168
- - Commits built artifacts back if changed.
192
+ - Commits built artifacts back if changed (commit message carries `[skip ci]` so the dist commit-back does not re-trigger CI).
169
193
  - Runs `npm run pre-publish:validate`.
170
194
  - Auto-detects npm tag from version string (see "Publish" above).
171
195
  - Publishes with `npm publish --access public --tag <detected>`.
@@ -43,5 +43,9 @@ requires explicit user instruction.
43
43
 
44
44
  ## Hook Enforcement
45
45
 
46
- The `commit_validator.py` hook validates against `config/git_standards.json`.
47
- Format violations block the commit. Body line length triggers warnings only.
46
+ The `commit_validator.py` hook validates against standards inlined as
47
+ module-level constants in that file (`TYPE_ALLOWED`, `SUBJECT_MAX_LENGTH`,
48
+ `SUBJECT_RULES`, `BODY_MAX_LINE_LENGTH`) -- it covers the conventional-commits
49
+ format, subject, and body rules. Forbidden-footer detection lives separately
50
+ in `bash_validator` (hardcoded there). Format violations block the commit.
51
+ Body line length triggers warnings only.