Step Gate — External Execution Ledger
An external cryptographic gate for agent task execution. It does not control how you
work — it only verifies that you did what you planned. Think of it as a
proof-of-work chain for agent steps: every completed step produces a cryptographic key,
and you cannot finalize a task without the final chain key.
Hierarchy
Every piece of work in Step Gate lives at exactly one of four levels:
Program ← 跨会话的完整项目(如"重构整个后端")
├─ Node ← 一个执行阶段(如"Phase 1: 提取中间件")
│ ├─ Task ← 一次 Agent 交互 = 一个 Task
│ │ ├─ Step ← 一个具体、可验证的动作(如"创建 auth.ts")
│ │ ├─ Step ← 每个 Step 配一条一次性密钥
│ │ └─ Step ← DAG 依赖决定执行顺序
│ └─ Task
└─ Node
- Program: 最高层,跨多个会话。Node 全部完成自动传播。
- Node: 一个执行阶段,包含多个 Task。依赖排序决定何时激活。
- Task: 一次交互。一个 Task 包含多个 Step。
- Step: 最小执行单元。一个 Step = 一个具体动作 + 一条密钥。
Step 粒度规则:
Steps 必须够细,让 Sub Agent 无法跳步:
| 坏 Step | 好 Step |
|---|---|
| "重构 auth 模块" | "提取 auth middleware 到 src/middleware/auth.ts" |
| "写测试" | "为 auth.ts 写 3 个单元测试" |
| "更新文档" | "更新 README 的 Auth 章节" |
每个 Step 必须是一个可验证的完成/未完成二元问题。如果一个 Step 包含多个独立动作,拆成多个 Step。
Why this exists
Long-context agents lose track of plans. A 15-step refactor becomes 12 steps in the
agent's memory by the time it reaches step 9. Context compression drops the original
plan. A Sub Agent claims "all done" when it skipped step 7.
The Gate solves this by moving the plan ledger outside the agent's context. The plan
lives in SQLite. Each step is locked behind a 6-character key. The key appears only once
in the checkpoint response — if the agent loses it, the step cannot be faked.
Hierarchy — MANDATORY
If Step Gate is enabled, you MUST use at least Task + Step level. Even the simplest
single-step job requires a Task wrapping one Step. There is no way to skip this.
Program ← 跨会话项目(多 Node 时使用)
└─ Node ← 一个执行阶段/波次
└─ Task ← 一次交互 = 一个 Task(MANDATORY)
└─ Step ← 一个具体动作 + 一条密钥(MANDATORY, 至少 1 个)
A Task without Steps is invalid. Every Task needs at least one Step. If your work
is just "run a command", that's one Step. If it's "refactor three files", that's
three Steps — each one independently checkpointable.
Core rule — single Task workflow
For simple work that fits in one interaction:
- Plan — break the work into concrete Steps with DAG dependencies
- Show the DAG to the user — present the full step plan before calling
start-plan - Get confirmation — user must explicitly approve the plan
- Register — call
start-planto lock the plan in the external ledger - Execute — checkpoint each Step as you complete it
- Finalize — submit the taskKey to close the Task
Plan → Show DAG → User confirms → start-plan → checkpoint × N → finalize(taskKey) → done
Core rule — multi-wave Program workflow
For work spanning multiple nodes/phases (cross-session or multi-agent):
program init → show DAG → user confirms
↓
┌─────────────────────────────────────────────────────┐
│ For EACH Wave (Node): │
│ program start <node> → activates tasks │
│ ↓ │
│ Main Agent dispatches Sub Agents │
│ (inject taskId + stepKeys per Weaver protocol) │
│ ↓ │
│ Sub Agent(s) → checkpoint × N → taskKey │
│ ↓ │
│ Main Agent → finalize(taskId, taskKey) │
│ ↓ │
│ When all tasks in node are done → node complete │
│ ↓ │
│ User manually starts next wave: │
│ program start <next-node> │
└─────────────────────────────────────────────────────┘
Each wave requires an explicit program start. Waves do NOT auto-unlock — this is
intentional. A single interaction should not cascade through all waves automatically
(the Stop Hook would never let the session end).
Proactive checkpointing
After completing each step, immediately call checkpoint with the step's key. Do NOT
batch checkpoints, do NOT wait for the Hook to remind you. The Hook is a safety net,
not your workflow. If you see ACTION REQUIRED, stop everything and resolve it.
CLI reference
All commands return JSON. The CLI binary is at node dist/cli.js from the project root.
Task commands
start-plan — Create a task for this interaction
node dist/cli.js start-plan '{"title":"What this task does","steps":[...]}'
Each step: id (optional), title (required), dependsOn (string array or omit for
auto-serial), children (nested container). First call auto-creates session files.
Returns taskId, currentSteps, and stepKeys.
checkpoint — Complete a step and unlock its dependents
node dist/cli.js checkpoint '{"taskId":"tsk_XXX","stepId":"tsk_XXX_yy","stepKey":"KEY"}'
The key is consumed on use — it cannot be reused. Returns nextSteps + nextStepKeys
for newly unlocked steps. When all steps are done, returns allStepsCompleted: true and
a taskKey.
current — Read current progress (does NOT return keys)
node dist/cli.js current '{"taskId":"tsk_XXX"}'
finalize — Complete the task and auto-propagate upward
node dist/cli.js finalize '{"taskId":"tsk_XXX","taskKey":"KEY"}'
Verifies the taskKey, marks the task completed, then automatically checks whether the
parent Node (if any) and Program are also complete. Returns a level field: task,
node, or program.
cancel-task — Cancel the current session's task
node dist/cli.js cancel-task '{"taskId":"tsk_XXX"}'
Session-gated — you can only cancel your own tasks. Cross-session cancel requires
--admin --recovery-token <token>.
active-task — List active tasks (cross-session by default)
node dist/cli.js active-task # all sessions (default)
node dist/cli.js active-task --mine # current session only
Program commands (cross-session projects)
program init — Register the full DAG in one command. Nodes may optionally include tasks+steps
for bulk registration. Ready nodes (no deps) have their first steps activated immediately.
node dist/cli.js program init '{
"title":"Phase 7 — Audit Fixes",
"nodes":[
{"id":"wave0","title":"Docs","tasks":[
{"id":"F0","title":"OpenSpec","steps":[
{"id":"S0.1","title":"proposal.md","dependsOn":[]},
{"id":"S0.2","title":"tasks.md","dependsOn":["S0.1"]}
]}
]},
{"id":"wave1","title":"Fixes","dependsOn":["wave0"],"tasks":[
{"id":"F1","title":"Fix auth","steps":[
{"id":"S1.1","title":"Edit","dependsOn":[]}
]}
]}
]
}'
Returns programId, nodes, tasks with taskId + currentSteps + stepKeys for ready tasks.
program start — Activate a node's pre-registered tasks. Returns task credentials for dispatch.
node dist/cli.js program start '{"programId":"pgm_XXX","nodeId":"pgm_XXX_wave1"}'
Returns tasks array with taskId, currentSteps, and stepKeys — ready for Sub Agent dispatch.
program status — Read program progress
node dist/cli.js program status '{"programId":"pgm_XXX"}'
Program finalization is automatic — when the last Task in the last Node is finalized,
the system propagates completion all the way up. No manual program finalize needed.
program rebuild — Rebuild node/program after plan changes (dry-run first, then --confirm)
node dist/cli.js program rebuild '{"programId":"pgm_XXX"}' # dry-run
node dist/cli.js program rebuild '{"programId":"pgm_XXX"}' --confirm
Always show the user the dry-run output and get confirmation before running --confirm.
Diagnostics
node dist/cli.js gate reconcile # full read-only health check
node dist/cli.js gate reconcile '{"programId":"pgm_XXX"}' # scoped to one program
DAG rules
Example — parallel branches + merge point:
node dist/cli.js start-plan '{
"title":"Backend refactor",
"steps":[
{"id":"auth","title":"Auth module","dependsOn":[]},
{"id":"api","title":"API layer","dependsOn":[]},
{"id":"db","title":"DB migration","dependsOn":["auth"]},
{"id":"test","title":"Integration tests","dependsOn":["api","db"]}
]
}'
# auth + api activate immediately; db waits for auth; test waits for api + db
| dependsOn | Behavior |
|---|---|
[] (explicit empty) |
Parallel entry — activated immediately |
| omitted / undefined | Auto-serial — depends on previous leaf |
["a", "b"] |
Merge point — unlocks after both a and b complete |
| Container with children | Children inherit the container's dependsOn |
skipKey + skipTaskId |
Skip a previously completed step (one-time use) |
Cycle detection runs at plan creation time — circular dependencies are rejected before
any step starts.
Interruption recovery
When a session is interrupted, completed steps are permanent cryptographic proofs:
# Rebuild with skipKey to jump past already-completed steps
node dist/cli.js start-plan '{
"title":"Resume wave 2",
"steps":[
{"id":"auth","title":"Auth module","dependsOn":[],"skipKey":"OLD_KEY","skipTaskId":"tsk_OLD"},
{"id":"ci","title":"CI tests","dependsOn":["auth"]}
]
}'
A skipKey can only be consumed once — the system writes a skip_key_consumed event on
first use and rejects subsequent attempts. Skipped steps are marked skipped (not
completed) to preserve traceability.
Key rules
- Keys appear exactly once — in the checkpoint or start-plan response. If lost, they
cannot be recovered. Thecurrentcommand never returns keys. - Step double-consumption is impossible — the DB transaction uses
WHERE status='current'
with an affected-rows guard. - Cancel-task is session-gated — agents cannot cancel tasks they don't own.
- SkipKey is one-time — the
eventstable records every consumption. - Cycle detection runs at plan creation — dead DAGs are rejected before execution.
The Gate is a proof-of-completion system, not a security product. It protects against
agent hallucination, context loss, and accidental step-skipping. It does not protect
against deliberate external attack.
Session files
The first start-plan call creates:
- .step-gate/sessions/ses_XXXXXX.json — session credentials
- .step-gate/bindings/bind_cli_XXXXXX.json — hook binding
The CLI auto-discovers the session from binding files. No manual session management needed.
Further reading
Weaver.md— Multi-agent orchestration: how a Main Agent spawns Sub Agents, injects
taskId + stepKey, and verifies returned taskKeys. Read this before orchestrating
parallel Sub Agents.