Skip to content

feat: stateless phase gate and gated tools - #39

Merged
renezander030 merged 1 commit into
masterfrom
feat/phase-gates
Sep 25, 2026
Merged

renezander030 merged 1 commit into
masterfrom
feat/phase-gates

Conversation

@renezander030

Copy link
Copy Markdown
Owner

What changed

  • phase gate. Ordered phases, each with requires (ids of other gates). Being in a phase requires the gates of that phase and every earlier one to pass when checked. The active phase is read from a plain marker file (.skillgate/phase by default). Nothing is stored or signed, and each required gate runs once per run.
  • skillgate phase [<id>]. Without an id it shows each phase's status. With an id it moves to that phase only if the phase's requirements pass (exit 2 otherwise).
  • gatedTools. Tool-name globs that cross the finish line like finishLine commands, for example MCP tools that publish or deploy. Where it is checked:
    • skillgate gate reads the tool name from the Claude Code, Codex and Gemini CLI hook payload, or takes --tool <name>.
    • The opencode plugin checks it in tool.execute.before.
    • install adds the tools to the hook matcher, and doctor flags a hook whose matcher does not cover them.
  • when.tool. Scopes a gate to gated tools, alongside when.command.

Why

Closes #18. This is the reduced design agreed in that thread: a stateless phase gate evaluated live, plus tool gating. There is no salt and no hash chain. A phase marker can't be forged into passing, because every check re-runs the requirements against the workspace. Thanks @watanabefam for the proposal.

How I verified it

npm run build && npm run test:coverage   # Node 22: 163 pass, 0 fail; lines 93.5%, branches 80.9%, functions 97.6%
node dist/src/cli.js check                # dogfood policy passes
  • 12 new tests in test/phase.test.ts:
    • phase gate: cumulative live requirements, marker handling, run-once memo, validation
    • skillgate phase: moving between phases and the status output
    • gated tools: decideTool and when.tool, hook payloads (Claude, Gemini format, --tool)
    • opencode plugin: gated tools and the invalid-policy behaviour
    • install matchers and the doctor coverage check
  • Live smoke test: with gatedTools: ['Write'] and the matcher written by install claude-code (Bash|Write), a real Claude Code session's Write call was blocked with the unmet gate, and the file was never created.

Review notes

  • Base: master at 0.10.1.
  • Behaviour change: in the opencode plugin, an invalid policy still blocks shell commands but no longer blocks other tools, so the agent can edit the policy to fix it. Before this, non-bash tools skipped the policy entirely.
  • Cursor's hook covers shell commands only, so gatedTools does not apply there. The docs say so.
  • Changes are under ## Unreleased. Tag v0.11.0 after merge.

Checklist

  • Tests added (test/phase.test.ts)
  • npm run test:coverage passes locally
  • Documented in the README gate table, docs/spec-reference.md and schema/done.schema.json
  • CHANGELOG entry under ## Unreleased
  • Stays deterministic (no network, no model, no stored state)

- phase gate: ordered phases with requires; being in a phase needs its gates and
  every earlier phase's to pass now. The marker only names the phase; nothing is
  stored or signed.
- skillgate phase [<id>]: status, and move only when the target's gates pass.
- gatedTools: agent tools (MCP publish/deploy tools) cross the finish line like
  finishLine commands, in the gate CLI, the opencode plugin and hook installs.
- when.tool scopes a gate to gated tools.
@renezander030
renezander030 merged commit 80d6d58 into master Sep 25, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: phase-transition gates with hash-chained state tracking

1 participant