Development record
a-person-hands-an-item-from-their
Summary
This spec delivers the first rung of
itd-2610120738075547:
handing one item that is ready to build to a hosted cloud session, and getting
its work and its report back, as scripts under
script-first-mvp, on abcd's own
repository. The Go verbs (dispatch, collect) are a later rung that absorbs
the contract these scripts prove. Its trust rule is
adr-2610120937435856
and the brief's invariant 23.
Planned on 2026-10-12 by the technical facilitator on the product thinker's delegation ("decide the deferred item so that you can plan the intent. I want it ready to be build in a few days (while I'm away)"). A build session builds it without the person as far as the steps marked build-alone reach; the steps marked needs the person are acts only the person can perform (a hosted session in their claude.ai account, a forge setting only an admin can change, a public issue).
What a person does once it is built:
scripts/cloud/send.sh <item-id>on their own computer: abcd checks the item is ready and public, composes the brief and starts one hosted session through the host connector, and prints the session's id and link.- The session runs the setup (a non-root user, gitleaks, the toolchain, the
committed hooks), checks its machine, reserves the item, works, and hands
back: its commits and a report file pushed to
claude/abcd-wip/<item-id>, or, if the push fails, the report as a comment on the tracking issue. scripts/cloud/list.shshows each session's item, branch, state and report.scripts/cloud/collect.sh <item-id>re-runs preflight on a copy on the person's computer and, if it passes, offers a landing branch with a local receipt; nothing is pushed for them.
| Criterion (intent order) | Met by |
|---|---|
| C1, the machine is prepared, nothing runs as root | Steps 1 (the hypothesis), 4, 12 (proved live) |
| C2, the machine is checked before work, stops only for what the final checks need | Steps 2 (the gate-tool list), 3 |
| C3, a brief started in the wrong place stops | Step 3 |
| C4, one brief per item | Step 5 |
| C5, no quiet fallback, an expired login stops the run | Step 9 |
| C6, every session reports, on its branch | Step 8 |
| C7, finished work is not trapped and never reaches the default branch | Steps 6 (the hook entry), 7 (the required check and ruleset) |
| C8, one list of cloud sessions | Step 10 |
| C9, handed-back work lands through the person's own checks | Step 11 |
| C10, abcd starts the session | Steps 9, 12 (proved live) |
| C11, the cloud session runs the full abcd toolchain | Steps 4, 12 (proved live) |
| C12, everything host-specific sits behind the connector | Step 2 (the search test), held by every later step |
| C13, nothing private goes to the cloud | Step 5 |
Footprint
- packages: scripts/cloud, scripts/cloud/connectors/claude, .claude/settings.json, scripts/gate-tools.tsv, scripts/check-wip-source.sh, scripts/check-wip-source-cases.sh, .githooks/pre-push, .github/workflows/ci.yml, .abcd/work/rulesets/main-protection.json, internal/core/lint, internal/cloudscripts, internal/surface/cli, internal/gittest
- tests: per step below. Every script is exercised in a throwaway clone with a bare repository as its remote, as
internal/surface/cli/githook_prepush_test.godoes for the hook, and every host or forge call against a fakeclaudeorghon PATH that records its argv and serves fixture output. No test needs the network, a hosted session, root or gitleaks.
Design
Layout and the host connector
scripts/cloud/
cloud.conf # host-agnostic: tracking issue number, report dir
connector.sh # loads and validates connector declarations
begin.sh # the brief's first step: place check, env check
env-check.sh # C2
brief.sh, brief.md.tmpl # C4, C13
send.sh # C10, C5
hand-back.sh # C6, the session's last step
list.sh # C8
collect.sh # C9
connectors/claude/ # everything that names the first host
connector.conf # wip_prefix=claude/abcd-wip/ ; host markers
is-remote.sh # exit 0 only when CLAUDE_CODE_REMOTE=true
setup.sh # C1, run by the session-start hook
launch.sh # starts a hosted session, maps its failures
.claude/settings.json # the connector's, at the host's fixed path
A connector is a directory under scripts/cloud/connectors/ holding a
connector.conf of key=value lines, read (never sourced) by
connector.sh. Keys: name, wip_prefix, is_remote (a script path inside
the connector directory), setup, launch. connector.sh refuses a
declaration whose wip_prefix does not match ^([a-z0-9][a-z0-9-]*/)?abcd-wip/$
(the ADR's decision 2: it ends in the segment abcd-wip/ and holds at most one
segment before it), an unknown key, a script path that leaves the connector
directory, or two connectors declaring one prefix. The first rung has one
connector; send.sh takes --connector <name> and defaults to the only one
declared, refusing to guess when there are two.
Everything outside scripts/cloud/connectors/claude/ and .claude/settings.json
is host-agnostic (C12): the hook and the required check match only the
declared prefixes and the segment abcd-wip; the brief template takes the
branch name and the remote test from the connector.
The machine (C1, C11)
.claude/settings.json (committed) does three things and no more:
- enables the abcd plugin from this repository's own marketplace (the
extraKnownMarketplacesandenabledPluginskeys; the build session confirms the exact key names against the host's settings reference before writing them, and names the page it read in the commit); - switches off the host's AI co-author line, its pull-request footer and its
session link (
attribution.commit,attribution.pr,attribution.sessionUrl, same confirmation); - runs
scripts/cloud/connectors/claude/setup.shfrom aSessionStarthook. The script's first line exits 0 doing nothing unlessis-remote.shpasses, so on the person's computer the hook does nothing (C11).
The hook calls a script, never abcd, so adr-2610050711287503 ("hooks live only in the plugin", which governs the person's own configuration folder) is not engaged: this file is the project's, committed and reviewed, and the product thinker placed the setup there (the intent's decision 10). The plugin cannot carry the setup, because the marketplace serves the last release.
setup.sh, as root, idempotent:
- Create a normal user
abcd(no sudo), give it the checkout (chown -R), and refuse to go on if the user cannot be created; it says so and exits 1 (the scope condition is then falsified, see step 1). Then trust the checkout for root in git's system configuration (git config --system --add safe.directory <checkout>, once): from git 2.35.2 git refuses a repository another user owns, the host's own git commands included. - Install gitleaks at the version
.github/workflows/ci.ymlpins (GITLEAKS_VERSION, read from the file, so there is one pin), checksum verified exactly as the CI job does. - As
abcd: resolve the declared toolchain throughscripts/pinned-toolchain.sh(sogo run ./cmd/abcdworks) and setcore.hooksPath=.githooksin the checkout (the probe of 2026-10-15 found it unset in a hosted clone, so the committed hooks never ran there). Then build abcd once into.abcd/.work.local/cloud/bin/abcdand hand its path to the session's later shells through the host's file of variables (CLAUDE_ENV_FILE, which aSessionStarthook may appendexportlines to and every later Bash command runs first) asABCD_OUTBOUND_BIN(the attribution gate, so a work-in-progress push does not build abcd once per commit inside the push) andABCD_BIN(the cloud scripts). - Set the checkout's local author identity from the environment's forge
token (the intent's decisions 17 and 18): the token's login as
user.nameand<id>+<login>@users.noreply.github.comasuser.email, through the one token-identity primitive the session announcements use (spc-2610150728396006's holder fallback, reached as ago run ./cmd/abcdverb), never a second lookup in the script. When the lookup fails it sets nothing and prints one line saying the identity could not be taken from the token; no output, commit, ref or trailer ever repeats the identity the container came with. - Print one line per prerequisite put in place. Every command after setup runs as
abcd(su abcd -c), andbegin.shandenv-check.shrefuse to run as uid 0. Preflight runs in the background asabcd, so the host's per-command time limit does not cut it short.
Decision 15 of the intent, in the design: the plugin the settings file
enables supplies the hooks and command pages of the last release; every abcd
verb in a cloud session on this repository runs as go run ./cmd/abcd from the
checkout, which AGENTS.md already requires of a source checkout, and the brief
says so in its rules. A managed repository (a later rung) uses the released
binary.
The environment check (C2)
scripts/gate-tools.tsv lists, per line, a tool, the preflight gates that need
it (comma-separated, by their Makefile target names) and the setup step that
supplies it; a tool no preflight gate needs carries - for its gates. It lives
beside the gates' own scripts; a test holds it to the Makefile's preflight:
prerequisites (every gate named is a real target), so it cannot drift silently
(the cost decision 5 accepted, reduced).
env-check.sh reads it, tests each tool with command -v (and the toolchain
by scripts/pinned-toolchain.sh --check or the equivalent the build session
finds), and prints one line per missing tool naming it, its setup step and the
gates that need it. Exit 1 when a missing tool is needed by any preflight gate
(the session stops and hands back a report saying why, C6); exit 0 with a
warning line otherwise. It writes nothing anywhere (a test compares
git status --porcelain and the tree's mtimes before and after).
The brief (C3, C4, C13)
brief.sh <item-id> (run on the person's computer by send.sh, usable alone
to preview) refuses, writing nothing, unless:
- the item is ready: an open issue record under
.abcd/work/issues/open/, or a planned intent for whichabcd intent ready <id> --jsonexits 0 (decision 7); - the item is public: after
git fetch origin, its record path exists in the remote default branch's tree (git cat-file -e origin/<default>:<path>); a draft never pushed, a record only on a local branch, and a private record in the embargo store all fail this (C13, "not yet public"). The record here must also be the same blob as origin's, since readiness is judged on this copy while the session reads origin's: a record with changes not on origin's default branch is refused until they are pushed. An intent's readiness gate reads more than its record (the spec store, and the linked spec's body and steps), so for an intent no path under.abcd/development/may differ between this working tree, untracked files included, and origin's default branch; - the item is not under embargo: when the embargo store
(
~/.abcd.noindex/worktrees/<root-sha>/embargo/) exists on this machine, the composed brief and the item's record are passed throughabcd embargo check --file <brief> --file <record>(the brief carries only the item's id and path; the record's text may name an advisory's title), and a store that exists while the verb is unknown or cannot run is a refusal, never a pass (C13, "held under embargo"; fail closed). With no store, nothing is under embargo on this machine.
The brief, from brief.md.tmpl, names: the item and its record path; the
first step, scripts/cloud/begin.sh; the rule that every commit is authored
under the identity the setup took from the forge token (no author is read
from the person's computer, decision 17); the rules (no hook is ever
skipped, every abcd verb is go run ./cmd/abcd, every commit carries
Assisted-by: and no co-author trailer, reserve the item with abcd reserve
once it is on the default branch); the branch, the connector's wip_prefix
followed by the item id; and the hand-back, scripts/cloud/hand-back.sh with
the report fields. Before it is printed or sent, the brief passes abcd lint outbound --label brief, and a check that it holds no $HOME path and no
session link (C4).
The scripts that read abcd's answers (brief.sh, hand-back.sh, list.sh)
branch on its exit codes, and go run turns every non-zero exit into 1, so
each runs a binary built from this checkout once per run, or the one
ABCD_BIN names, through scripts/cloud/abcd-bin.sh; never go run.
ABCD_BIN is honoured only when it names the binary the cloud setup builds
in this checkout, .abcd/.work.local/cloud/bin/abcd (a regular executable
file, not a symlink, its folder resolved with pwd -P on both sides); any
other value, on any machine, is ignored and abcd is built.
begin.sh runs the connector's is_remote script first: outside the cloud it
prints that it is not running in the environment the brief was written for and
exits 1, before anything else (C3). Inside, it refuses uid 0, stops before any
work when the checkout's author identity is unset or is not a person's (the
attribution gate's own signals, through scripts/check-attribution.sh; it
names the cause without repeating the identity), and runs env-check.sh.
The push hook entry (C7, invariant 23)
The reservation build (itd-2609091034175565, branch feat/reserve-on-remote)
turns the hook's reservation exception into a declared list of receipt-less
prefixes, each entry naming the prefix, the commit shape a push under it must
have, and the record that rules it. This spec adds one entry and no special
case: its prefixes are those the connector declarations name (read through the
same validation as connector.sh, over the declarations HEAD commits; a
declaration that fails it adds no row, and the hook refuses a push to any
branch holding the segment abcd-wip, naming the file, while every other push
is judged as if no connector existed); its shape is (a) the branch is the prefix
followed by exactly one record id (iss-, itd- or spc- followed by sixteen
digits, or an ordinal itd-/iss- id) and nothing else, and (b)
scripts/check-attribution.sh commits <base> <local_sha> passes over the
pushed range (<remote_sha>..<local_sha> on an update; from the merge base
with origin/<default> on a create; the gate's findings name the identity
they refuse, so the hook discards them and prints a fixed sentence naming the
command that shows them); its record is adr-2610120937435856. The
default-branch refusal and the non-fast-forward refusal stay ahead of every
entry. The hook's header comment gains the entry and its reason.
The required check and the ruleset (C7, invariant 23)
scripts/check-wip-source.sh <source-ref> <base> <head> exits 1 when the
source branch holds the path segment abcd-wip (anywhere, so it is
host-agnostic) or when the diff <base>...<head> adds a path under
.abcd/work/cloud-reports/; exit 0 otherwise. Its cases file follows
scripts/check-attribution-cases.sh.
.github/workflows/ci.yml gains a job wip-guard that always runs, on
pull_request and merge_group (the docs-only classifier never stands it
down): on a pull request it passes github.head_ref; on a merge-queue entry it
reads the pull request number from the queue ref
(gh-readonly-queue/<base>/pr-<N>-<sha>) and the pull request's head ref from
the REST API with the job's read-only token, and refuses when either cannot be
read. Least-privilege permissions:, pinned actions, and zizmor clean.
.abcd/work/rulesets/main-protection.json adds wip-guard to the required
contexts. Applying the ruleset on GitHub is the person's act (step 7), and only
after the job has landed on the default branch, since a required context no
workflow reports blocks every merge.
The hand-back and the report (C6)
hand-back.sh --state <handed-back|stopped> --did <text> --failed <text> --needs <text> writes .abcd/work/cloud-reports/<item-id>.md (a short
frontmatter: item, state, channel, finished_at; then the three
sections), runs abcd lint outbound --label report over it and
refuses to send a report that fails (it prints the finding so the session can
fix its text), commits it alone as the branch's last commit (attribution-clean,
Assisted-by:), and pushes HEAD:refs/heads/<wip_prefix><item-id> through the
committed hook (never --no-verify). Its first act is the connector's
is_remote check: off the cloud it prints the report and commits, pushes and
posts nothing, and a connector that cannot be resolved (so where it runs
cannot be told) stops the same way, the report printed. It judges the checkout's identity before it commits, and never
repeats raw git or hook output, so a refused identity is shown nowhere. When
the push fails, it posts the same report with channel: issue-comment and why
the push failed (a fixed reason, with only the hook's and the remote's own
lines, identities replaced: addresses first, then names, each longest first,
so no domain survives) as a comment on the
tracking issue (cloud.conf's tracking_issue) through gh api repos/{owner}/{repo}/issues/{n}/comments --input -, after the same outbound
check. When both fail it exits 1 and prints the report, so the transcript still
holds it as a last resort.
The list (C8)
list.sh fetches refs/heads/<wip_prefix>* for every declared prefix and the
reservations (abcd-reserved/*, once that build is on the default branch),
reads each branch's report file from its tip with git show, and reads the
tracking issue's comments through gh api for sessions whose push failed. One
row per item: item, branch, state (handed-back, stopped, working when a
live reservation exists and no report does, unknown otherwise), and the
report's first line. It never opens a transcript, and it writes nothing but
the fetched remote-tracking refs.
The collect (C9)
collect.sh <item-id>: fetch the branch; refuse if its last commit is not the
report commit alone; create a worktree in the machine store
(~/.abcd.noindex/worktrees/<root-sha>/collect-<item-id>/, per
adr-2610031751065746) on a new branch collect/<item-id> from
origin/<default>; cherry-pick every commit of the branch except the report
commit; refuse if scripts/check-attribution.sh commits fails on them (the
commits must already name the person as author; collect never rewrites
authorship); run make preflight there, waiting while another preflight runs
on the machine; on a pass, print the branch, the receipt and the push command
for the person, and push nothing; on a failure, print the failures, keep the
worktree for inspection, and push nothing. Any receipt the cloud session made
is ignored: receipts live in the local tier and never travel, and collect never
reads one from the branch (decision 9). Earlier report commits (one per
earlier hand-back, each changing only this item's report) are left out as the
last one is. Before anything is made, collect refuses work that changes the
tooling, the build and gate machinery: the build file, scripts/, go.mod,
go.sum, go.work, go.work.sum, vendor/, the gates' settings and
baselines at the top of .abcd/ (every .abcd/*.json, such as
record-lint.json, docs-lint.json, site.json and the citation and site
baselines, and .abcd/config/), the files under .abcd/work/ a gate reads
(the reviews charter and its receipts in .abcd/work/reviews/, the ruleset
mirror in .abcd/work/rulesets/, and every .abcd/work/*.json, such as
brief-review-flags.json), and any top-level hidden directory but .abcd/
(.githooks/, .github/, .claude/; matched by shape, so the script names
no host), printing their diffstat, unless --allow-tooling-changes is
passed. The other records under .abcd/development/ and .abcd/work/ (the
issue records, CONTEXT.md, the decision records in decisions/,
intake.md) are no tooling.
Paths are read NUL-separated, so git quotes none, and matched case-folded, so
neither a quoted non-ASCII name nor a capital steps round a pattern. Whatever
the flags, collect refuses an added path whose case-folded form is another
path's in the tree the work lands in (the default branch's paths less those
the collected commits delete, with the copy's), since on a file system that
folds case it overwrites that path; a case-only rename deletes the path it
folds onto, so it passes. It cherry-picks with the person's own checkout's hooks, never the
worktree's.
Collect runs the collected copy's code on the person's computer, as the
person: preflight builds and runs that copy's Go code, its tests and its
Go-written gates included (go test, go run ./cmd/abcd, the evals). That is
the cost decision 9 accepted. The tooling refusal does not make the gates the
default branch's; it only keeps the build and gate machinery (the build file,
scripts, hooks, CI and the gates' settings) as the default branch has it, so
the session cannot choose which gates run or mint a receipt without running
them. Before preflight starts, collect prints this on standard output with the
full diffstat of the collected commits against origin/<default>, so the
person sees what code will run; there is no confirmation prompt.
Steps
- The proof rung: the platform's hypotheses on a real hosted session (needs the person)
- criteria: C1 and C10 (the hypotheses); the scope conditions on non-root running and on the proxy accepting the work-in-progress name
- packages: .abcd/development/research/notes (one dated note holding the receipts)
- the person, in a hosted session on this repository started from their own terminal with
claude -p "<probe brief>" --environment <env-id> --ref main --output-format json(proving that the start returns an id and a link; on a self-hosted environment only, since the host rejects an Anthropic-hostedenv_id there, iss-2610222040438242; an Anthropic-hosted start throughclaude --cloudis probed under iss-2610222106115276), runs as root:useradd -m abcd, gives it the checkout, installs gitleaks 8.24.3 checksum-verified, and, asabcd, runsmake preflighton unchangedmainin the background and records how long it took and what failed; then makes one empty-tree commit under their own author identity with anAssisted-by:trailer and pushes it toclaude/abcd-wip/iss-0000000000000000(a reserved example id), records whether the proxy accepted it, and posts one comment to a scratch issue throughgh apiover REST; also records whetherCLAUDE_CODE_REMOTEistrueand whether a commit made with the attribution settings off carries a co-author line or a session link - also the person's: create the one tracking issue for cloud sessions (a public act) and give its number to
scripts/cloud/cloud.confin step 8's change, or tell the build session - outcome: if the proxy refuses the work-in-progress name, steps 2, 6 and 8 stay unlanded and the prefix goes back to the person (the ADR's decision 2 and the intent's decision 3); if the
abcduser cannot run preflight, step 4 stays unlanded and the runner goes back to the person (decision 8); every other step proceeds - tests: none in code; the note's receipts are the evidence, kept free of session links and home paths
- The host connector's declaration and the gate-tool list (build-alone)
- criteria: C12, C2 (the list)
- packages: scripts/cloud/connector.sh, scripts/cloud/cloud.conf, scripts/cloud/connectors/claude/connector.conf, scripts/cloud/connectors/claude/is-remote.sh, scripts/gate-tools.tsv, internal/core/lint, internal/cloudscripts
- tests: TestConnectorDeclarationLoads; TestConnectorRefusesBadPrefix (table:
main,claude/,abcd-wip,a/b/abcd-wip/, empty, a trailing segment afterabcd-wip/); TestConnectorRefusesUnknownKeyAndEscapingPath; TestConnectorRefusesDuplicatePrefix; TestIsRemoteOnlyWhenMarkerSet; TestGateToolsNamesOnlyRealPreflightGates; TestNoHostMarkerOutsideConnector (searches every file this spec adds or changes outside the connector directory and.claude/settings.jsonfor the host's name, its command-line tool,CLAUDE_CODE_REMOTEandclaude/abcd-wip, and finds none)
- The place check and the environment check (build-alone)
- criteria: C2, C3
- packages: scripts/cloud/begin.sh, scripts/cloud/env-check.sh, internal/cloudscripts
- tests: TestBeginRefusesOutsideCloud (exit 1, the message, nothing else run); TestBeginRefusesRoot (a fake
id -uof 0); TestBeginStopsOnVendorOrUnsetIdentity (and the output never contains the identity it refused); TestEnvCheckNamesMissingToolSetupAndGates; TestEnvCheckStopsForAGateTool; TestEnvCheckWarnsForANonGateTool; TestEnvCheckWritesNothing
- The setup step and the settings file (build-alone; proved live in step 12)
- criteria: C1, C11
- needs: 2
- waits on: step 1's outcome on non-root running
- packages: scripts/cloud/connectors/claude/setup.sh, .claude/settings.json, internal/cloudscripts, internal/core/lint
- waits on: the token-identity verb from spc-2610150728396006's step 2 reaching the default branch, for the identity half only
- tests: TestSetupDoesNothingOffCloud; TestSetupSetsIdentityFromToken (a fake verb returning login and id;
user.nameanduser.emailset locally); TestSetupLookupFailureSetsNothingAndSaysSoOnce (the container's identity appears in no output, commit, ref or trailer); TestSetupReadsGitleaksPinFromCI (the version it would install equals ci.yml's); TestSetupRefusesWhenUserCannotBeCreated (a fakeuseraddfailing); TestSetupIsIdempotent (fakes foruseradd,chown, the download andsu, run twice); TestSettingsFileShape (plugin enabled from this repository's marketplace, the three attribution keys off, one SessionStart hook naming setup.sh, nothing else); TestSetupTrustsTheCheckoutForRoot (one safe.directory entry, the checkout, however often it runs); TestSetupBuildsAbcdForLaterShells (built asabcdinto the local tier, both exports written once, a failed build exported nowhere)
- The brief (build-alone)
- criteria: C4, C13
- packages: scripts/cloud/brief.sh, scripts/cloud/brief.md.tmpl, internal/cloudscripts
- tests: TestBriefNamesEveryPart (item, author, first step, the no-skip and go-run rules, branch from the connector's prefix, hand-back and fallback); TestBriefCarriesNoSessionLinkHomePathOrSecret; TestBriefRefusesUnreadyIntent; TestBriefAcceptsOpenIssueAndReadyIntent; TestBriefRefusesItemNotOnRemoteDefault (a draft never pushed; a record only on a local branch); TestBriefRefusesEmbargoedItem (a fixture embargo store and a fake
abcd embargo checkexiting 2); TestBriefFailsClosedWhenEmbargoCheckCannotRun (store present, verb unknown); TestBriefWithoutStoreSkipsEmbargoCheck; TestBriefReadsNoAuthorFromThisComputer; TestBriefReadsAbcdsRealExitCodes (go runcollapses an exit 2 to 1; the brief builds abcd once, or usesABCD_BIN, and reads 2 as 2); TestBriefChecksTheRecordAgainstTheEmbargo (the real abcd, a real store fromabcd embargo intakeover a fakegh, a record naming an embargoed title refused); TestBriefRefusesRecordWithUnpushedEdits; TestBriefRefusesUnpushedSpecEdits (an intent with any path under.abcd/development/differing from origin's); TestAbcdBinIgnoresAForeignABCDBIN
- The push hook's work-in-progress entry (build-alone, after
feat/reserve-on-remotehas landed its declared list on the default branch; until then this step waits)- criteria: C7 (the hook half), C12
- needs: 2
- waits on: the reservation build's declared list of receipt-less prefixes reaching the default branch (
feat/reserve-on-remote; landed by pull request #1004, so this step is free); the session-announcement build (spc-2610150728396006) adds a row to the same table, so the two rows are coordinated with that build - packages: .githooks/pre-push, internal/surface/cli/githook_prepush_test.go; internal/core/guard/defaults if the guard registry refuses the session's push (add it as a
known_good, as the reservation step did) - tests: TestPrePushAdmitsWIPWithoutReceipt (create and update); TestPrePushRefusesOtherHostBranchWithoutReceipt (
claude/fix-x); TestPrePushRefusesWIPWithBadRecordID (none, two ids, a trailing path); TestPrePushRefusesWIPFailingAttribution (an AI co-author trailer; noAssisted-by:); TestPrePushRefusesOnBadDeclaration; TestPrePushWIPStillRefusesForcePush; TestPrePushWIPAttributionRefusalNamesNoIdentity (the hook prints a fixed sentence, never the gate's findings); TestPrePushDefaultBranchStillRefused; the reservation entry's tests pass unchanged
- The required check and the ruleset (build-alone for the job and the mirror; needs the person to apply the ruleset)
- criteria: C7 (the boundary half)
- packages: scripts/check-wip-source.sh, scripts/check-wip-source-cases.sh, .github/workflows/ci.yml, .abcd/work/rulesets/main-protection.json, internal/core/lint
- tests: the cases file (a source holding
abcd-wipat either depth refused;abcd-wipe/andwip/passed; a diff adding.abcd/work/cloud-reports/x.mdrefused; a clean diff passed); TestWIPGuardJobRunsOnPullRequestAndMergeGroup; TestWIPGuardNeverStoodDown; TestMainProtectionRequiresWIPGuard; zizmor clean - the person's act, after the job is on the default branch: apply the ruleset to GitHub, then open a throwaway pull request from
test/abcd-wip/iss-0000000000000000and seewip-guardrefuse it; close it unmerged
- The hand-back and the report (build-alone)
- criteria: C6
- needs: 2
- packages: scripts/cloud/hand-back.sh, scripts/cloud/cloud.conf (the tracking issue number from step 1), internal/cloudscripts
- until the person has created the tracking issue,
cloud.confcarriestracking_issue=empty and the fallback refuses by name ("no tracking issue configured") and prints the report; it never posts anywhere else - tests: TestHandBackCommitsReportAloneAsLastCommit; TestHandBackPushesToWIPBranchThroughTheHook (no
--no-verifyin any argv); TestHandBackFallsBackToIssueCommentWhenPushFails (fakeghrecords the REST call and body); TestHandBackRefusesReportFailingOutbound; TestHandBackReportNamesItsChannel; TestHandBackPrintsReportWhenBothFail; TestHandBackReadsTheOutboundChecksOwnExitCode; TestHandBackBuildsAbcdOnceWithoutABCDBIN; TestHandBackScrubLeavesNoDomain (addresses before names, longest first); TestHandBackPrintsReportWhenConnectorCannotBeResolved; TestHandBackFallbackRefersToNothingDropped
- Starting a session through the connector (build-alone; proved live in step 12)
- criteria: C10, C5
- needs: 2, 5
- packages: scripts/cloud/send.sh, scripts/cloud/connectors/claude/launch.sh, internal/cloudscripts
- the environment id is read from the local tier (
.abcd/.work.local/cloud/environment), never committed; the session's link is printed to the person and never written to a tracked file - tests (fake
claudeon PATH): TestSendStartsOneSessionWithTheBrief; TestSendPrintsIdAndLink; TestSendRefusesWhenHostToolMissing; TestSendRefusesWithoutLogin; TestSendExpiredLoginSaysRenewByHand; TestSendRefusesUnknownEnvironment; TestSendHostRefusalStartsNothingLocal (no worktree, no process, no file beyond the brief preview); TestSendRefusesPrivateOrUnreadyItem (through step 5)
- The list (build-alone)
- criteria: C8
- needs: 2, 8
- packages: scripts/cloud/list.sh, internal/cloudscripts
- tests (bare origin fixture, fake
gh): TestListShowsHandedBackStoppedAndWorking (the working one from a reservation branch); TestListShowsFallbackCommentReport; TestListReadsNoTranscript (no path under any transcript store is opened); TestListWritesOnlyRemoteTrackingRefs; TestListReadsPeersOwnExitCode (peers' exit 2 is a source not read, never the last fetched reservations)
- The collect (build-alone)
- criteria: C9
- needs: 8
- packages: scripts/cloud/collect.sh, internal/cloudscripts
- tests (fake
make): TestCollectLeavesReportCommitOut; TestCollectRefusesBranchWithoutReportTip; TestCollectRefusesAttributionFailure; TestCollectRunsPreflightInMachineStoreWorktree; TestCollectPassPrintsBranchAndPushesNothing; TestCollectFailurePushesNothingAndKeepsWorktree; TestCollectIgnoresAnyCloudReceipt; TestCollectRefusesToolingChangesByDefault; TestCollectRefusesCaseVariantToolingPath; TestCollectRefusesCaseCollidingAddedPath; TestCollectMatchesNonASCIIPaths; TestCollectShowsWhatWillRunBeforePreflight
- The live run end to end, and the close (needs the person)
- criteria: C1, C10, C11 proved in a real hosted session; C6, C8, C9 seen working
- needs: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
- waits on: the ruleset applied on GitHub (step 7's person act)
- the person sends one small open issue with
send.sh, lets the session hand back, runslist.shandcollect.sh, and lands the collected branch the usual way; a build session can prepare everything and write the note, but the send and the landing are the person's - records: a dated research note with the receipts (no session link, no home path); the change that finishes this step closes this spec (
go run ./cmd/abcd spec close spc-2610120941441282, after the docs review for HEAD), carriesDelivers: itd-2610120738075547, and resolves iss-2610120729071657, iss-2610120729094755, iss-2610120729087253, iss-2610120729083126 and iss-2610120724251698 with aResolves:trailer each; iss-2610120729076366 keeps its worktree-guidance half and stays open with a note that C5 answered its other half
Delivery
Step 1, partly answered on 2026-10-15. A probe in a real hosted session
found that the forge proxy accepted a push to a branch outside the host's own
namespace as well as inside it, that the forge credited both pushes to the
person's own account through the environment's token, that the clone's git
identity was the host vendor's default (decisions 17 and 18 answer it in
step 4), and that the committed pre-push hook never ran because the clone had
no core.hooksPath (step 4 sets it). Still unproved there: a push to
claude/abcd-wip/<item-id> itself, running preflight as a non-root user,
CLAUDE_CODE_REMOTE, and a start from the person's terminal; step 12 proves
them.
A build session does not wait for step 1 to start: steps 2, 3, 5, 7 (its code
half), 8, 9, 10 and 11 need nothing from the person, and step 1's outcome can
only hold back steps 2, 4, 6 and 8, as step 1 says. Step 6 waits on the
reservation build's declared list reaching the default branch; the reserve
line in the brief and the working state in the list wait on abcd reserve
reaching it, and work without it until then (the list shows unknown). After
step 2, steps 3, 5, 8 and 11 touch disjoint files and can run as parallel
lanes.
Every lane honours the machine's one-preflight-at-a-time rule, and no lane runs a hosted session or touches the forge's settings.
Safety properties
Each is held by a named test above.
- No receipt-less push outside a declared work-in-progress prefix (TestPrePushRefusesOtherHostBranchWithoutReceipt, TestConnectorRefusesBadPrefix, TestPrePushRefusesOnBadDeclaration).
- No work-in-progress branch or cloud report reaches the default branch (the check-wip-source cases, TestMainProtectionRequiresWIPGuard, TestWIPGuardNeverStoodDown).
- No cloud receipt is trusted (TestCollectIgnoresAnyCloudReceipt, TestCollectRunsPreflightInMachineStoreWorktree).
- Nothing private is handed out (TestBriefRefusesItemNotOnRemoteDefault, TestBriefRefusesEmbargoedItem, TestBriefFailsClosedWhenEmbargoCheckCannotRun).
- No quiet local fallback (TestSendHostRefusalStartsNothingLocal, TestBeginRefusesOutsideCloud).
- No hook is skipped (TestHandBackPushesToWIPBranchThroughTheHook).
- No check runs as root (TestBeginRefusesRoot; the env check refuses uid 0).
- Public text carries no session link, tool footer, home path or secret (TestBriefCarriesNoSessionLinkHomePathOrSecret, TestHandBackRefusesReportFailingOutbound).
Out of scope
The intent's "Left out of the first rung": the Go verbs, managed repositories, other hosts and runners, a session cap, a stored login, automatic merging, messages between running sessions, the wider session register, recurring routines, deleting spent branches, and the two issue-local halves.
Frontmatter
| id | spc-2610120941441282 |
|---|---|
| type | spec |
| lifecycle | open |
.abcd/development/specs/open/spc-2610120941441282-a-person-hands-an-item-from-their.md
open on GitHub ↗ · commit history ↗
References1
- itd-2610120738075547Work handed to a cloud session comes back, or says why notimplements
mentions15
- adr-2610031751065746The worktree store lives under the renamed home, `~/.abcd.noindex/worktrees/<root-sha>/<name>/`, and the rest of its rule is unchanged
- adr-2610050711287503abcd reaches the harness's user settings only for the status line, with consent
- adr-2610120937435856A cloud session's unchecked work goes only to a declared work-in-progress prefix that never reaches the default branch
- iss-2610120724251698Cloud agent sessions started on 2026-10-12 to work drain-eligible issues all failed abcd's preflight and had git identity problems, so none pushed a branch (the person reported this; the exact errors are with the launching session abcd-0b). abcd's gates assume the local development machine: every commit must name a human author (the attribution gate refuses a machine identity or none), and preflight resolves the go.mod-pinned toolchain and other local prerequisites. A cloud session gets neither by default, so abcd cannot yet be worked from a cloud session.
- iss-2610120729071657abcd has no way to launch or manage cloud agent sessions. On 2026-10-12 a session asked to move three drain-eligible issues to cloud sessions: its sub-agents could not reach the cloud and quietly started on the local machine instead, and the real cloud sessions had to be started by the person at claude.ai/code. Nothing in abcd starts a cloud session with the right brief, lists the running ones, or collects their results, so cloud work is launched and tracked by hand.
- iss-2610120729076366A request to run lanes as cloud sessions fell back to local sub-agents without saying so: on 2026-10-12 three lanes meant for the cloud started on this machine, inside the requesting checkout (under its .claude/worktrees/), and other sessions only learned of it when the requesting session noticed and corrected its own announcement. A run that cannot reach the place it was asked to run in must say so before it starts (principle loud-staging).
- iss-2610120729083126A cloud session's stop report (what it did, what failed, what it needs) is visible only in its transcript at claude.ai/code: on 2026-10-12 the cloud sessions could not message the local sessions and pushed nothing, so the person had to copy one session's report into a local session by hand, and the other two sessions' reports are still unread.
- iss-2610120729087253A cloud session whose preflight cannot pass has no way to hand its commits back: the pre-push hook refuses a push without a preflight receipt, and skipping that hook is forbidden, so on 2026-10-12 a finished fix (two commits and a resolved record) stayed trapped in the cloud container and had to be redone locally.
- iss-2610120729094755Nothing prepares a cloud container to work on abcd the way the development machine is prepared: on 2026-10-12 the container ran everything as root, had no gitleaks, and needed the brief to set the git identity, so preflight failed (iss-2610120724251698). abcd documents its local prerequisites but has no setup step or check for a cloud environment.
- iss-2610222040438242The cloud launch connector (scripts/cloud/connectors/claude/launch.sh lines 10 and 51, pinned by internal/cloudscripts/cloud_send_test.go:113, and stated as the launch form in itd-2610120738075547 and spc-2610120941441282 step 12) passes whatever environment id the person configured to `claude -p "<prompt>" --environment <id> --ref <branch> --output-format json`. Claude Code's CLI reference and cloud-environments pages, read on 2026-10-22, document that form for a self-hosted environment only: `--environment` takes a `ccpool_` id and Claude Code rejects an Anthropic-hosted `env_` id there, and `--ref` applies only with it; the test fixture itself used an `env_` id. So a person whose cloud environment is Anthropic-hosted, the usual case, gets a refusal at launch. Found while preparing the product thinker's hosted run for 2026-10-23.
- iss-2610222106115276The cloud hand-off cannot start a session on the Anthropic-hosted cloud environment: Claude Code names an environment at the start only for a self-hosted one (--environment takes a ccpool_ id and rejects Anthropic-hosted env_ ids, CLI reference read 2026-10-22), and the documented way to start an Anthropic-hosted session from a terminal, claude --cloud "<task>", takes its environment from the /remote-env default and its branch from the current checkout, and documents neither its output on creation (no machine-readable form is documented for creating, only for sending to an existing session) nor whether it exits or attaches; so scripts/cloud/connectors/claude/launch.sh refuses an env_ id rather than guess, and the planned hand-off (itd-2610120738075547) works only on a self-hosted environment until the create form is probed live
- iss-2610241549334990A spec's Footprint packages line is reserved exactly as written. An entry carrying a note or prose therefore becomes a footprint path with that text in it. A build of spc-2610111627213138 would reserve internal/core/memnote (new). A build of spc-2610120941441282 would reserve internal/surface/cli (the pre-push hook's fixture tests), and its parenthetical containing a comma would split into the two entries scripts/cloud/connectors/claude (new and the host connector). Overlap compares paths segment by segment, and claim.ValidFootprintPath accepts spaces. So such an entry shares no path with the package it means, and the build does not hold that package against another build. Several open specs carry such entries, among them spc-2609221011151661, spc-2610111627213138 and spc-2610120941441282.
- itd-2609091034175565Before an agent starts on an item, it reserves it on the shared remote, and everyone who works on the project sees the reservation
- spc-2610031753359849Reservations on the shared remote: `abcd reserve`, one liveness judgement, and the build, drain and land integration
- spc-2610150728396006sessions-announce-what-they-are-doing