Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Decision: item visibility (board membership + assignee) is a gate-enforced PM-tool adapter contract, not a skill rule

## Date

2026-07-31

## Status

Active

## Category

Convention Adoption

## Context

`way-of-working.md` › Assignment requires skills to set an item's assignee "as part of the write, never as a follow-up step". Nothing implemented it: `assignee` appeared zero times in `pair-capability-write-issue/SKILL.md` and zero times across every PM-tool implementation guide. Separately, `/write-issue` Step 7.3 writes the board status field "per the implementation guide", presuming the item was already in the tracked view — on GitHub Projects an issue and a project item are distinct objects, so the item lookup could yield an empty id and either fail hard (`gh: Could not resolve to a node with the global id of ''`) or, worse, be treated as "board write skipped" and reported as success.

Observed 2026-07-30: issues #384 and #372 were open, assigned, green, and absent from the board entirely — invisible in the assignee-filtered view the team reads. No gate caught it. Two questions had to be settled before writing anything: **where** the fix lives, and **how** it stays true for adapters that do not exist yet.

## Decision

1. **The coverage lives in the adapters (`*-implementation.md`), not in the skills.** Each adapter carries a level-3 section `### Item Visibility: Membership and Assignee` stating (a) its board-membership semantics in the pinned form `Board membership is explicit …` / `Board membership is implicit …`, (b) the concrete assignee mechanic set as part of the create, (c) the unresolvable-assignee behaviour (report it, never drop it silently), and (d) for explicit-membership tools, that membership precedes the state write with an explicit branch for an empty lookup — never a silently skipped board write reported as success.

2. **The contract is a stated, gate-enforced requirement**, recorded in `project-management-tool/README.md` › _Adapter Contract — Required Coverage_ and enforced by `packages/knowledge-hub/src/conformance/pm-tool-adapter-contract.test.ts`. The guard is data-driven over the `*-implementation.md` files present in both corpora, so a new adapter is enrolled the moment its file lands and **no adapter count is asserted anywhere**. An omission reddens CI instead of surfacing in production.

3. **The assignee stays out of the shared item templates.** `user-story-template.md` / `task-template.md` gain no `Assignee` slot: on every adapter but one the assignee is a native tracker field (GitHub `assignees`, Azure `--assigned-to`, Linear `assigneeId`), where a template slot would be dead weight and a second source of truth competing with the field the board filters on. The filesystem adapter — which has no native field — owns the convention itself and states the placement rule in its own visibility section.

## Alternatives Considered

- **A generic "add the item to the board first" rule in `/write-issue` (skill layer)**: Rejected. Membership-as-a-separate-step is **GitHub-specific**. On Azure Boards the work item belongs to its team project on create, on Linear `issueCreate` requires `teamId`, and in the filesystem adapter the file's location _is_ its membership — so the generic rule would be false for three adapters out of four, and would invite an agent to invent an `addProjectV2ItemById` equivalent that those tools have no concept of. It is also unnecessary: `/write-issue` Step 7.2 already delegates field mechanics to the guide, so a corrected guide is picked up through the existing delegation with **no skill edit**. This constrains #403: the skill half is limited to the tool-agnostic invariant (`$assignee` in the write contract, membership-precedes-state as a precondition, HALT on an unresolvable item) and never to a tool-shaped step.
- **Prose only, no conformance guard**: Rejected. The gap being closed existed _because_ an adoption rule had no enforcement — repeating that shape for the adapters would have re-created it one layer down.
- **A count-based conformance assertion** ("all four adapters carry the section"): Rejected. It reddens on adapter _addition_ rather than on adapter _omission_, and hardcodes a number that goes stale; disk discovery plus a non-empty guard gives the same protection without the count.
- **Adding an `Assignee` field to the shared templates**: Rejected, per decision 3 above.

## Consequences

