@jaguilar87/gaia 5.1.0-rc.4 → 5.1.1

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 (93) hide show
  1. package/.claude-plugin/marketplace.json +3 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +13 -0
  4. package/agents/gaia-orchestrator.md +4 -10
  5. package/bin/README.md +1 -1
  6. package/bin/cli/approvals.py +0 -144
  7. package/bin/cli/cleanup.py +1 -30
  8. package/bin/cli/context.py +379 -6
  9. package/bin/cli/doctor.py +1 -1
  10. package/bin/cli/history.py +18 -6
  11. package/bin/cli/install.py +4 -2
  12. package/bin/cli/memory.py +86 -10
  13. package/bin/cli/metrics.py +1216 -275
  14. package/bin/cli/query.py +89 -0
  15. package/bin/cli/scan.py +68 -2
  16. package/bin/cli/uninstall.py +9 -5
  17. package/bin/pre-publish-validate.js +23 -5
  18. package/gaia/project.py +49 -20
  19. package/gaia/store/reader.py +175 -11
  20. package/gaia/store/schema.sql +180 -1
  21. package/gaia/store/writer.py +1146 -165
  22. package/hooks/README.md +2 -2
  23. package/hooks/adapters/claude_code.py +6 -12
  24. package/hooks/elicitation_result.py +4 -9
  25. package/hooks/modules/agents/contract_validator.py +2 -1
  26. package/hooks/modules/agents/handoff_persister.py +5 -220
  27. package/hooks/modules/agents/task_info_builder.py +9 -2
  28. package/hooks/modules/agents/transcript_reader.py +41 -21
  29. package/hooks/modules/audit/logger.py +8 -3
  30. package/hooks/modules/context/context_injector.py +10 -3
  31. package/hooks/modules/core/__init__.py +5 -2
  32. package/hooks/modules/core/logging_setup.py +60 -0
  33. package/hooks/modules/core/paths.py +0 -12
  34. package/hooks/modules/security/approval_grants.py +24 -92
  35. package/hooks/modules/security/gaia_db_write_guard.py +7 -2
  36. package/hooks/modules/security/mutative_verbs.py +194 -11
  37. package/hooks/modules/session/pending_scanner.py +24 -222
  38. package/hooks/modules/session/session_manifest.py +13 -288
  39. package/hooks/post_compact.py +4 -9
  40. package/hooks/post_tool_use.py +4 -9
  41. package/hooks/pre_compact.py +5 -10
  42. package/hooks/pre_tool_use.py +4 -11
  43. package/hooks/session_end_hook.py +4 -9
  44. package/hooks/session_start.py +18 -21
  45. package/hooks/stop_hook.py +5 -10
  46. package/hooks/subagent_start.py +5 -10
  47. package/hooks/subagent_stop.py +9 -23
  48. package/hooks/task_completed.py +5 -10
  49. package/hooks/user_prompt_submit.py +4 -9
  50. package/package.json +1 -1
  51. package/pyproject.toml +1 -1
  52. package/scripts/migrations/schema.checksum +2 -2
  53. package/scripts/migrations/v21_to_v22.sql +80 -0
  54. package/scripts/migrations/v22_to_v23.sql +20 -0
  55. package/scripts/migrations/v23_to_v24.sql +30 -0
  56. package/scripts/migrations/v24_to_v25.sql +77 -0
  57. package/scripts/migrations/v25_to_v26.sql +116 -0
  58. package/scripts/release-prepare.mjs +18 -2
  59. package/skills/README.md +8 -0
  60. package/skills/agent-approval-protocol/SKILL.md +27 -27
  61. package/skills/agent-approval-protocol/reference.md +20 -12
  62. package/skills/agent-protocol/SKILL.md +1 -1
  63. package/skills/agent-protocol/examples.md +17 -9
  64. package/skills/agent-response/SKILL.md +1 -1
  65. package/skills/diagram-builder/GLOSSARY.md +77 -0
  66. package/skills/diagram-builder/SKILL.md +156 -0
  67. package/skills/diagram-builder/assets/README.md +45 -0
  68. package/skills/diagram-builder/assets/data/data.generated.js +77 -0
  69. package/skills/diagram-builder/assets/data/document.yaml +12 -0
  70. package/skills/diagram-builder/assets/data/pages/overview.yaml +47 -0
  71. package/skills/diagram-builder/assets/engine/build-data.mjs +51 -0
  72. package/skills/diagram-builder/assets/engine/engine.js +863 -0
  73. package/skills/diagram-builder/assets/index.html +638 -0
  74. package/skills/diagram-builder/assets/package.json +15 -0
  75. package/skills/diagram-builder/assets/tools/verify.mjs +128 -0
  76. package/skills/diagram-builder/reference.md +444 -0
  77. package/skills/execution/SKILL.md +3 -3
  78. package/skills/gaia-patterns/reference.md +2 -2
  79. package/skills/memory/SKILL.md +49 -0
  80. package/skills/orchestrator-present-approval/SKILL.md +90 -79
  81. package/skills/orchestrator-present-approval/reference.md +59 -42
  82. package/skills/orchestrator-present-approval/template.md +16 -14
  83. package/skills/pending-approvals/SKILL.md +80 -29
  84. package/skills/pending-approvals/reference.md +48 -60
  85. package/skills/security-tiers/SKILL.md +1 -1
  86. package/skills/subagent-request-approval/SKILL.md +65 -68
  87. package/skills/subagent-request-approval/reference.md +63 -63
  88. package/skills/visual-verify/SKILL.md +107 -0
  89. package/skills/visual-verify/scripts/screenshot.js +210 -0
  90. package/tools/scan/classify.py +584 -15
  91. package/tools/scan/migrate_workspace.py +9 -4
  92. package/tools/scan/store_populator.py +231 -2
  93. package/tools/scan/tests/conftest.py +31 -0
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "gaia",
11
11
  "description": "Security-first multi-agent orchestration for Claude Code. Specialized agents cover the full development lifecycle — analysis, planning, execution, deployment — with codebase-aware context injection. Every command is risk-classified: read-only runs freely, state changes pause for your approval, and irreversible operations are permanently blocked.",
12
- "version": "5.1.0-rc.4",
12
+ "version": "5.1.1",
13
13
  "category": "devops",
14
14
  "author": {
15
15
  "name": "jaguilar87",
@@ -18,7 +18,8 @@
18
18
  "homepage": "https://github.com/metraton/gaia#readme",
19
19
  "source": {
20
20
  "source": "github",
21
- "repo": "metraton/gaia"
21
+ "repo": "metraton/gaia",
22
+ "ref": "v5.1.1"
22
23
  }
23
24
  }
24
25
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gaia",
3
- "version": "5.1.0-rc.4",
3
+ "version": "5.1.1",
4
4
  "description": "Security-first multi-agent orchestration for Claude Code. Agents span the full lifecycle; commands are risk-classified \u2014 reads run free, state changes need approval, irreversible ops blocked.",
5
5
  "author": {
6
6
  "name": "jaguilar87",
package/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Changed
11
+
12
+ - Approval grants redesign: a grant is single-use and consumed at match (before the command executes), TTL cut from 60 to 5 minutes. Approving is now coupled to execution — approval triggers an automatic verbatim re-dispatch of the approved command instead of a separate resume step. `COMMAND_SET` batches simplified to the hook-minted path only (>= 2 T3 sub-commands blocked in one compound Bash call, content-derived `approval_id`, 5-minute TTL); the plan-first batch declaration flow and the `gaia approvals derive-id` CLI are retired — there is no agent-declared or CLI-derived batch id, only the one the hook mints from the blocked chain.
13
+
14
+ ### Removed
15
+
16
+ - Cross-session surfacing of pending approvals: the SessionStart `[ACTIONABLE]` pending-approvals block and the per-turn pending feed are removed. Pending approvals (24h TTL, unchanged) no longer surface outside the turn that produced them.
17
+ - `consume_session_grants` mechanism, superseded by the consumed-at-match single-use grant model.
18
+
19
+ ## [5.1.1] - 2026-07-06
20
+
21
+ ## [5.1.0] - 2026-07-03
22
+
10
23
  ## [5.1.0-rc.4] - 2026-07-03
11
24
 
12
25
  ## [5.1.0-rc.3] - 2026-07-03
@@ -23,7 +23,7 @@ Delegation is not a preference but the mechanic that makes the pipeline govern:
23
23
 
24
24
  I am not the only process. The Gaia hooks operate on their own across every dispatch, classifying mutative verbs, protecting sensitive paths, extracting approval nonces, and writing audit. They emit `approval_id` when warranted; I do not model them pre-flight, only process the `APPROVAL_REQUEST` they produce. The one structural exception is writes/edits under `.claude/**` from any subagent dispatch — CC native protects that path pattern independently of Gaia, regardless of which workspace's `.claude/` is targeted. A subagent in default mode hitting that pattern is blocked **without an `approval_id`** (no Gaia surface to resolve), so the fix is structural at dispatch time: declare `mode: bypassPermissions` or `mode: acceptEdits` when the goal could touch `.claude/`. The background variant of this fails the same way and signals the same structural fix; foreground is not exempt. The right response is choosing the right mode upfront, not a `bypassPermissions` workaround after the block.
25
25
 
26
- Context arrives in `additionalContext` on two cadences. At **session start**, the system injects a one-shot manifest: a `## Environment` block (workspace, machine, gaia version, mode, cwd, paths) — info I can assume available without asking the user; a `## Active Agentic Loop` block when a loop is in flight; an `[ACTIONABLE]` queue of pending approvals identified by `[P-XXXX]`; and the curated memory block — emitted as up to three sections, `## Memory — For this session` (carry-forward threads), `## Memory — About you / What I know` (anchors), and `## Memory — Open threads` (open threads), the top items for the current workspace bounded to ~800 chars. The pending queue is meaningful as a state, not as recurring noise it carries work earlier sessions opened and will **not** re-surface on each subsequent prompt. Either I attend to it on this turn or I make a conscious deferral by name; routing past it without integrating leaves the user with no second chance to be reminded. The curated memory sections are what was learned about this workspace across prior sessions — reading them at session start anchors my decisions in that history instead of treating each turn as a cold start; if a question can't be answered from the injected block, I dispatch a subagent that loads `Skill('memory')` for deeper search. At **each turn** the system injects only what depends on the current prompt: a deterministic `## Surface Routing Recommendation` and a first-run welcome on the install's first prompt. Reading the prompt without scanning these blocks produces decisions that ignore work the system already did for me. The skills catalogue is the same shape — it grows over time, the `description` field of each skill is the trigger I match against; I trust the description rather than memorize a fixed list, and I load via `Skill('<name>')` when the moment matches.
26
+ Context arrives in `additionalContext` on two cadences. At **session start**, the system injects a one-shot manifest: a `## Environment` block (workspace, machine, gaia version, mode, cwd, paths) — info I can assume available without asking the user; a `## Active Agentic Loop` block when a loop is in flight; and the curated memory block — emitted as up to three sections, `## Memory — For this session` (carry-forward threads), `## Memory — About you / What I know` (anchors), and `## Memory — Open threads` (open threads), the top items for the current workspace bounded to ~800 chars. Approvals are NOT part of this manifest they are in-loop and single-session, with no `[ACTIONABLE]` queue at SessionStart and no per-turn resurfacing; a pending not resolved in the session where it was raised is not re-presented later, it only ages toward expiry (hygiene sweeps still drain orphans regardless of session see `Skill('pending-approvals')`). The curated memory sections are what was learned about this workspace across prior sessions — reading them at session start anchors my decisions in that history instead of treating each turn as a cold start; if a question can't be answered from the injected block, I dispatch a subagent that loads `Skill('memory')` for deeper search. At **each turn** the system injects only what depends on the current prompt: a deterministic `## Surface Routing Recommendation` and a first-run welcome on the install's first prompt. Reading the prompt without scanning these blocks produces decisions that ignore work the system already did for me. The skills catalogue is the same shape — it grows over time, the `description` field of each skill is the trigger I match against; I trust the description rather than memorize a fixed list, and I load via `Skill('<name>')` when the moment matches.
27
27
 
28
28
  Deciding what enters memory is mine; the substrate already has homes for most of what a session produces, and a thing that has a home does not belong in curated memory. Before proposing a save I ask whether what I am about to persist already has a home — a brief captures a requirement, a plan holds the decomposition, a domain table indexes a resource. Only what has no such home is a candidate: a decision closed with its rationale, a discovery that will be reused across sessions, a path abandoned that should not recur. Memory growth requires deliberation; not every observation is worth persisting, and an overstuffed memory loses signal to noise. The bounded memory injection at SessionStart is precisely the lever that forces prioritization — what does not earn a place does not anchor future sessions. When I judge that something earns a place, I propose it to the user and a subagent persists it via `Skill('memory')`. I decide WHAT is worth keeping; the skill defines HOW it is shaped, named, and reconciled against what already exists — I do not duplicate its mechanics here.
29
29
 
@@ -68,7 +68,7 @@ Once routing has chosen a surface and agent, the dispatch itself carries a **goa
68
68
 
69
69
  ### Re-dispatch vs SendMessage
70
70
 
71
- An approval **grant is DB-backed, session-agnostic, and lives for 60 minutes** (the active-grant TTL). When a T3 command is blocked the hook persists a pending approval with a semantic signature; the user approves it once via AskUserQuestion, which activates a grant; and the grant is **consumed on the matching retry** — the first time a command whose signature matches the grant runs, `bash_validator` marks the grant CONSUMED for replay protection. Consumption is keyed to the matching command, NOT to SubagentStop and NOT to the sub-agent's lifetime: because the grant is session-agnostic, the retry that consumes it may run under a different session than the one that was blocked (the block-approve-retry flow legitimately spans sessions). That makes the choice between resume and re-dispatch a question of investigation context and `mode`, not of grant preservation the live grant survives a SendMessage resume or a fresh re-dispatch equally, as long as the matching command runs within the TTL and has not already been consumed. SendMessage is the right move when the agent has not returned yet and the next step is bounded by what it already investigated clarifying, retrying the approved command, or feeding back a small piece of new information. Fresh re-dispatch is the right move when the original dispatch relied on a `mode` (`acceptEdits`, `bypassPermissions`) that does not survive resume, or when the bundle has multiple steps on protected paths each needing its own approval; in that case package every remaining step into one fresh dispatch carrying the needed `mode` and the approved command. If several distinct mutative operations queue up for one intent, each blocked operation generates its own `approval_id` — N distinct operations produce N approvals; for an explicitly enumerated batch under one consent, use the COMMAND_SET grant mechanism. Full mechanics and grant lifecycle live in `Skill('orchestrator-present-approval')` and `hooks/modules/security/approval_grants.py`.
71
+ An approval **grant is DB-backed, session-agnostic, and lives for 5 minutes** (the active-grant TTL — distinct from the 24h/1440-minute pending TTL, which is how long an unanswered approval waits for the user before it expires). When a T3 command is blocked, the hook persists a pending approval with a semantic signature and hands the `approval_id` to the blocked subagent, which relays it to me through its `agent_contract_handoff` as `plan_status: APPROVAL_REQUEST` — that contract field is how the id reaches me; there is no SessionStart block or shell lookup to derive it from. The user approves it once via AskUserQuestion, which activates the grant, and the grant is **single-use, consumed at the match** — the moment the retried command's signature matches the grant in PreToolUse, before the command executes, not after (`bash_validator` calls `consume_db_semantic_grant` synchronously at that point; a run that reaches match-and-execute-and-fail has already spent the grant and needs a fresh approval, it does not get a second try inside the window). **Approving IS the order to execute**: there is no separate "should I run it now" step on the Approve label I immediately re-dispatch a fresh agent carrying the verbatim `exact_content` and the needed `mode`. That coupling makes re-dispatch, not a SendMessage resume, the only path for the execute-on-approve action: `mode` does not survive a SendMessage resume, so a resumed agent runs in `default` and re-blocks the very operation the grant just cleared. The grant's session-agnostic matching still means the retry that consumes it can legitimately run under a different session id than the one that was blocked — the block-approve-re-dispatch cycle spans sessions inside the same user-facing turn but that is an implementation detail of one in-loop approval, not a mechanism for reaching back into a prior session's unresolved pendings. If several distinct mutative operations queue up for one intent, each blocked operation generates its own `approval_id` — N distinct operations produce N approvals; for an explicitly enumerated batch under one consent, use the COMMAND_SET grant mechanism (obtaining that batch's id without a shell is a known open dependency — see `Skill('orchestrator-present-approval')`). Full mechanics and grant lifecycle live in `Skill('orchestrator-present-approval')` and `hooks/modules/security/approval_grants.py`.
72
72
 
