Skip to content

refactor(work-items)!: drop the .work-item-tracker.json discovery fallback - #15459

Open
kyle-sexton wants to merge 5 commits into
mainfrom
refactor/work-items-drop-json-fallback
Open

kyle-sexton wants to merge 5 commits into
mainfrom
refactor/work-items-drop-json-fallback

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

No related issue: owner decision Q316 (cohort hub)

Summary

Step 4 of the Q316 plan ("migrate, then drop"): every reader stops falling back to a repo-root .work-item-tracker.json. The tracker binding is now only the tracker mapping of <home>/work-items.yaml. All 8 melodic-software repositories already carry docs/conventions/work-items.yaml and no JSON (the migration PRs under Related). kyle-sexton/autonomy-demo-scratch keeps both files on purpose: its vendored seam reads only the JSON, and this PR does not touch that copy.

A repository still bound only by the JSON file gets exit 3 from every seam verb. The message names /work-items:setup apply as the one-step fix, which copies the JSON binding and its personal override under tracker:.

Fix

  • work-items seam (tools/work-item-tracker/lib/binding.sh): wit_find_binding fails after the YAML check, and the legacy notice: is gone. New wit_no_binding_reason gives one line per cause: a root .work-item-tracker.json that no longer binds, a work-items.yaml with no tracker mapping, a declined or inapplicable convention home (run apply from a git work tree), a broken convention-home pointer (run /work-items:setup check, since apply refuses that state), missing shared resolver copies (reinstall the plugin), or a missing WORK_ITEM_TRACKER_BINDING file. wit_convention_home fails 2 and 3 for the last two causes so the reason can tell them apart. 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. LEGACY_PATH is gone and PATHS holds one file, so the merge-skip cache key changes once. Babysit never exits on a missing binding: a JSON-only repository silently gets the default role labels, as a repository with no binding does today.
  • autonomy resolver (resolve-prerequisites.mjs): with no YAML tracker mapping the tracker need is absent, and its detail names /work-items:setup apply (and the JSON file when present). The four fixture JSONs became docs/conventions/work-items.yaml; the now-duplicate positive-verdict-yaml-binding fixture became retired-json-binding, which asserts the absent verdict and the fix text.
  • Loop-lane shell (prompts/loops/loop-lane-prompts.md, docs/contracts/loop-lane/README.md): YAML only; the no-binding warning names apply.
  • Docs: CONTRACT.md's "Legacy fallback" section became "No binding"; setup SKILL.md and check.md (a legacy-only binding is now FAIL with apply as the remediation), README, schema description, label taxonomy, tracker-seam, and the role-resolution lines in attend-queue, track, due, recheck, triage, work, babysit-loop, safety, create, session-mode, the autonomy routines and the config-cascade table.
  • Kept, per the hub decisions: migrate-binding.sh and setup's copy step (Q2); the WORK_ITEM_TRACKER_BINDING env override's JSON acceptance, with its .work-item-tracker.local.json override beside it (Q3); merge-holds.mjs's non-yaml branch and the workflows' YAML-derived temp file, unrenamed, so .github/ is untouched. The legacy personal-override ignore guidance stays because the migrator still reads that file.
  • Bumps (changelog fragments): work-items major, stating the fix. source-control and autonomy major, because each stops reading a config file consumers could rely on (Basis: semver 2.0.0 item 8, a backward-incompatible change). plugin-quality patch (docs only).
  • History stays: changelogs, ADRs 0015/0040/0042/0075, frozen eval fixtures.

Verification

  • lib/binding.test.sh: 129 pass. Discovery cases now bind through YAML in a git work tree. The legacy-only, neither, non-git, declined-home, broken-pointer and missing-shared-copies cases assert the reason text, and the dispatcher exits 3 naming apply.
  • migrate-binding.test.sh: all pass.
  • babysit unittest: tracker_binding 11, merge_policy 579, postmerge 361, risk 143 (11 skipped), merge_skip 22, all OK.
  • node --test --test-concurrency=4 on resolve-prerequisites.fixtures.test.mjs (25 checks) and unattended-callers.test.mjs: 5 of 5 pass.
  • generate-identity-prerequisites.test.sh 6 pass; generate-identity-prerequisites.mjs --check in sync.
  • Every affected *.test.sh in work-items, autonomy and source-control plus the loop-lane-floor, config-cascade-semantics and plugin-contract validators (80 suites, scripts/affected-tests.sh selection, 4 at a time): 80 pass. The dispatcher suite first failed one case because the missing-env-binding message did not name /work-items:setup; the message now does, and the suite passes.
  • shellcheck on the changed shell files: clean. python3 -I scripts/check-vocabulary.py on the changed files: exit 0. scripts/check-changelog-fragments.sh --check: valid. Ruff check: clean. gitleaks on the commit: no leaks.
  • scripts/check-affected.sh --no-fetch: 16 passed, 0 skipped over 58 changed files. The full .github suite was not run (unchanged).

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_01VQb9Pz6CBRwjqpnFDbP9Rr

