Skip to content

feat(items): link and unlink blockers and parent on an existing item - #104

Merged
ryanlindsey merged 2 commits into
mainfrom
issue-98-item-link-adapter
Oct 11, 2026
Merged

ryanlindsey merged 2 commits into
mainfrom
issue-98-item-link-adapter

Conversation

@ryanlindsey

Copy link
Copy Markdown
Owner

Closes #98

The adapter half of item_link. It adds linkItem in server/providers/github/items.ts, which adds and removes blockers on an existing item and sets, replaces or clears its parent. It then brings every canonical Depends on line into step with the resulting blockers, including drift the call did not name. Each phase is verified by its own read-back. This PR only adds the adapter; wiring it into the provider, dispatch, TOOLS, the contract suite and the README is #99.

Stacked on #103 (issue-97-item-update-tool).

What's in it

  • Types LinkChange and LinkedItem in server/providers/types.ts.
  • Documents REMOVE_BLOCKED_BY, SET_PARENT (addSubIssue with replaceParent: true) and REMOVE_SUB_ISSUE.
  • One more document than the plan named: ISSUE_IDENTITY. It resolves a target's node id together with GitHub's own spelling of its owner and repo. That keeps the dry run, the written Depends on line and the read-back on one spelling, so Acme/Web#3 is written as acme/web#3. Without it, the dry-run prediction and the real result disagreed on spelling. PARENT_ID is unchanged, so createItem is unaffected.
  • Errors, one for each end state:
    • ConflictingLinkError: raised before any read.
    • MissingLinkTargetError: raised before any write, dry run included.
    • PartialLinkError: some link writes did not show on read-back. The prose is not touched. Its opening sentence states only what was observed: in part, none confirmed, or links moved while this call sent nothing.
    • ProseOutOfStepError: every link is confirmed but the prose is in doubt.
  • UnclosedFenceError placement: syncDependsOn(before.body, desired) runs before any link mutation, dry run included. A body that can never take a new line is refused with nothing written, so it is never left half-linked. If the fence appears only after the links land, the error is restated as ProseOutOfStepError, because the original error's "nothing was changed" is no longer true.
  • The prose sync is computed from the read taken after the links land, not the first read, so a person's concurrent edit survives.
  • Not version-guarded, as the issue specifies.

