Documentation menu

Documentation

Human gates

Where a person approves, rejects or redirects the work.

On this page

The stage that runs nothing

A stage whose kind is HumanGate stops automation. The scheduler moves the task into it and ends the run as awaiting human — no agent is dispatched, no worktree is touched, nothing advances until a person decides. In the default pipeline that stage is called Loop, and it sits between Review and PR Open.

Its prompt states the contract the previous stage has to satisfy:

text
This is the human Loop gate. Stop autonomous execution and present the
deliverable, verification evidence, Review summary, risks, and clear testing
instructions. Wait for the developer to test, approve, or request changes.
Human feedback must be added to task context and sends the task back to Build.
Human approval advances the task to PR Open.

You can have more than one. Any stage you create with kind HumanGate behaves the same way, wherever it sits in the list. The GStack preset ends on one (its QA Gate); the govctl preset ends on one (its Approve stage).

Deciding in the desktop

Right-click a card — or press Space with it focused — for the action menu:

text
≣ Open detail | ± Git Diff | → Advance | ✓ Approve | ↩ Send back | ✕ Delete
  • ✓ Approve closes the task, then releases any dependent tasks it was blocking — each gets a wave: unblocked by #N — auto-started comment and starts running.
  • ↩ Send back moves the task to the previous pipeline stage.
  • → Advance moves it forward one stage without closing it.

The task detail screen carries the same decision in its Status picker, whose last option is ✓ Approve & close. That one asks for confirmation first — Closing a task is irreversible. Continue?

Deciding from a shell

bash
tde task approve 41 --response "Verified locally against the staging DB"
tde task reject  41 --reason  "Migration is missing a down step"

approve requires the task to actually be at a human-gate stage, otherwise it returns Task is not awaiting approval; an already-closed task returns Task is already approved.

reject moves the task to the stage immediately before its current one, clears the blocked flag, resets the run state to idle, and writes a human comment reading Changes requested: <reason>. Omit --reason and it records Changes requested.

Both verbs are durable operations: replaying the same decision returns the same result rather than acting twice, and a conflicting second decision is refused with a message like This human gate already has a durable approval decision.

See tde task for the full grammar.

Deciding from your phone

Two different Telegram flows reach a decision, and they are not the same thing.

Parked questions. When an agent emits an ASK: block, TDE mirrors it to Telegram with one inline button per real option — the button labels are the question's own option labels, never guessed from prose. Tapping one records the answer, unparks the task and re-runs the stage with your answer in context. Answering twice reports Already handled.

Guard approvals. When a guarded action needs a decision, the message reads:

text
🔐 Approval needed
run: rm -rf ./build
Guard rule: rm -rf (recursive delete)

with two buttons, ✅ Approve and ❌ Deny. The desktop shows the same modal; whichever surface answers first wins, and the other reports Already handled.

The bot also answers three commands:

text
/tasks — list current board tasks by stage
/status — task counts per stage
/help — this message

Anything else you send is treated as a new task. See Slack and Telegram.

Parked questions in detail

An agent that hits a genuine decision — a real choice with real consequences, not a guess — is instructed to stop and ask rather than pick. The block it emits looks like this:

agent output
ASK: Which database should this service use?
HEADER: Database choice
MULTI
OPTION: Postgres :: managed, strong consistency
OPTION: SQLite :: zero-ops, single node
INPUT: or name another
SECRET

MULTI allows more than one selection, INPUT allows a typed free-text answer, and SECRET masks that typed answer. With no options at all, free text is implied.

TDE parks the task, stores the question, logs awaiting human input: … and raises an Activities event titled <task> needs your input. In the Activities detail, the footer buttons are the question's real options, followed by Submit for a multi-select and ✏ Type answer when free text is allowed.

Answering writes a comment — Human answer to: <question> plus your selection and input — clears the parked question and re-runs the stage. If a newer question arrived in the meantime, the older answer is refused with A newer question is waiting. Review it before answering.

The CLI can answer the same question: tde task approve ID --response TEXT matches your text against the option ids and labels, case-insensitively, and falls back to free text only when the question allows it.

Guards

Guards are the policy layer around risky actions, configured in Control → Guards. Each guard is a toggleable rule that matches either a shell command the agent wants to run or a file path it wants to write.

Destructive rules ship enabled; the rest are opt-in:

RuleCategoryDefault
rm -rf / (wipe root)Filesystemon
rm -rf ~ (wipe home)Filesystemon
mkfs (format filesystem)Filesystemon
dd if=… (raw disk write)Filesystemon
read SSH private keysSecrets & credentialson
write into ~/.sshSecrets & credentialson
rm -rf (recursive delete)Filesystemoff
sudo, chmod 777Privilege & permissionsoff
curl|wget piped to a shell, wgetNetworkoff
write a .env fileSecrets & credentialsoff
git push --force, git reset --hard, git clean -fdVersion controloff
npm publish, cargo publishPackage publishingoff

The same panel carries Human gate: (on by default), Max diff: (off, 200, 500 or 800 lines) and Smart effort:. You can add your own rules in label | command form. Everything persists to ~/.tde/pipeline.json, and newly shipped built-ins are merged in without clobbering your toggles.

A guard violation found at the Review stage fails the verdict — the task goes back to Build with a reason like blocked command (git push --force) used, blocked write (write into ~/.ssh) to … or diff too large: 940 lines > 800 limit.

Why an agent cannot approve its own work

This is enforced structurally, in three independent places.

The tool surface has no approve verb. The Control MCP server exposes exactly runtime_status, task_create, task_list, task_show, task_run, task_watch, task_logs, task_cancel, task_retry, task_block, task_unblock, task_comment, project_list, pipeline_list, agent_list and share_status. There is no task_approve and no task_reject. Approval exists only as a command on the deterministic CLI over the owner-only socket, and in the UI.

The agent is told so. The chat agent's system prompt states that approval, rejection and share lifecycle are human-only actions and that it must ask you to use the CLI or the UI — and that it must never claim an action succeeded unless a tool actually returned success.

Request-but-not-grant flows exist where they are needed. An agent can decision_propose a decision record, which is never auto-accepted, and decision_request_approval, which only links that proposal to a task so the human Loop gates it. There is no decision_accept tool; accepting is a click in the Decisions panel. The Slack plan gate works the same way: only the explicit approve option unlocks it, and any other answer leaves the task blocked rather than resuming a state-changing request.