@jaguilar87/gaia 5.0.9 → 5.0.11
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 +17 -0
- package/bin/README.md +4 -2
- package/bin/cli/_install_helpers.py +0 -3
- package/bin/cli/ac.py +2 -2
- package/bin/cli/brief.py +42 -7
- package/bin/cli/cleanup.py +304 -4
- package/bin/cli/doctor.py +1 -5
- package/bin/cli/uninstall.py +20 -0
- package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-ops/hooks/adapters/__init__.py +12 -2
- package/dist/gaia-ops/hooks/adapters/base.py +122 -5
- package/dist/gaia-ops/hooks/adapters/claude_code.py +175 -53
- package/dist/gaia-ops/hooks/adapters/host_session.py +53 -0
- package/dist/gaia-ops/hooks/adapters/host_transcript.py +75 -0
- package/dist/gaia-ops/hooks/adapters/registry.py +87 -0
- package/dist/gaia-ops/hooks/adapters/types.py +134 -6
- package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +34 -71
- package/dist/gaia-ops/hooks/modules/core/hook_entry.py +6 -4
- package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +0 -5
- package/dist/gaia-ops/hooks/modules/core/state.py +12 -10
- package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +2 -2
- package/dist/gaia-ops/hooks/modules/security/approval_grants.py +7 -7
- package/dist/gaia-ops/hooks/modules/security/capability_classes.py +83 -6
- package/dist/gaia-ops/hooks/modules/security/inline_ast_analyzer.py +237 -0
- package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +414 -3
- package/dist/gaia-ops/hooks/modules/session/pending_scanner.py +4 -3
- package/dist/gaia-ops/hooks/modules/session/session_manager.py +6 -15
- package/dist/gaia-ops/hooks/modules/session/session_manifest.py +3 -3
- package/dist/gaia-ops/hooks/modules/session/session_registry.py +3 -3
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +191 -32
- package/dist/gaia-ops/hooks/modules/tools/hook_response.py +14 -12
- package/dist/gaia-ops/hooks/post_tool_use.py +2 -2
- package/dist/gaia-ops/hooks/pre_tool_use.py +9 -8
- package/dist/gaia-ops/hooks/stop_hook.py +2 -2
- package/dist/gaia-ops/hooks/subagent_start.py +2 -2
- package/dist/gaia-ops/hooks/subagent_stop.py +2 -2
- package/dist/gaia-ops/hooks/task_completed.py +2 -2
- package/dist/gaia-ops/skills/security-tiers/SKILL.md +1 -1
- package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-security/hooks/adapters/__init__.py +12 -2
- package/dist/gaia-security/hooks/adapters/base.py +122 -5
- package/dist/gaia-security/hooks/adapters/claude_code.py +175 -53
- package/dist/gaia-security/hooks/adapters/host_session.py +53 -0
- package/dist/gaia-security/hooks/adapters/host_transcript.py +75 -0
- package/dist/gaia-security/hooks/adapters/registry.py +87 -0
- package/dist/gaia-security/hooks/adapters/types.py +134 -6
- package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +34 -71
- package/dist/gaia-security/hooks/modules/core/hook_entry.py +6 -4
- package/dist/gaia-security/hooks/modules/core/plugin_setup.py +0 -5
- package/dist/gaia-security/hooks/modules/core/state.py +12 -10
- package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +2 -2
- package/dist/gaia-security/hooks/modules/security/approval_grants.py +7 -7
- package/dist/gaia-security/hooks/modules/security/capability_classes.py +83 -6
- package/dist/gaia-security/hooks/modules/security/inline_ast_analyzer.py +237 -0
- package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +414 -3
- package/dist/gaia-security/hooks/modules/session/pending_scanner.py +4 -3
- package/dist/gaia-security/hooks/modules/session/session_manager.py +6 -15
- package/dist/gaia-security/hooks/modules/session/session_manifest.py +3 -3
- package/dist/gaia-security/hooks/modules/session/session_registry.py +3 -3
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +191 -32
- package/dist/gaia-security/hooks/modules/tools/hook_response.py +14 -12
- package/dist/gaia-security/hooks/post_tool_use.py +2 -2
- package/dist/gaia-security/hooks/pre_tool_use.py +9 -8
- package/dist/gaia-security/hooks/stop_hook.py +2 -2
- package/gaia/briefs/__init__.py +4 -0
- package/gaia/briefs/store.py +144 -1
- package/gaia/state/__init__.py +8 -1
- package/gaia/state/transitions.py +18 -4
- package/gaia/store/schema.sql +5 -1
- package/hooks/adapters/__init__.py +12 -2
- package/hooks/adapters/base.py +122 -5
- package/hooks/adapters/claude_code.py +175 -53
- package/hooks/adapters/host_session.py +53 -0
- package/hooks/adapters/host_transcript.py +75 -0
- package/hooks/adapters/registry.py +87 -0
- package/hooks/adapters/types.py +134 -6
- package/hooks/modules/agents/transcript_reader.py +34 -71
- package/hooks/modules/core/hook_entry.py +6 -4
- package/hooks/modules/core/plugin_setup.py +0 -5
- package/hooks/modules/core/state.py +12 -10
- package/hooks/modules/security/approval_cleanup.py +2 -2
- package/hooks/modules/security/approval_grants.py +7 -7
- package/hooks/modules/security/capability_classes.py +83 -6
- package/hooks/modules/security/inline_ast_analyzer.py +237 -0
- package/hooks/modules/security/mutative_verbs.py +414 -3
- package/hooks/modules/session/pending_scanner.py +4 -3
- package/hooks/modules/session/session_manager.py +6 -15
- package/hooks/modules/session/session_manifest.py +3 -3
- package/hooks/modules/session/session_registry.py +3 -3
- package/hooks/modules/tools/bash_validator.py +191 -32
- package/hooks/modules/tools/hook_response.py +14 -12
- package/hooks/post_tool_use.py +2 -2
- package/hooks/pre_tool_use.py +9 -8
- package/hooks/stop_hook.py +2 -2
- package/hooks/subagent_start.py +2 -2
- package/hooks/subagent_stop.py +2 -2
- package/hooks/task_completed.py +2 -2
- package/package.json +1 -1
- package/pyproject.toml +20 -1
- package/scripts/migrations/schema.checksum +2 -2
- package/scripts/migrations/v20_to_v21.sql +68 -0
- package/skills/security-tiers/SKILL.md +1 -1
|
@@ -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.11",
|
|
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.11",
|
|
24
24
|
"category": "security",
|
|
25
25
|
"author": {
|
|
26
26
|
"name": "jaguilar87",
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.0.11] - 2026-06-30
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Host decoupling (#88): la lógica del core (clasificación T0–T3, grants, validación, audit) queda desacoplada de Claude Code tras la capa adapter. Lo específico del host vive en seams: `host_session`, `host_transcript`, `registry`/`get_adapter`, `request_consent`/`ConsentRequest`, `HostCapability`/degradación, `HostDistribution`. Soportar un host nuevo de la familia hook-interception = escribir un adapter + declarar capacidades, sin tocar el core.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Estado terminal `descoped` para acceptance criteria (descope deliberado, hard-terminal) más invariantes de `verify_brief` (`closed_brief_nonterminal_ac`, `closed_brief_open_plan`) para coherencia brief/plan/AC al cerrar.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Endurecimiento del security-core a 100% killable (mutation testing) en `blocked_commands`, `mutative_verbs`, `tiers` y `approval_grants`. Arreglado el mecanismo de skip-file de equivalentes para casar por identidad estable (`operator|posición|occurrence`) en vez de `job_ids` regenerados — elimina la exclusión-cero silenciosa ("falso 100%") tras cada `cosmic-ray init`.
|
|
23
|
+
- Corregido el help de `brief close` (verify advisory, sin cascade de estado).
|
|
24
|
+
|
|
25
|
+
## [5.0.10] - 2026-06-29
|
|
26
|
+
|
|
10
27
|
## [5.0.9] - 2026-06-25
|
|
11
28
|
|
|
12
29
|
### Changed
|
package/bin/README.md
CHANGED
|
@@ -33,7 +33,9 @@ npm uninstall @jaguilar87/gaia
|
|
|
33
33
|
|
|
|
34
34
|
preuninstall script -> python3 bin/gaia uninstall --preuninstall
|
|
35
35
|
|
|
|
36
|
-
|
|
36
|
+
Removes Gaia-owned symlinks (agents, hooks, skills, …), cleans caches /
|
|
37
|
+
logs / __pycache__, and surgically removes only Gaia's contributions from
|
|
38
|
+
settings.local.json and plugin-registry.json
|
|
37
39
|
```
|
|
38
40
|
|
|
39
41
|
No Claude Code session is involved in either case. The subcommands run in a normal Python process and interact with the filesystem directly.
|
|
@@ -97,7 +99,7 @@ Modules whose name starts with `_` (e.g. `_install_helpers.py`) are private help
|
|
|
97
99
|
|
|
98
100
|
**Exit codes:** `0` on success, `1` on warnings, `2` on errors. The release pipeline's sandbox harness relies on these -- do not print a success line and exit non-zero, or vice versa.
|
|
99
101
|
|
|
100
|
-
**
|
|
102
|
+
**Cleanup footprint:** Full cleanup (the default, used by `gaia uninstall`) removes everything `gaia install` wrote: `CLAUDE.md`, `.claude/settings.json`, all Gaia-owned symlinks (`.claude/agents`, `.claude/hooks`, `.claude/skills`, and siblings), and the `.claude/.plugin-initialized` marker. Two files are handled surgically because they are shared with Claude Code: `settings.local.json` has only Gaia-injected keys removed (agent identity, two env vars, Gaia's permission entries; user content is preserved); `plugin-registry.json` has only Gaia's `installed[]` entry removed and is deleted only if it contained nothing else. The user DB at `~/.gaia/gaia.db` is never touched; pass `--purge` to `gaia uninstall` to remove it. The canonical source for what gets removed is `cli/cleanup.py` (`SYMLINKS_TO_REMOVE`, `_clean_settings_local_json`, `_remove_plugin_registry_entry`).
|
|
101
103
|
|
|
102
104
|
**`package.json` `bin` field:**
|
|
103
105
|
|
|
@@ -175,9 +175,6 @@ def merge_local_permissions(
|
|
|
175
175
|
|
|
176
176
|
# env vars (smart merge -- preserve user values)
|
|
177
177
|
env = existing.setdefault("env", {})
|
|
178
|
-
if "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS" not in env:
|
|
179
|
-
env["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"] = "1"
|
|
180
|
-
changed_fields.append("env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS")
|
|
181
178
|
if "CLAUDE_CODE_DISABLE_AUTO_MEMORY" not in env:
|
|
182
179
|
env["CLAUDE_CODE_DISABLE_AUTO_MEMORY"] = "1"
|
|
183
180
|
changed_fields.append("env.CLAUDE_CODE_DISABLE_AUTO_MEMORY")
|
package/bin/cli/ac.py
CHANGED
|
@@ -229,8 +229,8 @@ def register(subparsers) -> None:
|
|
|
229
229
|
setstatus_p.add_argument("ac_id", metavar="AC_ID", help="AC identifier.")
|
|
230
230
|
setstatus_p.add_argument(
|
|
231
231
|
"status",
|
|
232
|
-
choices=("pending", "done", "blocked"),
|
|
233
|
-
help="Target status.",
|
|
232
|
+
choices=("pending", "done", "blocked", "descoped"),
|
|
233
|
+
help="Target status ('descoped' is a hard-terminal drop; no reopen).",
|
|
234
234
|
)
|
|
235
235
|
setstatus_p.add_argument("--workspace", default=None, metavar="W")
|
|
236
236
|
setstatus_p.add_argument("--json", action="store_true", default=False,
|
package/bin/cli/brief.py
CHANGED
|
@@ -14,7 +14,9 @@ Subcommands:
|
|
|
14
14
|
gaia brief show <name> [--json] Print brief as markdown
|
|
15
15
|
gaia brief list [--status=...] List briefs in the workspace
|
|
16
16
|
[--format=table|count|json]
|
|
17
|
-
gaia brief close <name> Set status -> closed
|
|
17
|
+
gaia brief close <name> Set status -> closed (advisory: runs
|
|
18
|
+
verify_brief and prints inconsistencies;
|
|
19
|
+
does NOT change AC/milestone/plan status)
|
|
18
20
|
gaia brief set-status <name> <status> Validated state-machine transition
|
|
19
21
|
(DB-only)
|
|
20
22
|
gaia brief deps <name> [--json] Print dependency graph
|
|
@@ -380,16 +382,44 @@ def _cmd_edit(args) -> int:
|
|
|
380
382
|
|
|
381
383
|
|
|
382
384
|
def _cmd_show(args) -> int:
|
|
383
|
-
from gaia.briefs import
|
|
385
|
+
from gaia.briefs import (
|
|
386
|
+
get_brief,
|
|
387
|
+
get_brief_by_id,
|
|
388
|
+
find_brief_workspaces,
|
|
389
|
+
serialize_brief_to_markdown,
|
|
390
|
+
)
|
|
384
391
|
workspace = _resolve_workspace(getattr(args, "workspace", None))
|
|
385
392
|
name = args.name
|
|
393
|
+
as_json = getattr(args, "json", False)
|
|
394
|
+
|
|
395
|
+
# FIX 1: when the argument is all-digits, resolve by numeric id first.
|
|
396
|
+
if name.isdigit():
|
|
397
|
+
brief = get_brief_by_id(int(name))
|
|
398
|
+
if brief is not None:
|
|
399
|
+
if as_json:
|
|
400
|
+
out = {k: v for k, v in brief.items() if k != "id"}
|
|
401
|
+
print(json.dumps(out, indent=2, default=str))
|
|
402
|
+
return 0
|
|
403
|
+
print(serialize_brief_to_markdown(brief))
|
|
404
|
+
return 0
|
|
405
|
+
# Numeric id not found -- fall through to the slug path so that a
|
|
406
|
+
# name that happens to look like a number still gets a useful error.
|
|
386
407
|
|
|
387
408
|
brief = get_brief(workspace, name)
|
|
388
409
|
if brief is None:
|
|
389
|
-
|
|
390
|
-
|
|
410
|
+
# FIX 2: cross-workspace hint instead of bare "not found".
|
|
411
|
+
other_workspaces = find_brief_workspaces(name)
|
|
412
|
+
if other_workspaces:
|
|
413
|
+
hint = ", ".join(repr(w) for w in other_workspaces)
|
|
414
|
+
msg = (
|
|
415
|
+
f"brief '{name}' not found in workspace '{workspace}', "
|
|
416
|
+
f"but exists in: {hint} -- use --workspace=<workspace> to show it"
|
|
417
|
+
)
|
|
418
|
+
else:
|
|
419
|
+
msg = f"brief '{name}' not found in workspace '{workspace}'"
|
|
420
|
+
return _err(msg, as_json=as_json)
|
|
391
421
|
|
|
392
|
-
if
|
|
422
|
+
if as_json:
|
|
393
423
|
# Drop internal SQL columns for cleanliness
|
|
394
424
|
out = {k: v for k, v in brief.items() if k != "id"}
|
|
395
425
|
print(json.dumps(out, indent=2, default=str))
|
|
@@ -725,8 +755,13 @@ def register(subparsers) -> None:
|
|
|
725
755
|
# -- close --------------------------------------------------------------
|
|
726
756
|
close_p = actions.add_parser(
|
|
727
757
|
"close",
|
|
728
|
-
help="Set brief status to closed",
|
|
729
|
-
description=
|
|
758
|
+
help="Set brief status to closed (advisory verify, no cascade)",
|
|
759
|
+
description=(
|
|
760
|
+
"Set the brief's status to 'closed', then run verify_brief and "
|
|
761
|
+
"print any inconsistencies as warnings. ADVISORY ONLY: it does NOT "
|
|
762
|
+
"change AC, milestone, or plan status, and performs no cascade. To "
|
|
763
|
+
"resolve a flagged AC, use 'gaia ac set-status' (done / descoped)."
|
|
764
|
+
),
|
|
730
765
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
731
766
|
epilog="Examples:\n gaia brief close <name>\n gaia brief close my-feature --workspace=me\n",
|
|
732
767
|
)
|
package/bin/cli/cleanup.py
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
"""
|
|
2
|
-
gaia cleanup -- Remove
|
|
3
|
-
|
|
2
|
+
gaia cleanup -- Remove Gaia's workspace footprint and apply data retention.
|
|
3
|
+
|
|
4
|
+
The full-cleanup footprint mirrors what `gaia install` writes, in reverse:
|
|
5
|
+
- CLAUDE.md and .claude/settings.json (removed outright -- Gaia-owned)
|
|
6
|
+
- .claude/ symlinks incl. skills (removed -- Gaia-owned)
|
|
7
|
+
- .claude/.plugin-initialized marker (removed -- Gaia-owned)
|
|
8
|
+
- .claude/plugin-registry.json (surgical: only Gaia's installed[] entry is
|
|
9
|
+
removed; the file is shared with Claude Code's plugin system)
|
|
10
|
+
- .claude/settings.local.json (surgical: only Gaia-injected keys are removed
|
|
11
|
+
-- agent, two env vars, Gaia's permission entries; user config preserved)
|
|
12
|
+
The user DB at ~/.gaia/gaia.db is never touched here.
|
|
4
13
|
|
|
5
14
|
Modes:
|
|
6
|
-
--prune / --retain Apply data retention policy only (no
|
|
7
|
-
(default) Remove
|
|
15
|
+
--prune / --retain Apply data retention policy only (no footprint removal)
|
|
16
|
+
(default) Remove the footprint above + run retention
|
|
8
17
|
|
|
9
18
|
Flags:
|
|
10
19
|
--dry-run Print what would be pruned/removed without modifying files
|
|
@@ -16,6 +25,11 @@ import os
|
|
|
16
25
|
import sys
|
|
17
26
|
from pathlib import Path
|
|
18
27
|
|
|
28
|
+
# bin/cli/cleanup.py -> bin/cli -> bin -> gaia/
|
|
29
|
+
_PACKAGE_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
30
|
+
if str(_PACKAGE_ROOT) not in sys.path:
|
|
31
|
+
sys.path.insert(0, str(_PACKAGE_ROOT))
|
|
32
|
+
|
|
19
33
|
|
|
20
34
|
# ---------------------------------------------------------------------------
|
|
21
35
|
# Project root detection (mirrors JS findProjectRoot)
|
|
@@ -334,6 +348,7 @@ SYMLINKS_TO_REMOVE = [
|
|
|
334
348
|
".claude/hooks",
|
|
335
349
|
".claude/commands",
|
|
336
350
|
".claude/config",
|
|
351
|
+
".claude/skills",
|
|
337
352
|
".claude/CHANGELOG.md",
|
|
338
353
|
".claude/README.en.md",
|
|
339
354
|
".claude/README.md",
|
|
@@ -358,6 +373,271 @@ def _remove_settings_json(root: Path, dry_run: bool) -> dict:
|
|
|
358
373
|
return {"found": True, "removed": not dry_run, "dry_run": dry_run}
|
|
359
374
|
|
|
360
375
|
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
# Gaia-owned data-dir markers (.plugin-initialized, plugin-registry.json)
|
|
378
|
+
#
|
|
379
|
+
# Both live in get_plugin_data_dir(), which falls back to .claude/ when
|
|
380
|
+
# CLAUDE_PLUGIN_DATA is unset (the common npm-install case). The marker is a
|
|
381
|
+
# pure Gaia artifact -- removed outright. The registry is shared with Claude
|
|
382
|
+
# Code's plugin system, so only Gaia's own entry is removed surgically.
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
# Plugin names Gaia registers in plugin-registry.json (see _read_plugin_name
|
|
386
|
+
# in _install_helpers.py: package "gaia" maps to the canonical "gaia-ops").
|
|
387
|
+
_GAIA_PLUGIN_NAMES = {"gaia-ops", "gaia-security", "gaia"}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _remove_plugin_initialized(root: Path, dry_run: bool) -> dict:
|
|
391
|
+
"""Remove the .plugin-initialized first-run marker.
|
|
392
|
+
|
|
393
|
+
Written by plugin_setup.mark_initialized() into get_plugin_data_dir().
|
|
394
|
+
A pure Gaia artifact (timestamp + mode), safe to delete outright.
|
|
395
|
+
"""
|
|
396
|
+
path = root / ".claude" / ".plugin-initialized"
|
|
397
|
+
if not path.exists():
|
|
398
|
+
return {"found": False}
|
|
399
|
+
if not dry_run:
|
|
400
|
+
try:
|
|
401
|
+
path.unlink()
|
|
402
|
+
except OSError as exc:
|
|
403
|
+
return {"found": True, "removed": False, "error": str(exc)}
|
|
404
|
+
return {"found": True, "removed": not dry_run, "dry_run": dry_run}
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _remove_plugin_registry_entry(root: Path, dry_run: bool) -> dict:
|
|
408
|
+
"""Remove Gaia's entry from plugin-registry.json, preserving other plugins.
|
|
409
|
+
|
|
410
|
+
plugin-registry.json is shared with Claude Code's plugin system. Install
|
|
411
|
+
(register_plugin / ensure_plugin_registry) writes Gaia's entry into the
|
|
412
|
+
``installed`` list. Uninstall is symmetric: drop only Gaia-owned entries
|
|
413
|
+
from ``installed``. If that empties the registry of all plugins, the file
|
|
414
|
+
is equivalent to its pre-Gaia state and is removed; otherwise it is
|
|
415
|
+
rewritten with the surviving entries.
|
|
416
|
+
|
|
417
|
+
Idempotent: a registry with no Gaia entry returns found=False.
|
|
418
|
+
"""
|
|
419
|
+
path = root / ".claude" / "plugin-registry.json"
|
|
420
|
+
if not path.exists():
|
|
421
|
+
return {"found": False}
|
|
422
|
+
|
|
423
|
+
data = _read_json_file(path)
|
|
424
|
+
if not isinstance(data, dict):
|
|
425
|
+
# Malformed/unexpected shape -- leave it untouched rather than risk
|
|
426
|
+
# destroying a file Gaia did not write.
|
|
427
|
+
return {"found": False, "skipped": "registry not a JSON object"}
|
|
428
|
+
|
|
429
|
+
installed = data.get("installed")
|
|
430
|
+
if not isinstance(installed, list):
|
|
431
|
+
return {"found": False, "skipped": "no installed[] array"}
|
|
432
|
+
|
|
433
|
+
kept = [
|
|
434
|
+
e for e in installed
|
|
435
|
+
if not (isinstance(e, dict) and e.get("name") in _GAIA_PLUGIN_NAMES)
|
|
436
|
+
]
|
|
437
|
+
removed_entries = [
|
|
438
|
+
e for e in installed
|
|
439
|
+
if isinstance(e, dict) and e.get("name") in _GAIA_PLUGIN_NAMES
|
|
440
|
+
]
|
|
441
|
+
|
|
442
|
+
if not removed_entries:
|
|
443
|
+
return {"found": False}
|
|
444
|
+
|
|
445
|
+
# Only Gaia keys present (installed + source) and nothing survives ->
|
|
446
|
+
# the file existed solely for Gaia; remove it entirely.
|
|
447
|
+
only_gaia_keys = set(data.keys()) <= {"installed", "source"}
|
|
448
|
+
delete_file = not kept and only_gaia_keys
|
|
449
|
+
|
|
450
|
+
result = {
|
|
451
|
+
"found": True,
|
|
452
|
+
"removed_entries": [e.get("name") for e in removed_entries],
|
|
453
|
+
"file_removed": delete_file and not dry_run,
|
|
454
|
+
"dry_run": dry_run,
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if dry_run:
|
|
458
|
+
return result
|
|
459
|
+
|
|
460
|
+
try:
|
|
461
|
+
if delete_file:
|
|
462
|
+
path.unlink()
|
|
463
|
+
else:
|
|
464
|
+
data["installed"] = kept
|
|
465
|
+
_write_json_file(path, data)
|
|
466
|
+
except OSError as exc:
|
|
467
|
+
result["error"] = str(exc)
|
|
468
|
+
return result
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
# ---------------------------------------------------------------------------
|
|
472
|
+
# settings.local.json -- surgical removal of Gaia-injected config
|
|
473
|
+
#
|
|
474
|
+
# Install (merge_local_permissions) MERGES into a user-owned file: it sets
|
|
475
|
+
# agent=gaia-orchestrator, adds two env vars, and authoritative-merges its
|
|
476
|
+
# permission entries. Uninstall must mirror that injection key-for-key and
|
|
477
|
+
# leave everything the user added intact -- never delete the whole file.
|
|
478
|
+
#
|
|
479
|
+
# Note on permissions: install's authoritative merge already overwrote any
|
|
480
|
+
# user-scoped variants of tool names Gaia manages (e.g. a prior Edit(/tmp/*)
|
|
481
|
+
# became Edit). That loss is not recoverable at uninstall time; the symmetric
|
|
482
|
+
# action is to drop the tool names Gaia manages and its deny rules, leaving
|
|
483
|
+
# only entries for tools Gaia never touched.
|
|
484
|
+
# ---------------------------------------------------------------------------
|
|
485
|
+
|
|
486
|
+
_GAIA_ENV_KEYS = {
|
|
487
|
+
"CLAUDE_CODE_DISABLE_AUTO_MEMORY": "1",
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _gaia_managed_permission_sets():
|
|
492
|
+
"""Return (managed_tool_names, deny_entries) Gaia injects into settings.
|
|
493
|
+
|
|
494
|
+
Pulled from the canonical OPS/SECURITY permission constants in
|
|
495
|
+
plugin_setup.py (the same source install merges from). Falls back to a
|
|
496
|
+
minimal set if the hooks package cannot be imported (partial install).
|
|
497
|
+
"""
|
|
498
|
+
try:
|
|
499
|
+
from hooks.modules.core.plugin_setup import ( # type: ignore
|
|
500
|
+
OPS_PERMISSIONS,
|
|
501
|
+
SECURITY_PERMISSIONS,
|
|
502
|
+
_tool_name,
|
|
503
|
+
)
|
|
504
|
+
allow = (
|
|
505
|
+
set(OPS_PERMISSIONS["permissions"]["allow"])
|
|
506
|
+
| set(SECURITY_PERMISSIONS["permissions"]["allow"])
|
|
507
|
+
)
|
|
508
|
+
deny = (
|
|
509
|
+
set(OPS_PERMISSIONS["permissions"]["deny"])
|
|
510
|
+
| set(SECURITY_PERMISSIONS["permissions"]["deny"])
|
|
511
|
+
)
|
|
512
|
+
managed_names = {_tool_name(e) for e in allow}
|
|
513
|
+
return managed_names, deny
|
|
514
|
+
except Exception: # noqa: BLE001
|
|
515
|
+
return {"Bash"}, set()
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _clean_settings_local_json(root: Path, dry_run: bool) -> dict:
|
|
519
|
+
"""Remove only Gaia-injected keys from settings.local.json; preserve user config.
|
|
520
|
+
|
|
521
|
+
Mirrors merge_local_permissions:
|
|
522
|
+
- agent: removed only if it equals "gaia-orchestrator".
|
|
523
|
+
- env: removes the two Gaia keys only when their value matches what
|
|
524
|
+
install set (a user override of the value is preserved).
|
|
525
|
+
- permissions.allow: drops entries whose tool name Gaia manages.
|
|
526
|
+
- permissions.deny: drops Gaia's deny rules.
|
|
527
|
+
- Empty containers (env / permissions / allow / deny) are pruned so the
|
|
528
|
+
file does not retain hollow Gaia scaffolding.
|
|
529
|
+
|
|
530
|
+
If nothing Gaia-owned remains and the file becomes ``{}``, it is removed.
|
|
531
|
+
Idempotent: a file with no Gaia keys returns found=False.
|
|
532
|
+
"""
|
|
533
|
+
path = root / ".claude" / "settings.local.json"
|
|
534
|
+
if not path.exists():
|
|
535
|
+
return {"found": False}
|
|
536
|
+
|
|
537
|
+
data = _read_json_file(path)
|
|
538
|
+
if not isinstance(data, dict):
|
|
539
|
+
return {"found": False, "skipped": "settings.local.json not a JSON object"}
|
|
540
|
+
|
|
541
|
+
removed_fields: list[str] = []
|
|
542
|
+
|
|
543
|
+
# agent identity
|
|
544
|
+
if data.get("agent") == "gaia-orchestrator":
|
|
545
|
+
removed_fields.append("agent")
|
|
546
|
+
if not dry_run:
|
|
547
|
+
del data["agent"]
|
|
548
|
+
|
|
549
|
+
# env vars (only when value still matches what install set)
|
|
550
|
+
env = data.get("env")
|
|
551
|
+
if isinstance(env, dict):
|
|
552
|
+
for key, expected in _GAIA_ENV_KEYS.items():
|
|
553
|
+
if env.get(key) == expected:
|
|
554
|
+
removed_fields.append(f"env.{key}")
|
|
555
|
+
if not dry_run:
|
|
556
|
+
del env[key]
|
|
557
|
+
if not dry_run and not env:
|
|
558
|
+
del data["env"]
|
|
559
|
+
|
|
560
|
+
# permissions
|
|
561
|
+
perms = data.get("permissions")
|
|
562
|
+
if isinstance(perms, dict):
|
|
563
|
+
managed_names, gaia_deny = _gaia_managed_permission_sets()
|
|
564
|
+
|
|
565
|
+
allow = perms.get("allow")
|
|
566
|
+
if isinstance(allow, list):
|
|
567
|
+
kept_allow = [e for e in allow if _perm_tool_name(e) not in managed_names]
|
|
568
|
+
if len(kept_allow) != len(allow):
|
|
569
|
+
removed_fields.append("permissions.allow")
|
|
570
|
+
if not dry_run:
|
|
571
|
+
if kept_allow:
|
|
572
|
+
perms["allow"] = kept_allow
|
|
573
|
+
else:
|
|
574
|
+
del perms["allow"]
|
|
575
|
+
|
|
576
|
+
deny = perms.get("deny")
|
|
577
|
+
if isinstance(deny, list):
|
|
578
|
+
kept_deny = [e for e in deny if e not in gaia_deny]
|
|
579
|
+
if len(kept_deny) != len(deny):
|
|
580
|
+
removed_fields.append("permissions.deny")
|
|
581
|
+
if not dry_run:
|
|
582
|
+
if kept_deny:
|
|
583
|
+
perms["deny"] = kept_deny
|
|
584
|
+
else:
|
|
585
|
+
del perms["deny"]
|
|
586
|
+
|
|
587
|
+
# Drop hollow empty lists (ask/allow/deny) and an empty permissions
|
|
588
|
+
# block -- these are Gaia scaffolding, not user data.
|
|
589
|
+
if not dry_run:
|
|
590
|
+
for empty_key in ("ask", "allow", "deny"):
|
|
591
|
+
if perms.get(empty_key) == []:
|
|
592
|
+
del perms[empty_key]
|
|
593
|
+
if not perms:
|
|
594
|
+
del data["permissions"]
|
|
595
|
+
|
|
596
|
+
if not removed_fields:
|
|
597
|
+
return {"found": False}
|
|
598
|
+
|
|
599
|
+
result = {
|
|
600
|
+
"found": True,
|
|
601
|
+
"removed_fields": removed_fields,
|
|
602
|
+
"file_removed": False,
|
|
603
|
+
"dry_run": dry_run,
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if dry_run:
|
|
607
|
+
return result
|
|
608
|
+
|
|
609
|
+
try:
|
|
610
|
+
if not data:
|
|
611
|
+
path.unlink()
|
|
612
|
+
result["file_removed"] = True
|
|
613
|
+
else:
|
|
614
|
+
_write_json_file(path, data)
|
|
615
|
+
except OSError as exc:
|
|
616
|
+
result["error"] = str(exc)
|
|
617
|
+
return result
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _perm_tool_name(entry: str) -> str:
|
|
621
|
+
"""Base tool name from a permission entry (mirror of plugin_setup._tool_name)."""
|
|
622
|
+
if not isinstance(entry, str):
|
|
623
|
+
return ""
|
|
624
|
+
paren = entry.find("(")
|
|
625
|
+
return entry[:paren] if paren != -1 else entry
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _read_json_file(path: Path):
|
|
629
|
+
"""Read JSON, returning None on any error."""
|
|
630
|
+
try:
|
|
631
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
632
|
+
except (OSError, json.JSONDecodeError):
|
|
633
|
+
return None
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _write_json_file(path: Path, data) -> None:
|
|
637
|
+
"""Write JSON with indent=2 + trailing newline (gaia convention)."""
|
|
638
|
+
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
639
|
+
|
|
640
|
+
|
|
361
641
|
def _remove_symlinks(root: Path, dry_run: bool) -> dict:
|
|
362
642
|
removed = []
|
|
363
643
|
skipped = []
|
|
@@ -513,11 +793,17 @@ def cmd_cleanup(args) -> int:
|
|
|
513
793
|
|
|
514
794
|
claude_md = _remove_claude_md(root, dry_run)
|
|
515
795
|
settings = _remove_settings_json(root, dry_run)
|
|
796
|
+
settings_local = _clean_settings_local_json(root, dry_run)
|
|
797
|
+
plugin_initialized = _remove_plugin_initialized(root, dry_run)
|
|
798
|
+
plugin_registry = _remove_plugin_registry_entry(root, dry_run)
|
|
516
799
|
symlinks = _remove_symlinks(root, dry_run)
|
|
517
800
|
retention_actions = _apply_retention_policy(root, dry_run)
|
|
518
801
|
|
|
519
802
|
result["claude_md"] = claude_md
|
|
520
803
|
result["settings_json"] = settings
|
|
804
|
+
result["settings_local_json"] = settings_local
|
|
805
|
+
result["plugin_initialized"] = plugin_initialized
|
|
806
|
+
result["plugin_registry"] = plugin_registry
|
|
521
807
|
result["symlinks"] = symlinks
|
|
522
808
|
result["retention_actions"] = retention_actions
|
|
523
809
|
result["retention_policy"] = retention_policy_info
|
|
@@ -529,6 +815,9 @@ def cmd_cleanup(args) -> int:
|
|
|
529
815
|
anything_done = (
|
|
530
816
|
claude_md.get("found")
|
|
531
817
|
or settings.get("found")
|
|
818
|
+
or settings_local.get("found")
|
|
819
|
+
or plugin_initialized.get("found")
|
|
820
|
+
or plugin_registry.get("found")
|
|
532
821
|
or symlinks.get("removed")
|
|
533
822
|
or retention_actions
|
|
534
823
|
)
|
|
@@ -539,6 +828,17 @@ def cmd_cleanup(args) -> int:
|
|
|
539
828
|
if settings.get("found"):
|
|
540
829
|
verb = "Would remove" if dry_run else "Removed"
|
|
541
830
|
print(f" {verb}: .claude/settings.json")
|
|
831
|
+
if settings_local.get("found"):
|
|
832
|
+
verb = "Would clean" if dry_run else "Cleaned"
|
|
833
|
+
fields = ", ".join(settings_local.get("removed_fields", []))
|
|
834
|
+
print(f" {verb}: .claude/settings.local.json ({fields})")
|
|
835
|
+
if plugin_initialized.get("found"):
|
|
836
|
+
verb = "Would remove" if dry_run else "Removed"
|
|
837
|
+
print(f" {verb}: .claude/.plugin-initialized")
|
|
838
|
+
if plugin_registry.get("found"):
|
|
839
|
+
verb = "Would remove" if dry_run else "Removed"
|
|
840
|
+
entries = ", ".join(plugin_registry.get("removed_entries", []))
|
|
841
|
+
print(f" {verb}: plugin-registry.json entry ({entries})")
|
|
542
842
|
for rel in symlinks.get("removed", []):
|
|
543
843
|
verb = "Would remove symlink" if dry_run else "Removed symlink"
|
|
544
844
|
print(f" {verb}: {rel}")
|
package/bin/cli/doctor.py
CHANGED
|
@@ -185,7 +185,7 @@ def _package_root() -> Path:
|
|
|
185
185
|
# in lock-step with the INSERT it adds to bootstrap_database.sh. If a user
|
|
186
186
|
# upgrades the CLI past a schema bump but does not re-run `gaia install`,
|
|
187
187
|
# `check_schema_version` raises a warning telling them how to repair.
|
|
188
|
-
EXPECTED_SCHEMA_VERSION =
|
|
188
|
+
EXPECTED_SCHEMA_VERSION = 21
|
|
189
189
|
|
|
190
190
|
# Locations the doctor reads outside the workspace.
|
|
191
191
|
_INSTALL_ERROR_MARKER = Path("~/.gaia/last-install-error.json").expanduser()
|
|
@@ -854,10 +854,6 @@ def check_settings(project_root: Path) -> dict:
|
|
|
854
854
|
if deny_count == 0:
|
|
855
855
|
issues.append("No deny rules (destructive commands not blocked)")
|
|
856
856
|
|
|
857
|
-
env = data.get("env", {})
|
|
858
|
-
if not env.get("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"):
|
|
859
|
-
infos.append("AGENT_TEAMS env not set")
|
|
860
|
-
|
|
861
857
|
if issues:
|
|
862
858
|
return _result("Settings", "error", "; ".join(issues), "Run `gaia scan` or `gaia update`")
|
|
863
859
|
|
package/bin/cli/uninstall.py
CHANGED
|
@@ -38,8 +38,11 @@ from pathlib import Path
|
|
|
38
38
|
# duplicating retention policy, symlink lists, or root detection.
|
|
39
39
|
from cli.cleanup import ( # type: ignore # noqa: E402
|
|
40
40
|
_apply_retention_policy,
|
|
41
|
+
_clean_settings_local_json,
|
|
41
42
|
_find_project_root,
|
|
42
43
|
_remove_claude_md,
|
|
44
|
+
_remove_plugin_initialized,
|
|
45
|
+
_remove_plugin_registry_entry,
|
|
43
46
|
_remove_settings_json,
|
|
44
47
|
_remove_symlinks,
|
|
45
48
|
)
|
|
@@ -250,6 +253,9 @@ def cmd_uninstall(args: argparse.Namespace) -> int:
|
|
|
250
253
|
try:
|
|
251
254
|
result["claude_md"] = _remove_claude_md(workspace, dry_run)
|
|
252
255
|
result["settings_json"] = _remove_settings_json(workspace, dry_run)
|
|
256
|
+
result["settings_local_json"] = _clean_settings_local_json(workspace, dry_run)
|
|
257
|
+
result["plugin_initialized"] = _remove_plugin_initialized(workspace, dry_run)
|
|
258
|
+
result["plugin_registry"] = _remove_plugin_registry_entry(workspace, dry_run)
|
|
253
259
|
result["symlinks"] = _remove_symlinks(workspace, dry_run)
|
|
254
260
|
result["retention_actions"] = _apply_retention_policy(workspace, dry_run)
|
|
255
261
|
except Exception as exc: # noqa: BLE001
|
|
@@ -345,6 +351,9 @@ def _print_human(result: dict, *, preuninstall: bool, purge: bool, dry_run: bool
|
|
|
345
351
|
|
|
346
352
|
claude_md = result.get("claude_md") or {}
|
|
347
353
|
settings = result.get("settings_json") or {}
|
|
354
|
+
settings_local = result.get("settings_local_json") or {}
|
|
355
|
+
plugin_initialized = result.get("plugin_initialized") or {}
|
|
356
|
+
plugin_registry = result.get("plugin_registry") or {}
|
|
348
357
|
symlinks = result.get("symlinks") or {}
|
|
349
358
|
retention = result.get("retention_actions") or []
|
|
350
359
|
db = result.get("db") or {}
|
|
@@ -355,6 +364,17 @@ def _print_human(result: dict, *, preuninstall: bool, purge: bool, dry_run: bool
|
|
|
355
364
|
if settings.get("found"):
|
|
356
365
|
verb = "Would remove" if dry_run else "Removed"
|
|
357
366
|
print(f" {verb}: .claude/settings.json")
|
|
367
|
+
if settings_local.get("found"):
|
|
368
|
+
verb = "Would clean" if dry_run else "Cleaned"
|
|
369
|
+
fields = ", ".join(settings_local.get("removed_fields", []))
|
|
370
|
+
print(f" {verb}: .claude/settings.local.json ({fields})")
|
|
371
|
+
if plugin_initialized.get("found"):
|
|
372
|
+
verb = "Would remove" if dry_run else "Removed"
|
|
373
|
+
print(f" {verb}: .claude/.plugin-initialized")
|
|
374
|
+
if plugin_registry.get("found"):
|
|
375
|
+
verb = "Would remove" if dry_run else "Removed"
|
|
376
|
+
entries = ", ".join(plugin_registry.get("removed_entries", []))
|
|
377
|
+
print(f" {verb}: plugin-registry.json entry ({entries})")
|
|
358
378
|
for rel in symlinks.get("removed", []):
|
|
359
379
|
verb = "Would remove symlink" if dry_run else "Removed symlink"
|
|
360
380
|
print(f" {verb}: {rel}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gaia-ops",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.11",
|
|
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",
|
|
@@ -13,10 +13,13 @@ Modules:
|
|
|
13
13
|
from .types import (
|
|
14
14
|
HookEventType,
|
|
15
15
|
PermissionDecision,
|
|
16
|
-
|
|
16
|
+
HostDistribution,
|
|
17
|
+
HostCapability,
|
|
17
18
|
HookEvent,
|
|
18
19
|
ValidationRequest,
|
|
19
20
|
ValidationResult,
|
|
21
|
+
ConsentRequest,
|
|
22
|
+
CapabilityDegradation,
|
|
20
23
|
ToolResult,
|
|
21
24
|
AgentCompletion,
|
|
22
25
|
CompletionResult,
|
|
@@ -28,15 +31,19 @@ from .types import (
|
|
|
28
31
|
)
|
|
29
32
|
from .base import HookAdapter
|
|
30
33
|
from .claude_code import ClaudeCodeAdapter
|
|
34
|
+
from .registry import get_adapter, register_adapter, DEFAULT_HOST
|
|
31
35
|
from .utils import has_stdin_data, warn_if_dual_channel
|
|
32
36
|
|
|
33
37
|
__all__ = [
|
|
34
38
|
"HookEventType",
|
|
35
39
|
"PermissionDecision",
|
|
36
|
-
"
|
|
40
|
+
"HostDistribution",
|
|
41
|
+
"HostCapability",
|
|
37
42
|
"HookEvent",
|
|
38
43
|
"ValidationRequest",
|
|
39
44
|
"ValidationResult",
|
|
45
|
+
"ConsentRequest",
|
|
46
|
+
"CapabilityDegradation",
|
|
40
47
|
"ToolResult",
|
|
41
48
|
"AgentCompletion",
|
|
42
49
|
"CompletionResult",
|
|
@@ -47,6 +54,9 @@ __all__ = [
|
|
|
47
54
|
"HookResponse",
|
|
48
55
|
"HookAdapter",
|
|
49
56
|
"ClaudeCodeAdapter",
|
|
57
|
+
"get_adapter",
|
|
58
|
+
"register_adapter",
|
|
59
|
+
"DEFAULT_HOST",
|
|
50
60
|
"has_stdin_data",
|
|
51
61
|
"warn_if_dual_channel",
|
|
52
62
|
]
|