feat(task): task new --adopts and task finish closing the adopted captures (#270) - #294
Conversation
There was a problem hiding this comment.
Reviewer verdict: request changes. M14-R1 is delivered in shape — the flag, the section, the records, the post-merge close, the dry-run lines, the SPEC §4/§6/§10 prose, the contracts, the tests, the record. Two findings against AdoptedRefs, and both are the same failure the implementer's own Decision on #270 says the design exists to prevent: "a body-wide ref scan [would give] a second adoption, and task finish closes what it finds." The head-of-line rule closes that hole on the line; the section boundary leaves it open twice. Both reproduce against the merged branch, both are closed by one line each in internal/tracker/tracker.go, and neither has a test.
Everything else below is confirmation and nits.
1. A ref quoted in code inside ## Adopts is read as an adoption (blocking)
internal/tracker/tracker.go:387 reads the raw body:
for _, m := range adoptedLine.FindAllStringSubmatch(section(body, AdoptsHeading), -1) {Run against the merged branch, with the section as task new writes it plus one code block:
fenced ref inside section -> [o/hub#193 o/hub#42]
indented ref inside section -> [o/hub#193 o/hub#42]
code-span ref -> [o/hub#193] <- span survives, by luck of the regex
#42 is an issue nobody adopted. task finish closes it after the merge, with a comment saying it was "adopted by" the task and "delivered by" the PR, and nothing in closeAdopted can refuse — by design, correctly, because the merge is done. The wrong close is unrecoverable in the record even if a human reopens the issue.
This repo shipped the rule against exactly this two tasks ago. internal/tracker/markdown.go:20 StripCode blanks all three code forms, and its own doc comment states the contract: "One rule, one implementation: the citation walk (milestone evidence) and the verdict scan (ParseVerdicts) both read a comment through it, so a URL or a verdict quoted in code is content in both — the record-reading rule and the citation-reading rule are the same rule (M13-R6)." AdoptedRefs is the third record-reading scan in the package and the only one that does not use it — while being the only one whose output closes other people's issues. #285 and #288 were both spent making this rule right; the new reader should inherit it.
Fix: section(StripCode(body), AdoptsHeading). Test: a - #42 inside a fence and inside a four-column block in the section, neither adopted.
2. Prose containing the literal ## Adopts shadows the real section (blocking)
section (internal/tracker/tracker.go:416) finds the heading by substring, first occurrence:
_, rest, found := strings.Cut(body, heading)So any earlier text carrying the literal ## Adopts becomes the section. Reproduced:
body: "## Goal\nRecords an ## Adopts line, e.g.\n- #999 the shape\n\n## Requirements\n…\n\n## Adopts\n- #193 — the real capture\n\n## Plan\n"
AdoptedRefs -> [o/hub#999]
#193, the capture the coordinator actually named with the flag, is not closed; #999, which nobody adopted, is. In the benign form — earlier prose with no list line carrying a ref — nothing is closed at all and the requirement silently does not happen, with no note in the finish output to say so.
This is not hypothetical for this project: task #270's own Goal contains the string ## Adopts ("task new --adopts records an ## Adopts line and comments on each capture"). I ran AdoptedRefs against #270's live body — it returns [], so this PR's own finish is safe, but only because the shadowed span happens to hold no list line with a ref. Any task about the adoption feature, and any Goal that quotes the heading it is asking for, sits on this.
Fix: anchor the heading at the start of a line — search for "\n"+AdoptsHeading+"\n" (with the start-of-body case), or a line-anchored regexp — rather than a bare substring. The same weakness exists for ## Plan at line 325 and ## Requirements at line 511, where the consequence is only a presence check or an ID list; here it decides what gets closed, so it is worth fixing at least for this caller.
Also worth a line (non-blocking)
-
internal/cli/task.go:519— "read once every gate has passed so a refused finish pays for none of it." Two gates come after it:operator confirmation(internal/cli/task.go:545) andbypass actor(internal/cli/task.go:562). A finish refusedSELF_CONFIRMorCREW_BYPASShas already paid theIssueBodyread and oneTaskread per capture. Reads only, nothing written, so this is a comment to correct rather than code to move. -
resolveAdoptionsfolds "GitHub could not be reached" intoADOPT_NOT_OPEN. SPEC §6 sets the opposite precedent one row away —migrateraisesGH_UNREACHABLE"when the lookup could not reach GitHub at all, never folded into the value's own refusal". The detail does quote the underlying error, so nothing is hidden, and SPEC §10's row documents the choice deliberately; noting it as a protocol-consistency question for the coordinator, not a change I am asking for. -
SPEC.md:233— theBody structurefenced template still shows Goal / Requirements / Plan / Ask-the-human points. The new prose at line 252 says where## Adoptsgoes, but a reader who skims the block will not see it. A commented line in the block would close the gap. -
closeAdoptedprints a note whenMergeCommiterrors, but a successful read returning""(GitHub has not computed it yet, moments after the merge) posts the comment without a SHA and says nothing. Cheap to make the same note.
What I verified
- Build and suites:
go build ./cmd/codecrew,go vet ./...,gofmt -l .,go test ./...all clean on the merged branch (internal/cli,internal/config,internal/gh,internal/trackerok). - The premise behind
MergeCommit, live — this is the highest-value thing in the diff and it holds. PR #290 rebase-merged;gh pr view 290 --json mergeCommitreturns01fb42d787af1b4b161fea4cbdb923a4ad0f4e47, whichgit branch -r --containsputs onorigin/mainas its tip.HeadSHAwould have named a commit on no branch. An unmerged PR (#294) returns null, i.e."", not an error — so the empty-SHA path is real and reachable, which is finding 6. - The refusal, live, as
radiusred-checky[bot]:task new --milestone 14 --adopts 99999999→refused[ADOPT_NOT_OPEN]: … could not be read (…);--adopts 284,193(284 closed) →refused[ADOPT_NOT_OPEN]: … is closed. Highest issue number in the repo before and after both runs: 288. Nothing was created, nothing was linked, nothing was commented — the check really does run beforeCreateIssue. task finish 270 --dry-runas the reviewer seat: gates print in order and stop atrefused[NOT_OWNER], exit 1, nothing written.- The template: with no adoptions
taskTemplate's new%scollapses to the blank line that was there before, so a task that adopts nothing gets byte-identical output;TestAdoptsBlockRoundTripsandTestTaskNewAdoptsWritesTheSectionAndRecordsItpin both directions. - Section interaction: a task body's
## Requirementsnow ends at## Adoptsrather than at## Plan, which is correct forRequirementIDs;PlanPresentstill finds## Planafter it. - Tests read, not trusted by name. They fail without the change:
TestTaskNewRefusesAnAdoptionThatIsNotOpenassertscreated,linked,postedand stdout are all empty on both refusal shapes;TestPlanFinishClosesTheAdoptedCapturesasserts the exact write ordermerge,close o/r#193,delete task/7-x— the ordering requirement, pinned as a sequence rather than as prose — and that planning wrote nothing;TestAdoptedRefspins the head-of-line rule with a#42inside a title and a#999in prose outside the section. What no test pins is the section boundary itself, which is findings 1 and 2. - Docs sweep:
git grepforadopt,capture,Closes #andby handacrossSPEC.md,docs/,.codecrew/roles/,README.md,CONTRIBUTING.mdfinds no sentence left describing the old convention. The implementer contract'sCloses #123at line 96 is the task's own number and stays right.TestRefusalCodesMatchTheSpecTableenforces theADOPT_NOT_OPENrow and the forty-two → forty-three count, and passes. - Record: plan edited onto #270 at 17:03:02Z, start record 17:03:08Z, three Decision comments at 17:07:5xZ, first commit 17:08:23Z — plan and Decisions both before the first commit, Decisions as comments and not only in the PR body. Commit subjects are conventional, lowercase after the type, 81/83/94 chars, each carrying
(#270).Closes #270andCloses #193are the last lines of the PR body, in that order. CHANGELOG entry is under[Unreleased]and ends(#270, closing #193), which matches the file's existing forms. - Rebase:
origin/mainis at01fb42dand neither #271 (PR #292) nor #273 (PR #293) has merged, so there is no earlier work for this branch to have dropped. Both of those touchtask finishand the branch sweep, so whichever merges second will need the rebase — worth the coordinator's sequencing, not a finding here.
Fix 1 and 2 with a test each and I will approve on the next round.
…, the merge commit (#270) A task adopts backlog captures, and the protocol needs to write that link down and read it back. AdoptsBlock renders the ## Adopts section a task body carries — the ref short in the task's own repo, in full anywhere else, the capture's title after it for the reader — and AdoptedRefs reads it back, taking only the ref at the head of a list line so a #N inside a title is never a second adoption. AdoptionRecord and AdoptionClose are the two comments the verbs post on a capture: adopted by a task, and closed by its merge. MergeCommit joins the backend interface for the second of those: the closing comment names the commit the merge left on the default branch, and a rebase merge rewrites the head, so PR.HeadSHA is not it. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
…adopted (#270) A backlog capture a milestone adopts stayed open after its task shipped unless a brief remembered a Closes line — every hub milestone since M3 paid for that by hand, and topos M3 is where it was captured (#193). task new --adopts <ref>[,<ref>] names the captures a task takes up: repeatable, comma-separated, a bare number resolving against the task's own repo. Every ref must be an open issue, checked before anything is created — refused[ADOPT_NOT_OPEN] for one that cannot be read as much as for one already closed — so a refusal leaves no half-adopted task. The refs go into the body's ## Adopts section and each capture gets a comment naming the task; a comment that fails once the task exists is a note, the body carrying the link either way. task finish closes each of them after the merge, with a comment naming the task, the PR and the commit the merge left. planAdoptions decides each capture's fate before anything is written, the way the milestone close's branch sweep does, so --dry-run lists what the live run does. Nothing in the step refuses: the merge is done, so an already-closed capture is reported and one that cannot be closed is a note. SPEC §4 says what adoption means, §6's two rows say what the verbs do, and §10 carries ADOPT_NOT_OPEN. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
… hand (#270) The coordinator's contract gains the bullet: a milestone adopts a capture with task new --adopts, not with "adopts #N" written into a Goal, and an adopted capture is never closed by hand. The implementer's says the task's own number is the only one that needs a closing keyword — a capture under ## Adopts is closed by task finish, so no Closes line for it. CONTRIBUTING's capture bullet names the flag, and the changelog carries the entry. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
… code is content (#270) Two ways AdoptedRefs handed task finish an issue nobody adopted, both found in review, both closing an issue after a merge where nothing can refuse. It read the raw body, so a `- #42` inside a fenced or four-column block in the section was an adoption. It now reads through StripCode, the rule the verdict scan and the citation walk already share (M13-R6): a ref quoted in code is content, in the third record-reading scan as in the other two. And it found its heading by substring, so any earlier prose carrying the literal "## Adopts" shadowed the real section — #270's own Goal carries it. The heading is now matched as a heading, at the head of a line and exact, and the section runs to the next heading of level 1 or 2. section() keeps the substring for PlanPresent and RequirementIDs, where a miss costs a presence check or an ID list rather than a wrong close. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
…erge commit as a note (#270) Three review notes on the adoption step. --adopts folded "GitHub could not be reached" into ADOPT_NOT_OPEN, where SPEC §6 sets the opposite precedent one row away: it now classifies through the shared helper and raises GH_UNREACHABLE, since a dead network is not a verdict on a ref. closeAdopted noted a merge commit it could not read but posted a SHA-less comment in silence when GitHub returned one that was empty — which it does for a PR merged moments earlier; both are the same note now. And planAdoptions moves past the last two gates, where the comment claiming every gate has passed is true. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
… read by (#270) SPEC §4's body-structure block shows the section, so a reader who skims it sees where adoption lives; the prose beside it now states the three rules the reader enforces — the heading is a heading, only the ref at the head of a list line counts, and a ref quoted in code is content. §10's GH_UNREACHABLE row names task new. The changelog carries the same. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
6b3f4a4 to
24280e8
Compare
|
Both blocking findings are real and both are fixed, with a test each that fails on the old code. New head 1 — a ref quoted in code. 2 — the shadowed heading. I left 3 — The rebase over #271 touched two lines both of us wrote: SPEC §6's SPEC §4 also states the three rules the reader enforces now, so the strictness is protocol rather than an implementation detail: the heading is a heading, only the ref at the head of a list line counts, and a ref quoted in code is content. |
There was a problem hiding this comment.
Reviewer verdict: approve. Both blocking findings from round one (review 5126049014) are fixed, each with a test that fails without the fix — I confirmed that by mutation, not by reading. All four non-blocking notes were taken, and each one is right. The rebase onto 8a1e5ce kept #292's CHANGELOG section and the refusal table is consistent. M14-R1 is delivered: the flag, the section, the records, the post-merge close, the dry-run lines, SPEC §4/§6/§10, both contracts, the tests and the record.
One new note below, non-blocking, and a nit. Neither blocks the merge.
The two blocking fixes, mutation-verified
1. StripCode (finding 1). internal/tracker/tracker.go:395 now reads adoptsSection(StripCode(body)). Reverting that one call to adoptsSection(body) on the branch:
--- FAIL: TestAdoptedRefsIgnoresRefsQuotedInCode
fenced: AdoptedRefs = [o/hub#193 o/hub#42], want [o/hub#193]
tilde-fenced: AdoptedRefs = [o/hub#193 o/hub#42], want [o/hub#193]
indented: AdoptedRefs = [o/hub#193 o/hub#42], want [o/hub#193]
The test pins all four forms; the span case passes either way (the regex's [-*][ \t]+ never reached the backticked ref), which is fine — it documents the rule rather than proving it, and the three block forms prove it. AdoptedRefs is now the third record-reading scan in the package on the same rule as the verdict scan and the citation walk, which is what M13-R6 asked for.
2. The heading is a heading (finding 2). adoptsSection (internal/tracker/tracker.go:422) anchors on adoptsHeadingLine and ends at sectionEnd. Restoring the old strings.Cut(body, AdoptsHeading) in its place, keeping everything else:
--- FAIL: TestAdoptedRefsFindsTheHeadingNotTheQuotedString
AdoptedRefs = [o/hub#999], want [o/hub#193]
Exactly the shadowing case, and the second half of that test — ### A note inside the section does not end it, # Appendix does — pins the boundary in both directions. The Decision comment on #270 at 17:22:01Z explains why section() is left alone for PlanPresent and RequirementIDs, and the reasoning holds: those two callers pay a presence check and an ID list, this one closes other people's issues after a merge that cannot refuse. Scoping the strict reader to the caller that pays for it is the right call, and it is on the record.
AdoptedRefs against #270's live body
I ran it against the issue body as GitHub serves it today, with defaultRepo = the hub:
AdoptedRefs(#270 body) = []
adoptsSection(StripCode(body)) = ""
A correction to the round-two premise: it returns nothing, and nothing is what it should return. #270 has no ## Adopts section — it was opened before the flag existed, and its own Goal says so ("Closes #193 with the PR — the last capture closed by hand"). What the run does prove is that the fix holds on the real body that motivated it: the Goal's inline ## Adopts (line 2, mid-sentence) no longer opens a section, and the four ## Adopts occurrences in the Plan are inside code spans that StripCode removes. So task finish 270 will close no capture by adoption, #193 closes through the PR's own Closes #193, and the PR body says plainly that it is the last one closed that way. Honest and self-consistent.
The four notes
- The gate comment —
internal/cli/task.go:576now reads "read once every gate has passed — the last two of them depend on the flags, so this comes after them". Correct as written:planAdoptionsat line 579 sits after theoperator confirmationandbypass actorgates, which were the two the old comment was wrong about. The claim and the code agree. GH_UNREACHABLE—internal/cli/task.go:119routes through the sharedunreachable()helper (internal/cli/context.go:213), the same onemigrateandroles showuse, so the code, the wording and the SPEC §10 row stay in one place.TestTaskNewAdoptsRefusesUnreachableAsItselfpins it and asserts nothing was created. SPEC'sGH_UNREACHABLErow now liststask new.- SPEC §4's template — the body block shows
## Adoptswith a one-line gloss, and the new paragraph after it states the three strict-reading rules (heading, head-of-line ref, code). A reader who skims the block now sees the section. - An empty merge commit —
internal/cli/task.go:670notessha == ""with the same line a failed read gets, andTestCloseAdoptedNotesAMergeCommitItCannotNameasserts both the note and that the comment carries no "merged as".
New, non-blocking: a CRLF task body makes the section invisible
internal/tracker/tracker.go:411:
adoptsHeadingLine = regexp.MustCompile(`(?m)^ {0,3}` + AdoptsHeading + `[ \t]*$`)Go's $ under (?m) matches before \n only, and \r is in neither [ \t] nor the anchor. So on a body with CRLF line endings the heading does not match, adoptsSection returns "", and task finish closes nothing and says nothing. Measured on the branch:
lf synthetic ("## Adopts\n- #193 — cap\n…") -> [o/hub#193]
crlf synthetic ("## Adopts\r\n- #193 — cap\r\n…") -> []
GitHub's web form submits CRLF, so this is reachable in the field rather than theoretical: no issue body in this repo's last 100 carries a \r (everything here is written by gh), but 9/100 in cli/cli, 4/100 in golang/go and 11/100 in microsoft/vscode do. The path that matters for a framework other people run: task new --adopts writes the body with \n, then someone edits that body in the web UI — to write the Plan, to fix a typo in the Goal — and GitHub rewrites the whole body as CRLF. The adoptions then silently stop being closed, which is #193 again.
Two reasons I am not blocking on it. It fails closed, not open: nothing wrong is closed, the finish output simply carries no closed #N (adopted) line, unlike the round-one findings which closed issues nobody adopted. And it is a pre-existing assumption of the package rather than something this PR introduced — paragraphBreak at internal/tracker/tracker.go:497 (\n[ \t]*\n) has the identical blind spot for record parsing, so the codebase already reads LF bodies throughout.
The local fix is one character ([ \t\r]*$) plus a CRLF case in TestAdoptedRefsFindsTheHeadingNotTheQuotedString. The better fix is probably a body-wide normalisation where GitHub text enters the package, which would cover paragraphBreak too — coordinator's call, and a backlog capture rather than a change to this PR.
Nit
closeAdopted (internal/cli/task.go:670) reads the merge commit whenever items is non-empty, including when every item is already closed. In that case a failed or empty read prints "the captures below are closed without naming it" above a list in which none are. Costs one API call and a slightly odd line; not worth a round.
The PR description does not mention either round-two fix — the CHANGELOG's "The section is read strictly" bullet and SPEC §4 both do, so the record is complete, but the description is meant to be the task summary.
What I verified
- Suites, on the branch head
24280e8:go build ./cmd/codecrew,go vet ./...,gofmt -l .,go test ./...all clean (internal/cli,internal/config,internal/gh,internal/trackerok). Both required checks green on the head commit: "Go build and test", "Lint commit messages". - Mutation, both fixes, as above — each reverted in isolation, the named test fails, the tree restored and the suite re-run green.
AdoptedRefson #270's live body, as above, plus the CRLF variants.- Rebase: merge-base is
8a1e5ce, which isorigin/main's tip right now, so the branch is current.CHANGELOG.mdis32/0added/deleted — #292's "task finishtidies the clone it ran in" section is intact and nothing else was dropped. #291 and #293 are still open, so no other work could have been lost. - Refusal count: the §10 table holds 43 rows against
main's 42, and the count sentence reads "Forty-three" againstmain's "Forty-two".ADOPT_NOT_OPENis the only code added;TestRefusalCodesMatchTheSpecTablepasses. - The verb, executed:
task finish 270 --dry-runas the reviewer seat prints the gates in order and stops atrefused[NOT_OWNER], exit 1, nothing written;--helpshows the--adoptsrow and the amendedtask finishline. - Template:
taskTemplate's new%ssits on its own line, so an emptyAdoptsBlockcollapses to the blank line that was there before — a task that adopts nothing gets a byte-identical body, pinned in both directions byTestAdoptsBlockRoundTripsandTestTaskNewAdoptsWritesTheSectionAndRecordsIt. - Tests read, not trusted by name:
TestTaskNewAdoptsWritesTheSectionAndRecordsItasserts the written section round-trips throughAdoptedRefsand pins the exact comment bodies;TestTaskNewRefusesAnAdoptionThatIsNotOpenassertscreated,linked,postedand stdout are all empty on both refusal shapes;TestPlanFinishClosesTheAdoptedCapturespins the write ordermerge,close o/r#193,delete task/7-xand that planning wrote nothing;TestPlanFinishWithoutAdoptionsClosesNothingasserts no merge-commit read when nothing was adopted. - Docs sweep:
git grepforadopt,captureandtask newacrossSPEC.md,docs/,.codecrew/roles/,README.mdandCONTRIBUTING.mdfinds no sentence left describing the old convention. - Record: plan on #270 before the first commit; Decisions as comments including the round-two one at 17:22:01Z;
Closes #270andCloses #193in the PR body; CHANGELOG under[Unreleased]ending(#270, closing #193). Six commits, conventional, lowercase after the type, 81–97 chars, each carrying(#270), all authored asradiusred-cody[bot]. Nocc:needs-decisionon #270. The harness Deviation covering this Claude-run review is recorded on #269. - Identity: minted as
radiusred-checky, App 4719924, matchingGET /apps/radiusred-checky; distinct from the PR's authorradiusred-cody[bot].
M14-R1 as #269 words it is satisfied. Approving.
M14-R1: backlog adoption becomes a first-class link, so the protocol does the bookkeeping it already knows how to do.
A milestone adopts a backlog capture, a task delivers it, and the capture stays open — unless a brief remembers a
Closesline. That has been paid by hand at every hub milestone since M3 and was captured from davison/topos M3 as #193. This PR makes the link something the verbs hold.task new --adopts <ref>[,<ref>]— repeatable and comma-separated, a bare number resolving against the task's own repo andowner/repo#nnaming a capture anywhere. Every ref must be an open issue and is checked before anything is created, so a refusal leaves no half-adopted task:refused[ADOPT_NOT_OPEN]covers both ways a ref is not one — it cannot be read, or it is already closed. The refs go into an## Adoptssection of the task body, between Requirements and Plan, each line carrying the capture's title for the reader; each capture then gets a comment naming the task. Duplicates collapse; a comment that fails once the task exists is anote:, since the body carries the link either way.task finishcloses each adopted capture after the merge, with a comment naming the task, the pull request and the commit the merge left on the default branch.planAdoptionsdecides each capture's fate before anything is written — the same plan/execute split the milestone close's branch sweep uses — so--dry-runprintswould close <ref> (adopted)andwould skip <ref>: already closed (adopted)beside the branch it would delete, and writes nothing. Nothing in the step can refuse: the merge is done, so an already-closed capture is reported as a note and one that cannot be closed is a note naming it.Only the ref at the head of a list line in
## Adoptsis an adoption; the prose after it is the capture's title. A capture titled "fix #42 in the parser" must not handtask finisha second issue to close.TrackergainsMergeCommit(repo, number)(gh pr view --json mergeCommit): a rebase merge rewrites the head, soPR.HeadSHAis not the commit the merge left, and the back-reference should point at one that exists. It is read once per finish, only when the task adopted something, and a failed read is a note with the comment posted without the SHA.Requirements: M14-R1.
Docs: SPEC §4 says what adoption means, §6's
task newandtask finishrows say what the verbs do, §10 carries theADOPT_NOT_OPENrow and the count sentence it states once (forty-two → forty-three, whichinternal/cli/refusals_test.goenforces)..codecrew/roles/coordinator.mdsays a milestone adopts with the flag, not with "adopts #N" in a Goal, and never closes an adopted capture by hand;.codecrew/roles/implementer.mdsays the task's own number is the only one that needs a closing keyword.CONTRIBUTING.md's capture bullet names the flag;CHANGELOG.mdcarries the entry.Tests (fake tracker throughout):
task newwrites the section and posts the comments, over repeated and comma-separated values, a cross-repo ref and a duplicate, and the section it writes is the sectionAdoptedRefsreads back; an unknown ref and a closed one each refuseADOPT_NOT_OPENwith nothing created.AdoptedRefscovers the head-of-line rule, a#Ninside a title, refs outside the section, and the round trip withAdoptsBlock.task finishlists both shapes under--dry-runand writes nothing, then closes the open capture with a comment naming task, PR and merge SHA and reports the already-closed one as a note; a task that adopts nothing closes nothing and reads no merge commit.go test ./...,go vet ./...andgofmt -l .are clean.Decisions are recorded on #270: the
MergeCommitaddition and what it rejected, the head-of-line parsing rule, and the plan-before-the-merge split with a failed capture read planned as a close.Closes #270
Closes #193 — the last capture closed by the PR-body convention. From the next task that adopts one,
task new --adoptsrecords it andtask finishcloses it.🤖 Generated with Claude Code
https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr