| name | pending-approvals |
| description | Use when the user invokes approvals directly -- "ver pendientes", "aprobar P-XXXX", "rechazar P-XXXX", "approve P-", "reject P-" |
| metadata | {"user-invocable":true,"type":"technique"} |
Pending Approvals
pending-approvals is the workflow the orchestrator follows when the user
drives an approval -- "ver pendientes", "aprobar P-XXXX", "rechazar P-XXXX".
Approvals are in-loop and single-session: a pending is raised and resolved
within the same session, and there is no proactive surfacing of it outside
that flow -- no [ACTIONABLE] block at SessionStart, no per-turn resurfacing.
The orchestrator's role here is to translate an explicit user ask into the
right gaia approvals subcommand against the right store. See "Approvals do
not resurface across sessions" below for what does and does not survive
cross-session.
For the universal envelope of an approval payload see agent-approval-protocol;
for how the orchestrator relays a subagent-initiated APPROVAL_REQUEST into
AskUserQuestion see orchestrator-present-approval -- that skill owns the
verbatim-COMANDO, [P-{nonce8}] label, and single-use grant discipline; this
skill does not restate them.
The DB / filesystem store gap
There are two stores for pending approvals, and the CLI subcommands do not
cover them symmetrically. Misreading this surface is what makes the orchestrator
report "rejected" when nothing actually changed.
| Subcommand | Store | Backed by |
|---|
gaia approvals pending | DB only | store.list_pending |
gaia approvals history | DB only | store.list_all |
gaia approvals approve P-xxx | DB only | store.approve |
gaia approvals revoke P-xxx | DB only | store.revoke (falls back to legacy) |
gaia approvals show P-xxx | DB first, then filesystem | cmd_show_v2 in bin/cli/approvals.py |
gaia approvals list | DB grants + filesystem pendings | cmd_list (mixed) |
gaia approvals reject NONCE | filesystem only | reject_pending in hooks/modules/security/approval_grants.py |
gaia approvals reject-all | filesystem only | loops reject_pending |
gaia approvals clean | DB (cross-session stale pendings) + filesystem | cmd_clean in bin/cli/approvals.py: calls store.list_pending(all_sessions=True), transitions every pending older than DEFAULT_PENDING_TTL_MINUTES (24 h) to revoked via store.revoke(), then calls cleanup_expired_grants for filesystem files |
The practical consequence: revoke is the DB-aware single-id verb; reject and
reject-all only touch the legacy filesystem queue. If you need to mark a DB
row as terminated, use revoke. Bulk DB cleanup currently has no first-class
CLI -- it requires a Python loop over store.revoke().
Approvals do not resurface across sessions
Pendings are in-loop: they are raised by a blocked T3 command in the current
session and expected to be resolved in that same session, by the user acting
on the block message or asking directly ("ver pendientes", "aprobar P-XXXX").
There is no automatic resumption surface -- no [ACTIONABLE] block injected
at SessionStart, no per-turn feed of verified pendings. If the user does not
act on a pending in the session where it was raised, it is not re-presented
later; it simply ages toward expiry.
Two things survive cross-session, and neither is surfacing:
- Hygiene.
gaia approvals clean (and the periodic grant-expiry sweep)
still drains orphaned pendings and expired grants regardless of which
session created them. This is housekeeping, not a resumption prompt to the
user -- it never asks "do you want to resume P-XXXX?".
- Session-agnostic grant matching. The DB lookup that checks whether a
retried command has an active grant (
check_db_semantic_grant) is not
scoped to a single session_id, because the block -> approve -> re-dispatch
cycle can legitimately span the subagent's session and the orchestrator's
session inside the SAME user-facing turn. This is an implementation detail
of one in-loop approval, not a mechanism for reaching back into a prior
session's unresolved pendings.
When the user explicitly asks to see pendings from another session (e.g.
gaia approvals pending --all-sessions), that is a deliberate, user-invoked
query -- distinct from proactive resurfacing, and still supported.
When user says "ver P-XXXX"
- Run
gaia approvals show P-XXXX -- cmd_show_v2 checks the DB row first,
then falls back to the filesystem pending. Either path returns the verbatim
command and the context fields.
- Relay the detail to the user. Do not paraphrase the command.
- Ask whether to approve or reject.
When user says "aprobar P-XXXX"
- If you have not already, call
gaia approvals show P-XXXX to load the
verbatim command and context fields (cmd_show_v2 checks the DB then the
filesystem).
- Present the approval via
AskUserQuestion following
orchestrator-present-approval -- 5 labeled fields and the [P-{nonce8}]
option label. The same presentation works for both stores; the nonce-prefix
matcher (activate_db_pending_by_prefix in
hooks/modules/security/approval_grants.py) covers DB rows and the legacy
path covers filesystem pendings.
- On
"Approve", the ElicitationResult hook writes SHOWN + APPROVED
events and activates a single-use grant (5-minute TTL) in the current
session.
- Approving IS the order to execute -- there is no separate "should I run it
now" step. The orchestrator immediately re-dispatches a one-shot agent with
the verbatim command using the dispatch template in
reference.md
(preflight + recovery, mode per target).
The grant is consumed at match -- the moment the retried command
authorizes against the grant, before it executes -- not after the command
finishes. This has two consequences:
- If the re-dispatched subagent dies before it reaches the command (crash,
turn limit, disconnect), the grant is still alive; a re-dispatch within the
5-minute TTL reuses it without a new approval.
- If the command reached the point of match and then executed and failed, the
grant was already consumed. Do not retry inside the window expecting it to
still work -- report the failure and request a fresh approval.
The CLI gaia approvals approve P-XXXX is the explicit, user-invoked admin
path: it inserts APPROVED directly in the DB and does not create a
hook-side grant, so it does not trigger the approve-executes-automatically
flow above. Use it only when the user explicitly wants the CLI-only path
(audit, marking a row from a different session as decided). For any approval
that needs to execute the blocked command in this session, AskUserQuestion is
the only path that activates the grant and the automatic re-dispatch.
When user says "rechazar P-XXXX" / "reject P-XXXX"
Single rejection has two routes; pick by which store owns the row.
- DB row:
gaia approvals revoke P-XXXX --yes -- store.revoke inserts a
REVOKED event and updates approvals.status to revoked.
- Filesystem pending:
gaia approvals reject P-XXXX -- reject_pending
rewrites the JSON file with status: "rejected" (no rm, which would itself
be T3).
Confirm to the user: "P-XXXX rechazado." If revoke returns not_found, fall
back to reject before declaring failure -- the row may have been a legacy
filesystem pending.
Bulk cleanup
Offer bulk cleanup when the user says "limpia todos los pendings", "borra los
pendientes", or when the user explicitly checks gaia approvals pending --all-sessions and finds a backlog of orphaned rows. This is never something
the orchestrator surfaces proactively -- there is no SessionStart scan that
counts stale pendings and offers to clean them; the user has to ask, or to
have already looked.
gaia approvals reject-all -- bulk soft-reject across the filesystem queue.
Returns "0 rejected" when the queue is empty. Does not touch DB rows.
gaia approvals clean -- the first-class cross-session bulk drain for stale
DB pendings: cmd_clean calls store.list_pending(all_sessions=True) and
transitions every pending older than 24 h (DEFAULT_PENDING_TTL_MINUTES) to
revoked via store.revoke(), then runs cleanup_expired_grants to clean
expired filesystem grant files. Runs without a T3 prompt (consent-reducing,
listed in CONSENT_REDUCING_SUBCOMMAND_EXCEPTIONS). This is hygiene, not
surfacing: it drains orphans without presenting them for resumption. Use it
when gaia approvals pending --all-sessions shows a backlog of stale rows.
Do not report "bulk cleanup done" after reject-all alone -- it only clears
the filesystem queue. Run gaia approvals clean to drain the DB backlog, then
confirm with gaia approvals pending --all-sessions.
Do not offer reject-all when there are active same-session pendings the user
may still want to approve.
Anti-patterns
- Approving without showing the exact COMANDO -- the user consents on the
verbatim string, not a summary. The full presentation discipline lives in
orchestrator-present-approval; this skill does not restate it.
- Treating
gaia approvals reject-all as a full cleanup -- it operates on the
filesystem queue only; DB rows survive the call. Use gaia approvals clean
to drain the DB backlog.
- Reporting "rechazado" without verifying the store --
revoke returns
not_found for filesystem-only pendings; the inverse happens for reject on
DB rows. Pick the verb by store, or be ready to fall back.
- Dispatching execution before AskUserQuestion returns "Approve" -- the grant
does not activate until the ElicitationResult hook fires.
- Asking the user a second time whether to execute after they selected
"Approve" -- approving already is the order to execute; re-dispatch the
verbatim command immediately, do not insert an extra confirmation turn.
- Retrying a T3 command inside the 5-minute grant window after it already
executed and failed -- the grant was consumed at match, before execution;
a failed run needs a fresh approval, not a retry.
- Presenting a pending as resumed from a prior session via an
[ACTIONABLE]
block or a per-turn feed -- that surfacing no longer exists. A pending not
resolved in the session where it was raised is not re-presented; only an
explicit user query (gaia approvals pending --all-sessions) or the
hygiene sweep (gaia approvals clean) touches it afterward.
- Using
rm on a pending-*.json file -- deletion itself is T3 and blocks.
The legacy soft-reject path rewrites the file in place; use it.
For the dispatch template (preflight, recovery, mode selection) and the
filesystem pending JSON schema, read reference.md.