Skip to content

fix(work-items): resolve consumer-local gh-bot.sh wrapper independent of adapter location - #826

Merged
kyle-sexton merged 1 commit into
mainfrom
fix/365-work-items-gh-bot-wrapper-resolution
Jul 21, 2026
Merged

fix(work-items): resolve consumer-local gh-bot.sh wrapper independent of adapter location#826
kyle-sexton merged 1 commit into
mainfrom
fix/365-work-items-gh-bot-wrapper-resolution

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Summary

  • The GitHub adapter's WIT_GH_BOT resolved the bot wrapper relative to the adapter's own directory (${CLAUDE_PLUGIN_ROOT}/tools/github-auth/gh-bot.sh in the normal bundled path), never checking the consumer-local override path CONTRACT.md already documented (${CLAUDE_PROJECT_DIR}/tools/github-auth/gh-bot.sh).
  • As a result, a consuming repo's own bot wrapper was never found, and all tracker writes (item create, lease/reclaim comments) silently fell back to the ambient gh (session-user) identity instead of bot attribution.

Fix

  • common.sh: extracted wit_gh_resolve_bot_wrapper, which now resolves the bot wrapper consumer-local-first, plugin-bundled fallback — checking ${CLAUDE_PROJECT_DIR}/tools/github-auth/gh-bot.sh first (independent of where the adapter itself resolved from, so a shadowed consumer-local adapter still finds the consumer's wrapper), then falling back to the bundled path beside the seam tree. This mirrors the existing two-rule adapter resolution documented in CONTRACT.md's "Adapter resolution" section.
  • CONTRACT.md: updated the "Identity routing (GitHub adapter)" section to document the corrected consumer-local-first/plugin-bundled-fallback resolution order (previously it documented the buggy adapter-relative-only behavior).
  • common.test.sh: added regression coverage for wit_gh_resolve_bot_wrapper — resolves the consumer-local wrapper when present, falls back to the bundled path when the consumer has none, and falls back when CLAUDE_PROJECT_DIR is unset.

Verification

Full adapter + dispatcher test suite run, all green:

$ bash adapters/github/common.test.sh
PASS: [1] common.sh exposes gh_write
PASS: [2] common.sh exposes wit_run_gh
PASS: [3] common.sh exposes wit_resolve_repo
PASS: [4] common.sh exposes wit_emit_item
PASS: [5] common.sh exposes wit_lease_json
PASS: [6] common.sh exposes wit_lease_is_live
PASS: [7] common.sh exposes wit_list_lease_comments
PASS: [8] common.sh exposes wit_help_if_requested
PASS: [9] common.sh exposes wit_map_gh_error
PASS: [10] lease marker constant
PASS: [11] 404 → not-found (5)
PASS: [12] rate limit → unavailable (8)
PASS: [13] resolves consumer-local wrapper first when present
PASS: [14] falls back to bundled path when consumer has none
PASS: [15] falls back to bundled path when CLAUDE_PROJECT_DIR unset

All other adapters/github/*.test.sh files and work-item-tracker.test.sh (including its own consumer-local/adapter-resolution cases) also pass unchanged.

shellcheck on both modified shell files: no findings.

Closes #365

Related

… of adapter location

The GitHub adapter's WIT_GH_BOT resolved the bot wrapper relative to the
adapter's own directory, so a consuming repo's wrapper at
${CLAUDE_PROJECT_DIR}/tools/github-auth/gh-bot.sh — the override path
CONTRACT.md already documented — was never found, and tracker writes silently
fell back to the ambient gh (session-user) identity.

wit_gh_resolve_bot_wrapper now checks the consumer-local path first, falling
back to the plugin-bundled path, mirroring the adapter's own
consumer-local-first/plugin-bundled-fallback resolution.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1V3gkrfSf75isB8MiDy3o
@kyle-sexton kyle-sexton added the automated Opened by automation. label Jul 21, 2026
@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 53s —— View job


PR Review — fix(work-items): consumer-local gh-bot.sh wrapper resolution

  • Gather context and understand the request
  • Review changed files
    • common.sh — new wit_gh_resolve_bot_wrapper function
    • common.test.sh — regression test coverage
    • CONTRACT.md — documentation update
    • plugin.json — version bump
    • CHANGELOG.md — changelog entry
  • Assess correctness, security, and alignment with project guidelines
  • Post final review

Assessment: Approve

The fix is correct, minimal, and architecturally consistent. The bug (adapater-relative-only resolution of gh-bot.sh bypassing the documented consumer-local override path) is cleanly fixed without scope creep.


common.sh — wit_gh_resolve_bot_wrapper

common.sh L44–56

The implementation is correct:

  • Reads CLAUDE_PROJECT_DIR dynamically (not captured at function definition time), so subshell overrides in tests work cleanly.
  • The ${CLAUDE_PROJECT_DIR:-} guard avoids a set -u failure when the variable is unset — correct, matches how the adapter resolution guard handles the same variable.
  • The bundled fallback $WIT_GH_ADAPTER_DIR/../../../github-auth/gh-bot.sh is deliberately non-normalized (matching the prior behavior); [[ -f ... ]] in gh_write handles .. correctly.
  • Split from readonly WIT_GH_BOT=... to WIT_GH_BOT="$(...)"; readonly WIT_GH_BOT is the standard Bash pattern for assigning a computed value and immediately making it read-only.

One observation worth noting: WIT_GH_BOT is resolved once at source time and cached as readonly. The _WIT_GH_COMMON_LOADED guard means if common.sh is re-sourced in the same shell, the previously resolved value is kept — correct, since CLAUDE_PROJECT_DIR is expected stable within a session.

No issues.


common.test.sh — regression coverage

common.test.sh L30–52

Three cases, covering the contract exactly:

  • Consumer-local present (L39–44): uses -ef (inode-based comparison) rather than string equality — correctly handles any normalisation differences between the path the function returns and the path constructed in the test.
  • Consumer root exists but has no wrapper (L46–47): string-equals the bundled fallback. BUNDLED is constructed with the same $WIT_GH_ADAPTER_DIR/../../../... form the function emits, so the comparison is stable.
  • CLAUDE_PROJECT_DIR unset (L49–50): runs in a subshell ($()), so unset is scoped to the subshell and doesn't affect the parent test environment. Correct.

Temp dirs are cleaned up (L52). Tests don't add wit_gh_resolve_bot_wrapper to the public-API declaration list — correct, it's an internal helper.

No issues.


CONTRACT.md

CONTRACT.md L286–295

The updated "Identity routing" section accurately reflects the implemented behaviour: consumer-local-first, plugin-bundled fallback, independent of where the adapter resolved from. The cross-reference to "Adapter resolution" makes the design intent clear. The "zero vendoring" phrasing is a useful new clarification for consuming-repo authors.

No issues.


plugin.json / CHANGELOG.md

Patch version bump (0.18.10.18.2) is appropriate — this is a bug fix, not a feature addition. CHANGELOG entry is thorough and consistent with the format of surrounding entries (no dates, which matches the rest of the file).

No issues.


Summary

Clean, targeted fix that closes the gap between documented and implemented bot-wrapper resolution. All three critical resolution paths are covered by tests. No security concerns (the wrapper path is validated with [[ -f ... ]] before execution; the wrapper lives in the consumer's own repo tree, which is already trusted). No scope creep. Good to merge.

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 20s —— View job


Security Review

  • Gather PR diff and changed files
  • Analyze common.sh for shell injection, path traversal, and credential handling
  • Analyze common.test.sh for test-introduced risks
  • Analyze CONTRACT.md and plugin.json for config/permission risks
  • Synthesize and post findings

Findings

SUGGESTION — CLAUDE_PROJECT_DIR is not canonicalized before path construction · Confidence: Low

File: plugins/work-items/tools/work-item-tracker/adapters/github/common.sh

wit_gh_resolve_bot_wrapper() {
  if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]]; then
    local consumer_wrapper="$CLAUDE_PROJECT_DIR/tools/github-auth/gh-bot.sh"
    if [[ -f "$consumer_wrapper" ]]; then
      printf '%s\n' "$consumer_wrapper"

The constructed path is later passed to bash "$WIT_GH_BOT" (line 98). Because CLAUDE_PROJECT_DIR is not canonicalized (e.g., via realpath), a value containing .. segments could resolve outside the project root. A theoretical attacker who controls CLAUDE_PROJECT_DIR (e.g., via environment injection in a multi-tenant CI scenario) could cause an arbitrary file to be executed as the bot wrapper.

Confidence is low because in normal Claude Code operation CLAUDE_PROJECT_DIR is set by the platform harness and is not consumer-controllable. The rest of the plugin already uses CLAUDE_PROJECT_DIR without canonicalization, so this is consistent with the established trust model rather than a regression. Defense-in-depth would call for CLAUDE_PROJECT_DIR="$(realpath -- "$CLAUDE_PROJECT_DIR")" at the top of the function, but it is not strictly necessary given the platform trust model.

Fix this →


Summary

No CRITICAL or IMPORTANT security issues were found in this PR.

  • Shell injection / command injection: arguments to gh_write are passed through as "$@" — properly double-quoted throughout. No injection surface.
  • Credential/token handling: unchanged. The bot wrapper executes with the wrapper script's own ambient credentials; WIT_GH_BOT is captured at source time via readonly, preventing late mutation.
  • Path traversal: the constructed path flows through [[ -f ... ]] before bash execution. The check is necessary for the consumer-local-first fallback logic to work correctly. The pre-existing ../ sequences in the bundled fallback path are resolved by the shell naturally and were present before this PR.
  • Trust model: this change correctly implements the behavior already documented in CONTRACT.md's "Identity routing" section. The consumer controlling ${CLAUDE_PROJECT_DIR}/tools/github-auth/gh-bot.sh is an intentional capability — a consumer who can write to their own project root already has full code execution there.
  • Test code (common.test.sh): mktemp -d + set -u + cleanup with rm -rf is correct. The [[ -ef ]] (same-inode) comparison for path resolution testing is the right idiom here.
  • plugin.json / CONTRACT.md: version bump and documentation update only — no security-relevant configuration changes.

@kyle-sexton

kyle-sexton commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Post-green review classification (babysit lane):

Both automated PR reviews returned Approve / Good to merge with no blocking findings. Both automated security-review runs raised one identical Low-confidence defense-in-depth item, classified below.

# Finding Classification Evidence Reacted
1 CLAUDE_PROJECT_DIR not canonicalized before building consumer_wrapper in wit_gh_resolve_bot_wrapper (raised by both automated security-review runs) VALID (defer) Claim is technically true — no realpath at common.sh L45-46 — but non-actionable in isolation. The reviewer itself marks it Low confidence and "not strictly necessary given the platform trust model": CLAUDE_PROJECT_DIR is platform-set (not consumer-controllable in normal operation), and a consumer who can write ${CLAUDE_PROJECT_DIR}/tools/github-auth/gh-bot.sh already holds code execution in their own trusted tree. No other CLAUDE_PROJECT_DIR consumer in the plugin canonicalizes (grep: zero realpath in the adapter tree), so hardening only this one call site would be an inconsistent band-aid, not a root-cause fix. Deferred as a repo-wide hardening candidate: canonicalize consistently across all CLAUDE_PROJECT_DIR consumers, or not at all. Not a regression this diff introduces. 👍

No blocking or defer-forbidden item applies. No inline review threads exist on this PR (GraphQL query returned empty) — nothing to resolve.

@kyle-sexton
kyle-sexton marked this pull request as ready for review July 21, 2026 09:18
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 55s —— View job


Security Review

  • Gather context and understand the request
  • Read PR diff and changed files
  • Analyze common.sh for shell injection, path traversal, credential handling
  • Analyze common.test.sh for test-introduced risks
  • Analyze CONTRACT.md, plugin.json, CHANGELOG.md for config/permission risks
  • Synthesize and post findings

Findings

SUGGESTION — CLAUDE_PROJECT_DIR not canonicalized before path construction · Confidence: Low

File: plugins/work-items/tools/work-item-tracker/adapters/github/common.sh L45–52

wit_gh_resolve_bot_wrapper() {
  if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]]; then
    local consumer_wrapper="$CLAUDE_PROJECT_DIR/tools/github-auth/gh-bot.sh"
    if [[ -f "$consumer_wrapper" ]]; then
      printf '%s\n' "$consumer_wrapper"
      return 0
    fi
  fi
  printf '%s\n' "$WIT_GH_ADAPTER_DIR/../../../github-auth/gh-bot.sh"
}

The resolved path is subsequently passed to bash "$WIT_GH_BOT" "$@". Because CLAUDE_PROJECT_DIR is not canonicalized (e.g., via realpath), a value containing .. segments could resolve the wrapper to a path outside the project root. In a theoretical multi-tenant CI environment where CLAUDE_PROJECT_DIR is attacker-controlled, this could cause an arbitrary file to be executed as the bot wrapper.

Confidence is low for two reasons: (1) in normal Claude Code operation CLAUDE_PROJECT_DIR is set by the platform harness and is not consumer-controllable, and (2) this is fully consistent with how CLAUDE_PROJECT_DIR is used throughout the rest of the plugin — this PR introduces no regression in the trust model.

Defense-in-depth fix: add local project_dir; project_dir="$(realpath -- "$CLAUDE_PROJECT_DIR" 2>/dev/null || printf '%s' "$CLAUDE_PROJECT_DIR")" and use $project_dir in place of $CLAUDE_PROJECT_DIR when building consumer_wrapper.

Fix this →


No other security issues found

Independent review confirms the prior automated security review's conclusions:

  • Shell / command injection: "$@" is properly double-quoted throughout gh_write and wit_run_gh. No injection surface is introduced by this PR.
  • Arbitrary file execution surface: The attack chain (CLAUDE_PROJECT_DIRconsumer_wrapperbash "$WIT_GH_BOT") requires controlling a platform-set variable or writing a file to the consumer's own project root — both already in the trusted perimeter. The [[ -f "$WIT_GH_BOT" ]] guard at L97 is load-bearing for the fallback logic; it does not create a new execution surface.
  • Credential / token handling: WIT_GH_BOT is captured once at source time via readonly. The _WIT_GH_COMMON_LOADED guard ensures the resolved path cannot be changed by a second source. No credentials flow through the new code path.
  • bash hardcoded as interpreter for the wrapper (L98): pre-existing behavior, unchanged by this PR.
  • Test code (common.test.sh): mktemp -d paths are properly double-quoted throughout; rm -rf targets are known mktemp outputs; [[ -ef ]] (inode comparison) is the correct idiom for path-resolution assertions; unset CLAUDE_PROJECT_DIR is correctly scoped inside $(...) so it does not pollute the parent test environment.
  • plugin.json / CHANGELOG.md / CONTRACT.md: version bump and documentation only — no security-relevant configuration changes.

No CRITICAL or IMPORTANT findings. The SUGGESTION above is consistent with an existing codebase convention, not a regression this diff introduces.

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 33s —— View job


PR Review — fix(work-items): consumer-local gh-bot.sh wrapper resolution

  • Gather context and review changed files
  • Assess correctness, security, and alignment with project guidelines
  • Post final review

Assessment: Approve

This is a clean, targeted bug fix. The root cause (adapter-relative-only WIT_GH_BOT resolution silently bypassing the documented consumer-local path) is correctly identified and fixed without scope creep.


common.shwit_gh_resolve_bot_wrapper

common.sh L44–56

Correctness. All three resolution paths are correctly handled:

  • CLAUDE_PROJECT_DIR set and consumer wrapper exists → returns consumer path.
  • CLAUDE_PROJECT_DIR set but no consumer wrapper → falls through to bundled path.
  • CLAUDE_PROJECT_DIR unset → ${CLAUDE_PROJECT_DIR:-} safely evaluates to empty under set -u; falls through to bundled path.

Bundled fallback path. $WIT_GH_ADAPTER_DIR/../../../github-auth/gh-bot.sh navigates three levels up from adapters/github/ to reach tools/, then into tools/github-auth/gh-bot.sh — which is the correct plugin-bundled location. The path is deliberately non-normalized (matching the prior behavior); [[ -f "$WIT_GH_BOT" ]] in gh_write (L97) handles .. correctly.

readonly assignment pattern. Splitting WIT_GH_BOT="$(...)"; readonly WIT_GH_BOT (L55–56) is the correct Bash idiom. Under set -e, assigning directly into readonly WIT_GH_BOT=$(...) would suppress the subshell exit code; the split form preserves it.

Source guard. The _WIT_GH_COMMON_LOADED guard at L8–9 means wit_gh_resolve_bot_wrapper is called exactly once. Since WIT_GH_BOT is readonly, re-sourcing in the same shell would fail at the assignment if the guard were absent — the guard makes this safe.

No issues.


common.test.sh — regression coverage

common.test.sh L30–52

Three cases cover the contract exactly:

  • Consumer-local present (L39–44): uses -ef (inode comparison) — correctly handles any normalisation difference between the returned path and the test-constructed path.
  • Consumer root exists, no wrapper (L46–47): BUNDLED is built with the same $WIT_GH_ADAPTER_DIR/../../../... form the function emits, so the string comparison is stable.
  • CLAUDE_PROJECT_DIR unset (L49–50): unset runs inside $(...) so it's scoped to the subshell; the parent test environment is unaffected. Correct.

Temp dirs are cleaned up (L52). wit_gh_resolve_bot_wrapper is correctly absent from the public-API declaration list (L12–18) — it is an internal helper.

One minor observation: the test sources common.sh at L10, which captures WIT_GH_BOT via wit_gh_resolve_bot_wrapper and marks it readonly. The three new test cases then call wit_gh_resolve_bot_wrapper directly (not via the cached constant), which is intentional — the tests are exercising the resolution function, not the cached value. This is the right approach for regression coverage.

No issues.


CONTRACT.md — Identity routing section

CONTRACT.md L286–295

The updated "Identity routing" section now accurately documents the implemented behaviour: consumer-local-first, plugin-bundled fallback, independent of adapter location. The "zero vendoring" phrasing is a useful addition for consuming-repo authors. The cross-reference to "Adapter resolution" makes the design symmetry explicit.

No issues.


plugin.json / CHANGELOG.md

Patch version bump (0.18.10.18.2) is appropriate for a bug fix. CHANGELOG entry is thorough, accurate, and consistent with the format of surrounding entries.

No issues.


Summary

No correctness, security, or guideline concerns. The fix is minimal, all three resolution paths have regression tests, and the documentation is updated to match. Good to merge.

kyle-sexton added a commit that referenced this pull request Jul 21, 2026
…de-ops, work-items

Re-checked open PRs touching these plugins' plugin.json right before the version-bump
collision protocol requires it: source-control carries #839 (0.15.8) and #840 (0.15.9),
claude-ops carries #844 (0.17.2), work-items carries #826 (0.18.2) — all still open.
Bumps this PR's claims one past each plugin's current highest open-PR claim
(source-control 0.15.10, claude-ops 0.17.3, work-items 0.18.3) so this PR does not
collide at merge time regardless of which sibling lands first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1V3gkrfSf75isB8MiDy3o
@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Tower merge-sit: merge-drive lane dark ~5.5h (rate-limit hold since ~10:36Z); tower sitting merges per stall protocol. Gate-verified live: CLEAN, 0 unresolved threads. This was generated by AI (control tower, session 6).

@kyle-sexton
kyle-sexton merged commit 5ab7e3b into main Jul 21, 2026
24 of 25 checks passed
@kyle-sexton
kyle-sexton deleted the fix/365-work-items-gh-bot-wrapper-resolution branch July 21, 2026 16:07
kyle-sexton added a commit that referenced this pull request Jul 21, 2026
Resolves conflicts on source-control and work-items plugin.json/CHANGELOG.md:
main advanced source-control to 0.15.8 (#839, merged) and work-items to 0.18.2
(#826, merged) since this branch was last rebased. Also re-checked live open
PRs at merge time and found work-items now carries a new open claim, #857 at
0.19.0 (Jira adapter) — re-bumped this branch's work-items claim from 0.18.3
to 0.19.1 to stay ahead of it. source-control's 0.15.10 and claude-ops's
0.17.3 remain valid (still one past #840/#860's 0.15.9 and #844/#860's 0.17.2
open claims, respectively).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1V3gkrfSf75isB8MiDy3o
kyle-sexton added a commit that referenced this pull request Jul 25, 2026
… as deliberate (#853)

## Summary

Closes the design fork raised in #820: whether the shell assert-helper
duplicated across
5 plugins (and the divergent per-script exit-code taxonomies alongside
it) should be
consolidated into a shared mechanism, or documented as deliberate. This
PR documents.

## Fix

- Adds `docs/conventions/shell-test-helpers/README.md` as the owner doc
explaining why the
duplication and divergence stay as-is, and registers it in
`docs/PLUGIN-PHILOSOPHY.md`'s
  convention registry table.
- Adds a one-line pointer comment at each copy site back to the owner
doc:
`guardrails/hooks/guardrails-test-helpers.sh`,
`claude-ops/hooks/claude-ops-test-helpers.sh`,
`source-control/scripts/test-helpers.sh`,
`repo-hygiene/skills/clean/scripts/lib/test-helpers.sh`,
  `work-items/tools/work-item-tracker/tests/lib.sh`.
- Adds the same kind of pointer to
`scripts/check-skill-portability.test.sh`, which opted out
for an unrelated reason (it's repo tooling, not a plugin, so no plugin
assertion library
applies) — noted so the fork's second observation isn't left
unexplained.
- Bumps `plugin.json` + adds a `CHANGELOG.md` entry for every plugin
whose helper file gained
the pointer comment. Each has been re-derived from current `main` many
times as sibling PRs
  merged or rebased mid-flight (see Related for the current picture):
  - `repo-hygiene`: 0.4.4→0.4.6
  - `source-control`: 0.15.7→0.16.1
  - `claude-ops`: 0.17.1→0.17.5
  - `work-items`: 0.18.1→0.20.1
  - `guardrails`: 0.9.5→0.9.6 (no open-PR collision at any point so far)

No behavior change anywhere — comments and docs only.

## Decision

**Chose Option B: document the per-plugin duplication and exit-code
divergence as deliberate.
No shared helper introduced.**

Investigation before deciding:

- This repo already has one sanctioned cross-plugin shared-source
mechanism: a canonical file
under `lib/` (e.g. `lib/hook-utils.sh`), copied — not imported — into
each carrying plugin by
a dedicated `scripts/sync-*.sh`, tracked in
`scripts/cross-plugin-source-registry.txt`, and
  drift-checked by `scripts/check-cross-plugin-source-drift.sh --check`.
- That mechanism is scoped to clusters meant to stay **byte-identical**.
Running
`check-cross-plugin-source-drift.sh discover` confirms it never even
flags the five
assert-helper files as a cluster candidate — they live at different
paths per plugin and
are not byte-identical, so they fall outside that mechanism's scope
entirely.
- Reading all five files: they are already three genuinely different
shapes, not one library
that drifted — a hook-contract shape (`guardrails`/`claude-ops`:
`ok`/`bad`, `PASS`/`FAIL`,
`make_sink`/`wait_for_sink`), a skill-script shape
(`source-control`/`repo-hygiene`: `pass`/`fail`,
`FAILED`/`CASE_NUM`, file-existence assertions), and a vendored-seam
shape (`work-items`: same
primitives, but owned by the seam itself so it stays correct wherever
the seam is resolved
  from, independent of this repo's tooling).
- Consolidating would mean designing a fourth, unified assertion API and
rewriting every
existing `*.test.sh` onto it — a bigger, riskier change than the
coupling it would remove,
and it would cross the plugin-independence boundary
`docs/PLUGIN-PHILOSOPHY.md`'s design
boundary section already draws (no plugin imports files from a sibling
plugin).
- Exit-code taxonomies (`remove-path.sh` 0/1/2/3/4,
`git-tree-reset-batch.sh` 0/1/2 forwarding
a child's 5/7, `check-skill-portability.sh` 0/1/2) encode genuinely
different per-script
contracts, not arbitrary numbering — each script already documents its
own `Exit:` line, and
a shared usage/exit helper would either flatten those contracts or grow
per-caller branching.
- Deferred, not rejected: `guardrails-test-helpers.sh` and
`claude-ops-test-helpers.sh` are the
one pair that already share a shape closely. If they converge to
byte-identical, vendoring
just that pair through the existing `lib/` + `sync-*.sh` + registry
mechanism is the smaller,
precedented move — recorded as the trigger in the owner doc rather than
acted on now.

## Verification

- `shellcheck` clean on all 6 edited shell files.
- Full `check-skill-portability.test.sh` suite: 16/16 pass.
- `check-cross-plugin-source-drift.sh --check`: no unregistered or
drifted clusters.
- `check-changelog-parity.sh --check`: passes with every version bump.
- Ran every `*.test.sh` that sources an edited helper
(repo-hygiene/clean, guardrails hooks,
claude-ops hooks, work-items adapters/lib) — all green, confirming the
comment-only edits
  changed no behavior.
- `markdownlint-cli2` and `lychee` clean on the new and modified docs.

## Related

- Scope note: issue #820's title says "disk-hygiene/clean", but
`disk-hygiene` is Python-only
(`hygiene.py`) with no shell assert-helper — the actual duplication
lives in the 5 plugins
the issue body names (repo-hygiene, source-control, guardrails,
claude-ops, work-items) plus
root `scripts/`. Treating the title as a triage typo (disk-hygiene vs.
repo-hygiene, both
"-hygiene" plugins with a `clean` skill) rather than touching
disk-hygiene.
- **`do-not-merge` held.** This session has had exceptionally heavy
concurrent-lane traffic
against these same 5 plugins — this PR has been rebased/re-derived nine
times as siblings
merged (#839, #826, #857, #877, #870, #844) or rebased in place (#840,
#882, #861, each more
than once). Current picture, last verified fresh at commit `e75e45ed`
(`mergeable: MERGEABLE`;
all 5 plugins re-checked against current `main` AND every live open PR):
- `repo-hygiene` (claims 0.4.6): no open-PR collision. Main is at 0.4.5.
- `source-control` (claims 0.16.1): held behind **#882**
(`fix/511-babysit-self-identity-decouple`,
claims 0.16.0, open) and **#840** (claims 0.15.10, open). Main is at
0.15.9.
- `claude-ops` (claims 0.17.5): **no open-PR collision anymore** — #844
(the PR this leg was
previously held behind) has merged, landing at exactly 0.17.4; this
claim stays one past it.
    Main is at 0.17.4.
- `work-items` (claims 0.20.1): held behind **#861**
(`feat/613-mini-sdlc-pipeline-ssot`). #861
itself has re-derived its claim twice as `main` moved — from 0.19.0 up
to 0.20.0 (following
#857's Jira-adapter minor bump into main) — so this PR's claim moved
from 0.19.1 to 0.20.1 to
    stay ahead. Main is at 0.19.0.
  - `guardrails` (claims 0.9.6): no open-PR collision. Main is at 0.9.5.
- This plugin set has produced a new collision within minutes of nearly
every prior check —
including siblings re-deriving their own claims upward more than once,
an unrelated PR
landing at the exact same version by coincidence, and legs clearing and
new ones opening.
Re-run the full collision protocol (`gh pr list --repo
melodic-software/claude-code-plugins
--state open --json number,headRefName,files` filtered per plugin, AND a
fresh diff of each
plugin's version on `main` since this PR's last rebase) immediately
before removing
    `do-not-merge` — do not trust this snapshot.

Closes #820

Work-class: C2 (mechanical) — attended triage 2026-07-23,
operator-ratified. 🤖

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automated Opened by automation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

work-items: github adapter should resolve a consumer-local gh-bot.sh wrapper independent of adapter location

1 participant