Acceptance receipt

  • linkItem adds and removes blockers and sets, replaces and clears a parent, each verified by read-back. Evidence: tests/items-link.test.ts covers add, remove, set-over-existing (SET_PARENT, replaceParent) and clear (REMOVE_SUB_ISSUE, current parent's id). Each test scripts the read-back. The suite is green.
  • After a successful call, the item's canonical Depends on set equals its blockedBy, including blockers the call did not name. Evidence: the "repairs drift on blockers the call did not name" test. The step-9 check raises ProseOutOfStepError otherwise, and a test covers that too.
  • Each partial end state raises its own named error, and a concurrent body edit survives. Evidence: tests for PartialLinkError (missing add, refused write, failed read-back, concurrent blocker change, no-op with moved links), for ProseOutOfStepError (prose mismatch, fence opened since, refused body write, links moved after confirmation), and for the person's edit surviving.
  • All three new documents are in the integration query suite. Evidence: tests/integration/queries.integration.test.ts, run against the live schema with ARMATURE_INTEGRATION=1 ARMATURE_IT_OWNER=ryanlindsey ARMATURE_IT_BOARD=1 ARMATURE_IT_REPO=armature: 13/13 passed. That includes the three new mutations and ISSUE_IDENTITY.
  • dist/server.js rebuilt; npm test and npm run typecheck pass. Evidence: npm run build ran and the bundle is byte-identical, because nothing imports linkItem until feat(item_link): expose blocker and parent changes as a tool #99 wires it in. npm test passed (688, 18 skipped) and npm run typecheck is clean.

Review

One review round and one re-review. No Critical findings. The Important findings were fixed before this PR:

  • PartialLinkError wording overclaimed what had landed.
  • The dry run and the real path reported different spellings.
  • Test gaps, including a test that runs the same change through dry run and the real path and compares the results.

Minor findings left open:

  • The dry run returns the old version, and its blockedBy order may differ from GitHub's.
  • ISSUE_IDENTITY follows the redirect for a transferred issue. Naming such an issue by its old location can send one extra add, which is idempotent and verified by the read-back. An old ref that redirects to the item itself gets past the self-reference check, and GitHub refuses the write, which raises PartialLinkError.
  • The up-front fence check syncs against the whole desired set, so unnamed drift in a fenced-off body refuses even a parent-only change. This is intentional and commented.

Board repairs

none

🤖 Generated with Claude Code

https://claude.ai/code/session_014YX1awC8SkssEro6XoXTaE

@ryanlindsey
ryanlindsey force-pushed the issue-97-item-update-tool branch from 340105e to ac26ee2 Compare October 11, 2026 00:57
Base automatically changed from issue-97-item-update-tool to main October 11, 2026 00:58
ryanlindsey and others added 2 commits October 10, 2026 17:58
linkItem adds and removes blockers and sets, replaces or clears a parent,
verifies the links by read-back, then syncs every canonical Depends on line
to the resulting blockers from the latest body, and verifies again.

dist/server.js is unchanged: nothing imports linkItem until item_link (#99).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ling

Resolve link targets through ISSUE_IDENTITY so dry run, prose and read-back
carry one spelling; open PartialLinkError with only what was observed; never
call a write refused that the read-back shows landed; word a missing current
parent as a removal. Cover the no-op, read-failure and dry-run-parity cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ryanlindsey
ryanlindsey force-pushed the issue-98-item-link-adapter branch from d4cecfc to e8e572c Compare October 11, 2026 00:58
@ryanlindsey
ryanlindsey merged commit 60c05b0 into main Oct 11, 2026
2 checks passed
@ryanlindsey
ryanlindsey deleted the issue-98-item-link-adapter branch October 11, 2026 00:59
ryanlindsey added a commit that referenced this pull request Oct 11, 2026
Closes #99

Stacked on #104 (#98), the `linkItem` adapter. Part
of epic #94.

## What

Wires `linkItem` through the provider and `dispatch` as the `item_link`
tool.

- `BoardProvider.link?(ref, change): Promise<LinkedItem>` is
**optional**, documented like `check?`. Keeping `Depends on` prose in
step is a Markdown-body shape, so an adapter that cannot do that
declines the method. `LinkedItem` now requires `blockedBy`, and
`BoardItem` gains an optional `blockedBy?`, because dispatch logs the
blocker change against `getItem`.
- `GitHubBoardProvider.link` calls `invalidate()` in a `finally`.
- `LinkUnsupportedError` is shaped like `ChecklistUnsupportedError`:
nothing was written; change the links in the tracker itself.
- Tool `item_link` takes `{ ref, addBlockedBy?, removeBlockedBy?,
parent?, clearParent? }`, with only `ref` required. Every argument is
checked before the capability check. `clearParent: true` reaches the
provider as `parent: null`, and `parent` sent together with it is
refused. A call that asks for no change is refused.
- `refList` is extracted and shared with `item_create`'s `blockedBy`.
- Log lines: `Blocked by` and `Parent` are written only when they
changed. `Depends on` is written as a diff (`before` = lines removed,
`after` = lines added). No body text is logged. Every line is marked
under a dry run.
- Contract: `links?` fixture block with three tests. The GitHub contract
fake now handles `removeBlockedBy` and `ISSUE_IDENTITY`.
- README: ten tools, an `item_link` row, and a sentence on changing
links later.
- `dist/server.js` rebuilt.

## Acceptance receipt

- [x] `item_link` is advertised and handled, with every argument checked
before the capability check. Evidence: `tests/index.test.ts` › item_link
› "is advertised with exactly the arguments the handler accepts" and
"validates every argument before checking the capability" (seven
malformed calls against a provider without `link`, none raising
`LinkUnsupportedError`); suite green.
- [x] Clearing a parent uses `clearParent: true`, and `parent` together
with it is refused. Evidence: "hands clearParent to the provider as
parent: null", "names the parent when it is sent with clearParent", and
the "a parent together with clearParent" refusal case; suite green.
- [x] Log lines record blockers, parent and Depends-on changes; dry-run
results are marked. Evidence: "logs the blockers, the parent and the
Depends on lines that changed", "logs a cleared parent", "logs a Depends
on repair even when the links did not change", "marks a dry run, in the
result and in every log line"; all captured via `logWrite`; suite green.
- [x] The contract suite holds an adapter that offers `link` to links
and lines together. Evidence: `tests/contract/github.contract.test.ts` ›
"changing links, if the adapter keeps prose in step" (add, remove,
refuse-missing) passes against `GitHubBoardProvider`.
- [x] README updated; `dist/server.js` rebuilt; `npm test` and `npm run
typecheck` pass. Evidence: README read back (ten tools, `item_link` row
and sentence); `npm run build` followed by `git diff --exit-code
dist/server.js` clean; `npm test` 725 passed / 18 skipped; `npm run
typecheck` clean.

The plan-step checkboxes (Steps 1–6, under no heading) are left alone,
as they are not acceptance criteria.

## Review

One review round: no Critical or Important findings. Minor findings
fixed in the second commit: the `link?` doc now says `getItem` must
report `blockedBy`, a comment explains the `Depends on` log shape, and
the fake's `removeBlockedBy` now matches on owner too.

Open, minor, left for a follow-up: **writes that landed are not logged
when `link` throws** `PartialLinkError` / `ProseOutOfStepError`. Logging
runs only after the provider resolves. `item_create`'s partial errors
behave the same way, so a fix belongs to both tools.

## Board repairs

none

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_014YX1awC8SkssEro6XoXTaE

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ryanlindsey pushed a commit that referenced this pull request Oct 11, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.9.0](armature-v0.8.0...armature-v0.9.0)
(2026-10-11)


### Features

* **item_link:** expose blocker and parent changes as a tool
([#105](#105))
([6d28299](6d28299))
* **item_update:** expose title and body edits as a tool
([#103](#103))
([a9626f0](a9626f0))
* **items:** link and unlink blockers and parent on an existing item
([#104](#104))
([60c05b0](60c05b0))
* **items:** report an item's version and edit its title and body,
guarded ([#102](#102))
([eca5b1e](eca5b1e))
* **skill:** repair defects through item_update and item_link, and
report them ([#106](#106))
([fd8671a](fd8671a))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: ryanlindsey-bot[bot] <324957459+ryanlindsey-bot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(items): link and unlink blockers and parent on an existing item

1 participant