fix: an unattended driver must not mint the human-presence token (2.6.6) - #766
fix: an unattended driver must not mint the human-presence token (2.6.6)#766warren830 wants to merge 1 commit into
Conversation
The `UserPromptSubmit` mint hook records a `HUMAN_TURN` — the token the approval
and interview gates spend — but it has no evidence for the assertion it is
making. The event carries no signal about who submitted, and the hook reads no
stdin ("Presence-only: the prompt text is irrelevant"). That is sound while
every prompt comes from a person, and unsound the moment something drives
`/aidlc` on a schedule: an overnight runner, a CI job, a cron. Each cycle mints
a fresh, spendable turn, so "walking away" stops meaning "no new human turn" —
which is the premise awslabs#427's fix was verified against.
Measured on a real unattended run: 10 runner-submitted prompts, zero humans, and
`humanActedSinceGate()` answered true. The new t188 case proves the consequence
rather than assuming it — without this change a gate APPROVES on a turn minted
only by an unattended prompt.
`AIDLC_UNATTENDED=1` now suppresses the mint, on both of the hook's paths (typed
prompt and answered AskUserQuestion widget). This is the doctrine the engine
already applies elsewhere: an unattended autonomous Construction run "has no
human at the gate", which is why aidlc-utility refuses scope changes and plan
re-shapes under it and aidlc-state refuses park. This closes the one path where
an unattended turn still manufactured a human.
Fail direction is deliberate: the flag can only WITHHOLD authority. A leak into
an interactive shell refuses your approvals until you unset it — annoying, and
safe. The inverse mistake cannot be undone, because the ledger is append-only.
The `.aidlc-human-turn` marker is deliberately still written. It is not an
authority signal, and suppressing it would change the Stop hook's conversational
carve-out, which is separate behaviour with its own tests. Flagged in the code as
a reviewable choice rather than made silently.
Nothing changes for an interactive session: with the flag unset the mint is
byte-for-byte what it was.
|
Warren, this PR addresses a real gap in the human-presence guard: a scheduled driver can submit the same prompt event as a person and thereby mint the STAMP: tests/logs/2026-08-14T18-07-21Z-p2 Part A - Direction: sound-but-misplaced The problem exists on current Part B - Review: request changes
Non-blocking release note: open PR #749 currently also declares 2.6.6. The version, README badge, and CHANGELOG heading are internally synchronized here, but whichever PR merges second needs the documented rebase/re-bump. Part C - UX impact This adds one operator environment setting and no new command, stage, or default. With the flag honored, unattended prompt cycles no longer grant approval/interview authority and the workflow waits for a person; interactive sessions remain unchanged. The marker used by the Stop-hook conversational carve-out is intentionally unchanged. The submitted code currently delivers that UX only on Claude, Cursor, and opencode, and a leaked flag produces an unexplained retry loop, so the release behavior is not yet consistent or diagnosable. Verification: focused t188 plus t68 passed 31/31; package drift, coverage registry, and static checks also passed. No e2e or live-service variables were needed. |
apackeer
left a comment
There was a problem hiding this comment.
Requesting changes for the cross-harness, diagnostic, and documentation gaps described in my review comment.
Summary
The human-presence gate can be satisfied by something that is not a person.
hooks/aidlc-record-human-turn.tsrecords aHUMAN_TURNonUserPromptSubmit— the evidencehumanActedSinceGate()spends to authorize an approval or an interview answer. But the hook has no evidence for the assertion it is making: the event carries no signal about who submitted, and the hook reads no stdin ("Presence-only: the prompt text is irrelevant"). Its only guard is "does a state file exist".That is sound while every prompt comes from a person. It stops being sound the moment something drives
/aidlcon a schedule — an overnight runner, a CI job, a cron. Each cycle submits a prompt, so each cycle mints fresh, spendable presence.This is not a regression of #427 — it is the premise #427's fix was verified against. That verification reads "At every approval gate the agent paused and ended its turn; walking away (no input) did not auto-approve", which holds precisely because no prompt means no presence. An unattended driver submits prompts, so "walking away" no longer implies "no new human turn".
Evidence
Measured on a real unattended run against a stock v2 install (10 detached cycles, no human involved at any point):
The new t188 case then proves the consequence rather than assuming it. Reverting only the hook change turns it red with
Expected: not 0— i.e. without this change the gate approves on a turn minted solely by an unattended prompt. The remaining barrier today is the conductor's prose ("NEVER self-answer questions"), not a mechanism.Changes
AIDLC_UNATTENDED=1suppresses the mint. One env check in one hook.AskUserQuestionwidget — since an unattended driver should mint from neither.aidlc-utility.ts), which is why it already refuses scope changes and plan re-shapes, and whyaidlc-state.ts's autonomy guard refusespark. This closes the one path where an unattended turn still manufactured a human.One choice I am flagging rather than making silently
The
.aidlc-human-turnmarker is still written. It is not an authority signal, and suppressing it would change the Stop hook's conversational carve-out — separate behaviour, with its own tests, that I have not exercised here. It is a one-line follow-on if you want it; I would rather you decide than have it ride along.Two further design calls that are yours, not mine:
RUNNER_TURNthathumanActedSinceGatedoes not accept, which keeps the trail richer at the cost of a newVALID_EVENT_TYPESmember plus the state-machine chapter tables under the same-commit rule.SESSION_STARTEDalready records that a session happened, so I took the smaller surface.AIDLC_UNATTENDEDreuses the word the codebase already uses for this condition. It is the symmetric half of the existingAIDLC_SKIP_HUMAN_PRESENCE_GUARD(which turns the guard off); what was missing was a way to say "this prompt is not a person's".User experience
An operator running AI-DLC unattended sets
AIDLC_UNATTENDED=1in the driving process, and gates then refuse exactly as they do when nobody has acted — the workflow parks and waits for a real human instead of being able to walk past its own approval. Interactive users see no change.Related: #530 ("a first unattended-gate story") is where this belongs as a design thread; I have posted the finding there.
Checklist
docs/reference/06-hooks-and-tools.mdhook table, same commit per the Documentation Policy)Test Plan
Two cases added to
tests/unit/t188-human-presence-gate.test.ts, whosecovers:header already carriesfile:hooks/aidlc-record-human-turn.ts, so no coverage-registry change. Both spawn the real hook as a process (the env read is the contract under test) withAIDLC_PROJECT_DIRpointed at the fixture:approveCLI.Red-green proven: stash the hook change, regenerate
dist/, and both fail — the second withExpected: not 0, i.e. the gate approved. Restore, and 24/24 pass.Verified locally (bun 1.3.11; CI pins 1.3.14):
The first three exit 0. The suite result matches the baseline on this branch point: the same 2 files fail (
t248-codekb-scope-diff,t255-workspace-sync), both unrelated to presence.t255's failures are ~5s timeouts on live git remote queries and its count moves with load — 16 when the file runs alone here, 7-8 under--parallel 8— so it is environmental.t188is 24/24.Version bumped to 2.6.6: 2.6.3 is claimed by #754 and #758, 2.6.4 by #759, 2.6.5 by #760.
Acknowledgment
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.