…lback

The tracker binding is now only the tracker mapping of
<home>/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 <home>/work-items.yaml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Pz6CBRwjqpnFDbP9Rr
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-11T23:16:36.379060Z 7369728 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

melodic-automation-lanes Bot and others added 2 commits November 11, 2026 23:23
…rop-json-fallback

# Conflicts:
#	plugins/work-items/README.md
@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed b34a5c9bd00b89dc1c03c96a43a922aaeea2c8db: read 58 of 58 changed file(s); 0 finding(s), 0 as inline comments.

Chunk 1 of 2 (37 files)

Security review: chunk 1 of 2 (37 files)

No security issues found in this chunk. This lane already reviewed an earlier head (blocking only: yes), so only CRITICAL or IMPORTANT findings would be reported; there are none.

critical-findings: 0

What was checked

Trusted path plugins/source-control/skills/babysit-prs/scripts/babysit_tracker_binding.py. I read the whole change.

  • role_label now reads only docs/conventions/work-items.yaml. If the file is missing, the caller's default is used.
  • A scalar tracker still raises BindingError. A YAML file that does not parse still raises through records, so the caller refuses it as unreadable:. The test that a broken file is refused now uses an unparseable YAML file, test_babysit_merge_policy.py.
  • Callers still read the binding at the base or default branch, so a pull request head cannot pick its own admission label.
  • A repository bound only by the JSON file now gets the default role labels. That changes which label name is matched. It does not change who may apply the label: the issue-vouch check still requires a human with write access. Nothing new becomes trusted.
  • PATHS shrinks to the YAML file, so the merge-skip cache key no longer covers the JSON file. That is consistent, because the JSON file no longer affects any decision.

test_babysit_risk.py. The edit only swaps LEGACY_PATH for YAML_PATH in a test's list of paths expected to score LOW. docs/conventions/x.md was already in that list, and babysit_risk.py is unchanged, so the risk floor is not weakened.

plugins/autonomy/skills/setup/scripts/resolve-prerequisites.mjs. With no YAML tracker mapping, the tracker need is now absent, so routines that need it resolve unsupported (it fails closed). The detail text is built only from constants and a pathExists check (pathExists is defined at L126). Nothing new is read from the repository or run.

Loop-lane operator snippet (docs/contracts/loop-lane/README.md). The cat .work-item-tracker.json fallback is removed. The label now comes from yq | jq ... // empty, defaults to ${L:-needs-human}, and is passed quoted to gh issue edit --add-label "$L". There is no injection surface.

Instruction-surface lens. The edits to docs and skill bodies only remove mentions of the retired JSON fallback: session-mode.md, babysit-loop/SKILL.md, safety.md, create.md, the autonomy routines and prerequisite reference, label-taxonomy.md, the README, the config-cascade row and the eval prompt. No rule that prevents an action is deleted or softened. The standards-sync no-update rule in safety.md keeps its wording; only the file it reads the label from changed. I did not read the protected-class list in the instruction exception register, because no removed rule needed it.

