From 7369728a58fe3d1f13673cffd62db8e0e356849a Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 11 Nov 2026 18:04:13 -0500 Subject: [PATCH 1/3] refactor(work-items)!: drop the .work-item-tracker.json discovery fallback The tracker binding is now only the tracker mapping of /work-items.yaml. Every reader stops falling back to a repo-root .work-item-tracker.json: - work-items seam (lib/binding.sh): wit_find_binding fails after the YAML check, and the legacy notice is gone. wit_no_binding_reason says why no binding resolved and names /work-items:setup apply; the dispatcher, the jira/gitea/linear seeds and the onboard-adapter template print it with exit 3. - source-control babysit (babysit_tracker_binding.py): YAML only; PATHS holds one file. - autonomy prerequisite resolver: the tracker need is absent without the YAML mapping, with a detail naming the fix. - loop-lane prompt and contract shell read only the YAML. Kept per the Q316 hub decisions: migrate-binding.sh and setup's copy step, the WORK_ITEM_TRACKER_BINDING env override's JSON acceptance, and the merge-holds non-yaml branch with the workflows' temp file. BREAKING CHANGE: a repository bound only by .work-item-tracker.json gets exit 3 from every seam verb. Run /work-items:setup apply to copy it into /work-items.yaml. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VQb9Pz6CBRwjqpnFDbP9Rr --- ...-work-items-drop-json-fallback-6632485e.md | 11 +++ ...-work-items-drop-json-fallback-3413e338.md | 7 ++ ...-work-items-drop-json-fallback-a72963b9.md | 11 +++ ...-work-items-drop-json-fallback-59140d30.md | 14 +++ docs/contracts/config-cascade/README.md | 2 +- docs/contracts/loop-lane/README.md | 10 +- .../reference/prerequisite-resolution.md | 2 +- .../routines/backlog-readiness-check.md | 2 +- .../routines/duplicate-detection-sweep.md | 2 +- .../reference/routines/issue-triage-sweep.md | 2 +- .../reference/routines/pr-queue-tending.md | 2 +- .../repo/docs/conventions/work-items.yaml | 0 .../repo/.work-item-tracker.json | 1 - .../repo/docs/conventions/work-items.yaml | 5 + .../repo/.work-item-tracker.json | 1 - .../repo/docs/conventions/work-items.yaml | 5 + .../repo/.work-item-tracker.json | 1 - .../repo/docs/conventions/work-items.yaml | 5 + .../repo/.claude/autonomy/binding.json | 0 .../repo/.claude/ecosystems/typescript.yaml | 0 .../repo/.github/workflows/ci.yml | 0 .../repo/.work-item-tracker.json | 0 .../repo/docs/README.md | 0 .../repo/package.json | 0 .../repo/src/main.ts | 0 .../adapters/github/capabilities.json | 0 .../generate-identity-prerequisites.test.sh | 2 +- ...-prerequisites.fixtures.test.manifest.json | 4 +- .../resolve-prerequisites.fixtures.test.mjs | 26 +++-- .../setup/scripts/resolve-prerequisites.mjs | 26 +++-- .../skills/audit/reference/session-mode.md | 4 +- .../skills/babysit-loop/SKILL.md | 4 +- .../skills/babysit-prs/reference/safety.md | 2 +- .../scripts/babysit_tracker_binding.py | 47 +++------- .../tests/test_babysit_merge_policy.py | 22 +++-- .../scripts/tests/test_babysit_postmerge.py | 4 +- .../scripts/tests/test_babysit_risk.py | 2 +- .../tests/test_babysit_tracker_binding.py | 77 +++++++-------- .../skills/pull-request/reference/create.md | 2 +- plugins/work-items/README.md | 7 +- .../prompt.md | 2 +- .../work-items/reference/label-taxonomy.md | 2 +- plugins/work-items/reference/tracker-seam.md | 3 +- .../work-items/schemas/work-items.schema.json | 2 +- .../work-items/skills/attend-queue/SKILL.md | 2 +- .../scripts/templates/common.sh.tmpl | 2 +- plugins/work-items/skills/setup/SKILL.md | 16 ++-- .../skills/setup/reference/check.md | 24 ++--- plugins/work-items/skills/track/SKILL.md | 2 +- .../work-items/skills/track/actions/due.md | 4 +- .../skills/track/actions/recheck.md | 4 +- plugins/work-items/skills/triage/SKILL.md | 2 +- plugins/work-items/skills/work/SKILL.md | 2 +- .../tools/work-item-tracker/CONTRACT.md | 39 +++----- .../tools/work-item-tracker/lib/binding.sh | 54 +++++++---- .../work-item-tracker/lib/binding.test.sh | 94 +++++++++++++------ .../work-item-tracker/seeds/gitea/common.sh | 2 +- .../work-item-tracker/seeds/jira/common.sh | 2 +- .../work-item-tracker/seeds/linear/common.sh | 2 +- .../work-item-tracker/work-item-tracker.sh | 2 +- prompts/loops/loop-lane-prompts.md | 13 +-- 61 files changed, 342 insertions(+), 245 deletions(-) create mode 100644 .changes/autonomy/refactor-work-items-drop-json-fallback-6632485e.md create mode 100644 .changes/plugin-quality/refactor-work-items-drop-json-fallback-3413e338.md create mode 100644 .changes/source-control/refactor-work-items-drop-json-fallback-a72963b9.md create mode 100644 .changes/work-items/refactor-work-items-drop-json-fallback-59140d30.md rename plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/{positive-verdict-yaml-binding => declared-absent-narrows}/repo/docs/conventions/work-items.yaml (100%) delete mode 100644 plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict/repo/.work-item-tracker.json create mode 100644 plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict/repo/docs/conventions/work-items.yaml delete mode 100644 plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/posture-divergence/repo/.work-item-tracker.json create mode 100644 plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/posture-divergence/repo/docs/conventions/work-items.yaml delete mode 100644 plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/probe-could-not-run/repo/.work-item-tracker.json create mode 100644 plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/probe-could-not-run/repo/docs/conventions/work-items.yaml rename plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/{positive-verdict-yaml-binding => retired-json-binding}/repo/.claude/autonomy/binding.json (100%) rename plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/{positive-verdict-yaml-binding => retired-json-binding}/repo/.claude/ecosystems/typescript.yaml (100%) rename plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/{positive-verdict-yaml-binding => retired-json-binding}/repo/.github/workflows/ci.yml (100%) rename plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/{declared-absent-narrows => retired-json-binding}/repo/.work-item-tracker.json (100%) rename plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/{positive-verdict-yaml-binding => retired-json-binding}/repo/docs/README.md (100%) rename plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/{positive-verdict-yaml-binding => retired-json-binding}/repo/package.json (100%) rename plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/{positive-verdict-yaml-binding => retired-json-binding}/repo/src/main.ts (100%) rename plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/{positive-verdict-yaml-binding => retired-json-binding}/repo/tools/work-item-tracker/adapters/github/capabilities.json (100%) diff --git a/.changes/autonomy/refactor-work-items-drop-json-fallback-6632485e.md b/.changes/autonomy/refactor-work-items-drop-json-fallback-6632485e.md new file mode 100644 index 00000000000..6acee59d744 --- /dev/null +++ b/.changes/autonomy/refactor-work-items-drop-json-fallback-6632485e.md @@ -0,0 +1,11 @@ +--- +bump: major +--- + +### Removed + +- The prerequisite resolver (`skills/setup/scripts/resolve-prerequisites.mjs`) no longer reads a repo-root `.work-item-tracker.json`. The tracker need is present only when `docs/conventions/work-items.yaml` sets a `tracker` mapping; otherwise it is absent, and routines that need the tracker resolve `unsupported`. The absent detail names `/work-items:setup apply` as the fix, and names the JSON file when one is present. + +### Changed + +- The prerequisite-resolution reference and the backlog-readiness, duplicate-detection, issue-triage and pr-queue-tending routines name only the YAML binding. diff --git a/.changes/plugin-quality/refactor-work-items-drop-json-fallback-3413e338.md b/.changes/plugin-quality/refactor-work-items-drop-json-fallback-3413e338.md new file mode 100644 index 00000000000..201cc7c1b9f --- /dev/null +++ b/.changes/plugin-quality/refactor-work-items-drop-json-fallback-3413e338.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Changed + +- The audit skill's session-mode reference reads role labels only from the `tracker` mapping of `docs/conventions/work-items.yaml`, matching the work-items seam, which no longer reads a repo-root `.work-item-tracker.json`. diff --git a/.changes/source-control/refactor-work-items-drop-json-fallback-a72963b9.md b/.changes/source-control/refactor-work-items-drop-json-fallback-a72963b9.md new file mode 100644 index 00000000000..0f25139a845 --- /dev/null +++ b/.changes/source-control/refactor-work-items-drop-json-fallback-a72963b9.md @@ -0,0 +1,11 @@ +--- +bump: major +--- + +### Removed + +- The babysit scripts no longer read role labels from a repo-root `.work-item-tracker.json`. `babysit_tracker_binding.py` reads only the `tracker` mapping of `docs/conventions/work-items.yaml`; `LEGACY_PATH` is gone and `PATHS` holds only that file, so the merge-skip cache key no longer covers the JSON file. A repository bound only by the JSON file gets the default role labels. Fix: run `/work-items:setup apply`, which copies the binding into `docs/conventions/work-items.yaml`. + +### Changed + +- The babysit-loop skill, the babysit safety reference and the pull-request create reference no longer describe a JSON binding. diff --git a/.changes/work-items/refactor-work-items-drop-json-fallback-59140d30.md b/.changes/work-items/refactor-work-items-drop-json-fallback-59140d30.md new file mode 100644 index 00000000000..a4d511b1270 --- /dev/null +++ b/.changes/work-items/refactor-work-items-drop-json-fallback-59140d30.md @@ -0,0 +1,14 @@ +--- +bump: major +--- + +### Removed + +- The tracker seam no longer reads a repo-root `.work-item-tracker.json`. The binding is only the `tracker` mapping of `/work-items.yaml` (or a file `WORK_ITEM_TRACKER_BINDING` names). A repository bound only by the JSON file now gets exit `3` from every seam verb. **Fix: run `/work-items:setup apply`**, which copies the JSON binding and its `.work-item-tracker.local.json` personal override under `tracker:` in `/work-items.yaml` and `/work-items.local.yaml`, and leaves the JSON files in place. +- The one-line `notice:` the seam printed when it read the JSON file is gone. + +### Changed + +- With no binding, the seam's exit `3` message (`wit_no_binding_reason` in `lib/binding.sh`) says why and names `/work-items:setup apply`: a repo-root `.work-item-tracker.json` that no longer binds, a `work-items.yaml` with no `tracker` mapping, a root where no convention home applies, or a missing `WORK_ITEM_TRACKER_BINDING` file. The jira, gitea and linear seeds and the onboard-adapter template print the same message. +- `/work-items:setup check` reports a binding that exists only in `.work-item-tracker.json` as FAIL, remediation `/work-items:setup apply`, instead of INFO. +- The seam contract, setup skill, README, schema and the skill bodies that resolve role labels no longer describe a JSON fallback. diff --git a/docs/contracts/config-cascade/README.md b/docs/contracts/config-cascade/README.md index bc493257601..01702776a97 100644 --- a/docs/contracts/config-cascade/README.md +++ b/docs/contracts/config-cascade/README.md @@ -681,7 +681,7 @@ its conformance cell. | `code-tidying` | no-floor | `.claude/tidy-lanes/.md` | team only | team only; residual wholesale if no `## Merge semantics` | per-section when declared | declared deviation; no user-global or `*.local.*` personal override (#723). Team layer over a bundled default. A project lane declaring `## Merge semantics` merges per-section with its bundled lane (`Scope` per-section override, watch-for patterns additive, per `docs-prose` #701 and `shell-tooling` #724). Residual deviation: a project lane that declares nothing still resolves project-only wholesale, the first-match fallback retained in #701 so unmigrated consumer lanes keep working, undeclared at the layer that takes it. Personal variation is limited to lane names the team does not track, an uncommitted `.claude/tidy-lanes/.md` never added to the index; gitignoring a path the team already tracks does not make it personal | | `code-metrics` | no-floor | `docs/conventions/code-metrics.yaml` (ADR 0064; schema `plugins/code-metrics/schemas/code-metrics.schema.json`) | all three | later layer | per-key | conforms (per-key override, declared because every value is a scalar or a closed list: `scope.exclude` and `lanes..collectors.` replace whole), except declared divergences from "Resolution algorithm": (1) the root is `git rev-parse --show-toplevel`, else the working directory, and `CLAUDE_PROJECT_DIR` is not read (`plugins/code-metrics/scripts/resolve-config.py:221-234`; step 1); (2) no home-root or same-file classification of the team and personal override paths (`resolve-config.py:236-252`; step 2). The team layer is the docs file alone: the retired `.claude` team file of earlier releases is never read (`resolve-config.py:244-252`), and `/code-metrics:setup check` reports it. A layer outside the YAML subset reads as absent, and a value of the wrong shape is dropped by file, key and value and resolves from a valid higher layer, else the bundled default, never a lower layer; neither stops the run (`resolve-config.py:133-165,273-297`; ADR 0064 Decision 7). Unknown keys inert. Keys owned by [`plugins/code-metrics/reference/config.md`](../../../plugins/code-metrics/reference/config.md). Written (team layer only, carrying every declared key of a retired `.claude` file) by `/code-metrics:setup apply`; read by every audit skill. The consumer's `.claude/ecosystems/.yaml` files are a separate convention (ecosystem-commands); this surface does not absorb them. **Claim:** the plugin implements this row with the declared divergences above. **Basis:** `plugins/code-metrics/reference/config.md` "Layers and merge form" and the cited `resolve-config.py` lines. **As of:** 2026-11-02. **Recheck:** when that section adds a layer, changes merge form, or starts owning an ecosystem-commands key | | `repo-fleet-hygiene` | no-floor | `.claude/repo-fleet-hygiene.conf` | user-global + team | `--config` then team then user-global | whole-file, no per-key | declared deviation; whole-file precedence (explicit `--config` > team > user-global fallback), no per-key merge, no personal override layer (#1099) | -| `work-items` | no-floor | the `tracker` mapping of `/work-items.yaml`; the legacy `.work-item-tracker.json` (repo root) while a repository is not migrated | team + personal override | personal override on the allowlist | per-key personal override | declared deviation ([ADR 0015](../../adr/0015-bind-the-tracker-at-repo-root-with-an-allowlisted-personal-overlay.md)): layers live at the repo root, not under `.claude/`; personal override (`.work-item-tracker.local.json`) merges per-key over a deny-by-default allowlist (lease TTL, jira/linear/gitea auth identity, `docs`); deliberately no user-global layer, since a cross-repo personal rung would reopen the per-user provider trap the allowlist forecloses. Anchors at the repo root (`CLAUDE_PROJECT_DIR`, else git toplevel), no CWD climb. The personal override's gitignore line is outside the `.claude/**/*.local.*` one-liner, so `/work-items:setup apply` appends it, announced, a declared exception to the recommend default (ADR 0015) | +| `work-items` | no-floor | the `tracker` mapping of `/work-items.yaml` | team + personal override | personal override on the allowlist | per-key personal override | declared deviation ([ADR 0015](../../adr/0015-bind-the-tracker-at-repo-root-with-an-allowlisted-personal-overlay.md), superseded by ADR 0075): layers live in the convention home, not under `.claude/`; personal override (`/work-items.local.yaml`) merges per-key over a deny-by-default allowlist (lease TTL, jira/linear/gitea auth identity, `docs`); deliberately no user-global layer, since a cross-repo personal rung would reopen the per-user provider trap the allowlist forecloses. Anchors at the repo root (`CLAUDE_PROJECT_DIR`, else git toplevel), no CWD climb. The personal override's gitignore line is outside the `.claude/**/*.local.*` one-liner, so `/work-items:setup apply` appends it, announced, a declared exception to the recommend default (ADR 0015) | | `work-items` (recurring schedule) | no-floor | `.github/recurring-schedule.json` | team only | team only | single-layer | declared deviation ([ADR 0042](../../adr/0042-ratify-consumer-config-location-outliers-in-place.md)): the schedule lives under `.github/`, not `.claude/`, tracked and team-shared with no user-global layer and no personal override. Written by `track add` and `track recheck`; read by `track audit`, `done`, `due`, `search` and `stats`, by `work` candidate discovery, and by `setup`, which also writes it; anchors at `CLAUDE_PROJECT_DIR`, else git toplevel. Separate from the tracker binding row above | | `work-items` (settings keys) | any-layer-wins | team: `docs/conventions/work-items.yaml` (schema `plugins/work-items/schemas/work-items.schema.json`); per user: the plugin's `userConfig` option of the same name | `userConfig` + team | team; `true` in any layer for the five `bug_path.fix_gate` floors (policy floor) | per-key | conforms to the [location axis](#location-of-the-team-layer-a-docs-convention-file-or-claudename) ([ADR 0064](../../adr/0064-home-plugin-customization-in-docs-conventions-yaml.md)), with the layer order of its Decision 8: the manifest default, then `userConfig`, then the team file; no `~/.claude` file and no personal override. Keys are `triage_repro_count`, `triage_objection_window_hours` and `ai_disclaimer`; `triage_cluster` (signature-cluster defaults) has the team layer only. A value outside a key's range is named with its file, key and value, and that layer is dropped; the key then takes its manifest default, never a lower layer. This repository's file sets `ai_disclaimer: false`. The triage keys are read by `/work-items:triage` only, through `skills/triage/scripts/parse-concern-value.sh` from the repository root, or at a pinned ref under `--config-ref`; the `/work-items:work-loop` admission gate reads the window's end from the marker comment triage posts, not the file. `ai_disclaimer` is read through the same script by each lane that follows `plugins/work-items/reference/ai-disclaimer.md`. `bug_path.fix_gate` (team file only, no `userConfig` option) is read by `/work-items:work` through the same script with `--ref origin/`, so a branch cannot loosen it; its floor keys `confirmed_repro`, `evidence_reviewed`, `no_fix_artifact`, `no_human_claim` and `root_cause_evidenced` default `true`, and a `false` is named and ignored, while `before_after_runnable`, `budget_minutes` and `scope` are plain keys. Hand-edited, since `/work-items:setup apply` does not write this file yet | | `guardrails` | no-floor | team: `docs/conventions/guardrails.yaml` (schema `plugins/guardrails/schemas/guardrails.schema.json`) | team only, over the plugin default | team | per-key | declared deviation: one key, `fix_request_post` (`block` default, `allow`), read by `hooks/block-fix-request-post.sh` from the session's repository (the payload's cwd) at its local `origin/HEAD`, never the working tree or index, and applied whichever repository the post targets; no personal override and no `userConfig` value of the key (the guard's `block_fix_request_post_enabled` switch stays a separate per-user escape). A value other than `allow` or `block`, or a file that does not parse, is named and keeps `block` | diff --git a/docs/contracts/loop-lane/README.md b/docs/contracts/loop-lane/README.md index cf2c5558ec9..1050c03167e 100644 --- a/docs/contracts/loop-lane/README.md +++ b/docs/contracts/loop-lane/README.md @@ -291,7 +291,7 @@ App installed on that repository only: a machine user is a `User` and would pass A lane escalates by creating or labeling a tracker item that carries the **`needs-human` role label**, resolved through the consumer's tracker binding (`config.role_labels` of the `tracker` -mapping in `/work-items.yaml`, else of the legacy `.work-item-tracker.json`) and +mapping in `/work-items.yaml`) and never compared as a string literal ([`label-taxonomy.md`](../../../plugins/work-items/reference/label-taxonomy.md#canonical-roles) owns the canonical roles and the resolution). A machine-marked bot comment discriminates a @@ -1147,8 +1147,7 @@ offer to persist the answer; the headless path never waits on one. **Lane-editable config and the four sandbox conditions.** Every local lane reads some config at run time that the lane's own process could edit (audited in #7680). work-loop and attend-queue read -the tracker binding (`docs/conventions/work-items.yaml`'s `tracker` mapping, else the legacy -`.work-item-tracker.json`) from the checkout, including the role labels that +the tracker binding (`docs/conventions/work-items.yaml`'s `tracker` mapping) from the checkout, including the role labels that admission matches on. The triage step they run reads `docs/conventions/work-items.yaml` and `docs/conventions/pr-pipeline.yaml` from the working tree when no `--config-ref` is given, and both lanes read that `work-items.yaml` for `ai_disclaimer` the same way. The @@ -1498,8 +1497,9 @@ binding resolves from the same root the tracker seam uses. ```bash root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel)}" -L=$({ yq -o=json -e '.tracker' "$root/docs/conventions/work-items.yaml" 2>/dev/null || cat "$root/.work-item-tracker.json"; } | - jq -r '.config.role_labels["human-gated"] // "needs-human"' 2>/dev/null || echo needs-human) +L=$(yq -o=json -e '.tracker' "$root/docs/conventions/work-items.yaml" 2>/dev/null | + jq -r '.config.role_labels["human-gated"] // empty' 2>/dev/null) +L=${L:-needs-human} gh issue edit -R "$R" --add-label "$L" gh issue comment -R "$R" --body "Parked by operator: ." ``` diff --git a/plugins/autonomy/reference/prerequisite-resolution.md b/plugins/autonomy/reference/prerequisite-resolution.md index 8aec87ce22b..b137f352e16 100644 --- a/plugins/autonomy/reference/prerequisite-resolution.md +++ b/plugins/autonomy/reference/prerequisite-resolution.md @@ -158,7 +158,7 @@ presence-gated with a documented fallback per the marketplace's seam-phrasing co |---|---|---| | Ecosystems | Toolchain seam (when the `toolchain` plugin is installed), reading *resolved* consumer state **with resolution-rung provenance**: consume only consumer-authored or repo-inferred rungs; an ecosystem present but `enabled: false` is not configured; uncommitted user-global and `.local.yaml` layers report unresolvable; **reject the seam's bundled-default rung** as repository evidence | Inference from the repo's own build files; never another plugin's bundled defaults | | MCP enablement | Config-audit surface (when the sibling config-audit plugin is installed; marketplace id in README, since this `reference/` surface may not name vendors) | Report `.mcp.json` presence only; name enablement as unprobeable in provenance | -| Tracker | Work-item tracker seam: the `tracker` mapping of `docs/conventions/work-items.yaml`, else the legacy `.work-item-tracker.json`, plus the bound adapter's `capabilities.json` (when the `work-items` plugin is installed) | Treat tracker-dependent prerequisites as unestablished (`unknown`) | +| Tracker | Work-item tracker seam: the `tracker` mapping of `docs/conventions/work-items.yaml`, plus the bound adapter's `capabilities.json` (when the `work-items` plugin is installed) | Treat tracker-dependent prerequisites as unestablished (`unknown`) | | Substrates, schedulers, observability | Autonomy setup skill's own discovery slices | Same plugin, no gate | | Configured-surface enumeration | Each surface's own presence in the repo | Never by reading the config-cascade registry table (a conformance ledger, not a runtime inventory) | | Ownerless signals (CI-config presence, flag-SDK presence) | Probes owned by this contract | N/A | diff --git a/plugins/autonomy/reference/routines/backlog-readiness-check.md b/plugins/autonomy/reference/routines/backlog-readiness-check.md index aad7f158ca2..a8dc18772fa 100644 --- a/plugins/autonomy/reference/routines/backlog-readiness-check.md +++ b/plugins/autonomy/reference/routines/backlog-readiness-check.md @@ -75,7 +75,7 @@ Single-posture identity: `backlog-readiness-check` (bare class token). | Connector entitlements | none. Access is `repo`, so the connector branch of [Access to prerequisites](../routines.md#access-to-prerequisites) does not apply | | Connector entitlement rung | n/a (no connector). For `prod` / `product` / `org` / `ext`, entitlement binds at the [Org binding layer](../binding-seam.md#resolution-ladder) | | `executor_class` merge cap | cited from [executor surface classes](../trigger-dispatch.md#executor-surface-classes). None: `executor_class` is deprecated and caps nothing. Merge policy for this identity is n/a (`C1`) | -| Repo needs | tracker binding via the work-item tracker seam (the `tracker` mapping of `docs/conventions/work-items.yaml`, else the legacy `.work-item-tracker.json`, + adapter `capabilities.json`); a deterministic repo-file probe that finds those files absent is `unsupported`; `unknown` only when the work-item seam cannot establish the fact (composition fallback) | +| Repo needs | tracker binding via the work-item tracker seam (the `tracker` mapping of `docs/conventions/work-items.yaml`, + adapter `capabilities.json`); a deterministic repo-file probe that finds those files absent is `unsupported`; `unknown` only when the work-item seam cannot establish the fact (composition fallback) | ## Admission and escalation diff --git a/plugins/autonomy/reference/routines/duplicate-detection-sweep.md b/plugins/autonomy/reference/routines/duplicate-detection-sweep.md index 57a561d3d0a..d9eafb531ec 100644 --- a/plugins/autonomy/reference/routines/duplicate-detection-sweep.md +++ b/plugins/autonomy/reference/routines/duplicate-detection-sweep.md @@ -76,7 +76,7 @@ Single-posture identity: `duplicate-detection-sweep` (bare class token). | Connector entitlements | none. Access is `repo`, so the connector branch of [Access to prerequisites](../routines.md#access-to-prerequisites) does not apply | | Connector entitlement rung | n/a (no connector). For `prod` / `product` / `org` / `ext`, entitlement binds at the [Org binding layer](../binding-seam.md#resolution-ladder) | | `executor_class` merge cap | cited from [executor surface classes](../trigger-dispatch.md#executor-surface-classes). None: `executor_class` is deprecated and caps nothing. Merge policy for this identity is n/a (`C1`) | -| Repo needs | tracker binding via the work-item tracker seam (the `tracker` mapping of `docs/conventions/work-items.yaml`, else the legacy `.work-item-tracker.json`, + adapter `capabilities.json`); a deterministic repo-file probe that finds those files absent is `unsupported`; `unknown` only when the work-item seam cannot establish the fact (composition fallback) | +| Repo needs | tracker binding via the work-item tracker seam (the `tracker` mapping of `docs/conventions/work-items.yaml`, + adapter `capabilities.json`); a deterministic repo-file probe that finds those files absent is `unsupported`; `unknown` only when the work-item seam cannot establish the fact (composition fallback) | ## Admission and escalation diff --git a/plugins/autonomy/reference/routines/issue-triage-sweep.md b/plugins/autonomy/reference/routines/issue-triage-sweep.md index 7854b200518..d567e3a1989 100644 --- a/plugins/autonomy/reference/routines/issue-triage-sweep.md +++ b/plugins/autonomy/reference/routines/issue-triage-sweep.md @@ -74,7 +74,7 @@ Single-posture identity: `issue-triage-sweep` (bare class token). | Connector entitlements | none. Access is `repo`, so the connector branch of [Access to prerequisites](../routines.md#access-to-prerequisites) does not apply | | Connector entitlement rung | n/a (no connector). For `prod` / `product` / `org` / `ext`, entitlement binds at the [Org binding layer](../binding-seam.md#resolution-ladder) | | `executor_class` merge cap | cited from [executor surface classes](../trigger-dispatch.md#executor-surface-classes). None: `executor_class` is deprecated and caps nothing. Merge policy for this identity is n/a (`C1`) | -| Repo needs | tracker binding via the work-item tracker seam (the `tracker` mapping of `docs/conventions/work-items.yaml`, else the legacy `.work-item-tracker.json`, + adapter `capabilities.json`); a deterministic repo-file probe that finds those files absent is `unsupported`; `unknown` only when the work-item seam cannot establish the fact (composition fallback) | +| Repo needs | tracker binding via the work-item tracker seam (the `tracker` mapping of `docs/conventions/work-items.yaml`, + adapter `capabilities.json`); a deterministic repo-file probe that finds those files absent is `unsupported`; `unknown` only when the work-item seam cannot establish the fact (composition fallback) | ## Admission and escalation diff --git a/plugins/autonomy/reference/routines/pr-queue-tending.md b/plugins/autonomy/reference/routines/pr-queue-tending.md index 424927f65da..d89ba0e47c0 100644 --- a/plugins/autonomy/reference/routines/pr-queue-tending.md +++ b/plugins/autonomy/reference/routines/pr-queue-tending.md @@ -75,7 +75,7 @@ Single-posture identity: `pr-queue-tending` (bare class token). | Connector entitlements | none. Access is `repo`, so the connector branch of [Access to prerequisites](../routines.md#access-to-prerequisites) does not apply | | Connector entitlement rung | n/a (no connector). For `prod` / `product` / `org` / `ext`, entitlement binds at the [Org binding layer](../binding-seam.md#resolution-ladder) | | `executor_class` merge cap | cited from [executor surface classes](../trigger-dispatch.md#executor-surface-classes). None: `executor_class` is deprecated and caps nothing. Merge policy for this identity is n/a (`C1`) | -| Repo needs | tracker binding via the work-item tracker seam (the `tracker` mapping of `docs/conventions/work-items.yaml`, else the legacy `.work-item-tracker.json`, + adapter `capabilities.json`); CI-config presence (ownerless repo-file probe owned by the resolution contract) so CI outcomes are readable; either absence is `unknown` or `unsupported` per probe outcome | +| Repo needs | tracker binding via the work-item tracker seam (the `tracker` mapping of `docs/conventions/work-items.yaml`, + adapter `capabilities.json`); CI-config presence (ownerless repo-file probe owned by the resolution contract) so CI outcomes are readable; either absence is `unknown` or `unsupported` per probe outcome | ## Admission and escalation diff --git a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict-yaml-binding/repo/docs/conventions/work-items.yaml b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/declared-absent-narrows/repo/docs/conventions/work-items.yaml similarity index 100% rename from plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict-yaml-binding/repo/docs/conventions/work-items.yaml rename to plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/declared-absent-narrows/repo/docs/conventions/work-items.yaml diff --git a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict/repo/.work-item-tracker.json b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict/repo/.work-item-tracker.json deleted file mode 100644 index 3712808571a..00000000000 --- a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict/repo/.work-item-tracker.json +++ /dev/null @@ -1 +0,0 @@ -{"provider":"github"} diff --git a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict/repo/docs/conventions/work-items.yaml b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict/repo/docs/conventions/work-items.yaml new file mode 100644 index 00000000000..8fc0385f111 --- /dev/null +++ b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict/repo/docs/conventions/work-items.yaml @@ -0,0 +1,5 @@ +tracker: + schema_version: "1.0" + provider: "github" + config: + lease_ttl_hours: 24 diff --git a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/posture-divergence/repo/.work-item-tracker.json b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/posture-divergence/repo/.work-item-tracker.json deleted file mode 100644 index 3712808571a..00000000000 --- a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/posture-divergence/repo/.work-item-tracker.json +++ /dev/null @@ -1 +0,0 @@ -{"provider":"github"} diff --git a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/posture-divergence/repo/docs/conventions/work-items.yaml b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/posture-divergence/repo/docs/conventions/work-items.yaml new file mode 100644 index 00000000000..8fc0385f111 --- /dev/null +++ b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/posture-divergence/repo/docs/conventions/work-items.yaml @@ -0,0 +1,5 @@ +tracker: + schema_version: "1.0" + provider: "github" + config: + lease_ttl_hours: 24 diff --git a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/probe-could-not-run/repo/.work-item-tracker.json b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/probe-could-not-run/repo/.work-item-tracker.json deleted file mode 100644 index 3712808571a..00000000000 --- a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/probe-could-not-run/repo/.work-item-tracker.json +++ /dev/null @@ -1 +0,0 @@ -{"provider":"github"} diff --git a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/probe-could-not-run/repo/docs/conventions/work-items.yaml b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/probe-could-not-run/repo/docs/conventions/work-items.yaml new file mode 100644 index 00000000000..8fc0385f111 --- /dev/null +++ b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/probe-could-not-run/repo/docs/conventions/work-items.yaml @@ -0,0 +1,5 @@ +tracker: + schema_version: "1.0" + provider: "github" + config: + lease_ttl_hours: 24 diff --git a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict-yaml-binding/repo/.claude/autonomy/binding.json b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/retired-json-binding/repo/.claude/autonomy/binding.json similarity index 100% rename from plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict-yaml-binding/repo/.claude/autonomy/binding.json rename to plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/retired-json-binding/repo/.claude/autonomy/binding.json diff --git a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict-yaml-binding/repo/.claude/ecosystems/typescript.yaml b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/retired-json-binding/repo/.claude/ecosystems/typescript.yaml similarity index 100% rename from plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict-yaml-binding/repo/.claude/ecosystems/typescript.yaml rename to plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/retired-json-binding/repo/.claude/ecosystems/typescript.yaml diff --git a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict-yaml-binding/repo/.github/workflows/ci.yml b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/retired-json-binding/repo/.github/workflows/ci.yml similarity index 100% rename from plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict-yaml-binding/repo/.github/workflows/ci.yml rename to plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/retired-json-binding/repo/.github/workflows/ci.yml diff --git a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/declared-absent-narrows/repo/.work-item-tracker.json b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/retired-json-binding/repo/.work-item-tracker.json similarity index 100% rename from plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/declared-absent-narrows/repo/.work-item-tracker.json rename to plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/retired-json-binding/repo/.work-item-tracker.json diff --git a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict-yaml-binding/repo/docs/README.md b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/retired-json-binding/repo/docs/README.md similarity index 100% rename from plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict-yaml-binding/repo/docs/README.md rename to plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/retired-json-binding/repo/docs/README.md diff --git a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict-yaml-binding/repo/package.json b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/retired-json-binding/repo/package.json similarity index 100% rename from plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict-yaml-binding/repo/package.json rename to plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/retired-json-binding/repo/package.json diff --git a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict-yaml-binding/repo/src/main.ts b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/retired-json-binding/repo/src/main.ts similarity index 100% rename from plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict-yaml-binding/repo/src/main.ts rename to plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/retired-json-binding/repo/src/main.ts diff --git a/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict-yaml-binding/repo/tools/work-item-tracker/adapters/github/capabilities.json b/plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/retired-json-binding/repo/tools/work-item-tracker/adapters/github/capabilities.json similarity index 100% rename from plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/positive-verdict-yaml-binding/repo/tools/work-item-tracker/adapters/github/capabilities.json rename to plugins/autonomy/skills/setup/scripts/fixtures/prerequisite-resolution/retired-json-binding/repo/tools/work-item-tracker/adapters/github/capabilities.json diff --git a/plugins/autonomy/skills/setup/scripts/generate-identity-prerequisites.test.sh b/plugins/autonomy/skills/setup/scripts/generate-identity-prerequisites.test.sh index 1c60e1df826..863bcd6ebc7 100755 --- a/plugins/autonomy/skills/setup/scripts/generate-identity-prerequisites.test.sh +++ b/plugins/autonomy/skills/setup/scripts/generate-identity-prerequisites.test.sh @@ -59,7 +59,7 @@ Single-posture identity: `alpha` (bare class token). | Connector entitlements | none — `repo` access | | Connector entitlement rung | n/a (no connector). For `prod` / `product` / `org` / `ext`, entitlement binds at the Org binding layer | | `executor_class` merge cap | cited from security-binding | -| Repo needs | tracker binding via the work-item tracker seam (`.work-item-tracker.json` + adapter `capabilities.json`); absent is `unsupported` | +| Repo needs | tracker binding via the work-item tracker seam (the `tracker` mapping of `docs/conventions/work-items.yaml` + adapter `capabilities.json`); absent is `unsupported` | EOF # Seed a correct emission, then hand-edit it. diff --git a/plugins/autonomy/skills/setup/scripts/resolve-prerequisites.fixtures.test.manifest.json b/plugins/autonomy/skills/setup/scripts/resolve-prerequisites.fixtures.test.manifest.json index ac4b1d4b50a..225ee4c7ed4 100644 --- a/plugins/autonomy/skills/setup/scripts/resolve-prerequisites.fixtures.test.manifest.json +++ b/plugins/autonomy/skills/setup/scripts/resolve-prerequisites.fixtures.test.manifest.json @@ -29,9 +29,9 @@ "surface": "ci-cron", "assert": "positive-verdict" }, - "positive-verdict-yaml-binding": { + "retired-json-binding": { "surface": "ci-cron", - "assert": "positive-verdict-yaml-binding" + "assert": "retired-json-binding" } } } diff --git a/plugins/autonomy/skills/setup/scripts/resolve-prerequisites.fixtures.test.mjs b/plugins/autonomy/skills/setup/scripts/resolve-prerequisites.fixtures.test.mjs index 0a2168291a8..678bc4ce08e 100755 --- a/plugins/autonomy/skills/setup/scripts/resolve-prerequisites.fixtures.test.mjs +++ b/plugins/autonomy/skills/setup/scripts/resolve-prerequisites.fixtures.test.mjs @@ -173,18 +173,28 @@ function assertPositiveVerdict(output) { if (!tracker.provenance || !tracker.provenance.kind) { throw new Error("missing provenance on positive verdict"); } -} - -// The same repository bound by docs/conventions/work-items.yaml, with no -// legacy JSON file: the probe reads the YAML binding. -function assertPositiveVerdictYamlBinding(output) { - assertPositiveVerdict(output); - const tracker = requireRow(output, "issue-triage-sweep").signals.find((s) => s.need === "tracker"); if (!JSON.stringify(tracker.provenance).includes("docs/conventions/work-items.yaml")) { throw new Error(`expected the YAML binding in provenance; got ${JSON.stringify(tracker.provenance)}`); } } +// The same repository with only a root .work-item-tracker.json: it no longer +// binds, so the tracker is absent and the detail names the one-step fix. +function assertRetiredJsonBinding(output) { + const row = requireRow(output, "issue-triage-sweep"); + if (row.verdict === "supported") { + throw new Error("a root .work-item-tracker.json alone must not support the routine"); + } + const tracker = row.signals.find((s) => s.need === "tracker"); + if (!tracker || tracker.result !== "absent") { + throw new Error(`expected tracker absent; got ${tracker?.result}`); + } + const provenance = JSON.stringify(tracker.provenance); + if (!provenance.includes("run /work-items:setup apply") || !provenance.includes(".work-item-tracker.json")) { + throw new Error(`expected the retired file and /work-items:setup apply in provenance; got ${provenance}`); + } +} + const ASSERTIONS = { "bare-repo": assertBareRepo, "fail-closed": assertFailClosed, @@ -193,7 +203,7 @@ const ASSERTIONS = { "probe-could-not-run": assertProbeCouldNotRun, "posture-divergence": assertPostureDivergence, "positive-verdict": assertPositiveVerdict, - "positive-verdict-yaml-binding": assertPositiveVerdictYamlBinding, + "retired-json-binding": assertRetiredJsonBinding, }; for (const [name, entry] of Object.entries(fixtures)) { diff --git a/plugins/autonomy/skills/setup/scripts/resolve-prerequisites.mjs b/plugins/autonomy/skills/setup/scripts/resolve-prerequisites.mjs index 0b6ea3b4310..00af8bf609c 100755 --- a/plugins/autonomy/skills/setup/scripts/resolve-prerequisites.mjs +++ b/plugins/autonomy/skills/setup/scripts/resolve-prerequisites.mjs @@ -37,11 +37,12 @@ const DEFAULT_EMISSION = join( const SCHEMA_VERSION = 1; // The tracker binding: the `tracker` mapping of the default convention home's -// work-items.yaml, else the legacy repo-root JSON file the work-items seam -// still reads for a repository not yet migrated (its CONTRACT.md "Setup -// (binding file)"). +// work-items.yaml (the work-items seam's CONTRACT.md "Setup (binding file)"). +// A repo-root .work-item-tracker.json no longer binds; its presence only names +// the fix. const TRACKER_YAML = "docs/conventions/work-items.yaml"; -const TRACKER_LEGACY = ".work-item-tracker.json"; +const TRACKER_RETIRED = ".work-item-tracker.json"; +const TRACKER_FIX = "run /work-items:setup apply"; const DEPENDENCY_MANIFESTS = [ "package.json", @@ -222,9 +223,9 @@ function declarationFor(binding, surface, identity, need) { return matches[0] ?? null; } -// The binding file that binds the tracker and its read: the YAML file when it -// sets a `tracker` key (or does not parse, so the error is its own), else the -// legacy JSON file. +// The tracker binding's read: the YAML file's `tracker` mapping when it sets +// one (or the file's own error when it does not parse), else absent with a +// detail naming the fix. function readTrackerBinding(repoRoot) { let text = null; try { @@ -245,13 +246,20 @@ function readTrackerBinding(repoRoot) { return { path: TRACKER_YAML, read: { ok: true, value: doc.tracker } }; } } - return { path: TRACKER_LEGACY, read: readJson(join(repoRoot, TRACKER_LEGACY)) }; + const detail = pathExists(join(repoRoot, TRACKER_RETIRED)) + ? `${TRACKER_RETIRED} no longer binds the tracker; ${TRACKER_FIX} to copy it into ${TRACKER_YAML}` + : `no tracker mapping in ${TRACKER_YAML}; ${TRACKER_FIX}`; + return { path: TRACKER_YAML, read: { ok: false, reason: "absent", detail } }; } function probeTracker(repoRoot) { const { path: bindingRel, read: bindingRead } = readTrackerBinding(repoRoot); if (!bindingRead.ok) { - return probeOutcome("absent", { ...TRACKER_PROBE, path: bindingRel, detail: bindingRead.reason }); + return probeOutcome("absent", { + ...TRACKER_PROBE, + path: bindingRel, + detail: bindingRead.reason === "absent" ? bindingRead.detail : bindingRead.reason, + }); } const provider = bindingRead.value?.provider; if (typeof provider !== "string" || provider.length === 0) { diff --git a/plugins/plugin-quality/skills/audit/reference/session-mode.md b/plugins/plugin-quality/skills/audit/reference/session-mode.md index 6c0037f90ef..dd7fb838ce6 100644 --- a/plugins/plugin-quality/skills/audit/reference/session-mode.md +++ b/plugins/plugin-quality/skills/audit/reference/session-mode.md @@ -102,8 +102,8 @@ carries a readiness label, decided at step 4: assigns `agent-ready` on its own judgment. The two names are roles, not literal labels. Read each from the target repository's tracker -binding, `config.role_labels` under `tracker:` in its `docs/conventions/work-items.yaml` (else the -legacy `.work-item-tracker.json`): `agent-ready` is the `autonomous-eligible` role and +binding, `config.role_labels` under `tracker:` in its `docs/conventions/work-items.yaml`: +`agent-ready` is the `autonomous-eligible` role and `needs-decision` the `owner-decision` role. With no binding or no entry for a role, use its default: `agent-ready` and `needs-owner-decision`, unless the repository declares others. Where the sink has no labels, keep the rule and state the decision in the item. diff --git a/plugins/source-control/skills/babysit-loop/SKILL.md b/plugins/source-control/skills/babysit-loop/SKILL.md index 75c3c9388fc..a53fd8c758b 100644 --- a/plugins/source-control/skills/babysit-loop/SKILL.md +++ b/plugins/source-control/skills/babysit-loop/SKILL.md @@ -226,8 +226,8 @@ Escalation is the convention's contract ([`docs/contracts/loop-lane/README.md`](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/docs/contracts/loop-lane/README.md) §2), held by citation: a tracker item carrying the human-gated role label, resolved from the `config.role_labels` -map of the consumer's tracker binding (the `tracker` mapping of `docs/conventions/work-items.yaml`, -else the legacy JSON binding), never compared as a literal (when no binding is +map of the consumer's tracker binding (the `tracker` mapping of `docs/conventions/work-items.yaml`), +never compared as a literal (when no binding is present, the canonical `needs-human` default applies with a loud notice), plus a machine-marked escalation comment whose first line is ``. That marker grammar is the diff --git a/plugins/source-control/skills/babysit-prs/reference/safety.md b/plugins/source-control/skills/babysit-prs/reference/safety.md index 41b4c4a223d..96da838364e 100644 --- a/plugins/source-control/skills/babysit-prs/reference/safety.md +++ b/plugins/source-control/skills/babysit-prs/reference/safety.md @@ -100,7 +100,7 @@ value and its unset fallback. - Never update the branch of a PR carrying the tracker's standards-sync role label (`standards-sync` unless the tracker binding's `config.role_labels` maps it, as `scripts/babysit_tracker_binding.py` reads it on the default branch: the `tracker` mapping of - `docs/conventions/work-items.yaml`, else the legacy JSON binding at the root): the sync App + `docs/conventions/work-items.yaml`): the sync App is its only writer, and a merge commit from the base fails its managed-files guard, so the sync cannot re-arm. `refresh_pr_branch.py` refuses it, and the unattended enqueue skips its update from the base (`checkInputs.updateSkipped`) and enqueues as after a refused update. An diff --git a/plugins/source-control/skills/babysit-prs/scripts/babysit_tracker_binding.py b/plugins/source-control/skills/babysit-prs/scripts/babysit_tracker_binding.py index 45440993e3d..bfea87c9a03 100755 --- a/plugins/source-control/skills/babysit-prs/scripts/babysit_tracker_binding.py +++ b/plugins/source-control/skills/babysit-prs/scripts/babysit_tracker_binding.py @@ -1,25 +1,21 @@ #!/usr/bin/env python3 """The work-item tracker binding, as every babysit script reads it. -The binding is the `tracker` mapping of `YAML_PATH`. A repository that has not -moved it there is still bound by the legacy `LEGACY_PATH` at its root, the -fallback the work-items seam keeps (its CONTRACT.md "Setup (binding file)"; -ADR 0075 fix 11). The babysit scripts read it from the default branch (the -admission label at the pull request's base branch), at the default convention -home `docs/conventions`, where they read `pr-pipeline.yaml` too. +The binding is the `tracker` mapping of `YAML_PATH` (the work-items seam's +CONTRACT.md "Setup (binding file)"). A repository root `.work-item-tracker.json` +no longer binds: `/work-items:setup apply` copies it into `YAML_PATH`. The +babysit scripts read the binding from the default branch (the admission label at +the pull request's base branch), at the default convention home +`docs/conventions`, where they read `pr-pipeline.yaml` too. """ from __future__ import annotations -import json from collections.abc import Callable, Mapping -from babysit_util import json_object - YAML_PATH = "docs/conventions/work-items.yaml" -LEGACY_PATH = ".work-item-tracker.json" -# Each file that can bind the tracker, in the order `role_label` tries them. -PATHS = (YAML_PATH, LEGACY_PATH) +# Each file that can bind the tracker. +PATHS = (YAML_PATH,) class BindingError(ValueError): @@ -38,28 +34,15 @@ def role_label( `read(path)` is the file's text, or None when the file is absent; its failures pass through. `records(text)` is the YAML parser's `dotted.key -> value` records, and raises on a document the parser - rejects. The YAML file binds when it sets any `tracker` key, else the - legacy file does; an empty legacy file maps nothing. Raises - `BindingError` when `tracker` is a scalar or the legacy file is not - JSON.""" + rejects. The YAML file binds when it sets any `tracker` key. Raises + `BindingError` when `tracker` is a scalar.""" text = read(YAML_PATH) - if text is not None: - found = records(text) - if "tracker" in found: - raise BindingError(f"{YAML_PATH}: tracker is not a mapping") - if any(key.startswith("tracker.") for key in found): - return _label(found.get(f"tracker.config.role_labels.{role}"), default) - text = read(LEGACY_PATH) - if text is None or not text.strip(): + if text is None: return default - try: - config = json.loads(text) - except ValueError as exc: - raise BindingError(f"{LEGACY_PATH} is not JSON: {exc}") from exc - roles = json_object( - json_object(json_object(config).get("config")).get("role_labels") - ) - return _label(roles.get(role), default) + found = records(text) + if "tracker" in found: + raise BindingError(f"{YAML_PATH}: tracker is not a mapping") + return _label(found.get(f"tracker.config.role_labels.{role}"), default) def _label(value: object, default: str) -> str: diff --git a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_merge_policy.py b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_merge_policy.py index 5f70de76782..f4f36e92246 100644 --- a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_merge_policy.py +++ b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_merge_policy.py @@ -85,7 +85,7 @@ TEAM_YAML = "docs/conventions/source-control.yaml" # The retired legacy file the gate no longer reads. RETIRED_SOURCE_CONTROL = ".claude/source-control.md" -TRACKER_LEGACY = policy.tracker_binding.LEGACY_PATH +TRACKER_YAML = policy.tracker_binding.YAML_PATH # The YAML files `GhFake` answers absent when a test sets none. UNSET_YAML = (TEAM_YAML, policy.tracker_binding.YAML_PATH) @@ -2641,12 +2641,14 @@ def test_the_admission_label_is_the_resolved_one(self) -> None: def test_the_admission_label_reads_the_tracker_config(self) -> None: gh = GhFake().install(self) - path = f"repos/{REPO}/contents/{TRACKER_LEGACY}?ref=main" - mapped = {"config": {"role_labels": {"autonomous-eligible": "go"}}} + path = f"repos/{REPO}/contents/{TRACKER_YAML}?ref=main" + mapped = ( + "tracker:\n config:\n role_labels:\n autonomous-eligible: go\n" + ) gh.rest[path] = { "type": "file", "encoding": "base64", - "content": base64.b64encode(json.dumps(mapped).encode()).decode(), + "content": base64.b64encode(mapped.encode()).decode(), } self.assertEqual(policy.admission_label("owner", "repo", "main"), "go") gh.rest[path] = 404 @@ -2739,7 +2741,7 @@ def test_an_admission_label_with_no_labeled_event_is_refused(self) -> None: self.assertEqual(eligibility().reason, "issue-not-vouched:no-labeled-event") def tracker(self, gh: GhFake, text: str) -> None: - gh.rest[f"repos/{REPO}/contents/{TRACKER_LEGACY}?ref=main"] = { + gh.rest[f"repos/{REPO}/contents/{TRACKER_YAML}?ref=main"] = { "type": "file", "encoding": "base64", "content": base64.b64encode(text.encode()).decode(), @@ -2764,19 +2766,21 @@ def test_the_production_default_reads_the_tracker_at_the_base(self) -> None: "created_at": "2026-10-11T09:00:00Z", } ] - mapped = {"config": {"role_labels": {"autonomous-eligible": "ready-for-agent"}}} - self.tracker(gh, json.dumps(mapped)) + self.tracker( + gh, + "tracker:\n config:\n role_labels:\n autonomous-eligible: ready-for-agent\n", + ) production = policy.eligibility( REPO, 7, agent_identity=IDENTITY, permission=HUMANS ) self.assertTrue(production.eligible, production.reason) - def test_a_non_json_tracker_config_refuses_unreadable(self) -> None: + def test_a_tracker_config_that_does_not_parse_refuses_unreadable(self) -> None: gh = self.outside_issue(["agent-ready"], {"login": OPERATOR, "type": "User"}) gh.graphql["PolicyEligibility"]["repository"]["pullRequest"]["baseRefName"] = ( "main" ) - self.tracker(gh, "{ not json") + self.tracker(gh, "tracker: {provider: github}\n") result = policy.eligibility(REPO, 7, agent_identity=IDENTITY, permission=HUMANS) self.assertFalse(result.eligible) self.assertTrue(result.reason.startswith("unreadable:"), result.reason) diff --git a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_postmerge.py b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_postmerge.py index 7574724ebce..411405070e9 100644 --- a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_postmerge.py +++ b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_postmerge.py @@ -5062,8 +5062,8 @@ def answer(args: list[str]) -> subprocess.CompletedProcess[str]: return pm._role_label("o", "r", **kw) def test_a_mapped_label(self) -> None: - legacy = {"config": {"role_labels": {"human-gated": " human-review "}}} - files = {pm.tracker_binding.LEGACY_PATH: json.dumps(legacy)} + yaml = "tracker:\n config:\n role_labels:\n human-gated: ' human-review '\n" + files = {pm.tracker_binding.YAML_PATH: yaml} self.assertEqual(self._read_files(files, strict=True), "human-review") def test_a_label_the_yaml_binding_maps(self) -> None: diff --git a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_risk.py b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_risk.py index 294f486f241..54635f120a4 100644 --- a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_risk.py +++ b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_risk.py @@ -1303,7 +1303,7 @@ def test_instructions_hooks_and_scripts_merge(self): "scripts/x.sh", "lib/x.sh", ".gitleaks.toml", - babysit_tracker_binding.LEGACY_PATH, + babysit_tracker_binding.YAML_PATH, ".node-version", ".python-version", ".gitattributes", diff --git a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_tracker_binding.py b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_tracker_binding.py index 2ac37ff16a9..b770469825e 100644 --- a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_tracker_binding.py +++ b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_tracker_binding.py @@ -1,12 +1,11 @@ """The tracker binding reader: the `tracker` mapping of -docs/conventions/work-items.yaml binds first, the legacy root -.work-item-tracker.json binds a repository that has not moved it, and with -neither the caller's default applies. +docs/conventions/work-items.yaml binds, a root .work-item-tracker.json never +does, and with no binding the caller's default applies. Expected values come from the work-items seam's binding discovery (plugins/work-items/tools/work-item-tracker/lib/binding.sh `wit_find_binding`): -a YAML file with no `tracker` key falls through to the legacy file, and a YAML -file that does not parse is an error, never a fallback. +a YAML file with no `tracker` key binds nothing, and a YAML file that does not +parse is an error. """ from __future__ import annotations @@ -23,16 +22,27 @@ import babysit_tracker_binding as binding YAML = "docs/conventions/work-items.yaml" -LEGACY = ".work-item-tracker.json" +RETIRED = ".work-item-tracker.json" ROLE = "human-gated" DEFAULT = "needs-human" -def legacy(label: object) -> str: +def retired(label: object) -> str: config = {"role_labels": {ROLE: label}} return json.dumps({"schema_version": "1.0", "config": config}) +def yaml_binding(label: str) -> str: + return ( + "tracker:\n" + " schema_version: '1.0'\n" + " provider: github\n" + " config:\n" + " role_labels:\n" + f" {ROLE}: {label}\n" + ) + + class RoleLabel(unittest.TestCase): def label(self, files: dict[str, str]) -> str: self.reads: list[str] = [] @@ -44,63 +54,48 @@ def read(path: str) -> str | None: return binding.role_label(read, policy.yaml_records, ROLE, DEFAULT) def test_the_paths_are_the_ones_the_seam_reads(self) -> None: - self.assertEqual(binding.PATHS, (YAML, LEGACY)) + self.assertEqual(binding.PATHS, (YAML,)) def test_the_yaml_binding_maps_the_role(self) -> None: - text = ( - "tracker:\n" - " schema_version: '1.0'\n" - " provider: github\n" - " config:\n" - " role_labels:\n" - f" {ROLE}: ' human-review '\n" - ) - files = {YAML: text, LEGACY: legacy("from-json")} + files = {YAML: yaml_binding("' human-review '"), RETIRED: retired("from-json")} self.assertEqual(self.label(files), "human-review") self.assertEqual(self.reads, [YAML]) def test_a_yaml_binding_that_maps_no_role_is_the_default(self) -> None: - # It binds, so the legacy file is never read. - files = {YAML: "tracker:\n provider: github\n", LEGACY: legacy("from-json")} + files = {YAML: "tracker:\n provider: github\n", RETIRED: retired("from-json")} self.assertEqual(self.label(files), DEFAULT) self.assertEqual(self.reads, [YAML]) - def test_a_yaml_file_with_no_tracker_key_falls_back(self) -> None: + def test_a_yaml_file_with_no_tracker_key_is_the_default(self) -> None: other = "work_priority_order:\n - 'priority: high'\n" - files = {YAML: other, LEGACY: legacy("from-json")} - self.assertEqual(self.label(files), "from-json") - self.assertEqual(self.reads, [YAML, LEGACY]) + files = {YAML: other, RETIRED: retired("from-json")} + self.assertEqual(self.label(files), DEFAULT) + self.assertEqual(self.reads, [YAML]) - def test_with_no_yaml_file_the_legacy_file_binds(self) -> None: - self.assertEqual(self.label({LEGACY: legacy(" from-json ")}), "from-json") + def test_a_root_json_file_alone_never_binds(self) -> None: + self.assertEqual(self.label({RETIRED: retired("from-json")}), DEFAULT) + self.assertEqual(self.reads, [YAML]) - def test_a_blank_or_non_string_label_is_the_default(self) -> None: - for value in ("", " ", 7, None): + def test_a_blank_label_is_the_default(self) -> None: + for value in ("''", "' '"): with self.subTest(value=value): - self.assertEqual(self.label({LEGACY: legacy(value)}), DEFAULT) + self.assertEqual(self.label({YAML: yaml_binding(value)}), DEFAULT) - def test_with_neither_file_the_default_applies(self) -> None: + def test_with_no_file_the_default_applies(self) -> None: self.assertEqual(self.label({}), DEFAULT) - self.assertEqual(self.reads, [YAML, LEGACY]) + self.assertEqual(self.reads, [YAML]) def test_a_yaml_file_that_does_not_parse_raises(self) -> None: - files = {YAML: "tracker: {provider: github}\n", LEGACY: legacy("from-json")} + files = {YAML: "tracker: {provider: github}\n"} with self.assertRaises(policy.PolicyReadError): self.label(files) def test_a_scalar_tracker_key_raises(self) -> None: - # The seam counts it as a binding and rejects it; it never falls back. - files = {YAML: "tracker: github\n", LEGACY: legacy("from-json")} + # The seam counts it as a binding and rejects it. + files = {YAML: "tracker: github\n"} with self.assertRaises(binding.BindingError): self.label(files) - def test_an_empty_legacy_file_is_the_default(self) -> None: - self.assertEqual(self.label({LEGACY: " \n"}), DEFAULT) - - def test_a_legacy_file_that_is_not_json_raises(self) -> None: - with self.assertRaises(binding.BindingError): - self.label({LEGACY: "{ not json"}) - def test_a_failed_read_passes_through(self) -> None: def read(path: str) -> str | None: raise RuntimeError(f"{path}: HTTP 502") @@ -110,7 +105,7 @@ def read(path: str) -> str | None: class SkipKeysOnTheBinding(unittest.TestCase): - def test_the_skip_key_covers_both_binding_files(self) -> None: + def test_the_skip_key_covers_the_binding_file(self) -> None: self.assertLessEqual(set(binding.PATHS), set(skip.MAIN_PATHS)) diff --git a/plugins/source-control/skills/pull-request/reference/create.md b/plugins/source-control/skills/pull-request/reference/create.md index 22844c0a76d..ec8adef6a12 100644 --- a/plugins/source-control/skills/pull-request/reference/create.md +++ b/plugins/source-control/skills/pull-request/reference/create.md @@ -164,7 +164,7 @@ ISSUE_NUM=$(bash "/scripts/parse-branch-issue.sh" "" '` with the resolved ERE. Its last capture group must resolve to an issue number or a tracker key such as `SW2-1234` (a pattern with no capture group, or a capture of another shape, prints nothing and takes the no-closure path); configure a scheme that captures the reference wherever it sits, e.g. `^[^/]+/([0-9]+)-` for `alice/1234-slug` or `-([0-9]+)$` for `feat/add-widget-1234`. -**Tracker-bound path.** When the `work-items` plugin is installed and the repository has a work-item tracker binding (the `tracker` mapping of `/work-items.yaml`, or the legacy JSON binding at the root), the bound adapter owns the link text (`Closes #42`, `Closes ENG-123`, a Jira key) and the default branch grammar, so this flow writes neither. Parse with `--no-default` as the third positional (pass `''` as the second when no userConfig value is set), so only a consumer `branch_issue_pattern` applies: +**Tracker-bound path.** When the `work-items` plugin is installed and the repository has a work-item tracker binding (the `tracker` mapping of `/work-items.yaml`), the bound adapter owns the link text (`Closes #42`, `Closes ENG-123`, a Jira key) and the default branch grammar, so this flow writes neither. Parse with `--no-default` as the third positional (pass `''` as the second when no userConfig value is set), so only a consumer `branch_issue_pattern` applies: ```bash ISSUE_NUM=$(bash "/scripts/parse-branch-issue.sh" "" '' --no-default); PARSE_RC=$? diff --git a/plugins/work-items/README.md b/plugins/work-items/README.md index ebadb771c7f..51268a7be97 100644 --- a/plugins/work-items/README.md +++ b/plugins/work-items/README.md @@ -131,10 +131,9 @@ enough that one skill no longer predicts its contents. which owns the copy from then on. On a re-run, setup fast-forwards a copy nobody edited and stops with the diff on one somebody did. The consuming repo declares its active provider under the `tracker` key of `/work-items.yaml` in its convention home - (default `docs/conventions`). Until `/work-items:setup` writes that key, it writes a legacy - repo-root JSON binding, which the seam still reads with a notice. A personal override beside the - binding (`work-items.local.yaml` beside the YAML one; the legacy JSON override, which setup - gitignores, beside the legacy one) may carry the per-user + (default `docs/conventions`). A repo-root `.work-item-tracker.json` no longer binds: + `/work-items:setup apply` copies it under that key. A personal override beside the + binding (`work-items.local.yaml`) may carry the per-user lease TTL and the per-provider auth identity: `auth_email` and `auth_env` for jira, `auth_env` for linear, and `auth_env` for gitea. A repo may add or shadow an adapter consumer-local at diff --git a/plugins/work-items/evals/su-apply-copies-legacy-json-binding/prompt.md b/plugins/work-items/evals/su-apply-copies-legacy-json-binding/prompt.md index 3c97dbd0f7c..c619eedd6ce 100644 --- a/plugins/work-items/evals/su-apply-copies-legacy-json-binding/prompt.md +++ b/plugins/work-items/evals/su-apply-copies-legacy-json-binding/prompt.md @@ -4,7 +4,7 @@ tags: [setup, apply, migration, hard] max_turns: 15 timeout_seconds: 420 allowed_tools: [Read, Glob, Grep, Skill] -expected_outcome: 'Check reports that the binding exists only in the legacy JSON file. Apply ensures a .gitignore rule covers docs/conventions/work-items.local.yaml and announces that edit, then runs migrate-binding.sh, which appends a tracker mapping to docs/conventions/work-items.yaml below ai_disclaimer and writes docs/conventions/work-items.local.yaml with the personal override''s lease_ttl_hours 6. It announces both copies and says the JSON files stay unchanged because older installed plugin versions and the repository''s own tooling may still read only them, and that later binding edits land only in the YAML. It does not delete, rewrite or ask the user to edit .work-item-tracker.json, and it keeps the copied binding rather than re-binding a provider.' +expected_outcome: 'Check reports that the binding exists only in the legacy JSON file. Apply ensures a .gitignore rule covers docs/conventions/work-items.local.yaml and announces that edit, then runs migrate-binding.sh, which appends a tracker mapping to docs/conventions/work-items.yaml below ai_disclaimer and writes docs/conventions/work-items.local.yaml with the personal override''s lease_ttl_hours 6. It announces both copies and says the JSON files stay unchanged because a vendored seam or the repository''s own tooling may still read only them, and that later binding edits land only in the YAML. It does not delete, rewrite or ask the user to edit .work-item-tracker.json, and it keeps the copied binding rather than re-binding a provider.' --- /work-items:setup apply diff --git a/plugins/work-items/reference/label-taxonomy.md b/plugins/work-items/reference/label-taxonomy.md index e5643812a96..cdb679a2631 100644 --- a/plugins/work-items/reference/label-taxonomy.md +++ b/plugins/work-items/reference/label-taxonomy.md @@ -28,7 +28,7 @@ These axes work in any repo and don't change per team. Do not snapshot their mem Four labels are **canonical roles**: skill and action prose speaks the role name, and the repo-actual label string resolves from the tracker binding (the `tracker` mapping of -`/work-items.yaml`, else the legacy repo-root JSON binding), key `config.role_labels`. When the key (or an individual role entry) is absent, the defaults below +`/work-items.yaml`), key `config.role_labels`. When the key (or an individual role entry) is absent, the defaults below apply, so existing repos need zero migration. | Role | Default label | What it marks | diff --git a/plugins/work-items/reference/tracker-seam.md b/plugins/work-items/reference/tracker-seam.md index 4dec5963343..d02a6d0fb10 100644 --- a/plugins/work-items/reference/tracker-seam.md +++ b/plugins/work-items/reference/tracker-seam.md @@ -344,5 +344,4 @@ operations reference. For GitHub, that is [`${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md`](${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md) (universal + repo-specific groups). - Bind the provider. The active provider lives under the `tracker` key of `/work-items.yaml`, - else in the legacy repo-root JSON binding `/work-items:setup` writes; these skills read the - binding but never write it. + which `/work-items:setup` writes; these skills read the binding but never write it. diff --git a/plugins/work-items/schemas/work-items.schema.json b/plugins/work-items/schemas/work-items.schema.json index 7af5812cf8d..67c07da30a6 100644 --- a/plugins/work-items/schemas/work-items.schema.json +++ b/plugins/work-items/schemas/work-items.schema.json @@ -151,7 +151,7 @@ "type": "object", "additionalProperties": false, "required": ["schema_version", "provider", "config"], - "description": "The work-item tracker binding: the one active provider the seam (tools/work-item-tracker/CONTRACT.md, Setup) dispatches to. Absent: the seam reads the legacy repo-root JSON binding while the migration runs, with a notice, and stops pointing at /work-items:setup when neither exists. In work-items.local.yaml only config.lease_ttl_hours, config.lease_ttl_minutes, the per-provider auth_email/auth_env keys and docs may be set.", + "description": "The work-item tracker binding: the one active provider the seam (tools/work-item-tracker/CONTRACT.md, Setup) dispatches to. Absent: nothing binds, and every seam verb exits 3 naming /work-items:setup apply, which also copies a retired repo-root .work-item-tracker.json here. In work-items.local.yaml only config.lease_ttl_hours, config.lease_ttl_minutes, the per-provider auth_email/auth_env keys and docs may be set.", "properties": { "schema_version": { "type": "string", diff --git a/plugins/work-items/skills/attend-queue/SKILL.md b/plugins/work-items/skills/attend-queue/SKILL.md index 87d8ef4b783..104e5299689 100644 --- a/plugins/work-items/skills/attend-queue/SKILL.md +++ b/plugins/work-items/skills/attend-queue/SKILL.md @@ -144,7 +144,7 @@ writes to ("Claim before the first write"), and `claim` backs off from an earlie the same login, so a row this lane claimed first would make triage skip it. **Binding.** `claim` and session-start `reclaim` are seam coordination verbs, if -the binding (`tracker:` in `/work-items.yaml`, else the legacy repo-root JSON binding) does not resolve, surface the same actionable choice as +the binding (`tracker:` in `/work-items.yaml`) does not resolve, surface the same actionable choice as `/work-items:work` before the first coordination verb (session-start reclaim, then row `claim`): **(1) setup was never run** → run `/work-items:setup`; **(2) deliberate gh-native mode** → proceed for provider-mechanic reads only, accepting that concurrent attended sessions have no race-safe row diff --git a/plugins/work-items/skills/onboard-adapter/scripts/templates/common.sh.tmpl b/plugins/work-items/skills/onboard-adapter/scripts/templates/common.sh.tmpl index d1903244751..c4231a615eb 100644 --- a/plugins/work-items/skills/onboard-adapter/scripts/templates/common.sh.tmpl +++ b/plugins/work-items/skills/onboard-adapter/scripts/templates/common.sh.tmpl @@ -180,7 +180,7 @@ wit_need_@@PROVIDER_FUNC@@_config() { name="$(basename "${BASH_SOURCE[1]}")" binding="$(wit_find_binding)" || { - printf '%s: no binding found (tracker: in /work-items.yaml, or the legacy repo-root JSON binding) — see CONTRACT.md Setup\n' "$name" >&2 + printf '%s: %s; see CONTRACT.md Setup\n' "$name" "$(wit_no_binding_reason)" >&2 exit "$EX_CONFIG" } ejson="$(wit_effective_binding_json "$binding")" || diff --git a/plugins/work-items/skills/setup/SKILL.md b/plugins/work-items/skills/setup/SKILL.md index f6e42583785..2e51f59ee54 100644 --- a/plugins/work-items/skills/setup/SKILL.md +++ b/plugins/work-items/skills/setup/SKILL.md @@ -46,15 +46,16 @@ bash "${CLAUDE_PLUGIN_ROOT}/lib/resolve-convention-home.sh" --root "$ROOT" Exit `0` prints the home; exit `1` (no pointer) means `docs/conventions`. Exit `4` (the repository declined a home) or `5` (the root is `$HOME`, an ancestor of it, or not a git work tree) means no home -applies: the seam then reads only the legacy JSON file, so setup writes no binding and reports that the +applies: nothing can bind the repository there, so setup writes no binding and reports that the repository needs a convention home first. Exit `3` (a broken pointer) is a `check` FAIL naming the resolver's message. With `` resolved: - `BINDING` is the `tracker` key of `$ROOT//work-items.yaml`. That file may hold other work-items settings; setup edits only its `tracker` mapping. - `OVERRIDE` is the gitignored personal override `$ROOT//work-items.local.yaml` (its `tracker` mapping). -- `LEGACY` is `$ROOT/.work-item-tracker.json` and its personal override `$ROOT/.work-item-tracker.local.json`: - read, and copied by step 1 of "Provider binding", never written. +- `LEGACY` is the retired binding file `$ROOT/.work-item-tracker.json` and its personal override + `$ROOT/.work-item-tracker.local.json`. The seam no longer reads them; step 1 of "Provider binding" + copies them, and setup never writes them. - `SCHEDULE` is `$ROOT/.github/recurring-schedule.json`. The binding and schedule are version-controlled and shared by the whole team. They belong in the @@ -65,8 +66,8 @@ consumer's tree, never in the plugin directory or any machine-local state. `apply` runs this first, the recurring-schedule and role-label passes below resolve the binding; the `check` binding probe validates it read-only. The tracker seam runs against exactly one provider per repo, declared under the `tracker` key of the tracked `/work-items.yaml` (resolved as `BINDING` -above); every seam verb resolves the bound provider from it, falling back to the legacy JSON file -while that file is the only binding, and with no binding the seam hard-errors (exit 3). The seam **ships with this plugin** and bundles two adapters, `github` and `local-markdown`; +above); every seam verb resolves the bound provider from it, and with no binding the seam hard-errors +(exit 3) with a message naming `/work-items:setup apply`. The seam **ships with this plugin** and bundles two adapters, `github` and `local-markdown`; for those, installing the plugin is enough and a repo only declares which one it uses. `jira`, `gitea`, and `linear` ship as **seeds**, which step 4 copies into the consuming repo, where the repo owns them. Binding shape, discovery, and @@ -92,9 +93,8 @@ when this pass must stop instead of guessing. `empty`). Exit `1` names a value it cannot copy (a null, an empty object, a list of mappings); `3` an `OVERRIDE` path that no `.gitignore` rule covers or that the index holds; `4` a repository where no home applies or whose pointer is broken. Each writes nothing and is a finding to report. The JSON files - stay where they are, unchanged: older installed plugin versions and the repository's own tooling - may still read only them. Say so, and say that a later - binding edit lands only in the YAML file. Then treat the copy as the current binding. If neither + stay where they are, unchanged, and no longer bind: a vendored seam or the repository's own tooling + may still read only them. Say so, and say that a later binding edit lands only in the YAML file. Then treat the copy as the current binding. If neither file binds the repository, say so and continue to the interview. 2. **Choose the provider**, recommendation first. One line each below; the selection detail, how to verify a `github` bind, what each provider can and cannot do, and every config key, diff --git a/plugins/work-items/skills/setup/reference/check.md b/plugins/work-items/skills/setup/reference/check.md index 0ea4a6ea150..8908e9c0d3c 100644 --- a/plugins/work-items/skills/setup/reference/check.md +++ b/plugins/work-items/skills/setup/reference/check.md @@ -14,24 +14,26 @@ check. reference's install remediation, the schedule snippets parse with `jq` unconditionally. 2. **Tracker provider binding**, resolve the convention home and `BINDING` (SKILL.md "Resolving the paths"). A broken pointer (resolver exit `3`) is FAIL, naming the resolver's message. Where no home - applies (exit `4` or `5`) → INFO: the seam reads only the legacy JSON file there, and setup - writes no binding until the repository binds a convention home. Then branch on which files bind: + applies (exit `4` or `5`) → INFO: nothing binds the repository there, and setup writes no + binding until the repository binds a convention home. Then branch on which files bind: - **Neither** `BINDING` nor `LEGACY` → INFO: the tracker seam is not bound, so every seam verb hard-errors (exit 3) until `apply` seeds it, and the role remap has nothing to configure; the remediation is `/work-items:setup apply`. - **`LEGACY` only** (`.work-item-tracker.json` exists and `/work-items.yaml` has no - `tracker` key) → INFO: the binding exists only in the legacy JSON file, which the seam still - reads with a notice; `/work-items:setup apply` copies it into `/work-items.yaml` and - keeps the JSON file. Validate the JSON file as below. - - **`BINDING`, with `LEGACY` too** → INFO: the JSON file is kept for the readers that still read - only it (SKILL.md "Provider binding" step 1); name any `provider` or `config` value that - differs between the two. When a `.work-item-tracker.local.json` exists and + `tracker` key) → FAIL: the binding exists only in the retired JSON file, which the seam no + longer reads, so every seam verb hard-errors (exit 3); the remediation is + `/work-items:setup apply`, which copies it into `/work-items.yaml` and keeps the JSON + file. Validate the JSON file as below, so the copy's findings show up front. + - **`BINDING`, with `LEGACY` too** → INFO: the JSON file no longer binds and is kept only for + readers outside the plugin that still read it (SKILL.md "Provider binding" step 1); name any + `provider` or `config` value that differs between the two. When a `.work-item-tracker.local.json` exists and `/work-items.local.yaml` has no `tracker` key, say the seam no longer reads that personal override, since it reads the one beside the YAML binding; `apply` copies it. Validate the binding that binds without mutating: `/work-items.yaml` parses with `awk -f /skills/triage/scripts/yaml-subset.awk` and its `tracker` mapping carries no - key outside `schema_version`, `provider`, `config` and `docs`, or the legacy file parses as JSON. + key outside `schema_version`, `provider`, `config` and `docs`, or, for `LEGACY` only, the JSON + file parses as JSON. It carries `schema_version` and a `provider`, and that provider resolves to a bundled adapter (`github`, `local-markdown`) or a consumer-local one at `/tools/work-item-tracker/adapters//` ([`tracker-seam.md`](../../../reference/tracker-seam.md) @@ -43,8 +45,8 @@ check. unknown/unresolvable provider, or a missing required config key is FAIL, naming what is wrong. The ignore rule for `OVERRIDE` (`/work-items.local.yaml`) is required whether or not the personal override file exists. A present personal override must parse (the YAML file with the same parser, its - `tracker` mapping; a legacy `.work-item-tracker.local.json`, still read beside a legacy-only - binding, as JSON), carry only allowlisted keys + `tracker` mapping; a legacy `.work-item-tracker.local.json`, which `apply` copies beside a + legacy-only binding, as JSON), carry only allowlisted keys (CONTRACT.md "Setup (binding file)"), and be gitignored, never tracked. Otherwise FAIL. Ignored and untracked are **two independent probes**: run the same pair "Provider binding" step 5 uses, per [`reference/personal-override-ignore-probes.md`](personal-override-ignore-probes.md). A bare diff --git a/plugins/work-items/skills/track/SKILL.md b/plugins/work-items/skills/track/SKILL.md index 6541d634a26..272a6be9e26 100644 --- a/plugins/work-items/skills/track/SKILL.md +++ b/plugins/work-items/skills/track/SKILL.md @@ -27,7 +27,7 @@ actions below: - **Role-label resolution is an action-entry invariant.** `add`, `due`, `recheck`, and `audit` query, create, or filter items by a canonical role. Resolve each role from `config.role_labels` of the binding the seam resolves (the `tracker` mapping of - `/work-items.yaml`, else the legacy repo-root JSON binding) at action entry and use the resolved strings in + `/work-items.yaml`) at action entry and use the resolved strings in every query. An absent file or entry falls back to the documented default without a warning (a conforming binding omits entries that keep their default); a present malformed, empty, or non-string value is a hard stop. diff --git a/plugins/work-items/skills/track/actions/due.md b/plugins/work-items/skills/track/actions/due.md index 3adc7c18b65..81da116ee2e 100644 --- a/plugins/work-items/skills/track/actions/due.md +++ b/plugins/work-items/skills/track/actions/due.md @@ -11,8 +11,8 @@ Show recurring items that are past their `next_due` date. ## Workflow 1. **Resolve the recurring-maintenance role label before any tracker read.** Read - the binding the seam resolves (the `tracker` mapping of `/work-items.yaml`, else the - legacy repo-root JSON binding) at action entry and resolve + the binding the seam resolves (the `tracker` mapping of `/work-items.yaml`) at action + entry and resolve `config.role_labels["recurring-maintenance"]`; use `recurring` when the file or entry is absent (the documented default, no warning). Stop on a malformed, empty, or non-string configured value. Use the resolved string in every adapter filter below. diff --git a/plugins/work-items/skills/track/actions/recheck.md b/plugins/work-items/skills/track/actions/recheck.md index fca8c48125f..676eaf9dcfa 100644 --- a/plugins/work-items/skills/track/actions/recheck.md +++ b/plugins/work-items/skills/track/actions/recheck.md @@ -11,8 +11,8 @@ Update a recurring item's `last_checked` and `next_due` dates after completing a ## Workflow 1. **Resolve the recurring-maintenance role label before any tracker read.** Read - the binding the seam resolves (the `tracker` mapping of `/work-items.yaml`, else the - legacy repo-root JSON binding) at action entry and resolve + the binding the seam resolves (the `tracker` mapping of `/work-items.yaml`) at action + entry and resolve `config.role_labels["recurring-maintenance"]`; use `recurring` when the file or entry is absent (the documented default, no warning). Stop on a malformed, empty, or non-string configured value. Use the resolved string in the item read below. diff --git a/plugins/work-items/skills/triage/SKILL.md b/plugins/work-items/skills/triage/SKILL.md index 4f680b57a7e..477b5c9ed02 100644 --- a/plugins/work-items/skills/triage/SKILL.md +++ b/plugins/work-items/skills/triage/SKILL.md @@ -153,7 +153,7 @@ reporter-info listing, the sweep's list and count, and every read before the fir name; the next `reclaim` after a further quiet TTL frees it. Exit `7` means the lease is no longer this run's; leave it and the assignee. -When the binding (`tracker:` in `/work-items.yaml`, else the legacy repo-root JSON binding) does not resolve, give the choice +When the binding (`tracker:` in `/work-items.yaml`) does not resolve, give the choice [`tracker-seam.md`](${CLAUDE_PLUGIN_ROOT}/reference/tracker-seam.md) "Provider-neutral over the seam" names before the first write: run `/work-items:setup`, or, in a deliberate gh-native mode, write unclaimed and report that no race-safe claim was held. An unbound local-markdown store writes diff --git a/plugins/work-items/skills/work/SKILL.md b/plugins/work-items/skills/work/SKILL.md index 1c1368436b8..19fe5e75b03 100644 --- a/plugins/work-items/skills/work/SKILL.md +++ b/plugins/work-items/skills/work/SKILL.md @@ -71,7 +71,7 @@ broader grants. The dated basis, the full contract, remediation, and the Step 0's `reclaim` is this lane's **first seam coordination verb**, so the binding-presence entry invariant ([`${CLAUDE_PLUGIN_ROOT}/reference/tracker-seam.md`](${CLAUDE_PLUGIN_ROOT}/reference/tracker-seam.md) "Shared tracker context") is discharged **here, before Step 0 runs**, never left to surface as a raw -`exit 3` mid-reclaim. If the binding (`tracker:` in `/work-items.yaml`, else the legacy repo-root JSON binding) does not resolve, surface the actionable choice +`exit 3` mid-reclaim. If the binding (`tracker:` in `/work-items.yaml`) does not resolve, surface the actionable choice before attempting `reclaim`: **(1) setup was never run** → run `/work-items:setup` to bind the provider; **(2) a deliberate gh-native operating mode** → this lane is coordination-*dependent* (Step 0 `reclaim`, `list-frontier`, and the Step 5 `claim` are all seam verbs that need the binding), so an diff --git a/plugins/work-items/tools/work-item-tracker/CONTRACT.md b/plugins/work-items/tools/work-item-tracker/CONTRACT.md index abbd7474a8f..6c68fa86884 100644 --- a/plugins/work-items/tools/work-item-tracker/CONTRACT.md +++ b/plugins/work-items/tools/work-item-tracker/CONTRACT.md @@ -119,8 +119,8 @@ copy of `resolve-convention-home.sh`; `docs/conventions` when the repository bin and location follow [ADR 0075](../../../../docs/adr/0075-resolve-plugin-settings-through-one-four-layer-cascade-and-a-bound-convention-home.md) fix 11, which supersedes ADR 0015: one team layer in the home, one gitignored personal override -beside it, deliberately no user-global layer. The `tracker` mapping has the same keys as the -legacy JSON file, so every adapter reads the same effective JSON. +beside it, deliberately no user-global layer. The seam turns the `tracker` mapping into one +JSON object, so every adapter reads the same effective JSON. ```yaml tracker: @@ -134,33 +134,21 @@ tracker: The seam reads the file with the plugin's copy of `yaml-subset.awk`, the repository's YAML subset parser. Every scalar reads as a string (the parser drops quotes, so a quoted id stays the id), and every reader takes a number or boolean as text. An empty flow sequence (`[]`) -reads as an empty array, as the legacy JSON file writes it, so `done_category_keys: []` is +reads as an empty array, as JSON writes it, so `done_category_keys: []` is present and judged like the JSON value (jira rejects an empty set) instead of taking the default, and `wont_do: []` keeps its key. One difference remains: a key with no value reads as absent, as though left out, never as a JSON `null`. A key under `tracker` that the schema does not declare, a document the parser rejects (named by file and line), or a rejected value (named by file, key and value; the personal override when it set the value) is exit `3`. -- **Legacy fallback (migration).** While the migration runs, when `/work-items.yaml` has - no `tracker` key (or the repository declined a home), the seam still reads the legacy - repo-root JSON binding (`WIT_LEGACY_BINDING` in `lib/binding.sh`) and prints a one-line - `notice:` on stderr naming it, which asks for it to move under `tracker:`. A repo - root where no home applies (it is `$HOME`, an ancestor of `$HOME`, or not inside a git work - tree: ADR 0075 fix 5) reads no `work-items.yaml` at all, not even under - `docs/conventions`; only the legacy JSON file binds it. The JSON file has the same keys as - the `tracker` mapping: - - ```json - { - "schema_version": "1.0", - "provider": "github", - "config": { "lease_ttl_hours": 24 } - } - ``` - - A `work-items.yaml` that does not parse is still read as the binding and fails, never a - silent fall back to the JSON file. A broken pointer region is reported on stderr and the - seam falls back as if no home applied. +- **No binding.** When `/work-items.yaml` sets no `tracker` key, the repository + declined a home, or no home applies (the root is `$HOME`, an ancestor of `$HOME`, or not + inside a git work tree: ADR 0075 fix 5), nothing binds and every verb exits `3`. The message + (`wit_no_binding_reason` in `lib/binding.sh`) names `/work-items:setup apply` as the fix. + A repo-root `.work-item-tracker.json`, the retired binding file, no longer binds; when one + is present the message names it, and `apply` copies it under `tracker:`. A + `work-items.yaml` that does not parse is still read as the binding and fails. A broken + pointer region is reported on stderr, and the seam then finds no binding. - Discovery: the binding resolves from the **repo root**, meaning `${CLAUDE_PROJECT_DIR}` when set, else `git rev-parse --show-toplevel`. Whichever of the two resolves, that one value governs ALL of the seam's repo-relative resolution (binding read, consumer-local adapter @@ -177,8 +165,9 @@ value (named by file, key and value; the personal override when it set the value per-subdirectory bindings are unsupported until requested. Env override `WORK_ITEM_TRACKER_BINDING=` (tests, conformance). - **Personal override**: an optional gitignored file beside the team binding, - `/work-items.local.yaml` (its `tracker` mapping) beside a YAML binding or - the legacy JSON override beside the legacy file (`wit_override_path` in `lib/binding.sh`), + `/work-items.local.yaml` (its `tracker` mapping) beside a YAML binding, or + `.work-item-tracker.local.json` beside a JSON file the env override names + (`wit_override_path` in `lib/binding.sh`), merges **per-key over an allowlist, deny-by-default**. A personal override with no `tracker` key changes nothing. Overridable keys: `config.lease_ttl_hours`, `config.lease_ttl_minutes` (TTL travels inside each lease record, so a per-user value is coherent), the auth-identity keys of every credentialed diff --git a/plugins/work-items/tools/work-item-tracker/lib/binding.sh b/plugins/work-items/tools/work-item-tracker/lib/binding.sh index fa8cb979874..987f4648f6e 100755 --- a/plugins/work-items/tools/work-item-tracker/lib/binding.sh +++ b/plugins/work-items/tools/work-item-tracker/lib/binding.sh @@ -10,11 +10,12 @@ _WIT_BINDING_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$_WIT_BINDING_LIB_DIR/labels.sh" # The repository's one YAML parser and its convention-home resolver, as the plugin's -# generated copies (scripts/shared-copies.txt). A vendored seam without them reads -# only the legacy JSON binding. +# generated copies (scripts/shared-copies.txt). Without them no binding resolves. WIT_YAML_PARSER="$_WIT_BINDING_LIB_DIR/../../../skills/triage/scripts/yaml-subset.awk" WIT_HOME_RESOLVER="$_WIT_BINDING_LIB_DIR/../../../lib/resolve-convention-home.sh" readonly WIT_DEFAULT_CONVENTION_HOME="docs/conventions" +# The retired repo-root binding file. Discovery never reads it; its presence only +# names the fix (wit_no_binding_reason), and /work-items:setup apply copies it. readonly WIT_LEGACY_BINDING=".work-item-tracker.json" # What an empty flow sequence reads as while the parser reads a binding, which turns # back into []. It opens with a control character no YAML scalar can hold (raw, it is @@ -35,8 +36,8 @@ wit_project_root() { # wit_convention_home : the repo-relative convention home the pointer region # binds, or the default home when the repository binds none. Fails when it declined # a home, when no home applies (exit 5: the root is $HOME, an ancestor of it, or not -# in a git work tree, so only the legacy JSON can bind it), or when the pointer is -# broken (that message passes through to stderr). +# in a git work tree, so nothing can bind it), or when the pointer is broken (that +# message passes through to stderr). wit_convention_home() { local home rc [[ -f "$WIT_HOME_RESOLVER" && -f "$WIT_YAML_PARSER" ]] || return 1 @@ -93,7 +94,7 @@ wit_yaml_records() { } # wit_yaml_has_binding : true when the file sets a `tracker` key, or does not -# parse (so the read names the error rather than falling back to the legacy file). +# parse (so the read names the error rather than reporting no binding). wit_yaml_has_binding() { local records records="$(wit_yaml_records "$1" 2>/dev/null)" || return 0 @@ -104,7 +105,7 @@ wit_yaml_has_binding() { # Every scalar stays a string: the parser drops quotes, so a type guessed from the # text would turn a quoted id such as "10000" into a number. Every reader takes # numbers and booleans as text (jq -r, then a regex or string compare). An empty flow -# sequence stays [], as the legacy JSON file writes it: jira and linear tell a present +# sequence stays [], as a JSON binding writes it: jira and linear tell a present # empty list (a config error) from an absent key (the default) with has(). A key with # no value reads as absent (CONTRACT.md "Setup (binding file)"). wit_yaml_binding_json() { @@ -126,8 +127,8 @@ wit_yaml_binding_json() { } # wit_find_binding: echo the binding file path: the env override, else the `tracker` -# mapping of /work-items.yaml, else the legacy root .work-item-tracker.json. -# Never a CWD-upward climb: a stray ancestor or home binding would silently capture +# mapping of /work-items.yaml. Fails when neither binds; the caller reports +# wit_no_binding_reason. Never a CWD-upward climb: a stray ancestor or home binding would silently capture # every repo beneath it (#2941). wit_find_binding() { if [[ -n "${WORK_ITEM_TRACKER_BINDING:-}" ]]; then @@ -145,8 +146,31 @@ wit_find_binding() { return 0 fi fi - [[ -f "$root/$WIT_LEGACY_BINDING" ]] || return 1 - printf '%s\n' "$root/$WIT_LEGACY_BINDING" + return 1 +} + +# wit_no_binding_reason: one line saying why wit_find_binding found no binding, naming +# /work-items:setup apply as the fix. A retired repo-root .work-item-tracker.json gets +# its own line: apply copies it under tracker: in /work-items.yaml. +wit_no_binding_reason() { + local root home legacy="" + if [[ -n "${WORK_ITEM_TRACKER_BINDING:-}" ]]; then + printf 'no binding file at WORK_ITEM_TRACKER_BINDING=%s; unset it to read /work-items.yaml, which /work-items:setup apply writes\n' "$WORK_ITEM_TRACKER_BINDING" + return 0 + fi + root="$(wit_project_root)" + if [[ -z "$root" ]]; then + printf 'no binding: not in a git work tree and CLAUDE_PROJECT_DIR is unset; run /work-items:setup apply from the repository root\n' + return 0 + fi + [[ -f "$root/$WIT_LEGACY_BINDING" ]] && legacy="$root/$WIT_LEGACY_BINDING no longer binds the tracker, and " + if ! home="$(wit_convention_home "$root" 2>/dev/null)"; then + printf 'no binding: %sno convention home applies under %s (it declines one, is the home directory or an ancestor of it, or is not a git work tree); run /work-items:setup apply from a git work tree that binds a home\n' "$legacy" "$root" + elif [[ -n "$legacy" ]]; then + printf 'no binding: %s no longer binds the tracker; run /work-items:setup apply to copy it under tracker: in %s/work-items.yaml\n' "$root/$WIT_LEGACY_BINDING" "$home" + else + printf 'no binding: %s/%s/work-items.yaml sets no tracker: mapping; run /work-items:setup apply\n' "$root" "$home" + fi } # The only keys the personal override may refine; any other key there is a config @@ -154,8 +178,9 @@ wit_find_binding() { readonly WIT_OVERRIDE_ALLOWED_PATHS='[["config","lease_ttl_hours"],["config","lease_ttl_minutes"],["config","jira","auth_email"],["config","jira","auth_env"],["config","linear","auth_env"],["config","gitea","auth_env"],["docs"]]' # wit_override_path : echo where the personal override sits beside that -# binding (work-items.local.yaml beside a YAML binding, else .work-item-tracker.local.json), -# whether or not it exists. +# binding (work-items.local.yaml beside a YAML binding; beside a JSON file the +# WORK_ITEM_TRACKER_BINDING override names, .work-item-tracker.local.json), whether or +# not it exists. wit_override_path() { local dir name=".work-item-tracker.local.json" [[ "$1" == *.yaml ]] && name="work-items.local.yaml" @@ -173,7 +198,7 @@ wit_find_override() { } # wit_binding_layer_json : one layer as a JSON value: the `tracker` mapping of -# a YAML file, else the whole JSON file. +# a YAML file, else (a JSON file the env override names) the whole file. wit_binding_layer_json() { if [[ "$1" == *.yaml ]]; then wit_yaml_binding_json "$1" @@ -292,9 +317,6 @@ wit_read_binding() { local path="$1" json version provider ttl storage human_gated autonomous_eligible recurring_maintenance container minutes local owner_decision hold_labels hold_states hold_state_labels wayfind json="$(wit_effective_binding_json "$path")" || return 1 - if [[ "$path" == */"$WIT_LEGACY_BINDING" && "$path" != "${WORK_ITEM_TRACKER_BINDING:-}" ]]; then - printf 'work-item-tracker: notice: read the legacy binding %s; move it under tracker: in /work-items.yaml (CONTRACT.md "Setup (binding file)")\n' "$path" >&2 - fi version="$(jq -r '.schema_version // empty' <<<"$json")" [[ "$version" == 1.* ]] || { wit_invalid "$path" schema_version "$version"; return 1; } provider="$(jq -r '.provider // empty' <<<"$json")" diff --git a/plugins/work-items/tools/work-item-tracker/lib/binding.test.sh b/plugins/work-items/tools/work-item-tracker/lib/binding.test.sh index bb067c1d199..6f2fc3ae821 100755 --- a/plugins/work-items/tools/work-item-tracker/lib/binding.test.sh +++ b/plugins/work-items/tools/work-item-tracker/lib/binding.test.sh @@ -15,33 +15,47 @@ write_binding() { } VALID='{"schema_version":"1.0","provider":"github","config":{"lease_ttl_hours":24}}' +VALID_YAML='tracker: + schema_version: "1.0" + provider: github + config: + lease_ttl_hours: 24' +HOME_YAML="docs/conventions/work-items.yaml" + +# home_binding : a git repository at bound by the default home's YAML. +home_binding() { + mkdir -p "$1/docs/conventions" + git init -q "$1" + write_binding "$1/$HOME_YAML" "$VALID_YAML" +} # --- discovery: anchored at CLAUDE_PROJECT_DIR (never a CWD climb) --- ROOT="$TEST_TMPDIR/repo" mkdir -p "$ROOT/deep/nested" -write_binding "$ROOT/.work-item-tracker.json" "$VALID" +home_binding "$ROOT" OUT="$(cd "$ROOT/deep/nested" && unset WORK_ITEM_TRACKER_BINDING && export CLAUDE_PROJECT_DIR="$ROOT" && wit_find_binding)" -assert_eq "CLAUDE_PROJECT_DIR anchor finds root binding from nested CWD" "$ROOT/.work-item-tracker.json" "$OUT" +assert_eq "CLAUDE_PROJECT_DIR anchor finds root binding from nested CWD" "$ROOT/$HOME_YAML" "$OUT" # --- discovery: git-toplevel fallback when CLAUDE_PROJECT_DIR is unset --- GITROOT="$TEST_TMPDIR/gitrepo" mkdir -p "$GITROOT/deep/nested" -git init -q "$GITROOT" -write_binding "$GITROOT/.work-item-tracker.json" "$VALID" +home_binding "$GITROOT" OUT="$(cd "$GITROOT/deep/nested" && unset WORK_ITEM_TRACKER_BINDING CLAUDE_PROJECT_DIR && wit_find_binding)" -assert_eq "git-toplevel fallback finds root binding from nested CWD" "$GITROOT/.work-item-tracker.json" "$OUT" +assert_eq "git-toplevel fallback finds root binding from nested CWD" "$GITROOT/$HOME_YAML" "$OUT" # --- discovery: a nested per-subdirectory binding is not discovered --- -write_binding "$GITROOT/deep/.work-item-tracker.json" "$VALID" +mkdir -p "$GITROOT/deep/docs/conventions" +write_binding "$GITROOT/deep/$HOME_YAML" "$VALID_YAML" OUT="$(cd "$GITROOT/deep/nested" && unset WORK_ITEM_TRACKER_BINDING CLAUDE_PROJECT_DIR && wit_find_binding)" -assert_eq "nested binding is ignored; root binding wins" "$GITROOT/.work-item-tracker.json" "$OUT" +assert_eq "nested binding is ignored; root binding wins" "$GITROOT/$HOME_YAML" "$OUT" # --- discovery: a stray ancestor binding ABOVE the repo root never captures it --- -write_binding "$TEST_TMPDIR/.work-item-tracker.json" "$VALID" +mkdir -p "$TEST_TMPDIR/docs/conventions" +write_binding "$TEST_TMPDIR/$HOME_YAML" "$VALID_YAML" BARE="$TEST_TMPDIR/bare-gitrepo" mkdir -p "$BARE/sub" git init -q "$BARE" @@ -54,13 +68,15 @@ fi # --- discovery: no anchor at all (no CLAUDE_PROJECT_DIR, not a git repo) fails --- NOANCHOR="$TEST_TMPDIR/no-anchor" -mkdir -p "$NOANCHOR" -write_binding "$NOANCHOR/.work-item-tracker.json" "$VALID" +mkdir -p "$NOANCHOR/docs/conventions" +write_binding "$NOANCHOR/$HOME_YAML" "$VALID_YAML" if (cd "$NOANCHOR" && unset WORK_ITEM_TRACKER_BINDING CLAUDE_PROJECT_DIR && wit_find_binding >/dev/null); then fail "no anchor → no discovery, even with a binding in CWD" "failure" "success" else pass "no anchor → no discovery, even with a binding in CWD" fi +ERR="$(cd "$NOANCHOR" && unset WORK_ITEM_TRACKER_BINDING CLAUDE_PROJECT_DIR && wit_no_binding_reason)" +assert_contains "no anchor → the reason names /work-items:setup apply" "$ERR" "run /work-items:setup apply from the repository root" # --- discovery: env override wins --- @@ -76,6 +92,9 @@ if (WORK_ITEM_TRACKER_BINDING="$TEST_TMPDIR/missing.json" wit_find_binding >/dev else pass "missing env-override binding fails" fi +ERR="$(WORK_ITEM_TRACKER_BINDING="$TEST_TMPDIR/missing.json" wit_no_binding_reason)" +assert_eq "missing env-override binding → the reason names the env path" \ + "no binding file at WORK_ITEM_TRACKER_BINDING=$TEST_TMPDIR/missing.json; unset it to read /work-items.yaml, which /work-items:setup apply writes" "$ERR" # --- validation --- @@ -120,9 +139,9 @@ assert_rejected "provider with slash rejected" '{"schema_version":"1.0","provide STORAGE_ROOT="$TEST_TMPDIR/storage-repo" mkdir -p "$STORAGE_ROOT/deep/nested" -write_binding "$STORAGE_ROOT/.work-item-tracker.json" \ +write_binding "$STORAGE_ROOT/binding.json" \ '{"schema_version":"1.0","provider":"local-markdown","config":{"lease_ttl_hours":24,"storage_dir":".work-items"}}' -OUT="$(cd "$STORAGE_ROOT/deep/nested" && wit_read_binding "$STORAGE_ROOT/.work-item-tracker.json" && printf '%s\n' "$WIT_STORAGE_DIR")" +OUT="$(cd "$STORAGE_ROOT/deep/nested" && wit_read_binding "$STORAGE_ROOT/binding.json" && printf '%s\n' "$WIT_STORAGE_DIR")" assert_eq "relative storage_dir roots against binding dir, not CWD" \ "$STORAGE_ROOT/.work-items" "$OUT" @@ -198,11 +217,12 @@ assert_rejected "hold_states with an empty entry rejected" '{"schema_version":"1 assert_rejected "hold_state_labels with a numeric entry rejected" '{"schema_version":"1.0","provider":"github","config":{"lease_ttl_hours":24,"hold_state_labels":[5]}}' assert_rejected "non-string wayfind_label_prefix rejected" '{"schema_version":"1.0","provider":"github","config":{"lease_ttl_hours":24,"wayfind_label_prefix":["map:"]}}' -# --- personal override: .work-item-tracker.local.json beside the binding, allowlist-only --- +# --- personal override: .work-item-tracker.local.json beside a JSON binding (one the +# WORK_ITEM_TRACKER_BINDING override names), allowlist-only --- OROOT="$TEST_TMPDIR/override-repo" mkdir -p "$OROOT" -OBINDING="$OROOT/.work-item-tracker.json" +OBINDING="$OROOT/binding.json" PERSONAL_OVERRIDE="$OROOT/.work-item-tracker.local.json" # Personal override cases write the personal override and re-read the team binding through it. A @@ -332,9 +352,8 @@ tracker: YREPO="$(yaml_repo yaml-repo)" write_binding "$YREPO/docs/conventions/work-items.yaml" "$YAML_BINDING" -write_binding "$YREPO/.work-item-tracker.json" '{"schema_version":"1.0","provider":"local-markdown","config":{"lease_ttl_hours":1,"storage_dir":"x"}}' OUT="$(cd "$YREPO/deep" && unset WORK_ITEM_TRACKER_BINDING CLAUDE_PROJECT_DIR && wit_find_binding)" -assert_eq "YAML binding in the default home wins over the legacy JSON" "$YREPO/docs/conventions/work-items.yaml" "$OUT" +assert_eq "YAML binding in the default home binds" "$YREPO/docs/conventions/work-items.yaml" "$OUT" if wit_read_binding "$OUT" 2>"$TEST_TMPDIR/yaml.err"; then pass "YAML binding accepted" assert_eq "YAML provider exported" "github" "$WIT_PROVIDER" @@ -343,7 +362,7 @@ if wit_read_binding "$OUT" 2>"$TEST_TMPDIR/yaml.err"; then else fail "YAML binding accepted" "success" "failure" fi -assert_not_contains "YAML binding prints no legacy notice" "$(<"$TEST_TMPDIR/yaml.err")" "notice" +assert_not_contains "YAML binding prints no notice" "$(<"$TEST_TMPDIR/yaml.err")" "notice" # Scalars stay strings and sequences become arrays: a jira resolution id is a # digit string the adapter requires as a string. @@ -357,7 +376,7 @@ write_binding "$TEST_TMPDIR/ids.yaml" 'tracker: assert_eq "YAML scalars stay strings inside arrays" '{"completed":["10000"],"wont_do":["10001"]}' \ "$(wit_yaml_binding_json "$TEST_TMPDIR/ids.yaml" | jq -c '.config.jira.resolutions')" -# An empty flow sequence stays [], as the legacy JSON writes it, where the parser +# An empty flow sequence stays [], as a JSON binding writes it, where the parser # alone prints nothing: jira tells a present empty done_category_keys (a config # error) from an absent one (the default) by presence, and requires both resolutions # keys. A `[]` inside a quoted scalar or a comment is text, and a key with no value @@ -372,7 +391,7 @@ write_binding "$TEST_TMPDIR/empty.yaml" 'tracker: resolutions: completed: ["10000"] wont_do: [ ] # none yet' -assert_eq "an empty YAML flow sequence stays [], as in the legacy JSON" \ +assert_eq "an empty YAML flow sequence stays [], as in a JSON binding" \ '{"site":"a.atlassian.net","blocked_by_link_type":"[]","done_category_keys":[],"resolutions":{"completed":["10000"],"wont_do":[]}}' \ "$(wit_yaml_binding_json "$TEST_TMPDIR/empty.yaml" | jq -c '.config.jira')" printf 'tracker:\r\n config:\r\n jira:\r\n done_category_keys: []\r\n' >"$TEST_TMPDIR/crlf.yaml" @@ -431,18 +450,24 @@ assert_succeeds "YAML personal override with no tracker key is inert" "success" wit_read_binding "$YREPO/docs/conventions/work-items.yaml" rm -f "$YOVERRIDE" -# --- legacy JSON fallback, with a one-line notice, when the YAML has no binding --- +# --- the retired root .work-item-tracker.json never binds; the reason names the fix --- -JREPO="$(yaml_repo json-fallback-repo)" +JREPO="$(yaml_repo json-only-repo)" write_binding "$JREPO/docs/conventions/work-items.yaml" 'ai_disclaimer: false' write_binding "$JREPO/.work-item-tracker.json" "$VALID" -OUT="$(unset WORK_ITEM_TRACKER_BINDING && CLAUDE_PROJECT_DIR="$JREPO" wit_find_binding)" -assert_eq "legacy JSON read when the YAML has no tracker key" "$JREPO/.work-item-tracker.json" "$OUT" -ERR="$(unset WORK_ITEM_TRACKER_BINDING && wit_read_binding "$OUT" 2>&1 >/dev/null)" -assert_contains "legacy JSON read prints a notice" "$ERR" "notice: read the legacy binding $JREPO/.work-item-tracker.json" -assert_eq "the notice is one line" "1" "$(printf '%s\n' "$ERR" | wc -l | tr -d ' ')" +if (unset WORK_ITEM_TRACKER_BINDING && CLAUDE_PROJECT_DIR="$JREPO" wit_find_binding >/dev/null); then + fail "a root .work-item-tracker.json alone → discovery fails" "failure" "success" +else + pass "a root .work-item-tracker.json alone → discovery fails" +fi +ERR="$(unset WORK_ITEM_TRACKER_BINDING && CLAUDE_PROJECT_DIR="$JREPO" wit_no_binding_reason)" +assert_eq "a root .work-item-tracker.json alone → the reason names apply and the target" \ + "no binding: $JREPO/.work-item-tracker.json no longer binds the tracker; run /work-items:setup apply to copy it under tracker: in docs/conventions/work-items.yaml" "$ERR" +ERR="$(unset WORK_ITEM_TRACKER_BINDING && CLAUDE_PROJECT_DIR="$JREPO" bash "$SCRIPT_DIR/../work-item-tracker.sh" capabilities 2>&1 >/dev/null)" +assert_eq "the dispatcher exits 3 on a root .work-item-tracker.json alone" "3" "$?" +assert_contains "the dispatcher's message names /work-items:setup apply" "$ERR" "run /work-items:setup apply to copy it" -# --- neither: no YAML binding and no legacy JSON → no binding (the caller stops) --- +# --- neither: no YAML binding and no root JSON → no binding (the caller stops) --- NREPO="$(yaml_repo neither-repo)" write_binding "$NREPO/docs/conventions/work-items.yaml" 'ai_disclaimer: false' @@ -451,8 +476,11 @@ if (unset WORK_ITEM_TRACKER_BINDING && CLAUDE_PROJECT_DIR="$NREPO" wit_find_bind else pass "neither YAML nor JSON binding → discovery fails" fi +ERR="$(unset WORK_ITEM_TRACKER_BINDING && CLAUDE_PROJECT_DIR="$NREPO" wit_no_binding_reason)" +assert_eq "neither → the reason names the home file and apply" \ + "no binding: $NREPO/docs/conventions/work-items.yaml sets no tracker: mapping; run /work-items:setup apply" "$ERR" -# --- no home applies: a root outside a git work tree reads only the legacy JSON --- +# --- no home applies: a root outside a git work tree binds nothing --- LOOSE="$TEST_TMPDIR/loose-folder" mkdir -p "$LOOSE/docs/conventions" @@ -463,8 +491,14 @@ else pass "a non-git root never reads /work-items.yaml" fi write_binding "$LOOSE/.work-item-tracker.json" "$VALID" -OUT="$(unset WORK_ITEM_TRACKER_BINDING && CLAUDE_PROJECT_DIR="$LOOSE" wit_find_binding)" -assert_eq "a non-git root binds through the legacy JSON only" "$LOOSE/.work-item-tracker.json" "$OUT" +if (unset WORK_ITEM_TRACKER_BINDING && CLAUDE_PROJECT_DIR="$LOOSE" wit_find_binding >/dev/null); then + fail "a non-git root never binds through a root .work-item-tracker.json" "failure" "success" +else + pass "a non-git root never binds through a root .work-item-tracker.json" +fi +ERR="$(unset WORK_ITEM_TRACKER_BINDING && CLAUDE_PROJECT_DIR="$LOOSE" wit_no_binding_reason)" +assert_contains "no home → the reason names the retired file" "$ERR" "$LOOSE/.work-item-tracker.json no longer binds the tracker" +assert_contains "no home → the reason names apply from a git work tree" "$ERR" "run /work-items:setup apply from a git work tree that binds a home" # --- invalid YAML: the error names the file, the key and the value --- diff --git a/plugins/work-items/tools/work-item-tracker/seeds/gitea/common.sh b/plugins/work-items/tools/work-item-tracker/seeds/gitea/common.sh index ff588b4ddb5..a8e4cf3d5a7 100755 --- a/plugins/work-items/tools/work-item-tracker/seeds/gitea/common.sh +++ b/plugins/work-items/tools/work-item-tracker/seeds/gitea/common.sh @@ -199,7 +199,7 @@ wit_need_gitea_config() { name="$(basename "${BASH_SOURCE[1]}")" binding="$(wit_find_binding)" || { - printf '%s: no binding found (tracker: in /work-items.yaml, or the legacy repo-root JSON binding) — see CONTRACT.md Setup\n' "$name" >&2 + printf '%s: %s; see CONTRACT.md Setup\n' "$name" "$(wit_no_binding_reason)" >&2 exit "$EX_CONFIG" } ejson="$(wit_effective_binding_json "$binding")" || diff --git a/plugins/work-items/tools/work-item-tracker/seeds/jira/common.sh b/plugins/work-items/tools/work-item-tracker/seeds/jira/common.sh index d63df41a5b4..1267401a181 100755 --- a/plugins/work-items/tools/work-item-tracker/seeds/jira/common.sh +++ b/plugins/work-items/tools/work-item-tracker/seeds/jira/common.sh @@ -147,7 +147,7 @@ wit_need_jira_config() { name="$(basename "${BASH_SOURCE[1]}")" binding="$(wit_find_binding)" || { - printf '%s: no binding found (tracker: in /work-items.yaml, or the legacy repo-root JSON binding) — see CONTRACT.md Setup\n' "$name" >&2 + printf '%s: %s; see CONTRACT.md Setup\n' "$name" "$(wit_no_binding_reason)" >&2 exit "$EX_CONFIG" } # The effective (team + allowlisted personal override) view: auth_email/auth_env are diff --git a/plugins/work-items/tools/work-item-tracker/seeds/linear/common.sh b/plugins/work-items/tools/work-item-tracker/seeds/linear/common.sh index 6c25c47b3e3..f79ab68a6a8 100755 --- a/plugins/work-items/tools/work-item-tracker/seeds/linear/common.sh +++ b/plugins/work-items/tools/work-item-tracker/seeds/linear/common.sh @@ -212,7 +212,7 @@ wit_need_linear_config() { name="$(basename "${BASH_SOURCE[1]}")" binding="$(wit_find_binding)" || { - printf '%s: no binding found (tracker: in /work-items.yaml, or the legacy repo-root JSON binding) — see CONTRACT.md Setup\n' "$name" >&2 + printf '%s: %s; see CONTRACT.md Setup\n' "$name" "$(wit_no_binding_reason)" >&2 exit "$EX_CONFIG" } ejson="$(wit_effective_binding_json "$binding")" || diff --git a/plugins/work-items/tools/work-item-tracker/work-item-tracker.sh b/plugins/work-items/tools/work-item-tracker/work-item-tracker.sh index 6d09756bf6d..faba8b6bfbc 100755 --- a/plugins/work-items/tools/work-item-tracker/work-item-tracker.sh +++ b/plugins/work-items/tools/work-item-tracker/work-item-tracker.sh @@ -212,7 +212,7 @@ main() { local binding_path binding_path="$(wit_find_binding)" || - fail_config "no binding found (tracker: in /work-items.yaml, or the legacy repo-root JSON binding) — run /work-items:setup; see CONTRACT.md Setup" + fail_config "$(wit_no_binding_reason); see CONTRACT.md Setup" wit_read_binding "$binding_path" || fail_config "invalid binding at $binding_path — run /work-items:setup check for the itemized breakdown; see CONTRACT.md Setup" diff --git a/prompts/loops/loop-lane-prompts.md b/prompts/loops/loop-lane-prompts.md index 22add8c0888..296898690c2 100644 --- a/prompts/loops/loop-lane-prompts.md +++ b/prompts/loops/loop-lane-prompts.md @@ -100,9 +100,9 @@ into one profile. loop-lane keys at all means **every merge is human**, whatever tier you pass. - **Tracker binding present?** The `tracker` mapping of - `docs/conventions/work-items.yaml` (or, in a repository not yet migrated, the - legacy `.work-item-tracker.json`) must resolve from the worker lane's working - directory or its preflight stops the lane. + `docs/conventions/work-items.yaml` must resolve from the worker lane's working + directory or its preflight stops the lane. A root `.work-item-tracker.json` + no longer binds; `/work-items:setup apply` copies it into that mapping. - **Role labels.** The human-gated and autonomous-eligible names come from that file's `config.role_labels`, not from a literal. Resolve the `autonomous-eligible` role **before** any query below; a repo that remapped @@ -120,19 +120,16 @@ into one profile. string, since a label's real value may legitimately carry spaces. ```bash - # The `tracker` mapping of docs/conventions/work-items.yaml, as JSON, else - # the legacy .work-item-tracker.json. + # The `tracker` mapping of docs/conventions/work-items.yaml, as JSON. BINDING= if [ -f docs/conventions/work-items.yaml ] && yq -e 'has("tracker")' docs/conventions/work-items.yaml >/dev/null; then BINDING=$(mktemp) yq -o=json '.tracker' docs/conventions/work-items.yaml >"$BINDING" - elif [ -f .work-item-tracker.json ]; then - BINDING=.work-item-tracker.json fi if [ -z "$BINDING" ]; then # No binding is the documented warn-and-default case, NOT a malformed # one. jq cannot express it, since it fails before the program runs. - echo "WARNING: no tracker binding; defaulting role to agent-ready" >&2 + echo "WARNING: no tracker binding (run /work-items:setup apply); defaulting role to agent-ready" >&2 ROLE=agent-ready else ROLE=$(jq -er ' From c5ed3c922c286338a940553c0baa632590999fce Mon Sep 17 00:00:00 2001 From: "melodic-automation-lanes[bot]" <337595936+melodic-automation-lanes[bot]@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:23:05 +0000 Subject: [PATCH 2/3] Apply fix-docs on refactor/work-items-drop-json-fallback Lane-Activity: pr-refine/fix-docs --- .../autonomy/reference/routines/backlog-readiness-check.md | 3 ++- plugins/autonomy/reference/routines/pr-queue-tending.md | 3 ++- plugins/work-items/README.md | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/autonomy/reference/routines/backlog-readiness-check.md b/plugins/autonomy/reference/routines/backlog-readiness-check.md index a8dc18772fa..83a2457e59a 100644 --- a/plugins/autonomy/reference/routines/backlog-readiness-check.md +++ b/plugins/autonomy/reference/routines/backlog-readiness-check.md @@ -9,7 +9,8 @@ need before the meeting discovers it. Refinement-time discovery is the toil addressed: backlog sessions burn shared meeting time finding out an item is not ready: acceptance criteria missing, fields absent, scope unclear. The check does the recurring pre-refinement pass and annotates each item with what -is missing, so refinement starts from ready-or-annotated items instead of live archaeology. +is missing, so refinement starts from ready-or-annotated items and nobody re-investigates +missing details during the meeting. ## Trigger and cadence diff --git a/plugins/autonomy/reference/routines/pr-queue-tending.md b/plugins/autonomy/reference/routines/pr-queue-tending.md index d89ba0e47c0..bcdd40f4a24 100644 --- a/plugins/autonomy/reference/routines/pr-queue-tending.md +++ b/plugins/autonomy/reference/routines/pr-queue-tending.md @@ -9,7 +9,8 @@ humans each stalled PR is waiting on. Silently stalling PRs are the toil addressed: open PRs go stale waiting on a forgotten review, red CI nobody noticed, or an unanswered review thread, and each one is rediscovered only when someone trips over it. The sweep does the recurring pass over the whole queue and -turns every stall into a named nudge, so PRs wait on decisions, not on attention. +turns every stall into a named nudge, so a PR waits only on a decision, never because +nobody noticed it. ## Trigger and cadence diff --git a/plugins/work-items/README.md b/plugins/work-items/README.md index 51268a7be97..80ba2b76c8d 100644 --- a/plugins/work-items/README.md +++ b/plugins/work-items/README.md @@ -54,8 +54,8 @@ plugin: `track`, `work`, `work-loop`, `triage`, `decompose`, `attend-queue` and infrastructure skills (`setup`, `onboard-adapter`) speak in provider and tracker terms, and `ship` speaks in container and journey terms. Nobody says "add a ticket" to an adapter generator. Stuffing the tokens into their triggers to -satisfy a fleet-wide claim would buy a tidier sentence at the cost of worse -routing, so the claim is scoped instead. +satisfy a fleet-wide claim would make the claim simpler to state but would +worsen routing, so the claim is scoped instead. ## `/work-items:track` actions From 78d775d10085c1b0d3115c18e494a1d8db9555a0 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 11 Nov 2026 20:48:53 -0500 Subject: [PATCH 3/3] fix(work-items): name each no-home cause and its fix in the no-binding reason wit_convention_home now fails 2 when the shared resolver copies are missing and 3 when the convention-home pointer is broken, keeping 1 for a declined home or a root where none applies. wit_no_binding_reason reports each: a broken pointer names /work-items:setup check (apply refuses that state), missing copies name a plugin reinstall, and the declined or no-home case keeps naming apply. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7 --- ...-work-items-drop-json-fallback-59140d30.md | 2 +- .../tools/work-item-tracker/CONTRACT.md | 3 +- .../tools/work-item-tracker/lib/binding.sh | 36 +++++++++++++------ .../work-item-tracker/lib/binding.test.sh | 30 ++++++++++++++++ 4 files changed, 59 insertions(+), 12 deletions(-) diff --git a/.changes/work-items/refactor-work-items-drop-json-fallback-59140d30.md b/.changes/work-items/refactor-work-items-drop-json-fallback-59140d30.md index a4d511b1270..9d4306aae7c 100644 --- a/.changes/work-items/refactor-work-items-drop-json-fallback-59140d30.md +++ b/.changes/work-items/refactor-work-items-drop-json-fallback-59140d30.md @@ -9,6 +9,6 @@ bump: major ### Changed -- With no binding, the seam's exit `3` message (`wit_no_binding_reason` in `lib/binding.sh`) says why and names `/work-items:setup apply`: a repo-root `.work-item-tracker.json` that no longer binds, a `work-items.yaml` with no `tracker` mapping, a root where no convention home applies, or a missing `WORK_ITEM_TRACKER_BINDING` file. The jira, gitea and linear seeds and the onboard-adapter template print the same message. +- With no binding, the seam's exit `3` message (`wit_no_binding_reason` in `lib/binding.sh`) says why and names `/work-items:setup apply`: a repo-root `.work-item-tracker.json` that no longer binds, a `work-items.yaml` with no `tracker` mapping, a root where no convention home applies, or a missing `WORK_ITEM_TRACKER_BINDING` file. A broken convention-home pointer names `/work-items:setup check` instead, and missing shared resolver copies name a plugin reinstall. The jira, gitea and linear seeds and the onboard-adapter template print the same message. - `/work-items:setup check` reports a binding that exists only in `.work-item-tracker.json` as FAIL, remediation `/work-items:setup apply`, instead of INFO. - The seam contract, setup skill, README, schema and the skill bodies that resolve role labels no longer describe a JSON fallback. diff --git a/plugins/work-items/tools/work-item-tracker/CONTRACT.md b/plugins/work-items/tools/work-item-tracker/CONTRACT.md index 6c68fa86884..b025e01ed50 100644 --- a/plugins/work-items/tools/work-item-tracker/CONTRACT.md +++ b/plugins/work-items/tools/work-item-tracker/CONTRACT.md @@ -148,7 +148,8 @@ value (named by file, key and value; the personal override when it set the value A repo-root `.work-item-tracker.json`, the retired binding file, no longer binds; when one is present the message names it, and `apply` copies it under `tracker:`. A `work-items.yaml` that does not parse is still read as the binding and fails. A broken - pointer region is reported on stderr, and the seam then finds no binding. + pointer region is reported on stderr, the seam then finds no binding, and the message names + `/work-items:setup check` instead; missing shared resolver copies name a plugin reinstall. - Discovery: the binding resolves from the **repo root**, meaning `${CLAUDE_PROJECT_DIR}` when set, else `git rev-parse --show-toplevel`. Whichever of the two resolves, that one value governs ALL of the seam's repo-relative resolution (binding read, consumer-local adapter diff --git a/plugins/work-items/tools/work-item-tracker/lib/binding.sh b/plugins/work-items/tools/work-item-tracker/lib/binding.sh index 987f4648f6e..8f0dd9913a8 100755 --- a/plugins/work-items/tools/work-item-tracker/lib/binding.sh +++ b/plugins/work-items/tools/work-item-tracker/lib/binding.sh @@ -34,13 +34,13 @@ wit_project_root() { } # wit_convention_home : the repo-relative convention home the pointer region -# binds, or the default home when the repository binds none. Fails when it declined -# a home, when no home applies (exit 5: the root is $HOME, an ancestor of it, or not -# in a git work tree, so nothing can bind it), or when the pointer is broken (that -# message passes through to stderr). +# binds, or the default home when the repository binds none. Fails 1 when it declined +# a home or no home applies (the root is $HOME, an ancestor of it, or not in a git +# work tree, so nothing can bind it), 2 when the shared copies are missing, and 3 when +# the pointer is broken (that message passes through to stderr). wit_convention_home() { local home rc - [[ -f "$WIT_HOME_RESOLVER" && -f "$WIT_YAML_PARSER" ]] || return 1 + [[ -f "$WIT_HOME_RESOLVER" && -f "$WIT_YAML_PARSER" ]] || return 2 home="$(bash "$WIT_HOME_RESOLVER" --root "$1" 2>/dev/null)" rc=$? case "$rc" in @@ -48,7 +48,7 @@ wit_convention_home() { 1) printf '%s\n' "$WIT_DEFAULT_CONVENTION_HOME" ;; 3) bash "$WIT_HOME_RESOLVER" --root "$1" >/dev/null - return 1 + return 3 ;; *) return 1 ;; esac @@ -149,8 +149,9 @@ wit_find_binding() { return 1 } -# wit_no_binding_reason: one line saying why wit_find_binding found no binding, naming -# /work-items:setup apply as the fix. A retired repo-root .work-item-tracker.json gets +# wit_no_binding_reason: one line saying why wit_find_binding found no binding and the +# fix: /work-items:setup check for a broken pointer, a reinstall for missing shared +# copies, else /work-items:setup apply. A retired repo-root .work-item-tracker.json gets # its own line: apply copies it under tracker: in /work-items.yaml. wit_no_binding_reason() { local root home legacy="" @@ -164,9 +165,24 @@ wit_no_binding_reason() { return 0 fi [[ -f "$root/$WIT_LEGACY_BINDING" ]] && legacy="$root/$WIT_LEGACY_BINDING no longer binds the tracker, and " - if ! home="$(wit_convention_home "$root" 2>/dev/null)"; then + home="$(wit_convention_home "$root" 2>/dev/null)" + case $? in + 0) ;; + 2) + printf 'no binding: %sthe plugin is missing its shared copies %s or %s; reinstall the work-items plugin\n' "$legacy" \ + "${WIT_HOME_RESOLVER#"$_WIT_BINDING_LIB_DIR/../../../"}" "${WIT_YAML_PARSER#"$_WIT_BINDING_LIB_DIR/../../../"}" + return 0 + ;; + 3) + printf 'no binding: %sthe convention-home pointer under %s is broken; run /work-items:setup check to see why\n' "$legacy" "$root" + return 0 + ;; + *) printf 'no binding: %sno convention home applies under %s (it declines one, is the home directory or an ancestor of it, or is not a git work tree); run /work-items:setup apply from a git work tree that binds a home\n' "$legacy" "$root" - elif [[ -n "$legacy" ]]; then + return 0 + ;; + esac + if [[ -n "$legacy" ]]; then printf 'no binding: %s no longer binds the tracker; run /work-items:setup apply to copy it under tracker: in %s/work-items.yaml\n' "$root/$WIT_LEGACY_BINDING" "$home" else printf 'no binding: %s/%s/work-items.yaml sets no tracker: mapping; run /work-items:setup apply\n' "$root" "$home" diff --git a/plugins/work-items/tools/work-item-tracker/lib/binding.test.sh b/plugins/work-items/tools/work-item-tracker/lib/binding.test.sh index 6f2fc3ae821..6f39057c37a 100755 --- a/plugins/work-items/tools/work-item-tracker/lib/binding.test.sh +++ b/plugins/work-items/tools/work-item-tracker/lib/binding.test.sh @@ -500,6 +500,36 @@ ERR="$(unset WORK_ITEM_TRACKER_BINDING && CLAUDE_PROJECT_DIR="$LOOSE" wit_no_bin assert_contains "no home → the reason names the retired file" "$ERR" "$LOOSE/.work-item-tracker.json no longer binds the tracker" assert_contains "no home → the reason names apply from a git work tree" "$ERR" "run /work-items:setup apply from a git work tree that binds a home" +# --- a declined home: no home applies --- + +DREPO="$(yaml_repo declined-repo)" +write_binding "$DREPO/AGENTS.md" ' +declined +' +ERR="$(unset WORK_ITEM_TRACKER_BINDING && CLAUDE_PROJECT_DIR="$DREPO" wit_no_binding_reason)" +assert_contains "declined home → the reason says no home applies" "$ERR" "no convention home applies under $DREPO" + +# --- a broken pointer: the reason names setup check, not apply --- + +PREPO="$(yaml_repo broken-pointer-repo)" +# shellcheck disable=SC2016 # the backticks are markdown, not an expansion +write_binding "$PREPO/AGENTS.md" ' +Team conventions live in `docs/missing`. +' +ERR="$(unset WORK_ITEM_TRACKER_BINDING && CLAUDE_PROJECT_DIR="$PREPO" wit_find_binding 2>&1 >/dev/null)" +assert_eq "broken pointer → discovery fails" "1" "$?" +assert_contains "broken pointer → discovery passes the resolver's message through" "$ERR" "docs/missing" +ERR="$(unset WORK_ITEM_TRACKER_BINDING && CLAUDE_PROJECT_DIR="$PREPO" wit_no_binding_reason 2>/dev/null)" +assert_eq "broken pointer → the reason names setup check" \ + "no binding: the convention-home pointer under $PREPO is broken; run /work-items:setup check to see why" "$ERR" + +# --- missing shared copies: the reason names a reinstall --- + +ERR="$(unset WORK_ITEM_TRACKER_BINDING && WIT_HOME_RESOLVER="$TEST_TMPDIR/absent.sh" && CLAUDE_PROJECT_DIR="$NREPO" wit_no_binding_reason)" +assert_contains "missing shared copies → the reason names the missing copy" "$ERR" "$TEST_TMPDIR/absent.sh" +assert_contains "missing shared copies → the reason names a reinstall" "$ERR" "reinstall the work-items plugin" +assert_contains "missing shared copies → the parser copy is plugin-relative" "$ERR" " or skills/triage/scripts/yaml-subset.awk;" + # --- invalid YAML: the error names the file, the key and the value --- IREPO="$(yaml_repo invalid-repo)"