@jaguilar87/gaia 5.0.7 → 5.0.9

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 (99) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +13 -0
  4. package/bin/README.md +6 -1
  5. package/bin/cli/approvals.py +486 -474
  6. package/bin/cli/brief.py +13 -0
  7. package/bin/cli/doctor.py +1 -1
  8. package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
  9. package/dist/gaia-ops/hooks/adapters/claude_code.py +92 -86
  10. package/dist/gaia-ops/hooks/modules/agents/handoff_persister.py +13 -2
  11. package/dist/gaia-ops/hooks/modules/context/context_injector.py +23 -7
  12. package/dist/gaia-ops/hooks/modules/events/event_writer.py +63 -96
  13. package/dist/gaia-ops/hooks/modules/security/__init__.py +0 -2
  14. package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +238 -69
  15. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +506 -1103
  16. package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +24 -1
  17. package/dist/gaia-ops/hooks/modules/session/pending_scanner.py +150 -90
  18. package/dist/gaia-ops/hooks/modules/session/session_manifest.py +257 -28
  19. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +19 -0
  20. package/dist/gaia-ops/hooks/post_compact.py +1 -0
  21. package/dist/gaia-ops/hooks/pre_compact.py +1 -0
  22. package/dist/gaia-ops/hooks/user_prompt_submit.py +20 -0
  23. package/dist/gaia-ops/skills/agent-approval-protocol/SKILL.md +50 -14
  24. package/dist/gaia-ops/skills/agent-approval-protocol/reference.md +16 -9
  25. package/dist/gaia-ops/skills/agent-protocol/examples.md +12 -1
  26. package/dist/gaia-ops/skills/gaia-patterns/reference.md +2 -2
  27. package/dist/gaia-ops/skills/orchestrator-present-approval/SKILL.md +69 -22
  28. package/dist/gaia-ops/skills/orchestrator-present-approval/reference.md +16 -3
  29. package/dist/gaia-ops/skills/orchestrator-present-approval/template.md +20 -14
  30. package/dist/gaia-ops/skills/pending-approvals/SKILL.md +16 -11
  31. package/dist/gaia-ops/skills/subagent-request-approval/SKILL.md +28 -3
  32. package/dist/gaia-ops/skills/subagent-request-approval/reference.md +34 -8
  33. package/dist/gaia-ops/tools/migration/README.md +10 -12
  34. package/dist/gaia-ops/tools/scan/orchestrator.py +194 -10
  35. package/dist/gaia-ops/tools/scan/tests/test_integration.py +1 -2
  36. package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
  37. package/dist/gaia-security/hooks/adapters/claude_code.py +92 -86
  38. package/dist/gaia-security/hooks/modules/agents/handoff_persister.py +13 -2
  39. package/dist/gaia-security/hooks/modules/context/context_injector.py +23 -7
  40. package/dist/gaia-security/hooks/modules/events/event_writer.py +63 -96
  41. package/dist/gaia-security/hooks/modules/security/__init__.py +0 -2
  42. package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +238 -69
  43. package/dist/gaia-security/hooks/modules/security/approval_grants.py +506 -1103
  44. package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +24 -1
  45. package/dist/gaia-security/hooks/modules/session/pending_scanner.py +150 -90
  46. package/dist/gaia-security/hooks/modules/session/session_manifest.py +257 -28
  47. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +19 -0
  48. package/dist/gaia-security/hooks/user_prompt_submit.py +20 -0
  49. package/gaia/approvals/__init__.py +2 -1
  50. package/gaia/approvals/store.py +165 -15
  51. package/gaia/store/schema.sql +38 -1
  52. package/gaia/store/writer.py +400 -0
  53. package/hooks/adapters/claude_code.py +92 -86
  54. package/hooks/elicitation_result.py +20 -75
  55. package/hooks/modules/agents/handoff_persister.py +13 -2
  56. package/hooks/modules/context/context_injector.py +23 -7
  57. package/hooks/modules/events/event_writer.py +63 -96
  58. package/hooks/modules/security/__init__.py +0 -2
  59. package/hooks/modules/security/approval_cleanup.py +238 -69
  60. package/hooks/modules/security/approval_grants.py +506 -1103
  61. package/hooks/modules/security/mutative_verbs.py +24 -1
  62. package/hooks/modules/session/pending_scanner.py +150 -90
  63. package/hooks/modules/session/session_manifest.py +257 -28
  64. package/hooks/modules/tools/bash_validator.py +19 -0
  65. package/hooks/post_compact.py +1 -0
  66. package/hooks/pre_compact.py +1 -0
  67. package/hooks/user_prompt_submit.py +20 -0
  68. package/package.json +1 -1
  69. package/pyproject.toml +1 -1
  70. package/scripts/bootstrap_database.sh +66 -17
  71. package/scripts/migrations/README.md +26 -14
  72. package/scripts/migrations/schema.checksum +2 -2
  73. package/scripts/migrations/v18_to_v19.sql +36 -0
  74. package/scripts/migrations/v19_to_v20.sql +20 -0
  75. package/skills/agent-approval-protocol/SKILL.md +50 -14
  76. package/skills/agent-approval-protocol/reference.md +16 -9
  77. package/skills/agent-protocol/examples.md +12 -1
  78. package/skills/gaia-patterns/reference.md +2 -2
  79. package/skills/orchestrator-present-approval/SKILL.md +69 -22
  80. package/skills/orchestrator-present-approval/reference.md +16 -3
  81. package/skills/orchestrator-present-approval/template.md +20 -14
  82. package/skills/pending-approvals/SKILL.md +16 -11
  83. package/skills/subagent-request-approval/SKILL.md +28 -3
  84. package/skills/subagent-request-approval/reference.md +34 -8
  85. package/tools/migration/README.md +10 -12
  86. package/tools/scan/orchestrator.py +194 -10
  87. package/tools/scan/tests/test_integration.py +1 -2
  88. package/bin/cli/plans.py +0 -517
  89. package/dist/gaia-ops/tools/context/deep_merge.py +0 -159
  90. package/dist/gaia-ops/tools/migration/migrate_04_harness_events.py +0 -132
  91. package/dist/gaia-ops/tools/migration/migrate_04_harness_events.sh +0 -23
  92. package/dist/gaia-ops/tools/scan/merge.py +0 -213
  93. package/dist/gaia-ops/tools/scan/tests/test_merge.py +0 -269
  94. package/gaia/approvals/revert.py +0 -282
  95. package/tools/context/deep_merge.py +0 -159
  96. package/tools/migration/migrate_04_harness_events.py +0 -132
  97. package/tools/migration/migrate_04_harness_events.sh +0 -23
  98. package/tools/scan/merge.py +0 -213
  99. package/tools/scan/tests/test_merge.py +0 -269