- Adding a PM-tool adapter now requires its `Item Visibility: Membership and Assignee` section, at level 3, with the pinned sentence form — otherwise `pm-tool-adapter-contract.test.ts` fails.
- Adapters whose semantics the guard knows are pinned **by name** (`github` explicit; `azure-devops`, `linear`, `filesystem` implicit), so a regression that flips one cannot pass on the loose either-word assertion.
- `gh issue create` recipes are guarded KB-wide for `--assignee`, so the issue-management guides cannot drift back into a second, incomplete create recipe that contradicts the adapter.
- Implicit membership is documented as _not_ equivalent to "cannot be invisible": Azure's area path and, when the adoption names a project view, Linear's `projectId` remain part of visibility. Contract clause 5 makes that a **requirement** — an adapter declaring implicit membership must name the field that decides the read view — and the guard is table-driven, so a new implicit adapter has to be enrolled deliberately rather than inheriting silence.
- **Azure inlines `--area` in its create snippet; Linear does not inline `projectId`. Deliberate, not an oversight.** The two fields differ in conditionality: a team's board on Azure always reads configured area paths, so the flag belongs in the recipe unconditionally, whereas a Linear project view exists only when the adoption names one — inlining `projectId` would teach an agent to send a project id on every create, including the (common) case of a team-scoped board where there is none. Linear's snippet therefore carries the field in a comment with its condition, and the prose states it; the guard requires the field to be **named in the visibility section**, not present in the recipe, for exactly this reason.

## Adoption Impact

- [project-management-tool/README.md](../../knowledge/guidelines/collaboration/project-management-tool/README.md): the Adapter Contract section references this ADL by path for the "why the adapter, not the skill" rationale.
- No dataset mirror of this file: sibling ADLs in `adoption/decision-log/` are adoption-only records — the dataset is a curated seed, not an auto-mirror of adoption, and a link into it from the shipped KB would break in a freshly seeded install (which is why the README cites the path rather than linking it).
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ Inherited processes with custom type names override this table in `way-of-workin

## Creating Work Items

> **The authoritative create recipe is the adapter's.** Every create below carries `--assigned-to`, because a work item without an assignee is open, green — and invisible in the assignee-filtered board view the team reads. The full mechanics (membership is implicit here, but the **area path** still decides whether the team's board shows the item; what to do when the assignee cannot be resolved) live in [azure-devops-implementation.md → Item Visibility: Membership and Assignee](../project-management-tool/azure-devops-implementation.md#item-visibility-membership-and-assignee). The snippets here are abbreviations of it, never an alternative to it.

### User Story (PBI)

```bash
az boards work-item create \
--type "Product Backlog Item" \
--title "[Story title]" \
--description "[Body following the user-story-template]" \
--assigned-to "[user@example.com]" \
--area "[team area path]" \
--project <project>
```

Expand All @@ -41,6 +45,8 @@ az boards work-item create \
--type "Task" \
--title "[Task title]" \
--description "[Body following the task-template]" \
--assigned-to "[user@example.com]" \
--area "[team area path]" \
--project <project>
```

Expand All @@ -51,6 +57,8 @@ az boards work-item create \
--type "Bug" \
--title "[Bug title]" \
--description "[Reproduction steps, expected vs actual]" \
--assigned-to "[user@example.com]" \
--area "[team area path]" \
--project <project>
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ GitHub Issues workflow and configuration for comprehensive issue management inte

This guide covers GitHub Issues setup and workflows for managing user stories, tasks, bugs, and feature requests within the pair development framework.

> **The authoritative create recipe is the adapter's.** Every `gh issue create` below carries `--assignee` and `--project` because an issue without them is open, green — and invisible in the board and assignee-filtered views the team reads. The full mechanics (MCP has no project field, so the add-item step is mandatory after it; what to do when the assignee cannot be resolved) live in [github-implementation.md → Item Visibility: Membership and Assignee](../project-management-tool/github-implementation.md#item-visibility-membership-and-assignee). The snippets here are abbreviations of it, never an alternative to it.

## Issue Types and Labels

### Type Labels (Always Recommended)
Expand Down Expand Up @@ -51,7 +53,7 @@ pair "Create a new user story: 'As a user, I want to [functionality] so that [be
#### Via GitHub CLI:

