daniel-ospina

enforcement

Manage the sequence-enforcer that gates skill execution. Check mode, change enforcement level, or kill-switch.

daniel-ospina 0 2 Actualitzat fa 1 d
GitHub

Instal·la

npx skillscat add daniel-ospina/agent-infra/enforcement

Instal·la des del registre de SkillsCat.

SKILL.md

⛔ This skill MUST be read in full — not skimmed. Formal review gates depend on its workflow.
Skipping steps silently bypasses quality checks. Missing gates = undetected breakages.

⛔ HUMAN APPROVAL REQUIRED for ALL operations except checking current mode. Do not change enforcement level, kill enforcement, or re-enable enforcement without explicit human authorization. The kill switch exists as a safety valve for humans — agents must NEVER engage it autonomously. If enforcement is blocking legitimate work, escalate to the human.

Enforcement

Manages the sequence-enforcer that validates agent tool calls against YAML skill step declarations.

Print / Background Sessions (pi -p)

Non-interactive pi -p sessions (epic-executor sub-agents, background workers) default to warn — never blocking. Post-#357 this covers ALL print sessions, env- OR argv-detected (bare-shell pi -p with no PI_MODE included) — see "#201 carve-out reversal" below. Explicit overrides always win: AGENT_SEQUENCE_MODE=gate|strict, PI_ENFORCER_MODE, or the mode file force full enforcement in print sessions. Interactive sessions keep the gate default.

⚠️ Verifier-gate trade-off of the reversal: the bare-shell pi -p class loses the verifier-step tool allow-list (task/subagent/read/loop_enforcer) — those calls become warn-only. The merge gate (review-enforcer) stays active; restore per-session with AGENT_SEQUENCE_MODE=gate. Monitoring: if warn_blocked volume on verifier gates from bare-shell sessions rises (enforcement.jsonl mode:warn + verifier steps), re-evaluate the default — the trigger is a spike, not a steady background.