@@ -1,282 +0,0 @@
1
- """gaia.approvals.revert -- Inverse-command derivation for approval revert.
2
-
3
- Per D14 (plan design decision), revert works by querying EXECUTED events
4
- for an approval, deriving candidate inverse commands using a hardcoded
5
- best-effort mapping, and presenting them for user confirmation.
6
-
7
- Public API:
8
- InverseCommand -- dataclass for a candidate inverse operation
9
- derive_inverse(event) -> InverseCommand | None
10
- derive_inverses_for_approval(approval_id, con) -> list[InverseCommand]
11
-
12
- The InverseCommand dataclass carries:
13
- event_id: int -- the original EXECUTED event id
14
- original_command: str -- the original command (from payload or metadata)
15
- inverse_command: str | None -- the derived inverse, or None if NOT REVERSIBLE
16
- reversible: bool -- False when no inverse can be derived
17
- notes: str -- human-readable explanation
18
- """
19
-
20
- from __future__ import annotations
21
-
22
- import json
23
- import re
24
- import sqlite3
25
- from dataclasses import dataclass, field
26
- from pathlib import Path
27
- from typing import Any, Dict, List, Optional
28
-
29
-
30
- @dataclass
31
- class InverseCommand:
32
- """Candidate inverse operation for an EXECUTED approval event."""
33
-
34
- event_id: int
35
- original_command: str
36
- inverse_command: Optional[str]
37
- reversible: bool
38
- notes: str
39
-
40
-
41
- # ---------------------------------------------------------------------------
42
- # Hardcoded verb -> inverse mapping (D14)
43
- # ---------------------------------------------------------------------------
44
-
45
- # Pattern-based inverse rules. Each entry is (pattern_re, inverse_template_fn).
46
- # The pattern is matched against the original command. The template function
47
- # receives the re.Match object and returns the inverse command string.
48
- _INVERSE_RULES: list[tuple[re.Pattern, Any]] = []
49
-
50
-
51
- def _rule(pattern: str):
52
- """Decorator to register an inverse rule."""
53
- def decorator(fn):
54
- _INVERSE_RULES.append((re.compile(pattern), fn))
55
- return fn
56
- return decorator
57
-
58
-
59
- @_rule(r"^gaia\s+brief\s+set-status\s+(\S+)\s+done\s*$")
60
- def _brief_done_to_pending(m):
61
- brief_id = m.group(1)
62
- return f"gaia brief set-status {brief_id} pending"
63
-
64
-
65
- @_rule(r"^gaia\s+brief\s+set-status\s+(\S+)\s+active\s*$")
66
- def _brief_active_to_draft(m):
67
- brief_id = m.group(1)
68
- return f"gaia brief set-status {brief_id} draft"
69
-
70
-
71
- @_rule(r"^gaia\s+brief\s+set-status\s+(\S+)\s+pending\s*$")
72
- def _brief_pending_to_draft(m):
73
- brief_id = m.group(1)
74
- return f"gaia brief set-status {brief_id} draft"
75
-
76
-
77
- @_rule(r"^git\s+branch\s+(\S+)\s*$")
78
- def _git_branch_create_to_delete(m):
79
- branch = m.group(1)
80
- if branch.startswith("-"):
81
- # Already a delete flag -- not a create
82
- return None
83
- return f"git branch -D {branch}"
84
-
85
-
86
- @_rule(r"^git\s+branch\s+-[bB]\s+(\S+)\s*$")
87
- def _git_branch_b_to_delete(m):
88
- branch = m.group(1)
89
- return f"git branch -D {branch}"
90
-
91
-
92
- @_rule(r"^rm\s+(.+)\s*$")
93
- def _rm_not_reversible(_m):
94
- # rm has no generic inverse; caller sees NOT REVERSIBLE
95
- return None
96
-
97
-
98
- def _derive_from_command_string(command: str) -> InverseCommand | None:
99
- """Apply hardcoded rules to a single command string.
100
-
101
- Returns an InverseCommand on the first match, or None when no rule matches.
102
- """
103
- cmd = command.strip()
104
- for pattern, fn in _INVERSE_RULES:
105
- m = pattern.match(cmd)
106
- if m:
107
- inverse = fn(m)
108
- if inverse is not None:
109
- return InverseCommand(
110
- event_id=0,
111
- original_command=cmd,
112
- inverse_command=inverse,
113
- reversible=True,
114
- notes=f"Derived from pattern: {pattern.pattern}",
115
- )
116
- else:
117
- return InverseCommand(
118
- event_id=0,
119
- original_command=cmd,
120
- inverse_command=None,
121
- reversible=False,
122
- notes="NOT REVERSIBLE -- matched pattern has no safe inverse",
123
- )
124
- return None
125
-
126
-
127
- def _is_file_create(payload: dict, original_cmd: str) -> bool:
128
- """Heuristic: detect if the payload represents a file creation."""
129
- commands = payload.get("commands") or []
130
- scope = payload.get("scope") or ""
131
- # A Write tool on a new path is stored with 'write' or 'create' in operation.
132
- operation = (payload.get("operation") or "").lower()
133
- if "write" in operation or "create" in operation:
134
- return True
135
- # If the scope looks like a file path and command is a write-like verb
136
- if scope and ("write" in original_cmd.lower() or "create" in original_cmd.lower()):
137
- return True
138
- return False
139
-
140
-
141
- def derive_inverse(event: Dict[str, Any]) -> InverseCommand:
142
- """Derive a candidate inverse command for a single EXECUTED event.
143
-
144
- Best-effort approach per D14:
145
- - Tries hardcoded verb patterns first.
146
- - Detects file create -> suggests rm <path>.
147
- - Falls through to "NOT REVERSIBLE" with original command for reference.
148
-
149
- Args:
150
- event: A dict from store.replay_for_approval() representing an
151
- EXECUTED event row. Must have 'id', 'payload_json', and
152
- optionally 'metadata_json'.
153
-
154
- Returns:
155
- InverseCommand with reversible=True if an inverse was derived, or
156
- reversible=False with inverse_command=None and a "NOT REVERSIBLE"
157
- note.
158
- """
159
- event_id = event.get("id", 0)
160
- payload_json = event.get("payload_json") or ""
161
- metadata_json = event.get("metadata_json") or ""
162
-
163
- payload: dict = {}
164
- if payload_json:
165
- try:
166
- payload = json.loads(payload_json)
167
- except (json.JSONDecodeError, TypeError):
168
- pass
169
-
170
- metadata: dict = {}
171
- if metadata_json:
172
- try:
173
- metadata = json.loads(metadata_json)
174
- except (json.JSONDecodeError, TypeError):
175
- pass
176
-
177
- # Collect commands to try inverting.
178
- commands = payload.get("commands") or []
179
- exact_content = payload.get("exact_content") or ""
180
-
181
- # Build list of individual command strings to invert.
182
- cmd_strings: list[str] = []
183
- if commands:
184
- cmd_strings = [str(c).strip() for c in commands if c]
185
- elif exact_content:
186
- # Split newline-separated commands per D13.
187
- cmd_strings = [l.strip() for l in exact_content.splitlines() if l.strip()]
188
-
189
- if not cmd_strings:
190
- # No commands found -- check operation field.
191
- operation = payload.get("operation") or ""
192
- if operation:
193
- cmd_strings = [operation]
194
-
195
- if not cmd_strings:
196
- return InverseCommand(
197
- event_id=event_id,
198
- original_command="(no command recorded)",
199
- inverse_command=None,
200
- reversible=False,
201
- notes="NOT REVERSIBLE -- no command data found in event payload",
202
- )
203
-
204
- # For multi-command events, try to invert each command.
205
- # If ALL have inverses, return a compound inverse. If any fails, NOT REVERSIBLE.
206
- inverses = []
207
- original_summary = "; ".join(cmd_strings)
208
-
209
- for cmd in cmd_strings:
210
- result = _derive_from_command_string(cmd)
211
- if result is None:
212
- # No rule matched -- check for file create heuristic.
213
- scope = payload.get("scope") or ""
214
- if scope and _is_file_create(payload, cmd):
215
- # Suggest rm <scope> as the inverse.
216
- scope_path = scope.strip()
217
- inverses.append(
218
- InverseCommand(
219
- event_id=event_id,
220
- original_command=cmd,
221
- inverse_command=f"rm {scope_path}",
222
- reversible=True,
223
- notes=f"File create detected -- inverse is rm {scope_path} (requires confirm)",
224
- )
225
- )
226
- else:
227
- return InverseCommand(
228
- event_id=event_id,
229
- original_command=original_summary,
230
- inverse_command=None,
231
- reversible=False,
232
- notes=f"NOT REVERSIBLE -- no inverse rule matches: {cmd!r}",
233
- )
234
- else:
235
- result.event_id = event_id
236
- inverses.append(result)
237
-
238
- if len(inverses) == 1:
239
- return inverses[0]
240
-
241
- # Multiple inverses -- combine into a compound inverse.
242
- combined_inverse = " && ".join(ic.inverse_command for ic in inverses if ic.inverse_command)
243
- combined_notes = "; ".join(ic.notes for ic in inverses)
244
- return InverseCommand(
245
- event_id=event_id,
246
- original_command=original_summary,
247
- inverse_command=combined_inverse if combined_inverse else None,
248
- reversible=bool(combined_inverse),
249
- notes=combined_notes,
250
- )
251
-
252
-
253
- def derive_inverses_for_approval(
254
- approval_id: str,
255
- con: sqlite3.Connection,
256
- ) -> List[InverseCommand]:
257
- """Return a list of InverseCommand for all EXECUTED events of an approval.
258
-
259
- Args:
260
- approval_id: The P-{uuid4} approval identifier.
261
- con: An open sqlite3.Connection.
262
-
263
- Returns:
264
- List of InverseCommand, one per EXECUTED event, in insertion order.
265
- Empty list if no EXECUTED events exist.
266
- """
267
- cur = con.execute(
268
- "SELECT id, payload_json, metadata_json FROM approval_events "
269
- "WHERE approval_id = ? AND event_type = 'EXECUTED' "
270
- "ORDER BY id ASC",
271
- (approval_id,),
272
- )
273
- rows = cur.fetchall()
274
- result = []
275
- for row in rows:
276
- event = {
277
- "id": row[0],
278
- "payload_json": row[1],
279
- "metadata_json": row[2],
280
- }
281
- result.append(derive_inverse(event))
282
- return result
@@ -1,159 +0,0 @@
1
- """
2
- Deep merge utility for project-context.json updates.
3
-
4
- Merges two dicts recursively following the gaia-ops merge decision tree:
5
- 1. Key missing in current -> ADD
6
- 2. Both values are dicts -> RECURSE (deep merge)
7
- 3. Both values are lists -> UNION (primitives: sorted set union;
8
- dicts with "name": merge by name;
9
- other dicts: concatenate + deduplicate)
10
- 4. Both values are scalars -> OVERWRITE (new replaces old)
11
- 5. Type mismatch -> OVERWRITE with warning
12
-
13
- No-Delete Policy: keys in current but NOT in update are always preserved.
14
- """
15
-
16
- import copy
17
- import json
18
- import logging
19
-
20
- logger = logging.getLogger(__name__)
21
-
22
-
23
- def deep_merge(current: dict, update: dict) -> tuple[dict, dict]:
24
- """Merge *update* into *current* returning ``(merged, diff)``.
25
-
26
- Parameters
27
- ----------
28
- current:
29
- The existing data (will NOT be mutated).
30
- update:
31
- New data to merge on top of *current*.
32
-
33
- Returns
34
- -------
35
- tuple[dict, dict]
36
- ``merged`` – the result of the merge.
37
- ``diff`` – audit trail recording changes (``{key: {old, new}}``).
38
- """
39
- merged = copy.deepcopy(current)
40
- diff: dict = {}
41
-
42
- for key, new_value in update.items():
43
- if key not in merged:
44
- # Rule 1: ADD missing key
45
- merged[key] = copy.deepcopy(new_value)
46
- continue
47
-
48
- old_value = merged[key]
49
-
50
- # Rule 2: Both dicts -> recurse
51
- if isinstance(old_value, dict) and isinstance(new_value, dict):
52
- sub_merged, sub_diff = deep_merge(old_value, new_value)
53
- merged[key] = sub_merged
54
- if sub_diff:
55
- diff[key] = sub_diff
56
- continue
57
-
58
- # Rule 3: Both lists -> union strategy
59
- if isinstance(old_value, list) and isinstance(new_value, list):
60
- merged_list = _merge_lists(old_value, new_value)
61
- if merged_list != old_value:
62
- diff[key] = {"old": old_value, "new": merged_list}
63
- merged[key] = merged_list
64
- continue
65
-
66
- # Rule 5: Type mismatch -> overwrite with warning
67
- if type(old_value) is not type(new_value):
68
- logger.warning(
69
- "Type mismatch for key '%s': %s -> %s. New value wins.",
70
- key,
71
- type(old_value).__name__,
72
- type(new_value).__name__,
73
- )
74
- diff[key] = {"old": old_value, "new": new_value}
75
- merged[key] = copy.deepcopy(new_value)
76
- continue
77
-
78
- # Rule 4: Both scalars -> overwrite
79
- if old_value != new_value:
80
- diff[key] = {"old": old_value, "new": new_value}
81
- merged[key] = copy.deepcopy(new_value)
82
-
83
- return merged, diff
84
-
85
-
86
- # ---------------------------------------------------------------------------
87
- # List merge helpers
88
- # ---------------------------------------------------------------------------
89
-
90
- def _merge_lists(current: list, update: list) -> list:
91
- """Merge two lists following the union strategy.
92
-
93
- a) All items are primitives (str, int, float, bool) -> sorted set union.
94
- b) Items are dicts with a ``"name"`` key -> merge by name, preserve missing.
95
- c) Otherwise -> concatenate, deduplicate by JSON equality.
96
- """
97
- if _all_primitives(current) and _all_primitives(update):
98
- return sorted(set(current) | set(update))
99
-
100
- if _all_dicts_with_name(current) and _all_dicts_with_name(update):
101
- return _merge_named_dicts(current, update)
102
-
103
- # Fallback: concatenate + deduplicate by JSON equality
104
- return _concat_deduplicate(current, update)
105
-
106
-
107
- def _all_primitives(items: list) -> bool:
108
- """Return True if every item is a primitive (str, int, float, bool)."""
109
- return all(isinstance(i, (str, int, float, bool)) for i in items)
110
-
111
-
112
- def _all_dicts_with_name(items: list) -> bool:
113
- """Return True if every item is a dict containing a ``"name"`` key."""
114
- return bool(items) and all(
115
- isinstance(i, dict) and "name" in i for i in items
116
- )
117
-
118
-
119
- def _merge_named_dicts(current: list[dict], update: list[dict]) -> list[dict]:
120
- """Merge lists of dicts by their ``"name"`` field.
121
-
122
- - Matching names: deep-merge the dict fields.
123
- - Names only in current: preserved (no-delete).
124
- - Names only in update: appended.
125
- """
126
- result_by_name: dict[str, dict] = {}
127
- order: list[str] = []
128
-
129
- # Seed with current entries (preserves order + no-delete)
130
- for item in current:
131
- name = item["name"]
132
- result_by_name[name] = copy.deepcopy(item)
133
- order.append(name)
134
-
135
- # Merge / add from update
136
- for item in update:
137
- name = item["name"]
138
- if name in result_by_name:
139
- merged_item, _ = deep_merge(result_by_name[name], item)
140
- result_by_name[name] = merged_item
141
- else:
142
- result_by_name[name] = copy.deepcopy(item)
143
- order.append(name)
144
-
145
- return [result_by_name[n] for n in order]
146
-
147
-
148
- def _concat_deduplicate(current: list, update: list) -> list:
149
- """Concatenate two lists, deduplicating by JSON equality."""
150
- seen: list[str] = []
151
- result: list = []
152
-
153
- for item in current + update:
154
- serialized = json.dumps(item, sort_keys=True)
155
- if serialized not in seen:
156
- seen.append(serialized)
157
- result.append(copy.deepcopy(item))
158
-
159
- return result
@@ -1,132 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- migrate_04_harness_events.py
4
-
5
- Convierte events.jsonl -> archivo SQL con INSERT batched.
6
-
7
- Reglas:
8
- - Solo I/O sobre filesystem.
9
- - NO importa sqlite3.
10
- - `id` es AUTOINCREMENT en la tabla; NO lo insertamos.
11
- - `payload` = json.dumps(record) entero -- preserva todos los campos.
12
- - Idempotencia: harness_events NO tiene UNIQUE constraint útil.
13
- Aplicar una sola vez. Re-ejecuciones requieren DELETE WHERE project=...
14
-
15
- CLI args (parametrización cross-workspace):
16
- --project workspace name (default: 'me')
17
- --src path al events.jsonl (default: ws/me)
18
- --out path al SQL de salida (default: /tmp/migrate_04_harness_events.sql)
19
- --fragment emite solo INSERTs (sin BEGIN/COMMIT)
20
- """
21
- from __future__ import annotations
22
-
23
- import argparse
24
- import json
25
- import sys
26
- from pathlib import Path
27
-
28
- DEFAULT_PROJECT = "me"
29
- DEFAULT_SRC = Path("/home/jorge/ws/me/.claude/events/events.jsonl")
30
- DEFAULT_OUT = Path("/tmp/migrate_04_harness_events.sql")
31
- BATCH_SIZE = 200
32
-
33
- COLUMNS = ["project", "ts", "type", "source", "agent", "result", "severity", "payload"]
34
-
35
-
36
- def sql_quote(value) -> str:
37
- if value is None:
38
- return "NULL"
39
- if isinstance(value, bool):
40
- return "1" if value else "0"
41
- if isinstance(value, (int, float)):
42
- if isinstance(value, float) and (value != value or value in (float("inf"), float("-inf"))):
43
- return "NULL"
44
- return str(value)
45
- s = str(value)
46
- return "'" + s.replace("'", "''") + "'"
47
-
48
-
49
- def extract_row(record: dict, project: str) -> dict:
50
- return {
51
- "project": project,
52
- "ts": record.get("ts"),
53
- "type": record.get("type"),
54
- "source": record.get("source"),
55
- "agent": record.get("agent"),
56
- "result": record.get("result"),
57
- "severity": record.get("severity"),
58
- "payload": json.dumps(record, ensure_ascii=False, separators=(",", ":")),
59
- }
60
-
61
-
62
- def row_values_sql(row: dict) -> str:
63
- return "(" + ",".join(sql_quote(row.get(col)) for col in COLUMNS) + ")"
64
-
65
-
66
- def main() -> int:
67
- parser = argparse.ArgumentParser(description="Generate INSERT SQL for harness_events table.")
68
- parser.add_argument("--project", default=DEFAULT_PROJECT)
69
- parser.add_argument("--src", default=str(DEFAULT_SRC), help="path to events.jsonl")
70
- parser.add_argument("--out", default=str(DEFAULT_OUT))
71
- parser.add_argument("--fragment", action="store_true")
72
- args = parser.parse_args()
73
-
74
- project = args.project
75
- src = Path(args.src)
76
- out = Path(args.out)
77
- fragment = args.fragment
78
-
79
- if not src.exists():
80
- print(f"[migrate_04:{project}] ERROR: source not found: {src}", file=sys.stderr)
81
- return 1
82
-
83
- rows = []
84
- skipped = 0
85
- total_lines = 0
86
-
87
- with src.open("r", encoding="utf-8") as f:
88
- for line in f:
89
- total_lines += 1
90
- s = line.strip()
91
- if not s:
92
- continue
93
- try:
94
- rec = json.loads(s)
95
- except json.JSONDecodeError:
96
- skipped += 1
97
- continue
98
- if not rec.get("ts") or not rec.get("type"):
99
- skipped += 1
100
- continue
101
- rows.append(extract_row(rec, project))
102
-
103
- cols_csv = ",".join(COLUMNS)
104
- insert_prefix = f"INSERT INTO harness_events ({cols_csv}) VALUES\n"
105
-
106
- with out.open("w", encoding="utf-8") as fh:
107
- fh.write(f"-- Generated by migrate_04_harness_events.py\n")
108
- fh.write(f"-- Project: {project}\n")
109
- fh.write(f"-- Source: {src}\n")
110
- fh.write(f"-- Total source lines: {total_lines}\n")
111
- fh.write(f"-- Records to insert: {len(rows)}\n")
112
- fh.write(f"-- Skipped: {skipped}\n")
113
- fh.write("--\n")
114
- fh.write("-- WARNING: harness_events sin PK natural; aplicar 2 veces duplica filas.\n")
115
- if not fragment:
116
- fh.write("BEGIN TRANSACTION;\n")
117
-
118
- for i in range(0, len(rows), BATCH_SIZE):
119
- batch = rows[i : i + BATCH_SIZE]
120
- fh.write(insert_prefix)
121
- fh.write(",\n".join(row_values_sql(r) for r in batch))
122
- fh.write(";\n")
123
-
124
- if not fragment:
125
- fh.write("COMMIT;\n")
126
-
127
- print(f"[migrate_04:{project}] wrote {out} ({len(rows)} rows, {skipped} skipped)")
128
- return 0
129
-
130
-
131
- if __name__ == "__main__":
132
- sys.exit(main())
@@ -1,23 +0,0 @@
1
- #!/usr/bin/env bash
2
- # migrate_04_harness_events.sh
3
- # Wrapper: regenera el .sql desde events.jsonl y lo carga en ~/.gaia/gaia.db.
4
- #
5
- # OJO: harness_events no tiene PK natural. Re-ejecutar este wrapper duplica
6
- # filas. Si necesitas re-ejecutar limpio, primero elimina las filas con:
7
- # sqlite3 ~/.gaia/gaia.db "DELETE FROM harness_events WHERE project='me';"
8
- set -euo pipefail
9
-
10
- HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
- PY_SCRIPT="${HERE}/migrate_04_harness_events.py"
12
- SQL_FILE="/tmp/migrate_04_harness_events.sql"
13
- DB_PATH="${HOME}/.gaia/gaia.db"
14
-
15
- # Paso 1: regenerar el .sql.
16
- echo "[migrate_04] regenerando ${SQL_FILE} ..."
17
- python3 "${PY_SCRIPT}"
18
-
19
- # Paso 2: aplicar el SQL (interceptado por el hook).
20
- echo "[migrate_04] aplicando ${SQL_FILE} en ${DB_PATH} ..."
21
- sqlite3 "${DB_PATH}" < "${SQL_FILE}"
22
-
23
- echo "[migrate_04] OK"