```bash
gh issue create --title "User Story: [Title]" --body "[Description]" --label "user story,P1"
gh issue create --title "User Story: [Title]" --body "[Description]" --label "user story,P1" --assignee "[login]" --project "[project title]"
```

### User Story Template
Expand Down Expand Up @@ -99,7 +101,7 @@ pair "Break down user story #[story_number] into development tasks and create Gi
#### Manual Task Creation:

```bash
gh issue create --title "Task: [Title]" --body "[Description]" --label "task,P1" --milestone "[Sprint]"
gh issue create --title "Task: [Title]" --body "[Description]" --label "task,P1" --milestone "[Sprint]" --assignee "[login]" --project "[project title]"
```

### Task Template
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,23 @@ Labels are **per team**: resolve label ids for the adopted team before using the

All examples use the GraphQL API through the **`linear_gql` helper** defined once in [linear-implementation.md → GraphQL API setup](../project-management-tool/linear-implementation.md#quick-setup) — endpoint and credentials live there, and the key is never passed inline on the command line. Define the helper before running anything below. With MCP, call the server's equivalent tool with the same arguments.

> **The authoritative create recipe is the adapter's.** Every `issueCreate` below carries `assigneeId`, because an issue without an assignee is open, green — and invisible in the assignee-filtered view the team reads. The full mechanics (resolving the user id, membership is implicit via `teamId`, when `projectId` becomes part of visibility, what to do when the assignee cannot be resolved) live in [linear-implementation.md → Item Visibility: Membership and Assignee](../project-management-tool/linear-implementation.md#item-visibility-membership-and-assignee). The snippets here are abbreviations of it, never an alternative to it.

### User Story

```bash
linear_gql '{"query":"mutation($i:IssueCreateInput!){ issueCreate(input:$i){ success issue { id identifier url } } }",
"variables":{"i":{"teamId":"<team-id>","title":"[Story title]",
"description":"[Story body — markdown per user-story-template]",
"assigneeId":"<user-id>",
"parentId":"<epic-id>","labelIds":["<user-story-label-id>"],
"estimate":5,"priority":2}}}'
```

`issue.identifier` (e.g. `ENG-412`) is the item id pair uses in commits, branch names, and the PR's `Refs:` line.

Add `"projectId":"<project-id>"` to the same `input` **only when [way-of-working.md](../../../../adoption/tech/way-of-working.md) names a project view** — on Linear membership comes from the required `teamId`, so a project is a grouping inside the team, not what makes the issue visible. JSON carries no comments, which is why the condition is stated here rather than inside the snippet.

### Epic

Same mutation without `parentId`, labelled `epic`. Attach it to a Project via `projectId` when initiatives are tracked as Projects.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,24 @@ All implementation guides include:
- ✅ Team collaboration patterns
- ✅ Development workflow integration

### Adapter Contract — Required Coverage

Every implementation guide in this directory is an **adapter**: skills stay tool-agnostic and delegate tool-specific mechanics here. An adapter that omits one of the following is incomplete, and the omission surfaces as an item that exists, is open, is green — and is invisible to the team.

Why this coverage lives in the **adapters** and not in the skills, and why the contract is gate-enforced rather than advisory, is recorded as an ADL in the adopting project's decision log — in this repository, `.pair/adoption/decision-log/2026-07-31-pm-adapter-visibility-contract.md`. (Referenced as a path, not a link: the decision log is per-project `add`-behaviour content, so the file does not exist in a freshly seeded install.)

**Membership and assignee are independent, and both are required for visibility** — assigned but not a member is invisible on the board; a member but unassigned is invisible in the assignee-filtered view the team reads. Fixing one does not fix the other.

Every adapter therefore carries a **level-3** section headed **`### Item Visibility: Membership and Assignee`** — level 3 exactly, because the conformance guard locates the section by that heading level and reads only its body. Documenting:

1. **Board membership semantics** — whether membership in the tracked view is **explicit** (a separate call, e.g. GitHub Projects' `addProjectV2ItemById`, because an issue and a project item are distinct objects) or **implicit** (creating the item is its membership, e.g. Azure Boards' team project, or the file's location in a filesystem backlog). State it either way, in the words `membership is explicit` / `membership is implicit`: **silence is what makes an agent invent an add-item step that does not exist for that tool**, or skip one that does. Write it in the pinned sentence form — **`Board membership is explicit …`** or **`Board membership is implicit …`** — the `Board` prefix included: the guard pins that whole phrase for every adapter whose semantics it knows, so `membership is implicit` without it reddens the gate.
2. **The assignee mechanic** — the concrete field/flag that sets the assignee `as part of the create, never as a follow-up step`, per the Assignment rule in the project's `way-of-working.md`.
3. **The unresolvable-assignee behaviour** — `if the assignee cannot be resolved`, report it and `never drop it silently`. Dropping it reproduces the invisibility the section exists to remove.
4. **The status-write precondition** — for explicit-membership tools, that membership precedes the state write, plus what happens when the item lookup returns nothing (an explicit branch, never an unhandled empty value and never a silently skipped board write reported as success).
5. **Implicit membership that can still miss the read view** — "implicit" answers *how* membership happens, not *whether the item is visible*. Where creating the item makes it a member and it can still land outside the view the team reads (Azure Boards: an **area path** outside the team's configured areas; Linear: a project-scoped view the issue was never added to), name the **field** that decides it AND carry that field on the create recipe, not only in prose. An adapter that stops at "membership is implicit" hands an agent a create that satisfies the contract and produces an invisible item — the exact failure the contract exists to prevent.

Conformance coverage is **data-driven over the `*-implementation.md` files present**, in both the dataset and the generated root mirror — see `packages/knowledge-hub/src/conformance/pm-tool-adapter-contract.test.ts`. Adding an adapter therefore enrols it in the contract automatically; **no adapter count is asserted anywhere**, and an omission fails the gate instead of being discovered in production.

## Tool Selection Decision Framework

### Decision Matrix
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,17 +148,34 @@ Azure Boards states differ per process and per team board columns. Skills resolv

## Working with Work Items

### Item Visibility: Membership and Assignee

**Board membership is implicit on Azure Boards.** A work item belongs to its team project the moment it is created (`--project`), and the board is a view over the project's work items — there is **no separate add-to-board call**. Do not invent one: unlike GitHub Projects, where an issue and a project item are distinct objects requiring an explicit `addProjectV2ItemById`, here creating the work item **is** its membership. Area path and iteration path refine _which_ board view shows it, they do not grant membership.

**Implicit membership is not the same as "cannot be invisible."** A work item whose **area path** falls outside the area paths configured for the team (Project settings → Team configuration → Areas) is genuinely absent from that team's board — created, assigned, and invisible in the view the team actually reads. So pass `--area` on the create with a path the team's board covers, taking the team named in [way-of-working.md](../../../../adoption/tech/way-of-working.md). This is the Azure analogue of the GitHub membership defect: the field write itself cannot fail, but the item can still land outside the read view.

**The assignee is not implicit** and is still required — the board is read filtered by assignee (Assignment rule in [way-of-working.md](../../../../adoption/tech/way-of-working.md)). Set it via `--assigned-to` as part of the create, never as a follow-up step. **If the assignee cannot be resolved** (not a project member, AAD/licensing restriction): **report it** — never drop it silently, which leaves the item invisible in the filtered view.

### Create

```bash
# Create a user story (PBI in Scrum process)
# Create a user story (PBI in Scrum process) — assignee set as part of the create
# --area: the team's board reads only its configured area paths, so an item
# outside them is created, assigned, and absent from that board.
az boards work-item create \
--type "Product Backlog Item" \
--title "[Story title]" \
--description "[Story body — markdown per user-story-template]" \
--assigned-to "[user@example.com]" \
--area "[team area path]" \
--project <project>

# Existing work item: set or correct the assignee
az boards work-item update --id [id] --assigned-to "[user@example.com]"
```

Azure Repos pull requests carry a creator and reviewers, **not an assignee field** — so the assignee lives on the linked work item. Link the PR with `--work-items` (see the PR recipe below) so the assignee-filtered board view reaches the PR through its work item.

### Link Parent-Child

```bash
Expand Down
Loading
Loading