fix: a symlinked home is never trusted, the run counts its agents, and pages name who waits - #739
Merged
Merged
Conversation
…d dangling entry
`ahoy install` with no verified release artefact in the persistent plugin
data directory wrote the PATH entry as a symlink into the versioned plugin
root, reported success, and left an entry the next plugin update strands.
Its remedy named a condition the operator cannot check ("a session whose
hooks provisioned the cache"), and a later run could not repair the
result because a dangling link classified as foreign.
Install now writes no entry on a cold cache and names a command the
operator can run first: the README install one-liner, which fetches the
release binary, verifies it against that release's checksums.txt and
records it, after which a re-run adopts the copy in place. Fetching inside
install is not taken: adr-38 lets the network answer only a verb whose
documented meaning is the fetch, and the one fetch-and-verify primitive
(`abcd update`'s) imports ahoy. A working pin into the plugin root that an
earlier release wrote is left where it stands, raised as a required
symlink.legacy gap whatever the cache holds (the folded duplicate's first
ask), and recorded so the hooks accept it meanwhile. installPinnedSymlink
has no caller left and is removed.
A dangling link ~/.abcd/path-entry names — read through the same owned,
not-group-or-other-writable guard the hook shims apply — is now abcd's own
(classifyBinTarget), so detection names it with the owned wording, `abcd
update` routes it to the repair, install replaces it with the verified
copy, and uninstall removes it with its record. The record claims only a
DANGLING link, which `command -v` never yields, so no execution path
widens. An unrecorded dangling link keeps iss-2609100506256636's ruling:
no provenance claimed, untouched by detection and by any run with nothing
to write in its place.
The hermetic test setups provision an attested cache by default, the
state a hook session meets; cold-cache tests say so with coldCache.
Refs: iss-2609100506263330
Refs: iss-2609100506256636
Assisted-by: Claude:claude-opus-5-5
…ache link Resolves: iss-2609100506263330 Assisted-by: Claude:claude-opus-5-5
…f the adopted copy is never repaired Found while finishing lane drainH: with an abcd-owned dangling link earlier on PATH than the owned copy ahoy adopts, install never touches the earlier entry, so its symlink.dangling gap stays in Remaining on every run, and the shadow note calls it what runs and something abcd does not own. The warm-cache half reproduces at base 0f9d652. Not fixed in this lane; named in the lane report for a ruling or a recorded deferral. Refs: iss-2609280932480608 Assisted-by: Claude:claude-opus-5-5
… the one-liner The cold-cache refusal names the install one-liner and promises that a re-run of `abcd ahoy install` adopts what it wrote. When any symlink.* gap drives the install step while the target is already that owned copy (an entry a plugin update stranded earlier on PATH is enough), the step reached the cold-cache refusal, said "no PATH entry was written" where the copy stands, and answered the operator who had just run the remedy with the same remedy. A cold cache has nothing to refresh an owned copy from, so the copy is now adopted as it stands. TestColdCacheRemedyIsAdoptedBehindAStrandedEntry was watched fail on a scratch copy of 4cb8e7b and pass with this change; TestColdCacheRemedyIsAdoptedByTheReRun pins the plain re-run and was watched fail under a mutation that drops stepSymlink's gap gate. Refs: iss-2609100506263330 Assisted-by: Claude:claude-opus-5-5
A banned token may declare extra_roots: repo-relative directories or files that entry alone reads in addition to the configuration's roots, every text file there and not only markdown. It is how one ban reaches past the documentation (the command pages, a rules file written in JSON) without arming the rest of the family there: the docs lint over commands/ would otherwise raise some 170 unrelated blockers (em dashes in list items, present-tense and spelling tokens) on pages that are not documentation. The walk is the name gate's, factored into one helper both passes share: contained and gitignore-pruned roots, exempt_paths and exempt_if_status, the escape and the fence default as under roots, a binary file skipped, a file the roots walk already read not read twice, and a missing or escaping extra root refused as a configuration error naming extra_roots. Refs: itd-2609212137129937 Assisted-by: Claude:claude-opus-5-5
…litator The retired role word blurred the two people abcd addresses, and agents said it because abcd's pages did. Every use in the command pages, the bundled and repository rules, the brief, the principles, the personas registry, the plugin agent pages, CONTRIBUTING and abcd's rendered text is rewritten to the role the sentence asks of the person: the product thinker for what to build, rulings, adoption, sign-off and the irreversible acts itd-97 keeps human (adjudication, dependency sign-off, publishing); the technical facilitator for gates, merges, hooks, installs and mechanics; both named where the sentence genuinely means either. - The lint: `roles/retired-role-word` joins the one docs-lint banned_tokens list as a blocker, reaching the docs roots and, through its extra_roots, commands/, .abcd/rules.json and the bundled rules source. The allow escape is the only way past it. It is a deliberate seed omission: its roots are abcd's own trees. - The questions: every plugin page that has the agent put a question to a human names which role it asks and sets that role's mode first; a test reads the addressee vocabulary from the mode package and walks the pages. - The glossary: product-thinker and technical-facilitator beside the record-families page, citing itd-97's stance that the facilitator is a mode, not a person; the generated index and layout re-rendered. - The test: every command's rendered help and every plugin page is walked for the word. - press-release-composer 0.1.1 attributes quotes by role. Refs: itd-2609212137129937 Assisted-by: Claude:claude-opus-5-5
… root The owned symlink.dangling gap sends the operator to `ahoy uninstall` "if abcd is gone", but uninstall answered "plugin root unresolved; left untouched" in exactly that case: it judged the plugin root before the entry, although the provenance record vouches for a dangling link without one. And with no root, adoptedBinTarget skipped the PATH scan, so a recorded dangling entry outside the default location was never even found. Uninstall now judges recordedDanglingLink before the root, and adoptedBinTarget finds a recorded dangling link on PATH when no root resolves (recordedDanglingPathEntry, which requires a symlink so a record naming an absent path finds nothing). An unrecorded dangling link in the same shape is still left untouched, its unrelated record with it. The claim "uninstall removes it with its record" in the fix hint, commands/ahoy.md, DECISIONS and the resolved record now holds as written; the command page and the brief surface chapter say it holds without a plugin root. Test watched red on a git-archive copy of c094510: TestUninstallTakesARecordedDanglingEntryWithNoPluginRoot/recorded/* (the unrecorded subtest is the guard and passes on both). Refs: iss-2609100506263330 Assisted-by: Claude:claude-opus-5-5
The resolution says uninstall removes a recorded dangling link with its record; 2c80445 is the commit that makes that hold when no plugin root resolves. Refs: iss-2609100506263330 Assisted-by: Claude:claude-opus-5-5
… dangling link runs nothing
An abcd-owned link whose target has gone, sitting on PATH ahead of the entry
install acts on (typically a link a plugin update stranded ahead of the copy
the install one-liner wrote), was never repaired: install acts only on its own
target, so the symlink.dangling gap stayed in Remaining on every run and no run
could finish clean. The install-time shadow note also said that link "is what
runs when you type `abcd`" and ended "abcd never clobbers a binary it does not
own" about an entry abcd classifies as its own.
Ruling (orchestrator abcd-9f, autonomous run A): fix it by applying
iss-2609100506256636's rule, danglingness not provenance, one entry earlier.
stepSymlink, when gap-driven, now ends with clearStrandedEntries: every OWNED
dangling entry on PATH other than the target is removed with its record, and a
note names it. Unowned dangling links are never touched.
Decision taken in this lane: the removal is gated on the target being a WORKING
entry of abcd's own after the step (entryAnswers), not on the clearDanglingEntry
gate ("the run has something to write"). The brief asked for both that gate and
for the cold-cache adoption test to end with nothing Remaining; on a cold cache
the run writes nothing while the adopted copy answers, so the write gate would
leave the gap in place. The post-condition gate is the stronger of the two on a
warm cache (a failed write removes nothing) and covers adoption; with nothing
working at the target, the dangling entry stays and the refusal names what to
run first, as clearDanglingEntry does at the target itself.
Wording: a dangling link runs nothing — bash, zsh and sh all skip it and answer
from the next PATH entry (probed on this machine) — so shadowMessage gains a
dangling branch, and danglingEntryGap's detail no longer claims the link
"shadows every later PATH entry", which was false in the same way and would
otherwise contradict the new note in one report. Comments carrying the same
claim are corrected; the command page and the brief surface chapter say it.
Tests watched red on a git-archive copy of c094510:
TestColdCacheRemedyIsAdoptedBehindAStrandedEntry (extended: stranded entry
gone, Remaining empty, no live-shadow wording),
TestWarmInstallRemovesAnOwnedDanglingEntryAheadOfIt,
TestUnownedDanglingEntryAheadIsLeftAlone (red on wording only; its "left alone"
half is a guard), TestDanglingEntryWordingNeverClaimsItRuns.
Refs: iss-2609280932480608
Refs: iss-2609100506256636
Assisted-by: Claude:claude-opus-5-5
…ng entry ahead of it Fixed by 7711d16: install removes every abcd-owned dangling PATH entry other than its target, with its record, once the target is a working entry of abcd's own, and a dangling link is described as running nothing. Resolves: iss-2609280932480608 Assisted-by: Claude:claude-opus-5-5
…gling-link clause Appends the 2026-09-28 ruling that fixed iss-2609280932480608 and the gate the removal waits for. The same line carries a dated correction to the 2026-09-27 entry on iss-2609100506263330, which says an unrecorded dangling link is "cleared only when install writes the verified copy there": clearDanglingEntry also clears it on a --dev run when the plugin binary the shim rebuilds beside exists. The 2026-09-27 entry stays as written; the log is append-only. Refs: iss-2609280932480608 Refs: iss-2609100506263330 Assisted-by: Claude:claude-opus-5-5
…ers follow a symlinked ~/.abcd Pre-existing LOW from review-drainH, left unfixed in this lane by the brief; no open record covered it (iss-2609260958587561 is the credential store's copy). Refs: iss-2609281017573862 Refs: iss-2609260958587561 Assisted-by: Claude:claude-opus-5-5
The per-root walk keeps markdown alone, so a file such as a JSON rules file named in roots passed the does-not-exist check, contributed zero documents, and every armed rule reported it clean. It is now a configuration error that points at a banned token's extra_roots, in the lint and in the document count alike. Captured in the same change. Refs: iss-2609281045487620 Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609281045487620 Assisted-by: Claude:claude-opus-5-5
All five criteria are met: the sweep (no occurrence left in the command pages, rules, brief, principles, docs, personas, plugin agent pages or rendered text), the lint (roles/retired-role-word on every docs-lint root and its extra_roots), the questions (each plugin question block names its role and sets that mode first), the glossary (product-thinker and technical-facilitator beside record-families, citing itd-97) and the test over the rendered help and the plugin pages. Delivers: itd-2609212137129937 Assisted-by: Claude:claude-opus-5-5
fsutil.AppendLineIn now vets its leaf as its read twin ReadGuardedInRoot does: an Lstat refuses a symlink or non-regular file before the open, every open carries O_NOFOLLOW and O_NONBLOCK, and the descriptor must be the regular file the Lstat saw. A run log symlinked onto a claim file no longer appends into the claim. Every AppendLineIn caller (the run log, the lab index, the source ledger, the voyage log, the inbox's promoted log) gains the refusal. readClaim runs validName on a claim's session and lane and treats a failure as an unreadable claim, so a hand-edited claim carrying terminal escapes or a path never reaches HeldError or the release refusal. Both lock primitives create their lock file 0600 (lockFileMode): an open for the flock needs write access, which 0644 never gave another account. Refs: iss-2609230720193756 Assisted-by: Claude:claude-opus-5-5
…reads `implement log` now checks, per event, the fields the report counts and refuses a line missing one, naming it: lane_close needs lane and outcome; agent_start its agent; agent_end its agent, role, model and a number under minutes, wall_minutes or wall_min. A session's ceiling is held against the agents it declares: the verb counts the agents its agent_start lines since it joined name, less those an agent_end of the same agent ended, refuses an agent_start past the ceiling and logs the refusal (condition agent_ceiling). check reports agents_alive beside the ceiling. A fork or an agent started outside the log stays invisible, so the no-fork rule stays the discipline for that half. New loggable events: ceiling_overrun (alive, ceiling, lane, minutes) and the run's evidence events, intervention (kind in a closed set, by, what, why, autonomy_gap; at and detected_after_min checked when given) and decision (what, alternative, why). stop, already loggable, now needs a cause and checks last_productive and noticed_after_min when given. The report gains a ceiling-overrun column per mode and session, an evidence count over the run (interventions by kind, stops, decisions, with the minutes unnoticed), missing_fields (per event, the lines lacking a required field) and coverage (each of lane_open, lane_close, agent_start, agent_end and gate_run whose lines stop more than six hours before the run's last line). A session_open logged at most a minute before a window_mode counts in that window. Older lines never fail the report; they are counted under missing_fields instead. The command page and the brief chapter say the bounds, the release refusal included, rest on a cooperative, unauthenticated role. Refs: iss-2609240646542516, iss-2609240646549900, iss-2609240646544930, iss-2609240646555891, iss-2609230720193756 Assisted-by: Claude:claude-opus-5-5
…hared run `abcd build <itd-N> --session <id>` names the host session joined to the shared run state. A new run then claims its intent there for that session, the run id as its lane and the longest lease a claim takes, so a build of the same intent from any other checkout of the repository (another worktree, a second clone on the machine) meets the claim at its peers check before this run's lane has moved or claimed anything. The session's own live claim on the intent is not counted as a peer's, so a session that claimed the intent before building it is not refused by itself; its claim is renewed for the run. A session the shared run does not hold is refused at the new `claim` step before anything is created; a claim refused under the lock leaves no run behind; a run whose state cannot be written releases the claim it took. Without --session nothing changes, and the result now says the run holds no claim, so the invisibility the record names is stated rather than silent. implement.Run.Joined exposes the joined-session lookup the start needs. Refs: iss-2609252050506863 Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609230720193756 Assisted-by: Claude:claude-opus-5-5
…t column Resolves: iss-2609240646549900 Assisted-by: Claude:claude-opus-5-5
…nts in it Resolves: iss-2609240646544930 Assisted-by: Claude:claude-opus-5-5
…gaps Resolves: iss-2609240646555891 Assisted-by: Claude:claude-opus-5-5
… against the ceiling Resolves: iss-2609240646542516 Assisted-by: Claude:claude-opus-5-5
…nt in the shared run Resolves: iss-2609252050506863 Assisted-by: Claude:claude-opus-5-5
The review of this lane found three stops that still said "the user" and an interview page that contradicted itself. The house-style question in ahoy install is relayed to the technical facilitator, the --remote confirmation and its --yes are the technical facilitator's word, and site's --yes waits on the technical facilitator; each sets `abcd mode facilitator` in its section. The planning interview no longer claims every question is the product thinker's: the sign-off is theirs, and the mode follows the hat per question, as the rule below it says. The AC3 detector missed these phrasings, so it now also reads "ask them", "relay the question", "present the question" and a `--yes` that answers a question in advance, and it reads a section's mode setter across a line wrap (ingest and source wrap theirs). Refs: itd-2609212137129937 Assisted-by: Claude:claude-opus-5-5
…butors The reflect chapter credited its 2026-07-12 tier adjudication to the product thinker, which the record never says; tier placement is a how-question, so the sentence names no role. CONTRIBUTING asks an outside contributor for the product thinker's nod without defining the term, so it now says who that is. Refs: itd-2609212137129937 Assisted-by: Claude:claude-opus-5-5
Found while routing every ~/.abcd reader and writer through one symlink check: the ahoy history registry is a store rather than a trust declaration, so it is captured for its own ruling instead of being changed in this lane. Refs: iss-2609281129171021 Assisted-by: Claude:claude-opus-5-5
…fusal Two LOWs the verification of the drainH lane left on this code. A successful removal of abcd's own dangling PATH entry was recorded through refuse, the channel documented as loud refusals; it now goes through inform, which lands in the same notes (the person reads both in one place) but says in the code that it reports something done. Two comments still said a dangling link "shadows" the entries behind it; it runs nothing, and they say so. Refs: iss-2609280932480608 Assisted-by: Claude:claude-opus-5-5
…s there AGENTS.md states the rule for the rules loader: a dotfiles-symlinked ~/.abcd never hosts a rules.json. Every other file abcd acts on in ~/.abcd followed the link, because each home-scoped primitive guards the leaf and lets the kernel resolve the directories above it. The path-entry that decides which binary the hook shims run and the cache attestation that decides which binary is promoted onto PATH were honoured behind the link, and the credential store wrote a secret through it into whatever the link points at. One check now carries the rule. fsutil.HomeScopeLink Lstats every directory of a home-relative path below the home (never the home itself, never the leaf) and refuses the first symlink with a HomeScopeLinkError that names it and the repair. fsutil.ReadHomeDeclaration composes it with ReadDeclaration, keeping the rule's shape exactly: a file that is absent reads as absent whatever the directories are, and a file behind the link is DeclarationBehindSymlink, refused before a byte is read. A detector (TestHomeDeclarationsReadThroughReadHomeDeclaration) refuses any bare fsutil.ReadDeclaration call outside fsutil. It flagged nine readers. Routed readers: rules.json (message unchanged), trusted-roots and local-transcript-roots (ignored with a note), path-entry and cache-attestation (ahoy.homeScope refuses the link, so neither vouches and cacheBindingProblem names it, and the install remedy says to replace the link rather than re-run the hooks), config.json and oracle-routing.json (the layered machine layer fails loudly), load-limits (defaults with the reason), statusline.json (ignored with a note; ReadSettingsFile takes the home), and credentials.json (loud refusal). Routed writers: credential.SetMachine and the provider block write refuse before creating anything, the lock included; writePathEntry, the routing table and the status-line setting refuse through the same check. The hook shims test `-L "$HOME/.abcd"` before they read path-entry, hooks/bootstrap.sh treats the link as a refused home (no path-entry refresh, no attestation, and the notice says why), and the three install one-liners refuse the link before downloading anything. The credential store's other half: the store lock was created 0644, and flock needs only a read-only descriptor, so any local user could hold it and stall every write for its whole wait. fsutil's locks are created 0600 and an older lock is narrowed on the descriptor the next writer opens. Every writer opens its lock O_RDWR, which a 0644 file admitted to the owner alone, so nothing that could take a lock before loses it. The ahoy history registry is a store, not a declaration, and still writes through the link; it is captured as iss-2609281129171021. Refs: iss-2609281017573862, iss-2609260958587561, iss-2609281129171021 Assisted-by: Claude:claude-opus-5-5
…d behind a symlink Both are fixed by 734b26a: every reader and writer of a file abcd trusts in ~/.abcd refuses a symlinked ~/.abcd through fsutil.HomeScopeLink, and the credential store's lock is created owner-only. Resolves: iss-2609281017573862 Resolves: iss-2609260958587561 Assisted-by: Claude:claude-opus-5-5
…ion reading iss-2609281134544802 holds the four classes the reading leaves unread for a ruling, deferred past v0.11.0: a pid list, a stream path, shell text and a pkill or killall program name carried in a variable. The DECISIONS entry records the reading, the sweep's before and after counts, and the over-reads kept. Refs: iss-2609281134544802, iss-2609251824244354, iss-2609270036253187 Assisted-by: Claude:claude-opus-5-5
…ard reads a variable, and three more kill feeds The parameter-expansion reading and the three kill-by-search feeds landed in 7cf24a4. The carried-value classes the reading leaves for a ruling are iss-2609281134544802, captured and deferred before this resolve. Refs: iss-2609281134544802 Resolves: iss-2609251824244354 Resolves: iss-2609270036253187 Assisted-by: Claude:claude-opus-5-5
Refs: iss-2609281229109140 Assisted-by: Claude:claude-opus-5-5
The payload reading spelled a variable-only word back as `$X` text and re-tokenized it, so the quote or backslash a string puts round the expansion applied to the name on the re-read, while bash applied it to the value the enclosing shell had already put in. A single-quoted `'--$X'` or `'-$F'` inside a string handed to sh -c, bash -c or eval, and a single-quoted `'$GIT'` in command position there, read as literal text and allowed every blocker (review-drainG3 finding 1; the seven probes are pinned in TestAStringsQuotingAppliesToTheVariablesValue). A variable's value now reaches the re-read as its own mark byte (varMark, 0x01) rather than text: the tokenizer's addCur turns it into unknownMark wherever it lands, unquoted, quoted, escaped or in an ANSI-C string, and records the word as a variable's (curVar), so the carve-outs still read it as a variable. addCur also records a raw unknownMark as a substitution's, so no word holding one is spelled as a variable's. An ANSI-C escape that decodes to 0x01 stays text (readAnsiCQuote), so no escape forges the mark; env -S refuses the mark as it refused `$`. The review's backslash probe (`\\$X` as a whole word in a string) stays allowed: the backslash leaves a word that is wholly a variable, which DECISIONS 2026-09-28 reads as one operand, as its bare twin and the top-level `$X` operand are. A glued one (`-\\$F`, `--for\\$X`) blocks. The 4,315-input false-positive sweep is unchanged, line for line. Refs: iss-2609251824244354 Assisted-by: Claude:claude-opus-5-5
…open openAppendIn opens through os.Root, which resolves an in-root leaf symlink itself whatever flags the open carries, so O_NOFOLLOW never refused a link: the refusal rested on the pre-open Lstat and a SameFile check that was skipped when that Lstat saw nothing. A link planted in that window was followed and appended through. After the open, an unconditional second Lstat of the leaf must see a regular file that is the same file as the opened descriptor; anything else closes the descriptor and is ErrNotRegular. The comment names the Lstat+SameFile pair as the mechanism. A test-only seam, nil in production, plants the link in the window deterministically. Refs: iss-2609281229109140 Assisted-by: Claude:claude-opus-5-5
…d after its Lstat Resolves: iss-2609281229109140 Assisted-by: Claude:claude-opus-5-5
… into An unquoted here-document's substitutions were handed to the standard input of the command that opened the document only. The bodies are read at the newline, after the whole pipeline was emitted, so the search they ran lay past the pipe window of every command downstream of the owner: `cat <<EOF | xargs kill` over `$(pgrep make)` allowed, as did the tee, sh -c 'xargs kill', brace-group and backtick spellings, while the here-string twin blocked (review-drainG3 finding 2). handOnDocs now hands each body on to every later command whose pipe, or whose inherited input (a pipe into its group, or into the command a substitution sits in), is a run holding the owner, as one feed spanning the bodies of the owners in that run. The walk reads each command once and asks two binary searches per run, so a pipeline of owners stays linear (TestAHereDocBodyFeedStaysLinear, growth 3.97x against the 6x bar). The 4,315-input false-positive sweep is unchanged, line for line. Refs: iss-2609270036253187 Assisted-by: Claude:claude-opus-5-5
iss-2609281134544802 stated classes 1 (a pid list carried in a variable) and 3 (shell text a variable holds) without an everyday line a reading would refuse, so the ruling it waits on did not have the cost in front of it (review-drainG3, LOW). The body now names them from the sweep's everyday-variable lines, each of which allows today: kill "$pid", kill -TERM "$PID" and kill -- -"$pg" for class 1, and eval "$cmd" for class 3. The deferral is unchanged. Refs: iss-2609281134544802 Assisted-by: Claude:claude-opus-5-5
Without --session the text said "claim: none" while the JSON omitted the claim key (omitempty), so a machine reader could not tell "no claim" from a payload that never carried the field. The key is now always present and null when no claim was taken, the explicit-null convention sibling payloads use for an absent pointer (ahoy's adopted, memory's write_report). The command page and the build chapter say so. Refs: iss-2609252050506863 Assisted-by: Claude:claude-opus-5-5
The seam's declaration sat between openAppendIn's doc comment and the function, so godoc attached the whole comment to the variable. The seam moves above the comment. Refs: iss-2609281229109140 Assisted-by: Claude:claude-opus-5-5
HomeScopeLink passed an escaping rel ("../x/f", ".abcd/../../x/f") as
"no link here" and judged HOME ITSELF for an absolute one, the one
directory the rule deliberately leaves alone. Every caller passes a
constant, so nothing reached it; the check still vouched for a path its
walk never covered. It now refuses !fsutil.ValidRelPath(rel) with an
*os.PathError wrapping os.ErrInvalid, and ReadHomeDeclaration refuses
the same rel as DeclarationUnreadable before looking at anything, rather
than reporting it absent or behind a symlink it did not find.
Refs: iss-2609281017573862
Assisted-by: Claude:claude-opus-5-5
The history registry was the one writer in ~/.abcd still following a symlinked ~/.abcd: `ahoy install` created history/index.json wherever the link pointed, typically a dotfiles checkout. historyRoot() is now the registry's single chokepoint: after UserHomeDir it applies fsutil.HomeScopeLink to .abcd/history/index.json, so ~/.abcd and ~/.abcd/history are judged and every caller (the index reader and writer, the install step, the registration, the detector) refuses on it. Both os.MkdirAll(root) calls (withHistoryLock, bootstrapHistory) become ensureHistoryRoot, which creates the levels through fsutil.EnsureRealDirAll, so a link planted after the check is refused rather than followed. The install step reports the refusal through a.refuse, naming the link and the repair, and writes nothing. The detector raises a diagnostic (history.home_symlinked, neither required nor resolvable) in place of history.bootstrap_missing and history.meta_missing, which install would report as outstanding on every run and could never close. Decision taken: EnsureRealDirAll's walk starts at home with its symlinks resolved. EnsureRealDirAll proves its base real, and a home that is itself a link (/home -> /usr/home) is the machine's layout, which the rule never judges; with the literal base a linked home lost its registration (TestInstallRegistersThroughAHomeThatIsItselfALink, watched fail in a scratch copy with the literal base). The brief's configuration chapter says the registry is created through the link; it now states the refusal. Refs: iss-2609281129171021 Assisted-by: Claude:claude-opus-5-5
…ymlinked ~/.abcd Resolves: iss-2609281129171021 Assisted-by: Claude:claude-opus-5-5
HomeScopeLink judges ~/.abcd by Lstat and every reader and writer then opens by path, so a same-uid swap between the two follows the link. The record names the airtight standard-library form (Openat with O_DIRECTORY|O_NOFOLLOW from a home descriptor) and is left open: it is not built in this fix round. Refs: iss-2609281310017733 Assisted-by: Claude:claude-opus-5-5
…teg/land-12 Lane drainHome (fix/drain-symlinked-home, tip b47a60f), which carries lane drainH (fix/drain-ahoy-path, 819dd3c): ahoy install on a cold cache, recorded dangling PATH entries, and every reader and writer of a trusted file under ~/.abcd refusing a symlinked ~/.abcd. Conflict: hooks/hooks.json, the PreToolUse guard shim. Main (9a5b11c) reads the hook input first and names the tool it let through (AskUserQuestion or shell); the lane adds the symlinked ~/.abcd check before the path-entry read. Combined by hunk: main's command with the lane's two fragments (the -L test and its message) spliced in, so all five path-entry shims carry the same guard. Refs: iss-2609281310017733 Assisted-by: Claude:claude-opus-5-5
Lane drainI (fix/drain-implement, tip 9b39a6b): the run state's same-uid hygiene gaps, the declared-agent ceiling and the evidence events the report reads, a build started for a session claiming its intent, and the append primitive's post-open Lstat. Conflict: internal/fsutil/flock.go, both lock-open lines. Both lanes create lock files 0600; drainHome's lockPerm also narrows an older group- or other-readable lock on the open descriptor (tightenLock), and its comment is right that flock holds LOCK_EX on a read-only descriptor. Took drainHome's two lockPerm lines and dropped drainI's lockFileMode constant and its comment, which nothing else references. flock.go is now byte-identical to drainHome's. fsutil.go merged clean; openAppendIn keeps drainI's unconditional post-open Lstat plus SameFile. Generated files regenerated (go generate ./internal/surface/cli): no change from the auto-merge. Assisted-by: Claude:claude-opus-5-5
Lane drainG3 (fix/drain-guard-3, tip 7d96abd), which carries the drainG chain (d833a6f, also on integ/land-11): the guard reads a parameter expansion as an unknown word, a string's own quotes apply to a variable's value rather than its name, and a here-document body reaches the commands its owner pipes into. The carried-value remainder stays deferred for a ruling. No conflict. DECISIONS.md merged as a pure append; generated files unchanged on regeneration. Refs: iss-2609281134544802 Assisted-by: Claude:claude-opus-5-5
Lane roles (feat/roles-not-maintainer, tip 88830a3): abcd's own text names the product thinker or the technical facilitator, a banned token refuses the retired role word across the plugin pages and rules files, and every plugin-page question names whose answer it waits on. Ships itd-2609212137129937. Conflict: commands/intent.md, the paragraph on the expectation and its falsifier. Main widened the degenerate-text list (text the site cannot render); the lane named the product thinker as the addressee. Combined by hunk: main's wording with the lane's "put the question to the product thinker". Semantic conflict: three plugin-page questions that main added after the lane's base (920afe8, aafd4f2) put a question to "the user", which the lane's AC3 test (TestPluginQuestionBlocksNameTheAddressee) refuses on the merged tree. Named by itd-97's split, with the mode set in each section: the ahoy tool-install question goes to the technical facilitator (its section already sets the facilitator mode); confirming a capture's likely-double link is capture triage, the technical facilitator's (sets `abcd mode facilitator`); confirming an intent draft's likely-double link is the product thinker's (sets `abcd mode product-thinker`). Assisted-by: Claude:claude-opus-5-5
commands/docs.md told the agent to confirm a citation "on the user's word", a stop that named no role. Confirming a citation clears the currency gate's manual queue, which the docs brief (10-docs) already gives to the technical facilitator, so the page now sets `abcd mode facilitator` first and confirms on the technical facilitator's word. The AC3 detector did not see the stop: an act taken "on the <person>'s word" is now a question block. Watched it fail on docs.md:68 before the page change and pass after. Refs: itd-2609212137129937 Assisted-by: Claude:claude-opus-5-5
Brings integ9 (#733), cap-45 (#734), integ10 (#735) and integ11 (#736). Resolved by hunk: - .github/CONTRIBUTING.md: integ11 moved the guide from the root; the roles lane's dependency sign-off line ("the product thinker's explicit sign-off") is re-applied at the new path with main's re-rooted ../docs link. The role gloss at line 18 merged clean. No root CONTRIBUTING.md remains. - internal/core/ahoy/docslint_seed_test.go: keeps both seed omissions, roles' "roles/" and integ10's "persona_registry". - banlist/public_test.go (2 hunks), lint/lint_test.go: the name_roots fixtures take .github/CONTRIBUTING.md and keep roles' commands/README.md, .abcd/rules.json and internal/core/rules/defaults/rules.json. - .abcd/work/DECISIONS.md: the union driver emitted base's last entry (the build loop's worktree store) twice, because main carries the drainG chain's two entries above it and this branch below it: the residual DA003 names. Rebuilt as main's ledger with this branch's five appended entries after it, so both parents stay intact (DA002) and the two drainG entries, added once by each side, appear twice (bound 2). The guard (drainG chain d833a6f, a9f1676) is on both sides and merges as already merged; the merged guard tree equals this branch's. iss-2609270036253187 ends in resolved/ only. Generation produced no drift. Refs: iss-2609270036253187 Assisted-by: Claude:claude-opus-5-5
Measured on a clean clone of 3d5055c by dry-run assembly; the window is the smallest ten-thousand boundary with at least one per cent headroom. - widening 1,332,620 tokens / 5,130,589 bytes: 1,340,000 -> 1,350,000 - entailment 387,345 / 1,491,280: 390,000 -> 400,000 - detection 1,341,656 / 5,165,377: 1,350,000 -> 1,360,000 Comparative is unchanged. Refs: iss-2609251455354719 Assisted-by: Claude:claude-opus-5-5
Main carries the v0.11.1 release (#737): the cut, its gate receipts, the reference-page currency fix and one DECISIONS entry. No file conflicted. The release files (CHANGELOG.md, RELEASE.md, the marketplace manifest, releases/0.11.0.md) are main's exactly, and `## [Unreleased]` stays empty. The ledger is rebuilt as main's DECISIONS.md followed by this branch's five appended entries, so main's new entry sits before them rather than after. The result removes no line against either parent. The root sentence in cli.go is main's (this branch never changed it); generation produced no drift. Assisted-by: Claude:claude-opus-5-5
Measured by dry-run assembly on a clean clone of the merge commit c72b665. The figures are unchanged from the previous measurement: widening 1,332,620 estimated tokens over 5,130,589 bytes (window 1,350,000), entailment 387,345 over 1,491,280 (400,000), detection 1,341,656 over 5,165,377 (1,360,000). Each window stays the smallest ten-thousand boundary with at least one per cent headroom, so only measured_at moves. Comparative is not re-measured (its figure comes from a planted run and is exempt by name). Refs: iss-2609251455354719 Assisted-by: Claude:claude-opus-5-5
Assisted-by: Claude:claude-opus-5-5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This lands four reviewed lanes as one change. abcd no longer trusts anything it finds behind a symlinked
~/.abcd, andahoy installstops leaving a PATH entry that a plugin update can break. The implement run counts its agents against the ceiling and logs the evidence its report reads. The shell guard reads a variable in a command the way the shell will run it. abcd's own pages name the product thinker or the technical facilitator wherever they wait on a person.drainH + drainHome (ahoy PATH, symlinked home). On a cold plugin cache,
ahoy installwrites no PATH entry. It names the verified install one-liner instead, and it adopts a copy that one-liner already placed. A danglingabcdlink that abcd recorded as its own is abcd's to repair or remove. That includes one earlier on PATH than the working entry, andahoy uninstallremoves it even when no plugin root resolves. Every reader and writer of a file abcd trusts under~/.abcdrefuses a symlinked~/.abcd, and so do the history registry, the hook shims and the install one-liners. A symlinked~/.abcdwith none of those files in it still reads as absent, and a home directory that is itself a link is left alone. Lock files are created owner-only, and an older readable lock is narrowed on its next use.drainI (the implement cluster). The run's state closes two same-uid hygiene gaps.
implement logcounts declared agents against a session's ceiling and refuses the one past it. It also accepts intervention and decision events and requires a stop's cause. The report counts that evidence and names the lines that are missing fields.build --session <id>claims the intent in the shared run. Without a session, the build says it holds no claim, in the text and in the JSON. An append to a store refuses a leaf that is linked in between its checks.drainG3 (guard parameter expansion). The guard reads
$Xand${…}in a command as an unknown word, not as the text it spells. A string's own quotes apply to the variable's value. A here-document body reaches every command its owner pipes into. The carried-value remainder, which covers a pid list, a stream path or shell text held in a variable from an earlier command, is recorded and deferred for a ruling.roles (itd-2609212137129937). abcd's plugin pages, rules, brief, principles, docs and rendered strings name the product thinker (what to build) or the technical facilitator (how). A banned token refuses the retired word over those trees. Every plugin-page question names whose answer it waits on and sets the mode first. This integration also names the role on the citation confirmation (
commands/docs.md) and on three questions added to main after the lane's base.Reviews: drainH FIX FIRST -> fix2 + fix3 (history rewrite for RS004/DA002) -> SHIP. drainHome FIX FIRST -> fix2 DONE (the fix round the review ruled). drainI SHIP -> fix2 DONE (the MEDIUM fixed). drainG3 FIX FIRST -> fix2 -> SHIP. roles FIX FIRST -> fix2 DONE. itd-2609212137129937 is in shipped/ with its fidelity review OWED (receipt rcp-12ccf7627aac).
Re-merge (after #733-#736). Main came in by hunk. The contributing guide lives at
.github/CONTRIBUTING.md, and it now carries the roles lane's dependency sign-off by the product thinker, so no root copy remains. The docs-lint seed test keeps both the role-vocabulary omission and the persona-roster omission. The name_roots fixtures read.github/CONTRIBUTING.mdbeside the role ban's extra roots, andlint docsreports 0 blockers. In the decision log, the union merge wrote the base's last entry twice: main carries the guard chain's two entries above that entry, and this branch carries them below it. That shape is the residual the append gate names. The merged log is main's log with this branch's five entries appended after it. Both sides' logs survive intact, and the two guard entries that each side added appear twice, which is within the gate's bound. The guard chain that integ11 landed is also in this branch, so it merged as already merged, and the merged guard tree is identical to this branch's. iss-2609270036253187 is in resolved/ only. Main added nothing that reads a credential store after the base, and the home-scope link check is unchanged. The intent close path on main takes the store locks around its repoint only: itd-2609212137129937's frontmatter is byte-identical across its close, and it sits in shipped/ with its spec in closed/, once each. Main renders a new OWED stub with a closing line, and it still reads this record's stub as owed on rcp-12ccf7627aac. Re-emitting withintent audit itd-2609212137129937rewrote only the local request and left the record as it was. The five drainG records that integ11 resolved on main have left the Resolves, and so have two Refs that were resolved before the base. Generation produced no drift, the hook copies match main, and the symlinked-home branch of the hook shims is intact. The reading windows were re-measured at the merged tip: widening 1,332,620 tokens (window 1,350,000), entailment 387,345 (400,000) and detection 1,341,656 (1,360,000).Re-merge after v0.11.1 (#737). Main's release came in with no conflict. The changelog, the release notes, the marketplace manifest and the 0.11.0 release record are main's exactly, and
## [Unreleased]stays empty: this branch adds nothing there, so the next cut derives these records itself. The decision log is main's log with this branch's five entries after it, and removes no line against either side. The root help sentence is main's, and generation produced no drift. The reading windows were re-measured on a clean clone of the merge: every figure matches the previous measurement, so the windows hold and only the measured commit moves.Delivers: itd-2609212137129937
Resolves: iss-2609100506263330
Resolves: iss-2609260958587561
Resolves: iss-2609280932480608
Resolves: iss-2609281017573862
Resolves: iss-2609281129171021
Resolves: iss-2609230720193756
Resolves: iss-2609240646542516
Resolves: iss-2609240646544930
Resolves: iss-2609240646549900
Resolves: iss-2609240646555891
Resolves: iss-2609252050506863
Resolves: iss-2609281229109140
Resolves: iss-2609251824244354
Resolves: iss-2609270036253187
Resolves: iss-2609281045487620
Refs: iss-2609281310017733
Refs: iss-2609281134544802
Refs: iss-2609262259360005
Assisted-by: Claude:claude-opus-5-5