Fixtures, identical copies, changelog fragments. These are test data and release notes. The three identical docs/conventions/work-items.yaml fixtures sit under fixtures/prerequisite-resolution/*/repo/, and being there does not change what their content does. The renamed fixtures are pure renames.

Plugin manifests. This chunk changes no .claude-plugin/plugin.json or marketplace.json.

Settled thread and owner threat model. The settled thread on binding.sh (the broken-pointer reason) belongs to chunk 2. No named friction hold is weakened in this chunk. The PR context contains no embedded instructions aimed at the review lane.

Spend. None: this change starts no paid resources and adds no lane runs.

Chunk 2 of 2 (19 files)

Security review: chunk 2 of 2 (19 files)

No security issues found in this chunk. I read every hunk of all 19 files. This lane already reviewed an earlier head, so this review reported only blocking findings. None came up.

critical-findings: 0

What this chunk changes

The work-items tracker seam no longer falls back to a repo-root .work-item-tracker.json. The binding now comes only from the tracker mapping of <home>/work-items.yaml, or from a file named by the WORK_ITEM_TRACKER_BINDING env override. A new helper, wit_no_binding_reason, prints why no binding was found. The dispatcher, the jira, gitea and linear seeds, and the onboard-adapter template print that line and exit 3. The rest of the chunk is docs and skill text that remove mentions of the fallback.

What was checked

  • The binding code (lib/binding.sh) only narrows. wit_find_binding now ends with return 1 where it used to return the root JSON path. Nothing new is read from the repository. wit_convention_home now returns 2 when the shared copies are missing and 3 when the pointer is broken; before, both returned 1. Its only callers treat any non-zero result as "no home", so no caller now passes when it used to fail.
  • No injection in the new messages. wit_no_binding_reason passes every variable value (the env path, the repo root, the home) to printf as %s arguments, never in the format string. Its output goes to stderr. Nothing evaluates it or runs it as a command. In work-item-tracker.sh, fail_config "$(wit_no_binding_reason); …" is quoted. The seeds and the template use printf '%s: %s; …' with quoted arguments.
  • case $? after the command substitution: home is declared local on an earlier line, so the assignment keeps the resolver's exit status. Each branch matches the status it is meant for.
  • Personal override: the allowlist of keys a personal override may change (WIT_OVERRIDE_ALLOWED_PATHS) is unchanged. A JSON override is still read only beside a JSON binding that the env override names. Removing the legacy notice: changes only a stderr message.
  • Loop-lane snippet (prompts/loops/loop-lane-prompts.md): the .work-item-tracker.json branch is gone. A repository with no YAML binding gets the warning that was already documented, and the role defaults to agent-ready. In a repository bound only by JSON that had renamed its autonomous-eligible label, the lane would now look for agent-ready. That changes which label name is matched, not who can apply the label. The owner's threat model has no outside actors, and the PR says every owner repository has already moved to YAML. So it widens no trust and is not a finding.
  • Schema (work-items.schema.json): only the description string changed. No type, required or additionalProperties change.
  • Instruction-surface lens: the edits to the setup SKILL.md and check.md, CONTRACT.md, tracker-seam.md, and the attend-queue, track, due, recheck, triage and work skills remove only the fallback wording. check.md makes a JSON-only binding stricter (INFO becomes FAIL). No rule that prevents an action is deleted or softened. I did not consult the instruction exception register, because no removed rule needed it.
  • Tests (binding.test.sh): test data only. The new cases cover the no-anchor, env-override, JSON-only, neither, non-git, declined-home, broken-pointer and missing-copies reasons.

Settled and prior threads

  • Settled thread: the earlier finding on binding.sh was that a broken pointer named the wrong fix. It is settled: the resolution head is this head, its lines have not changed, and the owner's reply records the fix. The code now does what the reply says (the 2) and 3) branches of wit_no_binding_reason), so I did not raise it again.
  • Prior findings: this lane's earlier summary on this PR reported 0 findings, so nothing carries over.

Other checks

  • Plugin manifests: this chunk changes no .claude-plugin/plugin.json or marketplace.json.
  • Owner threat model: no named friction hold is weakened in this chunk.
  • Embedded text: the PR context and the settled thread contain no instructions aimed at the review lane.
  • Spend: none. No paid resources, billing settings or extra lane runs.
  • Hold: none.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude-review: 1 inline finding(s) on c39918419c6bf404c249e2b6f011fcc0edf10544; the summary is in this lane's pull request comment.

claude-lane: claude-review

@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed b34a5c9bd00b89dc1c03c96a43a922aaeea2c8db: read 58 of 58 changed file(s); 0 finding(s), 0 as inline comments.

Chunk 1 of 2 (37 files)

Code review: chunk 1 of 2 (PR #15459, head b34a5c9)

No findings in this chunk. This lane already reviewed an earlier head (c3991841), so this pass reported blocking findings only. I read every hunk of all 37 files in the chunk. That includes the renamed and deleted fixtures and the positive-verdict work-items.yaml, which is the first path in the identical-files list. Its two copies, under posture-divergence and probe-could-not-run, sit at ordinary fixture paths, so their location doesn't change what they do.

What I checked

  • babysit_tracker_binding.py (trusted path)
    • role_label now reads only docs/conventions/work-items.yaml, and PATHS is (YAML_PATH,).
    • A missing file returns the default, a scalar tracker still raises BindingError, and a document the parser rejects still raises through records.
    • A YAML file with no tracker key now returns the default without a fallback read. The rewritten tests match this, including reads == [YAML] when only a root JSON file exists.
    • The json / json_object imports it dropped have no other use in the file.
  • resolve-prerequisites.mjs
    • readTrackerBinding returns absent with a fix detail. The detail names .work-item-tracker.json when that file exists and run /work-items:setup apply in both cases.
    • probeTracker passes that detail through and keeps the bare reason for unreadable YAML.
    • readJson and pathExists are still used elsewhere in the file, at lines 172 and 401.
    • The renamed manifest entry and the ASSERTIONS map agree. assertRetiredJsonBinding matches the fixture's contents: the JSON file is present and there is no docs/conventions/work-items.yaml.
  • Fixtures. declared-absent-narrows now binds through the YAML file renamed into it. positive-verdict, posture-divergence and probe-could-not-run swap their JSON files for the identical YAML binding, which the provenance check folded into assertPositiveVerdict now requires.
  • Tests.
    • test_babysit_merge_policy.py: the case "a tracker config that does not parse" uses the flow mapping tracker: {provider: github}. That still reaches the unreadable: path through admission_label.
    • The test_babysit_postmerge.py and test_babysit_risk.py swaps from the legacy path to YAML_PATH line up with the reader change.
  • Loop-lane README §7 snippet. The new yq … | jq '… // empty' followed by L=${L:-needs-human} falls back to needs-human when the file or the tracker mapping is missing.
  • Docs, routines and eval prompt. All of them now name only the YAML binding, which matches the code changes.

Manifests and versioning

This chunk changes no plugin.json or marketplace.json. The changelog fragments set work-items, source-control and autonomy to bump: major, because each stops reading a config file consumers could rely on. plugin-quality gets bump: patch (docs only). Those levels fit semver.

Other notes

  • Closing issue: none. The PR cites owner decision Q316 instead of an issue, so there are no acceptance criteria to check.
  • Settled threads: the one settled thread is about binding.sh, which is in chunk 2. It was not re-raised here.
  • Embedded instructions: none found in the PR context, settled threads or prior findings.
  • Verification: the test, lint and validator results in the PR description are the author's claims. This lane has no shell and ran none of them.

Chunk 2 of 2 (19 files)

Code review: chunk 2 of 2 (PR #15459, head b34a5c9)

No blocking findings in this chunk. I read every hunk of all 19 files in the chunk. This lane already reviewed an earlier head (c3991841), so this pass reports blocking findings only, and advisory findings are dropped.

Settled and prior points

  • Broken-pointer remediation in binding.sh: this was the lane's earlier inline finding, and the owner resolved it by fixing it in 78d775d. At this head the fix is in place, so I am not raising it again:
    • wit_convention_home now returns 2 for missing shared copies and 3 for a broken pointer (lib/binding.sh L41-55).
    • wit_no_binding_reason gives each cause its own fix: plugin reinstall, /work-items:setup check, or /work-items:setup apply (L168-184).
    • binding.test.sh has a case for each: declined home, broken pointer, missing shared copies.

What I checked

  • wit_find_binding / wit_no_binding_reason:
    • Discovery now ends at the YAML check, so the JSON file never binds.
    • The $? read after home="$(wit_convention_home …)" is the substitution's status, which is the intended value.
    • The [[ -f … ]] && legacy=… line is safe. None of the dispatcher, the seeds, the template or the test enables set -e (only comments mention it), so a missing legacy file cannot abort the function.
    • A YAML file that does not parse still counts as the binding (wit_yaml_has_binding returns 0), so it fails loudly instead of reporting "no binding".
  • Dispatcher and seed callers (work-item-tracker.sh, jira/gitea/linear common.sh, the onboard-adapter template): each now prints $(wit_no_binding_reason) with exit 3.
    • The seeds source binding.sh from WIT_SEAM_LIB_DIR, else ../../lib, so a seed run through the dispatcher picks up the new function.
    • Seed copies a consumer already vendored keep their old message, which still works (no new function is called).
  • The legacy-notice removal in wit_read_binding: nothing else depends on it. The JSON is still accepted through the WORK_ITEM_TRACKER_BINDING env override, and wit_override_path and wit_binding_layer_json still pair a JSON binding with .work-item-tracker.local.json, which matches CONTRACT.md's revised "Personal override" bullet.
  • binding.test.sh: the discovery cases moved to YAML in git work trees. The storage-dir and override cases moved to a neutral binding.json read directly or through the env override, so they keep testing what they tested before. The "neither", legacy-only, non-git, declined, broken-pointer and missing-copies cases match the strings the code prints, judged by reading the code.
  • Docs (tracker-seam, schema description, attend-queue, setup SKILL.md and check.md, track/due/recheck, triage, work, CONTRACT.md, loop-lane prompts): each now names only the YAML binding, which matches the code.
    • The new check.md rule (a legacy-only binding is FAIL, fixed by apply) agrees with the seam's exit 3.
    • The loop-lane snippet still warns and falls back to agent-ready when there is no binding.

Manifests and versioning

This chunk changes no plugin.json or marketplace.json. The changelog fragments are in chunk 1 (work-items major), and that bump level fits dropping a config file the plugin used to read.

Other notes

  • Closing issue: none. The PR cites owner decision Q316 instead of an issue, so there are no acceptance criteria to check.
  • Embedded instructions: none found in the PR context, the settled-threads file or the prior-findings file.
  • Verification: the test, lint and shellcheck results in the PR description are the author's claims. This lane has no shell and ran nothing.

kyle-sexton and others added 2 commits November 11, 2026 20:48
…g 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant