Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .abcd/development/brief/04-surfaces/01-ahoy.md
Original file line number Diff line number Diff line change
Expand Up @@ -659,15 +659,16 @@ carries the prompt-router entries it expects. Neither install nor uninstall ever
mutates it: the manifest is plugin-static. A missing or malformed manifest
surfaces as a non-resolvable diagnostic.

The shipped manifest wires nine event types, and every event command is a
resolving shim rather than a plain binary call. Five of them self-provision.
`UserPromptSubmit`, `PreToolUse`, `PostToolUse` and `PreCompact` each attempt
The shipped manifest wires ten event types, and every event command is a
resolving shim rather than a plain binary call. Six of them self-provision.
`UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PermissionRequest` and
`PreCompact` each attempt
`hooks/bootstrap.sh` only when the plugin-root binary is missing, recording the
try in a `.bootstrap.attempt` marker that throttles the next one to a ten-minute
window. `UserPromptSubmit`, the hook that runs on every message, declares a
120-second `timeout`, the time a salvage on a slow link has before the host
cancels it; `PreToolUse`, `PostToolUse` and `PreCompact` declare none and take
the host's ten-minute default, so a slow first download finishes there. `SessionStart`
cancels it; `PreToolUse`, `PostToolUse`, `PermissionRequest` and `PreCompact`
declare none and take the host's ten-minute default, so a slow first download finishes there. `SessionStart`
declares 240 seconds, the script's worst case with room to spare,
`SessionEnd`, `SubagentStop` and `StopFailure` declare none, and `Stop` declares
30 seconds, room for the away mode's stop check to make its one forge listing
Expand All @@ -682,7 +683,7 @@ the binary in place the script's own fast path costs a file test and does the
provisioning housekeeping that keeps the next plugin update served from the
local cache rather than the network, and it is the one place a binary that no
longer matches its provenance record is called out. It stamps the same marker,
so the four throttled events see a recent try, and reads no throttle of its
so the five throttled events see a recent try, and reads no throttle of its
own. `SessionEnd` and `SubagentStop` are the deliberate exceptions and download
nothing: both fire where the host cancels a slow hook rather than wait — one as
the session is going away, the other inside a live session as a sub-agent
Expand Down
27 changes: 27 additions & 0 deletions .abcd/development/brief/04-surfaces/34-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,33 @@ state with the session it was stored under; a run in progress is refused, since
the record's transcripts are the run's, captured at its end. A capture that
fails stops the call, with the transcripts before it recorded.

## Approval prompts while it runs

A run is unattended: nobody is at the terminal to answer the host's approval
prompt for a shell command, so a prompt would stall the run until someone came
back (iss-2610171110154902). The plugin's `PermissionRequest` hook, matched to
the shell tool, refuses the prompt while a build runs for the session's checkout or the session is one of a
running build's lanes. A build counts while its run is unfinished and its state
was written within the reservation lease, two hours: a run idle past it is one
the reservation already treats as abandoned, so a checkout holding old
unfinished runs does not count as running one. A run with a lane handed back
or held does not count either: it waits on the person, who is then at the
terminal taking the work over. So refusal stops when the run finishes, hands a
lane back or holds one, or after two hours with no activity; no verb ends a
run early, so a person who interrupts a build and carries on by hand sees the
prompts again two hours after the run last moved. The refusal tells the agent to
rewrite the command so it needs no approval, each step its own plain command
and a script's steps on the command line rather than in a file, a heredoc or a
`bash -c` string, or to skip the step and say why in its report. It is the
same every time the prompt comes back, and the hook never grants a prompt: an
approval given to a retry would let any command past every check by being sent
twice. A lane started through the claude runner runs with the host's hooks off
(`--bare`), so the hook never sees its prompts; the guard answers them as that
runner's permission prompt answerer. In every other session the hook says nothing and the host's prompt
shows as usual. A new run, and a pick as it starts one or begins a series,
says once on stderr that the prompts are refused while it runs; a
resume says nothing.

## Exit codes

`0` done, including a resumed start, a stage that re-tells an await, a call
Expand Down
11 changes: 11 additions & 0 deletions .abcd/development/brief/04-surfaces/35-drain.md
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,17 @@ so that a later drain takes issues a person would have decided. What guards it:
liveness the checkout cannot read: no release tag, a tag newer than the
checkout's own, or a value that is not a release tag.

## Approval prompts while it runs

A drain is unattended, as a build is, so while one has not stopped and its
state was written within the reservation lease, the host's approval prompts
for shell commands in its checkout and in the lanes the host runs are refused rather than shown,
on the terms [`34-build.md`](34-build.md#approval-prompts-while-it-runs) sets
out (iss-2610171110154902). A lane handed back does not end it, since the
drain routes the hand-back and goes on unattended; refusal stops when the
drain stops or after two hours with no activity. The move that begins a drain
says so once, on stderr.

## What it refuses

The dry run and the bare verb both refuse, exit 2 with nothing written, when
Expand Down
2 changes: 1 addition & 1 deletion .abcd/development/brief/04-surfaces/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ verb the binary registers apart from the framework's own `help`.
|---|---|---|
| `rules` | Renders the active rule set; a positional `DOMAIN` scopes to one. Read-only diagnostics over the hook-driven rule injection. | itd-3 (the modular rules loader); the loader it reports on is documented in [`05-internals/03-configuration.md`](../05-internals/03-configuration.md), and the verb in [The rules verb](#the-rules-verb) below |
| `spec` | The native spec store: bare invocation is a read-only status board, and `spec close` closes a spec and ships its linked intent (`planned/` → `shipped/`) only when no open spec is left naming it — an intent owns one or more specs, and `--remainder <slug>` mints the follow-on for a partial delivery in the same operation, carrying the closing spec's steps not marked landed. | itd-80 / spc-2 (intent lifecycle automation), adr-2609151513118583 (the 1:n relation); its lifecycle is documented in [`05-intent.md`](05-intent.md), and the verb in [The spec verb](#the-spec-verb) below |
| `hook` | Hidden from `--help`: six host hook entrypoints, live-wired from `hooks/hooks.json`. `prompt-router` injects the rules a prompt matches, and on the prompt that owes it sets the session's persona name as its title (itd-2610111707085098), and `prompt-router-reset` clears the per-session ledger so they inject again; `session-end` stages the session's own transcript, `subagent-stop` stages a finished sub-agent's transcript with its lineage, and `session-start` files both away, says how many reports wait in the inbox ([`29-report.md`](29-report.md)) and reserves the session's persona name in the machine register ([`08-abcd.md`](08-abcd.md)); `question-answered`, on the question tool's `PostToolUse`, resets the presence badge once a question's answer comes back ([`08-abcd.md`](08-abcd.md)). The pre-tool-use adapter is `guard hook`, under `guard`. | itd-3 (the prompt router), itd-89 / spc-4 (the transcript clock), itd-103 / spc-16 (the guard hook); the transcript entrypoints are documented in [`11-history.md`](11-history.md), and the rule injection the router drives, with `prompt-router`'s two outputs — the injected block and the `--json` envelope that names the active-domain set for a client that snapshots rules — in [`05-internals/03-configuration.md`](../05-internals/03-configuration.md#the-prompt-routers-output); the generated CLI reference omits both router entrypoints by design |
| `hook` | Hidden from `--help`: seven host hook entrypoints, live-wired from `hooks/hooks.json`. `prompt-router` injects the rules a prompt matches, and on the prompt that owes it sets the session's persona name as its title (itd-2610111707085098), and `prompt-router-reset` clears the per-session ledger so they inject again; `session-end` stages the session's own transcript, `subagent-stop` stages a finished sub-agent's transcript with its lineage, and `session-start` files both away, says how many reports wait in the inbox ([`29-report.md`](29-report.md)) and reserves the session's persona name in the machine register ([`08-abcd.md`](08-abcd.md)); `question-answered`, on the question tool's `PostToolUse`, resets the presence badge once a question's answer comes back ([`08-abcd.md`](08-abcd.md)); `permission-request`, on the shell tool's `PermissionRequest`, refuses the host's approval prompt while a build or drain runs with nobody at the terminal ([`34-build.md`](34-build.md#approval-prompts-while-it-runs)). The pre-tool-use adapter is `guard hook`, under `guard`. | itd-3 (the prompt router), itd-89 / spc-4 (the transcript clock), itd-103 / spc-16 (the guard hook); the transcript entrypoints are documented in [`11-history.md`](11-history.md), and the rule injection the router drives, with `prompt-router`'s two outputs — the injected block and the `--json` envelope that names the active-domain set for a client that snapshots rules — in [`05-internals/03-configuration.md`](../05-internals/03-configuration.md#the-prompt-routers-output); the generated CLI reference omits both router entrypoints by design |
| `completion` | The CLI framework's generated per-shell autocompletion scripts. | No record: generated by the CLI framework, not designed here |
| `statusline` | The harness-invoked status-line render: the harness runs it on every refresh with its payload on stdin, and it prints abcd's row in a managed repository or runs the user's recorded previous status command everywhere else. `ahoy install` wires it; no user invokes it. | itd-200 / spc-70 (the presence badge); the row and the state behind it are documented in [`08-abcd.md`](08-abcd.md) |

Expand Down
19 changes: 15 additions & 4 deletions .abcd/development/brief/05-internals/03-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1311,16 +1311,17 @@ warns so, naming the check to run by hand. Its build strings are printed
instructions to the operator for the cases it cannot provision. `SessionStart` runs one chained command rather than siblings,
because siblings would run in parallel and share one stdin.

`SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse` and `PreCompact`
are the events that reach `bootstrap.sh` at all. The last four self-provision
`SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`,
`PermissionRequest` and `PreCompact` are the events that reach `bootstrap.sh` at
all. The last five self-provision
only when the
plugin-root binary is missing, throttled by a `.bootstrap.attempt` marker within a
ten-minute window, and then fall back to a PATH-resolved abcd that must be
absolute, outside the working directory, in a directory and a file that are not
world-writable, and recorded as this
machine's own, before failing loudly. `UserPromptSubmit`, which runs on every
message, declares a 120-second `timeout`; `PreToolUse`, `PostToolUse` and
`PreCompact` declare none and take the host's ten-minute default; `SessionStart` declares 240 seconds,
message, declares a 120-second `timeout`; `PreToolUse`, `PostToolUse`,
`PermissionRequest` and `PreCompact` declare none and take the host's ten-minute default; `SessionStart` declares 240 seconds,
and the transcript hooks declare none. Every event that runs `bootstrap.sh`
names a `statusMessage`, the host's spinner text while the hook runs, because the
salvage sends the script's output nowhere; the text states no duration, since
Expand Down Expand Up @@ -1349,6 +1350,16 @@ supply. `SessionStart` drains both through the fail-closed redaction path.
question-answered`, which resets the mode to managed when an answer comes back
to a question the guard marked open (iss-2610100626211810); the prompt hook
runs the same reset on the next message, for a host that does not run it.
`PermissionRequest` is matched to the shell tool alone and carries `hook
permission-request`, which refuses the host's approval prompt while a build or
drain runs for the session's checkout or the session is one of a running
build's lanes (iss-2610171110154902): the agent is told to rewrite the command
into plain steps abcd can check, or skip the step and say why. A build counts
while its run is unfinished, no lane of it is handed back or held, and its
state was written within the reservation lease; a drain while it has not
stopped and was written within the same window.
It never grants a prompt, and in every other session it says nothing, so the
host's prompt shows as usual.

**The plugin-internal development namespace** (committed in private repos,
gitignored in public) holds, at its root, the config file and the per-surface
Expand Down
5 changes: 5 additions & 0 deletions .abcd/development/release/surface.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
schema_version: 1
id: "iss-2610171200535090"
slug: "a-person-cannot-switch-a-session-to"
severity: "minor"
category: "future-work-seed"
source: "user-observation"
found_during: "the approval-prompt ruling of 2026-10-17"
origin: researcher-authored
production_mode: hand-written
found_at: "commands/mode.md"
remedy: "Add a person-run switch (abcd mode --autonomous or a sibling verb) that turns approval-prompt refusal on for a checkout outside a build or drain, and off again, including off during a build the person interrupted to carry on by hand (today refusal then lasts until two hours after the run last moved, since no verb ends a run early); announced once when set, and shown on the status line while on."
---

A person cannot mark a hand-steered session as autonomous. The approval-prompt refusal turns on only while abcd's own build or drain runs, so a session the person tells to work alone and leaves still stalls on the host's approval prompts. The product thinker named a switch on the mode verb as the likely shape.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
schema_version: 1
id: "iss-2610171200549757"
slug: "a-sandboxed-run-could-run-what-the-guard"
severity: "minor"
category: "future-work-seed"
source: "user-observation"
found_during: "the approval-prompt ruling of 2026-10-17"
origin: researcher-authored
production_mode: hand-written
found_at: "internal/core/guard/script.go"
remedy: "Once autonomous agents run in a sandbox, let a command the guard cannot fully read run inside it instead of being refused, since the sandbox bounds what it can touch; until then extend what the guard reads (other interpreters' files) so fewer commands need the refusal."
---

A command the guard cannot read is refused outright in an autonomous run, even where a sandbox would make running it safe. The guard already reads shell scripts a command runs, but not other interpreters' files or file text substituted into a command; the sandbox environment under way for autonomous agents would bound what such a command can touch.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
schema_version: 1
id: "iss-2610222047465878"
slug: "loop-samepath-is-case-sensitive-on-a"
severity: "minor"
category: "bug"
source: "impl-review"
found_during: "review pair of fix/autonomous-permission-ask-denied, 2026-10-22"
origin: researcher-authored
production_mode: hand-written
found_at: "internal/core/implement/loop/loop.go"
remedy: "Compare with os.SameFile on os.Stat results when both paths exist, falling back to the resolved-string comparison when either does not, as ahoy's samePath does; consider making that one helper in fsutil and sweeping the loop, ahoy and embargo call sites onto it, keeping embargo's refusal on a mismatch"
---

The implement loop's samePath (internal/core/implement/loop/loop.go:1024) decides whether two paths name one directory by comparing their cleaned, symlink-resolved strings, so on a case-insensitive file system (macOS APFS by default) two spellings of one directory that differ only in case compare unequal. Its callers in the loop are unattended.go:64 and :75 (is this session a running build's lane), judgement.go:327, ratelimit.go:155, configwatch.go:273 and schedule.go:115. For the permission-prompt refusal the failure is open: an unattended lane whose host reports its directory in a different case is not recognised, and the host's prompt shows and waits, which is the behaviour before the refusal existed. Paths there normally come from git and the host's own working directory, so a mismatch is unlikely. ahoy has its own samePath that compares os.Stat results with os.SameFile, which handles case and links together; embargo's compares resolved strings and refuses on a mismatch, failing closed. Found during the review of the permission-prompt refusal, deferred from that change so as not to alter a helper six other loop call sites share.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
schema_version: 1
id: "iss-2610171110154902"
slug: "a-host-approval-prompt-for-a-shell"
severity: "major"
category: "bug"
source: "user-observation"
found_during: "autonomous runs, week of 2026-10-17"
origin: researcher-authored
production_mode: hand-written
found_at: "hooks/hooks.json"
remedy: "Wire the host's PermissionRequest event for Bash to abcd: while a build or drain is running in the checkout (unfinished, active within the reservation lease) or the session is one of its lanes, deny every approval prompt with a message telling the agent to rewrite the command into plain steps the guard can read, or skip the step and record why; never grant; leave interactive sessions' prompts as they are; build and drain say once at start that refusals are on."
resolution: "abcd now refuses the host's approval prompt for a shell command while a build or drain runs for the checkout or one of its lanes, telling the agent to rewrite the command into plain checkable steps or skip it; it never grants one, and build and drain say so once as they start"
impact: additive
---

A host approval prompt for a shell command stalls an autonomous run. The host asks the person to approve a command it cannot check (an unread script, a compound line), and with nobody at the terminal the run waits until someone returns. Auto-approving a repeat was considered and rejected: it lets any agent past every check by retrying, so the refusal is repeated every time instead and the agent is told how to rewrite the command.
21 changes: 21 additions & 0 deletions commands/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,27 @@ check, and reports `resumed: true` with the same run and an empty `checks`: the
run's own lanes move and claim the intent, so judging it again would refuse the
run as its own peer.

**Approval prompts are refused while the run runs.** A new run says so once,
in one line on stderr (stdout's JSON is untouched; a resume says nothing).
While the run is unfinished and its state was written within the last two
hours (the reservation lease), the host's approval prompt for a shell command
in this checkout or in a lane the host runs is refused, not shown, because nobody is
at the terminal to answer it: the agent is told to rewrite the command so it
needs no approval, each step its own plain command and a script's steps on
the command line rather than in a file, a heredoc or a `bash -c` string, or to
skip the step and say why in its report. Sent again unchanged, it is refused
again; this refusal never turns into an approval. A lane abcd starts through
the claude runner runs with the host's hooks off, so its prompts never reach
this refusal: the guard answers them (`abcd guard approve`, below). In a
session with no run, the prompt
shows as usual. `build next` says the same once, as it starts a run or begins
a series.

Refusal stops when the run finishes, when it hands a lane back or holds one
(the work is then the person's), or after two hours with no activity in the
run. No verb ends a run early, so a person who interrupts a build and carries
on by hand sees the prompts again two hours after the run last moved.

## Pick the next intent

When the argument is `next`, let the run choose the intent:
Expand Down
Loading
Loading