@jaguilar87/gaia 5.0.6 → 5.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +12 -0
- package/bin/cli/_install_helpers.py +1 -1
- package/bin/cli/approvals.py +145 -236
- package/bin/cli/doctor.py +19 -17
- package/bin/validate-sandbox.sh +8 -3
- package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-ops/hooks/adapters/claude_code.py +73 -1
- package/dist/gaia-ops/hooks/modules/agents/handoff_persister.py +13 -2
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +19 -0
- package/dist/gaia-ops/skills/agent-approval-protocol/SKILL.md +28 -12
- package/dist/gaia-ops/skills/agent-approval-protocol/reference.md +5 -3
- package/dist/gaia-ops/skills/agent-protocol/examples.md +12 -1
- package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +2 -6
- package/dist/gaia-ops/skills/gaia-patterns/reference.md +2 -14
- package/dist/gaia-ops/skills/orchestrator-present-approval/SKILL.md +8 -2
- package/dist/gaia-ops/skills/orchestrator-present-approval/template.md +11 -10
- package/dist/gaia-ops/skills/subagent-request-approval/SKILL.md +11 -0
- package/dist/gaia-ops/skills/subagent-request-approval/reference.md +21 -3
- package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-security/hooks/adapters/claude_code.py +73 -1
- package/dist/gaia-security/hooks/modules/agents/handoff_persister.py +13 -2
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +19 -0
- package/gaia/approvals/__init__.py +2 -1
- package/gaia/approvals/store.py +78 -6
- package/hooks/adapters/claude_code.py +73 -1
- package/hooks/modules/agents/handoff_persister.py +13 -2
- package/hooks/modules/tools/bash_validator.py +19 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/agent-approval-protocol/SKILL.md +28 -12
- package/skills/agent-approval-protocol/reference.md +5 -3
- package/skills/agent-protocol/examples.md +12 -1
- package/skills/gaia-patterns/SKILL.md +2 -6
- package/skills/gaia-patterns/reference.md +2 -14
- package/skills/orchestrator-present-approval/SKILL.md +8 -2
- package/skills/orchestrator-present-approval/template.md +11 -10
- package/skills/subagent-request-approval/SKILL.md +11 -0
- package/skills/subagent-request-approval/reference.md +21 -3
- package/gaia/approvals/revert.py +0 -282
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "gaia-ops",
|
|
10
10
|
"description": "Full DevOps orchestration for Claude Code. Eight specialized agents handle the complete development lifecycle — analysis, planning, execution, and deployment. Gaia-Ops scans your codebase to understand it and injects the right context into each sub-agent. Every command is classified by risk: read-only runs freely, state changes pause for your approval, and irreversible operations are permanently blocked.",
|
|
11
|
-
"version": "5.0.
|
|
11
|
+
"version": "5.0.8",
|
|
12
12
|
"category": "devops",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "jaguilar87",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
{
|
|
21
21
|
"name": "gaia-security",
|
|
22
22
|
"description": "Keeps you in the loop only when it matters. Gaia Security analyzes every command and classifies it into risk tiers: read-only queries run freely, simulations and validations pass through, and state-changing operations (create, delete, apply, push) pause for your explicit approval before executing. Irreversible commands like dropping databases or deleting cloud infrastructure are permanently blocked.",
|
|
23
|
-
"version": "5.0.
|
|
23
|
+
"version": "5.0.8",
|
|
24
24
|
"category": "security",
|
|
25
25
|
"author": {
|
|
26
26
|
"name": "jaguilar87",
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.0.8] - 2026-06-24
|
|
11
|
+
|
|
12
|
+
## [5.0.7] - 2026-06-12
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- `gaia doctor` now passes (rc=0) on a clean install: the `commands` symlink (a removed surface) was dropped from the symlink checks, and `memory_fts5_count` now reads the canonical gaia.db store instead of the legacy search.db — eliminating the two warnings that made a fresh install report "degraded" in 5.0.5/5.0.6.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Release pipeline: the sandbox validation harness is now a PRE-publish gate (runs against the packed tarball before `npm publish`), and the harness fails if `gaia doctor` returns rc>=1 — so a degraded build can no longer be published.
|
|
21
|
+
|
|
10
22
|
## [5.0.6] - 2026-06-12
|
|
11
23
|
|
|
12
24
|
### Fixed
|
|
@@ -345,7 +345,7 @@ def merge_local_hooks(
|
|
|
345
345
|
# ---------------------------------------------------------------------------
|
|
346
346
|
|
|
347
347
|
# Directories the package exposes via .claude/<name> symlinks
|
|
348
|
-
_SYMLINK_NAMES = ["agents", "tools", "hooks", "
|
|
348
|
+
_SYMLINK_NAMES = ["agents", "tools", "hooks", "config", "skills"]
|
|
349
349
|
# Files (not dirs) we link or copy into .claude/
|
|
350
350
|
_SYMLINK_FILES = ["CHANGELOG.md"]
|
|
351
351
|
|
package/bin/cli/approvals.py
CHANGED
|
@@ -889,12 +889,6 @@ def _import_approval_display():
|
|
|
889
889
|
return display
|
|
890
890
|
|
|
891
891
|
|
|
892
|
-
def _import_approval_revert():
|
|
893
|
-
"""Import gaia.approvals.revert lazily."""
|
|
894
|
-
from gaia.approvals import revert as revert_mod
|
|
895
|
-
return revert_mod
|
|
896
|
-
|
|
897
|
-
|
|
898
892
|
# ---------------------------------------------------------------------------
|
|
899
893
|
# T3.1: gaia approvals pending -- shortcut for list --status=pending
|
|
900
894
|
# ---------------------------------------------------------------------------
|
|
@@ -1209,199 +1203,6 @@ def cmd_history(args) -> int:
|
|
|
1209
1203
|
return 0
|
|
1210
1204
|
|
|
1211
1205
|
|
|
1212
|
-
# ---------------------------------------------------------------------------
|
|
1213
|
-
# T3.5: gaia approvals revert <id> -- interactive inverse-command UX (D14)
|
|
1214
|
-
# ---------------------------------------------------------------------------
|
|
1215
|
-
|
|
1216
|
-
def cmd_revert(args) -> int:
|
|
1217
|
-
"""Revert an approval by executing inverse commands for its EXECUTED events.
|
|
1218
|
-
|
|
1219
|
-
Per D14:
|
|
1220
|
-
- Interactive by default: shows numbered list of candidate inverse commands,
|
|
1221
|
-
user selects by number, comma-separated, 'all', or 'none'.
|
|
1222
|
-
- ``--yes`` suppresses per-event confirmation.
|
|
1223
|
-
- ``--file ids.txt`` reads event_ids from a file (one per line) for batch mode.
|
|
1224
|
-
- ``--dry-run`` shows the inverse commands without executing them.
|
|
1225
|
-
|
|
1226
|
-
Exits 0 on success or when no reversible events exist.
|
|
1227
|
-
Exits 1 on error.
|
|
1228
|
-
"""
|
|
1229
|
-
raw_id = _resolve_approval_id(args.approval_id)
|
|
1230
|
-
skip_confirm = getattr(args, "yes", False)
|
|
1231
|
-
dry_run = getattr(args, "dry_run", False)
|
|
1232
|
-
batch_file = getattr(args, "file", None)
|
|
1233
|
-
output_json = getattr(args, "json", False)
|
|
1234
|
-
|
|
1235
|
-
# Resolve the approval and its EXECUTED events.
|
|
1236
|
-
try:
|
|
1237
|
-
store = _import_approval_store()
|
|
1238
|
-
approval = store.get_by_id(raw_id)
|
|
1239
|
-
if approval is None:
|
|
1240
|
-
_print_error(f"No approval found for id: {raw_id}", args)
|
|
1241
|
-
return 1
|
|
1242
|
-
except Exception as exc:
|
|
1243
|
-
_print_error(f"Failed to look up approval: {exc}", args)
|
|
1244
|
-
return 1
|
|
1245
|
-
|
|
1246
|
-
# Derive inverse commands.
|
|
1247
|
-
try:
|
|
1248
|
-
revert_mod = _import_approval_revert()
|
|
1249
|
-
store = _import_approval_store()
|
|
1250
|
-
con = store._open_db()
|
|
1251
|
-
try:
|
|
1252
|
-
inverses = revert_mod.derive_inverses_for_approval(raw_id, con)
|
|
1253
|
-
finally:
|
|
1254
|
-
con.close()
|
|
1255
|
-
except Exception as exc:
|
|
1256
|
-
_print_error(f"Failed to derive inverse commands: {exc}", args)
|
|
1257
|
-
return 1
|
|
1258
|
-
|
|
1259
|
-
if not inverses:
|
|
1260
|
-
print(f"No EXECUTED events found for approval {raw_id}. Nothing to revert.")
|
|
1261
|
-
return 0
|
|
1262
|
-
|
|
1263
|
-
# Display the candidate inverse commands.
|
|
1264
|
-
print(f"\nCandidate inverse commands for approval {raw_id}:")
|
|
1265
|
-
print("-" * 60)
|
|
1266
|
-
for i, ic in enumerate(inverses):
|
|
1267
|
-
reversible_marker = "" if ic.reversible else " [NOT REVERSIBLE]"
|
|
1268
|
-
inverse_display = ic.inverse_command if ic.inverse_command else "N/A"
|
|
1269
|
-
print(f" [{i}] Original : {ic.original_command}")
|
|
1270
|
-
print(f" Inverse : {inverse_display}{reversible_marker}")
|
|
1271
|
-
print(f" Notes : {ic.notes}")
|
|
1272
|
-
print()
|
|
1273
|
-
|
|
1274
|
-
# Filter to only reversible ones.
|
|
1275
|
-
reversible = [ic for ic in inverses if ic.reversible and ic.inverse_command]
|
|
1276
|
-
if not reversible:
|
|
1277
|
-
print("None of the events have derivable inverse commands.")
|
|
1278
|
-
return 0
|
|
1279
|
-
|
|
1280
|
-
if dry_run:
|
|
1281
|
-
print("[dry-run] Would execute the following inverse commands:")
|
|
1282
|
-
for ic in reversible:
|
|
1283
|
-
print(f" {ic.inverse_command}")
|
|
1284
|
-
return 0
|
|
1285
|
-
|
|
1286
|
-
# Batch file mode: read event_ids to revert.
|
|
1287
|
-
selected = reversible
|
|
1288
|
-
if batch_file:
|
|
1289
|
-
try:
|
|
1290
|
-
with open(batch_file) as fh:
|
|
1291
|
-
event_ids_str = {line.strip() for line in fh if line.strip()}
|
|
1292
|
-
except OSError as exc:
|
|
1293
|
-
_print_error(f"Cannot read batch file {batch_file!r}: {exc}", args)
|
|
1294
|
-
return 1
|
|
1295
|
-
event_ids = set()
|
|
1296
|
-
for eid in event_ids_str:
|
|
1297
|
-
try:
|
|
1298
|
-
event_ids.add(int(eid))
|
|
1299
|
-
except ValueError:
|
|
1300
|
-
pass
|
|
1301
|
-
selected = [ic for ic in reversible if ic.event_id in event_ids]
|
|
1302
|
-
if not selected:
|
|
1303
|
-
print(f"No matching reversible events found in batch file.")
|
|
1304
|
-
return 0
|
|
1305
|
-
elif not skip_confirm:
|
|
1306
|
-
# Interactive selection.
|
|
1307
|
-
print("Select events to revert (comma-separated numbers, 'all', or 'none'):")
|
|
1308
|
-
try:
|
|
1309
|
-
choice = input("> ").strip().lower()
|
|
1310
|
-
except EOFError:
|
|
1311
|
-
choice = "none"
|
|
1312
|
-
|
|
1313
|
-
if choice == "none" or choice == "":
|
|
1314
|
-
print("Revert cancelled.")
|
|
1315
|
-
return 0
|
|
1316
|
-
elif choice == "all":
|
|
1317
|
-
selected = reversible
|
|
1318
|
-
else:
|
|
1319
|
-
try:
|
|
1320
|
-
indices = [int(x.strip()) for x in choice.split(",") if x.strip()]
|
|
1321
|
-
selected = [reversible[i] for i in indices if 0 <= i < len(reversible)]
|
|
1322
|
-
except (ValueError, IndexError):
|
|
1323
|
-
_print_error("Invalid selection. Use numbers, 'all', or 'none'.", args)
|
|
1324
|
-
return 1
|
|
1325
|
-
|
|
1326
|
-
if not selected:
|
|
1327
|
-
print("No events selected. Revert cancelled.")
|
|
1328
|
-
return 0
|
|
1329
|
-
|
|
1330
|
-
# Final confirmation.
|
|
1331
|
-
if not skip_confirm:
|
|
1332
|
-
print("\nWill execute:")
|
|
1333
|
-
for ic in selected:
|
|
1334
|
-
print(f" {ic.inverse_command}")
|
|
1335
|
-
try:
|
|
1336
|
-
confirm = input("\nProceed? [y/N] ").strip().lower()
|
|
1337
|
-
except EOFError:
|
|
1338
|
-
confirm = "n"
|
|
1339
|
-
if confirm not in ("y", "yes"):
|
|
1340
|
-
print("Revert cancelled.")
|
|
1341
|
-
return 0
|
|
1342
|
-
|
|
1343
|
-
# Execute inverse commands.
|
|
1344
|
-
import subprocess
|
|
1345
|
-
results = []
|
|
1346
|
-
session_id = os.environ.get("CLAUDE_SESSION_ID") or "cli-session"
|
|
1347
|
-
all_ok = True
|
|
1348
|
-
|
|
1349
|
-
for ic in selected:
|
|
1350
|
-
print(f"Executing: {ic.inverse_command}")
|
|
1351
|
-
try:
|
|
1352
|
-
proc = subprocess.run(
|
|
1353
|
-
ic.inverse_command,
|
|
1354
|
-
shell=True,
|
|
1355
|
-
capture_output=True,
|
|
1356
|
-
text=True,
|
|
1357
|
-
)
|
|
1358
|
-
ok = proc.returncode == 0
|
|
1359
|
-
results.append({
|
|
1360
|
-
"event_id": ic.event_id,
|
|
1361
|
-
"command": ic.inverse_command,
|
|
1362
|
-
"exit_code": proc.returncode,
|
|
1363
|
-
"stdout": proc.stdout.strip(),
|
|
1364
|
-
"stderr": proc.stderr.strip(),
|
|
1365
|
-
})
|
|
1366
|
-
if ok:
|
|
1367
|
-
print(f" OK (exit 0)")
|
|
1368
|
-
# Record REVERTED event in the chain.
|
|
1369
|
-
try:
|
|
1370
|
-
store = _import_approval_store()
|
|
1371
|
-
import json as _json
|
|
1372
|
-
metadata = _json.dumps({
|
|
1373
|
-
"original_event_id": ic.event_id,
|
|
1374
|
-
"inverse_command": ic.inverse_command,
|
|
1375
|
-
})
|
|
1376
|
-
store.record_event(
|
|
1377
|
-
raw_id,
|
|
1378
|
-
"REVERTED",
|
|
1379
|
-
session_id=session_id,
|
|
1380
|
-
metadata_json=metadata,
|
|
1381
|
-
)
|
|
1382
|
-
except Exception:
|
|
1383
|
-
pass # Chain write failure is non-fatal for the revert operation.
|
|
1384
|
-
else:
|
|
1385
|
-
print(f" FAILED (exit {proc.returncode})")
|
|
1386
|
-
if proc.stderr:
|
|
1387
|
-
print(f" stderr: {proc.stderr.strip()}")
|
|
1388
|
-
all_ok = False
|
|
1389
|
-
except Exception as exc:
|
|
1390
|
-
print(f" ERROR: {exc}")
|
|
1391
|
-
results.append({
|
|
1392
|
-
"event_id": ic.event_id,
|
|
1393
|
-
"command": ic.inverse_command,
|
|
1394
|
-
"exit_code": -1,
|
|
1395
|
-
"error": str(exc),
|
|
1396
|
-
})
|
|
1397
|
-
all_ok = False
|
|
1398
|
-
|
|
1399
|
-
if output_json:
|
|
1400
|
-
print(json.dumps({"results": results, "all_ok": all_ok}))
|
|
1401
|
-
|
|
1402
|
-
return 0 if all_ok else 1
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
1206
|
# ---------------------------------------------------------------------------
|
|
1406
1207
|
# T3.5: gaia approvals replay <id> [--dry-run]
|
|
1407
1208
|
# ---------------------------------------------------------------------------
|
|
@@ -1522,6 +1323,114 @@ def cmd_replay(args) -> int:
|
|
|
1522
1323
|
return 0 if all_ok else 1
|
|
1523
1324
|
|
|
1524
1325
|
|
|
1326
|
+
# ---------------------------------------------------------------------------
|
|
1327
|
+
# derive-id -- reproduce a plan-first COMMAND_SET approval_id from its commands
|
|
1328
|
+
# ---------------------------------------------------------------------------
|
|
1329
|
+
|
|
1330
|
+
def _read_command_set_input(args) -> list:
|
|
1331
|
+
"""Resolve the command list for derive-id from args/stdin.
|
|
1332
|
+
|
|
1333
|
+
Accepts, in order of precedence:
|
|
1334
|
+
1. ``--commands-json '[{"command": "..."}, ...]'`` or a bare list of
|
|
1335
|
+
strings ``["cmd a", "cmd b"]`` -- the command_set as the orchestrator
|
|
1336
|
+
reads it from the contract.
|
|
1337
|
+
2. stdin (when ``--commands-json`` is omitted), same JSON shapes.
|
|
1338
|
+
|
|
1339
|
+
Returns the ordered list of command STRINGS (rationale is irrelevant to the
|
|
1340
|
+
derivation). Raises ValueError on malformed input.
|
|
1341
|
+
"""
|
|
1342
|
+
raw = getattr(args, "commands_json", None)
|
|
1343
|
+
if raw is None:
|
|
1344
|
+
raw = sys.stdin.read()
|
|
1345
|
+
raw = (raw or "").strip()
|
|
1346
|
+
if not raw:
|
|
1347
|
+
raise ValueError("no command_set provided (use --commands-json or stdin)")
|
|
1348
|
+
|
|
1349
|
+
parsed = json.loads(raw)
|
|
1350
|
+
|
|
1351
|
+
# Accept either a top-level list, or {"command_set": [...]} / {"commands": [...]}.
|
|
1352
|
+
if isinstance(parsed, dict):
|
|
1353
|
+
parsed = parsed.get("command_set") or parsed.get("commands") or []
|
|
1354
|
+
|
|
1355
|
+
if not isinstance(parsed, list):
|
|
1356
|
+
raise ValueError("command_set must be a JSON array")
|
|
1357
|
+
|
|
1358
|
+
commands: list = []
|
|
1359
|
+
for item in parsed:
|
|
1360
|
+
if isinstance(item, str):
|
|
1361
|
+
if item:
|
|
1362
|
+
commands.append(item)
|
|
1363
|
+
elif isinstance(item, dict) and item.get("command"):
|
|
1364
|
+
commands.append(item["command"])
|
|
1365
|
+
return commands
|
|
1366
|
+
|
|
1367
|
+
|
|
1368
|
+
def cmd_derive_id(args) -> int:
|
|
1369
|
+
"""Derive the deterministic COMMAND_SET approval_id from its commands.
|
|
1370
|
+
|
|
1371
|
+
This is the orchestrator-side mirror of the intake's mint: given the
|
|
1372
|
+
``command_set`` the subagent emitted in its contract (no DB search), it
|
|
1373
|
+
reproduces the EXACT ``P-...`` id the SubagentStop intake wrote as the
|
|
1374
|
+
pending row, by applying the SAME mutative filter and the SAME
|
|
1375
|
+
``derive_command_set_id`` canonicalization the intake uses.
|
|
1376
|
+
|
|
1377
|
+
The mutative filter is shared with the intake
|
|
1378
|
+
(``handoff_persister._filter_mutative_command_set``) so the CLI and the hook
|
|
1379
|
+
operate on the identical post-filter command list. When fewer than 2
|
|
1380
|
+
mutative commands remain, NO COMMAND_SET was minted (the singular path owns
|
|
1381
|
+
it) -- the helper reports that rather than emitting a bogus id.
|
|
1382
|
+
|
|
1383
|
+
Exits 0 on success, 1 on error.
|
|
1384
|
+
"""
|
|
1385
|
+
output_json = getattr(args, "json", False)
|
|
1386
|
+
apply_filter = not getattr(args, "no_filter", False)
|
|
1387
|
+
|
|
1388
|
+
try:
|
|
1389
|
+
commands = _read_command_set_input(args)
|
|
1390
|
+
except Exception as exc:
|
|
1391
|
+
_print_error(f"Failed to parse command_set: {exc}", args)
|
|
1392
|
+
return 1
|
|
1393
|
+
|
|
1394
|
+
# Apply the SAME mutative filter the intake uses, so the orchestrator's
|
|
1395
|
+
# derivation operates on the identical post-filter list. Skippable via
|
|
1396
|
+
# --no-filter for callers that already hold the filtered list.
|
|
1397
|
+
if apply_filter:
|
|
1398
|
+
try:
|
|
1399
|
+
from modules.agents.handoff_persister import _filter_mutative_command_set
|
|
1400
|
+
filtered = _filter_mutative_command_set(
|
|
1401
|
+
[{"command": c, "rationale": ""} for c in commands]
|
|
1402
|
+
)
|
|
1403
|
+
commands = [it["command"] for it in filtered]
|
|
1404
|
+
except Exception as exc:
|
|
1405
|
+
_print_error(f"Failed to apply mutative filter: {exc}", args)
|
|
1406
|
+
return 1
|
|
1407
|
+
|
|
1408
|
+
if len(commands) < 2:
|
|
1409
|
+
msg = (
|
|
1410
|
+
f"Not a COMMAND_SET: {len(commands)} mutative command(s) after filter "
|
|
1411
|
+
"(need >= 2). No COMMAND_SET approval was minted -- the singular path "
|
|
1412
|
+
"owns this."
|
|
1413
|
+
)
|
|
1414
|
+
if output_json:
|
|
1415
|
+
print(json.dumps({"approval_id": None, "command_count": len(commands), "reason": msg}))
|
|
1416
|
+
else:
|
|
1417
|
+
_print_error(msg, args)
|
|
1418
|
+
return 1
|
|
1419
|
+
|
|
1420
|
+
try:
|
|
1421
|
+
store = _import_approval_store()
|
|
1422
|
+
approval_id = store.derive_command_set_id(commands)
|
|
1423
|
+
except Exception as exc:
|
|
1424
|
+
_print_error(f"Failed to derive id: {exc}", args)
|
|
1425
|
+
return 1
|
|
1426
|
+
|
|
1427
|
+
if output_json:
|
|
1428
|
+
print(json.dumps({"approval_id": approval_id, "command_count": len(commands)}))
|
|
1429
|
+
else:
|
|
1430
|
+
print(approval_id)
|
|
1431
|
+
return 0
|
|
1432
|
+
|
|
1433
|
+
|
|
1525
1434
|
# ---------------------------------------------------------------------------
|
|
1526
1435
|
# Plugin registration (called by bin/gaia dispatcher)
|
|
1527
1436
|
# ---------------------------------------------------------------------------
|
|
@@ -1531,7 +1440,7 @@ def register(subparsers) -> None:
|
|
|
1531
1440
|
p = subparsers.add_parser(
|
|
1532
1441
|
"approvals",
|
|
1533
1442
|
help="Manage T3 pending approvals",
|
|
1534
|
-
description="View, approve, reject,
|
|
1443
|
+
description="View, approve, reject, and replay Gaia approval requests.",
|
|
1535
1444
|
)
|
|
1536
1445
|
sub = p.add_subparsers(dest="approvals_cmd", metavar="SUBCOMMAND")
|
|
1537
1446
|
sub.required = True
|
|
@@ -1652,33 +1561,6 @@ def register(subparsers) -> None:
|
|
|
1652
1561
|
p_history.add_argument("--json", action="store_true", help="JSON output")
|
|
1653
1562
|
p_history.set_defaults(func=cmd_history)
|
|
1654
1563
|
|
|
1655
|
-
# revert (T3.5) -- interactive inverse-command UX
|
|
1656
|
-
p_revert = sub.add_parser(
|
|
1657
|
-
"revert",
|
|
1658
|
-
help="Revert an approval by executing inverse commands (interactive)",
|
|
1659
|
-
description=(
|
|
1660
|
-
"Per D14: interactive inverse-command UX for reverting executed approvals.\n\n"
|
|
1661
|
-
"Shows candidate inverse commands, prompts for selection, then executes\n"
|
|
1662
|
-
"the selected inverses sequentially. Uses --yes to skip per-event prompts.\n"
|
|
1663
|
-
"Use --dry-run to preview without executing."
|
|
1664
|
-
),
|
|
1665
|
-
)
|
|
1666
|
-
p_revert.add_argument(
|
|
1667
|
-
"approval_id",
|
|
1668
|
-
metavar="APPROVAL_ID",
|
|
1669
|
-
help="P-{uuid4hex} of the approval to revert",
|
|
1670
|
-
)
|
|
1671
|
-
p_revert.add_argument("--yes", action="store_true", help="Skip confirmation prompts")
|
|
1672
|
-
p_revert.add_argument("--dry-run", action="store_true", dest="dry_run", help="Preview only")
|
|
1673
|
-
p_revert.add_argument(
|
|
1674
|
-
"--file",
|
|
1675
|
-
metavar="PATH",
|
|
1676
|
-
default=None,
|
|
1677
|
-
help="File of event_ids to revert (one per line) for batch mode",
|
|
1678
|
-
)
|
|
1679
|
-
p_revert.add_argument("--json", action="store_true", help="JSON output for results")
|
|
1680
|
-
p_revert.set_defaults(func=cmd_revert)
|
|
1681
|
-
|
|
1682
1564
|
# replay (T3.5) -- re-run commands from an executed approval
|
|
1683
1565
|
p_replay = sub.add_parser(
|
|
1684
1566
|
"replay",
|
|
@@ -1763,6 +1645,35 @@ def register(subparsers) -> None:
|
|
|
1763
1645
|
p_stats.add_argument("--json", action="store_true", help="JSON output")
|
|
1764
1646
|
p_stats.set_defaults(func=cmd_stats)
|
|
1765
1647
|
|
|
1648
|
+
# derive-id -- reproduce a plan-first COMMAND_SET id from its commands
|
|
1649
|
+
p_derive = sub.add_parser(
|
|
1650
|
+
"derive-id",
|
|
1651
|
+
help="Derive the deterministic COMMAND_SET approval_id from its commands",
|
|
1652
|
+
description=(
|
|
1653
|
+
"Reproduce the content-derived approval_id the SubagentStop intake\n"
|
|
1654
|
+
"minted for a plan-first COMMAND_SET, from the command_set in the\n"
|
|
1655
|
+
"contract -- no DB search. Pass the command_set as JSON via\n"
|
|
1656
|
+
"--commands-json or stdin (a list of strings, a list of\n"
|
|
1657
|
+
"{command, rationale} objects, or an object with a command_set/\n"
|
|
1658
|
+
"commands key). Applies the same mutative filter the intake uses."
|
|
1659
|
+
),
|
|
1660
|
+
)
|
|
1661
|
+
p_derive.add_argument(
|
|
1662
|
+
"--commands-json",
|
|
1663
|
+
dest="commands_json",
|
|
1664
|
+
metavar="JSON",
|
|
1665
|
+
default=None,
|
|
1666
|
+
help="command_set as JSON (omit to read from stdin)",
|
|
1667
|
+
)
|
|
1668
|
+
p_derive.add_argument(
|
|
1669
|
+
"--no-filter",
|
|
1670
|
+
action="store_true",
|
|
1671
|
+
dest="no_filter",
|
|
1672
|
+
help="Skip the mutative filter (input is already the filtered list)",
|
|
1673
|
+
)
|
|
1674
|
+
p_derive.add_argument("--json", action="store_true", help="JSON output")
|
|
1675
|
+
p_derive.set_defaults(func=cmd_derive_id)
|
|
1676
|
+
|
|
1766
1677
|
p.set_defaults(func=_approvals_default)
|
|
1767
1678
|
|
|
1768
1679
|
|
|
@@ -1788,13 +1699,13 @@ def _approvals_default(args) -> int:
|
|
|
1788
1699
|
print(" approve APPROVAL_ID -- cross-session approve")
|
|
1789
1700
|
print(" revoke APPROVAL_ID -- revoke a pending approval")
|
|
1790
1701
|
print(" history [APPROVAL_ID] [--limit N] -- temporal history or per-approval chain")
|
|
1791
|
-
print(" revert APPROVAL_ID [--dry-run] -- interactive inverse-command revert")
|
|
1792
1702
|
print(" replay APPROVAL_ID [--dry-run] -- replay an executed approval")
|
|
1793
1703
|
print(" list [--session S] [--orphans-only] -- list (legacy + DB grants)")
|
|
1794
1704
|
print(" reject NONCE [--all] -- reject pending (legacy)")
|
|
1795
1705
|
print(" reject-all [--dry-run] -- bulk reject (legacy)")
|
|
1796
1706
|
print(" clean [--dry-run] -- remove expired approvals")
|
|
1797
1707
|
print(" stats -- approval system statistics")
|
|
1708
|
+
print(" derive-id --commands-json JSON -- reproduce a COMMAND_SET id (no DB)")
|
|
1798
1709
|
print("")
|
|
1799
1710
|
print("Run 'gaia approvals --help' for more information.")
|
|
1800
1711
|
return 0
|
|
@@ -1850,14 +1761,6 @@ def _build_standalone_parser() -> argparse.ArgumentParser:
|
|
|
1850
1761
|
p_history.add_argument("--json", action="store_true")
|
|
1851
1762
|
p_history.set_defaults(func=cmd_history)
|
|
1852
1763
|
|
|
1853
|
-
p_revert = subparsers.add_parser("revert", help="Revert an approval (interactive)")
|
|
1854
|
-
p_revert.add_argument("approval_id", metavar="APPROVAL_ID")
|
|
1855
|
-
p_revert.add_argument("--yes", action="store_true")
|
|
1856
|
-
p_revert.add_argument("--dry-run", action="store_true", dest="dry_run")
|
|
1857
|
-
p_revert.add_argument("--file", metavar="PATH", default=None)
|
|
1858
|
-
p_revert.add_argument("--json", action="store_true")
|
|
1859
|
-
p_revert.set_defaults(func=cmd_revert)
|
|
1860
|
-
|
|
1861
1764
|
p_replay = subparsers.add_parser("replay", help="Replay an executed approval")
|
|
1862
1765
|
p_replay.add_argument("approval_id", metavar="APPROVAL_ID")
|
|
1863
1766
|
p_replay.add_argument("--dry-run", action="store_true", dest="dry_run")
|
|
@@ -1886,6 +1789,12 @@ def _build_standalone_parser() -> argparse.ArgumentParser:
|
|
|
1886
1789
|
p_stats.add_argument("--json", action="store_true")
|
|
1887
1790
|
p_stats.set_defaults(func=cmd_stats)
|
|
1888
1791
|
|
|
1792
|
+
p_derive = subparsers.add_parser("derive-id", help="Derive a COMMAND_SET approval_id from its commands")
|
|
1793
|
+
p_derive.add_argument("--commands-json", dest="commands_json", metavar="JSON", default=None)
|
|
1794
|
+
p_derive.add_argument("--no-filter", action="store_true", dest="no_filter")
|
|
1795
|
+
p_derive.add_argument("--json", action="store_true")
|
|
1796
|
+
p_derive.set_defaults(func=cmd_derive_id)
|
|
1797
|
+
|
|
1889
1798
|
return parser
|
|
1890
1799
|
|
|
1891
1800
|
|
package/bin/cli/doctor.py
CHANGED
|
@@ -762,7 +762,7 @@ def _extract_check_values(
|
|
|
762
762
|
@register_check("Symlinks", order=50)
|
|
763
763
|
def check_symlinks(project_root: Path) -> dict:
|
|
764
764
|
"""Check .claude/ symlinks resolve to package content."""
|
|
765
|
-
names = ["agents", "tools", "hooks", "
|
|
765
|
+
names = ["agents", "tools", "hooks", "config", "skills", "CHANGELOG.md"]
|
|
766
766
|
critical = {"agents", "hooks", "skills"}
|
|
767
767
|
valid = 0
|
|
768
768
|
has_critical_missing = False
|
|
@@ -1053,34 +1053,36 @@ def check_memory_fts5_db(project_root: Path) -> dict:
|
|
|
1053
1053
|
|
|
1054
1054
|
@register_check("memory_fts5_count", order=130)
|
|
1055
1055
|
def check_memory_fts5_count(project_root: Path) -> dict:
|
|
1056
|
-
"""Check FTS5 indexed count against total episode count in
|
|
1057
|
-
index_path = project_root / ".claude" / "project-context" / "episodic-memory" / "index.json"
|
|
1058
|
-
|
|
1059
|
-
if not index_path.is_file():
|
|
1060
|
-
return _result("memory_fts5_count", "info", "index.json not found — no episodes yet")
|
|
1061
|
-
|
|
1062
|
-
index_data = _read_json(index_path)
|
|
1063
|
-
if not index_data:
|
|
1064
|
-
return _result("memory_fts5_count", "info", "index.json unreadable")
|
|
1065
|
-
|
|
1066
|
-
total = len(index_data.get("episodes") or [])
|
|
1056
|
+
"""Check FTS5 indexed count against total episode count in gaia.db.
|
|
1067
1057
|
|
|
1058
|
+
T6 migration: replaced legacy search_store.count()/index.json check
|
|
1059
|
+
(which read search.db and ignored GAIA_DATA_DIR) with queries against
|
|
1060
|
+
the canonical gaia.db -- episodes_fts for indexed, episodes for total.
|
|
1061
|
+
"""
|
|
1068
1062
|
try:
|
|
1069
1063
|
import sys as _sys
|
|
1070
|
-
# Ensure package root is on path for lazy import
|
|
1071
1064
|
pkg_root = str(_package_root())
|
|
1072
1065
|
if pkg_root not in _sys.path:
|
|
1073
1066
|
_sys.path.insert(0, pkg_root)
|
|
1074
|
-
from
|
|
1075
|
-
indexed = search_store.count()
|
|
1067
|
+
from gaia.store.writer import _connect as _store_connect
|
|
1076
1068
|
except ImportError:
|
|
1077
1069
|
return _result(
|
|
1078
1070
|
"memory_fts5_count",
|
|
1079
1071
|
"info",
|
|
1080
|
-
"
|
|
1072
|
+
"gaia.store.writer not importable — FTS5 count skipped",
|
|
1081
1073
|
)
|
|
1074
|
+
|
|
1075
|
+
try:
|
|
1076
|
+
con = _store_connect()
|
|
1077
|
+
try:
|
|
1078
|
+
indexed_row = con.execute("SELECT COUNT(*) FROM episodes_fts").fetchone()
|
|
1079
|
+
indexed = indexed_row[0] if indexed_row else 0
|
|
1080
|
+
total_row = con.execute("SELECT COUNT(*) FROM episodes").fetchone()
|
|
1081
|
+
total = total_row[0] if total_row else 0
|
|
1082
|
+
finally:
|
|
1083
|
+
con.close()
|
|
1082
1084
|
except Exception as exc:
|
|
1083
|
-
return _result("memory_fts5_count", "info", f"Could not query FTS5 count: {exc}")
|
|
1085
|
+
return _result("memory_fts5_count", "info", f"Could not query FTS5 count from gaia.db: {exc}")
|
|
1084
1086
|
|
|
1085
1087
|
if total == 0:
|
|
1086
1088
|
return _result("memory_fts5_count", "pass", "No episodes to index")
|
package/bin/validate-sandbox.sh
CHANGED
|
@@ -590,11 +590,16 @@ else
|
|
|
590
590
|
rc=$?
|
|
591
591
|
fi
|
|
592
592
|
ms=$(( $(now_ms) - t0 ))
|
|
593
|
-
#
|
|
594
|
-
|
|
593
|
+
# The harness gate must not pass a degraded doctor: a non-zero rc means at
|
|
594
|
+
# least one check is warning/error, so treat rc>=1 as a hard FAILURE. Only a
|
|
595
|
+
# clean rc=0 (all checks pass/info) with parseable JSON counts as PASS.
|
|
596
|
+
if [[ "${rc}" -ne 0 ]]; then
|
|
597
|
+
nonpass="$(python3 -c "import json,sys; d=json.loads(sys.argv[1]); print(', '.join(f\"{r['name']}={r['severity']}\" for r in d['checks'] if r['severity'] not in ('pass','info')))" "${out}" 2>/dev/null || echo "unparseable output")"
|
|
598
|
+
record "gaia doctor --json" "FAIL" "doctor returned rc=${rc}; non-pass: ${nonpass:-none}" "${ms}"
|
|
599
|
+
elif python3 -c "import json,sys; d=json.loads(sys.argv[1]); c=d['checks']; t=len(c); sys.exit(0 if t>=5 else 1)" "${out}" 2>/dev/null; then
|
|
595
600
|
total=$(python3 -c "import json,sys; d=json.loads(sys.argv[1]); print(len(d['checks']))" "${out}")
|
|
596
601
|
passed=$(python3 -c "import json,sys; d=json.loads(sys.argv[1]); print(sum(1 for r in d['checks'] if r['severity']=='pass'))" "${out}")
|
|
597
|
-
record "gaia doctor --json" "PASS" "${passed}/${total} checks passed" "${ms}"
|
|
602
|
+
record "gaia doctor --json" "PASS" "rc=0, ${passed}/${total} checks passed" "${ms}"
|
|
598
603
|
else
|
|
599
604
|
record "gaia doctor --json" "FAIL" "parse/threshold failure (rc=${rc})" "${ms}"
|
|
600
605
|
fi
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gaia-ops",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.8",
|
|
4
4
|
"description": "Full DevOps orchestration for Claude Code. Eight specialized agents handle the complete development lifecycle \u2014 analysis, planning, execution, and deployment. Gaia-Ops scans your codebase to understand it and injects the right context into each sub-agent. Every command is classified by risk: read-only runs freely, state changes pause for your approval, and irreversible operations are permanently blocked.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "jaguilar87",
|