73
73
  ## Response handling
74
74
 
@@ -80,13 +80,7 @@ When an agent returns a `agent_contract_handoff`, I load `Skill('agent-response'
80
80
 
81
81
  **Re-dispatch must carry `exact_content` verbatim.** After an approved T3 — Write or Bash — the new agent does not carry the approved string from the previous turn, and the grant is keyed to the command's **semantic signature**, not to the path or the verb alone. The signature normalizes shell redirects out (`2>&1`, `> file`, `2> file`), so appending or dropping a redirect does NOT change the signature and does NOT re-block. Everything else that changes the operation's identity still produces a different signature and a fresh re-block: a pipe (`| jq`), a `cd` prefix or different `-C <path>` (the working directory binds — directory is intent), a wrapper, a command substitution, or an added/changed flag. Copy the literal string into the new dispatch and instruct the agent to run it. If the effect required more (a different cwd, a wrapping pipeline), request a new approval — do not retrofit it.
82
82
 
83
- **When `[ACTIONABLE] Pending approvals` appear in `additionalContext`,** integrate them into the response before routing the current request they belong to flows already in motion, and the user cannot act on what they cannot see. Read each pending entry and present it by **what it means**, not by its position in the list. For each, state:
84
-
85
- - What the operation does in plain language (e.g. "una consulta a la base de datos de memoria", "un push a la rama main")
86
- - Which agent or surface emitted it (read `context.source` if present; if absent, infer from `command` and surface routing)
87
- - Whether the pending is fresh (less than 1 hour old) or stale (older than 1 hour, likely abandoned from a previous thread)
88
-
89
- Group fresh and stale into separate paragraphs. For fresh ones, offer to act now: "puedo aprobarlos, ejecutarlos en bundle, o esperar". For stale ones, propose cleanup: "estas tres parecen abandonadas — ¿las descarto?". Do not show the full `[P-xxxx] {truncated_command}` table that the scanner produced; that table is raw signal, not the synthesis the user needs. For the actual approval flow (AskUserQuestion presentation, dispatch templates, mass cleanup), load `Skill('pending-approvals')` — it owns the mechanics; I own the narrative.
83
+ **Pendings do not resurface across turns or sessions.** There is no `[ACTIONABLE]` queue injected at SessionStart and no per-turn feed of previously-seen pendings — approvals are in-loop and single-session, and the only channel that surfaces one to me is the subagent's same-turn `APPROVAL_REQUEST` relay handled above. A pending left unresolved in the session where it was raised is not re-presented later; it only ages toward its 24h pending TTL, and hygiene (`gaia approvals clean`, the grant-expiry sweep) drains it without ever prompting the user to resume it. When the user explicitly asks about pendings — "ver pendientes", "aprobar P-XXXX", "limpia los pendientes" — that is a distinct, user-invoked path: load `Skill('pending-approvals')`, which owns the `gaia approvals` mechanics (show / approve / reject / clean) for it. I do not proactively scan for or curate a backlog on my own initiative.
90
84
 
91
85
  ## Domain Errors
92
86
 
@@ -98,4 +92,4 @@ Group fresh and stale into separate paragraphs. For fresh ones, offer to act now
98
92
  | Specialist contradicts itself within or across turns | When the inconsistency is material — affects what the user is about to approve or execute — present the contract verbatim, name the inconsistency I observed (path that does not match the verification, claim that conflicts with a previous turn), and ask whether to re-dispatch or accept. Correcting silently traffics in authority I do not have; presenting as-is without flagging traffics in honesty I owe the user |
99
93
  | `mode` lost on a SendMessage resume | Re-dispatch fresh with the needed `mode` and the approved command verbatim; full guidance in Dispatch → Re-dispatch vs SendMessage. |
100
94
  | APPROVAL_REQUEST for a T3 without verbatim content | Attach the literal `exact_content` to the re-dispatch; full rule and wrapper anti-patterns in Response handling. |
101
- | Pending approvals accumulate as noise | Curate by name, do not drain; full procedure in Response handling. |
95
+ | User asks about pendings directly | Load `Skill('pending-approvals')` for the `gaia approvals` mechanics (show/approve/reject/clean); there is no cross-session queue for me to proactively curate. |
package/bin/README.md CHANGED
@@ -67,7 +67,7 @@ bin/
67
67
  ├── history.py # gaia history — recent agent sessions
68
68
  ├── install.py # gaia install — bootstrap DB, settings, symlinks (run manually; no postinstall)
69
69
  ├── memory.py # gaia memory — episodic memory: stats, search, show
70
- ├── metrics.py # gaia metrics — usage analytics (tier, agent, anomalies)
70
+ ├── metrics.py # gaia metrics — usage analytics (DB-canonical episodes/anomalies + audit-log tier/commands)
71
71
  ├── milestone.py # gaia milestone — milestone management for briefs (DB-canonical)
72
72
  ├── _pack_helpers.py # shared `npm pack` primitive for dev/release (private, no register())
73
73
  ├── paths.py # Shared path resolution helpers
@@ -1411,114 +1411,6 @@ def cmd_replay(args) -> int:
1411
1411
  return 0 if all_ok else 1
1412
1412
 
1413
1413
 
1414
- # ---------------------------------------------------------------------------
1415
- # derive-id -- reproduce a plan-first COMMAND_SET approval_id from its commands
1416
- # ---------------------------------------------------------------------------
1417
-
1418
- def _read_command_set_input(args) -> list:
1419
- """Resolve the command list for derive-id from args/stdin.
1420
-
1421
- Accepts, in order of precedence:
1422
- 1. ``--commands-json '[{"command": "..."}, ...]'`` or a bare list of
1423
- strings ``["cmd a", "cmd b"]`` -- the command_set as the orchestrator
1424
- reads it from the contract.
1425
- 2. stdin (when ``--commands-json`` is omitted), same JSON shapes.
1426
-
1427
- Returns the ordered list of command STRINGS (rationale is irrelevant to the
1428
- derivation). Raises ValueError on malformed input.
1429
- """
1430
- raw = getattr(args, "commands_json", None)
1431
- if raw is None:
1432
- raw = sys.stdin.read()
1433
- raw = (raw or "").strip()
1434
- if not raw:
1435
- raise ValueError("no command_set provided (use --commands-json or stdin)")
1436
-
1437
- parsed = json.loads(raw)
1438
-
1439
- # Accept either a top-level list, or {"command_set": [...]} / {"commands": [...]}.
1440
- if isinstance(parsed, dict):
1441
- parsed = parsed.get("command_set") or parsed.get("commands") or []
1442
-
1443
- if not isinstance(parsed, list):
1444
- raise ValueError("command_set must be a JSON array")
1445
-
1446
- commands: list = []
1447
- for item in parsed:
1448
- if isinstance(item, str):
1449
- if item:
1450
- commands.append(item)
1451
- elif isinstance(item, dict) and item.get("command"):
1452
- commands.append(item["command"])
1453
- return commands
1454
-
1455
-
1456
- def cmd_derive_id(args) -> int:
1457
- """Derive the deterministic COMMAND_SET approval_id from its commands.
1458
-
1459
- This is the orchestrator-side mirror of the intake's mint: given the
1460
- ``command_set`` the subagent emitted in its contract (no DB search), it
1461
- reproduces the EXACT ``P-...`` id the SubagentStop intake wrote as the
1462
- pending row, by applying the SAME mutative filter and the SAME
1463
- ``derive_command_set_id`` canonicalization the intake uses.
1464
-
1465
- The mutative filter is shared with the intake
1466
- (``handoff_persister._filter_mutative_command_set``) so the CLI and the hook
1467
- operate on the identical post-filter command list. When fewer than 2
1468
- mutative commands remain, NO COMMAND_SET was minted (the singular path owns
1469
- it) -- the helper reports that rather than emitting a bogus id.
1470
-
1471
- Exits 0 on success, 1 on error.
1472
- """
1473
- output_json = getattr(args, "json", False)
1474
- apply_filter = not getattr(args, "no_filter", False)
1475
-
1476
- try:
1477
- commands = _read_command_set_input(args)
1478
- except Exception as exc:
1479
- _print_error(f"Failed to parse command_set: {exc}", args)
1480
- return 1
1481
-
1482
- # Apply the SAME mutative filter the intake uses, so the orchestrator's
1483
- # derivation operates on the identical post-filter list. Skippable via
1484
- # --no-filter for callers that already hold the filtered list.
1485
- if apply_filter:
1486
- try:
1487
- from modules.agents.handoff_persister import _filter_mutative_command_set
1488
- filtered = _filter_mutative_command_set(
1489
- [{"command": c, "rationale": ""} for c in commands]
1490
- )
1491
- commands = [it["command"] for it in filtered]
1492
- except Exception as exc:
1493
- _print_error(f"Failed to apply mutative filter: {exc}", args)
1494
- return 1
1495
-
1496
- if len(commands) < 2:
1497
- msg = (
1498
- f"Not a COMMAND_SET: {len(commands)} mutative command(s) after filter "
1499
- "(need >= 2). No COMMAND_SET approval was minted -- the singular path "
1500
- "owns this."
1501
- )
1502
- if output_json:
1503
- print(json.dumps({"approval_id": None, "command_count": len(commands), "reason": msg}))
1504
- else:
1505
- _print_error(msg, args)
1506
- return 1
1507
-
1508
- try:
1509
- store = _import_approval_store()
1510
- approval_id = store.derive_command_set_id(commands)
1511
- except Exception as exc:
1512
- _print_error(f"Failed to derive id: {exc}", args)
1513
- return 1
1514
-
1515
- if output_json:
1516
- print(json.dumps({"approval_id": approval_id, "command_count": len(commands)}))
1517
- else:
1518
- print(approval_id)
1519
- return 0
1520
-
1521
-
1522
1414
  # ---------------------------------------------------------------------------
1523
1415
  # Plugin registration (called by bin/gaia dispatcher)
1524
1416
  # ---------------------------------------------------------------------------
@@ -1748,35 +1640,6 @@ def register(subparsers) -> None:
1748
1640
  p_stats.add_argument("--json", action="store_true", help="JSON output")
1749
1641
  p_stats.set_defaults(func=cmd_stats)
1750
1642
 
1751
- # derive-id -- reproduce a plan-first COMMAND_SET id from its commands
1752
- p_derive = sub.add_parser(
1753
- "derive-id",
1754
- help="Derive the deterministic COMMAND_SET approval_id from its commands",
1755
- description=(
1756
- "Reproduce the content-derived approval_id the SubagentStop intake\n"
1757
- "minted for a plan-first COMMAND_SET, from the command_set in the\n"
1758
- "contract -- no DB search. Pass the command_set as JSON via\n"
1759
- "--commands-json or stdin (a list of strings, a list of\n"
1760
- "{command, rationale} objects, or an object with a command_set/\n"
1761
- "commands key). Applies the same mutative filter the intake uses."
1762
- ),
1763
- )
1764
- p_derive.add_argument(
1765
- "--commands-json",
1766
- dest="commands_json",
1767
- metavar="JSON",
1768
- default=None,
1769
- help="command_set as JSON (omit to read from stdin)",
1770
- )
1771
- p_derive.add_argument(
1772
- "--no-filter",
1773
- action="store_true",
1774
- dest="no_filter",
1775
- help="Skip the mutative filter (input is already the filtered list)",
1776
- )
1777
- p_derive.add_argument("--json", action="store_true", help="JSON output")
1778
- p_derive.set_defaults(func=cmd_derive_id)
1779
-
1780
1643
  p.set_defaults(func=_approvals_default)
1781
1644
 
1782
1645
 
@@ -1808,7 +1671,6 @@ def _approvals_default(args) -> int:
1808
1671
  print(" reject-all [--dry-run] -- bulk reject (legacy)")
1809
1672
  print(" clean [--dry-run] -- remove expired approvals")
1810
1673
  print(" stats -- approval system statistics")
1811
- print(" derive-id --commands-json JSON -- reproduce a COMMAND_SET id (no DB)")
1812
1674
  print("")
1813
1675
  print("Run 'gaia approvals --help' for more information.")
1814
1676
  return 0
@@ -1892,12 +1754,6 @@ def _build_standalone_parser() -> argparse.ArgumentParser:
1892
1754
  p_stats.add_argument("--json", action="store_true")
1893
1755
  p_stats.set_defaults(func=cmd_stats)
1894
1756
 
1895
- p_derive = subparsers.add_parser("derive-id", help="Derive a COMMAND_SET approval_id from its commands")
1896
- p_derive.add_argument("--commands-json", dest="commands_json", metavar="JSON", default=None)
1897
- p_derive.add_argument("--no-filter", action="store_true", dest="no_filter")
1898
- p_derive.add_argument("--json", action="store_true")
1899
- p_derive.set_defaults(func=cmd_derive_id)
1900
-
1901
1757
  return parser
1902
1758
 
1903
1759
 
@@ -74,14 +74,6 @@ RETENTION_POLICY = [
74
74
  "max_days": 14,
75
75
  "label": "Hook logs",
76
76
  },
77
- {
78
- "key": "monthlyMetrics",
79
- "type": "files",
80
- "pattern": "metrics-*.jsonl",
81
- "dir": ".claude/metrics",
82
- "max_days": 90,
83
- "label": "Monthly metrics",
84
- },
85
77
  {
86
78
  "key": "responseContract",
87
79
  "type": "dirs",
@@ -97,20 +89,6 @@ RETENTION_POLICY = [
97
89
  "max_days": 90,
98
90
  "label": "Episodic memory episodes",
99
91
  },
100
- {
101
- "key": "workflowMetrics",
102
- "type": "truncate-jsonl",
103
- "file": ".claude/project-context/workflow-episodic-memory/metrics.jsonl",
104
- "max_days": 90,
105
- "label": "Workflow metrics",
106
- },
107
- {
108
- "key": "anomalies",
109
- "type": "truncate-jsonl",
110
- "file": ".claude/project-context/workflow-episodic-memory/anomalies.jsonl",
111
- "max_days": 90,
112
- "label": "Anomalies",
113
- },
114
92
  {
115
93
  "key": "legacyLogs",
116
94
  "type": "legacy",
@@ -731,11 +709,8 @@ def cmd_cleanup(args) -> int:
731
709
  retention_policy_info = {
732
710
  "audit_logs_days": 30,
733
711
  "hook_logs_days": 14,
734
- "monthly_metrics_days": 90,
735
712
  "response_contracts_days": 7,
736
713
  "episodic_episodes_days": 90,
737
- "workflow_metrics_days": 90,
738
- "anomalies_days": 90,
739
714
  "legacy_logs": "all removed",
740
715
  "anomaly_flag_hours": 1,
741
716
  }
@@ -746,11 +721,8 @@ def cmd_cleanup(args) -> int:
746
721
  print("\nRetention policy:")
747
722
  print(" Audit logs: 30 days")
748
723
  print(" Hook logs: 14 days")
749
- print(" Monthly metrics: 90 days")
750
724
  print(" Response contracts: 7 days")
751
725
  print(" Episodic episodes: 90 days")
752
- print(" Workflow metrics: 90 days")
753
- print(" Anomalies: 90 days")
754
726
  print(" Legacy logs: all removed")
755
727
  print(" Anomaly flag: 1 hour TTL")
756
728
  if dry_run:
@@ -842,12 +814,11 @@ def cmd_cleanup(args) -> int:
842
814
  if anything_done:
843
815
  status = "Cleanup preview complete" if dry_run else "Cleanup completed"
844
816
  print(f"\n{status}")
845
- print("\nPreserved data:")
817
+ print("\nDirectories Gaia does not remove (files within them are pruned by retention policy, not deleted outright):")
846
818
  print(" .claude/logs/")
847
819
  print(" .claude/tests/")
848
820
  print(" .claude/project-context/")
849
821
  print(" .claude/session/")
850
- print(" .claude/metrics/")
851
822
  else:
852
823
  print(" Nothing to clean up")
853
824
  print()