Audit visibility (~/.pi/agent/audit/enforcement.jsonl) for background workers:

  • warn_blocked — a call that WOULD have been blocked under gate mode was allowed in warn mode; includes allowed + hint context.
  • timeout_park — a GATED step was parked at step N in print mode (state preserved), not popped. Written ONCE per activation-step (silent-window repeats are coalesced; if the lane keeps working, this line is all you will see), carries step_name, gate, parks, cap, and the derived cause/remedy.
  • timeout_idle_human — a print lane idled at a human_review / human_approval step. Reported once per episode, never bounded: a person owns the stall. (Distinct from timeout_idle_ungated, which means NO gate is applied — a human gate blocks every tool except read/web/loop_enforcer.)
  • timeout_idle_ungated — a print lane idled with NO gate holding it: a gate: auto step, or a checkpoint step in a warn session (warn applies no checkpoint gate). Written ONCE per idle episode (coalesced; an episode ends at the next tool call). This is the liveness signal for a parked worker — no gate is applied, so it can never be a gate stall and never produces a verdict.
  • sequence_gate_unsatisfiable — TERMINAL (#1770): a gated step stayed SILENT across the bound, so the lane emitted SEQUENCE-GATE-UNSATISFIABLE and exited 3. Carries step_name, gate, parks, cap, the MEASURED idle_min (since the last tool call) and step_min (since the step was entered), plus the derived cause/remedy. One row per stall, never per window. A lane that ends here did not fail silently — grep the marker in its log.
  • blocked entries now include allowed + hint so orchestrators can see why a worker is stuck and what the exit is.

Park policy (#1770). Parks are BOUNDED and NAMED, and a step with no gate is never park-bounded:

  • gate: auto → never parks; one coalesced timeout_idle_ungated line per idle episode, no verdict, no exit (nothing is blocked, so nothing can be unsatisfiable).
  • gate: verifier / empty gate → park ONCE per activation-step with the cause + remedy derived from that step's own gate, and exit 3 with SEQUENCE-GATE-UNSATISFIABLE after 3 consecutive silent windows (30 min with no tool call at all). The owner is found by STACK-WALK, so a gate below a sub-skill frame (a read is an escape tool at a verifier step and pushes a frame) is still bounded.
  • ⛔ This bounds SILENCE, not busyness. A window only elapses after 10 min with no tool event (every call/result re-arms the timer), so a lane that keeps calling tools — including one polling a background reviewer, or retrying a refused call — is never parked by this mechanism. That is deliberate: such a lane is visibly working to the orchestrator, the fleet board and the lane reaper, and an enforcer-side "busy but stuck" bound cannot distinguish it from a lane legitimately waiting on a 40-minute dispatch.
  • gate: checkpoint in a print lane → gate/strict sessions are bounded the same way (the gate blocks every git op, so it must never sit forever at rc 0), and an in-flight call suppresses it like any other; warn sessions — the default for pi -p — apply no checkpoint gate at all (the first call auto-advances), so they get the one coalesced idle line and no verdict.
  • The stall OWNER is resolved by one stack-walk, then classified: a human_* frame anywhere on top wins (terminal — a person owns the stall, so a checkpoint or verifier below it is never bounded).
  • A tool call IN FLIGHT suppresses the idle clock while it is outstanding. The set is FAIL-SAFE BY INVERSION: only provably instant tools (read, write, edit, grep, find, ls, todo_write, todo_read, loop_enforcer, task_status, task_collect, web_search, web_fetch) are excluded — everything else, including every mcp__* and any tool not named here, is assumed able to run for minutes (the opposite list silently missed this repo's own design_reviewer). It is tracked per toolCallId (pi runs sibling calls in parallel, so one result must never clear another call's liveness) and cleared by tool_execution_end — which pi emits on EVERY path, including a call that never ran because ANOTHER extension blocked it (a blocked call produces no tool_result, so a leaf listening only to tool_call/tool_result would hold a phantom entry and disarm the bound for the session). tool_call/tool_result and this extension's own block returns clear the same slot as a belt-and-braces. The only ceiling is the fleet's own task hard cap (6 h): an entry older than that cannot be a running tool, so a leak can never be permanent.
  • The count and the one-shot announcements are keyed per ACTIVATION of a step (path#stepIndex#activation), so a skill read again after being popped starts clean instead of being capped on its first silent window. The verdict reports measured times (step_min, idle_min), never parks × interval.
  • A human_review / human_approval step is never bounded: a person is the owner, so the lane gets the one coalesced idle line and no verdict.
  • A session escape or the kill switch bypasses this whole path: a bypassed session is never parked and never ended.
  • Interactive (non-print) sessions are handled FIRST and keep main's behaviour: pop any stale non-checkpoint frame; park a checkpoint frame one-shot only when the session is not warn and the step has stalled on the wall clock; never bound, never exit (a human is present and can run the checker or the force-pass). Nothing above applies to them.

Checkpoint Gates (#357 post-fix reality)

Checkpoint steps (gate: checkpoint, e.g. parallel_check_* in writing-plans / commit-workflow / executing-plans) are token-gated (Enforcement A #5039): a fresh phase-correct CLEAR token from parallel_work_check is required to pass. Canonical phases (C1–C5): start (executing-plans), scope (issue-scoping), plan (writing-plans), implement (commit-workflow + executing-plans), merge. There is NO verify checkpoint — the verifier gate is a separate mechanism. issue-scoping's start/scope checkpoints are prose instructions (self-enforced; no frontmatter gate). Post-fix (#357) the contract is satisfiable and escapable:

  • Warn mode (default for pi -p): the FIRST call at a checkpoint auto-advances with a checkpoint_skipped_warn audit entry (token_state: none/stale/missing-or-wrong-phase/corrupt/other/ok) — never blocks, never deadlocks. Advance is mode-independent: gate/strict advance on a valid token at tool_call; a producer-timing escape call advances at tool_result when the token transitions !ok→ok.
  • No-board skip (gate/strict): a no-board session (ALL board/card/agent/paths signal families empty — unset OR empty/whitespace; ANY non-empty signal → fail-closed NOT-no-board) gets a distinguishable CLEAR no-board-skip: <advisory> verdict from the vendored checker and a token carrying mode:"no-board-skip". The enforcer emits checkpoint_no_board_skip (distinct from checkpoint_force_pass) at token-driven advances; board sessions are unchanged (plain CLEAR, no skip audit).
  • The in-session escape (gate/strict): read + loop_enforcer are ALWAYS allowed at a pending checkpoint; bash ONLY for the sole-command checker invocation — the CANONICAL reference is $AGENT_INFRA_PATH/scripts/parallel_work_check.sh <phase> (the vendored checker; NOT any hardcoded swarm path) — resolved absolute path, optional env with the allowlisted keys GH_TOKEN|CHECKOUT_GUARD_ENFORCE|AGENT_INFRA_PATH, optional bare sudo, optional python3/uv run, mandatory .sh|.py suffix + phase arg; tab/NBSP/zero-width/metachars rejected. PARALLEL_CHECK_BIN is an INTERNAL bin-location override — never a gate-time env prefix (not in the escape allowlist, so env PARALLEL_CHECK_BIN=… fails closed at the gate). Everything else fails closed. A checker re-run at an ALREADY-ok checkpoint is blocked (checkpoint_token_fresh — re-running can REMOVE the token on UNKNOWN); non-checker tools at an already-ok checkpoint are allowed (the step advanced on the fresh token).
  • Operator force-pass (human-only): write /tmp/parallel-check-force.json — {"verdict":"CLEAR","phase":"<step token_phase>","operator":"<you>","origin":"shell","repo":"<git remote>","ts":"<ISO>"}. CONSUMED on ANY checkpoint advance (unlinked — including a real-token or warn-mode advance, so a leftover file can never pass a same-phase adjacent checkpoint), 60-min TTL (future timestamps rejected), phase + repo bound. New-session start unlinks lingering force files (machine-shared path — write it AFTER the checkpoint is reached; single-operator assumption); a mid-session /reload does NOT unlink. It is AUDITED as checkpoint_force_pass ONLY when it actually DROVE the pass — a real-token or warn-mode consume emits nothing (the event means "an operator CLEAR passed this step"). This is the operator path for gate-mode interactive worktree sessions where the checker CANNOT CLEAR by design (C1 DEFER on any non-main worktree branch).
    ⚠️ Trust boundary: /tmp is world-writable and the file carries no operator credential — this is workflow-enforcement, NOT a security boundary; the audit trail + 60-min TTL are the detections. The real token file has the same exposure — but IS repo-bound (gate/strict only; warn is binding-free). The token's repo is the URL form (git remote get-url of the resolved ops target, userinfo-stripped); binding compares token.repo against bindingRepo() = resolveRepo(PARALLEL_CHECK_REPO) || currentRepo(). No-remote / non-git cwd → both sides "unknown" → binding passes. Cross-repo mismatch → named-reason BLOCK → re-run the checker from this checkout. PARALLEL_CHECK_REPO is honored as a PATH (resolved via the same git remote get-url); set-but-not-a-git-dir → both "unknown". --repo X without the env binds X's remote — and --repo X from a different cwd with no env means the enforcer never learns the flag → named-reason BLOCK, remediation: "set PARALLEL_CHECK_REPO to the checkout path". SAME-REPO-DIFFERENT-URL-FORMAT also BLOCKs (https vs scp of the same origin) — re-run from the same checkout form. This is NOT "a token written in repo A fails repo B" — same repo does not guarantee a pass; phase, TTL, and URL format must all line up.
    Same-repo concurrent sessions: the token file is last-writer-wins and machine-global; it is NEVER unlinked on a token-driven advance (only the force file is one-shot) — the phase + TTL + repo-binding bound the concurrency class to fail-safe outcomes. A shared-token advance may be audit-attributed to EITHER concurrent session — accepted, documented. Mode-attribution sub-case: a no-board session advancing on a concurrent board session's CLEAR token emits a BOARD-mode audit with NO skip event (the event stream carries no tenant info); the post-merge lag tripwire queries for same-repo/same-phase multi-advance anomalies alongside the existing no-board-lacks-event query (per-session nonce deferred).
  • Recovery (park-only, never pop): ≥3 consecutive blocked calls at a checkpoint OR a >5-min wall-clock stall → immediate PARK (state preserved) with one checkpoint_block_recovery event per checkpoint. Blocked-call audit entries are coalesced (≤20/60s per step) to bound log volume. Conditional signature: per-call blocked + 1 recovery event + 1 park/10min. In an INTERACTIVE session a relentless retry loop re-arms the 10-min timer on every blocked call and produces no further recovery signal (the human present is the escalation). In a PRINT lane the #1770 bound applies instead (gate/strict sessions only — warn sessions are excepted, see the park policy above): a checkpoint that stays SILENT for 3 consecutive windows emits SEQUENCE-GATE-UNSATISFIABLE and the lane exits 3.
  • Fail-closed diagnosis: a checkpoint step missing token_phase in its frontmatter is unpassable-by-design (blocks + ⚠️ F2 warning at activation listing the step) — fix the skill declaration.

Audit event set (checkpoint): checkpoint_skipped_warn, checkpoint_force_pass, checkpoint_no_board_skip, checkpoint_block_recovery, checkpoint_token_fresh. Mutual exclusivity + mode domain: skip audits carry token_mode/mode (no-board-skip), force-pass carries viaForce — a skip-token advance emits NO force-pass audit and a force-driven advance emits NO skip audit. Audit-gap note (deploy window): tokens written PRE-deploy carry no mode → after the new enforcer deploys they advance as board-mode CLEAR with no skip audit — re-run the checker once. The audit is HUMAN-READ-ONLY — enforcement.jsonl has no automated consumer; a reader is a swarm-side follow-up.

#201 carve-out reversal (#357)

The pi -p → warn default is now argv-aware: resolveMode uses isPrintMode(env, argv) (bare-shell pi -p with no PI_MODE env included). The old carve-out (bare pi -p keeps gate because it can dispatch task) is REVERSED — the checkpoint gate is unsatisfiable/inescapable in exactly that class, so warn is the safe default for ALL print sessions. Explicit overrides (AGENT_SEQUENCE_MODE, ELDATO_SEQUENCE_MODE, PI_ENFORCER_MODE, mode file) still force gate/strict. --mode json/rpc spawns remain a documented gate-resolving residual.

Check Current Mode (agent may use)

cat /tmp/agent-sequence-mode

Modes: warn (log only), gate (block destructive without skill read), strict (block all unexpected).

Change Mode (⛔ HUMAN APPROVAL REQUIRED)

echo <mode> > /tmp/agent-sequence-mode

Emergency Kill Switch (⛔ HUMAN APPROVAL REQUIRED — agents must NEVER use autonomously)

touch /tmp/agent-state-machine.kill   # disables ALL enforcement
rm /tmp/agent-state-machine.kill      # re-enables

This is a safety valve for humans. If an agent is blocked by a false positive, escalate to the human — do NOT touch the kill switch.

View Audit Log (agent may use)

cat ~/.pi/agent/audit/enforcement.jsonl

Continue following the workflow as mandated by this skill. Do not skip steps.