Ship the suite as a Claude Code marketplace plugin (repo move, plugin root, CLAUDE_PLUGIN_ROOT paths, namespacing) #50

Closed
opened 2026-08-15 07:36:59 +00:00 by jbr870 · 67 comments
Owner

PREQ: Ship the suite as a Claude Code marketplace plugin

Created: 2026-08-15

Problem

The suite operator struggles to keep the SDLC suite consistent, versioned, and honestly dogfooded
across two machines when the suite is distributed as a hand-maintained symlink farm.

Today the suite's source lives at ~/.claude/repos/devwork-skills — a product's source inside the
runtime's config directory — and reaches Claude Code through 15 symlinks
(~/.claude/skills/<x> → ../repos/devwork-skills/.claude/skills/<x>). The desktop (jochems-forge)
runs a second clone of the same farm, synced by manual git pull, with no versioning: nothing
records which suite state a run executed, and stale checkouts are silent. Worst, the dev form is
not the shipped form — the farm hides exactly the failure classes real distribution introduces
(path resolution, namespacing, meta-lane fencing), so packaging regressions would surface on the
consumer machine instead of where development happens. The shipped/repo-local fence is convention
only: nothing structurally prevents a consumer from invoking the repo-local meta-* skills.

Users:

  • Primary: the suite operator/developer — develops, releases, and dogfoods the suite on the
    dev machine; consumes it on the desktop.
  • Secondary: consumer projects on both machines (castellan, verity, axana, outwrit, cunnan, and
    this repo itself) whose CLAUDE.md guidance blocks and operators invoke the suite's skills.

Current state: manual symlink farm on both machines; manual-pull sync; bare skill names; no
version identity; meta lane fenced by convention only.

Proposed Solution

Package the suite as a Claude Code marketplace plugin named dev and make the plugin form the
only form anyone runs:

  • The 14 shipped skills — develop, gh-cli, git-worktrees, glab-cli, integrate, local-fs,
    promote, requirements, resolve, setup, simplify-report, tea-cli, technical-plan,
    worktree-cleanup — plus _shared move to a plugin root inside the repo; the repo carries its
    own marketplace manifest. Repo-local material (meta skills, scripts/, analysis docs, run
    reports, .devwork/) never ships.
  • Dev machine consumes the plugin live (skills-dir form), so every project on the box —
    including this repo's own dogfooding — exercises the shipped form and packaging regressions
    surface where development happens.
  • Desktop becomes a pure consumer: versioned marketplace installs from the GitHub remote
    (github.com/jbr870/devwork-skills), updated by deliberate version bump. Unreleased suite
    changes are dogfooded on the dev machine only — the desktop never runs anything but releases.
  • The namespace is the fence: shipped skills are invoked as /dev:<skill> on every machine; bare
    names remain repo-local (meta lane), structurally unreachable from consumers.
  • As part of the same cutover, the repo moves out of ~/.claude/repos/ to
    ~/Coding/devwork-skills, preserving project identity (memory, trust, MCP config), and every
    consumer project's CLAUDE.md guidance block on the cut-over machine is updated to /dev: names
    in the same disruption window.
  • Releases become a disciplined, tooling-enforced act (clean tree, agreeing manifests — the
    version fields of the plugin manifest and the marketplace entry — unique version tag) starting
    at 0.1.0, instead of a pull.

Scope: Full — plugin packaging, namespacing, both machine cutovers, and the repo move, in one
feature (one disruption window; the move was judged severable but bundling was chosen, and the
scope was re-confirmed against the panel's simpler-alternative case).

User Stories

  • As the suite developer, I want the dev machine to run the suite in its shipped plugin form so
    that packaging regressions surface during development instead of on the consumer machine.
  • As the suite operator on the desktop, I want to install and update the suite as a versioned
    marketplace plugin so that every run's suite state is a known release instead of a silently
    stale checkout.
  • As a consumer-project operator, I want all shipped skills invoked under the /dev: namespace so
    that shipped capability and repo-local meta tooling cannot be confused or cross-invoked.
  • As the suite developer, I want the meta lane to stay repo-local and outside the shipped plugin so
    that the shipped/internal fence is enforced by the loader, not by convention.
  • As the suite developer, I want the repo to live in the normal coding-projects root so that the
    product's source no longer lives inside the runtime's config directory.

Acceptance Criteria

  • AC-1: Given the restructured repo, when the plugin and marketplace manifests are validated
    with the plugin tooling's strict mode, then validation passes with zero errors and zero
    warnings.

  • AC-2 (post-cutover; contingent on the P0.1 probe confirming plugin-skill
    slash-invocability)
    : Given a cut-over machine with the plugin loaded in its form for that
    machine, when the operator lists skills AND when the operator invokes each of the 14 shipped
    skills as /dev:<skill>, then all 14 appear in the listing and each invocation resolves —
    and no shipped skill resolves under its bare name. Both listing and invocation are checked
    on each machine's form.

  • AC-3: Given the shipped plugin's contents, when scanned mechanically, then they contain zero
    references to ~/.claude/skills/ paths, zero bare _shared/ invocations (any _shared/
    reference — in a shell fence or in markdown text — not prefixed by the plugin-root
    variable), and zero /dev:<x> references where <x> is not one of the 14 shipped skills —
    and the mechanical scan itself is verified non-vacuous (it demonstrably sees a non-zero file
    population).

  • AC-4: Given this repo, when the operator invokes /meta-amend or /meta-retrospective by
    bare name, then they run; and when the shipped plugin's contents are inspected, then no meta
    skill or repo-local script is present in it.

  • AC-5: Given the plugin form loaded pre-cutover (coexisting with the farm — bare names may
    still resolve during this window), when (a) a full /dev:requirements phase runs on a
    scratch issue of this repo (tea forge), (b) a worktree slot is created, (c) one meta-amend
    cycle runs, and (d) one pipeline phase runs on castellan (gh-cli — the non-tea adapter leg),
    then all four succeed with their forge records posted correctly per the forge contract: the
    Phase Outcome present and parseable on the right issue, and the phase label advanced.

  • AC-6: Given the dev-machine cutover has run, when the operator starts a session in the moved
    repo at ~/Coding/devwork-skills, then the suite works from the new location and project
    identity is preserved: memory loads, no trust re-prompt appears, and previously configured
    MCP servers are still available.

  • AC-7: Given cutover verification fails on the dev machine, when the abort path runs, then
    the pre-cutover state is restored (farm links, repo location, project identity) — and this
    abort path has been rehearsed on a scratch copy before the real cutover. The desktop's
    previous release remains installable from the remote as its recovery path.

  • AC-8: Given a cut release, when the desktop installs the plugin from the GitHub marketplace
    source and runs a helper-invoking pipeline phase, then the install succeeds at the released
    version and the phase completes with its forge records posted (same standard as AC-5).

  • AC-9: Given a subsequent release, when the desktop performs the update ritual, then it runs
    the new version — the vN → vN+1 path is exercised, not just fresh install.

  • AC-10: Given a dirty working tree, disagreeing manifest version fields, or an
    already-existing version tag, when a release is attempted, then the release tooling refuses
    with a non-zero exit and no tag or install artifact is produced — each refusal condition
    exercised separately.

  • AC-11: Given only the rewritten README — which enumerates the prerequisites (bash ≥ 3.2,
    jq, git, the project's forge CLI) — the desktop cutover itself is executed following
    only the README's instructions; any step that needed outside knowledge is a README defect.

  • AC-12: Given a machine's cutover completed, when that machine is inspected, then no
    symlink-farm links (and on the desktop, no suite clone) remain, every consumer-project
    CLAUDE.md guidance block on it references shipped skills by /dev: names only, and the
    README's consumer runbook carries the step by which the operator records the installed
    release version against a run.

    **Amended at UAT, 2026-08-22 (UAT-F33).** The third clause originally read "*and desktop run
    records are attributable to the installed release version*". As written that asks for
    **tooling-enforced** attribution — stamping the version into the phase-outcome record — which
    is a skill *logic* change and so barred by this issue's own Out of Scope. What #50 actually
    scoped and built is the manual mechanism, and UAT showed the manual mechanism is not
    reliable: in the one real consumer-machine run (`PO-239-1` on verity#239) the operator did
    not record it, and an instruction the tooling does not enforce is not an attribution
    mechanism. **Enforced attribution is deferred to #34**, which now carries the UAT evidence
    and the design input measured on both machines. This AC claims only what #50 delivered.
    

Out of Scope

  • Publishing the plugin beyond the operator's own remotes (no public marketplace listing).
  • Automated eval suites for the plugin (plugin eval) — future issue.
  • Any skill logic change: this feature is layout + references + packaging + tooling only. (The
    two sanctioned restorative text-behavior touches from the draft plan — the meta-helper path fix
    and the adapter discovery-order hand-rewrite — are the only exceptions.)
  • Version-stamping dev-machine run records: the dev machine runs the live checkout; git is its
    version identity. Release-version attribution applies to the desktop only (AC-12).
  • Automating desktop updates (update remains a deliberate manual act).
  • Any desktop escape hatch for unreleased suite changes (dormant clone, path-based side-load) —
    decided against: unreleased dogfooding is dev-machine-only.
  • Renaming or restructuring the meta-lane skills beyond what the fence requires.

Dependencies

  • Claude Code plugin subsystem (external system of record): marketplace add/install/update,
    skills-dir plugin loading, strict validation, version tagging, ${CLAUDE_PLUGIN_ROOT}
    substitution. Its real contract was probed 2026-08-14/15 (recorded on issue #50), but three
    behaviors remain unverified and are named as probe preconditions in the draft plan (P0.1
    slash-invocability of plugin skills, P0.2 farm/plugin coexistence, P0.3 desktop install
    credential path). P0.1 is decisive: if plugin skills are not invocable as /dev:<skill>, the
    run halts with a Pending Decision for the operator — the command-wrapper fallback is not
    pre-authorized.
  • GitHub remote github.com/jbr870/devwork-skills — the desktop's distribution source,
    including a working credential path from the desktop if the repo is private.
  • Claude Code runtime config (~/.claude/projects/, ~/.claude.json) — the repo move rewrites
    project-keyed state; identity preservation is an explicit criterion (AC-6).
  • Work-queue freeze: the meta-amend queue (#38–#49) and verity#237 frozen during the cutover
    phases; "no feature in flight on either machine" is asserted by the cutover script as a scripted
    precondition, not just declared.
  • Prior art: the panel-reviewed draft technical plan on issue #50 (comment 969) — input to
    /technical-plan, not a binding SREQ.

Timeline

Milestone Date Notes
Requirements complete 2026-08-15
Development complete
QA complete
UAT approved

Notes

  • Constraints: releases only from a clean tree (install provenance records HEAD while content
    ships from the working tree — verified hazard); the plugin name dev locks before any reference
    rewrite (renaming re-churns every reference); historical records (run reports, retrospectives,
    analysis docs) keep pre-migration names verbatim — audit integrity beats consistency.
  • Residual gap (acknowledged): two load forms remain — skills-dir (dev) and marketplace cache
    (desktop). The dev machine catches skills-dir-form regressions; cache-form-specific regressions
    are covered by the desktop's helper-invoking phase run (AC-8), not by dev-machine dogfooding.
  • Open questions: P0.1 outcome (deliberately open — halts for re-decision if skills are not
    slash-invocable); whether a running session hot-reloads skills-dir edits (affects /meta-amend
    desk-check wording only, new-session load is confirmed).
  • Key decisions: bundle the repo move into this feature (one disruption window; re-confirmed
    after adversarial review); plugin name dev (operator's call over the recommended devwork);
    GitHub as the desktop's distribution source; desktop dogfooding of unreleased changes ends with
    no escape hatch (dev machine only); P0.1 fallback not pre-authorized (stop and re-decide);
    version attribution is desktop-only; cutover requires a rehearsed rollback path; consumer
    guidance blocks updated in scope with AC; README verified by executing the desktop cutover from
    it alone.
# PREQ: Ship the suite as a Claude Code marketplace plugin **Created:** 2026-08-15 ## Problem The suite operator struggles to keep the SDLC suite consistent, versioned, and honestly dogfooded across two machines when the suite is distributed as a hand-maintained symlink farm. Today the suite's source lives at `~/.claude/repos/devwork-skills` — a product's source inside the runtime's config directory — and reaches Claude Code through 15 symlinks (`~/.claude/skills/<x> → ../repos/devwork-skills/.claude/skills/<x>`). The desktop (jochems-forge) runs a second clone of the same farm, synced by manual `git pull`, with no versioning: nothing records which suite state a run executed, and stale checkouts are silent. Worst, the dev form is not the shipped form — the farm hides exactly the failure classes real distribution introduces (path resolution, namespacing, meta-lane fencing), so packaging regressions would surface on the consumer machine instead of where development happens. The shipped/repo-local fence is convention only: nothing structurally prevents a consumer from invoking the repo-local `meta-*` skills. **Users:** - **Primary:** the suite operator/developer — develops, releases, and dogfoods the suite on the dev machine; consumes it on the desktop. - **Secondary:** consumer projects on both machines (castellan, verity, axana, outwrit, cunnan, and this repo itself) whose CLAUDE.md guidance blocks and operators invoke the suite's skills. **Current state:** manual symlink farm on both machines; manual-pull sync; bare skill names; no version identity; meta lane fenced by convention only. ## Proposed Solution Package the suite as a Claude Code **marketplace plugin named `dev`** and make the plugin form the only form anyone runs: - **The 14 shipped skills** — develop, gh-cli, git-worktrees, glab-cli, integrate, local-fs, promote, requirements, resolve, setup, simplify-report, tea-cli, technical-plan, worktree-cleanup — plus `_shared` move to a plugin root inside the repo; the repo carries its own marketplace manifest. Repo-local material (meta skills, `scripts/`, analysis docs, run reports, `.devwork/`) never ships. - **Dev machine** consumes the plugin live (skills-dir form), so every project on the box — including this repo's own dogfooding — exercises the shipped form and packaging regressions surface where development happens. - **Desktop** becomes a pure consumer: versioned marketplace installs from the GitHub remote (`github.com/jbr870/devwork-skills`), updated by deliberate version bump. Unreleased suite changes are dogfooded on the dev machine only — the desktop never runs anything but releases. - The namespace is the fence: shipped skills are invoked as `/dev:<skill>` on every machine; bare names remain repo-local (meta lane), structurally unreachable from consumers. - As part of the same cutover, the repo moves out of `~/.claude/repos/` to `~/Coding/devwork-skills`, preserving project identity (memory, trust, MCP config), and every consumer project's CLAUDE.md guidance block on the cut-over machine is updated to `/dev:` names in the same disruption window. - Releases become a disciplined, tooling-enforced act (clean tree, agreeing manifests — the version fields of the plugin manifest and the marketplace entry — unique version tag) starting at **0.1.0**, instead of a pull. **Scope:** Full — plugin packaging, namespacing, both machine cutovers, and the repo move, in one feature (one disruption window; the move was judged severable but bundling was chosen, and the scope was re-confirmed against the panel's simpler-alternative case). ## User Stories - As the suite developer, I want the dev machine to run the suite in its shipped plugin form so that packaging regressions surface during development instead of on the consumer machine. - As the suite operator on the desktop, I want to install and update the suite as a versioned marketplace plugin so that every run's suite state is a known release instead of a silently stale checkout. - As a consumer-project operator, I want all shipped skills invoked under the `/dev:` namespace so that shipped capability and repo-local meta tooling cannot be confused or cross-invoked. - As the suite developer, I want the meta lane to stay repo-local and outside the shipped plugin so that the shipped/internal fence is enforced by the loader, not by convention. - As the suite developer, I want the repo to live in the normal coding-projects root so that the product's source no longer lives inside the runtime's config directory. ## Acceptance Criteria - [ ] AC-1: Given the restructured repo, when the plugin and marketplace manifests are validated with the plugin tooling's strict mode, then validation passes with zero errors and zero warnings. - [ ] AC-2 *(post-cutover; contingent on the P0.1 probe confirming plugin-skill slash-invocability)*: Given a cut-over machine with the plugin loaded in its form for that machine, when the operator lists skills AND when the operator invokes each of the 14 shipped skills as `/dev:<skill>`, then all 14 appear in the listing and each invocation resolves — and no shipped skill resolves under its bare name. Both listing and invocation are checked on each machine's form. - [ ] AC-3: Given the shipped plugin's contents, when scanned mechanically, then they contain zero references to `~/.claude/skills/` paths, zero bare `_shared/` invocations (any `_shared/` reference — in a shell fence or in markdown text — not prefixed by the plugin-root variable), and zero `/dev:<x>` references where `<x>` is not one of the 14 shipped skills — and the mechanical scan itself is verified non-vacuous (it demonstrably sees a non-zero file population). - [ ] AC-4: Given this repo, when the operator invokes `/meta-amend` or `/meta-retrospective` by bare name, then they run; and when the shipped plugin's contents are inspected, then no meta skill or repo-local script is present in it. - [ ] AC-5: Given the plugin form loaded pre-cutover (coexisting with the farm — bare names may still resolve during this window), when (a) a full `/dev:requirements` phase runs on a scratch issue of this repo (tea forge), (b) a worktree slot is created, (c) one meta-amend cycle runs, and (d) one pipeline phase runs on castellan (gh-cli — the non-tea adapter leg), then all four succeed with their forge records posted correctly per the forge contract: the Phase Outcome present and parseable on the right issue, and the phase label advanced. - [ ] AC-6: Given the dev-machine cutover has run, when the operator starts a session in the moved repo at `~/Coding/devwork-skills`, then the suite works from the new location and project identity is preserved: memory loads, no trust re-prompt appears, and previously configured MCP servers are still available. - [ ] AC-7: Given cutover verification fails on the dev machine, when the abort path runs, then the pre-cutover state is restored (farm links, repo location, project identity) — and this abort path has been rehearsed on a scratch copy before the real cutover. The desktop's previous release remains installable from the remote as its recovery path. - [ ] AC-8: Given a cut release, when the desktop installs the plugin from the GitHub marketplace source and runs a helper-invoking pipeline phase, then the install succeeds at the released version and the phase completes with its forge records posted (same standard as AC-5). - [ ] AC-9: Given a subsequent release, when the desktop performs the update ritual, then it runs the new version — the vN → vN+1 path is exercised, not just fresh install. - [ ] AC-10: Given a dirty working tree, disagreeing manifest version fields, or an already-existing version tag, when a release is attempted, then the release tooling refuses with a non-zero exit and no tag or install artifact is produced — each refusal condition exercised separately. - [ ] AC-11: Given only the rewritten README — which enumerates the prerequisites (bash ≥ 3.2, `jq`, `git`, the project's forge CLI) — the desktop cutover itself is executed following only the README's instructions; any step that needed outside knowledge is a README defect. - [ ] AC-12: Given a machine's cutover completed, when that machine is inspected, then no symlink-farm links (and on the desktop, no suite clone) remain, every consumer-project CLAUDE.md guidance block on it references shipped skills by `/dev:` names only, and the README's consumer runbook carries the step by which the operator records the installed release version against a run. **Amended at UAT, 2026-08-22 (UAT-F33).** The third clause originally read "*and desktop run records are attributable to the installed release version*". As written that asks for **tooling-enforced** attribution — stamping the version into the phase-outcome record — which is a skill *logic* change and so barred by this issue's own Out of Scope. What #50 actually scoped and built is the manual mechanism, and UAT showed the manual mechanism is not reliable: in the one real consumer-machine run (`PO-239-1` on verity#239) the operator did not record it, and an instruction the tooling does not enforce is not an attribution mechanism. **Enforced attribution is deferred to #34**, which now carries the UAT evidence and the design input measured on both machines. This AC claims only what #50 delivered. ## Out of Scope - Publishing the plugin beyond the operator's own remotes (no public marketplace listing). - Automated eval suites for the plugin (`plugin eval`) — future issue. - Any skill *logic* change: this feature is layout + references + packaging + tooling only. (The two sanctioned restorative text-behavior touches from the draft plan — the meta-helper path fix and the adapter discovery-order hand-rewrite — are the only exceptions.) - Version-stamping dev-machine run records: the dev machine runs the live checkout; git is its version identity. Release-version attribution applies to the desktop only (AC-12). - Automating desktop updates (update remains a deliberate manual act). - Any desktop escape hatch for unreleased suite changes (dormant clone, path-based side-load) — decided against: unreleased dogfooding is dev-machine-only. - Renaming or restructuring the meta-lane skills beyond what the fence requires. ## Dependencies - **Claude Code plugin subsystem** (external system of record): marketplace add/install/update, skills-dir plugin loading, strict validation, version tagging, `${CLAUDE_PLUGIN_ROOT}` substitution. Its real contract was probed 2026-08-14/15 (recorded on issue #50), but three behaviors remain unverified and are named as probe preconditions in the draft plan (P0.1 slash-invocability of plugin *skills*, P0.2 farm/plugin coexistence, P0.3 desktop install credential path). **P0.1 is decisive: if plugin skills are not invocable as `/dev:<skill>`, the run halts with a Pending Decision for the operator — the command-wrapper fallback is not pre-authorized.** - **GitHub remote** `github.com/jbr870/devwork-skills` — the desktop's distribution source, including a working credential path from the desktop if the repo is private. - **Claude Code runtime config** (`~/.claude/projects/`, `~/.claude.json`) — the repo move rewrites project-keyed state; identity preservation is an explicit criterion (AC-6). - **Work-queue freeze:** the meta-amend queue (#38–#49) and verity#237 frozen during the cutover phases; "no feature in flight on either machine" is asserted by the cutover script as a scripted precondition, not just declared. - **Prior art:** the panel-reviewed draft technical plan on issue #50 (comment 969) — input to `/technical-plan`, not a binding SREQ. ## Timeline | Milestone | Date | Notes | |-----------|------|-------| | Requirements complete | 2026-08-15 | | | Development complete | | | | QA complete | | | | UAT approved | | | ## Notes - **Constraints:** releases only from a clean tree (install provenance records HEAD while content ships from the working tree — verified hazard); the plugin name `dev` locks before any reference rewrite (renaming re-churns every reference); historical records (run reports, retrospectives, analysis docs) keep pre-migration names verbatim — audit integrity beats consistency. - **Residual gap (acknowledged):** two load forms remain — skills-dir (dev) and marketplace cache (desktop). The dev machine catches skills-dir-form regressions; cache-form-specific regressions are covered by the desktop's helper-invoking phase run (AC-8), not by dev-machine dogfooding. - **Open questions:** P0.1 outcome (deliberately open — halts for re-decision if skills are not slash-invocable); whether a running session hot-reloads skills-dir edits (affects `/meta-amend` desk-check wording only, new-session load is confirmed). - **Key decisions:** bundle the repo move into this feature (one disruption window; re-confirmed after adversarial review); plugin name `dev` (operator's call over the recommended `devwork`); GitHub as the desktop's distribution source; desktop dogfooding of unreleased changes ends with no escape hatch (dev machine only); P0.1 fallback not pre-authorized (stop and re-decide); version attribution is desktop-only; cutover requires a rehearsed rollback path; consumer guidance blocks updated in scope with AC; README verified by executing the desktop cutover from it alone.
Author
Owner

Technical Plan v2: Ship the suite as a Claude Code marketplace plugin (#50)

Status: pre-pipeline draft, panel-reviewed/requirements has not run on #50, so this is
not an sreq:v1. Drafted at the operator's request 2026-08-15; revised after a five-viewpoint
adversarial panel (see §11). Product-decidable choices are marked [PREQ] and left open with
recommendations, not locked. Evidence: #50 probes (2026-08-14) + this session's verifications.

1. Context

The SDLC suite lives at ~/.claude/repos/devwork-skills, exposed on the dev machine via a
symlink farm (~/.claude/skills/<x> -> ../repos/devwork-skills/.claude/skills/<x>, 15 links =
14 shipped skills + _shared; meta-* are not linked — already project-scope only) and on the
desktop (jochems-forge) by a manually-pulled clone with the same farm. Target state:

  • Repo moves to ~/Coding/devwork-skills [PREQ — bundling the move multiplies cutover risk;
    recommended but severable]
    .
  • Suite ships as a plugin: dev machine consumes it live as a skills-dir plugin
    (~/.claude/skills/devwork -> <repo>/plugin), desktop consumes a versioned marketplace
    install from a git remote.
  • The namespace is the fence: devwork:* = shipped; bare names = repo-local meta lane.

2. Verified ground truth

From #50's probes (2026-08-14): skills-dir plugin auto-load, symlink support, live edits;
namespacing mandatory and identical across both forms; ${CLAUDE_PLUGIN_ROOT} substituted at
load time in plugin skill markdown but empty in the Bash tool env; marketplace installs are
versioned working-tree snapshots; update is version-gated; gitCommitSha records HEAD
(dirty-tree hazard); claude plugin validate/tag/details/init/eval exist.

Verified 2026-08-15 (plan + panel session):

  • Remotes: origin (Gitea forge), github (github.com/jbr870/devwork-skills), desktop
    (ssh jochems-forge). ~/Coding is the projects root. Tree clean at 1e6bd78.
  • claude plugin marketplace add <url|path|github-repo> [--scope user|project|local] [--sparse].
  • claude plugin validate exits 0 with warnings; only --strict fails — the gate must be
    --strict, and --strict requires author in plugin.json and description in the
    marketplace entry.
  • plugin.json version wins at install; a marketplace-entry version is silently ignored.
  • skills/_shared/ (no SKILL.md) is tolerated by --strict validate — no error, no
    warning. plugin.json also accepts an explicit "skills": [...] array as an escape hatch.
  • Frontmatter with only description: (the suite's shape) validates clean — no frontmatter churn.
  • claude plugin tag already enforces clean-tree, tag-uniqueness, and plugin↔marketplace
    agreement (--dry-run, --push, --remote).
  • claude --plugin-dir <path> loads a plugin session-scoped, without touching ~/.claude/skills.
  • claude plugin details <name> resolves installed plugins; skills-dir resolution is
    unverified (only plugin eval documents it).
  • Projects-dir slug format confirmed: old -home-jochem--claude-repos-devwork-skills, new
    -home-jochem-Coding-devwork-skills (no collision). ~/.claude.json projects map is keyed
    by absolute path and carries trust-dialog acceptance + MCP server config.
  • .git/worktrees is currently empty. .devwork/readiness-report.md hardcodes the old path.
  • Consumer CLAUDE.md survey (dev machine): only ~/Coding/castellan/CLAUDE.md carries a devwork
    guidance block; jochems-forge consumers (verity, axana, outwrit, cunnan) unverified — ssh
    unreachable from the drafting session.

Reference-rewrite inventory: 56 ~/.claude/skills/... refs (22 files); 108 bare
_shared/procedures/bin/... invocations; a large but unenumerated set of cross-skill slash
refs (the draft's "~500" overcounted: it includes /simplify (separate user-level skill),
built-ins (/init, /login), retired names (/closeout, /decisions), and path-lookalikes
(/tmp, /health, /timeline) that must NOT be rewritten). Enumeration is a Phase 2 input,
not a guess. The shipped set is exactly 14: develop, gh-cli, git-worktrees, glab-cli, integrate,
local-fs, promote, requirements, resolve, setup, simplify-report, tea-cli, technical-plan,
worktree-cleanup.

3. Approaches considered

A. In-place dual-form (plugin root mirrors .claude/skills/): rejected — same-skill
double-load hazard (verified) + guaranteed drift.

B. Big-bang restructure on the checked-out branch: rejected — the farm points into this
repo's working tree; the branch's git mv would kill the live suite mid-development.

C. Restructure in an isolated worktree; staged, scripted cutover (chosen) — with the panel's
correction: worktree isolation alone only defers B's failure to the merge instant (the farm
dies when main fast-forwards, while /integrate is still executing from it). The cutover is
therefore not "at integration" as a side effect — it is an explicit scripted sequence in
which the plugin form is loaded and verified before the merge, the merge runs under the plugin
form, and the farm is dismantled after (§6 Phase 5).

4. Decision record

# Decision Choice Rationale / status
D1 Plugin name / namespace devwork (recommended) [PREQ] Matches devwork_root, .devwork/. Locking is for the PREQ; the plan only insists it be decided before Phase 2 (renaming re-churns every ref).
D2 Plugin root plugin/ subdir Repo root must not ship (~20 analysis docs, run reports, scripts/). Marketplace source: ./plugin.
D3 _shared location plugin/skills/_shared/now verified tolerated (--strict clean) Mechanical prefix swap for all refs. The plugin/_shared/ fallback is withdrawn: _lib.sh:33/81 resolves adapters as siblings of _shared, so that fallback needs helper code changes, violating §10 (layout-only). Escape hatch if discovery ever changes: explicit "skills" array in plugin.json.
D4 Path canonicalization Loaded path (symlink) is canonical. Helpers keep logical BASH_SOURCE+dirname resolution; ban realpath, readlink -f, cd -P, and pwd -P in the helper tier (lint). Grep confirms zero existing violations — this is a regression fence, not a fix.
D5 Distribution remote for desktop GitHub remote (recommended, per 2026-08-14 note) [PREQ] Ties to repo visibility + clone-credential path on the desktop (P2). Fallbacks: Gitea URL source, or local-path marketplace from a minimal clone. --scope choice (user) documented at install.
D6 Versioning & release SemVer starting 0.1.0; plugin.json is the authoritative version (marketplace entry mirrors it but is ignored by install — verified). scripts/release.sh (repo-local) is the only writer of both version fields, and delegates enforcement to claude plugin tag (clean tree, manifest agreement, tag uniqueness) rather than reimplementing it: bump → lint → validate --stricttag --dry-runtag --push --remote github.
D7 Meta-lane resolution Meta skills stay bare in .claude/skills/. Their shipped-skill invocations use devwork: names. Their helper calls cannot use ${CLAUDE_PLUGIN_ROOT} (no substitution outside plugin scope) — they resolve the helper root once, before any cd, from the repo root they already run in: B="$(pwd)/plugin/skills/_shared/procedures/bin" (meta is repo-local and openly overfitted, so a repo-relative path is legitimate; the current meta-retrospective line-57 pattern cd "$P" && .claude/skills/_shared/... is already broken today and gets fixed by this rule).
D8 Repo-root files .devwork/, run reports, analysis docs, scripts/ stay at repo root, never ship. .claude-plugin/marketplace.json at repo root.
D9 Desktop clone retirement [PREQ — workflow change, not a cleanup.] A pure marketplace consumer can only run released versions, so desktop dogfooding of unreleased suite changes (how #230 ran) ends, and the desktop git remote loses its target. Options: accept (release-before-dogfood), or keep a dormant clone for slot runs. The plan recommends accept + release discipline, but this forecloses a real workflow and belongs to the PREQ.

5. Architecture (target layout)

~/Coding/devwork-skills/                    # repo root = marketplace root + dev workspace
├── .claude-plugin/marketplace.json         # entry: {name: devwork, source: ./plugin, description, version (mirror)}
├── plugin/                                 # plugin root — the ONLY shipped directory
│   ├── .claude-plugin/plugin.json          # {name: devwork, version: 0.1.0, author, description}
│   └── skills/
│       ├── _shared/                        # procedures, schemas, domains, bin helpers (D3)
│       └── <14 shipped skills>/
├── .claude/skills/                         # repo-local project scope (NOT shipped): meta-amend, meta-retrospective
├── scripts/                                # lint-conventions.sh, release.sh, rewrite script (audit), spikes/
├── sdlc-*.md, sdlc-dogfood-run-reports/, sdlc-retrospectives/
└── .devwork/                               # gitignored staging

Consumption: dev machine ~/.claude/skills/devwork -> <repo>/plugin (skills-dir, live);
desktop marketplace install (versioned cache). ${CLAUDE_PLUGIN_ROOT} = symlink path on dev,
cache path on desktop; helpers must behave identically under both (AC-6).

Reference rewrite rules (Phase 2) — four classes plus an exclusion set

Class Rule
1. ~/.claude/skills/<x>/... in shipped text (56) ${CLAUDE_PLUGIN_ROOT}/skills/<x>/...except refs that mean adapter discovery order (setup/SKILL.md:104-106, phase-outcome.md:49 document project-scope-then-user-scope adapter override): hand-rewrite preserving the documented search order, and decide explicitly that the project-scope adapter override survives (it does — _lib.sh:97 searches all locations).
2. Bare _shared/procedures/bin/... in shipped fences (108) ${CLAUDE_PLUGIN_ROOT}/skills/_shared/procedures/bin/...
3. Cross-skill slash/skill names in shipped text /devwork:<name> only for the enumerated 14 (allowlist). Never rewrite: /simplify, /simplify-adjacent user skills, built-ins, retired names, path-lookalikes. Lint rejects any /devwork:X where X ∉ allowlist.
4. Meta-skill helper paths + shipped-skill names in .claude/skills/meta-* Helper root per D7 (repo-relative, resolved pre-cd); shipped-skill names → devwork: form; also amend meta-amend's lane gate (.claude/skills/**/*.mdplugin/skills/**/*.md + meta) and meta-retrospective:13.
Exclusions sdlc-*.md, sdlc-dogfood-run-reports/, sdlc-retrospectives/, .devwork/ — historical records keep pre-migration names verbatim (audit integrity). The rewrite script takes the exclusion set as explicit input and is kept in scripts/ for audit.
Polysemy guard .claude/skills/ sometimes means the consumer project's skills dir (learnings.md:81, learning.v1.md:64,74, git-worktrees/SKILL.md:160,262) — those stay; glab-cli/SKILL.md:184 means the plugin's own file — moves. The rewrite is classifier-assisted + per-file diff review, not blind sed (R2).

Substitution constraint (verified): ${CLAUDE_PLUGIN_ROOT} works only as load-time text
substitution in plugin markdown (fences included) and is NOT a runtime env var. Emitted artifacts
that outlive the fence (temp .sh, cron lines) must bake the substituted path in at emission.
An unsubstituted occurrence expands to empty quietly in shell — hence lint bans unbraced
$CLAUDE_PLUGIN_ROOT and any read of it inside emitted-script heredocs (AC-2).

6. Implementation phases

Phase 0 — probes (all read-only or session-scoped via claude --plugin-dir; before any churn):

  • P0.1 Skill slash-invocability (decisive for Phase 2): the #50 probe verified /ns:cmd for
    a command; this suite is SKILL.md-only. Verify a plugin skill is invocable as
    /devwork:<skill> and how it appears in the picker. If skills are not slash-invocable, the
    fallback is thin commands/*.md wrappers per skill (amends D8's "manifest + skills/ only") —
    a design change the PREQ should see.
  • P0.2 Coexistence: skills-dir devwork plugin loaded alongside the live farm (distinct
    namespaces, distinct scopes — plausible, unverified). Required for pre-cutover dogfooding
    (Phase 4) and the cutover sequence (Phase 5).
  • P0.3 Desktop source: marketplace add + install from the chosen remote (D5) including the
    clone-credential path for a private repo; record the --scope choice.
  • P0.4 Inventory assertion mechanism: find the command that lists a skills-dir plugin's
    components (plugin details? plugin list? eval baseline?) — AC-4's assertion depends on it.
  • P0.5 Hot-reload: does a running session see skills-dir edits (affects /meta-amend
    desk-check wording only).
  • P0.6 Guard-rail behavior: what the model does when a STOP string names a bare skill that
    no longer resolves (develop/SKILL.md:61 "Run /technical-plan first") — R4's "fails loudly"
    is currently asserted only for the human picker path.
  • P0.7 Consumer survey: enumerate CLAUDE.md guidance blocks on both machines (jochems-forge
    reachable from the operator's normal env) → Phase 7's actual worklist.

Phase 1 — restructure + lint retarget (one change, in the worktree): git mv the 14 skills

  • _sharedplugin/skills/; author both manifests (0.1.0, author, description);
    validate --strict green. Same commit: retarget lint-conventions.sh — two roots
    (plugin/skills, .claude/skills), assert non-zero helper-file count (the current
    find … 2>/dev/null would green-light an empty tree), fix the adapter-exemption anchor
    (^\.claude/skills/(tea-cli|…) → plugin-relative). Lint must never be blind while Phases 1–2 edit.

Phase 2 — reference rewrite per §5 rules: enumerate → classify → scripted rewrite with
allowlist + exclusions → per-file diff review of hand-treatment cases (adapter discovery order,
consumer-project paths).

Phase 3 — gates: new lint rules: (i) no ~/.claude/skills/ and no bare _shared/
invocation in plugin/skills/**; (ii) no /devwork:X with X ∉ allowlist; (iii) no
realpath/readlink -f/cd -P/pwd -P in helper tier; (iv) no unbraced $CLAUDE_PLUGIN_ROOT;
(v) claude plugin validate --strict (loud skip if CLI absent). scripts/release.sh per D6.

Phase 4 — pre-cutover dogfood (AC-4, before anything irreversible): load the worktree's
plugin/ via P0.2 coexistence symlink or --plugin-dir, then run: (a) a full
/devwork:requirements phase on a scratch issue (tea-cli adapter dispatch, Phase Outcome
posts); (b) a git-worktrees slot creation (helper resolution with cwd inside a worktree —
D4's hardest case); (c) one /meta-amend cycle including the lint gate (D7 mechanics); (d) one
phase run on a non-tea consumer project (castellan, gh-cli) — the adapter matrix's other leg.

Phase 5 — dev-machine cutover (scripted, ordered; only after Phase 4 green):
Preconditions asserted by the script: clean tree; no git worktrees; no feature in flight on
either machine; #38–#49 meta-amend queue frozen (announced on the tracker). Steps:

  1. Record pre-merge main SHA (rollback anchor).
  2. Create ~/.claude/skills/devwork -> <worktree>/plugin; restart session; the merge itself
    runs under the plugin form
    — no step after the merge may read farm paths.
  3. Merge to main; repoint the symlink to the primary checkout's plugin/; delete the 15 farm
    links; retire the worktree.
  4. Repo move ~/.claude/repos/devwork-skills~/Coding/devwork-skills (if D-move approved):
    migrate ~/.claude/projects/<old-slug><new-slug> and the ~/.claude.json projects
    entry
    (trust acceptance, MCP config); delete + regenerate .devwork/readiness-report.md;
    update the symlink target.
  5. Verify (abort criteria → automatic rollback): plugin inventory lists all 14 skills;
    _shared helpers respond (phase-outcome-read-latest.sh --issue 50 returns); memory loads
    at the new path (AC-8/AC-9).
    Rollback = git reset --hard <pre-merge SHA> + move repo back + re-link farm (the inverse
    script exists and is tested on a scratch copy first).

Phase 6 — desktop cutover (only after Phase 5 green + tag pushed): release via
scripts/release.sh; on desktop: marketplace add (P0.3 mechanics), install, restart, verify
inventory + one helper-invoking phase run (AC-6); then remove farm + clone per D9's resolution;
remove the dead desktop remote from the dev repo. Rollback: reinstall previous version —
noting cache GC (.in_use markers) may have reclaimed it, so keep the previous tag installable
from the remote, not just the local cache.

Phase 7 — docs + records: README rewritten to bootstrap a new machine alone (AC-10):
prerequisites (bash ≥ 3.2, jq, git, adapter CLI), verbatim marketplace add/install commands,
verification step, namespaced command list, update ritual, dev-machine skills-dir form. Consumer
CLAUDE.md guidance blocks (P0.7 worklist) updated same-day as Phase 5. /devwork:setup's
emitted guidance templates updated to namespaced names. Tracker note: records predating the
migration use bare names (read /develop as /devwork:develop); jochems-forge memory note
replaced (stale-checkout warning → version-check ritual). sdlc-retrospectives/ watchlist entry
so /meta-retrospective doesn't read pre-migration run reports as plugin-form evidence.

7. Acceptance criteria (draft — to be formalized by the pipeline)

ID Criterion Verification
AC-1 claude plugin validate --strict passes on plugin/ and the marketplace manifest lint gate (mechanical)
AC-2 Zero ~/.claude/skills/ refs, zero bare _shared/ invocations, zero non-allowlist /devwork:X, zero unbraced $CLAUDE_PLUGIN_ROOT in plugin/skills/** lint grep (mechanical)
AC-3 Lint retarget provably non-blind: helper-file count asserted > 0; adapter-exemption still matches lint self-test
AC-4 Pre-cutover dogfood: full phase run + slot creation + meta-amend cycle + non-tea consumer run, all in plugin form Phase 4 runs (the real gate)
AC-5 All 14 skills + _shared helpers verified present/working at dev cutover; abort criteria defined Phase 5 script assertions
AC-6 Desktop marketplace install runs a helper-invoking phase (BASH_SOURCE under the cache form, not just the symlink form) Phase 6 run
AC-7 Meta skills invocable bare in this repo, absent from the shipped plugin, and their helper calls resolve post-restructure (D7) lint + Phase 4c
AC-8 No helper uses realpath/readlink -f/cd -P/pwd -P lint (regression fence)
AC-9 Repo move preserves project identity: memory readable AND trust/MCP config carried (~/.claude.json entry migrated) Phase 5 assertions
AC-10 A new machine reaches a working suite from the README alone desk-check / Phase 6
AC-11 Release tooling refuses dirty tree and version skew (delegated to claude plugin tag) script test

8. Risks & preconditions

ID Item Handling
P1 skills/_shared tolerance Closed — verified tolerated (panel probe).
P2 Marketplace add/install from chosen remote, incl. private-repo credential path + --scope P0.3 probe on desktop
P3 Skill slash-invocability (/devwork:<skill> for SKILL.md-only plugins) P0.1 — decisive; fallback commands/ wrappers
P4 Farm + skills-dir plugin coexistence P0.2 probe
R1 Working-tree breakage at merge instant Phase 5 ordered sequence: merge runs under plugin form; nothing post-merge reads farm paths
R2 Rewrite corruption (polysemy, non-devwork names, historical docs) §5 classifier + allowlist + exclusions + per-file review + AC-2
R3 Project-identity loss on move Phase 5 step 4 (projects dir + ~/.claude.json); AC-9
R4 Stale bare names during transition Guard-rail behavior probed (P0.6); consumer docs same-day (Phase 7); durable-record translation note; loud-failure claim no longer assumed
R5 Desktop rollback vs cache GC Keep previous tag installable from remote (Phase 6)
R6 Mid-flight work stranded Freeze #38–#49 + verity#237 during Phases 5–6; "no feature in flight" precondition asserted
R7 Desktop dogfooding of unreleased suite ends (D9) [PREQ] conscious workflow decision

9. Out of scope

  • Publishing beyond the user's own remotes; plugin eval suites (future issue); any skill
    logic change (layout + references + tooling only — the D7 meta-helper fix and the setup
    discovery-order hand-rewrite are the two sanctioned text-behavior touches, both restorative);
    automating desktop updates.

10. Open questions carried to the PREQ

D1 (name), D5 (remote), D9 (desktop dogfood workflow), repo-move bundling (§1), and the P0.1
outcome if it forces commands/ wrappers.

11. Expert review (adversarial panel, 2026-08-15)

Reviewers: plugin-mechanics skeptic · portability-baseline enforcer · migration/rollout
engineer · process & dogfooding steward · consumer-experience reviewer (independent, parallel,
read-only verification against the live repo and CLI).

Blocking concerns absorbed: slash-invocability unverified (→ P0.1, decisive); rewrite
corrupts non-devwork/consumer-sense refs (→ §5 classes, allowlist, exclusions); meta-lane helper
paths break, ${CLAUDE_PLUGIN_ROOT} unavailable to project scope (→ D7 redesign, rule 4);
merge-instant farm death + broken rollback (→ Phase 5 sequence, SHA anchor); ~/.claude.json
not migrated (→ Phase 5 step 4, AC-9); validate non-strict vacuous (→ AC-1); version
precedence inverted (→ D6); lint retarget blind/late (→ Phase 1, AC-3); AC-4 previously ordered
after the irreversible step (→ Phase 4 pre-cutover via coexistence/--plugin-dir); product calls
masquerading as locked decisions (→ [PREQ] markers); dogfood-evidence integrity + queue freeze
(→ Phase 7, R6).

Noted, not actioned: thin alias shims for ergonomics (rejected — drift the fence exists to
prevent; picker prefix-match suffices); pwd -P had zero existing violations (fence only);
scratchpad project dirs embedding the old path (ephemeral, cutover script ignores them);
CDPATH-hardening of the 19 cd $(dirname …) helpers (worth a lint idea, not this slice).

# Technical Plan v2: Ship the suite as a Claude Code marketplace plugin (#50) > Status: **pre-pipeline draft, panel-reviewed** — `/requirements` has not run on #50, so this is > not an `sreq:v1`. Drafted at the operator's request 2026-08-15; revised after a five-viewpoint > adversarial panel (see §11). Product-decidable choices are marked **[PREQ]** and left open with > recommendations, not locked. Evidence: #50 probes (2026-08-14) + this session's verifications. ## 1. Context The SDLC suite lives at `~/.claude/repos/devwork-skills`, exposed on the dev machine via a symlink farm (`~/.claude/skills/<x> -> ../repos/devwork-skills/.claude/skills/<x>`, 15 links = 14 shipped skills + `_shared`; `meta-*` are not linked — already project-scope only) and on the desktop (jochems-forge) by a manually-pulled clone with the same farm. Target state: - Repo moves to `~/Coding/devwork-skills` **[PREQ — bundling the move multiplies cutover risk; recommended but severable]**. - Suite ships as a **plugin**: dev machine consumes it live as a skills-dir plugin (`~/.claude/skills/devwork -> <repo>/plugin`), desktop consumes a versioned marketplace install from a git remote. - The namespace is the fence: `devwork:*` = shipped; bare names = repo-local meta lane. ## 2. Verified ground truth From #50's probes (2026-08-14): skills-dir plugin auto-load, symlink support, live edits; namespacing mandatory and identical across both forms; `${CLAUDE_PLUGIN_ROOT}` substituted at load time in plugin skill markdown but **empty in the Bash tool env**; marketplace installs are versioned working-tree snapshots; update is version-gated; `gitCommitSha` records HEAD (dirty-tree hazard); `claude plugin validate/tag/details/init/eval` exist. Verified 2026-08-15 (plan + panel session): - Remotes: `origin` (Gitea forge), `github` (`github.com/jbr870/devwork-skills`), `desktop` (ssh jochems-forge). `~/Coding` is the projects root. Tree clean at `1e6bd78`. - `claude plugin marketplace add <url|path|github-repo> [--scope user|project|local] [--sparse]`. - **`claude plugin validate` exits 0 with warnings; only `--strict` fails** — the gate must be `--strict`, and `--strict` requires `author` in plugin.json and `description` in the marketplace entry. - **`plugin.json` version wins at install; a marketplace-entry `version` is silently ignored.** - **`skills/_shared/` (no SKILL.md) is tolerated by `--strict` validate** — no error, no warning. `plugin.json` also accepts an explicit `"skills": [...]` array as an escape hatch. - Frontmatter with only `description:` (the suite's shape) validates clean — no frontmatter churn. - `claude plugin tag` already enforces clean-tree, tag-uniqueness, and plugin↔marketplace agreement (`--dry-run`, `--push`, `--remote`). - `claude --plugin-dir <path>` loads a plugin session-scoped, without touching `~/.claude/skills`. - `claude plugin details <name>` resolves **installed** plugins; skills-dir resolution is unverified (only `plugin eval` documents it). - Projects-dir slug format confirmed: old `-home-jochem--claude-repos-devwork-skills`, new `-home-jochem-Coding-devwork-skills` (no collision). `~/.claude.json` `projects` map is keyed by absolute path and carries trust-dialog acceptance + MCP server config. - `.git/worktrees` is currently empty. `.devwork/readiness-report.md` hardcodes the old path. - Consumer CLAUDE.md survey (dev machine): only `~/Coding/castellan/CLAUDE.md` carries a devwork guidance block; jochems-forge consumers (verity, axana, outwrit, cunnan) unverified — ssh unreachable from the drafting session. **Reference-rewrite inventory:** 56 `~/.claude/skills/...` refs (22 files); 108 bare `_shared/procedures/bin/...` invocations; a large but **unenumerated** set of cross-skill slash refs (the draft's "~500" overcounted: it includes `/simplify` (separate user-level skill), built-ins (`/init`, `/login`), retired names (`/closeout`, `/decisions`), and path-lookalikes (`/tmp`, `/health`, `/timeline`) that must NOT be rewritten). Enumeration is a Phase 2 input, not a guess. The shipped set is exactly 14: develop, gh-cli, git-worktrees, glab-cli, integrate, local-fs, promote, requirements, resolve, setup, simplify-report, tea-cli, technical-plan, worktree-cleanup. ## 3. Approaches considered **A. In-place dual-form** (plugin root mirrors `.claude/skills/`): rejected — same-skill double-load hazard (verified) + guaranteed drift. **B. Big-bang restructure on the checked-out branch**: rejected — the farm points into this repo's working tree; the branch's `git mv` would kill the live suite mid-development. **C. Restructure in an isolated worktree; staged, scripted cutover (chosen)** — with the panel's correction: worktree isolation alone only *defers* B's failure to the merge instant (the farm dies when `main` fast-forwards, while `/integrate` is still executing from it). The cutover is therefore **not** "at integration" as a side effect — it is an explicit scripted sequence in which the plugin form is loaded and verified *before* the merge, the merge runs under the plugin form, and the farm is dismantled after (§6 Phase 5). ## 4. Decision record | # | Decision | Choice | Rationale / status | |---|---|---|---| | D1 | Plugin name / namespace | **`devwork`** (recommended) **[PREQ]** | Matches `devwork_root`, `.devwork/`. Locking is for the PREQ; the plan only insists it be decided *before* Phase 2 (renaming re-churns every ref). | | D2 | Plugin root | `plugin/` subdir | Repo root must not ship (~20 analysis docs, run reports, `scripts/`). Marketplace `source: ./plugin`. | | D3 | `_shared` location | `plugin/skills/_shared/` — **now verified tolerated** (`--strict` clean) | Mechanical prefix swap for all refs. The `plugin/_shared/` fallback is **withdrawn**: `_lib.sh:33/81` resolves adapters as *siblings* of `_shared`, so that fallback needs helper code changes, violating §10 (layout-only). Escape hatch if discovery ever changes: explicit `"skills"` array in plugin.json. | | D4 | Path canonicalization | Loaded path (symlink) is canonical. Helpers keep logical `BASH_SOURCE`+`dirname` resolution; **ban `realpath`, `readlink -f`, `cd -P`, and `pwd -P`** in the helper tier (lint). Grep confirms zero existing violations — this is a regression fence, not a fix. | | D5 | Distribution remote for desktop | **GitHub remote** (recommended, per 2026-08-14 note) **[PREQ]** | Ties to repo visibility + clone-credential path on the desktop (P2). Fallbacks: Gitea URL source, or local-path marketplace from a minimal clone. `--scope` choice (user) documented at install. | | D6 | Versioning & release | SemVer starting **0.1.0**; **`plugin.json` is the authoritative version** (marketplace entry mirrors it but is ignored by install — verified). `scripts/release.sh` (repo-local) is the **only writer of both version fields**, and delegates enforcement to `claude plugin tag` (clean tree, manifest agreement, tag uniqueness) rather than reimplementing it: bump → lint → `validate --strict` → `tag --dry-run` → `tag --push --remote github`. | | D7 | Meta-lane resolution | Meta skills stay bare in `.claude/skills/`. Their shipped-skill invocations use `devwork:` names. **Their helper calls cannot use `${CLAUDE_PLUGIN_ROOT}` (no substitution outside plugin scope)** — they resolve the helper root **once, before any `cd`**, from the repo root they already run in: `B="$(pwd)/plugin/skills/_shared/procedures/bin"` (meta is repo-local and openly overfitted, so a repo-relative path is legitimate; the current `meta-retrospective` line-57 pattern `cd "$P" && .claude/skills/_shared/...` is *already* broken today and gets fixed by this rule). | | D8 | Repo-root files | `.devwork/`, run reports, analysis docs, `scripts/` stay at repo root, never ship. `.claude-plugin/marketplace.json` at repo root. | | D9 | Desktop clone retirement | **[PREQ — workflow change, not a cleanup.]** A pure marketplace consumer can only run *released* versions, so desktop dogfooding of unreleased suite changes (how #230 ran) ends, and the `desktop` git remote loses its target. Options: accept (release-before-dogfood), or keep a dormant clone for slot runs. The plan recommends accept + release discipline, but this forecloses a real workflow and belongs to the PREQ. | ## 5. Architecture (target layout) ``` ~/Coding/devwork-skills/ # repo root = marketplace root + dev workspace ├── .claude-plugin/marketplace.json # entry: {name: devwork, source: ./plugin, description, version (mirror)} ├── plugin/ # plugin root — the ONLY shipped directory │ ├── .claude-plugin/plugin.json # {name: devwork, version: 0.1.0, author, description} │ └── skills/ │ ├── _shared/ # procedures, schemas, domains, bin helpers (D3) │ └── <14 shipped skills>/ ├── .claude/skills/ # repo-local project scope (NOT shipped): meta-amend, meta-retrospective ├── scripts/ # lint-conventions.sh, release.sh, rewrite script (audit), spikes/ ├── sdlc-*.md, sdlc-dogfood-run-reports/, sdlc-retrospectives/ └── .devwork/ # gitignored staging ``` Consumption: dev machine `~/.claude/skills/devwork -> <repo>/plugin` (skills-dir, live); desktop marketplace install (versioned cache). `${CLAUDE_PLUGIN_ROOT}` = symlink path on dev, cache path on desktop; helpers must behave identically under both (AC-6). ### Reference rewrite rules (Phase 2) — four classes plus an exclusion set | Class | Rule | |---|---| | 1. `~/.claude/skills/<x>/...` in shipped text (56) | → `${CLAUDE_PLUGIN_ROOT}/skills/<x>/...` — **except** refs that mean adapter *discovery order* (`setup/SKILL.md:104-106`, `phase-outcome.md:49` document project-scope-then-user-scope adapter override): hand-rewrite preserving the documented search order, and decide explicitly that the project-scope adapter override survives (it does — `_lib.sh:97` searches all locations). | | 2. Bare `_shared/procedures/bin/...` in shipped fences (108) | → `${CLAUDE_PLUGIN_ROOT}/skills/_shared/procedures/bin/...` | | 3. Cross-skill slash/skill names in shipped text | → `/devwork:<name>` **only for the enumerated 14** (allowlist). Never rewrite: `/simplify`, `/simplify`-adjacent user skills, built-ins, retired names, path-lookalikes. Lint rejects any `/devwork:X` where X ∉ allowlist. | | 4. Meta-skill helper paths + shipped-skill names in `.claude/skills/meta-*` | Helper root per D7 (repo-relative, resolved pre-`cd`); shipped-skill names → `devwork:` form; also amend `meta-amend`'s lane gate (`.claude/skills/**/*.md` → `plugin/skills/**/*.md` + meta) and `meta-retrospective:13`. | | **Exclusions** | `sdlc-*.md`, `sdlc-dogfood-run-reports/`, `sdlc-retrospectives/`, `.devwork/` — historical records keep pre-migration names verbatim (audit integrity). The rewrite script takes the exclusion set as explicit input and is kept in `scripts/` for audit. | | **Polysemy guard** | `.claude/skills/` sometimes means *the consumer project's* skills dir (`learnings.md:81`, `learning.v1.md:64,74`, `git-worktrees/SKILL.md:160,262`) — those stay; `glab-cli/SKILL.md:184` means the plugin's own file — moves. The rewrite is classifier-assisted + per-file diff review, not blind sed (R2). | **Substitution constraint (verified):** `${CLAUDE_PLUGIN_ROOT}` works only as load-time text substitution in plugin markdown (fences included) and is NOT a runtime env var. Emitted artifacts that outlive the fence (temp `.sh`, cron lines) must bake the substituted path in at emission. An unsubstituted occurrence expands to empty **quietly** in shell — hence lint bans unbraced `$CLAUDE_PLUGIN_ROOT` and any read of it inside emitted-script heredocs (AC-2). ## 6. Implementation phases **Phase 0 — probes (all read-only or session-scoped via `claude --plugin-dir`; before any churn):** - **P0.1 Skill slash-invocability (decisive for Phase 2):** the #50 probe verified `/ns:cmd` for a *command*; this suite is SKILL.md-only. Verify a plugin **skill** is invocable as `/devwork:<skill>` and how it appears in the picker. If skills are not slash-invocable, the fallback is thin `commands/*.md` wrappers per skill (amends D8's "manifest + skills/ only") — a design change the PREQ should see. - **P0.2 Coexistence:** skills-dir `devwork` plugin loaded alongside the live farm (distinct namespaces, distinct scopes — plausible, unverified). Required for pre-cutover dogfooding (Phase 4) and the cutover sequence (Phase 5). - **P0.3 Desktop source:** marketplace add + install from the chosen remote (D5) including the clone-credential path for a private repo; record the `--scope` choice. - **P0.4 Inventory assertion mechanism:** find the command that lists a **skills-dir** plugin's components (`plugin details`? `plugin list`? eval baseline?) — AC-4's assertion depends on it. - **P0.5 Hot-reload:** does a *running* session see skills-dir edits (affects `/meta-amend` desk-check wording only). - **P0.6 Guard-rail behavior:** what the model does when a STOP string names a bare skill that no longer resolves (`develop/SKILL.md:61` "Run `/technical-plan` first") — R4's "fails loudly" is currently asserted only for the human picker path. - **P0.7 Consumer survey:** enumerate CLAUDE.md guidance blocks on both machines (jochems-forge reachable from the operator's normal env) → Phase 7's actual worklist. **Phase 1 — restructure + lint retarget (one change, in the worktree):** `git mv` the 14 skills + `_shared` → `plugin/skills/`; author both manifests (0.1.0, `author`, `description`); `validate --strict` green. **Same commit:** retarget `lint-conventions.sh` — two roots (`plugin/skills`, `.claude/skills`), **assert non-zero helper-file count** (the current `find … 2>/dev/null` would green-light an empty tree), fix the adapter-exemption anchor (`^\.claude/skills/(tea-cli|…)` → plugin-relative). Lint must never be blind while Phases 1–2 edit. **Phase 2 — reference rewrite** per §5 rules: enumerate → classify → scripted rewrite with allowlist + exclusions → per-file diff review of hand-treatment cases (adapter discovery order, consumer-project paths). **Phase 3 — gates:** new lint rules: (i) no `~/.claude/skills/` and no bare `_shared/` invocation in `plugin/skills/**`; (ii) no `/devwork:X` with X ∉ allowlist; (iii) no `realpath`/`readlink -f`/`cd -P`/`pwd -P` in helper tier; (iv) no unbraced `$CLAUDE_PLUGIN_ROOT`; (v) `claude plugin validate --strict` (loud skip if CLI absent). `scripts/release.sh` per D6. **Phase 4 — pre-cutover dogfood (AC-4, before anything irreversible):** load the worktree's `plugin/` via P0.2 coexistence symlink or `--plugin-dir`, then run: (a) a full `/devwork:requirements` phase on a scratch issue (tea-cli adapter dispatch, Phase Outcome posts); (b) a `git-worktrees` slot creation (helper resolution with cwd inside a worktree — D4's hardest case); (c) one `/meta-amend` cycle including the lint gate (D7 mechanics); (d) one phase run on a non-tea consumer project (castellan, `gh-cli`) — the adapter matrix's other leg. **Phase 5 — dev-machine cutover (scripted, ordered; only after Phase 4 green):** Preconditions asserted by the script: clean tree; **no git worktrees**; no feature in flight on either machine; #38–#49 meta-amend queue frozen (announced on the tracker). Steps: 1. Record pre-merge `main` SHA (rollback anchor). 2. Create `~/.claude/skills/devwork -> <worktree>/plugin`; restart session; **the merge itself runs under the plugin form** — no step after the merge may read farm paths. 3. Merge to `main`; repoint the symlink to the primary checkout's `plugin/`; delete the 15 farm links; retire the worktree. 4. Repo move `~/.claude/repos/devwork-skills` → `~/Coding/devwork-skills` (if D-move approved): migrate `~/.claude/projects/<old-slug>` → `<new-slug>` **and the `~/.claude.json` `projects` entry** (trust acceptance, MCP config); delete + regenerate `.devwork/readiness-report.md`; update the symlink target. 5. Verify (abort criteria → automatic rollback): plugin inventory lists all 14 skills; `_shared` helpers respond (`phase-outcome-read-latest.sh --issue 50` returns); memory loads at the new path (AC-8/AC-9). Rollback = `git reset --hard <pre-merge SHA>` + move repo back + re-link farm (the inverse script exists and is tested on a scratch copy first). **Phase 6 — desktop cutover (only after Phase 5 green + tag pushed):** release via `scripts/release.sh`; on desktop: marketplace add (P0.3 mechanics), install, restart, verify inventory + one helper-invoking phase run (AC-6); then remove farm + clone per D9's resolution; remove the dead `desktop` remote from the dev repo. Rollback: reinstall previous version — noting cache GC (`.in_use` markers) may have reclaimed it, so keep the previous tag installable from the remote, not just the local cache. **Phase 7 — docs + records:** README rewritten to bootstrap a new machine alone (AC-10): prerequisites (bash ≥ 3.2, jq, git, adapter CLI), verbatim marketplace add/install commands, verification step, namespaced command list, update ritual, dev-machine skills-dir form. Consumer CLAUDE.md guidance blocks (P0.7 worklist) updated same-day as Phase 5. `/devwork:setup`'s *emitted* guidance templates updated to namespaced names. Tracker note: records predating the migration use bare names (read `/develop` as `/devwork:develop`); `jochems-forge` memory note replaced (stale-checkout warning → version-check ritual). `sdlc-retrospectives/` watchlist entry so `/meta-retrospective` doesn't read pre-migration run reports as plugin-form evidence. ## 7. Acceptance criteria (draft — to be formalized by the pipeline) | ID | Criterion | Verification | |---|---|---| | AC-1 | `claude plugin validate --strict` passes on `plugin/` and the marketplace manifest | lint gate (mechanical) | | AC-2 | Zero `~/.claude/skills/` refs, zero bare `_shared/` invocations, zero non-allowlist `/devwork:X`, zero unbraced `$CLAUDE_PLUGIN_ROOT` in `plugin/skills/**` | lint grep (mechanical) | | AC-3 | Lint retarget provably non-blind: helper-file count asserted > 0; adapter-exemption still matches | lint self-test | | AC-4 | Pre-cutover dogfood: full phase run + slot creation + meta-amend cycle + non-tea consumer run, all in plugin form | Phase 4 runs (the real gate) | | AC-5 | All 14 skills + `_shared` helpers verified present/working at dev cutover; abort criteria defined | Phase 5 script assertions | | AC-6 | Desktop marketplace install runs a helper-invoking phase (BASH_SOURCE under the **cache** form, not just the symlink form) | Phase 6 run | | AC-7 | Meta skills invocable bare in this repo, absent from the shipped plugin, and their helper calls resolve post-restructure (D7) | lint + Phase 4c | | AC-8 | No helper uses `realpath`/`readlink -f`/`cd -P`/`pwd -P` | lint (regression fence) | | AC-9 | Repo move preserves project identity: memory readable AND trust/MCP config carried (`~/.claude.json` entry migrated) | Phase 5 assertions | | AC-10 | A new machine reaches a working suite from the README alone | desk-check / Phase 6 | | AC-11 | Release tooling refuses dirty tree and version skew (delegated to `claude plugin tag`) | script test | ## 8. Risks & preconditions | ID | Item | Handling | |---|---|---| | P1 | ~~`skills/_shared` tolerance~~ | **Closed — verified tolerated** (panel probe). | | P2 | Marketplace add/install from chosen remote, incl. private-repo credential path + `--scope` | P0.3 probe on desktop | | P3 | Skill slash-invocability (`/devwork:<skill>` for SKILL.md-only plugins) | **P0.1 — decisive**; fallback commands/ wrappers | | P4 | Farm + skills-dir plugin coexistence | P0.2 probe | | R1 | Working-tree breakage at merge instant | Phase 5 ordered sequence: merge runs under plugin form; nothing post-merge reads farm paths | | R2 | Rewrite corruption (polysemy, non-devwork names, historical docs) | §5 classifier + allowlist + exclusions + per-file review + AC-2 | | R3 | Project-identity loss on move | Phase 5 step 4 (projects dir + `~/.claude.json`); AC-9 | | R4 | Stale bare names during transition | Guard-rail behavior probed (P0.6); consumer docs same-day (Phase 7); durable-record translation note; loud-failure claim no longer assumed | | R5 | Desktop rollback vs cache GC | Keep previous tag installable from remote (Phase 6) | | R6 | Mid-flight work stranded | Freeze #38–#49 + verity#237 during Phases 5–6; "no feature in flight" precondition asserted | | R7 | Desktop dogfooding of unreleased suite ends (D9) | **[PREQ]** conscious workflow decision | ## 9. Out of scope - Publishing beyond the user's own remotes; `plugin eval` suites (future issue); any skill *logic* change (layout + references + tooling only — the D7 meta-helper fix and the setup discovery-order hand-rewrite are the two sanctioned text-behavior touches, both restorative); automating desktop updates. ## 10. Open questions carried to the PREQ D1 (name), D5 (remote), D9 (desktop dogfood workflow), repo-move bundling (§1), and the P0.1 outcome if it forces commands/ wrappers. ## 11. Expert review (adversarial panel, 2026-08-15) **Reviewers:** plugin-mechanics skeptic · portability-baseline enforcer · migration/rollout engineer · process & dogfooding steward · consumer-experience reviewer (independent, parallel, read-only verification against the live repo and CLI). **Blocking concerns absorbed:** slash-invocability unverified (→ P0.1, decisive); rewrite corrupts non-devwork/consumer-sense refs (→ §5 classes, allowlist, exclusions); meta-lane helper paths break, `${CLAUDE_PLUGIN_ROOT}` unavailable to project scope (→ D7 redesign, rule 4); merge-instant farm death + broken rollback (→ Phase 5 sequence, SHA anchor); `~/.claude.json` not migrated (→ Phase 5 step 4, AC-9); `validate` non-strict vacuous (→ AC-1); version precedence inverted (→ D6); lint retarget blind/late (→ Phase 1, AC-3); AC-4 previously ordered after the irreversible step (→ Phase 4 pre-cutover via coexistence/--plugin-dir); product calls masquerading as locked decisions (→ [PREQ] markers); dogfood-evidence integrity + queue freeze (→ Phase 7, R6). **Noted, not actioned:** thin alias shims for ergonomics (rejected — drift the fence exists to prevent; picker prefix-match suffices); `pwd -P` had zero existing violations (fence only); scratchpad project dirs embedding the old path (ephemeral, cutover script ignores them); CDPATH-hardening of the 19 `cd $(dirname …)` helpers (worth a lint idea, not this slice).
Author
Owner

Archived pre-PREQ issue body (2026-08-14 evidence record, preserved verbatim before /requirements replaced the body with the PREQ on 2026-08-15):


Ship the suite as a Claude Code marketplace plugin

Motivation

The suite currently lives at ~/.claude/repos/devwork-skills on two machines, exposed via a
symlink farm ~/.claude/skills/* -> ../repos/devwork-skills/.claude/skills/*, kept in sync by
hand (manual git pull on the desktop). Decision (2026-08-14 session):

  • This repo becomes the single development directory, moved out of ~/.claude/repos/ to the
    normal coding-projects root (source of a product does not belong inside the runtime's config dir).
  • The desktop becomes a pure consumer: marketplace add from the GitHub remote
    (jbr870/devwork-skills), versioned install, updates via version bump.
  • The dev machine also consumes the plugin form — every project on this box, including this
    repo (dogfooding), invokes the plugin, so packaging regressions surface where development
    happens, not on the consumer box. The current symlink farm hides exactly the failure classes
    the plugin form introduces (path resolution, namespacing, fencing).
  • Rationale for dev-machine live form: run reports feeding /meta-retrospective must capture
    friction in the form that ships, not in a form nobody ships.

Experimental findings (verified 2026-08-14, this machine, all probes cleaned up)

1. Local-path marketplace: works, but installs are versioned snapshots

  • claude plugin marketplace add <path> registers source type directory; metadata read live,
    no checkout.
  • claude plugin install copies to ~/.claude/plugins/cache/<mp>/<plugin>/<version>/ — a
    real directory, no .git, no link.
  • The copy takes the working tree, not HEAD — an uncommitted edit shipped into the cache.
  • …but the install record's gitCommitSha records HEAD. Installing from a dirty tree writes
    provenance that disagrees with the shipped bytes. Release discipline: only install/tag from a
    clean tree.
  • claude plugin update is version-gated, not content-gated: content edits without a version
    bump are never picked up (already at the latest version), even after
    claude plugin marketplace update. A bump in both manifests (marketplace.json entry +
    plugin.json) triggers re-copy into a new <version>/ dir, old version left beside it, plus
    "Restart to apply."
  • Consequence: a refresh-per-edit inner loop over a local marketplace would require a version
    bump per skill-text edit — untenable for /meta-amend. Not needed, because:

2. skills-dir plugins: live, symlinkable, no install step

  • A dir at ~/.claude/skills/<name>/ containing .claude-plugin/plugin.json auto-loads as
    <name>@skills-dir: no cache copy, no installed_plugins.json entry, status "loaded".
  • Works as a symlink pointing outside ~/.claude (probe pointed into /tmp scratchpad).
  • Edits are live — an entire new skill added to the source dir appeared in
    claude plugin details inventory with no reinstall/bump. (Not verified: whether an
    already-running session picks up edits, vs only new sessions.)
  • So: dev machine = ln -s <repo>/<plugin-root> ~/.claude/skills/devwork, /meta-amend needs no
    refresh step at all
    . The release to the desktop is the only versioned act
    (claude plugin tag exists: creates {name}--v{version}, validates plugin.json against the
    marketplace entry, --push).

3. Namespacing: mandatory, identical across both sources

Probe Result
skills-dir skill listed as wfns:nsprobe
marketplace-installed skill listed as wfmp:mpprobe
bare Skill call nsprobe Unknown skill: nsprobe
/wfns:nscmd (command) ran
bare /nscmd Unknown command: /nscmd
  • Parity between skills-dir and marketplace install confirmed → dev machine reproduces desktop
    naming exactly.
  • Every cross-reference /develop, /technical-plan, /qa-* in skill text and consumer
    CLAUDE.md guidance becomes /devwork:develop etc. Largest single chunk of migration churn.
  • Upside: the namespace is the fencedevwork:* = shipped; bare = repo-local
    (/meta-amend, /meta-retrospective stay in the repo's project-scope .claude/skills/,
    structurally unreachable from consumers).
  • The plugin name IS the namespace prefix — choose deliberately up front; renaming re-churns
    every reference.

4. Path resolution: ${CLAUDE_PLUGIN_ROOT} is load-time text substitution only

  • ${CLAUDE_PLUGIN_ROOT} in skill/command markdown is substituted at load time (probe
    returned the real path). This is the fix for the 56 hardcoded ~/.claude/skills/... references.
  • $CLAUDE_PLUGIN_ROOT in emitted shell is empty — not exported to the Bash tool env. The
    substitution must live in the markdown text; runtime shell cannot read it.
  • Bare-relative helper invocations ($(_shared/procedures/bin/foo.sh ...)) currently resolve
    against cwd by luck of the symlink farm — must gain the ${CLAUDE_PLUGIN_ROOT}/ prefix.
  • Helpers keep resolving siblings via BASH_SOURCE (already sanctioned bash surface).
  • Substituted value is the symlink path (~/.claude/skills/wfns), not the realpath — a
    helper doing cd -P/realpath lands in the repo instead. Decide which is canonical before
    anything depends on it.

5. Tooling gains

  • claude plugin validate <path> — manifest gate for lint-conventions.sh.
  • claude plugin details <name> — per-component token cost (always-on vs on-invoke); directly
    useful for the context-economy work (sdlc-context-economy-analysis.md).
  • claude plugin init scaffolds a skills-dir plugin; claude plugin eval runs evals against a
    plugin (skills-dir plugins resolve too).

Scope of the migration (for the PREQ)

  1. Move repo out of ~/.claude/repos/ to the coding-projects root.
  2. Restructure to a plugin root (.claude-plugin/plugin.json + skills/), shipped skills leave
    .claude/skills/ entirely (double-load hazard if any skill exists in both project scope and
    plugin scope); meta-* + scripts/ stay repo-local project scope.
  3. Rewrite ~/.claude/skills/... (56 refs) and bare _shared/... invocations to
    ${CLAUDE_PLUGIN_ROOT}/....
  4. Namespace all cross-skill references (/devwork:develop form); pick the plugin name first.
  5. Marketplace manifest in-repo; desktop consumes via GitHub source; version-bump + plugin tag
    release discipline (clean tree only, cf. gitCommitSha finding).
  6. lint-conventions.sh: add checks — no ~/.claude/skills/ or bare _shared/ in shipped skill
    text; claude plugin validate green; (existing checks unchanged).
  7. Dev-machine setup: symlink ~/.claude/skills/devwork → repo plugin root; remove old symlink
    farm. Desktop: remove farm + repo checkout, marketplace install.
  8. Update consumer-facing docs (README, project CLAUDE.md guidance blocks) to namespaced
    invocations.

Open questions for /requirements:

  • Plugin name (= namespace prefix).
  • Symlink-path vs realpath canonicalization for helpers.
  • Whether running sessions hot-reload skills-dir edits (affects /meta-amend desk-check wording
    only; new-session load is confirmed).
  • Whether .devwork/, run reports, analysis docs stay at repo root (presumably yes — plugin root
    is a subdirectory; marketplace source: ./<subdir> supports this, cf. official marketplace
    layout).

Lane

New capability + contract/layout change → full pipeline (/requirements → /technical-plan →
/develop → QA → /integrate), per the two-lane rule. Filed from the 2026-08-14 experiment session;
this issue is the evidence record.

**Archived pre-PREQ issue body** (2026-08-14 evidence record, preserved verbatim before /requirements replaced the body with the PREQ on 2026-08-15): --- # Ship the suite as a Claude Code marketplace plugin ## Motivation The suite currently lives at `~/.claude/repos/devwork-skills` on two machines, exposed via a symlink farm `~/.claude/skills/* -> ../repos/devwork-skills/.claude/skills/*`, kept in sync by hand (manual `git pull` on the desktop). Decision (2026-08-14 session): - **This repo becomes the single development directory**, moved out of `~/.claude/repos/` to the normal coding-projects root (source of a product does not belong inside the runtime's config dir). - **The desktop becomes a pure consumer**: marketplace add from the GitHub remote (`jbr870/devwork-skills`), versioned install, updates via version bump. - **The dev machine also consumes the plugin form** — every project on this box, including this repo (dogfooding), invokes the plugin, so packaging regressions surface where development happens, not on the consumer box. The current symlink farm hides exactly the failure classes the plugin form introduces (path resolution, namespacing, fencing). - **Rationale for dev-machine live form**: run reports feeding /meta-retrospective must capture friction in the form that ships, not in a form nobody ships. ## Experimental findings (verified 2026-08-14, this machine, all probes cleaned up) ### 1. Local-path marketplace: works, but installs are versioned snapshots - `claude plugin marketplace add <path>` registers source type `directory`; metadata read live, no checkout. - `claude plugin install` **copies** to `~/.claude/plugins/cache/<mp>/<plugin>/<version>/` — a real directory, no `.git`, no link. - The copy takes the **working tree, not HEAD** — an uncommitted edit shipped into the cache. - …but the install record's `gitCommitSha` records **HEAD**. Installing from a dirty tree writes provenance that disagrees with the shipped bytes. Release discipline: only install/tag from a clean tree. - `claude plugin update` is **version-gated, not content-gated**: content edits without a version bump are never picked up (`already at the latest version`), even after `claude plugin marketplace update`. A bump in **both** manifests (marketplace.json entry + plugin.json) triggers re-copy into a new `<version>/` dir, old version left beside it, plus "Restart to apply." - Consequence: a refresh-per-edit inner loop over a local marketplace would require a version bump per skill-text edit — untenable for /meta-amend. Not needed, because: ### 2. skills-dir plugins: live, symlinkable, no install step - A dir at `~/.claude/skills/<name>/` containing `.claude-plugin/plugin.json` auto-loads as `<name>@skills-dir`: **no cache copy, no installed_plugins.json entry**, status "loaded". - **Works as a symlink pointing outside `~/.claude`** (probe pointed into /tmp scratchpad). - **Edits are live** — an entire new skill added to the source dir appeared in `claude plugin details` inventory with no reinstall/bump. (Not verified: whether an already-running session picks up edits, vs only new sessions.) - So: dev machine = `ln -s <repo>/<plugin-root> ~/.claude/skills/devwork`, /meta-amend needs **no refresh step at all**. The release to the desktop is the only versioned act (`claude plugin tag` exists: creates `{name}--v{version}`, validates plugin.json against the marketplace entry, `--push`). ### 3. Namespacing: mandatory, identical across both sources | Probe | Result | |---|---| | skills-dir skill | listed as `wfns:nsprobe` | | marketplace-installed skill | listed as `wfmp:mpprobe` | | bare Skill call `nsprobe` | `Unknown skill: nsprobe` | | `/wfns:nscmd` (command) | ran | | bare `/nscmd` | `Unknown command: /nscmd` | - Parity between skills-dir and marketplace install confirmed → dev machine reproduces desktop naming exactly. - Every cross-reference `/develop`, `/technical-plan`, `/qa-*` in skill text and consumer CLAUDE.md guidance becomes `/devwork:develop` etc. Largest single chunk of migration churn. - Upside: **the namespace is the fence** — `devwork:*` = shipped; bare = repo-local (/meta-amend, /meta-retrospective stay in the repo's project-scope `.claude/skills/`, structurally unreachable from consumers). - The plugin name IS the namespace prefix — choose deliberately up front; renaming re-churns every reference. ### 4. Path resolution: `${CLAUDE_PLUGIN_ROOT}` is load-time text substitution only - `${CLAUDE_PLUGIN_ROOT}` **in skill/command markdown** is substituted at load time (probe returned the real path). This is the fix for the 56 hardcoded `~/.claude/skills/...` references. - `$CLAUDE_PLUGIN_ROOT` **in emitted shell is empty** — not exported to the Bash tool env. The substitution must live in the markdown text; runtime shell cannot read it. - Bare-relative helper invocations (`$(_shared/procedures/bin/foo.sh ...)`) currently resolve against cwd by luck of the symlink farm — must gain the `${CLAUDE_PLUGIN_ROOT}/` prefix. - Helpers keep resolving siblings via `BASH_SOURCE` (already sanctioned bash surface). - Substituted value is the **symlink path** (`~/.claude/skills/wfns`), not the realpath — a helper doing `cd -P`/`realpath` lands in the repo instead. Decide which is canonical before anything depends on it. ### 5. Tooling gains - `claude plugin validate <path>` — manifest gate for lint-conventions.sh. - `claude plugin details <name>` — per-component token cost (always-on vs on-invoke); directly useful for the context-economy work (sdlc-context-economy-analysis.md). - `claude plugin init` scaffolds a skills-dir plugin; `claude plugin eval` runs evals against a plugin (skills-dir plugins resolve too). ## Scope of the migration (for the PREQ) 1. Move repo out of `~/.claude/repos/` to the coding-projects root. 2. Restructure to a plugin root (`.claude-plugin/plugin.json` + `skills/`), shipped skills leave `.claude/skills/` entirely (double-load hazard if any skill exists in both project scope and plugin scope); meta-* + `scripts/` stay repo-local project scope. 3. Rewrite `~/.claude/skills/...` (56 refs) and bare `_shared/...` invocations to `${CLAUDE_PLUGIN_ROOT}/...`. 4. Namespace all cross-skill references (`/devwork:develop` form); pick the plugin name first. 5. Marketplace manifest in-repo; desktop consumes via GitHub source; version-bump + `plugin tag` release discipline (clean tree only, cf. gitCommitSha finding). 6. lint-conventions.sh: add checks — no `~/.claude/skills/` or bare `_shared/` in shipped skill text; `claude plugin validate` green; (existing checks unchanged). 7. Dev-machine setup: symlink `~/.claude/skills/devwork` → repo plugin root; remove old symlink farm. Desktop: remove farm + repo checkout, marketplace install. 8. Update consumer-facing docs (README, project CLAUDE.md guidance blocks) to namespaced invocations. Open questions for /requirements: - Plugin name (= namespace prefix). - Symlink-path vs realpath canonicalization for helpers. - Whether running sessions hot-reload skills-dir edits (affects /meta-amend desk-check wording only; new-session load is confirmed). - Whether `.devwork/`, run reports, analysis docs stay at repo root (presumably yes — plugin root is a subdirectory; marketplace `source: ./<subdir>` supports this, cf. official marketplace layout). ## Lane New capability + contract/layout change → **full pipeline** (/requirements → /technical-plan → /develop → QA → /integrate), per the two-lane rule. Filed from the 2026-08-14 experiment session; this issue is the evidence record.
Author
Owner

Test Plan: ship-the-suite-as-a-claude-code-marketplace-plugin

Prerequisites

  • Two machines, in the roles the PREQ names:
    • the dev machine — where the suite is developed, released, and dogfooded; currently carries the symlink farm at ~/.claude/skills/ and the repo at ~/.claude/repos/devwork-skills;
    • the desktop (jochems-forge) — currently carries a second clone plus its own farm; becomes a pure consumer.
  • On each machine, the prerequisites the README is required to enumerate are present and checkable: bash ≥ 3.2, jq, git, and the project's forge CLI (tea for this repo, gh for castellan).
  • The GitHub remote github.com/jbr870/devwork-skills exists and is reachable from the desktop, with whatever credential path that remote requires available to the desktop operator.
  • The P0.1 probe outcome is known and affirmative — plugin skills are slash-invocable. If the probe says otherwise, AC-2 (and every scenario depending on /dev: invocation) is void and the run halts on a Pending Decision instead.
  • The work queue is frozen for the cutover scenarios: the meta-amend queue (#38–#49) and verity#237 are not in flight, and no feature is mid-pipeline on either machine.
  • A scratch copy of the repo and of the machine's pre-cutover state, usable for rehearsal and for negative controls without touching the real machines.
  • Consumer projects reachable on the relevant machine: castellan (gh-cli forge), and this repo itself (tea forge). The remaining consumers (verity, axana, outwrit, cunnan) need only their CLAUDE.md files present for inspection.
  • Both pre-cutover state (farm links, repo location, project-keyed runtime config) and a way to observe it are captured before any cutover scenario runs, so restoration can be judged.

Required Test Data

  • A scratch issue on this repo's tracker (tea forge), used as the target of the /dev:requirements phase in the coexistence run.
  • An issue on castellan's tracker (gh forge) able to receive one pipeline phase's records.
  • A tracker issue suitable for one meta-amend cycle — a finding that still holds at HEAD and needs only a text amendment.
  • Two consecutive releases of the plugin: the first at version 0.1.0, and a later one at a higher version, so the update path can be exercised.
  • Three separately prepared release-refusal states: (a) a working tree with uncommitted changes, (b) a tree where the plugin manifest's version field and the marketplace entry's version field disagree, (c) a tree whose version is already claimed by an existing tag.
  • A deliberately defective manifest on a scratch copy (one field wrong or missing) for the validator's negative control.
  • A scratch copy seeded with one instance of each violation class the mechanical scan is meant to catch: a ~/.claude/skills/ path reference, a _shared/ reference not prefixed by the plugin-root variable (one in a shell fence, one in prose), and a /dev:<x> reference naming a skill outside the 14.
  • The list of the 14 shipped skills, used verbatim as the checklist: develop, gh-cli, git-worktrees, glab-cli, integrate, local-fs, promote, requirements, resolve, setup, simplify-report, tea-cli, technical-plan, worktree-cleanup.
  • The rewritten README, and an operator (or a session) with no prior knowledge of the cutover steps beyond what the README says.

Test Scenarios

Scenario 1: Manifests pass strict validation with a clean bill

Acceptance criterion: AC-1 — "validation passes with zero errors and zero warnings"

  1. Start from the restructured repo at a committed, clean state.
  2. Run the plugin tooling's validation of the plugin manifest and the marketplace manifest in its strict mode.
  3. Verify: the run reports zero errors.
  4. Verify: the run reports zero warnings — not "only warnings", not warnings that are waved off.
  5. Verify: the run exits successfully.

Expected outcome: Strict validation of both manifests is completely clean; nothing is deferred or explained away.

Scenario 2: The shipped plugin's contents scan clean, and the scan is proven non-vacuous

Acceptance criterion: AC-3 — "zero references to ~/.claude/skills/ paths, zero bare _shared/ invocations … and the mechanical scan itself is verified non-vacuous"

  1. Take the shipped plugin's contents as they would be delivered.
  2. Run the mechanical scan over them.
  3. Verify: the scan reports how many files it examined, and that number is non-zero and plausibly matches the plugin's real file population (spot-check by counting files under the plugin root by hand and comparing).
  4. Verify: zero hits for ~/.claude/skills/ paths.
  5. Verify: zero _shared/ references that are not prefixed by the plugin-root variable — counting references in markdown prose, not only those inside shell fences.
  6. Verify: zero /dev:<x> references where <x> is outside the 14 shipped skill names.

Expected outcome: The scan looks at a real, non-empty population of files and finds none of the three violation classes.

Scenario 3: The meta lane runs by bare name in the repo and is absent from the shipped plugin

Acceptance criterion: AC-4 — "when the operator invokes /meta-amend or /meta-retrospective by bare name, then they run; and … no meta skill or repo-local script is present in it"

  1. Start a session in this repo.
  2. Invoke /meta-amend by its bare name.
  3. Verify: the invocation resolves and the skill begins its work (it identifies the issue/finding it is operating on, rather than reporting an unknown command).
  4. Invoke /meta-retrospective by its bare name.
  5. Verify: the invocation resolves and the skill begins its review.
  6. Inspect the shipped plugin's contents.
  7. Verify: neither meta skill is present anywhere in it.
  8. Verify: no repo-local script directory, analysis docs, run reports, or .devwork/ material is present in it.

Expected outcome: The meta lane is fully usable inside the repo by bare name, and entirely absent from what ships.

Scenario 4: A full requirements phase runs on a scratch tea issue with the plugin loaded pre-cutover

Acceptance criterion: AC-5 — "a full /dev:requirements phase runs on a scratch issue of this repo (tea forge) … with their forge records posted correctly"

  1. With the plugin form loaded alongside the still-present farm (bare names may still resolve in this window), open a session in this repo.
  2. Invoke /dev:requirements and run the phase through to completion against the scratch issue.
  3. Verify: the phase completes without the operator having to fall back to a bare-name invocation.
  4. Open the scratch issue on the tracker.
  5. Verify: a Phase Outcome is present on that issue (not another), and reads as a well-formed Phase Outcome — its fields are legible and its next-state is stated.
  6. Verify: the issue's phase label has advanced to the value the completed phase implies.

Expected outcome: The requirements phase runs from the plugin form and leaves correct, parseable forge records on the right issue.

Scenario 5: A worktree slot is created with the plugin loaded pre-cutover

Acceptance criterion: AC-5 — "(b) a worktree slot is created"

  1. In the same coexistence window, invoke the shipped worktree skill under its /dev: name.
  2. Create a worktree slot.
  3. Verify: the slot is created and the operator can enter it and see the repo's files there.
  4. Verify: any record the slot creation is meant to post is present and legible where the phase records live.
  5. Verify: no step of slot creation failed on a path that pointed into the old farm location.

Expected outcome: Slot creation succeeds from the plugin form, with its records intact.

Scenario 6: One meta-amend cycle runs with the plugin loaded pre-cutover

Acceptance criterion: AC-5 — "(c) one meta-amend cycle runs"

  1. In the same coexistence window, invoke /meta-amend (bare name — it is repo-local) against a prepared finding issue.
  2. Let the cycle run: verification of the finding, the text amendment, the mechanical gate, the desk-check, the commit.
  3. Verify: the cycle completes without an unresolvable path error — in particular, the helpers it reaches for are found.
  4. Verify: the outcome is recorded on the originating issue and is legible there.
  5. Verify: the amendment is committed in the repo.

Expected outcome: The repo-local meta lane still works end to end while the plugin form is loaded.

Scenario 7: A pipeline phase runs on castellan (gh forge) with the plugin loaded pre-cutover

Acceptance criterion: AC-5 — "(d) one pipeline phase runs on castellan (gh-cli — the non-tea adapter leg)"

  1. In the same coexistence window, open a session in castellan.
  2. Invoke one shipped pipeline phase under its /dev: name and run it to completion against the prepared castellan issue.
  3. Verify: the phase completes.
  4. Verify: a Phase Outcome is present and parseable on that castellan issue.
  5. Verify: the issue's phase label has advanced.
  6. Verify: the records were written through the gh forge path — the issue on GitHub carries them, and nothing was written to this repo's tracker by mistake.

Expected outcome: The non-tea adapter leg works identically from the plugin form.

Scenario 8: A rehearsed abort restores the pre-cutover state on a scratch copy

Acceptance criterion: AC-7 — "when the abort path runs, then the pre-cutover state is restored (farm links, repo location, project identity) — and this abort path has been rehearsed on a scratch copy before the real cutover"

  1. On a scratch copy of the dev machine's pre-cutover state, record: the set of farm links present, the repo's location, and the project-keyed identity state (memory available, trust already granted, MCP servers configured).
  2. Run the cutover on the scratch copy.
  3. Induce a failure of the cutover's own verification step (e.g. make one of the things it checks not hold).
  4. Verify: the cutover does not report success; it reports the verification failure plainly.
  5. Let the abort path run.
  6. Verify: the farm links are back, matching the recorded set.
  7. Verify: the repo is back at its pre-cutover location.
  8. Verify: opening a session against the restored repo shows project identity intact — memory loads, no trust re-prompt, previously configured MCP servers still listed.
  9. Verify: this rehearsal happened before the real cutover was attempted — the real cutover has not yet been run at this point in the plan.

Expected outcome: A failed cutover leaves the machine as it was, and this is demonstrated on a scratch copy before the real machine is touched.

Scenario 9: On the cut-over dev machine, all 14 shipped skills list and invoke under /dev:, and bare names do not resolve

Acceptance criterion: AC-2 — "all 14 appear in the listing and each invocation resolves — and no shipped skill resolves under its bare name"

  1. After the dev-machine cutover completes, start a fresh session on that machine with the plugin loaded in the dev machine's form.
  2. List the available skills.
  3. Verify: all 14 shipped skills appear in the listing under their /dev: names — develop, gh-cli, git-worktrees, glab-cli, integrate, local-fs, promote, requirements, resolve, setup, simplify-report, tea-cli, technical-plan, worktree-cleanup. Tick each one off the list individually.
  4. Invoke each of the 14 as /dev:<skill> in turn.
  5. Verify: each invocation resolves and the skill starts (it prints its own opening behavior rather than an unknown-command response). Cancel each once it is clearly running.
  6. Invoke a handful of the shipped skills by their bare names (e.g. requirements, develop, tea-cli).
  7. Verify: none of them resolve — the bare name is not recognized as a shipped skill.

Expected outcome: On the dev machine, shipped capability is reachable only through the /dev: namespace, and all 14 skills are there.

Scenario 10: The moved repo keeps its project identity

Acceptance criterion: AC-6 — "the suite works from the new location and project identity is preserved"

  1. After the dev-machine cutover, start a session with the working directory at ~/Coding/devwork-skills.
  2. Verify: the session's loaded memory is the repo's own memory — ask for something only that memory would supply and confirm it comes back.
  3. Verify: no trust prompt appears for this directory.
  4. Verify: the MCP servers that were configured for this project before the move are listed as available.
  5. Invoke one shipped skill under its /dev: name from this location.
  6. Verify: it resolves and runs, with no error mentioning the old ~/.claude/repos/ path.

Expected outcome: The repo works from its new home in the coding-projects root with memory, trust, and MCP configuration carried over.

Scenario 11: The dev machine shows no farm remnants and /dev:-only consumer guidance

Acceptance criterion: AC-12 — "no symlink-farm links … remain, every consumer-project CLAUDE.md guidance block on it references shipped skills by /dev: names only"

  1. On the cut-over dev machine, inspect the location where the symlink farm lived.
  2. Verify: none of the 15 farm links remain — not one, and no leftover links pointing into ~/.claude/repos/devwork-skills.
  3. Verify: nothing remains at the old repo path.
  4. For each consumer project present on this machine (castellan, verity, axana, outwrit, cunnan, and this repo), open its CLAUDE.md and read its guidance block.
  5. Verify: every reference to a shipped skill uses a /dev: name.
  6. Verify: no bare-name reference to any of the 14 shipped skills remains in those blocks.
  7. Verify: references to the repo-local meta skills (where present) are still bare names — the rewrite did not sweep them into /dev:.
  8. Verify: historical records (past run reports, prior Phase Outcomes, analysis docs) still carry their pre-migration names verbatim and were not rewritten.

Expected outcome: The dev machine has no farm left, live guidance points at /dev: names, and the audit trail is untouched.

Scenario 12: A clean, agreeing, untagged state releases 0.1.0

Acceptance criterion: AC-10 — "when a release is attempted, then the release tooling refuses…" (positive control for the three refusal conditions)

  1. Bring the repo to a clean working tree with no uncommitted changes.
  2. Confirm the plugin manifest's version field and the marketplace entry's version field both read 0.1.0, and that no tag yet claims that version.
  3. Attempt the release.
  4. Verify: the release tooling completes with a zero exit status.
  5. Verify: a tag for version 0.1.0 now exists.
  6. Verify: the released artifact is reachable from the GitHub remote as a marketplace source.

Expected outcome: A legitimate release goes through and produces a versioned 0.1.0 tag — establishing that the later refusals are refusals, not a tool that never works.

Scenario 13: The desktop cutover is executed from the README alone

Acceptance criterion: AC-11 — "the desktop cutover itself is executed following only the README's instructions; any step that needed outside knowledge is a README defect"

  1. Give an operator (or a session) with no prior knowledge of the cutover plan only the rewritten README.
  2. Verify: the README enumerates the prerequisites — bash ≥ 3.2, jq, git, and the project's forge CLI — and the operator can check each one on the desktop from what the README says.
  3. Have the operator perform the desktop cutover following the README top to bottom, keeping a written log.
  4. Verify: at every step, the operator records whether they could proceed from the README alone, or had to consult something else (another document, the tracker, another person, trial and error).
  5. Verify: the cutover reaches a completed state.
  6. Verify: the log contains zero entries where outside knowledge was needed. Every such entry is filed as a README defect and must be fixed and the README re-walked before this scenario passes.

Expected outcome: The README is sufficient on its own to cut over the desktop; any gap is treated as a defect in the README rather than an operator problem.

Scenario 14: The desktop installs the released plugin and runs a helper-invoking phase

Acceptance criterion: AC-8 — "the install succeeds at the released version and the phase completes with its forge records posted"

  1. On the desktop, with the cutover done, install the plugin from the GitHub marketplace source.
  2. Verify: the install succeeds.
  3. Verify: the installed version reported by the desktop is the released version (0.1.0), not a branch, not "latest unknown".
  4. Open a session in a consumer project on the desktop and run one pipeline phase that invokes the suite's helpers.
  5. Verify: the phase completes — the helpers run, no step fails on a missing file or an unresolvable path inside the installed plugin.
  6. Verify: a Phase Outcome is present and parseable on the correct issue for that project.
  7. Verify: that issue's phase label has advanced.

Expected outcome: A released version installs cleanly on the desktop and does real work, including running its helpers from the installed form.

Scenario 15: On the desktop, all 14 shipped skills list and invoke under /dev:, and bare names do not resolve

Acceptance criterion: AC-2 — "Both listing and invocation are checked on each machine's form"

  1. On the desktop, with the plugin installed from the marketplace, start a fresh session.
  2. List the available skills.
  3. Verify: all 14 shipped skills appear under their /dev: names — tick each off individually.
  4. Invoke each of the 14 as /dev:<skill>; cancel each once it is clearly running.
  5. Verify: every invocation resolves.
  6. Invoke several shipped skills by bare name.
  7. Verify: none resolve.
  8. Invoke /meta-amend and /meta-retrospective by bare name on the desktop.
  9. Verify: neither resolves — the repo-local meta lane is unreachable from a consumer machine.

Expected outcome: The desktop's installed form exposes exactly the 14 shipped skills, only under /dev:, with no path to the meta lane.

Scenario 16: The desktop update ritual moves it from one release to the next

Acceptance criterion: AC-9 — "when the desktop performs the update ritual, then it runs the new version — the vN → vN+1 path is exercised"

  1. With the desktop already running the 0.1.0 install from Scenario 14, cut a second release from the dev machine at a higher version.
  2. On the desktop, perform the update ritual as the README describes it.
  3. Verify: the update succeeds.
  4. Verify: the version the desktop reports for the plugin is now the new version, not 0.1.0.
  5. Run one shipped skill under /dev: on the desktop.
  6. Verify: a change that exists only in the newer release is observably present in what runs (pick a visible, behavior-free marker introduced by the new release and confirm the running skill reflects it).
  7. Verify: the update did not require reinstalling from scratch or removing the previous install by hand.

Expected outcome: The desktop moves from one release to the next by a deliberate update, and demonstrably runs the newer one.

Scenario 17: The desktop shows no farm, no clone, /dev:-only guidance, and version-attributable run records

Acceptance criterion: AC-12 — "no symlink-farm links (and on the desktop, no suite clone) remain … and desktop run records are attributable to the installed release version"

  1. On the cut-over desktop, inspect where the symlink farm lived.
  2. Verify: no farm links remain.
  3. Verify: no clone of the suite repo remains anywhere on the desktop — the only copy of the suite is the installed plugin.
  4. For each consumer project on the desktop, read its CLAUDE.md guidance block.
  5. Verify: shipped skills are referenced by /dev: names only.
  6. Take a run record produced on the desktop (from Scenario 14 or 16).
  7. Verify: the record states which released version of the plugin produced it, and that version matches what the desktop reports as installed.
  8. Verify: the same is true for a record produced after the update — its stated version is the newer one, so records from the two runs are distinguishable.

Expected outcome: The desktop is a pure consumer with no local suite copy, and every run it produces can be traced to a specific release.

Scenario 18: A release from a dirty tree is refused

Acceptance criterion: AC-10 — "Given a dirty working tree … the release tooling refuses with a non-zero exit and no tag or install artifact is produced"

  1. Prepare a repo state with a valid, agreeing, unclaimed version, then leave an uncommitted change in the working tree.
  2. Record the current set of tags.
  3. Attempt a release.
  4. Verify: the attempt fails with a non-zero exit.
  5. Verify: the message names the dirty tree as the reason — the operator can tell what to fix without guessing.
  6. Verify: the tag set is unchanged — no new tag was created.
  7. Verify: no install artifact was produced or published; the remote offers nothing new to install.

Expected outcome: A dirty tree blocks the release loudly and leaves nothing behind.

Scenario 19: A release with disagreeing manifest version fields is refused

Acceptance criterion: AC-10 — "disagreeing manifest version fields … the release tooling refuses"

  1. Prepare a clean working tree where the plugin manifest's version field and the marketplace entry's version field differ.
  2. Record the current set of tags.
  3. Attempt a release.
  4. Verify: the attempt fails with a non-zero exit.
  5. Verify: the message identifies the disagreement between the two version fields.
  6. Verify: no new tag exists.
  7. Verify: no install artifact was produced.

Expected outcome: The two manifests must agree on the version before anything ships; disagreement is caught, not silently resolved.

Scenario 20: A release reusing an existing version tag is refused

Acceptance criterion: AC-10 — "an already-existing version tag … the release tooling refuses"

  1. Prepare a clean working tree whose agreeing version fields name a version already claimed by an existing tag (e.g. 0.1.0 after Scenario 12).
  2. Record the existing tag and what it points at.
  3. Attempt a release.
  4. Verify: the attempt fails with a non-zero exit.
  5. Verify: the message identifies the version as already released.
  6. Verify: the existing tag still points at exactly what it pointed at before — it was not moved or overwritten.
  7. Verify: no install artifact was produced.

Expected outcome: A version number can be released once; a second attempt is refused without disturbing the first release.

Scenario 21: The desktop can fall back to the previous release from the remote

Acceptance criterion: AC-7 — "The desktop's previous release remains installable from the remote as its recovery path"

  1. With the desktop on the newer release (post-Scenario 16), treat that release as unusable.
  2. From the desktop, install the previous release version from the GitHub marketplace source.
  3. Verify: the install succeeds.
  4. Verify: the desktop reports the previous version as installed.
  5. Run one shipped skill under /dev:.
  6. Verify: it resolves and runs.
  7. Verify: this recovery required nothing but the remote — no clone, no artifact copied from the dev machine, no manual file placement.

Expected outcome: The desktop's recovery is a reinstall of the prior release from the remote, and it works.

Scenario 22: A desktop install with no working credential path fails loudly

Acceptance criterion: AC-8 — "when the desktop installs the plugin from the GitHub marketplace source" (failure edge of the install path)

  1. On the desktop, put the credential path to the GitHub remote out of reach (as it would be if the remote were private and the desktop unauthenticated).
  2. Attempt to install the plugin from the GitHub marketplace source.
  3. Verify: the attempt fails visibly, naming the access problem — it does not appear to succeed.
  4. Verify: no partially installed plugin is left behind — listing skills afterwards shows no /dev: skills from a half-install.
  5. Verify: no previously working install (if one was present) was damaged by the failed attempt.
  6. Restore the credential path and repeat the install.
  7. Verify: the install now succeeds at the released version.

Expected outcome: A missing credential path produces a clear failure and a clean machine, never a silently broken or half-present install.

Scenario 23: Strict validation actually rejects a defective manifest

Acceptance criterion: AC-1 — "validated with the plugin tooling's strict mode" (non-vacuity control)

  1. On a scratch copy of the restructured repo, introduce one deliberate defect into the plugin manifest (remove a required field or give one an invalid value).
  2. Run the plugin tooling's strict validation.
  3. Verify: it reports at least one error and does not exit successfully.
  4. Verify: the message identifies the offending field.
  5. Repeat with a defect in the marketplace manifest instead.
  6. Verify: it likewise fails and points at the offending field.
  7. Discard the scratch copy.

Expected outcome: The clean result in Scenario 1 means something — the strict validator demonstrably fails a bad manifest and says which field is bad.

Scenario 24: The mechanical scan actually catches each violation class

Acceptance criterion: AC-3 — "the mechanical scan itself is verified non-vacuous" (per-class control)

  1. On a scratch copy of the shipped plugin contents, seed one ~/.claude/skills/ path reference into a skill file.
  2. Run the scan. Verify: it reports that hit and does not pass.
  3. Remove that seed; seed a _shared/ reference inside a shell fence that is not prefixed by the plugin-root variable.
  4. Run the scan. Verify: it reports that hit.
  5. Remove that seed; seed a _shared/ reference in plain markdown prose, not in any fence, without the plugin-root prefix.
  6. Run the scan. Verify: it reports that hit too — prose is scanned, not only fences.
  7. Remove that seed; seed a /dev: reference naming a skill outside the 14 (including one naming a meta skill).
  8. Run the scan. Verify: it reports that hit.
  9. Remove all seeds and re-run. Verify: the scan is clean again and still reports a non-zero examined-file count.

Expected outcome: Each of the three violation classes is provably detected, including the prose-not-fence case, so a clean scan is evidence rather than silence.

Notes

  • AC-2 is contingent. Every scenario that invokes a shipped skill as /dev:<skill> (9, 14, 15, 16, 21, and the coexistence runs 4–7) presumes the P0.1 probe confirmed that plugin skills are slash-invocable. If P0.1 comes back negative, these scenarios cannot be executed as written and the feature halts on a Pending Decision instead of failing these cases.
  • Scenarios 4–7 must run before any cutover, in the window where the plugin form and the farm coexist and bare names may still resolve. Their value is checking that plugin-form execution works while the old form is still available as a fallback — running them after cutover would not exercise the same thing. They also depend on P0.2 (coexistence) actually being possible; if the two forms cannot coexist, the pre-cutover leg of AC-5 has no window to run in.
  • Scenario 8 must complete before Scenarios 9–11. The rehearsal is a precondition of the real cutover, not a parallel activity.
  • Scenario 12 must run before 20, since 20 depends on 0.1.0 already being claimed. Scenario 16 depends on 14.
  • AC-11's judgment is self-reported. "Needed outside knowledge" is assessed by the person executing; the safeguard is choosing an executor with no prior exposure to the cutover plan and requiring a written per-step log rather than a summary verdict.
  • The freeze is a precondition, not a step. Scenarios 8–17 assume no feature is in flight on either machine; if the cutover asserts this itself and the assertion fails, that is a blocked run, not a failed scenario here.
  • Historical records are checked for non-change (Scenario 11 step 8) because the constraint is that pre-migration names stay verbatim — a rewrite that "helpfully" updates old run reports is a defect, not an improvement.
  • No browser surface exists. Every scenario is executed by an operator at a terminal, against the filesystem, the CLI, and the two forges.
  • Two load forms remain by design (skills-dir on the dev machine, marketplace cache on the desktop). Scenario 9 covers the first, Scenarios 14–16 the second; a regression specific to one form will not be caught by the other, which is why both machines get their own listing-and-invocation check.
<!-- test-plan:v1 issue=50 skill=requirements --> # Test Plan: ship-the-suite-as-a-claude-code-marketplace-plugin ## Prerequisites - **Two machines**, in the roles the PREQ names: - the **dev machine** — where the suite is developed, released, and dogfooded; currently carries the symlink farm at `~/.claude/skills/` and the repo at `~/.claude/repos/devwork-skills`; - the **desktop (jochems-forge)** — currently carries a second clone plus its own farm; becomes a pure consumer. - On each machine, the prerequisites the README is required to enumerate are present and checkable: **bash ≥ 3.2, `jq`, `git`, and the project's forge CLI** (`tea` for this repo, `gh` for castellan). - The **GitHub remote** `github.com/jbr870/devwork-skills` exists and is reachable from the desktop, with whatever credential path that remote requires available to the desktop operator. - The **P0.1 probe outcome is known and affirmative** — plugin skills are slash-invocable. If the probe says otherwise, AC-2 (and every scenario depending on `/dev:` invocation) is void and the run halts on a Pending Decision instead. - The **work queue is frozen** for the cutover scenarios: the meta-amend queue (#38–#49) and verity#237 are not in flight, and no feature is mid-pipeline on either machine. - A **scratch copy** of the repo and of the machine's pre-cutover state, usable for rehearsal and for negative controls without touching the real machines. - Consumer projects reachable on the relevant machine: **castellan** (gh-cli forge), and this repo itself (tea forge). The remaining consumers (verity, axana, outwrit, cunnan) need only their CLAUDE.md files present for inspection. - Both **pre-cutover state** (farm links, repo location, project-keyed runtime config) and a way to observe it are captured before any cutover scenario runs, so restoration can be judged. ### Required Test Data - **A scratch issue on this repo's tracker** (tea forge), used as the target of the `/dev:requirements` phase in the coexistence run. - **An issue on castellan's tracker** (gh forge) able to receive one pipeline phase's records. - **A tracker issue suitable for one meta-amend cycle** — a finding that still holds at HEAD and needs only a text amendment. - **Two consecutive releases** of the plugin: the first at version **0.1.0**, and a later one at a higher version, so the update path can be exercised. - **Three separately prepared release-refusal states**: (a) a working tree with uncommitted changes, (b) a tree where the plugin manifest's version field and the marketplace entry's version field disagree, (c) a tree whose version is already claimed by an existing tag. - **A deliberately defective manifest** on a scratch copy (one field wrong or missing) for the validator's negative control. - **A scratch copy seeded with one instance of each violation class** the mechanical scan is meant to catch: a `~/.claude/skills/` path reference, a `_shared/` reference not prefixed by the plugin-root variable (one in a shell fence, one in prose), and a `/dev:<x>` reference naming a skill outside the 14. - **The list of the 14 shipped skills**, used verbatim as the checklist: develop, gh-cli, git-worktrees, glab-cli, integrate, local-fs, promote, requirements, resolve, setup, simplify-report, tea-cli, technical-plan, worktree-cleanup. - **The rewritten README**, and an operator (or a session) with no prior knowledge of the cutover steps beyond what the README says. ## Test Scenarios ### Scenario 1: Manifests pass strict validation with a clean bill **Acceptance criterion:** AC-1 — "validation passes with zero errors and zero warnings" 1. Start from the restructured repo at a committed, clean state. 2. Run the plugin tooling's validation of the plugin manifest and the marketplace manifest in its strict mode. 3. Verify: the run reports **zero errors**. 4. Verify: the run reports **zero warnings** — not "only warnings", not warnings that are waved off. 5. Verify: the run exits successfully. **Expected outcome:** Strict validation of both manifests is completely clean; nothing is deferred or explained away. ### Scenario 2: The shipped plugin's contents scan clean, and the scan is proven non-vacuous **Acceptance criterion:** AC-3 — "zero references to `~/.claude/skills/` paths, zero bare `_shared/` invocations … and the mechanical scan itself is verified non-vacuous" 1. Take the shipped plugin's contents as they would be delivered. 2. Run the mechanical scan over them. 3. Verify: the scan reports **how many files it examined**, and that number is non-zero and plausibly matches the plugin's real file population (spot-check by counting files under the plugin root by hand and comparing). 4. Verify: **zero** hits for `~/.claude/skills/` paths. 5. Verify: **zero** `_shared/` references that are not prefixed by the plugin-root variable — counting references in markdown prose, not only those inside shell fences. 6. Verify: **zero** `/dev:<x>` references where `<x>` is outside the 14 shipped skill names. **Expected outcome:** The scan looks at a real, non-empty population of files and finds none of the three violation classes. ### Scenario 3: The meta lane runs by bare name in the repo and is absent from the shipped plugin **Acceptance criterion:** AC-4 — "when the operator invokes `/meta-amend` or `/meta-retrospective` by bare name, then they run; and … no meta skill or repo-local script is present in it" 1. Start a session in this repo. 2. Invoke `/meta-amend` by its bare name. 3. Verify: the invocation resolves and the skill begins its work (it identifies the issue/finding it is operating on, rather than reporting an unknown command). 4. Invoke `/meta-retrospective` by its bare name. 5. Verify: the invocation resolves and the skill begins its review. 6. Inspect the shipped plugin's contents. 7. Verify: neither meta skill is present anywhere in it. 8. Verify: no repo-local script directory, analysis docs, run reports, or `.devwork/` material is present in it. **Expected outcome:** The meta lane is fully usable inside the repo by bare name, and entirely absent from what ships. ### Scenario 4: A full requirements phase runs on a scratch tea issue with the plugin loaded pre-cutover **Acceptance criterion:** AC-5 — "a full `/dev:requirements` phase runs on a scratch issue of this repo (tea forge) … with their forge records posted correctly" 1. With the plugin form loaded alongside the still-present farm (bare names may still resolve in this window), open a session in this repo. 2. Invoke `/dev:requirements` and run the phase through to completion against the scratch issue. 3. Verify: the phase completes without the operator having to fall back to a bare-name invocation. 4. Open the scratch issue on the tracker. 5. Verify: a Phase Outcome is present on **that** issue (not another), and reads as a well-formed Phase Outcome — its fields are legible and its next-state is stated. 6. Verify: the issue's phase label has advanced to the value the completed phase implies. **Expected outcome:** The requirements phase runs from the plugin form and leaves correct, parseable forge records on the right issue. ### Scenario 5: A worktree slot is created with the plugin loaded pre-cutover **Acceptance criterion:** AC-5 — "(b) a worktree slot is created" 1. In the same coexistence window, invoke the shipped worktree skill under its `/dev:` name. 2. Create a worktree slot. 3. Verify: the slot is created and the operator can enter it and see the repo's files there. 4. Verify: any record the slot creation is meant to post is present and legible where the phase records live. 5. Verify: no step of slot creation failed on a path that pointed into the old farm location. **Expected outcome:** Slot creation succeeds from the plugin form, with its records intact. ### Scenario 6: One meta-amend cycle runs with the plugin loaded pre-cutover **Acceptance criterion:** AC-5 — "(c) one meta-amend cycle runs" 1. In the same coexistence window, invoke `/meta-amend` (bare name — it is repo-local) against a prepared finding issue. 2. Let the cycle run: verification of the finding, the text amendment, the mechanical gate, the desk-check, the commit. 3. Verify: the cycle completes without an unresolvable path error — in particular, the helpers it reaches for are found. 4. Verify: the outcome is recorded on the originating issue and is legible there. 5. Verify: the amendment is committed in the repo. **Expected outcome:** The repo-local meta lane still works end to end while the plugin form is loaded. ### Scenario 7: A pipeline phase runs on castellan (gh forge) with the plugin loaded pre-cutover **Acceptance criterion:** AC-5 — "(d) one pipeline phase runs on castellan (gh-cli — the non-tea adapter leg)" 1. In the same coexistence window, open a session in castellan. 2. Invoke one shipped pipeline phase under its `/dev:` name and run it to completion against the prepared castellan issue. 3. Verify: the phase completes. 4. Verify: a Phase Outcome is present and parseable on that castellan issue. 5. Verify: the issue's phase label has advanced. 6. Verify: the records were written through the gh forge path — the issue on GitHub carries them, and nothing was written to this repo's tracker by mistake. **Expected outcome:** The non-tea adapter leg works identically from the plugin form. ### Scenario 8: A rehearsed abort restores the pre-cutover state on a scratch copy **Acceptance criterion:** AC-7 — "when the abort path runs, then the pre-cutover state is restored (farm links, repo location, project identity) — and this abort path has been rehearsed on a scratch copy before the real cutover" 1. On a scratch copy of the dev machine's pre-cutover state, record: the set of farm links present, the repo's location, and the project-keyed identity state (memory available, trust already granted, MCP servers configured). 2. Run the cutover on the scratch copy. 3. Induce a failure of the cutover's own verification step (e.g. make one of the things it checks not hold). 4. Verify: the cutover does not report success; it reports the verification failure plainly. 5. Let the abort path run. 6. Verify: the farm links are back, matching the recorded set. 7. Verify: the repo is back at its pre-cutover location. 8. Verify: opening a session against the restored repo shows project identity intact — memory loads, no trust re-prompt, previously configured MCP servers still listed. 9. Verify: this rehearsal happened **before** the real cutover was attempted — the real cutover has not yet been run at this point in the plan. **Expected outcome:** A failed cutover leaves the machine as it was, and this is demonstrated on a scratch copy before the real machine is touched. ### Scenario 9: On the cut-over dev machine, all 14 shipped skills list and invoke under `/dev:`, and bare names do not resolve **Acceptance criterion:** AC-2 — "all 14 appear in the listing and each invocation resolves — and no shipped skill resolves under its bare name" 1. After the dev-machine cutover completes, start a fresh session on that machine with the plugin loaded in the dev machine's form. 2. List the available skills. 3. Verify: all 14 shipped skills appear in the listing under their `/dev:` names — develop, gh-cli, git-worktrees, glab-cli, integrate, local-fs, promote, requirements, resolve, setup, simplify-report, tea-cli, technical-plan, worktree-cleanup. Tick each one off the list individually. 4. Invoke each of the 14 as `/dev:<skill>` in turn. 5. Verify: each invocation resolves and the skill starts (it prints its own opening behavior rather than an unknown-command response). Cancel each once it is clearly running. 6. Invoke a handful of the shipped skills by their **bare** names (e.g. `requirements`, `develop`, `tea-cli`). 7. Verify: none of them resolve — the bare name is not recognized as a shipped skill. **Expected outcome:** On the dev machine, shipped capability is reachable only through the `/dev:` namespace, and all 14 skills are there. ### Scenario 10: The moved repo keeps its project identity **Acceptance criterion:** AC-6 — "the suite works from the new location and project identity is preserved" 1. After the dev-machine cutover, start a session with the working directory at `~/Coding/devwork-skills`. 2. Verify: the session's loaded memory is the repo's own memory — ask for something only that memory would supply and confirm it comes back. 3. Verify: **no trust prompt appears** for this directory. 4. Verify: the MCP servers that were configured for this project before the move are listed as available. 5. Invoke one shipped skill under its `/dev:` name from this location. 6. Verify: it resolves and runs, with no error mentioning the old `~/.claude/repos/` path. **Expected outcome:** The repo works from its new home in the coding-projects root with memory, trust, and MCP configuration carried over. ### Scenario 11: The dev machine shows no farm remnants and `/dev:`-only consumer guidance **Acceptance criterion:** AC-12 — "no symlink-farm links … remain, every consumer-project CLAUDE.md guidance block on it references shipped skills by `/dev:` names only" 1. On the cut-over dev machine, inspect the location where the symlink farm lived. 2. Verify: none of the 15 farm links remain — not one, and no leftover links pointing into `~/.claude/repos/devwork-skills`. 3. Verify: nothing remains at the old repo path. 4. For each consumer project present on this machine (castellan, verity, axana, outwrit, cunnan, and this repo), open its CLAUDE.md and read its guidance block. 5. Verify: every reference to a shipped skill uses a `/dev:` name. 6. Verify: no bare-name reference to any of the 14 shipped skills remains in those blocks. 7. Verify: references to the repo-local meta skills (where present) are still bare names — the rewrite did not sweep them into `/dev:`. 8. Verify: historical records (past run reports, prior Phase Outcomes, analysis docs) still carry their pre-migration names verbatim and were **not** rewritten. **Expected outcome:** The dev machine has no farm left, live guidance points at `/dev:` names, and the audit trail is untouched. ### Scenario 12: A clean, agreeing, untagged state releases 0.1.0 **Acceptance criterion:** AC-10 — "when a release is attempted, then the release tooling refuses…" (positive control for the three refusal conditions) 1. Bring the repo to a clean working tree with no uncommitted changes. 2. Confirm the plugin manifest's version field and the marketplace entry's version field both read `0.1.0`, and that no tag yet claims that version. 3. Attempt the release. 4. Verify: the release tooling completes with a zero exit status. 5. Verify: a tag for version `0.1.0` now exists. 6. Verify: the released artifact is reachable from the GitHub remote as a marketplace source. **Expected outcome:** A legitimate release goes through and produces a versioned `0.1.0` tag — establishing that the later refusals are refusals, not a tool that never works. ### Scenario 13: The desktop cutover is executed from the README alone **Acceptance criterion:** AC-11 — "the desktop cutover itself is executed following only the README's instructions; any step that needed outside knowledge is a README defect" 1. Give an operator (or a session) with no prior knowledge of the cutover plan **only** the rewritten README. 2. Verify: the README enumerates the prerequisites — bash ≥ 3.2, `jq`, `git`, and the project's forge CLI — and the operator can check each one on the desktop from what the README says. 3. Have the operator perform the desktop cutover following the README top to bottom, keeping a written log. 4. Verify: at every step, the operator records whether they could proceed from the README alone, or had to consult something else (another document, the tracker, another person, trial and error). 5. Verify: the cutover reaches a completed state. 6. Verify: the log contains **zero** entries where outside knowledge was needed. Every such entry is filed as a README defect and must be fixed and the README re-walked before this scenario passes. **Expected outcome:** The README is sufficient on its own to cut over the desktop; any gap is treated as a defect in the README rather than an operator problem. ### Scenario 14: The desktop installs the released plugin and runs a helper-invoking phase **Acceptance criterion:** AC-8 — "the install succeeds at the released version and the phase completes with its forge records posted" 1. On the desktop, with the cutover done, install the plugin from the GitHub marketplace source. 2. Verify: the install succeeds. 3. Verify: the installed version reported by the desktop is the released version (`0.1.0`), not a branch, not "latest unknown". 4. Open a session in a consumer project on the desktop and run one pipeline phase that invokes the suite's helpers. 5. Verify: the phase completes — the helpers run, no step fails on a missing file or an unresolvable path inside the installed plugin. 6. Verify: a Phase Outcome is present and parseable on the correct issue for that project. 7. Verify: that issue's phase label has advanced. **Expected outcome:** A released version installs cleanly on the desktop and does real work, including running its helpers from the installed form. ### Scenario 15: On the desktop, all 14 shipped skills list and invoke under `/dev:`, and bare names do not resolve **Acceptance criterion:** AC-2 — "Both listing and invocation are checked on each machine's form" 1. On the desktop, with the plugin installed from the marketplace, start a fresh session. 2. List the available skills. 3. Verify: all 14 shipped skills appear under their `/dev:` names — tick each off individually. 4. Invoke each of the 14 as `/dev:<skill>`; cancel each once it is clearly running. 5. Verify: every invocation resolves. 6. Invoke several shipped skills by bare name. 7. Verify: none resolve. 8. Invoke `/meta-amend` and `/meta-retrospective` by bare name on the desktop. 9. Verify: neither resolves — the repo-local meta lane is unreachable from a consumer machine. **Expected outcome:** The desktop's installed form exposes exactly the 14 shipped skills, only under `/dev:`, with no path to the meta lane. ### Scenario 16: The desktop update ritual moves it from one release to the next **Acceptance criterion:** AC-9 — "when the desktop performs the update ritual, then it runs the new version — the vN → vN+1 path is exercised" 1. With the desktop already running the `0.1.0` install from Scenario 14, cut a second release from the dev machine at a higher version. 2. On the desktop, perform the update ritual as the README describes it. 3. Verify: the update succeeds. 4. Verify: the version the desktop reports for the plugin is now the **new** version, not `0.1.0`. 5. Run one shipped skill under `/dev:` on the desktop. 6. Verify: a change that exists only in the newer release is observably present in what runs (pick a visible, behavior-free marker introduced by the new release and confirm the running skill reflects it). 7. Verify: the update did not require reinstalling from scratch or removing the previous install by hand. **Expected outcome:** The desktop moves from one release to the next by a deliberate update, and demonstrably runs the newer one. ### Scenario 17: The desktop shows no farm, no clone, `/dev:`-only guidance, and version-attributable run records **Acceptance criterion:** AC-12 — "no symlink-farm links (and on the desktop, no suite clone) remain … and desktop run records are attributable to the installed release version" 1. On the cut-over desktop, inspect where the symlink farm lived. 2. Verify: no farm links remain. 3. Verify: **no clone of the suite repo remains anywhere on the desktop** — the only copy of the suite is the installed plugin. 4. For each consumer project on the desktop, read its CLAUDE.md guidance block. 5. Verify: shipped skills are referenced by `/dev:` names only. 6. Take a run record produced on the desktop (from Scenario 14 or 16). 7. Verify: the record states which released version of the plugin produced it, and that version matches what the desktop reports as installed. 8. Verify: the same is true for a record produced after the update — its stated version is the newer one, so records from the two runs are distinguishable. **Expected outcome:** The desktop is a pure consumer with no local suite copy, and every run it produces can be traced to a specific release. ### Scenario 18: A release from a dirty tree is refused **Acceptance criterion:** AC-10 — "Given a dirty working tree … the release tooling refuses with a non-zero exit and no tag or install artifact is produced" 1. Prepare a repo state with a valid, agreeing, unclaimed version, then leave an uncommitted change in the working tree. 2. Record the current set of tags. 3. Attempt a release. 4. Verify: the attempt fails with a **non-zero exit**. 5. Verify: the message names the dirty tree as the reason — the operator can tell what to fix without guessing. 6. Verify: the tag set is unchanged — no new tag was created. 7. Verify: no install artifact was produced or published; the remote offers nothing new to install. **Expected outcome:** A dirty tree blocks the release loudly and leaves nothing behind. ### Scenario 19: A release with disagreeing manifest version fields is refused **Acceptance criterion:** AC-10 — "disagreeing manifest version fields … the release tooling refuses" 1. Prepare a clean working tree where the plugin manifest's version field and the marketplace entry's version field differ. 2. Record the current set of tags. 3. Attempt a release. 4. Verify: the attempt fails with a **non-zero exit**. 5. Verify: the message identifies the disagreement between the two version fields. 6. Verify: no new tag exists. 7. Verify: no install artifact was produced. **Expected outcome:** The two manifests must agree on the version before anything ships; disagreement is caught, not silently resolved. ### Scenario 20: A release reusing an existing version tag is refused **Acceptance criterion:** AC-10 — "an already-existing version tag … the release tooling refuses" 1. Prepare a clean working tree whose agreeing version fields name a version already claimed by an existing tag (e.g. `0.1.0` after Scenario 12). 2. Record the existing tag and what it points at. 3. Attempt a release. 4. Verify: the attempt fails with a **non-zero exit**. 5. Verify: the message identifies the version as already released. 6. Verify: the existing tag still points at exactly what it pointed at before — it was not moved or overwritten. 7. Verify: no install artifact was produced. **Expected outcome:** A version number can be released once; a second attempt is refused without disturbing the first release. ### Scenario 21: The desktop can fall back to the previous release from the remote **Acceptance criterion:** AC-7 — "The desktop's previous release remains installable from the remote as its recovery path" 1. With the desktop on the newer release (post-Scenario 16), treat that release as unusable. 2. From the desktop, install the **previous** release version from the GitHub marketplace source. 3. Verify: the install succeeds. 4. Verify: the desktop reports the previous version as installed. 5. Run one shipped skill under `/dev:`. 6. Verify: it resolves and runs. 7. Verify: this recovery required nothing but the remote — no clone, no artifact copied from the dev machine, no manual file placement. **Expected outcome:** The desktop's recovery is a reinstall of the prior release from the remote, and it works. ### Scenario 22: A desktop install with no working credential path fails loudly **Acceptance criterion:** AC-8 — "when the desktop installs the plugin from the GitHub marketplace source" (failure edge of the install path) 1. On the desktop, put the credential path to the GitHub remote out of reach (as it would be if the remote were private and the desktop unauthenticated). 2. Attempt to install the plugin from the GitHub marketplace source. 3. Verify: the attempt **fails visibly**, naming the access problem — it does not appear to succeed. 4. Verify: no partially installed plugin is left behind — listing skills afterwards shows no `/dev:` skills from a half-install. 5. Verify: no previously working install (if one was present) was damaged by the failed attempt. 6. Restore the credential path and repeat the install. 7. Verify: the install now succeeds at the released version. **Expected outcome:** A missing credential path produces a clear failure and a clean machine, never a silently broken or half-present install. ### Scenario 23: Strict validation actually rejects a defective manifest **Acceptance criterion:** AC-1 — "validated with the plugin tooling's strict mode" (non-vacuity control) 1. On a scratch copy of the restructured repo, introduce one deliberate defect into the plugin manifest (remove a required field or give one an invalid value). 2. Run the plugin tooling's strict validation. 3. Verify: it reports at least one error and does **not** exit successfully. 4. Verify: the message identifies the offending field. 5. Repeat with a defect in the marketplace manifest instead. 6. Verify: it likewise fails and points at the offending field. 7. Discard the scratch copy. **Expected outcome:** The clean result in Scenario 1 means something — the strict validator demonstrably fails a bad manifest and says which field is bad. ### Scenario 24: The mechanical scan actually catches each violation class **Acceptance criterion:** AC-3 — "the mechanical scan itself is verified non-vacuous" (per-class control) 1. On a scratch copy of the shipped plugin contents, seed one `~/.claude/skills/` path reference into a skill file. 2. Run the scan. Verify: it reports that hit and does not pass. 3. Remove that seed; seed a `_shared/` reference inside a shell fence that is **not** prefixed by the plugin-root variable. 4. Run the scan. Verify: it reports that hit. 5. Remove that seed; seed a `_shared/` reference in plain markdown prose, not in any fence, without the plugin-root prefix. 6. Run the scan. Verify: it reports that hit too — prose is scanned, not only fences. 7. Remove that seed; seed a `/dev:` reference naming a skill outside the 14 (including one naming a meta skill). 8. Run the scan. Verify: it reports that hit. 9. Remove all seeds and re-run. Verify: the scan is clean again and still reports a non-zero examined-file count. **Expected outcome:** Each of the three violation classes is provably detected, including the prose-not-fence case, so a clean scan is evidence rather than silence. ## Notes - **AC-2 is contingent.** Every scenario that invokes a shipped skill as `/dev:<skill>` (9, 14, 15, 16, 21, and the coexistence runs 4–7) presumes the P0.1 probe confirmed that plugin skills are slash-invocable. If P0.1 comes back negative, these scenarios cannot be executed as written and the feature halts on a Pending Decision instead of failing these cases. - **Scenarios 4–7 must run before any cutover**, in the window where the plugin form and the farm coexist and bare names may still resolve. Their value is checking that plugin-form execution works *while the old form is still available as a fallback* — running them after cutover would not exercise the same thing. They also depend on P0.2 (coexistence) actually being possible; if the two forms cannot coexist, the pre-cutover leg of AC-5 has no window to run in. - **Scenario 8 must complete before Scenarios 9–11.** The rehearsal is a precondition of the real cutover, not a parallel activity. - **Scenario 12 must run before 20**, since 20 depends on `0.1.0` already being claimed. Scenario 16 depends on 14. - **AC-11's judgment is self-reported.** "Needed outside knowledge" is assessed by the person executing; the safeguard is choosing an executor with no prior exposure to the cutover plan and requiring a written per-step log rather than a summary verdict. - **The freeze is a precondition, not a step.** Scenarios 8–17 assume no feature is in flight on either machine; if the cutover asserts this itself and the assertion fails, that is a blocked run, not a failed scenario here. - **Historical records are checked for non-change** (Scenario 11 step 8) because the constraint is that pre-migration names stay verbatim — a rewrite that "helpfully" updates old run reports is a defect, not an improvement. - **No browser surface exists.** Every scenario is executed by an operator at a terminal, against the filesystem, the CLI, and the two forges. - **Two load forms remain by design** (skills-dir on the dev machine, marketplace cache on the desktop). Scenario 9 covers the first, Scenarios 14–16 the second; a regression specific to one form will not be caught by the other, which is why both machines get their own listing-and-invocation check.
Author
Owner
{
  "next_state": "planning",
  "produced": [
    {
      "kind": "preq",
      "ref": "issue-body",
      "summary": "PREQ for ship-the-suite-as-a-claude-code-marketplace-plugin (panel-amended, 12 ACs)"
    },
    {
      "kind": "test-plan",
      "ref": "comment:971",
      "summary": "24 validation cases from 12 acceptance criteria"
    }
  ],
  "findings": [
    {
      "category": "out-of-scope",
      "summary": "No public marketplace listing — plugin published only to the operator's own remotes",
      "reasoning": "User base is the operator's two machines; public distribution adds review/maintenance surface with no stated user",
      "id": "F-PO-50-1-1"
    },
    {
      "category": "in-scope-deferrable",
      "summary": "Automated eval suites for the plugin (plugin eval)",
      "reasoning": "Valuable follow-up but independent of the migration; deliberately deferred to a future issue",
      "id": "F-PO-50-1-2"
    },
    {
      "category": "out-of-scope",
      "summary": "Automating desktop updates",
      "reasoning": "Update remains a deliberate manual act by design — deliberateness is the versioning discipline the feature introduces",
      "id": "F-PO-50-1-3"
    },
    {
      "category": "out-of-scope",
      "summary": "Desktop escape hatch for unreleased suite changes (dormant clone / side-load)",
      "reasoning": "Operator decided unreleased dogfooding is dev-machine-only; an escape hatch reintroduces the stale-checkout hazard the feature kills",
      "id": "F-PO-50-1-4"
    },
    {
      "category": "out-of-scope",
      "summary": "Version-stamping dev-machine run records",
      "reasoning": "The dev machine runs the live checkout — git is its version identity; release-version attribution is desktop-only (AC-12)",
      "id": "F-PO-50-1-5"
    },
    {
      "category": "implementation-necessity",
      "summary": "P0.1/P0.2/P0.3 probes: execution and outcome recording have no PREQ AC",
      "reasoning": "Probes are design preconditions, not product outcomes — they need SREQ-tier verification cases; a negative P0.1 halts the run with a Pending Decision per the PREQ Dependencies",
      "id": "F-PO-50-1-6"
    },
    {
      "category": "implementation-necessity",
      "summary": "Scripted no-feature-in-flight cutover precondition: assertion firing/blocking has no PREQ AC",
      "reasoning": "A guard mechanism, not a product outcome — verify at SREQ tier that it blocks when a feature is in flight",
      "id": "F-PO-50-1-7"
    },
    {
      "category": "implementation-necessity",
      "summary": "Plugin-name lock before reference rewrite is an ordering constraint with no observable AC",
      "reasoning": "Sequencing discipline for /technical-plan phase ordering, not an externally checkable outcome",
      "id": "F-PO-50-1-8"
    },
    {
      "category": "implementation-necessity",
      "summary": "The two sanctioned restorative text touches (meta-helper path fix, adapter discovery-order hand-rewrite) have no product parent",
      "reasoning": "Carved out of the no-logic-change rule; each needs an SREQ-tier verification case — Scenario 6 exercises the meta-helper path only incidentally",
      "id": "F-PO-50-1-9"
    },
    {
      "category": "implementation-necessity",
      "summary": "Historical-records-stay-verbatim constraint has no own AC",
      "reasoning": "Covered as a non-change assertion inside Scenario 11 under AC-12; flagged so /technical-plan keeps the rewrite exclusion set explicit",
      "id": "F-PO-50-1-10"
    }
  ],
  "pending_decisions": []
}
<!-- phase-outcome:v1 id=PO-50-1 skill=requirements --> ```json { "next_state": "planning", "produced": [ { "kind": "preq", "ref": "issue-body", "summary": "PREQ for ship-the-suite-as-a-claude-code-marketplace-plugin (panel-amended, 12 ACs)" }, { "kind": "test-plan", "ref": "comment:971", "summary": "24 validation cases from 12 acceptance criteria" } ], "findings": [ { "category": "out-of-scope", "summary": "No public marketplace listing — plugin published only to the operator's own remotes", "reasoning": "User base is the operator's two machines; public distribution adds review/maintenance surface with no stated user", "id": "F-PO-50-1-1" }, { "category": "in-scope-deferrable", "summary": "Automated eval suites for the plugin (plugin eval)", "reasoning": "Valuable follow-up but independent of the migration; deliberately deferred to a future issue", "id": "F-PO-50-1-2" }, { "category": "out-of-scope", "summary": "Automating desktop updates", "reasoning": "Update remains a deliberate manual act by design — deliberateness is the versioning discipline the feature introduces", "id": "F-PO-50-1-3" }, { "category": "out-of-scope", "summary": "Desktop escape hatch for unreleased suite changes (dormant clone / side-load)", "reasoning": "Operator decided unreleased dogfooding is dev-machine-only; an escape hatch reintroduces the stale-checkout hazard the feature kills", "id": "F-PO-50-1-4" }, { "category": "out-of-scope", "summary": "Version-stamping dev-machine run records", "reasoning": "The dev machine runs the live checkout — git is its version identity; release-version attribution is desktop-only (AC-12)", "id": "F-PO-50-1-5" }, { "category": "implementation-necessity", "summary": "P0.1/P0.2/P0.3 probes: execution and outcome recording have no PREQ AC", "reasoning": "Probes are design preconditions, not product outcomes — they need SREQ-tier verification cases; a negative P0.1 halts the run with a Pending Decision per the PREQ Dependencies", "id": "F-PO-50-1-6" }, { "category": "implementation-necessity", "summary": "Scripted no-feature-in-flight cutover precondition: assertion firing/blocking has no PREQ AC", "reasoning": "A guard mechanism, not a product outcome — verify at SREQ tier that it blocks when a feature is in flight", "id": "F-PO-50-1-7" }, { "category": "implementation-necessity", "summary": "Plugin-name lock before reference rewrite is an ordering constraint with no observable AC", "reasoning": "Sequencing discipline for /technical-plan phase ordering, not an externally checkable outcome", "id": "F-PO-50-1-8" }, { "category": "implementation-necessity", "summary": "The two sanctioned restorative text touches (meta-helper path fix, adapter discovery-order hand-rewrite) have no product parent", "reasoning": "Carved out of the no-logic-change rule; each needs an SREQ-tier verification case — Scenario 6 exercises the meta-helper path only incidentally", "id": "F-PO-50-1-9" }, { "category": "implementation-necessity", "summary": "Historical-records-stay-verbatim constraint has no own AC", "reasoning": "Covered as a non-change assertion inside Scenario 11 under AC-12; flagged so /technical-plan keeps the rewrite exclusion set explicit", "id": "F-PO-50-1-10" } ], "pending_decisions": [] } ```
Author
Owner

Software Requirements: ship-the-suite-as-a-claude-code-marketplace-plugin

Context

The suite operator cannot keep the SDLC suite consistent, versioned, and honestly dogfooded across
two machines while it ships as a hand-maintained symlink farm (15 links on the dev machine, a
manually-pulled clone + farm on the desktop). The dev form is not the shipped form, nothing records
which suite state a run executed, and the shipped/meta fence is convention only. The feature
packages the 14 shipped skills + _shared as a marketplace plugin named dev, makes the plugin
form the only form anyone runs (/dev:<skill> on both machines), moves the repo to
~/Coding/devwork-skills, and turns releases into a tooling-enforced act starting at 0.1.0.

Approaches Considered

Approach A: In-place dual-form (plugin root mirrors .claude/skills/)

Summary: Keep skills where they are; add plugin manifests pointing at the existing tree.
Pros: No restructure; no reference rewrite of file paths.
Cons: Same-skill double-load hazard (verified in the #50 probes); farm and plugin both resolve the same names with no fence; guaranteed drift between forms.
Effort: Low

Approach B: Big-bang restructure on the checked-out branch

Summary: git mv everything to plugin/ directly on the live checkout, cut over in one step.
Pros: Single disruption; simplest git history.
Cons: The farm points into this repo's working tree — the branch's git mv kills the live suite mid-development, including the very session doing the work.
Effort: Medium

Approach C: Restructure in an isolated worktree; staged, scripted cutover (chosen)

Summary: Restructure and rewrite on the feature branch in an isolated git worktree; probe first; dogfood the plugin form in a coexistence window pre-cutover; then an explicit scripted cutover sequence in which the plugin form is loaded and verified before the merge, the merge runs under the plugin form, and the farm is dismantled after.
Pros: Live suite keeps working throughout development; every irreversible step is preceded by a verified reversible one; rollback is rehearsable.
Cons: Most moving parts; needs a scripted cutover with ordered steps and asserted preconditions.
Effort: High

Decision

Selected: Approach C — isolated-worktree restructure with a staged, scripted cutover.
Rationale: A and B both break the running suite (A by double-load, B by killing the farm's target mid-development). C is the only approach in which the merge instant is survivable: worktree isolation alone merely defers B's failure to the moment main fast-forwards, so the cutover is an explicit ordered sequence, not a side effect of integration.

Architecture

Component Overview

~/Coding/devwork-skills/                    # repo root = marketplace root + dev workspace (post-move)
├── .claude-plugin/marketplace.json         # entry: {name: dev, source: ./plugin, description, version (mirror)}
├── plugin/                                 # plugin root — the ONLY shipped directory
│   ├── .claude-plugin/plugin.json          # {name: dev, version: 0.1.0, author, description} — authoritative version
│   └── skills/
│       ├── _shared/                        # procedures, schemas, domains, bin helpers (D3)
│       └── <14 shipped skills>/            # develop, gh-cli, git-worktrees, glab-cli, integrate, local-fs,
│                                           # promote, requirements, resolve, setup, simplify-report, tea-cli,
│                                           # technical-plan, worktree-cleanup
├── .claude/skills/                         # repo-local project scope (NOT shipped): meta-amend, meta-retrospective
├── scripts/                                # repo-local: lint-conventions.sh, release.sh, rewrite script (audit),
│                                           # test-plugin-gates.sh, cutover-dev.sh, cutover-abort.sh, history-scan
├── sdlc-*.md, sdlc-dogfood-run-reports/, sdlc-retrospectives/   # historical records — never rewritten, never shipped
└── .devwork/                               # gitignored staging

Consumption: dev machine ~/.claude/skills/dev -> <repo>/plugin (skills-dir form, live);
desktop: versioned marketplace install from the GitHub remote (cache form). ${CLAUDE_PLUGIN_ROOT}
resolves to the symlink path on dev and the cache path on desktop; helpers must behave identically
under both (AC-8 exercises the cache form).

Data Flow

  • Skill invocation: operator types /dev:<skill> → Claude Code resolves it from the loaded
    plugin (skills-dir or cache form) → skill markdown loads with ${CLAUDE_PLUGIN_ROOT} textually
    substituted at load time → emitted shell calls helpers at
    ${CLAUDE_PLUGIN_ROOT}/skills/_shared/procedures/bin/* → helpers self-locate via logical
    BASH_SOURCE+dirname (symlink-preserving) and resolve forge adapters as siblings of _shared.
  • Adapter precedence (D10): project-scope adapter (./.claude/skills/<adapter>) → plugin-bundled
    (${CLAUDE_PLUGIN_ROOT}/skills/<adapter>) → user-scope (~/.claude/skills/<adapter>). The
    discovery-order docs are hand-rewritten to state exactly this; a consumer project's local adapter
    override keeps winning over the bundled one.
  • Release: scripts/release.sh writes both version fields → commits the bump → lint →
    claude plugin validate --strictclaude plugin tag --dry-runtag --push --remote github
    → appends {version, tag, commit SHA} to the release record. Refusals (dirty tree, version
    disagreement, existing tag) come from claude plugin tag itself.
  • Desktop update ritual: deliberate claude plugin marketplace update + install at the new
    version; run records carry the installed version (AC-12).
  • Meta lane: stays in .claude/skills/ (bare names, repo project scope). Meta skills resolve the
    helper root once, before any cd, via git rev-parse --show-toplevel (not $(pwd) — agent
    threads reset cwd, and worktree slots add roots):
    B="$(git rev-parse --show-toplevel)/plugin/skills/_shared/procedures/bin".

External Data Contracts

Inventory: external-deps.json (this folder). Three boundaries verified at plan time; five could
not be and are gated by blocking preconditions (D-PO-50-2-1 … D-PO-50-2-5, in inventory order).

Boundary / source Operation Real shape (verified) Provenance
Claude Code plugin CLI plugin {validate,tag,list,marketplace} --strict gate; tag enforcement trio (clean tree / version agreement / tag uniqueness), tag format {name}--v{version}, default remote origin; marketplace add from URL|path|GitHub repo; list --json --available recordedexternal-contracts/plugin-cli-toolchain.provenance.json
Claude Code runtime config read ~/.claude.json projects entry + projects-dir slug projects map keyed by absolute path (trust + MCP config per entry); old slug present; new slug -home-jochem-Coding-devwork-skills collision-free recordedexternal-contracts/claude-runtime-config.provenance.json
Dev-machine consumer CLAUDE.md files grep survey castellan is the only dev-machine carrier of a shipped-skill guidance block; harbour/overkill carry none; koruso has no CLAUDE.md recordedexternal-contracts/consumer-guidance-dev-machine.provenance.json
Plugin subsystem: skill slash-invocability (P0.1) invoke a SKILL.md-only plugin skill as /dev:<skill> UNVERIFIED — interactive-session behavior; #50 probe covered a command, not a skill. Decisive: negative outcome halts (fallback not pre-authorized) gated by precondition
Plugin subsystem: farm/plugin coexistence (P0.2) load skills-dir plugin alongside the farm UNVERIFIED — session load behavior; AC-5's window depends on it gated by precondition
GitHub remote github.com/jbr870/devwork-skills git ls-remote UNVERIFIED — "Repository not found" at plan time: repo missing or private with no usable dev-machine credential. PREQ assumed it exists gated by precondition
Desktop marketplace install (P0.3) marketplace add + install at a released version UNVERIFIED — desktop unreachable over ssh at plan time; install probe is desktop-side gated by precondition
Desktop consumer CLAUDE.md survey (P0.7) grep survey (verity, axana, outwrit, cunnan) UNVERIFIED — ssh timed out; AC-12's desktop rewrite worklist cannot be enumerated from here gated by precondition

Key Decisions

Decision Choice Rationale
D1 Plugin name dev (locked by PREQ, operator's call) Locks before any reference rewrite (Phase 1 commit precedes Phase 2); renaming re-churns every reference.
D2 Plugin root plugin/ subdir; marketplace.json at repo root with source: ./plugin Repo root must not ship (analysis docs, run reports, scripts/, .devwork/).
D3 _shared location plugin/skills/_shared/ Verified tolerated by validate --strict; adapter _lib.sh resolves adapters as siblings of _shared — zero helper code changes. Escape hatch if a CLI update rejects it: explicit "skills": [...] array in plugin.json. Tolerance is re-checked by every release's validate --strict gate (R2).
D4 Path canonicalization Loaded (symlink) path is canonical; helpers keep logical BASH_SOURCE+dirname; lint bans realpath, readlink -f, cd -P, pwd -P in the helper tier Regression fence (zero current violations). Same helper text must work from symlink and cache paths.
D5 Distribution remote GitHub jbr870/devwork-skills (locked by PREQ). Recommended private + fine-grained read-only PAT (contents:read, this repo only) on the desktop, stored via credential helper Repo doesn't resolve at plan time — creation + visibility decision + credential paths are precondition D-…-3. Before the first push: a full-history sensitive-data scan (history carries run reports, internal hostnames, instance URLs — publishing the repo publishes its history).
D6 Versioning & release SemVer from 0.1.0; plugin.json authoritative (marketplace entry mirrors; ignored by install — verified); scripts/release.sh sole writer of both fields; commits the bump before tagging (tag requires a clean tree); enforcement delegated to claude plugin tag; release record gets {version, tag, commit SHA} so installs are attributable to content, not a movable label; re-run after mid-flow failure is idempotent (detects committed-but-untagged state and resumes at tagging — never double-bumps) Reimplementing enforcement invites drift; the CLI already enforces the trio.
D7 Meta-lane helper root B="$(git rev-parse --show-toplevel)/plugin/skills/_shared/procedures/bin", resolved once pre-cd Meta lane runs outside plugin scope — no ${CLAUDE_PLUGIN_ROOT} substitution. git rev-parse (panel fix) survives cwd resets and worktree slots where $(pwd) breaks. This is sanctioned restorative touch #1 (the current meta-retrospective path is already broken).
D8 Scan scope The three AC-3 violation-class scans cover plugin/skills/** only; the meta lane lives outside that population, so its repo-relative helper paths need no exemption pattern Resolves the panel's class-2/meta-lane collision by scope, not by exception list. Lint still runs its portability checks over both roots.
D9 Desktop role Pure consumer, releases only, no escape hatch (locked by PREQ) Deliberate versioning discipline; recovery is reinstalling the previous release from the remote.
D10 Adapter precedence project-scope → plugin-bundled → user-scope (documented in the hand-rewritten discovery-order text; exercised by the castellan gh-cli leg) The plugin adds a third scope to adapter discovery; unspecified precedence would make consumer adapter selection ambiguous (panel). This is sanctioned restorative touch #2.
D11 Farm teardown ordering Cutover creates ~/.claude/skills/dev, restarts, verifies the plugin form, merges under it, and only then deletes the 15 farm links; the repo move follows; every step's inverse is in the abort script No window where neither form resolves; deletion is the last reversible-by-abort step (panel).
D12 ${CLAUDE_PLUGIN_ROOT} hygiene Load-time text substitution only (verified: empty in the Bash env). Lint: ban unbraced $CLAUDE_PLUGIN_ROOT; require double-quoted "${CLAUDE_PLUGIN_ROOT}" at use sites; ban escaped/dynamic forms (\${CLAUDE_PLUGIN_ROOT}, string-built names) — the actual leak paths, narrower and mechanically checkable than a heredoc ban (panel). Emitted artifacts that outlive a fence bake the substituted path and assert it non-empty + absolute before any file operation A failed substitution must fail loudly, never yield root-relative writes.

Technical Risks

Risk Likelihood Impact Mitigation
R1 Working-tree breakage at merge instant Med High D11 ordered sequence; merge runs under plugin form; nothing post-merge reads farm paths; rehearsed abort (AC-7).
R2 validate --strict tolerance of skills/_shared/ changes in a future CLI update Low Med Every release re-runs validate --strict (D6), so a regression surfaces at release time, not on the consumer; documented escape hatch: explicit "skills" array (D3).
R3 Rewrite corruption (polysemy, non-suite names, historical docs) Med High Four-class rules + allowlist + exclusion set + classifier-assisted per-file diff review; mechanical scan with per-class negative controls (AC-3, Scenario 24); historical-verbatim assertion (S-5).
R4 Project-identity loss on repo move Low High ~/.claude.json backed up before edit; only the moved path's entry migrated (never fresh trust granted programmatically); projects-dir slug migrated; verified shape recorded at plan time.
R5 Desktop rollback vs cache GC Low Med Recovery is reinstall-from-remote of the previous tag (Scenario 21), never the local cache.
R6 Mid-flight work stranded by cutover Low Med Work-queue freeze (#38–#49, verity#237) announced on the tracker; cutover script asserts no feature in flight and blocks otherwise (S-2).
R7 Coexistence-window defects Med Low Decision rule (panel): Phase 4 defects are fix-forward inside the window — the farm remains primary and nothing irreversible has happened; cutover (Phase 5) starts only after all four legs are green. Abort of the window = unload the plugin.
R8 Operator muscle memory / half-migrated confusion Med Low README old-name→new-name table; one-command machine-state probe (plugin listed? farm links present?) printed by cutover scripts and documented in the README; per-machine guidance rewrite timed with that machine's cutover (panel).

Unverified external contracts are not rows here — they are the five blocking preconditions
(see External Data Contracts and the Phase Outcome).

Expert Review

Reviewers

  • Solution Architect: adapter discovery gains an undefined third scope; _shared placement rests on undocumented validator tolerance.
  • Security Specialist: pushing to GitHub publishes the full history (secrets/sensitive-data scan needed pre-push + visibility decision); tags are force-movable (record SHA); desktop credential unscoped.
  • Backend Developer: $(pwd)-based meta helper root breaks off-root; release.sh missing bump-commit + idempotency semantics; class-2 scan would flag the meta lane's own paths.
  • UX Expert: no post-cutover muscle-memory mitigation; no machine-state probe for half-migrated states; coexistence-window exit rule unstated; consumer guidance rewrite timing must be per-machine.

Changes Made

  • D10 added (adapter precedence: project → plugin → user), exercised via the castellan leg.
  • D3 amended: validator-tolerance re-checked at every release; escape hatch documented (R2).
  • D5 amended: full-history sensitive-data scan gates the first push (new Phase 0 item P0.8); visibility decision (recommend private) + scoped read-only PAT + credential-helper storage folded into the GitHub-remote precondition; desktop cutover script verifies the marketplace source points at jbr870/devwork-skills before install.
  • D6 amended: bump committed before tagging; idempotent re-run semantics; release record carries {version, tag, commit SHA}.
  • D7 amended: helper root via git rev-parse --show-toplevel, not $(pwd).
  • D8 added: scan population is plugin/skills/** only — resolves the meta-lane collision by scope.
  • D11 added: explicit farm teardown ordering with no dead window.
  • D12 amended: double-quoting required; heredoc ban narrowed to escaped/dynamic forms; baked paths assert non-empty + absolute.
  • R4 amended: ~/.claude.json backup; migrate only the moved entry.
  • R7, R8 added (coexistence exit rule; muscle-memory + state-probe README content, per-machine rewrite timing).

Noted (not actioned)

  • Declared-block schema compatibility policy (who bends when a v0.2.0 skill reads a v0.1.0-era consumer CLAUDE.md declaration) — real gap, but this slice changes layout/references only, no schema; warrants its own issue.
  • Dev-machine run-record version/SHA attribution — rejected on product grounds: the PREQ explicitly out-scopes it (git is the dev machine's version identity; attribution is desktop-only per AC-12).
  • Signed/protected release tags — partially absorbed: the release record's commit SHA (D6) gives content attribution; GitHub-side tag protection is operator remote configuration, folded into the D5 precondition's confirm guidance rather than build scope.

Acceptance Criteria

ID Criterion (from PREQ) Verification approach
AC-1 Strict validation of both manifests: zero errors, zero warnings Mechanical: claude plugin validate --strict as a lint-gate rule in scripts/lint-conventions.sh (loud skip if CLI absent); non-vacuity control Scenario 23 in scripts/test-plugin-gates.sh
AC-2 All 14 skills list + invoke as /dev:<skill> on each machine's form; bare names don't resolve (contingent on P0.1) Human-UAT: Scenario 9 (dev, skills-dir form) + Scenario 15 (desktop, cache form), per-skill checklist
AC-3 Shipped contents: zero ~/.claude/skills/ refs, zero non-prefixed _shared/ refs (fences AND prose), zero /dev:X outside the 14; scan non-vacuous Mechanical: lint rules over plugin/skills/** (D8) with asserted non-zero examined-file count; per-class negative controls Scenario 24 in scripts/test-plugin-gates.sh
AC-4 Meta skills run bare-name in this repo; no meta skill or repo-local script ships Human-UAT invocation half (Scenario 3); mechanical absence half: lint asserts no meta-*/scripts/ under plugin/
AC-5 Coexistence dogfood: tea requirements phase, worktree slot, meta-amend cycle, castellan gh-cli phase — forge records correct Human-UAT: Scenarios 4–7 in the Phase 4 window (pre-cutover, both forms loaded)
AC-6 Moved repo preserves identity (memory, no trust re-prompt, MCP servers) Human-UAT: Scenario 10; cutover script asserts the migrated ~/.claude.json entry + new projects-dir slug exist
AC-7 Abort restores pre-cutover state; rehearsed on scratch first; desktop recovery = previous release from remote Human-UAT: Scenario 8 (rehearsal precedes real cutover — ordering asserted) + Scenario 21
AC-8 Desktop installs released version from GitHub source; helper-invoking phase completes with forge records Human-UAT: Scenario 14; credential-failure edge Scenario 22
AC-9 vN→vN+1 update ritual runs the new version Human-UAT: Scenario 16 (visible marker introduced by the second release)
AC-10 Release refused on dirty tree / version disagreement / existing tag — separately, non-zero exit, nothing produced Mechanical: Scenarios 18–20 in scripts/test-plugin-gates.sh (via tag --dry-run on prepared scratch states); positive control Scenario 12 is the real 0.1.0 release (human-UAT)
AC-11 Desktop cutover executed from the README alone; gaps are README defects Human-UAT: Scenario 13 — executor is a fresh session/operator with no prior cutover exposure, keeping a per-step log
AC-12 No farm remnants (desktop: no clone); consumer guidance /dev:-only; desktop records version-attributable Human-UAT: Scenarios 11 + 17; mechanical assist: cutover scripts' final state probe (R8)

SREQ-tier criteria (design preconditions and sanctioned touches with no PREQ AC — from PO-50-1
findings F-PO-50-1-6…10):

ID Criterion Verification approach
S-1 Every Phase 0 probe (P0.1–P0.8) executes and its outcome is recorded on issue #50 before Phase 1 begins; a negative P0.1 halts the run with a Pending Decision Probe records cited when resolving the matching preconditions; desk-check of issue #50 comments
S-2 The cutover script's no-feature-in-flight assertion blocks when a feature is in flight Negative test during the Scenario 8 rehearsal: run the cutover with a feature mid-pipeline on the scratch copy; assert refusal
S-3 Plugin name dev is committed (manifests, Phase 1) before any reference-rewrite commit (Phase 2) Mechanical: git history order of the Phase 1 and Phase 2 commits
S-4 The two sanctioned restorative touches work: meta helper root resolves from a non-repo-root cwd and inside a worktree slot (D7); adapter discovery order matches D10 with a project-scope override winning Scripted check for D7 (cd into a subdir + slot, invoke a helper via the meta root); desk-check + castellan leg for D10
S-5 Historical records stay verbatim: rewrite commits touch nothing under sdlc-*, sdlc-dogfood-run-reports/, sdlc-retrospectives/ Mechanical: git diff --stat of the Phase 2 rewrite commits over the exclusion set is empty (Scenario 11 step 8 re-checks post-cutover)

"Every call site routes through helper X" criteria: AC-3's three scans and D12's hygiene rules
are exactly this class — enforced as lint rules over plugin/skills/**, never spot-checks. The
non-vacuity controls (Scenarios 23/24) keep the green meaningful.

Temporary-scaffolding cleanup: the coexistence window (farm + plugin both loaded) is the one
deliberately temporary state; its removal is the cutover itself (Phase 5 deletes the farm,
AC-12 asserts no remnants) — no separate cleanup ticket needed. The work-queue freeze auto-expires
when Phase 6 completes (unfreeze announced on the tracker, same comment thread as the freeze).

Implementation Scope

Areas

Area Files / directories involved Nature of change
Plugin scaffolding plugin/.claude-plugin/plugin.json, .claude-plugin/marketplace.json new
Restructure git mv .claude/skills/{<14>,_shared}plugin/skills/ move
Reference rewrite plugin/skills/** text (4 classes, allowlist, exclusions); rewrite script kept in scripts/ for audit modify / new
Meta lane .claude/skills/meta-amend, .claude/skills/meta-retrospective (D7 helper root, /dev: names for shipped-skill refs, lane-gate path update) modify
Lint gates scripts/lint-conventions.sh: two roots, asserted non-zero file counts, adapter-exemption anchors, new rules (path scans D8, allowlist, validate --strict, D4 canonicalization ban, D12 hygiene) extend
Release tooling scripts/release.sh (D6), scripts/test-plugin-gates.sh (refusal + validator + scan negative controls — new suite, the integration-covered lane's named file) new
Cutover tooling scripts/cutover-dev.sh, scripts/cutover-abort.sh (D11 ordering, S-2 assertion, state probe, ~/.claude.json backup/migration); desktop steps live in the README, not a script new
History scan (P0.8) one-shot full-history sensitive-data scan before the first GitHub push; findings recorded on issue #50 new (gate)
Docs README.md rewrite (prereqs, verbatim install/update/state-probe commands, old→new name table, failure modes with expected errors); consumer CLAUDE.md guidance blocks (castellan now; desktop consumers per P0.7 worklist at Phase 6); tracker + memory notes modify

File Boundaries

The restructure + rewrite happen in an isolated git worktree — the main checkout keeps serving
the farm until cutover. scripts/ and .claude/skills/meta-* are disjoint from plugin/** and can
be worked in parallel with the rewrite. The README and consumer guidance blocks are independent of
all code areas. scripts/lint-conventions.sh must be retargeted in the same commit as the
restructure so lint is never blind between phases.

Dependencies & Sequencing

Preconditions (5 blocking decisions) → Phase 0 probes (P0.1 decisive; P0.8 history scan) →
Phase 1 restructure + manifests + lint retarget (one commit; locks name dev, S-3) →
Phase 2 reference rewrite → Phase 3 remaining gates + release/cutover tooling →
Phase 4 coexistence dogfood (AC-5; fix-forward window per R7) →
Phase 5 dev cutover (Scenario 8 rehearsal first; then D11 sequence; consumer guidance on this
machine same-day) → Phase 6 release 0.1.0 + desktop cutover (README-driven, AC-11) →
Phase 7 second release + update ritual (AC-9) and closing docs. Strictly sequential at phase
granularity; parallelism only inside Phases 2–3.

Constraints & Non-Goals

Constraints:

  • Releases only from a clean tree (install provenance records HEAD while content ships from the working tree — verified hazard; enforced by claude plugin tag).
  • Plugin name dev locks before any reference rewrite (S-3).
  • Historical records keep pre-migration names verbatim (S-5) — audit integrity beats consistency.
  • Portability baseline unchanged: emitted glue stays POSIX; helper tier stays bash ≥ 3.2 + jq + git; no new interpreter dependencies.
  • No mutating write to any external system at plan time; the five unverified boundaries are gated, not assumed.

Non-goals (do NOT build):

  • Public marketplace listing (operator's remotes only).
  • Automated eval suites for the plugin (plugin eval) — future issue.
  • Any skill logic change beyond the two sanctioned restorative touches (D7, D10).
  • Version-stamping dev-machine run records (desktop-only attribution, per PREQ).
  • Automating desktop updates; any desktop escape hatch for unreleased changes.
  • Renaming/restructuring the meta-lane skills beyond what the fence requires.
<!-- sreq:v1 issue=50 skill=technical-plan --> # Software Requirements: ship-the-suite-as-a-claude-code-marketplace-plugin ## Context The suite operator cannot keep the SDLC suite consistent, versioned, and honestly dogfooded across two machines while it ships as a hand-maintained symlink farm (15 links on the dev machine, a manually-pulled clone + farm on the desktop). The dev form is not the shipped form, nothing records which suite state a run executed, and the shipped/meta fence is convention only. The feature packages the 14 shipped skills + `_shared` as a marketplace plugin named **`dev`**, makes the plugin form the only form anyone runs (`/dev:<skill>` on both machines), moves the repo to `~/Coding/devwork-skills`, and turns releases into a tooling-enforced act starting at 0.1.0. ## Approaches Considered ### Approach A: In-place dual-form (plugin root mirrors `.claude/skills/`) **Summary:** Keep skills where they are; add plugin manifests pointing at the existing tree. **Pros:** No restructure; no reference rewrite of file paths. **Cons:** Same-skill double-load hazard (verified in the #50 probes); farm and plugin both resolve the same names with no fence; guaranteed drift between forms. **Effort:** Low ### Approach B: Big-bang restructure on the checked-out branch **Summary:** `git mv` everything to `plugin/` directly on the live checkout, cut over in one step. **Pros:** Single disruption; simplest git history. **Cons:** The farm points into this repo's working tree — the branch's `git mv` kills the live suite mid-development, including the very session doing the work. **Effort:** Medium ### Approach C: Restructure in an isolated worktree; staged, scripted cutover (chosen) **Summary:** Restructure and rewrite on the feature branch in an isolated git worktree; probe first; dogfood the plugin form in a coexistence window pre-cutover; then an explicit scripted cutover sequence in which the plugin form is loaded and verified *before* the merge, the merge runs under the plugin form, and the farm is dismantled after. **Pros:** Live suite keeps working throughout development; every irreversible step is preceded by a verified reversible one; rollback is rehearsable. **Cons:** Most moving parts; needs a scripted cutover with ordered steps and asserted preconditions. **Effort:** High ## Decision **Selected:** Approach C — isolated-worktree restructure with a staged, scripted cutover. **Rationale:** A and B both break the running suite (A by double-load, B by killing the farm's target mid-development). C is the only approach in which the merge instant is survivable: worktree isolation alone merely *defers* B's failure to the moment `main` fast-forwards, so the cutover is an explicit ordered sequence, not a side effect of integration. ## Architecture ### Component Overview ``` ~/Coding/devwork-skills/ # repo root = marketplace root + dev workspace (post-move) ├── .claude-plugin/marketplace.json # entry: {name: dev, source: ./plugin, description, version (mirror)} ├── plugin/ # plugin root — the ONLY shipped directory │ ├── .claude-plugin/plugin.json # {name: dev, version: 0.1.0, author, description} — authoritative version │ └── skills/ │ ├── _shared/ # procedures, schemas, domains, bin helpers (D3) │ └── <14 shipped skills>/ # develop, gh-cli, git-worktrees, glab-cli, integrate, local-fs, │ # promote, requirements, resolve, setup, simplify-report, tea-cli, │ # technical-plan, worktree-cleanup ├── .claude/skills/ # repo-local project scope (NOT shipped): meta-amend, meta-retrospective ├── scripts/ # repo-local: lint-conventions.sh, release.sh, rewrite script (audit), │ # test-plugin-gates.sh, cutover-dev.sh, cutover-abort.sh, history-scan ├── sdlc-*.md, sdlc-dogfood-run-reports/, sdlc-retrospectives/ # historical records — never rewritten, never shipped └── .devwork/ # gitignored staging ``` Consumption: dev machine `~/.claude/skills/dev -> <repo>/plugin` (skills-dir form, live); desktop: versioned marketplace install from the GitHub remote (cache form). `${CLAUDE_PLUGIN_ROOT}` resolves to the symlink path on dev and the cache path on desktop; helpers must behave identically under both (AC-8 exercises the cache form). ### Data Flow - **Skill invocation:** operator types `/dev:<skill>` → Claude Code resolves it from the loaded plugin (skills-dir or cache form) → skill markdown loads with `${CLAUDE_PLUGIN_ROOT}` textually substituted at load time → emitted shell calls helpers at `${CLAUDE_PLUGIN_ROOT}/skills/_shared/procedures/bin/*` → helpers self-locate via logical `BASH_SOURCE`+`dirname` (symlink-preserving) and resolve forge adapters as siblings of `_shared`. - **Adapter precedence (D10):** project-scope adapter (`./.claude/skills/<adapter>`) → plugin-bundled (`${CLAUDE_PLUGIN_ROOT}/skills/<adapter>`) → user-scope (`~/.claude/skills/<adapter>`). The discovery-order docs are hand-rewritten to state exactly this; a consumer project's local adapter override keeps winning over the bundled one. - **Release:** `scripts/release.sh` writes both version fields → commits the bump → lint → `claude plugin validate --strict` → `claude plugin tag --dry-run` → `tag --push --remote github` → appends `{version, tag, commit SHA}` to the release record. Refusals (dirty tree, version disagreement, existing tag) come from `claude plugin tag` itself. - **Desktop update ritual:** deliberate `claude plugin marketplace update` + install at the new version; run records carry the installed version (AC-12). - **Meta lane:** stays in `.claude/skills/` (bare names, repo project scope). Meta skills resolve the helper root **once, before any `cd`, via `git rev-parse --show-toplevel`** (not `$(pwd)` — agent threads reset cwd, and worktree slots add roots): `B="$(git rev-parse --show-toplevel)/plugin/skills/_shared/procedures/bin"`. ### External Data Contracts Inventory: `external-deps.json` (this folder). Three boundaries verified at plan time; five could not be and are gated by blocking preconditions (D-PO-50-2-1 … D-PO-50-2-5, in inventory order). | Boundary / source | Operation | Real shape (verified) | Provenance | | ----------------- | --------- | --------------------- | ---------- | | Claude Code plugin CLI | `plugin {validate,tag,list,marketplace}` | `--strict` gate; tag enforcement trio (clean tree / version agreement / tag uniqueness), tag format `{name}--v{version}`, default remote `origin`; marketplace add from URL\|path\|GitHub repo; `list --json --available` | `recorded` → `external-contracts/plugin-cli-toolchain.provenance.json` | | Claude Code runtime config | read `~/.claude.json` projects entry + projects-dir slug | projects map keyed by absolute path (trust + MCP config per entry); old slug present; new slug `-home-jochem-Coding-devwork-skills` collision-free | `recorded` → `external-contracts/claude-runtime-config.provenance.json` | | Dev-machine consumer CLAUDE.md files | grep survey | castellan is the only dev-machine carrier of a shipped-skill guidance block; harbour/overkill carry none; koruso has no CLAUDE.md | `recorded` → `external-contracts/consumer-guidance-dev-machine.provenance.json` | | Plugin subsystem: skill slash-invocability (P0.1) | invoke a SKILL.md-only plugin skill as `/dev:<skill>` | **UNVERIFIED** — interactive-session behavior; #50 probe covered a *command*, not a skill. Decisive: negative outcome halts (fallback not pre-authorized) | gated by precondition | | Plugin subsystem: farm/plugin coexistence (P0.2) | load skills-dir plugin alongside the farm | **UNVERIFIED** — session load behavior; AC-5's window depends on it | gated by precondition | | GitHub remote `github.com/jbr870/devwork-skills` | `git ls-remote` | **UNVERIFIED** — "Repository not found" at plan time: repo missing or private with no usable dev-machine credential. PREQ assumed it exists | gated by precondition | | Desktop marketplace install (P0.3) | marketplace add + install at a released version | **UNVERIFIED** — desktop unreachable over ssh at plan time; install probe is desktop-side | gated by precondition | | Desktop consumer CLAUDE.md survey (P0.7) | grep survey (verity, axana, outwrit, cunnan) | **UNVERIFIED** — ssh timed out; AC-12's desktop rewrite worklist cannot be enumerated from here | gated by precondition | ### Key Decisions | Decision | Choice | Rationale | | -------- | ------ | --------- | | D1 Plugin name | **`dev`** (locked by PREQ, operator's call) | Locks **before** any reference rewrite (Phase 1 commit precedes Phase 2); renaming re-churns every reference. | | D2 Plugin root | `plugin/` subdir; marketplace.json at repo root with `source: ./plugin` | Repo root must not ship (analysis docs, run reports, `scripts/`, `.devwork/`). | | D3 `_shared` location | `plugin/skills/_shared/` | Verified tolerated by `validate --strict`; adapter `_lib.sh` resolves adapters as siblings of `_shared` — zero helper code changes. Escape hatch if a CLI update rejects it: explicit `"skills": [...]` array in plugin.json. Tolerance is re-checked by every release's `validate --strict` gate (R2). | | D4 Path canonicalization | Loaded (symlink) path is canonical; helpers keep logical `BASH_SOURCE`+`dirname`; lint bans `realpath`, `readlink -f`, `cd -P`, `pwd -P` in the helper tier | Regression fence (zero current violations). Same helper text must work from symlink and cache paths. | | D5 Distribution remote | GitHub `jbr870/devwork-skills` (locked by PREQ). **Recommended private** + fine-grained read-only PAT (contents:read, this repo only) on the desktop, stored via credential helper | Repo doesn't resolve at plan time — creation + visibility decision + credential paths are precondition D-…-3. **Before the first push: a full-history sensitive-data scan** (history carries run reports, internal hostnames, instance URLs — publishing the repo publishes its history). | | D6 Versioning & release | SemVer from 0.1.0; `plugin.json` authoritative (marketplace entry mirrors; ignored by install — verified); `scripts/release.sh` sole writer of both fields; **commits the bump before tagging** (tag requires a clean tree); enforcement delegated to `claude plugin tag`; release record gets `{version, tag, commit SHA}` so installs are attributable to content, not a movable label; re-run after mid-flow failure is idempotent (detects committed-but-untagged state and resumes at tagging — never double-bumps) | Reimplementing enforcement invites drift; the CLI already enforces the trio. | | D7 Meta-lane helper root | `B="$(git rev-parse --show-toplevel)/plugin/skills/_shared/procedures/bin"`, resolved once pre-`cd` | Meta lane runs outside plugin scope — no `${CLAUDE_PLUGIN_ROOT}` substitution. `git rev-parse` (panel fix) survives cwd resets and worktree slots where `$(pwd)` breaks. This is sanctioned restorative touch #1 (the current meta-retrospective path is already broken). | | D8 Scan scope | The three AC-3 violation-class scans cover **`plugin/skills/**` only**; the meta lane lives outside that population, so its repo-relative helper paths need no exemption pattern | Resolves the panel's class-2/meta-lane collision by scope, not by exception list. Lint still runs its portability checks over both roots. | | D9 Desktop role | Pure consumer, releases only, no escape hatch (locked by PREQ) | Deliberate versioning discipline; recovery is reinstalling the previous release from the remote. | | D10 Adapter precedence | project-scope → plugin-bundled → user-scope (documented in the hand-rewritten discovery-order text; exercised by the castellan gh-cli leg) | The plugin adds a third scope to adapter discovery; unspecified precedence would make consumer adapter selection ambiguous (panel). This is sanctioned restorative touch #2. | | D11 Farm teardown ordering | Cutover creates `~/.claude/skills/dev`, restarts, **verifies the plugin form, merges under it, and only then** deletes the 15 farm links; the repo move follows; every step's inverse is in the abort script | No window where neither form resolves; deletion is the last reversible-by-abort step (panel). | | D12 `${CLAUDE_PLUGIN_ROOT}` hygiene | Load-time text substitution only (verified: empty in the Bash env). Lint: ban unbraced `$CLAUDE_PLUGIN_ROOT`; require double-quoted `"${CLAUDE_PLUGIN_ROOT}"` at use sites; ban escaped/dynamic forms (`\${CLAUDE_PLUGIN_ROOT}`, string-built names) — the actual leak paths, narrower and mechanically checkable than a heredoc ban (panel). Emitted artifacts that outlive a fence bake the substituted path and **assert it non-empty + absolute before any file operation** | A failed substitution must fail loudly, never yield root-relative writes. | ## Technical Risks | Risk | Likelihood | Impact | Mitigation | | ---- | ---------- | ------ | ---------- | | R1 Working-tree breakage at merge instant | Med | High | D11 ordered sequence; merge runs under plugin form; nothing post-merge reads farm paths; rehearsed abort (AC-7). | | R2 `validate --strict` tolerance of `skills/_shared/` changes in a future CLI update | Low | Med | Every release re-runs `validate --strict` (D6), so a regression surfaces at release time, not on the consumer; documented escape hatch: explicit `"skills"` array (D3). | | R3 Rewrite corruption (polysemy, non-suite names, historical docs) | Med | High | Four-class rules + allowlist + exclusion set + classifier-assisted per-file diff review; mechanical scan with per-class negative controls (AC-3, Scenario 24); historical-verbatim assertion (S-5). | | R4 Project-identity loss on repo move | Low | High | `~/.claude.json` backed up before edit; only the moved path's entry migrated (never fresh trust granted programmatically); projects-dir slug migrated; verified shape recorded at plan time. | | R5 Desktop rollback vs cache GC | Low | Med | Recovery is reinstall-from-remote of the previous tag (Scenario 21), never the local cache. | | R6 Mid-flight work stranded by cutover | Low | Med | Work-queue freeze (#38–#49, verity#237) announced on the tracker; cutover script **asserts** no feature in flight and blocks otherwise (S-2). | | R7 Coexistence-window defects | Med | Low | Decision rule (panel): Phase 4 defects are fix-forward inside the window — the farm remains primary and nothing irreversible has happened; cutover (Phase 5) starts only after all four legs are green. Abort of the window = unload the plugin. | | R8 Operator muscle memory / half-migrated confusion | Med | Low | README old-name→new-name table; one-command machine-state probe (plugin listed? farm links present?) printed by cutover scripts and documented in the README; per-machine guidance rewrite timed with that machine's cutover (panel). | > Unverified external contracts are **not** rows here — they are the five blocking preconditions > (see External Data Contracts and the Phase Outcome). ## Expert Review ### Reviewers - Solution Architect: adapter discovery gains an undefined third scope; `_shared` placement rests on undocumented validator tolerance. - Security Specialist: pushing to GitHub publishes the full history (secrets/sensitive-data scan needed pre-push + visibility decision); tags are force-movable (record SHA); desktop credential unscoped. - Backend Developer: `$(pwd)`-based meta helper root breaks off-root; release.sh missing bump-commit + idempotency semantics; class-2 scan would flag the meta lane's own paths. - UX Expert: no post-cutover muscle-memory mitigation; no machine-state probe for half-migrated states; coexistence-window exit rule unstated; consumer guidance rewrite timing must be per-machine. ### Changes Made - D10 added (adapter precedence: project → plugin → user), exercised via the castellan leg. - D3 amended: validator-tolerance re-checked at every release; escape hatch documented (R2). - D5 amended: full-history sensitive-data scan gates the first push (new Phase 0 item P0.8); visibility decision (recommend private) + scoped read-only PAT + credential-helper storage folded into the GitHub-remote precondition; desktop cutover script verifies the marketplace source points at `jbr870/devwork-skills` before install. - D6 amended: bump committed before tagging; idempotent re-run semantics; release record carries `{version, tag, commit SHA}`. - D7 amended: helper root via `git rev-parse --show-toplevel`, not `$(pwd)`. - D8 added: scan population is `plugin/skills/**` only — resolves the meta-lane collision by scope. - D11 added: explicit farm teardown ordering with no dead window. - D12 amended: double-quoting required; heredoc ban narrowed to escaped/dynamic forms; baked paths assert non-empty + absolute. - R4 amended: `~/.claude.json` backup; migrate only the moved entry. - R7, R8 added (coexistence exit rule; muscle-memory + state-probe README content, per-machine rewrite timing). ### Noted (not actioned) - **Declared-block schema compatibility policy** (who bends when a v0.2.0 skill reads a v0.1.0-era consumer CLAUDE.md declaration) — real gap, but this slice changes layout/references only, no schema; warrants its own issue. - **Dev-machine run-record version/SHA attribution** — rejected on product grounds: the PREQ explicitly out-scopes it (git is the dev machine's version identity; attribution is desktop-only per AC-12). - **Signed/protected release tags** — partially absorbed: the release record's commit SHA (D6) gives content attribution; GitHub-side tag protection is operator remote configuration, folded into the D5 precondition's confirm guidance rather than build scope. ## Acceptance Criteria | ID | Criterion (from PREQ) | Verification approach | | -- | --------------------- | --------------------- | | AC-1 | Strict validation of both manifests: zero errors, zero warnings | Mechanical: `claude plugin validate --strict` as a lint-gate rule in `scripts/lint-conventions.sh` (loud skip if CLI absent); non-vacuity control Scenario 23 in `scripts/test-plugin-gates.sh` | | AC-2 | All 14 skills list + invoke as `/dev:<skill>` on each machine's form; bare names don't resolve (contingent on P0.1) | Human-UAT: Scenario 9 (dev, skills-dir form) + Scenario 15 (desktop, cache form), per-skill checklist | | AC-3 | Shipped contents: zero `~/.claude/skills/` refs, zero non-prefixed `_shared/` refs (fences AND prose), zero `/dev:X` outside the 14; scan non-vacuous | Mechanical: lint rules over `plugin/skills/**` (D8) with asserted non-zero examined-file count; per-class negative controls Scenario 24 in `scripts/test-plugin-gates.sh` | | AC-4 | Meta skills run bare-name in this repo; no meta skill or repo-local script ships | Human-UAT invocation half (Scenario 3); mechanical absence half: lint asserts no `meta-*`/`scripts/` under `plugin/` | | AC-5 | Coexistence dogfood: tea requirements phase, worktree slot, meta-amend cycle, castellan gh-cli phase — forge records correct | Human-UAT: Scenarios 4–7 in the Phase 4 window (pre-cutover, both forms loaded) | | AC-6 | Moved repo preserves identity (memory, no trust re-prompt, MCP servers) | Human-UAT: Scenario 10; cutover script asserts the migrated `~/.claude.json` entry + new projects-dir slug exist | | AC-7 | Abort restores pre-cutover state; rehearsed on scratch first; desktop recovery = previous release from remote | Human-UAT: Scenario 8 (rehearsal precedes real cutover — ordering asserted) + Scenario 21 | | AC-8 | Desktop installs released version from GitHub source; helper-invoking phase completes with forge records | Human-UAT: Scenario 14; credential-failure edge Scenario 22 | | AC-9 | vN→vN+1 update ritual runs the new version | Human-UAT: Scenario 16 (visible marker introduced by the second release) | | AC-10 | Release refused on dirty tree / version disagreement / existing tag — separately, non-zero exit, nothing produced | Mechanical: Scenarios 18–20 in `scripts/test-plugin-gates.sh` (via `tag --dry-run` on prepared scratch states); positive control Scenario 12 is the real 0.1.0 release (human-UAT) | | AC-11 | Desktop cutover executed from the README alone; gaps are README defects | Human-UAT: Scenario 13 — executor is a fresh session/operator with no prior cutover exposure, keeping a per-step log | | AC-12 | No farm remnants (desktop: no clone); consumer guidance `/dev:`-only; desktop records version-attributable | Human-UAT: Scenarios 11 + 17; mechanical assist: cutover scripts' final state probe (R8) | **SREQ-tier criteria** (design preconditions and sanctioned touches with no PREQ AC — from PO-50-1 findings F-PO-50-1-6…10): | ID | Criterion | Verification approach | | -- | --------- | --------------------- | | S-1 | Every Phase 0 probe (P0.1–P0.8) executes and its outcome is recorded on issue #50 before Phase 1 begins; a negative P0.1 halts the run with a Pending Decision | Probe records cited when resolving the matching preconditions; desk-check of issue #50 comments | | S-2 | The cutover script's no-feature-in-flight assertion **blocks** when a feature is in flight | Negative test during the Scenario 8 rehearsal: run the cutover with a feature mid-pipeline on the scratch copy; assert refusal | | S-3 | Plugin name `dev` is committed (manifests, Phase 1) before any reference-rewrite commit (Phase 2) | Mechanical: git history order of the Phase 1 and Phase 2 commits | | S-4 | The two sanctioned restorative touches work: meta helper root resolves from a non-repo-root cwd and inside a worktree slot (D7); adapter discovery order matches D10 with a project-scope override winning | Scripted check for D7 (cd into a subdir + slot, invoke a helper via the meta root); desk-check + castellan leg for D10 | | S-5 | Historical records stay verbatim: rewrite commits touch nothing under `sdlc-*`, `sdlc-dogfood-run-reports/`, `sdlc-retrospectives/` | Mechanical: `git diff --stat` of the Phase 2 rewrite commits over the exclusion set is empty (Scenario 11 step 8 re-checks post-cutover) | **"Every call site routes through helper X" criteria:** AC-3's three scans and D12's hygiene rules are exactly this class — enforced as lint rules over `plugin/skills/**`, never spot-checks. The non-vacuity controls (Scenarios 23/24) keep the green meaningful. **Temporary-scaffolding cleanup:** the coexistence window (farm + plugin both loaded) is the one deliberately temporary state; its removal **is** the cutover itself (Phase 5 deletes the farm, AC-12 asserts no remnants) — no separate cleanup ticket needed. The work-queue freeze auto-expires when Phase 6 completes (unfreeze announced on the tracker, same comment thread as the freeze). ## Implementation Scope ### Areas | Area | Files / directories involved | Nature of change | | ---- | ---------------------------- | ---------------- | | Plugin scaffolding | `plugin/.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json` | new | | Restructure | `git mv .claude/skills/{<14>,_shared}` → `plugin/skills/` | move | | Reference rewrite | `plugin/skills/**` text (4 classes, allowlist, exclusions); rewrite script kept in `scripts/` for audit | modify / new | | Meta lane | `.claude/skills/meta-amend`, `.claude/skills/meta-retrospective` (D7 helper root, `/dev:` names for shipped-skill refs, lane-gate path update) | modify | | Lint gates | `scripts/lint-conventions.sh`: two roots, asserted non-zero file counts, adapter-exemption anchors, new rules (path scans D8, allowlist, `validate --strict`, D4 canonicalization ban, D12 hygiene) | extend | | Release tooling | `scripts/release.sh` (D6), `scripts/test-plugin-gates.sh` (refusal + validator + scan negative controls — **new suite, the integration-covered lane's named file**) | new | | Cutover tooling | `scripts/cutover-dev.sh`, `scripts/cutover-abort.sh` (D11 ordering, S-2 assertion, state probe, `~/.claude.json` backup/migration); desktop steps live in the README, not a script | new | | History scan (P0.8) | one-shot full-history sensitive-data scan before the first GitHub push; findings recorded on issue #50 | new (gate) | | Docs | `README.md` rewrite (prereqs, verbatim install/update/state-probe commands, old→new name table, failure modes with expected errors); consumer CLAUDE.md guidance blocks (castellan now; desktop consumers per P0.7 worklist at Phase 6); tracker + memory notes | modify | ### File Boundaries The restructure + rewrite happen in an **isolated git worktree** — the main checkout keeps serving the farm until cutover. `scripts/` and `.claude/skills/meta-*` are disjoint from `plugin/**` and can be worked in parallel with the rewrite. The README and consumer guidance blocks are independent of all code areas. `scripts/lint-conventions.sh` must be retargeted **in the same commit** as the restructure so lint is never blind between phases. ### Dependencies & Sequencing Preconditions (5 blocking decisions) → Phase 0 probes (P0.1 decisive; P0.8 history scan) → Phase 1 restructure + manifests + lint retarget (one commit; locks name `dev`, S-3) → Phase 2 reference rewrite → Phase 3 remaining gates + release/cutover tooling → Phase 4 coexistence dogfood (AC-5; fix-forward window per R7) → Phase 5 dev cutover (Scenario 8 rehearsal first; then D11 sequence; consumer guidance on this machine same-day) → Phase 6 release 0.1.0 + desktop cutover (README-driven, AC-11) → Phase 7 second release + update ritual (AC-9) and closing docs. Strictly sequential at phase granularity; parallelism only inside Phases 2–3. ## Constraints & Non-Goals **Constraints:** - Releases only from a clean tree (install provenance records HEAD while content ships from the working tree — verified hazard; enforced by `claude plugin tag`). - Plugin name `dev` locks before any reference rewrite (S-3). - Historical records keep pre-migration names verbatim (S-5) — audit integrity beats consistency. - Portability baseline unchanged: emitted glue stays POSIX; helper tier stays bash ≥ 3.2 + jq + git; no new interpreter dependencies. - No mutating write to any external system at plan time; the five unverified boundaries are gated, not assumed. **Non-goals (do NOT build):** - Public marketplace listing (operator's remotes only). - Automated eval suites for the plugin (`plugin eval`) — future issue. - Any skill logic change beyond the two sanctioned restorative touches (D7, D10). - Version-stamping dev-machine run records (desktop-only attribution, per PREQ). - Automating desktop updates; any desktop escape hatch for unreleased changes. - Renaming/restructuring the meta-lane skills beyond what the fence requires.
Author
Owner

Test Plan: ship-the-suite-as-a-claude-code-marketplace-plugin

Prerequisites

  • Two machines, in the roles the PREQ names:
    • the dev machine — where the suite is developed, released, and dogfooded; currently carries the symlink farm at ~/.claude/skills/ and the repo at ~/.claude/repos/devwork-skills;
    • the desktop (jochems-forge) — currently carries a second clone plus its own farm; becomes a pure consumer.
  • On each machine, the prerequisites the README is required to enumerate are present and checkable: bash ≥ 3.2, jq, git, and the project's forge CLI (tea for this repo, gh for castellan).
  • The GitHub remote github.com/jbr870/devwork-skills exists and is reachable from the desktop, with whatever credential path that remote requires available to the desktop operator.
  • The P0.1 probe outcome is known and affirmative — plugin skills are slash-invocable. If the probe says otherwise, AC-2 (and every scenario depending on /dev: invocation) is void and the run halts on a Pending Decision instead.
  • The work queue is frozen for the cutover scenarios: the meta-amend queue (#38–#49) and verity#237 are not in flight, and no feature is mid-pipeline on either machine.
  • A scratch copy of the repo and of the machine's pre-cutover state, usable for rehearsal and for negative controls without touching the real machines.
  • Consumer projects reachable on the relevant machine: castellan (gh-cli forge), and this repo itself (tea forge). The remaining consumers (verity, axana, outwrit, cunnan) need only their CLAUDE.md files present for inspection.
  • Both pre-cutover state (farm links, repo location, project-keyed runtime config) and a way to observe it are captured before any cutover scenario runs, so restoration can be judged.

Required Test Data

  • A scratch issue on this repo's tracker (tea forge), used as the target of the /dev:requirements phase in the coexistence run.
  • An issue on castellan's tracker (gh forge) able to receive one pipeline phase's records.
  • A tracker issue suitable for one meta-amend cycle — a finding that still holds at HEAD and needs only a text amendment.
  • Two consecutive releases of the plugin: the first at version 0.1.0, and a later one at a higher version, so the update path can be exercised.
  • Three separately prepared release-refusal states: (a) a working tree with uncommitted changes, (b) a tree where the plugin manifest's version field and the marketplace entry's version field disagree, (c) a tree whose version is already claimed by an existing tag.
  • A deliberately defective manifest on a scratch copy (one field wrong or missing) for the validator's negative control.
  • A scratch copy seeded with one instance of each violation class the mechanical scan is meant to catch: a ~/.claude/skills/ path reference, a _shared/ reference not prefixed by the plugin-root variable (one in a shell fence, one in prose), and a /dev:<x> reference naming a skill outside the 14.
  • The list of the 14 shipped skills, used verbatim as the checklist: develop, gh-cli, git-worktrees, glab-cli, integrate, local-fs, promote, requirements, resolve, setup, simplify-report, tea-cli, technical-plan, worktree-cleanup.
  • The rewritten README, and an operator (or a session) with no prior knowledge of the cutover steps beyond what the README says.

Test Scenarios

Scenario 1: Manifests pass strict validation with a clean bill

Acceptance criterion: AC-1 — "validation passes with zero errors and zero warnings"

Lane: integration-coveredscripts/lint-conventions.sh (the validate --strict gate rule this slice adds to the existing lint suite).

  1. Start from the restructured repo at a committed, clean state.
  2. Run the plugin tooling's validation of the plugin manifest and the marketplace manifest in its strict mode.
  3. Verify: the run reports zero errors.
  4. Verify: the run reports zero warnings — not "only warnings", not warnings that are waved off.
  5. Verify: the run exits successfully.

Expected outcome: Strict validation of both manifests is completely clean; nothing is deferred or explained away.

Scenario 2: The shipped plugin's contents scan clean, and the scan is proven non-vacuous

Acceptance criterion: AC-3 — "zero references to ~/.claude/skills/ paths, zero bare _shared/ invocations … and the mechanical scan itself is verified non-vacuous"

Lane: integration-coveredscripts/lint-conventions.sh (the AC-3 scan rules + asserted non-zero examined-file count, added by this slice).

  1. Take the shipped plugin's contents as they would be delivered.
  2. Run the mechanical scan over them.
  3. Verify: the scan reports how many files it examined, and that number is non-zero and plausibly matches the plugin's real file population (spot-check by counting files under the plugin root by hand and comparing).
  4. Verify: zero hits for ~/.claude/skills/ paths.
  5. Verify: zero _shared/ references that are not prefixed by the plugin-root variable — counting references in markdown prose, not only those inside shell fences.
  6. Verify: zero /dev:<x> references where <x> is outside the 14 shipped skill names.

Expected outcome: The scan looks at a real, non-empty population of files and finds none of the three violation classes.

Scenario 3: The meta lane runs by bare name in the repo and is absent from the shipped plugin

Acceptance criterion: AC-4 — "when the operator invokes /meta-amend or /meta-retrospective by bare name, then they run; and … no meta skill or repo-local script is present in it"

Lane: human-uat — owner: suite operator. Bare-name invocation is interactive-session behavior; the absence-from-plugin half is additionally covered mechanically by the lint scan (SREQ AC-4).

  1. Start a session in this repo.
  2. Invoke /meta-amend by its bare name.
  3. Verify: the invocation resolves and the skill begins its work (it identifies the issue/finding it is operating on, rather than reporting an unknown command).
  4. Invoke /meta-retrospective by its bare name.
  5. Verify: the invocation resolves and the skill begins its review.
  6. Inspect the shipped plugin's contents.
  7. Verify: neither meta skill is present anywhere in it.
  8. Verify: no repo-local script directory, analysis docs, run reports, or .devwork/ material is present in it.

Expected outcome: The meta lane is fully usable inside the repo by bare name, and entirely absent from what ships.

Scenario 4: A full requirements phase runs on a scratch tea issue with the plugin loaded pre-cutover

Acceptance criterion: AC-5 — "a full /dev:requirements phase runs on a scratch issue of this repo (tea forge) … with their forge records posted correctly"

Lane: human-uat — owner: suite operator, in the Phase 4 coexistence window (live tea-forge writes; interactive /dev: session).

  1. With the plugin form loaded alongside the still-present farm (bare names may still resolve in this window), open a session in this repo.
  2. Invoke /dev:requirements and run the phase through to completion against the scratch issue.
  3. Verify: the phase completes without the operator having to fall back to a bare-name invocation.
  4. Open the scratch issue on the tracker.
  5. Verify: a Phase Outcome is present on that issue (not another), and reads as a well-formed Phase Outcome — its fields are legible and its next-state is stated.
  6. Verify: the issue's phase label has advanced to the value the completed phase implies.

Expected outcome: The requirements phase runs from the plugin form and leaves correct, parseable forge records on the right issue.

Scenario 5: A worktree slot is created with the plugin loaded pre-cutover

Acceptance criterion: AC-5 — "(b) a worktree slot is created"

Lane: human-uat — owner: suite operator, in the Phase 4 coexistence window.

  1. In the same coexistence window, invoke the shipped worktree skill under its /dev: name.
  2. Create a worktree slot.
  3. Verify: the slot is created and the operator can enter it and see the repo's files there.
  4. Verify: any record the slot creation is meant to post is present and legible where the phase records live.
  5. Verify: no step of slot creation failed on a path that pointed into the old farm location.

Expected outcome: Slot creation succeeds from the plugin form, with its records intact.

Scenario 6: One meta-amend cycle runs with the plugin loaded pre-cutover

Acceptance criterion: AC-5 — "(c) one meta-amend cycle runs"

Lane: human-uat — owner: suite operator, in the Phase 4 coexistence window (exercises SREQ D7 meta helper root).

  1. In the same coexistence window, invoke /meta-amend (bare name — it is repo-local) against a prepared finding issue.
  2. Let the cycle run: verification of the finding, the text amendment, the mechanical gate, the desk-check, the commit.
  3. Verify: the cycle completes without an unresolvable path error — in particular, the helpers it reaches for are found.
  4. Verify: the outcome is recorded on the originating issue and is legible there.
  5. Verify: the amendment is committed in the repo.

Expected outcome: The repo-local meta lane still works end to end while the plugin form is loaded.

Scenario 7: A pipeline phase runs on castellan (gh forge) with the plugin loaded pre-cutover

Acceptance criterion: AC-5 — "(d) one pipeline phase runs on castellan (gh-cli — the non-tea adapter leg)"

Lane: human-uat — owner: suite operator, in the Phase 4 coexistence window (castellan, gh forge — live external writes; also exercises SREQ D10 adapter precedence).

  1. In the same coexistence window, open a session in castellan.
  2. Invoke one shipped pipeline phase under its /dev: name and run it to completion against the prepared castellan issue.
  3. Verify: the phase completes.
  4. Verify: a Phase Outcome is present and parseable on that castellan issue.
  5. Verify: the issue's phase label has advanced.
  6. Verify: the records were written through the gh forge path — the issue on GitHub carries them, and nothing was written to this repo's tracker by mistake.

Expected outcome: The non-tea adapter leg works identically from the plugin form.

Scenario 8: A rehearsed abort restores the pre-cutover state on a scratch copy

Acceptance criterion: AC-7 — "when the abort path runs, then the pre-cutover state is restored (farm links, repo location, project identity) — and this abort path has been rehearsed on a scratch copy before the real cutover"

Lane: human-uat — owner: suite operator, on the scratch copy before the real cutover; includes the SREQ S-2 negative test (cutover blocks while a feature is in flight).

  1. On a scratch copy of the dev machine's pre-cutover state, record: the set of farm links present, the repo's location, and the project-keyed identity state (memory available, trust already granted, MCP servers configured).
  2. Run the cutover on the scratch copy.
  3. Induce a failure of the cutover's own verification step (e.g. make one of the things it checks not hold).
  4. Verify: the cutover does not report success; it reports the verification failure plainly.
  5. Let the abort path run.
  6. Verify: the farm links are back, matching the recorded set.
  7. Verify: the repo is back at its pre-cutover location.
  8. Verify: opening a session against the restored repo shows project identity intact — memory loads, no trust re-prompt, previously configured MCP servers still listed.
  9. Verify: this rehearsal happened before the real cutover was attempted — the real cutover has not yet been run at this point in the plan.

Expected outcome: A failed cutover leaves the machine as it was, and this is demonstrated on a scratch copy before the real machine is touched.

Scenario 9: On the cut-over dev machine, all 14 shipped skills list and invoke under /dev:, and bare names do not resolve

Acceptance criterion: AC-2 — "all 14 appear in the listing and each invocation resolves — and no shipped skill resolves under its bare name"

Lane: human-uat — owner: suite operator (interactive listing + per-skill invocation on the cut-over dev machine).

  1. After the dev-machine cutover completes, start a fresh session on that machine with the plugin loaded in the dev machine's form.
  2. List the available skills.
  3. Verify: all 14 shipped skills appear in the listing under their /dev: names — develop, gh-cli, git-worktrees, glab-cli, integrate, local-fs, promote, requirements, resolve, setup, simplify-report, tea-cli, technical-plan, worktree-cleanup. Tick each one off the list individually.
  4. Invoke each of the 14 as /dev:<skill> in turn.
  5. Verify: each invocation resolves and the skill starts (it prints its own opening behavior rather than an unknown-command response). Cancel each once it is clearly running.
  6. Invoke a handful of the shipped skills by their bare names (e.g. requirements, develop, tea-cli).
  7. Verify: none of them resolve — the bare name is not recognized as a shipped skill.

Expected outcome: On the dev machine, shipped capability is reachable only through the /dev: namespace, and all 14 skills are there.

Scenario 10: The moved repo keeps its project identity

Acceptance criterion: AC-6 — "the suite works from the new location and project identity is preserved"

Lane: human-uat — owner: suite operator (session-level identity checks: memory, trust, MCP).

  1. After the dev-machine cutover, start a session with the working directory at ~/Coding/devwork-skills.
  2. Verify: the session's loaded memory is the repo's own memory — ask for something only that memory would supply and confirm it comes back.
  3. Verify: no trust prompt appears for this directory.
  4. Verify: the MCP servers that were configured for this project before the move are listed as available.
  5. Invoke one shipped skill under its /dev: name from this location.
  6. Verify: it resolves and runs, with no error mentioning the old ~/.claude/repos/ path.

Expected outcome: The repo works from its new home in the coding-projects root with memory, trust, and MCP configuration carried over.

Scenario 11: The dev machine shows no farm remnants and /dev:-only consumer guidance

Acceptance criterion: AC-12 — "no symlink-farm links … remain, every consumer-project CLAUDE.md guidance block on it references shipped skills by /dev: names only"

Lane: human-uat — owner: suite operator; step 8's non-change assertion is additionally covered mechanically by SREQ S-5 (git diff --stat over the exclusion set).

  1. On the cut-over dev machine, inspect the location where the symlink farm lived.
  2. Verify: none of the 15 farm links remain — not one, and no leftover links pointing into ~/.claude/repos/devwork-skills.
  3. Verify: nothing remains at the old repo path.
  4. For each consumer project present on this machine (castellan, verity, axana, outwrit, cunnan, and this repo), open its CLAUDE.md and read its guidance block.
  5. Verify: every reference to a shipped skill uses a /dev: name.
  6. Verify: no bare-name reference to any of the 14 shipped skills remains in those blocks.
  7. Verify: references to the repo-local meta skills (where present) are still bare names — the rewrite did not sweep them into /dev:.
  8. Verify: historical records (past run reports, prior Phase Outcomes, analysis docs) still carry their pre-migration names verbatim and were not rewritten.

Expected outcome: The dev machine has no farm left, live guidance points at /dev: names, and the audit trail is untouched.

Scenario 12: A clean, agreeing, untagged state releases 0.1.0

Acceptance criterion: AC-10 — "when a release is attempted, then the release tooling refuses…" (positive control for the three refusal conditions)

Lane: human-uat — owner: suite operator. A real release is a live external write (tag pushed to the GitHub remote) an automated run must not perform.

  1. Bring the repo to a clean working tree with no uncommitted changes.
  2. Confirm the plugin manifest's version field and the marketplace entry's version field both read 0.1.0, and that no tag yet claims that version.
  3. Attempt the release.
  4. Verify: the release tooling completes with a zero exit status.
  5. Verify: a tag for version 0.1.0 now exists.
  6. Verify: the released artifact is reachable from the GitHub remote as a marketplace source.

Expected outcome: A legitimate release goes through and produces a versioned 0.1.0 tag — establishing that the later refusals are refusals, not a tool that never works.

Scenario 13: The desktop cutover is executed from the README alone

Acceptance criterion: AC-11 — "the desktop cutover itself is executed following only the README's instructions; any step that needed outside knowledge is a README defect"

Lane: human-uat — executor: a fresh operator/session with no prior cutover exposure, keeping the per-step log the scenario requires.

  1. Give an operator (or a session) with no prior knowledge of the cutover plan only the rewritten README.
  2. Verify: the README enumerates the prerequisites — bash ≥ 3.2, jq, git, and the project's forge CLI — and the operator can check each one on the desktop from what the README says.
  3. Have the operator perform the desktop cutover following the README top to bottom, keeping a written log.
  4. Verify: at every step, the operator records whether they could proceed from the README alone, or had to consult something else (another document, the tracker, another person, trial and error).
  5. Verify: the cutover reaches a completed state.
  6. Verify: the log contains zero entries where outside knowledge was needed. Every such entry is filed as a README defect and must be fixed and the README re-walked before this scenario passes.

Expected outcome: The README is sufficient on its own to cut over the desktop; any gap is treated as a defect in the README rather than an operator problem.

Scenario 14: The desktop installs the released plugin and runs a helper-invoking phase

Acceptance criterion: AC-8 — "the install succeeds at the released version and the phase completes with its forge records posted"

Lane: human-uat — owner: suite operator, on the desktop (live install + consumer-project phase run).

  1. On the desktop, with the cutover done, install the plugin from the GitHub marketplace source.
  2. Verify: the install succeeds.
  3. Verify: the installed version reported by the desktop is the released version (0.1.0), not a branch, not "latest unknown".
  4. Open a session in a consumer project on the desktop and run one pipeline phase that invokes the suite's helpers.
  5. Verify: the phase completes — the helpers run, no step fails on a missing file or an unresolvable path inside the installed plugin.
  6. Verify: a Phase Outcome is present and parseable on the correct issue for that project.
  7. Verify: that issue's phase label has advanced.

Expected outcome: A released version installs cleanly on the desktop and does real work, including running its helpers from the installed form.

Scenario 15: On the desktop, all 14 shipped skills list and invoke under /dev:, and bare names do not resolve

Acceptance criterion: AC-2 — "Both listing and invocation are checked on each machine's form"

Lane: human-uat — owner: suite operator, on the desktop.

  1. On the desktop, with the plugin installed from the marketplace, start a fresh session.
  2. List the available skills.
  3. Verify: all 14 shipped skills appear under their /dev: names — tick each off individually.
  4. Invoke each of the 14 as /dev:<skill>; cancel each once it is clearly running.
  5. Verify: every invocation resolves.
  6. Invoke several shipped skills by bare name.
  7. Verify: none resolve.
  8. Invoke /meta-amend and /meta-retrospective by bare name on the desktop.
  9. Verify: neither resolves — the repo-local meta lane is unreachable from a consumer machine.

Expected outcome: The desktop's installed form exposes exactly the 14 shipped skills, only under /dev:, with no path to the meta lane.

Scenario 16: The desktop update ritual moves it from one release to the next

Acceptance criterion: AC-9 — "when the desktop performs the update ritual, then it runs the new version — the vN → vN+1 path is exercised"

Lane: human-uat — owner: suite operator, on the desktop (second release + update ritual).

  1. With the desktop already running the 0.1.0 install from Scenario 14, cut a second release from the dev machine at a higher version.
  2. On the desktop, perform the update ritual as the README describes it.
  3. Verify: the update succeeds.
  4. Verify: the version the desktop reports for the plugin is now the new version, not 0.1.0.
  5. Run one shipped skill under /dev: on the desktop.
  6. Verify: a change that exists only in the newer release is observably present in what runs (pick a visible, behavior-free marker introduced by the new release and confirm the running skill reflects it).
  7. Verify: the update did not require reinstalling from scratch or removing the previous install by hand.

Expected outcome: The desktop moves from one release to the next by a deliberate update, and demonstrably runs the newer one.

Scenario 17: The desktop shows no farm, no clone, /dev:-only guidance, and version-attributable run records

Acceptance criterion: AC-12 — "no symlink-farm links (and on the desktop, no suite clone) remain … and desktop run records are attributable to the installed release version"

Lane: human-uat — owner: suite operator, on the desktop (filesystem + record inspection).

  1. On the cut-over desktop, inspect where the symlink farm lived.
  2. Verify: no farm links remain.
  3. Verify: no clone of the suite repo remains anywhere on the desktop — the only copy of the suite is the installed plugin.
  4. For each consumer project on the desktop, read its CLAUDE.md guidance block.
  5. Verify: shipped skills are referenced by /dev: names only.
  6. Take a run record produced on the desktop (from Scenario 14 or 16).
  7. Verify: the record states which released version of the plugin produced it, and that version matches what the desktop reports as installed.
  8. Verify: the same is true for a record produced after the update — its stated version is the newer one, so records from the two runs are distinguishable.

Expected outcome: The desktop is a pure consumer with no local suite copy, and every run it produces can be traced to a specific release.

Scenario 18: A release from a dirty tree is refused

Acceptance criterion: AC-10 — "Given a dirty working tree … the release tooling refuses with a non-zero exit and no tag or install artifact is produced"

Lane: integration-coveredscripts/test-plugin-gates.sh (new suite authored by this slice — SREQ Implementation Scope, release tooling row; QA's lane-bootability check re-verifies it exists and runs).

  1. Prepare a repo state with a valid, agreeing, unclaimed version, then leave an uncommitted change in the working tree.
  2. Record the current set of tags.
  3. Attempt a release.
  4. Verify: the attempt fails with a non-zero exit.
  5. Verify: the message names the dirty tree as the reason — the operator can tell what to fix without guessing.
  6. Verify: the tag set is unchanged — no new tag was created.
  7. Verify: no install artifact was produced or published; the remote offers nothing new to install.

Expected outcome: A dirty tree blocks the release loudly and leaves nothing behind.

Scenario 19: A release with disagreeing manifest version fields is refused

Acceptance criterion: AC-10 — "disagreeing manifest version fields … the release tooling refuses"

Lane: integration-coveredscripts/test-plugin-gates.sh (same suite as Scenario 18).

  1. Prepare a clean working tree where the plugin manifest's version field and the marketplace entry's version field differ.
  2. Record the current set of tags.
  3. Attempt a release.
  4. Verify: the attempt fails with a non-zero exit.
  5. Verify: the message identifies the disagreement between the two version fields.
  6. Verify: no new tag exists.
  7. Verify: no install artifact was produced.

Expected outcome: The two manifests must agree on the version before anything ships; disagreement is caught, not silently resolved.

Scenario 20: A release reusing an existing version tag is refused

Acceptance criterion: AC-10 — "an already-existing version tag … the release tooling refuses"

Lane: integration-coveredscripts/test-plugin-gates.sh (same suite as Scenario 18).

  1. Prepare a clean working tree whose agreeing version fields name a version already claimed by an existing tag (e.g. 0.1.0 after Scenario 12).
  2. Record the existing tag and what it points at.
  3. Attempt a release.
  4. Verify: the attempt fails with a non-zero exit.
  5. Verify: the message identifies the version as already released.
  6. Verify: the existing tag still points at exactly what it pointed at before — it was not moved or overwritten.
  7. Verify: no install artifact was produced.

Expected outcome: A version number can be released once; a second attempt is refused without disturbing the first release.

Scenario 21: The desktop can fall back to the previous release from the remote

Acceptance criterion: AC-7 — "The desktop's previous release remains installable from the remote as its recovery path"

Lane: human-uat — owner: suite operator, on the desktop (reinstall of the previous release from the remote).

  1. With the desktop on the newer release (post-Scenario 16), treat that release as unusable.
  2. From the desktop, install the previous release version from the GitHub marketplace source.
  3. Verify: the install succeeds.
  4. Verify: the desktop reports the previous version as installed.
  5. Run one shipped skill under /dev:.
  6. Verify: it resolves and runs.
  7. Verify: this recovery required nothing but the remote — no clone, no artifact copied from the dev machine, no manual file placement.

Expected outcome: The desktop's recovery is a reinstall of the prior release from the remote, and it works.

Scenario 22: A desktop install with no working credential path fails loudly

Acceptance criterion: AC-8 — "when the desktop installs the plugin from the GitHub marketplace source" (failure edge of the install path)

Lane: human-uat — owner: suite operator, on the desktop (credential manipulation on a live machine).

  1. On the desktop, put the credential path to the GitHub remote out of reach (as it would be if the remote were private and the desktop unauthenticated).
  2. Attempt to install the plugin from the GitHub marketplace source.
  3. Verify: the attempt fails visibly, naming the access problem — it does not appear to succeed.
  4. Verify: no partially installed plugin is left behind — listing skills afterwards shows no /dev: skills from a half-install.
  5. Verify: no previously working install (if one was present) was damaged by the failed attempt.
  6. Restore the credential path and repeat the install.
  7. Verify: the install now succeeds at the released version.

Expected outcome: A missing credential path produces a clear failure and a clean machine, never a silently broken or half-present install.

Scenario 23: Strict validation actually rejects a defective manifest

Acceptance criterion: AC-1 — "validated with the plugin tooling's strict mode" (non-vacuity control)

Lane: integration-coveredscripts/test-plugin-gates.sh (validator negative controls on a scratch copy).

  1. On a scratch copy of the restructured repo, introduce one deliberate defect into the plugin manifest (remove a required field or give one an invalid value).
  2. Run the plugin tooling's strict validation.
  3. Verify: it reports at least one error and does not exit successfully.
  4. Verify: the message identifies the offending field.
  5. Repeat with a defect in the marketplace manifest instead.
  6. Verify: it likewise fails and points at the offending field.
  7. Discard the scratch copy.

Expected outcome: The clean result in Scenario 1 means something — the strict validator demonstrably fails a bad manifest and says which field is bad.

Scenario 24: The mechanical scan actually catches each violation class

Acceptance criterion: AC-3 — "the mechanical scan itself is verified non-vacuous" (per-class control)

Lane: integration-coveredscripts/test-plugin-gates.sh (per-class scan negative controls on a scratch copy).

  1. On a scratch copy of the shipped plugin contents, seed one ~/.claude/skills/ path reference into a skill file.
  2. Run the scan. Verify: it reports that hit and does not pass.
  3. Remove that seed; seed a _shared/ reference inside a shell fence that is not prefixed by the plugin-root variable.
  4. Run the scan. Verify: it reports that hit.
  5. Remove that seed; seed a _shared/ reference in plain markdown prose, not in any fence, without the plugin-root prefix.
  6. Run the scan. Verify: it reports that hit too — prose is scanned, not only fences.
  7. Remove that seed; seed a /dev: reference naming a skill outside the 14 (including one naming a meta skill).
  8. Run the scan. Verify: it reports that hit.
  9. Remove all seeds and re-run. Verify: the scan is clean again and still reports a non-zero examined-file count.

Expected outcome: Each of the three violation classes is provably detected, including the prose-not-fence case, so a clean scan is evidence rather than silence.

Notes

  • AC-2 is contingent. Every scenario that invokes a shipped skill as /dev:<skill> (9, 14, 15, 16, 21, and the coexistence runs 4–7) presumes the P0.1 probe confirmed that plugin skills are slash-invocable. If P0.1 comes back negative, these scenarios cannot be executed as written and the feature halts on a Pending Decision instead of failing these cases.
  • Scenarios 4–7 must run before any cutover, in the window where the plugin form and the farm coexist and bare names may still resolve. Their value is checking that plugin-form execution works while the old form is still available as a fallback — running them after cutover would not exercise the same thing. They also depend on P0.2 (coexistence) actually being possible; if the two forms cannot coexist, the pre-cutover leg of AC-5 has no window to run in.
  • Scenario 8 must complete before Scenarios 9–11. The rehearsal is a precondition of the real cutover, not a parallel activity.
  • Scenario 12 must run before 20, since 20 depends on 0.1.0 already being claimed. Scenario 16 depends on 14.
  • AC-11's judgment is self-reported. "Needed outside knowledge" is assessed by the person executing; the safeguard is choosing an executor with no prior exposure to the cutover plan and requiring a written per-step log rather than a summary verdict.
  • The freeze is a precondition, not a step. Scenarios 8–17 assume no feature is in flight on either machine; if the cutover asserts this itself and the assertion fails, that is a blocked run, not a failed scenario here.
  • Historical records are checked for non-change (Scenario 11 step 8) because the constraint is that pre-migration names stay verbatim — a rewrite that "helpfully" updates old run reports is a defect, not an improvement.
  • No browser surface exists. Every scenario is executed by an operator at a terminal, against the filesystem, the CLI, and the two forges.
  • Two load forms remain by design (skills-dir on the dev machine, marketplace cache on the desktop). Scenario 9 covers the first, Scenarios 14–16 the second; a regression specific to one form will not be caught by the other, which is why both machines get their own listing-and-invocation check.
  • Lane annotation (added by /technical-plan, 2026-08-15): scenarios are PREQ-derived; lanes are SREQ-derived. The e2e-browser lane is not assignable in this project — qa_domains.not_applicable declares the e2e domain has no surface here (markdown+shell suite, no browser). Mechanical scenarios route to integration-covered (scripts/lint-conventions.sh exists and runs today; scripts/test-plugin-gates.sh is authored by this slice and re-checked by QA's lane-bootability gate); everything requiring an interactive session, a live machine cutover, or a live external write routes to human-uat with the owner named per scenario.
<!-- test-plan:v1 issue=50 skill=technical-plan --> # Test Plan: ship-the-suite-as-a-claude-code-marketplace-plugin ## Prerequisites - **Two machines**, in the roles the PREQ names: - the **dev machine** — where the suite is developed, released, and dogfooded; currently carries the symlink farm at `~/.claude/skills/` and the repo at `~/.claude/repos/devwork-skills`; - the **desktop (jochems-forge)** — currently carries a second clone plus its own farm; becomes a pure consumer. - On each machine, the prerequisites the README is required to enumerate are present and checkable: **bash ≥ 3.2, `jq`, `git`, and the project's forge CLI** (`tea` for this repo, `gh` for castellan). - The **GitHub remote** `github.com/jbr870/devwork-skills` exists and is reachable from the desktop, with whatever credential path that remote requires available to the desktop operator. - The **P0.1 probe outcome is known and affirmative** — plugin skills are slash-invocable. If the probe says otherwise, AC-2 (and every scenario depending on `/dev:` invocation) is void and the run halts on a Pending Decision instead. - The **work queue is frozen** for the cutover scenarios: the meta-amend queue (#38–#49) and verity#237 are not in flight, and no feature is mid-pipeline on either machine. - A **scratch copy** of the repo and of the machine's pre-cutover state, usable for rehearsal and for negative controls without touching the real machines. - Consumer projects reachable on the relevant machine: **castellan** (gh-cli forge), and this repo itself (tea forge). The remaining consumers (verity, axana, outwrit, cunnan) need only their CLAUDE.md files present for inspection. - Both **pre-cutover state** (farm links, repo location, project-keyed runtime config) and a way to observe it are captured before any cutover scenario runs, so restoration can be judged. ### Required Test Data - **A scratch issue on this repo's tracker** (tea forge), used as the target of the `/dev:requirements` phase in the coexistence run. - **An issue on castellan's tracker** (gh forge) able to receive one pipeline phase's records. - **A tracker issue suitable for one meta-amend cycle** — a finding that still holds at HEAD and needs only a text amendment. - **Two consecutive releases** of the plugin: the first at version **0.1.0**, and a later one at a higher version, so the update path can be exercised. - **Three separately prepared release-refusal states**: (a) a working tree with uncommitted changes, (b) a tree where the plugin manifest's version field and the marketplace entry's version field disagree, (c) a tree whose version is already claimed by an existing tag. - **A deliberately defective manifest** on a scratch copy (one field wrong or missing) for the validator's negative control. - **A scratch copy seeded with one instance of each violation class** the mechanical scan is meant to catch: a `~/.claude/skills/` path reference, a `_shared/` reference not prefixed by the plugin-root variable (one in a shell fence, one in prose), and a `/dev:<x>` reference naming a skill outside the 14. - **The list of the 14 shipped skills**, used verbatim as the checklist: develop, gh-cli, git-worktrees, glab-cli, integrate, local-fs, promote, requirements, resolve, setup, simplify-report, tea-cli, technical-plan, worktree-cleanup. - **The rewritten README**, and an operator (or a session) with no prior knowledge of the cutover steps beyond what the README says. ## Test Scenarios ### Scenario 1: Manifests pass strict validation with a clean bill **Acceptance criterion:** AC-1 — "validation passes with zero errors and zero warnings" **Lane:** `integration-covered` — `scripts/lint-conventions.sh` (the `validate --strict` gate rule this slice adds to the existing lint suite). 1. Start from the restructured repo at a committed, clean state. 2. Run the plugin tooling's validation of the plugin manifest and the marketplace manifest in its strict mode. 3. Verify: the run reports **zero errors**. 4. Verify: the run reports **zero warnings** — not "only warnings", not warnings that are waved off. 5. Verify: the run exits successfully. **Expected outcome:** Strict validation of both manifests is completely clean; nothing is deferred or explained away. ### Scenario 2: The shipped plugin's contents scan clean, and the scan is proven non-vacuous **Acceptance criterion:** AC-3 — "zero references to `~/.claude/skills/` paths, zero bare `_shared/` invocations … and the mechanical scan itself is verified non-vacuous" **Lane:** `integration-covered` — `scripts/lint-conventions.sh` (the AC-3 scan rules + asserted non-zero examined-file count, added by this slice). 1. Take the shipped plugin's contents as they would be delivered. 2. Run the mechanical scan over them. 3. Verify: the scan reports **how many files it examined**, and that number is non-zero and plausibly matches the plugin's real file population (spot-check by counting files under the plugin root by hand and comparing). 4. Verify: **zero** hits for `~/.claude/skills/` paths. 5. Verify: **zero** `_shared/` references that are not prefixed by the plugin-root variable — counting references in markdown prose, not only those inside shell fences. 6. Verify: **zero** `/dev:<x>` references where `<x>` is outside the 14 shipped skill names. **Expected outcome:** The scan looks at a real, non-empty population of files and finds none of the three violation classes. ### Scenario 3: The meta lane runs by bare name in the repo and is absent from the shipped plugin **Acceptance criterion:** AC-4 — "when the operator invokes `/meta-amend` or `/meta-retrospective` by bare name, then they run; and … no meta skill or repo-local script is present in it" **Lane:** `human-uat` — owner: suite operator. Bare-name invocation is interactive-session behavior; the absence-from-plugin half is additionally covered mechanically by the lint scan (SREQ AC-4). 1. Start a session in this repo. 2. Invoke `/meta-amend` by its bare name. 3. Verify: the invocation resolves and the skill begins its work (it identifies the issue/finding it is operating on, rather than reporting an unknown command). 4. Invoke `/meta-retrospective` by its bare name. 5. Verify: the invocation resolves and the skill begins its review. 6. Inspect the shipped plugin's contents. 7. Verify: neither meta skill is present anywhere in it. 8. Verify: no repo-local script directory, analysis docs, run reports, or `.devwork/` material is present in it. **Expected outcome:** The meta lane is fully usable inside the repo by bare name, and entirely absent from what ships. ### Scenario 4: A full requirements phase runs on a scratch tea issue with the plugin loaded pre-cutover **Acceptance criterion:** AC-5 — "a full `/dev:requirements` phase runs on a scratch issue of this repo (tea forge) … with their forge records posted correctly" **Lane:** `human-uat` — owner: suite operator, in the Phase 4 coexistence window (live tea-forge writes; interactive `/dev:` session). 1. With the plugin form loaded alongside the still-present farm (bare names may still resolve in this window), open a session in this repo. 2. Invoke `/dev:requirements` and run the phase through to completion against the scratch issue. 3. Verify: the phase completes without the operator having to fall back to a bare-name invocation. 4. Open the scratch issue on the tracker. 5. Verify: a Phase Outcome is present on **that** issue (not another), and reads as a well-formed Phase Outcome — its fields are legible and its next-state is stated. 6. Verify: the issue's phase label has advanced to the value the completed phase implies. **Expected outcome:** The requirements phase runs from the plugin form and leaves correct, parseable forge records on the right issue. ### Scenario 5: A worktree slot is created with the plugin loaded pre-cutover **Acceptance criterion:** AC-5 — "(b) a worktree slot is created" **Lane:** `human-uat` — owner: suite operator, in the Phase 4 coexistence window. 1. In the same coexistence window, invoke the shipped worktree skill under its `/dev:` name. 2. Create a worktree slot. 3. Verify: the slot is created and the operator can enter it and see the repo's files there. 4. Verify: any record the slot creation is meant to post is present and legible where the phase records live. 5. Verify: no step of slot creation failed on a path that pointed into the old farm location. **Expected outcome:** Slot creation succeeds from the plugin form, with its records intact. ### Scenario 6: One meta-amend cycle runs with the plugin loaded pre-cutover **Acceptance criterion:** AC-5 — "(c) one meta-amend cycle runs" **Lane:** `human-uat` — owner: suite operator, in the Phase 4 coexistence window (exercises SREQ D7 meta helper root). 1. In the same coexistence window, invoke `/meta-amend` (bare name — it is repo-local) against a prepared finding issue. 2. Let the cycle run: verification of the finding, the text amendment, the mechanical gate, the desk-check, the commit. 3. Verify: the cycle completes without an unresolvable path error — in particular, the helpers it reaches for are found. 4. Verify: the outcome is recorded on the originating issue and is legible there. 5. Verify: the amendment is committed in the repo. **Expected outcome:** The repo-local meta lane still works end to end while the plugin form is loaded. ### Scenario 7: A pipeline phase runs on castellan (gh forge) with the plugin loaded pre-cutover **Acceptance criterion:** AC-5 — "(d) one pipeline phase runs on castellan (gh-cli — the non-tea adapter leg)" **Lane:** `human-uat` — owner: suite operator, in the Phase 4 coexistence window (castellan, gh forge — live external writes; also exercises SREQ D10 adapter precedence). 1. In the same coexistence window, open a session in castellan. 2. Invoke one shipped pipeline phase under its `/dev:` name and run it to completion against the prepared castellan issue. 3. Verify: the phase completes. 4. Verify: a Phase Outcome is present and parseable on that castellan issue. 5. Verify: the issue's phase label has advanced. 6. Verify: the records were written through the gh forge path — the issue on GitHub carries them, and nothing was written to this repo's tracker by mistake. **Expected outcome:** The non-tea adapter leg works identically from the plugin form. ### Scenario 8: A rehearsed abort restores the pre-cutover state on a scratch copy **Acceptance criterion:** AC-7 — "when the abort path runs, then the pre-cutover state is restored (farm links, repo location, project identity) — and this abort path has been rehearsed on a scratch copy before the real cutover" **Lane:** `human-uat` — owner: suite operator, on the scratch copy before the real cutover; includes the SREQ S-2 negative test (cutover blocks while a feature is in flight). 1. On a scratch copy of the dev machine's pre-cutover state, record: the set of farm links present, the repo's location, and the project-keyed identity state (memory available, trust already granted, MCP servers configured). 2. Run the cutover on the scratch copy. 3. Induce a failure of the cutover's own verification step (e.g. make one of the things it checks not hold). 4. Verify: the cutover does not report success; it reports the verification failure plainly. 5. Let the abort path run. 6. Verify: the farm links are back, matching the recorded set. 7. Verify: the repo is back at its pre-cutover location. 8. Verify: opening a session against the restored repo shows project identity intact — memory loads, no trust re-prompt, previously configured MCP servers still listed. 9. Verify: this rehearsal happened **before** the real cutover was attempted — the real cutover has not yet been run at this point in the plan. **Expected outcome:** A failed cutover leaves the machine as it was, and this is demonstrated on a scratch copy before the real machine is touched. ### Scenario 9: On the cut-over dev machine, all 14 shipped skills list and invoke under `/dev:`, and bare names do not resolve **Acceptance criterion:** AC-2 — "all 14 appear in the listing and each invocation resolves — and no shipped skill resolves under its bare name" **Lane:** `human-uat` — owner: suite operator (interactive listing + per-skill invocation on the cut-over dev machine). 1. After the dev-machine cutover completes, start a fresh session on that machine with the plugin loaded in the dev machine's form. 2. List the available skills. 3. Verify: all 14 shipped skills appear in the listing under their `/dev:` names — develop, gh-cli, git-worktrees, glab-cli, integrate, local-fs, promote, requirements, resolve, setup, simplify-report, tea-cli, technical-plan, worktree-cleanup. Tick each one off the list individually. 4. Invoke each of the 14 as `/dev:<skill>` in turn. 5. Verify: each invocation resolves and the skill starts (it prints its own opening behavior rather than an unknown-command response). Cancel each once it is clearly running. 6. Invoke a handful of the shipped skills by their **bare** names (e.g. `requirements`, `develop`, `tea-cli`). 7. Verify: none of them resolve — the bare name is not recognized as a shipped skill. **Expected outcome:** On the dev machine, shipped capability is reachable only through the `/dev:` namespace, and all 14 skills are there. ### Scenario 10: The moved repo keeps its project identity **Acceptance criterion:** AC-6 — "the suite works from the new location and project identity is preserved" **Lane:** `human-uat` — owner: suite operator (session-level identity checks: memory, trust, MCP). 1. After the dev-machine cutover, start a session with the working directory at `~/Coding/devwork-skills`. 2. Verify: the session's loaded memory is the repo's own memory — ask for something only that memory would supply and confirm it comes back. 3. Verify: **no trust prompt appears** for this directory. 4. Verify: the MCP servers that were configured for this project before the move are listed as available. 5. Invoke one shipped skill under its `/dev:` name from this location. 6. Verify: it resolves and runs, with no error mentioning the old `~/.claude/repos/` path. **Expected outcome:** The repo works from its new home in the coding-projects root with memory, trust, and MCP configuration carried over. ### Scenario 11: The dev machine shows no farm remnants and `/dev:`-only consumer guidance **Acceptance criterion:** AC-12 — "no symlink-farm links … remain, every consumer-project CLAUDE.md guidance block on it references shipped skills by `/dev:` names only" **Lane:** `human-uat` — owner: suite operator; step 8's non-change assertion is additionally covered mechanically by SREQ S-5 (`git diff --stat` over the exclusion set). 1. On the cut-over dev machine, inspect the location where the symlink farm lived. 2. Verify: none of the 15 farm links remain — not one, and no leftover links pointing into `~/.claude/repos/devwork-skills`. 3. Verify: nothing remains at the old repo path. 4. For each consumer project present on this machine (castellan, verity, axana, outwrit, cunnan, and this repo), open its CLAUDE.md and read its guidance block. 5. Verify: every reference to a shipped skill uses a `/dev:` name. 6. Verify: no bare-name reference to any of the 14 shipped skills remains in those blocks. 7. Verify: references to the repo-local meta skills (where present) are still bare names — the rewrite did not sweep them into `/dev:`. 8. Verify: historical records (past run reports, prior Phase Outcomes, analysis docs) still carry their pre-migration names verbatim and were **not** rewritten. **Expected outcome:** The dev machine has no farm left, live guidance points at `/dev:` names, and the audit trail is untouched. ### Scenario 12: A clean, agreeing, untagged state releases 0.1.0 **Acceptance criterion:** AC-10 — "when a release is attempted, then the release tooling refuses…" (positive control for the three refusal conditions) **Lane:** `human-uat` — owner: suite operator. A real release is a live external write (tag pushed to the GitHub remote) an automated run must not perform. 1. Bring the repo to a clean working tree with no uncommitted changes. 2. Confirm the plugin manifest's version field and the marketplace entry's version field both read `0.1.0`, and that no tag yet claims that version. 3. Attempt the release. 4. Verify: the release tooling completes with a zero exit status. 5. Verify: a tag for version `0.1.0` now exists. 6. Verify: the released artifact is reachable from the GitHub remote as a marketplace source. **Expected outcome:** A legitimate release goes through and produces a versioned `0.1.0` tag — establishing that the later refusals are refusals, not a tool that never works. ### Scenario 13: The desktop cutover is executed from the README alone **Acceptance criterion:** AC-11 — "the desktop cutover itself is executed following only the README's instructions; any step that needed outside knowledge is a README defect" **Lane:** `human-uat` — executor: a fresh operator/session with no prior cutover exposure, keeping the per-step log the scenario requires. 1. Give an operator (or a session) with no prior knowledge of the cutover plan **only** the rewritten README. 2. Verify: the README enumerates the prerequisites — bash ≥ 3.2, `jq`, `git`, and the project's forge CLI — and the operator can check each one on the desktop from what the README says. 3. Have the operator perform the desktop cutover following the README top to bottom, keeping a written log. 4. Verify: at every step, the operator records whether they could proceed from the README alone, or had to consult something else (another document, the tracker, another person, trial and error). 5. Verify: the cutover reaches a completed state. 6. Verify: the log contains **zero** entries where outside knowledge was needed. Every such entry is filed as a README defect and must be fixed and the README re-walked before this scenario passes. **Expected outcome:** The README is sufficient on its own to cut over the desktop; any gap is treated as a defect in the README rather than an operator problem. ### Scenario 14: The desktop installs the released plugin and runs a helper-invoking phase **Acceptance criterion:** AC-8 — "the install succeeds at the released version and the phase completes with its forge records posted" **Lane:** `human-uat` — owner: suite operator, on the desktop (live install + consumer-project phase run). 1. On the desktop, with the cutover done, install the plugin from the GitHub marketplace source. 2. Verify: the install succeeds. 3. Verify: the installed version reported by the desktop is the released version (`0.1.0`), not a branch, not "latest unknown". 4. Open a session in a consumer project on the desktop and run one pipeline phase that invokes the suite's helpers. 5. Verify: the phase completes — the helpers run, no step fails on a missing file or an unresolvable path inside the installed plugin. 6. Verify: a Phase Outcome is present and parseable on the correct issue for that project. 7. Verify: that issue's phase label has advanced. **Expected outcome:** A released version installs cleanly on the desktop and does real work, including running its helpers from the installed form. ### Scenario 15: On the desktop, all 14 shipped skills list and invoke under `/dev:`, and bare names do not resolve **Acceptance criterion:** AC-2 — "Both listing and invocation are checked on each machine's form" **Lane:** `human-uat` — owner: suite operator, on the desktop. 1. On the desktop, with the plugin installed from the marketplace, start a fresh session. 2. List the available skills. 3. Verify: all 14 shipped skills appear under their `/dev:` names — tick each off individually. 4. Invoke each of the 14 as `/dev:<skill>`; cancel each once it is clearly running. 5. Verify: every invocation resolves. 6. Invoke several shipped skills by bare name. 7. Verify: none resolve. 8. Invoke `/meta-amend` and `/meta-retrospective` by bare name on the desktop. 9. Verify: neither resolves — the repo-local meta lane is unreachable from a consumer machine. **Expected outcome:** The desktop's installed form exposes exactly the 14 shipped skills, only under `/dev:`, with no path to the meta lane. ### Scenario 16: The desktop update ritual moves it from one release to the next **Acceptance criterion:** AC-9 — "when the desktop performs the update ritual, then it runs the new version — the vN → vN+1 path is exercised" **Lane:** `human-uat` — owner: suite operator, on the desktop (second release + update ritual). 1. With the desktop already running the `0.1.0` install from Scenario 14, cut a second release from the dev machine at a higher version. 2. On the desktop, perform the update ritual as the README describes it. 3. Verify: the update succeeds. 4. Verify: the version the desktop reports for the plugin is now the **new** version, not `0.1.0`. 5. Run one shipped skill under `/dev:` on the desktop. 6. Verify: a change that exists only in the newer release is observably present in what runs (pick a visible, behavior-free marker introduced by the new release and confirm the running skill reflects it). 7. Verify: the update did not require reinstalling from scratch or removing the previous install by hand. **Expected outcome:** The desktop moves from one release to the next by a deliberate update, and demonstrably runs the newer one. ### Scenario 17: The desktop shows no farm, no clone, `/dev:`-only guidance, and version-attributable run records **Acceptance criterion:** AC-12 — "no symlink-farm links (and on the desktop, no suite clone) remain … and desktop run records are attributable to the installed release version" **Lane:** `human-uat` — owner: suite operator, on the desktop (filesystem + record inspection). 1. On the cut-over desktop, inspect where the symlink farm lived. 2. Verify: no farm links remain. 3. Verify: **no clone of the suite repo remains anywhere on the desktop** — the only copy of the suite is the installed plugin. 4. For each consumer project on the desktop, read its CLAUDE.md guidance block. 5. Verify: shipped skills are referenced by `/dev:` names only. 6. Take a run record produced on the desktop (from Scenario 14 or 16). 7. Verify: the record states which released version of the plugin produced it, and that version matches what the desktop reports as installed. 8. Verify: the same is true for a record produced after the update — its stated version is the newer one, so records from the two runs are distinguishable. **Expected outcome:** The desktop is a pure consumer with no local suite copy, and every run it produces can be traced to a specific release. ### Scenario 18: A release from a dirty tree is refused **Acceptance criterion:** AC-10 — "Given a dirty working tree … the release tooling refuses with a non-zero exit and no tag or install artifact is produced" **Lane:** `integration-covered` — `scripts/test-plugin-gates.sh` (new suite authored by this slice — SREQ Implementation Scope, release tooling row; QA's lane-bootability check re-verifies it exists and runs). 1. Prepare a repo state with a valid, agreeing, unclaimed version, then leave an uncommitted change in the working tree. 2. Record the current set of tags. 3. Attempt a release. 4. Verify: the attempt fails with a **non-zero exit**. 5. Verify: the message names the dirty tree as the reason — the operator can tell what to fix without guessing. 6. Verify: the tag set is unchanged — no new tag was created. 7. Verify: no install artifact was produced or published; the remote offers nothing new to install. **Expected outcome:** A dirty tree blocks the release loudly and leaves nothing behind. ### Scenario 19: A release with disagreeing manifest version fields is refused **Acceptance criterion:** AC-10 — "disagreeing manifest version fields … the release tooling refuses" **Lane:** `integration-covered` — `scripts/test-plugin-gates.sh` (same suite as Scenario 18). 1. Prepare a clean working tree where the plugin manifest's version field and the marketplace entry's version field differ. 2. Record the current set of tags. 3. Attempt a release. 4. Verify: the attempt fails with a **non-zero exit**. 5. Verify: the message identifies the disagreement between the two version fields. 6. Verify: no new tag exists. 7. Verify: no install artifact was produced. **Expected outcome:** The two manifests must agree on the version before anything ships; disagreement is caught, not silently resolved. ### Scenario 20: A release reusing an existing version tag is refused **Acceptance criterion:** AC-10 — "an already-existing version tag … the release tooling refuses" **Lane:** `integration-covered` — `scripts/test-plugin-gates.sh` (same suite as Scenario 18). 1. Prepare a clean working tree whose agreeing version fields name a version already claimed by an existing tag (e.g. `0.1.0` after Scenario 12). 2. Record the existing tag and what it points at. 3. Attempt a release. 4. Verify: the attempt fails with a **non-zero exit**. 5. Verify: the message identifies the version as already released. 6. Verify: the existing tag still points at exactly what it pointed at before — it was not moved or overwritten. 7. Verify: no install artifact was produced. **Expected outcome:** A version number can be released once; a second attempt is refused without disturbing the first release. ### Scenario 21: The desktop can fall back to the previous release from the remote **Acceptance criterion:** AC-7 — "The desktop's previous release remains installable from the remote as its recovery path" **Lane:** `human-uat` — owner: suite operator, on the desktop (reinstall of the previous release from the remote). 1. With the desktop on the newer release (post-Scenario 16), treat that release as unusable. 2. From the desktop, install the **previous** release version from the GitHub marketplace source. 3. Verify: the install succeeds. 4. Verify: the desktop reports the previous version as installed. 5. Run one shipped skill under `/dev:`. 6. Verify: it resolves and runs. 7. Verify: this recovery required nothing but the remote — no clone, no artifact copied from the dev machine, no manual file placement. **Expected outcome:** The desktop's recovery is a reinstall of the prior release from the remote, and it works. ### Scenario 22: A desktop install with no working credential path fails loudly **Acceptance criterion:** AC-8 — "when the desktop installs the plugin from the GitHub marketplace source" (failure edge of the install path) **Lane:** `human-uat` — owner: suite operator, on the desktop (credential manipulation on a live machine). 1. On the desktop, put the credential path to the GitHub remote out of reach (as it would be if the remote were private and the desktop unauthenticated). 2. Attempt to install the plugin from the GitHub marketplace source. 3. Verify: the attempt **fails visibly**, naming the access problem — it does not appear to succeed. 4. Verify: no partially installed plugin is left behind — listing skills afterwards shows no `/dev:` skills from a half-install. 5. Verify: no previously working install (if one was present) was damaged by the failed attempt. 6. Restore the credential path and repeat the install. 7. Verify: the install now succeeds at the released version. **Expected outcome:** A missing credential path produces a clear failure and a clean machine, never a silently broken or half-present install. ### Scenario 23: Strict validation actually rejects a defective manifest **Acceptance criterion:** AC-1 — "validated with the plugin tooling's strict mode" (non-vacuity control) **Lane:** `integration-covered` — `scripts/test-plugin-gates.sh` (validator negative controls on a scratch copy). 1. On a scratch copy of the restructured repo, introduce one deliberate defect into the plugin manifest (remove a required field or give one an invalid value). 2. Run the plugin tooling's strict validation. 3. Verify: it reports at least one error and does **not** exit successfully. 4. Verify: the message identifies the offending field. 5. Repeat with a defect in the marketplace manifest instead. 6. Verify: it likewise fails and points at the offending field. 7. Discard the scratch copy. **Expected outcome:** The clean result in Scenario 1 means something — the strict validator demonstrably fails a bad manifest and says which field is bad. ### Scenario 24: The mechanical scan actually catches each violation class **Acceptance criterion:** AC-3 — "the mechanical scan itself is verified non-vacuous" (per-class control) **Lane:** `integration-covered` — `scripts/test-plugin-gates.sh` (per-class scan negative controls on a scratch copy). 1. On a scratch copy of the shipped plugin contents, seed one `~/.claude/skills/` path reference into a skill file. 2. Run the scan. Verify: it reports that hit and does not pass. 3. Remove that seed; seed a `_shared/` reference inside a shell fence that is **not** prefixed by the plugin-root variable. 4. Run the scan. Verify: it reports that hit. 5. Remove that seed; seed a `_shared/` reference in plain markdown prose, not in any fence, without the plugin-root prefix. 6. Run the scan. Verify: it reports that hit too — prose is scanned, not only fences. 7. Remove that seed; seed a `/dev:` reference naming a skill outside the 14 (including one naming a meta skill). 8. Run the scan. Verify: it reports that hit. 9. Remove all seeds and re-run. Verify: the scan is clean again and still reports a non-zero examined-file count. **Expected outcome:** Each of the three violation classes is provably detected, including the prose-not-fence case, so a clean scan is evidence rather than silence. ## Notes - **AC-2 is contingent.** Every scenario that invokes a shipped skill as `/dev:<skill>` (9, 14, 15, 16, 21, and the coexistence runs 4–7) presumes the P0.1 probe confirmed that plugin skills are slash-invocable. If P0.1 comes back negative, these scenarios cannot be executed as written and the feature halts on a Pending Decision instead of failing these cases. - **Scenarios 4–7 must run before any cutover**, in the window where the plugin form and the farm coexist and bare names may still resolve. Their value is checking that plugin-form execution works *while the old form is still available as a fallback* — running them after cutover would not exercise the same thing. They also depend on P0.2 (coexistence) actually being possible; if the two forms cannot coexist, the pre-cutover leg of AC-5 has no window to run in. - **Scenario 8 must complete before Scenarios 9–11.** The rehearsal is a precondition of the real cutover, not a parallel activity. - **Scenario 12 must run before 20**, since 20 depends on `0.1.0` already being claimed. Scenario 16 depends on 14. - **AC-11's judgment is self-reported.** "Needed outside knowledge" is assessed by the person executing; the safeguard is choosing an executor with no prior exposure to the cutover plan and requiring a written per-step log rather than a summary verdict. - **The freeze is a precondition, not a step.** Scenarios 8–17 assume no feature is in flight on either machine; if the cutover asserts this itself and the assertion fails, that is a blocked run, not a failed scenario here. - **Historical records are checked for non-change** (Scenario 11 step 8) because the constraint is that pre-migration names stay verbatim — a rewrite that "helpfully" updates old run reports is a defect, not an improvement. - **No browser surface exists.** Every scenario is executed by an operator at a terminal, against the filesystem, the CLI, and the two forges. - **Two load forms remain by design** (skills-dir on the dev machine, marketplace cache on the desktop). Scenario 9 covers the first, Scenarios 14–16 the second; a regression specific to one form will not be caught by the other, which is why both machines get their own listing-and-invocation check. - **Lane annotation (added by /technical-plan, 2026-08-15):** scenarios are PREQ-derived; lanes are SREQ-derived. The `e2e-browser` lane is not assignable in this project — `qa_domains.not_applicable` declares the e2e domain has no surface here (markdown+shell suite, no browser). Mechanical scenarios route to `integration-covered` (`scripts/lint-conventions.sh` exists and runs today; `scripts/test-plugin-gates.sh` is authored by this slice and re-checked by QA's lane-bootability gate); everything requiring an interactive session, a live machine cutover, or a live external write routes to `human-uat` with the owner named per scenario.
Author
Owner
{
  "next_state": "developing",
  "produced": [
    {
      "kind": "sreq",
      "ref": "comment:975",
      "summary": "SREQ for ship-the-suite-as-a-claude-code-marketplace-plugin (Tier 3)"
    },
    {
      "kind": "test-plan",
      "ref": "comment:978",
      "summary": "Test plan for ship-the-suite-as-a-claude-code-marketplace-plugin — lanes annotated: 7 integration-covered, 17 human-uat"
    }
  ],
  "findings": [
    {
      "category": "out-of-scope",
      "summary": "Publishing the plugin beyond the operator's own remotes (no public marketplace listing)",
      "reasoning": "Flagged as out of scope in the approved PREQ.",
      "proposed_action": "defer-to-issue",
      "target": null,
      "id": "F-PO-50-2-1"
    },
    {
      "category": "out-of-scope",
      "summary": "Automated eval suites for the plugin (plugin eval) — future issue",
      "reasoning": "Flagged as out of scope in the approved PREQ.",
      "proposed_action": "defer-to-issue",
      "target": null,
      "id": "F-PO-50-2-2"
    },
    {
      "category": "out-of-scope",
      "summary": "Any skill logic change beyond the two sanctioned restorative text touches (meta-helper path fix, adapter discovery-order hand-rewrite)",
      "reasoning": "Flagged as out of scope in the approved PREQ.",
      "proposed_action": "defer-to-issue",
      "target": null,
      "id": "F-PO-50-2-3"
    },
    {
      "category": "out-of-scope",
      "summary": "Version-stamping dev-machine run records (git is the dev machine's version identity; release attribution is desktop-only)",
      "reasoning": "Flagged as out of scope in the approved PREQ.",
      "proposed_action": "defer-to-issue",
      "target": null,
      "id": "F-PO-50-2-4"
    },
    {
      "category": "out-of-scope",
      "summary": "Automating desktop updates (update remains a deliberate manual act)",
      "reasoning": "Flagged as out of scope in the approved PREQ.",
      "proposed_action": "defer-to-issue",
      "target": null,
      "id": "F-PO-50-2-5"
    },
    {
      "category": "out-of-scope",
      "summary": "Desktop escape hatch for unreleased suite changes (dormant clone / side-load)",
      "reasoning": "Flagged as out of scope in the approved PREQ.",
      "proposed_action": "defer-to-issue",
      "target": null,
      "id": "F-PO-50-2-6"
    },
    {
      "category": "out-of-scope",
      "summary": "Renaming or restructuring the meta-lane skills beyond what the fence requires",
      "reasoning": "Flagged as out of scope in the approved PREQ.",
      "proposed_action": "defer-to-issue",
      "target": null,
      "id": "F-PO-50-2-7"
    },
    {
      "category": "in-scope-deferrable",
      "summary": "Declared-block schema compatibility policy (plugin version vs consumer CLAUDE.md declarations)",
      "reasoning": "Reviewer suggestion acknowledged but not actioned in this SREQ — see Expert Review > Noted (not actioned). This slice changes layout/references only, no declared-block schema; who bends when a v0.2.0 skill reads a v0.1.0-era consumer declaration needs its own versioning/tolerance rule and warrants a sibling issue.",
      "proposed_action": "defer-to-issue",
      "id": "F-PO-50-2-8"
    },
    {
      "category": "in-scope-deferrable",
      "summary": "Dev-machine run-record version/SHA attribution",
      "reasoning": "Reviewer suggestion acknowledged but not actioned — see Expert Review > Noted (not actioned). Rejected on product grounds: the PREQ explicitly out-scopes dev-machine version stamping (git is the dev machine's version identity; release attribution is desktop-only per AC-12).",
      "proposed_action": "accept",
      "id": "F-PO-50-2-9"
    },
    {
      "category": "in-scope-deferrable",
      "summary": "Signed/protected release tags on the GitHub remote",
      "reasoning": "Reviewer suggestion acknowledged and partially absorbed — see Expert Review > Noted (not actioned). The release record carries the tag's commit SHA (SREQ D6), giving content attribution; GitHub-side tag protection is operator remote configuration, folded into the D5 precondition's confirm guidance rather than build scope.",
      "proposed_action": "accept",
      "id": "F-PO-50-2-10"
    }
  ],
  "pending_decisions": [
    {
      "id": "D-PO-50-2-1",
      "type": "precondition",
      "blocking": true,
      "question": "Confirm the real Claude Code plugin subsystem (P0.1) contract for invoke a plugin SKILL.md-only skill as /dev:<skill> in a session before building — both the data shape (plugin skills are slash-invocable under the plugin namespace and appear in the picker) and a working access path (interactive Claude Code session (must-provision: operator-run probe with a scratch plugin via claude --plugin-dir)) captured against the real system?",
      "options": [
        "confirmed",
        "waived"
      ],
      "recommended": "confirmed",
      "reasoning": "2.2b could not verify against reality: reachability: behavior only observable in an interactive session; the #50 probe verified /ns:cmd for a command, not a SKILL.md-only skill. PREQ marks this decisive — fallback is not pre-authorized. The build will encode this boundary's two contracts — its data shape AND its credential/access path — and tests authored from the same assumptions would pass tautologically (fixtures-encode-code-assumptions). Either contract failing on real data breaks the feature, so the gate confirms both."
    },
    {
      "id": "D-PO-50-2-2",
      "type": "precondition",
      "blocking": true,
      "question": "Confirm the real Claude Code plugin subsystem (P0.2) contract for load skills-dir plugin alongside the live symlink farm in one session before building — both the data shape (distinct namespaces/scopes coexist; bare names and /dev: names both resolve during the transition window) and a working access path (interactive Claude Code session (must-provision: operator-run probe)) captured against the real system?",
      "options": [
        "confirmed",
        "waived"
      ],
      "recommended": "confirmed",
      "reasoning": "2.2b could not verify against reality: reachability: session load behavior not observable non-interactively; AC-5 coexistence window depends on it. The build will encode this boundary's two contracts — its data shape AND its credential/access path — and tests authored from the same assumptions would pass tautologically (fixtures-encode-code-assumptions). Either contract failing on real data breaks the feature, so the gate confirms both."
    },
    {
      "id": "D-PO-50-2-3",
      "type": "precondition",
      "blocking": true,
      "question": "Confirm the real GitHub remote github.com/jbr870/devwork-skills contract for git ls-remote (read) before building — both the data shape (repo exists; dev machine can push releases/tags; desktop can read/install from it) and a working access path (https from dev machine (blocked: no credential resolves; ssh key or PAT for GitHub not configured for this remote)) captured against the real system?",
      "options": [
        "confirmed",
        "waived"
      ],
      "recommended": "confirmed",
      "reasoning": "2.2b could not verify against reality: access: git ls-remote returns \"Repository not found\" at plan time — the repo does not exist yet or is private with no usable credential on the dev machine. PREQ Dependencies assumed it exists. The build will encode this boundary's two contracts — its data shape AND its credential/access path — and tests authored from the same assumptions would pass tautologically (fixtures-encode-code-assumptions). Either contract failing on real data breaks the feature, so the gate confirms both."
    },
    {
      "id": "D-PO-50-2-4",
      "type": "precondition",
      "blocking": true,
      "question": "Confirm the real Claude Code plugin subsystem on jochems-forge (P0.3) contract for marketplace add + install from the GitHub source at a released version before building — both the data shape (install succeeds at the tagged version; installed cache runs helpers; previous version reinstallable) and a working access path (ssh to jochems-forge (blocked at plan time: connection timed out) + desktop-side GitHub credential (unknown)) captured against the real system?",
      "options": [
        "confirmed",
        "waived"
      ],
      "recommended": "confirmed",
      "reasoning": "2.2b could not verify against reality: access: desktop unreachable over ssh at plan time (tailscale down or machine off), and the install probe is desktop-side + mutating. The build will encode this boundary's two contracts — its data shape AND its credential/access path — and tests authored from the same assumptions would pass tautologically (fixtures-encode-code-assumptions). Either contract failing on real data breaks the feature, so the gate confirms both."
    },
    {
      "id": "D-PO-50-2-5",
      "type": "precondition",
      "blocking": true,
      "question": "Confirm the real consumer CLAUDE.md files on jochems-forge (P0.7) contract for grep survey of guidance blocks (verity, axana, outwrit, cunnan) before building — both the data shape (guidance blocks referencing bare shipped-skill names; enumeration is the Phase 7 rewrite worklist) and a working access path (ssh to jochems-forge (blocked at plan time: connection timed out)) captured against the real system?",
      "options": [
        "confirmed",
        "waived"
      ],
      "recommended": "confirmed",
      "reasoning": "2.2b could not verify against reality: access: ssh to jochems-forge timed out at plan time; the AC-12 rewrite worklist for the desktop cannot be enumerated from here. The build will encode this boundary's two contracts — its data shape AND its credential/access path — and tests authored from the same assumptions would pass tautologically (fixtures-encode-code-assumptions). Either contract failing on real data breaks the feature, so the gate confirms both."
    },
    {
      "id": "D-PO-50-2-6",
      "type": "scope-disposition",
      "blocking": false,
      "question": "Out of scope: 'Publishing the plugin beyond the operator's own remotes (no public marketplace listing)'. Spawn a sibling issue, or accept (no follow-up)?",
      "options": [
        "defer-to-issue",
        "accept"
      ],
      "recommended": "defer-to-issue",
      "finding_ref": "F-PO-50-2-1",
      "reasoning": "Flagged as out of scope in the approved PREQ."
    },
    {
      "id": "D-PO-50-2-7",
      "type": "scope-disposition",
      "blocking": false,
      "question": "Out of scope: 'Automated eval suites for the plugin (plugin eval) — future issue'. Spawn a sibling issue, or accept (no follow-up)?",
      "options": [
        "defer-to-issue",
        "accept"
      ],
      "recommended": "defer-to-issue",
      "finding_ref": "F-PO-50-2-2",
      "reasoning": "Flagged as out of scope in the approved PREQ."
    },
    {
      "id": "D-PO-50-2-8",
      "type": "scope-disposition",
      "blocking": false,
      "question": "Out of scope: 'Any skill logic change beyond the two sanctioned restorative text touches (meta-helper path fix, adapter discovery-order hand-rewrite)'. Spawn a sibling issue, or accept (no follow-up)?",
      "options": [
        "defer-to-issue",
        "accept"
      ],
      "recommended": "defer-to-issue",
      "finding_ref": "F-PO-50-2-3",
      "reasoning": "Flagged as out of scope in the approved PREQ."
    },
    {
      "id": "D-PO-50-2-9",
      "type": "scope-disposition",
      "blocking": false,
      "question": "Out of scope: 'Version-stamping dev-machine run records (git is the dev machine's version identity; release attribution is desktop-only)'. Spawn a sibling issue, or accept (no follow-up)?",
      "options": [
        "defer-to-issue",
        "accept"
      ],
      "recommended": "defer-to-issue",
      "finding_ref": "F-PO-50-2-4",
      "reasoning": "Flagged as out of scope in the approved PREQ."
    },
    {
      "id": "D-PO-50-2-10",
      "type": "scope-disposition",
      "blocking": false,
      "question": "Out of scope: 'Automating desktop updates (update remains a deliberate manual act)'. Spawn a sibling issue, or accept (no follow-up)?",
      "options": [
        "defer-to-issue",
        "accept"
      ],
      "recommended": "defer-to-issue",
      "finding_ref": "F-PO-50-2-5",
      "reasoning": "Flagged as out of scope in the approved PREQ."
    },
    {
      "id": "D-PO-50-2-11",
      "type": "scope-disposition",
      "blocking": false,
      "question": "Out of scope: 'Desktop escape hatch for unreleased suite changes (dormant clone / side-load)'. Spawn a sibling issue, or accept (no follow-up)?",
      "options": [
        "defer-to-issue",
        "accept"
      ],
      "recommended": "defer-to-issue",
      "finding_ref": "F-PO-50-2-6",
      "reasoning": "Flagged as out of scope in the approved PREQ."
    },
    {
      "id": "D-PO-50-2-12",
      "type": "scope-disposition",
      "blocking": false,
      "question": "Out of scope: 'Renaming or restructuring the meta-lane skills beyond what the fence requires'. Spawn a sibling issue, or accept (no follow-up)?",
      "options": [
        "defer-to-issue",
        "accept"
      ],
      "recommended": "defer-to-issue",
      "finding_ref": "F-PO-50-2-7",
      "reasoning": "Flagged as out of scope in the approved PREQ."
    },
    {
      "id": "D-PO-50-2-13",
      "type": "scope-disposition",
      "blocking": false,
      "question": "Expert-review note (not actioned): 'Declared-block schema compatibility policy (plugin version vs consumer CLAUDE.md declarations)'. Spawn a sibling issue, or accept the disposition?",
      "options": [
        "defer-to-issue",
        "accept"
      ],
      "recommended": "defer-to-issue",
      "finding_ref": "F-PO-50-2-8",
      "reasoning": "Reviewer suggestion acknowledged but not actioned in this SREQ — see Expert Review > Noted (not actioned). This slice changes layout/references only, no declared-block schema; who bends when a v0.2.0 skill reads a v0.1.0-era consumer declaration needs its own versioning/tolerance rule and warrants a sibling issue."
    },
    {
      "id": "D-PO-50-2-14",
      "type": "scope-disposition",
      "blocking": false,
      "question": "Expert-review note (not actioned): 'Dev-machine run-record version/SHA attribution'. Spawn a sibling issue, or accept the disposition?",
      "options": [
        "defer-to-issue",
        "accept"
      ],
      "recommended": "accept",
      "finding_ref": "F-PO-50-2-9",
      "reasoning": "Reviewer suggestion acknowledged but not actioned — see Expert Review > Noted (not actioned). Rejected on product grounds: the PREQ explicitly out-scopes dev-machine version stamping (git is the dev machine's version identity; release attribution is desktop-only per AC-12)."
    },
    {
      "id": "D-PO-50-2-15",
      "type": "scope-disposition",
      "blocking": false,
      "question": "Expert-review note (not actioned): 'Signed/protected release tags on the GitHub remote'. Spawn a sibling issue, or accept the disposition?",
      "options": [
        "defer-to-issue",
        "accept"
      ],
      "recommended": "accept",
      "finding_ref": "F-PO-50-2-10",
      "reasoning": "Reviewer suggestion acknowledged and partially absorbed — see Expert Review > Noted (not actioned). The release record carries the tag's commit SHA (SREQ D6), giving content attribution; GitHub-side tag protection is operator remote configuration, folded into the D5 precondition's confirm guidance rather than build scope."
    }
  ]
}
<!-- phase-outcome:v1 id=PO-50-2 skill=technical-plan --> ```json { "next_state": "developing", "produced": [ { "kind": "sreq", "ref": "comment:975", "summary": "SREQ for ship-the-suite-as-a-claude-code-marketplace-plugin (Tier 3)" }, { "kind": "test-plan", "ref": "comment:978", "summary": "Test plan for ship-the-suite-as-a-claude-code-marketplace-plugin — lanes annotated: 7 integration-covered, 17 human-uat" } ], "findings": [ { "category": "out-of-scope", "summary": "Publishing the plugin beyond the operator's own remotes (no public marketplace listing)", "reasoning": "Flagged as out of scope in the approved PREQ.", "proposed_action": "defer-to-issue", "target": null, "id": "F-PO-50-2-1" }, { "category": "out-of-scope", "summary": "Automated eval suites for the plugin (plugin eval) — future issue", "reasoning": "Flagged as out of scope in the approved PREQ.", "proposed_action": "defer-to-issue", "target": null, "id": "F-PO-50-2-2" }, { "category": "out-of-scope", "summary": "Any skill logic change beyond the two sanctioned restorative text touches (meta-helper path fix, adapter discovery-order hand-rewrite)", "reasoning": "Flagged as out of scope in the approved PREQ.", "proposed_action": "defer-to-issue", "target": null, "id": "F-PO-50-2-3" }, { "category": "out-of-scope", "summary": "Version-stamping dev-machine run records (git is the dev machine's version identity; release attribution is desktop-only)", "reasoning": "Flagged as out of scope in the approved PREQ.", "proposed_action": "defer-to-issue", "target": null, "id": "F-PO-50-2-4" }, { "category": "out-of-scope", "summary": "Automating desktop updates (update remains a deliberate manual act)", "reasoning": "Flagged as out of scope in the approved PREQ.", "proposed_action": "defer-to-issue", "target": null, "id": "F-PO-50-2-5" }, { "category": "out-of-scope", "summary": "Desktop escape hatch for unreleased suite changes (dormant clone / side-load)", "reasoning": "Flagged as out of scope in the approved PREQ.", "proposed_action": "defer-to-issue", "target": null, "id": "F-PO-50-2-6" }, { "category": "out-of-scope", "summary": "Renaming or restructuring the meta-lane skills beyond what the fence requires", "reasoning": "Flagged as out of scope in the approved PREQ.", "proposed_action": "defer-to-issue", "target": null, "id": "F-PO-50-2-7" }, { "category": "in-scope-deferrable", "summary": "Declared-block schema compatibility policy (plugin version vs consumer CLAUDE.md declarations)", "reasoning": "Reviewer suggestion acknowledged but not actioned in this SREQ — see Expert Review > Noted (not actioned). This slice changes layout/references only, no declared-block schema; who bends when a v0.2.0 skill reads a v0.1.0-era consumer declaration needs its own versioning/tolerance rule and warrants a sibling issue.", "proposed_action": "defer-to-issue", "id": "F-PO-50-2-8" }, { "category": "in-scope-deferrable", "summary": "Dev-machine run-record version/SHA attribution", "reasoning": "Reviewer suggestion acknowledged but not actioned — see Expert Review > Noted (not actioned). Rejected on product grounds: the PREQ explicitly out-scopes dev-machine version stamping (git is the dev machine's version identity; release attribution is desktop-only per AC-12).", "proposed_action": "accept", "id": "F-PO-50-2-9" }, { "category": "in-scope-deferrable", "summary": "Signed/protected release tags on the GitHub remote", "reasoning": "Reviewer suggestion acknowledged and partially absorbed — see Expert Review > Noted (not actioned). The release record carries the tag's commit SHA (SREQ D6), giving content attribution; GitHub-side tag protection is operator remote configuration, folded into the D5 precondition's confirm guidance rather than build scope.", "proposed_action": "accept", "id": "F-PO-50-2-10" } ], "pending_decisions": [ { "id": "D-PO-50-2-1", "type": "precondition", "blocking": true, "question": "Confirm the real Claude Code plugin subsystem (P0.1) contract for invoke a plugin SKILL.md-only skill as /dev:<skill> in a session before building — both the data shape (plugin skills are slash-invocable under the plugin namespace and appear in the picker) and a working access path (interactive Claude Code session (must-provision: operator-run probe with a scratch plugin via claude --plugin-dir)) captured against the real system?", "options": [ "confirmed", "waived" ], "recommended": "confirmed", "reasoning": "2.2b could not verify against reality: reachability: behavior only observable in an interactive session; the #50 probe verified /ns:cmd for a command, not a SKILL.md-only skill. PREQ marks this decisive — fallback is not pre-authorized. The build will encode this boundary's two contracts — its data shape AND its credential/access path — and tests authored from the same assumptions would pass tautologically (fixtures-encode-code-assumptions). Either contract failing on real data breaks the feature, so the gate confirms both." }, { "id": "D-PO-50-2-2", "type": "precondition", "blocking": true, "question": "Confirm the real Claude Code plugin subsystem (P0.2) contract for load skills-dir plugin alongside the live symlink farm in one session before building — both the data shape (distinct namespaces/scopes coexist; bare names and /dev: names both resolve during the transition window) and a working access path (interactive Claude Code session (must-provision: operator-run probe)) captured against the real system?", "options": [ "confirmed", "waived" ], "recommended": "confirmed", "reasoning": "2.2b could not verify against reality: reachability: session load behavior not observable non-interactively; AC-5 coexistence window depends on it. The build will encode this boundary's two contracts — its data shape AND its credential/access path — and tests authored from the same assumptions would pass tautologically (fixtures-encode-code-assumptions). Either contract failing on real data breaks the feature, so the gate confirms both." }, { "id": "D-PO-50-2-3", "type": "precondition", "blocking": true, "question": "Confirm the real GitHub remote github.com/jbr870/devwork-skills contract for git ls-remote (read) before building — both the data shape (repo exists; dev machine can push releases/tags; desktop can read/install from it) and a working access path (https from dev machine (blocked: no credential resolves; ssh key or PAT for GitHub not configured for this remote)) captured against the real system?", "options": [ "confirmed", "waived" ], "recommended": "confirmed", "reasoning": "2.2b could not verify against reality: access: git ls-remote returns \"Repository not found\" at plan time — the repo does not exist yet or is private with no usable credential on the dev machine. PREQ Dependencies assumed it exists. The build will encode this boundary's two contracts — its data shape AND its credential/access path — and tests authored from the same assumptions would pass tautologically (fixtures-encode-code-assumptions). Either contract failing on real data breaks the feature, so the gate confirms both." }, { "id": "D-PO-50-2-4", "type": "precondition", "blocking": true, "question": "Confirm the real Claude Code plugin subsystem on jochems-forge (P0.3) contract for marketplace add + install from the GitHub source at a released version before building — both the data shape (install succeeds at the tagged version; installed cache runs helpers; previous version reinstallable) and a working access path (ssh to jochems-forge (blocked at plan time: connection timed out) + desktop-side GitHub credential (unknown)) captured against the real system?", "options": [ "confirmed", "waived" ], "recommended": "confirmed", "reasoning": "2.2b could not verify against reality: access: desktop unreachable over ssh at plan time (tailscale down or machine off), and the install probe is desktop-side + mutating. The build will encode this boundary's two contracts — its data shape AND its credential/access path — and tests authored from the same assumptions would pass tautologically (fixtures-encode-code-assumptions). Either contract failing on real data breaks the feature, so the gate confirms both." }, { "id": "D-PO-50-2-5", "type": "precondition", "blocking": true, "question": "Confirm the real consumer CLAUDE.md files on jochems-forge (P0.7) contract for grep survey of guidance blocks (verity, axana, outwrit, cunnan) before building — both the data shape (guidance blocks referencing bare shipped-skill names; enumeration is the Phase 7 rewrite worklist) and a working access path (ssh to jochems-forge (blocked at plan time: connection timed out)) captured against the real system?", "options": [ "confirmed", "waived" ], "recommended": "confirmed", "reasoning": "2.2b could not verify against reality: access: ssh to jochems-forge timed out at plan time; the AC-12 rewrite worklist for the desktop cannot be enumerated from here. The build will encode this boundary's two contracts — its data shape AND its credential/access path — and tests authored from the same assumptions would pass tautologically (fixtures-encode-code-assumptions). Either contract failing on real data breaks the feature, so the gate confirms both." }, { "id": "D-PO-50-2-6", "type": "scope-disposition", "blocking": false, "question": "Out of scope: 'Publishing the plugin beyond the operator's own remotes (no public marketplace listing)'. Spawn a sibling issue, or accept (no follow-up)?", "options": [ "defer-to-issue", "accept" ], "recommended": "defer-to-issue", "finding_ref": "F-PO-50-2-1", "reasoning": "Flagged as out of scope in the approved PREQ." }, { "id": "D-PO-50-2-7", "type": "scope-disposition", "blocking": false, "question": "Out of scope: 'Automated eval suites for the plugin (plugin eval) — future issue'. Spawn a sibling issue, or accept (no follow-up)?", "options": [ "defer-to-issue", "accept" ], "recommended": "defer-to-issue", "finding_ref": "F-PO-50-2-2", "reasoning": "Flagged as out of scope in the approved PREQ." }, { "id": "D-PO-50-2-8", "type": "scope-disposition", "blocking": false, "question": "Out of scope: 'Any skill logic change beyond the two sanctioned restorative text touches (meta-helper path fix, adapter discovery-order hand-rewrite)'. Spawn a sibling issue, or accept (no follow-up)?", "options": [ "defer-to-issue", "accept" ], "recommended": "defer-to-issue", "finding_ref": "F-PO-50-2-3", "reasoning": "Flagged as out of scope in the approved PREQ." }, { "id": "D-PO-50-2-9", "type": "scope-disposition", "blocking": false, "question": "Out of scope: 'Version-stamping dev-machine run records (git is the dev machine's version identity; release attribution is desktop-only)'. Spawn a sibling issue, or accept (no follow-up)?", "options": [ "defer-to-issue", "accept" ], "recommended": "defer-to-issue", "finding_ref": "F-PO-50-2-4", "reasoning": "Flagged as out of scope in the approved PREQ." }, { "id": "D-PO-50-2-10", "type": "scope-disposition", "blocking": false, "question": "Out of scope: 'Automating desktop updates (update remains a deliberate manual act)'. Spawn a sibling issue, or accept (no follow-up)?", "options": [ "defer-to-issue", "accept" ], "recommended": "defer-to-issue", "finding_ref": "F-PO-50-2-5", "reasoning": "Flagged as out of scope in the approved PREQ." }, { "id": "D-PO-50-2-11", "type": "scope-disposition", "blocking": false, "question": "Out of scope: 'Desktop escape hatch for unreleased suite changes (dormant clone / side-load)'. Spawn a sibling issue, or accept (no follow-up)?", "options": [ "defer-to-issue", "accept" ], "recommended": "defer-to-issue", "finding_ref": "F-PO-50-2-6", "reasoning": "Flagged as out of scope in the approved PREQ." }, { "id": "D-PO-50-2-12", "type": "scope-disposition", "blocking": false, "question": "Out of scope: 'Renaming or restructuring the meta-lane skills beyond what the fence requires'. Spawn a sibling issue, or accept (no follow-up)?", "options": [ "defer-to-issue", "accept" ], "recommended": "defer-to-issue", "finding_ref": "F-PO-50-2-7", "reasoning": "Flagged as out of scope in the approved PREQ." }, { "id": "D-PO-50-2-13", "type": "scope-disposition", "blocking": false, "question": "Expert-review note (not actioned): 'Declared-block schema compatibility policy (plugin version vs consumer CLAUDE.md declarations)'. Spawn a sibling issue, or accept the disposition?", "options": [ "defer-to-issue", "accept" ], "recommended": "defer-to-issue", "finding_ref": "F-PO-50-2-8", "reasoning": "Reviewer suggestion acknowledged but not actioned in this SREQ — see Expert Review > Noted (not actioned). This slice changes layout/references only, no declared-block schema; who bends when a v0.2.0 skill reads a v0.1.0-era consumer declaration needs its own versioning/tolerance rule and warrants a sibling issue." }, { "id": "D-PO-50-2-14", "type": "scope-disposition", "blocking": false, "question": "Expert-review note (not actioned): 'Dev-machine run-record version/SHA attribution'. Spawn a sibling issue, or accept the disposition?", "options": [ "defer-to-issue", "accept" ], "recommended": "accept", "finding_ref": "F-PO-50-2-9", "reasoning": "Reviewer suggestion acknowledged but not actioned — see Expert Review > Noted (not actioned). Rejected on product grounds: the PREQ explicitly out-scopes dev-machine version stamping (git is the dev machine's version identity; release attribution is desktop-only per AC-12)." }, { "id": "D-PO-50-2-15", "type": "scope-disposition", "blocking": false, "question": "Expert-review note (not actioned): 'Signed/protected release tags on the GitHub remote'. Spawn a sibling issue, or accept the disposition?", "options": [ "defer-to-issue", "accept" ], "recommended": "accept", "finding_ref": "F-PO-50-2-10", "reasoning": "Reviewer suggestion acknowledged and partially absorbed — see Expert Review > Noted (not actioned). The release record carries the tag's commit SHA (SREQ D6), giving content attribution; GitHub-side tag protection is operator remote configuration, folded into the D5 precondition's confirm guidance rather than build scope." } ] } ```
Author
Owner
{
  "status": "resolved",
  "chosen_option": "confirmed",
  "rationale": "Boundary verified 2026-08-15 after the operator created the repo: github.com/jbr870/devwork-skills exists (private, empty), reachable from both machines via gh auth as jbr870; dev-machine push access proven with git push --dry-run (no refs created). Evidence: external-contracts/github-remote-access.provenance.json. Authority: operator direction in this session ('created the repo'). First real push remains gated by the P0.8 full-history sensitive-data scan per SREQ D5."
}
<!-- decision-resolution:v1 ref=D-PO-50-2-3 --> ```json { "status": "resolved", "chosen_option": "confirmed", "rationale": "Boundary verified 2026-08-15 after the operator created the repo: github.com/jbr870/devwork-skills exists (private, empty), reachable from both machines via gh auth as jbr870; dev-machine push access proven with git push --dry-run (no refs created). Evidence: external-contracts/github-remote-access.provenance.json. Authority: operator direction in this session ('created the repo'). First real push remains gated by the P0.8 full-history sensitive-data scan per SREQ D5." } ```
Author
Owner
{
  "status": "resolved",
  "chosen_option": "confirmed",
  "rationale": "Access path verified 2026-08-15 with the desktop reachable again: gh auth as jbr870 on jochems-forge, git ls-remote to the private repo succeeds, claude CLI 2.1.232 present. Data shape (install = versioned working-tree snapshot, version-gated update) was recorded in the #50 probes of 2026-08-14 on the dev machine. Evidence: external-contracts/github-remote-access.provenance.json. The desktop-side install itself cannot run until a release is pushed — it is exercised by P0.3 in Phase 0 and AC-8/Scenario 14, per the SREQ. Authority: operator direction in this session ('Desktop is now reachable')."
}
<!-- decision-resolution:v1 ref=D-PO-50-2-4 --> ```json { "status": "resolved", "chosen_option": "confirmed", "rationale": "Access path verified 2026-08-15 with the desktop reachable again: gh auth as jbr870 on jochems-forge, git ls-remote to the private repo succeeds, claude CLI 2.1.232 present. Data shape (install = versioned working-tree snapshot, version-gated update) was recorded in the #50 probes of 2026-08-14 on the dev machine. Evidence: external-contracts/github-remote-access.provenance.json. The desktop-side install itself cannot run until a release is pushed — it is exercised by P0.3 in Phase 0 and AC-8/Scenario 14, per the SREQ. Authority: operator direction in this session ('Desktop is now reachable')." } ```
Author
Owner
{
  "status": "resolved",
  "chosen_option": "confirmed",
  "rationale": "Survey captured 2026-08-15 over restored ssh: desktop carriers are verity (one repo CLAUDE.md, ~18 refs, visible in main + 6 slot worktrees), axana/code (4 refs), outwrit (2 refs); cunnan and desktop-castellan carry no CLAUDE.md. Also recorded: 15 farm links, suite clone present, tea CLI MISSING on the desktop (README-prerequisite item for Phase 7). Evidence: external-contracts/desktop-consumer-survey.provenance.json. Authority: operator direction in this session."
}
<!-- decision-resolution:v1 ref=D-PO-50-2-5 --> ```json { "status": "resolved", "chosen_option": "confirmed", "rationale": "Survey captured 2026-08-15 over restored ssh: desktop carriers are verity (one repo CLAUDE.md, ~18 refs, visible in main + 6 slot worktrees), axana/code (4 refs), outwrit (2 refs); cunnan and desktop-castellan carry no CLAUDE.md. Also recorded: 15 farm links, suite clone present, tea CLI MISSING on the desktop (README-prerequisite item for Phase 7). Evidence: external-contracts/desktop-consumer-survey.provenance.json. Authority: operator direction in this session." } ```
Author
Owner
{
  "status": "resolved",
  "chosen_option": "accept",
  "rationale": "These are deliberate product exclusions the PREQ already decided, not deferred work — a sibling issue would track a non-goal. No follow-up needed. Rejected alternative: defer-to-issue (nothing to build later; the exclusion IS the decision). Authority: operator agreement in this session ('For the rest I agree with your recommendations')."
}
<!-- decision-resolution:v1 ref=D-PO-50-2-6 --> ```json { "status": "resolved", "chosen_option": "accept", "rationale": "These are deliberate product exclusions the PREQ already decided, not deferred work — a sibling issue would track a non-goal. No follow-up needed. Rejected alternative: defer-to-issue (nothing to build later; the exclusion IS the decision). Authority: operator agreement in this session ('For the rest I agree with your recommendations')." } ```
Author
Owner
{
  "status": "resolved",
  "chosen_option": "accept",
  "rationale": "PREQ decided dev-machine version identity is git itself; release attribution is desktop-only (AC-12). A ticket would track a non-goal. Authority: operator agreement in this session ('For the rest I agree with your recommendations')."
}
<!-- decision-resolution:v1 ref=D-PO-50-2-9 --> ```json { "status": "resolved", "chosen_option": "accept", "rationale": "PREQ decided dev-machine version identity is git itself; release attribution is desktop-only (AC-12). A ticket would track a non-goal. Authority: operator agreement in this session ('For the rest I agree with your recommendations')." } ```
Author
Owner
{
  "status": "resolved",
  "chosen_option": "accept",
  "rationale": "Manual, deliberate desktop updates are the versioning discipline this feature introduces — automating them is a rejected direction, not deferred work. Authority: operator agreement in this session ('For the rest I agree with your recommendations')."
}
<!-- decision-resolution:v1 ref=D-PO-50-2-10 --> ```json { "status": "resolved", "chosen_option": "accept", "rationale": "Manual, deliberate desktop updates are the versioning discipline this feature introduces — automating them is a rejected direction, not deferred work. Authority: operator agreement in this session ('For the rest I agree with your recommendations')." } ```
Author
Owner
{
  "status": "resolved",
  "chosen_option": "accept",
  "rationale": "The escape hatch reintroduces the stale-checkout hazard the feature kills; PREQ decided unreleased dogfooding is dev-machine-only. Authority: operator agreement in this session ('For the rest I agree with your recommendations')."
}
<!-- decision-resolution:v1 ref=D-PO-50-2-11 --> ```json { "status": "resolved", "chosen_option": "accept", "rationale": "The escape hatch reintroduces the stale-checkout hazard the feature kills; PREQ decided unreleased dogfooding is dev-machine-only. Authority: operator agreement in this session ('For the rest I agree with your recommendations')." } ```
Author
Owner
{
  "status": "resolved",
  "chosen_option": "accept",
  "rationale": "The fence requires no meta-lane restructuring; anything beyond it is churn without a user. Authority: operator agreement in this session ('For the rest I agree with your recommendations')."
}
<!-- decision-resolution:v1 ref=D-PO-50-2-12 --> ```json { "status": "resolved", "chosen_option": "accept", "rationale": "The fence requires no meta-lane restructuring; anything beyond it is churn without a user. Authority: operator agreement in this session ('For the rest I agree with your recommendations')." } ```
Author
Owner
{
  "status": "resolved",
  "chosen_option": "accept",
  "rationale": "Rejected on product grounds: PREQ explicitly out-scopes dev-machine version stamping — git is the dev machine's version identity. Authority: operator agreement in this session ('For the rest I agree with your recommendations')."
}
<!-- decision-resolution:v1 ref=D-PO-50-2-14 --> ```json { "status": "resolved", "chosen_option": "accept", "rationale": "Rejected on product grounds: PREQ explicitly out-scopes dev-machine version stamping — git is the dev machine's version identity. Authority: operator agreement in this session ('For the rest I agree with your recommendations')." } ```
Author
Owner
{
  "status": "resolved",
  "chosen_option": "accept",
  "rationale": "Partially absorbed into the SREQ: the release record carries the tag's commit SHA (D6) for content attribution; GitHub-side tag protection is one-time operator remote configuration folded into the D5 precondition guidance, not build scope. Authority: operator agreement in this session ('For the rest I agree with your recommendations')."
}
<!-- decision-resolution:v1 ref=D-PO-50-2-15 --> ```json { "status": "resolved", "chosen_option": "accept", "rationale": "Partially absorbed into the SREQ: the release record carries the tag's commit SHA (D6) for content attribution; GitHub-side tag protection is one-time operator remote configuration folded into the D5 precondition guidance, not build scope. Authority: operator agreement in this session ('For the rest I agree with your recommendations')." } ```
Author
Owner
{
  "status": "resolved",
  "chosen_option": "accept",
  "rationale": "The no-logic-change rule with its two sanctioned restorative exceptions is a constraint on this slice, enforced by SREQ S-4 and the rewrite exclusion set — not deferrable work. Authority: operator agreement in this session ('For the rest I agree with your recommendations')."
}
<!-- decision-resolution:v1 ref=D-PO-50-2-8 --> ```json { "status": "resolved", "chosen_option": "accept", "rationale": "The no-logic-change rule with its two sanctioned restorative exceptions is a constraint on this slice, enforced by SREQ S-4 and the rewrite exclusion set — not deferrable work. Authority: operator agreement in this session ('For the rest I agree with your recommendations')." } ```
Author
Owner
{
  "status": "resolved",
  "chosen_option": "defer-to-issue",
  "rationale": "Genuinely deferred capability (the PREQ itself says 'future issue'): eval suites are valuable but independent of the migration slice. Spawning a sibling keeps it tracked instead of re-raised every retrospective. Authority: operator agreement in this session."
}
<!-- decision-resolution:v1 ref=D-PO-50-2-7 --> ```json { "status": "resolved", "chosen_option": "defer-to-issue", "rationale": "Genuinely deferred capability (the PREQ itself says 'future issue'): eval suites are valuable but independent of the migration slice. Spawning a sibling keeps it tracked instead of re-raised every retrospective. Authority: operator agreement in this session." } ```
Author
Owner

Linked: this issue is sibling #51 (recorded by the devwork pipeline).

Linked: this issue is **sibling** #51 (recorded by the devwork pipeline).
Author
Owner
{
  "status": "resolved",
  "chosen_option": "defer-to-issue",
  "rationale": "New normative capability, not a migration touch: a versioning/tolerance policy for the declared CLAUDE.md blocks (forge:/observability:/uat:/parallel_dev:) belongs to the full pipeline lane per this repo's two-lane rule. Not needed for 0.1.0 — the cutover rewrites consumer declarations in the same window as the install, so drift is only possible from the second release on. Authority: operator agreement in this session ('I agree with your recommendation for D thirteen'). Fold-candidates checked: no eligible issue is a topical home, so spawn-new per the confidence rule."
}
<!-- decision-resolution:v1 ref=D-PO-50-2-13 --> ```json { "status": "resolved", "chosen_option": "defer-to-issue", "rationale": "New normative capability, not a migration touch: a versioning/tolerance policy for the declared CLAUDE.md blocks (forge:/observability:/uat:/parallel_dev:) belongs to the full pipeline lane per this repo's two-lane rule. Not needed for 0.1.0 — the cutover rewrites consumer declarations in the same window as the install, so drift is only possible from the second release on. Authority: operator agreement in this session ('I agree with your recommendation for D thirteen'). Fold-candidates checked: no eligible issue is a topical home, so spawn-new per the confidence rule." } ```
Author
Owner

Linked: this issue is sibling #52 (recorded by the devwork pipeline).

Linked: this issue is **sibling** #52 (recorded by the devwork pipeline).
Author
Owner
{
  "status": "resolved",
  "chosen_option": "confirmed",
  "rationale": "Probed 2026-08-15 with a scratch SKILL.md-only plugin (devprobe) via claude --plugin-dir: /devprobe:probeskill RESOLVED and ran (marker emitted), ${CLAUDE_PLUGIN_ROOT} was substituted at load time to the real plugin path, and the bare name /probeskill returned 'Unknown command' — namespacing is mandatory for skills and stale bare names fail loudly (P0.6 corroboration). The decisive P0.1 outcome is POSITIVE: no command-wrapper fallback needed. Caveat on record: print-mode probe, resolution + listing verified, interactive-picker cosmetics not. Evidence: external-contracts/plugin-skill-slash-invocability.provenance.json. Authority: operator direction in this session ('attempt to probe yourself')."
}
<!-- decision-resolution:v1 ref=D-PO-50-2-1 --> ```json { "status": "resolved", "chosen_option": "confirmed", "rationale": "Probed 2026-08-15 with a scratch SKILL.md-only plugin (devprobe) via claude --plugin-dir: /devprobe:probeskill RESOLVED and ran (marker emitted), ${CLAUDE_PLUGIN_ROOT} was substituted at load time to the real plugin path, and the bare name /probeskill returned 'Unknown command' — namespacing is mandatory for skills and stale bare names fail loudly (P0.6 corroboration). The decisive P0.1 outcome is POSITIVE: no command-wrapper fallback needed. Caveat on record: print-mode probe, resolution + listing verified, interactive-picker cosmetics not. Evidence: external-contracts/plugin-skill-slash-invocability.provenance.json. Authority: operator direction in this session ('attempt to probe yourself')." } ```
Author
Owner
{
  "status": "resolved",
  "chosen_option": "confirmed",
  "rationale": "Probed 2026-08-15 in the same scratch session: with the 15-link farm still present in ~/.claude/skills, a listing check returned probeskill=yes requirements=yes develop=yes tea-cli=yes — the skills-dir plugin and the farm coexist in one session, so the AC-5 pre-cutover coexistence window is real. Evidence: external-contracts/farm-plugin-coexistence.provenance.json. Authority: operator direction in this session."
}
<!-- decision-resolution:v1 ref=D-PO-50-2-2 --> ```json { "status": "resolved", "chosen_option": "confirmed", "rationale": "Probed 2026-08-15 in the same scratch session: with the 15-link farm still present in ~/.claude/skills, a listing check returned probeskill=yes requirements=yes develop=yes tea-cli=yes — the skills-dir plugin and the farm coexist in one session, so the AC-5 pre-cutover coexistence window is real. Evidence: external-contracts/farm-plugin-coexistence.provenance.json. Authority: operator direction in this session." } ```
Author
Owner
{
  "waves": [
    {
      "wave": 1,
      "work_units": [
        {"id": "WU-50-3-1", "title": "Restructure into plugin/ + both manifests + lint retarget (single commit, locks name dev)", "files": ["plugin/skills/** (git mv from .claude/skills)", "plugin/.claude-plugin/plugin.json", ".claude-plugin/marketplace.json", "scripts/lint-conventions.sh (retarget only)"], "acceptance_criteria": [{"id": "AC-1", "scope": "manifests exist and pass validate --strict"}, {"id": "S-3"}]}
      ]
    },
    {
      "wave": 2,
      "work_units": [
        {"id": "WU-50-3-2", "title": "Reference rewrite over plugin/skills/** (violation classes, allowlist, exclusion set; audit script)", "files": ["plugin/skills/**/*.md", "scripts/rewrite-plugin-refs.sh"], "acceptance_criteria": [{"id": "AC-3", "scope": "shipped tree scans clean"}, {"id": "S-5"}]},
        {"id": "WU-50-3-3", "title": "Meta-lane amendments (D7 helper root via git rev-parse, /dev: names for shipped-skill refs, lane-gate path)", "files": [".claude/skills/meta-amend/**", ".claude/skills/meta-retrospective/**"], "acceptance_criteria": [{"id": "AC-4", "scope": "bare-name meta lane intact"}, {"id": "S-4", "scope": "D7 helper root"}]}
      ]
    },
    {
      "wave": 3,
      "work_units": [
        {"id": "WU-50-3-4", "title": "Lint-gate extensions + release.sh + test-plugin-gates.sh (2-phase TDD: gate suite written red first)", "files": ["scripts/lint-conventions.sh (new rules)", "scripts/release.sh", "scripts/test-plugin-gates.sh"], "acceptance_criteria": [{"id": "AC-1", "scope": "validate --strict lint rule + Scenario 23 control"}, {"id": "AC-3", "scope": "scan rules + Scenario 24 controls"}, {"id": "AC-4", "scope": "mechanical absence half"}, {"id": "AC-10"}]}
      ]
    },
    {
      "wave": 4,
      "work_units": [
        {"id": "WU-50-3-5", "title": "Cutover tooling: cutover-dev.sh + cutover-abort.sh (D11 ordering, S-2 assertion, state probe, runtime-config migration)", "files": ["scripts/cutover-dev.sh", "scripts/cutover-abort.sh"], "acceptance_criteria": [{"id": "AC-6", "scope": "mechanical assertions"}, {"id": "AC-7", "scope": "scripted inverse of every step"}, {"id": "AC-12", "scope": "final state probe"}, {"id": "S-2"}]},
        {"id": "WU-50-3-6", "title": "README rewrite + P0.8 full-history sensitive-data scan (run + record on issue)", "files": ["README.md", "scripts/history-scan.sh"], "acceptance_criteria": [{"id": "AC-11", "scope": "README-only desktop cutover content"}, {"id": "S-1", "scope": "P0.8 probe record"}]}
      ]
    }
  ]
}

Decomposition rationale: the SREQ sequences Phases 1→2→3 strictly (restructure must precede the
rewrite so the rewrite operates on plugin/skills/**; the gate suite needs the rewritten tree and
the manifests to assert clean states). Execution is fully sequential in a single isolated
worktree
(devwork-skills-50) per Approach C/D11 — the main checkout sits on main serving the
live farm, so within-wave parallelism is forfeited for safety per the single-worktree rule.
WU-50-3-1/2/3/5/6 are artifact-shape units (verification = mechanical gates + UAT scenarios), so
their Test Writers are skipped per the artifact-shape exception; WU-50-3-4 is behavioral (release
refusal trio, scan/validator non-vacuity controls) and runs the full 2-phase TDD pair with
scripts/test-plugin-gates.sh written red first. Phases 4–7 of the SREQ (coexistence dogfood,
cutovers, releases) are human-UAT lanes and are deliberately outside /develop scope; the terminal
PO will carry them as in-scope-deferrable findings.

<!-- wu-plan:v1 po=PO-50-3 skill=develop --> ```json { "waves": [ { "wave": 1, "work_units": [ {"id": "WU-50-3-1", "title": "Restructure into plugin/ + both manifests + lint retarget (single commit, locks name dev)", "files": ["plugin/skills/** (git mv from .claude/skills)", "plugin/.claude-plugin/plugin.json", ".claude-plugin/marketplace.json", "scripts/lint-conventions.sh (retarget only)"], "acceptance_criteria": [{"id": "AC-1", "scope": "manifests exist and pass validate --strict"}, {"id": "S-3"}]} ] }, { "wave": 2, "work_units": [ {"id": "WU-50-3-2", "title": "Reference rewrite over plugin/skills/** (violation classes, allowlist, exclusion set; audit script)", "files": ["plugin/skills/**/*.md", "scripts/rewrite-plugin-refs.sh"], "acceptance_criteria": [{"id": "AC-3", "scope": "shipped tree scans clean"}, {"id": "S-5"}]}, {"id": "WU-50-3-3", "title": "Meta-lane amendments (D7 helper root via git rev-parse, /dev: names for shipped-skill refs, lane-gate path)", "files": [".claude/skills/meta-amend/**", ".claude/skills/meta-retrospective/**"], "acceptance_criteria": [{"id": "AC-4", "scope": "bare-name meta lane intact"}, {"id": "S-4", "scope": "D7 helper root"}]} ] }, { "wave": 3, "work_units": [ {"id": "WU-50-3-4", "title": "Lint-gate extensions + release.sh + test-plugin-gates.sh (2-phase TDD: gate suite written red first)", "files": ["scripts/lint-conventions.sh (new rules)", "scripts/release.sh", "scripts/test-plugin-gates.sh"], "acceptance_criteria": [{"id": "AC-1", "scope": "validate --strict lint rule + Scenario 23 control"}, {"id": "AC-3", "scope": "scan rules + Scenario 24 controls"}, {"id": "AC-4", "scope": "mechanical absence half"}, {"id": "AC-10"}]} ] }, { "wave": 4, "work_units": [ {"id": "WU-50-3-5", "title": "Cutover tooling: cutover-dev.sh + cutover-abort.sh (D11 ordering, S-2 assertion, state probe, runtime-config migration)", "files": ["scripts/cutover-dev.sh", "scripts/cutover-abort.sh"], "acceptance_criteria": [{"id": "AC-6", "scope": "mechanical assertions"}, {"id": "AC-7", "scope": "scripted inverse of every step"}, {"id": "AC-12", "scope": "final state probe"}, {"id": "S-2"}]}, {"id": "WU-50-3-6", "title": "README rewrite + P0.8 full-history sensitive-data scan (run + record on issue)", "files": ["README.md", "scripts/history-scan.sh"], "acceptance_criteria": [{"id": "AC-11", "scope": "README-only desktop cutover content"}, {"id": "S-1", "scope": "P0.8 probe record"}]} ] } ] } ``` Decomposition rationale: the SREQ sequences Phases 1→2→3 strictly (restructure must precede the rewrite so the rewrite operates on `plugin/skills/**`; the gate suite needs the rewritten tree and the manifests to assert clean states). Execution is **fully sequential in a single isolated worktree** (`devwork-skills-50`) per Approach C/D11 — the main checkout sits on `main` serving the live farm, so within-wave parallelism is forfeited for safety per the single-worktree rule. WU-50-3-1/2/3/5/6 are artifact-shape units (verification = mechanical gates + UAT scenarios), so their Test Writers are skipped per the artifact-shape exception; WU-50-3-4 is behavioral (release refusal trio, scan/validator non-vacuity controls) and runs the full 2-phase TDD pair with `scripts/test-plugin-gates.sh` written red first. Phases 4–7 of the SREQ (coexistence dogfood, cutovers, releases) are human-UAT lanes and are deliberately outside /develop scope; the terminal PO will carry them as in-scope-deferrable findings.
Author
Owner

Dev journal — WU-50-3-1 (plugin restructure)

2026-08-15T14:40 DISCOVERY: claude plugin validate invocations that work

type: discovery
timestamp: 2026-08-15T14:40
where: plugin/.claude-plugin/plugin.json, .claude-plugin/marketplace.json
tags: [plugin, validator, cli]

Context: AC-1 requires claude plugin validate --strict to pass with zero errors and zero warnings on both manifests. The CLI takes one <path> argument and auto-detects which manifest kind it is looking at.

Pattern: From the worktree root, two invocations cover both manifests:

  • claude plugin validate --strict plugin → resolves plugin/.claude-plugin/plugin.json and reports "Validating plugin manifest".
  • claude plugin validate --strict . → resolves .claude-plugin/marketplace.json and reports "Validating marketplace manifest".

Pass a directory, not the manifest file. The path is the plugin/marketplace root; the validator appends .claude-plugin/<kind>.json itself.

Why it matters: The two manifests cannot be validated in one call — a directory holding both would only surface one of them, and here they live at different roots anyway. Both invocations must be run and both must exit 0.

2026-08-15T14:42 DISCOVERY: marketplace validation recurses into the plugin, and needs its own description

type: discovery
timestamp: 2026-08-15T14:42
where: .claude-plugin/marketplace.json
tags: [plugin, validator, schema]

Context: Probed the marketplace schema in a scratch fixture before writing the real manifest (CLI version 2.1.229).

Pattern: Two behaviors worth knowing:

  1. A top-level description on the marketplace itself is required under --strict. Omitting it is only a warning ("No marketplace description provided"), which the runtime tolerates — but --strict turns it into a failure. The per-plugin description inside plugins[] does not satisfy it; they are separate fields.
  2. Marketplace validation follows source and validates the referenced plugin's plugin.json too. Deliberately corrupting plugin/.claude-plugin/plugin.json in the fixture produced plugins[0] plugin.json → name: … errors from the marketplace invocation. So the marketplace call is a superset check — but still run the plugin call directly, since it names failures against the plugin manifest without the plugins[0] indirection.

Unknown fields are warnings ("Claude Code ignores it at load time"), which --strict also fails on — so no speculative metadata fields. The manifests ship exactly the fields the WU specified: plugin = name/version/description/author.name; marketplace = name/description/owner.name/plugins[] with name/source/version/description.

2026-08-15T14:55 DISCOVERY: lint dual-root retarget stays bash-3.2 clean via word-split find roots

type: discovery
timestamp: 2026-08-15T14:55
where: scripts/lint-conventions.sh
tags: [lint, portability]

Context: scripts/lint-conventions.sh hardcoded one skills root ($REPO_ROOT/.claude/skills) in two find calls, and the script self-enforces the bash-3.2 ceiling, so an array of roots was not an option worth reaching for.

Pattern: Build a space-separated SKILLS_DIRS of the roots that exist, then pass it unquoted to findfind accepts multiple starting points, and the existing script already relies on word-splitting for helper_files, so this matches the file's own idiom and carries the same "repo paths have no spaces" assumption. An empty SKILLS_DIRS would silently make find scan the cwd instead, so the script fails fast with a message when neither root exists.

The adapter exemption in the direct-forge-CLI rule became ^(plugin|\.claude)/skills/(tea-cli|glab-cli|gh-cli|local-fs)/, keeping the meta-lane alternative in place rather than swapping the prefix, so the rule does not silently stop matching if an adapter is ever staged under either root.

Verified behaviorally, not by reading: planted a declare -A helper and a readarray fence under each root, confirmed all four were reported with the correct paths, removed them, and confirmed a clean run. The adapter exemption is genuinely exercised — 59 direct tea|glab|gh forge lines live in plugin/skills/{tea,glab,gh}-cli/**.md and the run is clean, so the new prefix matches.

2026-08-15T15:00 COMPLETE

type: complete
variant: development
timestamp: 2026-08-15T15:00
iterations: 1

Hardest part: Nothing fought back — the only real unknown was the validator's schema, and probing it in a scratch fixture before writing the real manifests turned that into a two-minute question instead of a guess-and-retry loop.

If I did this again: Same order — probe the validator on a throwaway fixture first, then move, then retarget lint. Moving first would have meant debugging manifest schema against a repo mid-restructure.

<!-- dev-journal:v1 wu=WU-50-3-1 skill=develop --> # Dev journal — WU-50-3-1 (plugin restructure) ## 2026-08-15T14:40 DISCOVERY: `claude plugin validate` invocations that work ```yaml type: discovery timestamp: 2026-08-15T14:40 where: plugin/.claude-plugin/plugin.json, .claude-plugin/marketplace.json tags: [plugin, validator, cli] ``` **Context:** AC-1 requires `claude plugin validate --strict` to pass with zero errors and zero warnings on both manifests. The CLI takes one `<path>` argument and auto-detects which manifest kind it is looking at. **Pattern:** From the worktree root, two invocations cover both manifests: - `claude plugin validate --strict plugin` → resolves `plugin/.claude-plugin/plugin.json` and reports "Validating plugin manifest". - `claude plugin validate --strict .` → resolves `.claude-plugin/marketplace.json` and reports "Validating marketplace manifest". Pass a **directory**, not the manifest file. The path is the plugin/marketplace *root*; the validator appends `.claude-plugin/<kind>.json` itself. **Why it matters:** The two manifests cannot be validated in one call — a directory holding both would only surface one of them, and here they live at different roots anyway. Both invocations must be run and both must exit 0. ## 2026-08-15T14:42 DISCOVERY: marketplace validation recurses into the plugin, and needs its own description ```yaml type: discovery timestamp: 2026-08-15T14:42 where: .claude-plugin/marketplace.json tags: [plugin, validator, schema] ``` **Context:** Probed the marketplace schema in a scratch fixture before writing the real manifest (CLI version 2.1.229). **Pattern:** Two behaviors worth knowing: 1. **A top-level `description` on the marketplace itself is required under `--strict`.** Omitting it is only a *warning* ("No marketplace description provided"), which the runtime tolerates — but `--strict` turns it into a failure. The per-plugin `description` inside `plugins[]` does not satisfy it; they are separate fields. 2. **Marketplace validation follows `source` and validates the referenced plugin's `plugin.json` too.** Deliberately corrupting `plugin/.claude-plugin/plugin.json` in the fixture produced `plugins[0] plugin.json → name: …` errors from the *marketplace* invocation. So the marketplace call is a superset check — but still run the plugin call directly, since it names failures against the plugin manifest without the `plugins[0]` indirection. Unknown fields are warnings ("Claude Code ignores it at load time"), which `--strict` also fails on — so no speculative metadata fields. The manifests ship exactly the fields the WU specified: plugin = `name`/`version`/`description`/`author.name`; marketplace = `name`/`description`/`owner.name`/`plugins[]` with `name`/`source`/`version`/`description`. ## 2026-08-15T14:55 DISCOVERY: lint dual-root retarget stays bash-3.2 clean via word-split find roots ```yaml type: discovery timestamp: 2026-08-15T14:55 where: scripts/lint-conventions.sh tags: [lint, portability] ``` **Context:** `scripts/lint-conventions.sh` hardcoded one skills root (`$REPO_ROOT/.claude/skills`) in two `find` calls, and the script self-enforces the bash-3.2 ceiling, so an array of roots was not an option worth reaching for. **Pattern:** Build a space-separated `SKILLS_DIRS` of the roots that exist, then pass it **unquoted** to `find` — `find` accepts multiple starting points, and the existing script already relies on word-splitting for `helper_files`, so this matches the file's own idiom and carries the same "repo paths have no spaces" assumption. An empty `SKILLS_DIRS` would silently make `find` scan the cwd instead, so the script fails fast with a message when neither root exists. The adapter exemption in the direct-forge-CLI rule became `^(plugin|\.claude)/skills/(tea-cli|glab-cli|gh-cli|local-fs)/`, keeping the meta-lane alternative in place rather than swapping the prefix, so the rule does not silently stop matching if an adapter is ever staged under either root. **Verified behaviorally, not by reading:** planted a `declare -A` helper and a `readarray` fence under *each* root, confirmed all four were reported with the correct paths, removed them, and confirmed a clean run. The adapter exemption is genuinely exercised — 59 direct `tea|glab|gh` forge lines live in `plugin/skills/{tea,glab,gh}-cli/**.md` and the run is clean, so the new prefix matches. ## 2026-08-15T15:00 COMPLETE ```yaml type: complete variant: development timestamp: 2026-08-15T15:00 iterations: 1 ``` **Hardest part:** Nothing fought back — the only real unknown was the validator's schema, and probing it in a scratch fixture before writing the real manifests turned that into a two-minute question instead of a guess-and-retry loop. **If I did this again:** Same order — probe the validator on a throwaway fixture first, then move, then retarget lint. Moving first would have meant debugging manifest schema against a repo mid-restructure.
Author
Owner
{
  "outcome": "complete",
  "summary": "Restructured the 14 shipped skills + _shared into plugin/skills/ (201 files, all R100 pure renames), added plugin.json (name=dev, v0.1.0) + marketplace.json (source ./plugin), retargeted lint-conventions.sh to dual roots — one commit; validate --strict clean on both manifests, lint clean. Artifact-shape WU: Test Writer skipped per 3.2 exception; ACs verified mechanically (validator + lint + R100 rename audit).",
  "commit_refs": ["77a5f2f"],
  "journal_ref": "1011",
  "dependencies_satisfied": [],
  "dependencies_violated": []
}
<!-- work-unit-outcome:v1 id=WU-50-3-1 skill=develop --> ```json { "outcome": "complete", "summary": "Restructured the 14 shipped skills + _shared into plugin/skills/ (201 files, all R100 pure renames), added plugin.json (name=dev, v0.1.0) + marketplace.json (source ./plugin), retargeted lint-conventions.sh to dual roots — one commit; validate --strict clean on both manifests, lint clean. Artifact-shape WU: Test Writer skipped per 3.2 exception; ACs verified mechanically (validator + lint + R100 rename audit).", "commit_refs": ["77a5f2f"], "journal_ref": "1011", "dependencies_satisfied": [], "dependencies_violated": [] } ```
Author
Owner

Dev journal — WU-50-3-2: rewrite shipped-skill references to the dev plugin namespace

Worktree: /home/jochem/.claude/repos/devwork-skills-50 (branch feature/50-ship-the-suite-as-a-claude-code-marketplace-plugin)
Baseline: 77a5f2f (WU-50-3-1 restructure)

Per-class counts (occurrences, not lines) over plugin/skills/**/*.md

Class At 77a5f2f After
1 — ~/.claude/skills/… 56 2 (both allow-anchored)
2 — bare _shared/… 299 0
3 — bare /<shipped-skill> 565 0
${CLAUDE_PLUGIN_ROOT} introduced 0 358
/dev:<skill> introduced 0 565

The arithmetic closes exactly: 358 = (56 − 2 class-1) + 299 class-2 + 5 hand-added occurrences
(the readiness-check for list, the phase-outcome precedence list, the setup precedence list, and
the two adapter smoke-test paths). Class 3 is 1:1 — 565 in, 565 out, and every /dev: name is one
of the 14.

Files changed: 69 under plugin/skills/** + scripts/rewrite-plugin-refs.sh.


[2026-08-15T14:05] DISCOVERY: quoting hygiene is fence-state-dependent, so the mechanical pass had to track fences

type: discovery
timestamp: 2026-08-15T14:05
where: scripts/rewrite-plugin-refs.sh
tags: [d12, portability, awk]

Context: D12 requires "${CLAUDE_PLUGIN_ROOT}/…" double-quoted at use sites but permits the
bare form in prose. A blind substitution can satisfy neither, because the same textual path is a
prose cross-reference in one line and an executable argv[0] in the next.

Pattern: the rewrite script carries the same CommonMark fence tracker
scripts/lint-conventions.sh uses (a fence closes only on ≥ its own backtick count), and applies the
quoting pass only while inside a bash/sh/zsh/shell fence. It also declines to quote a token
carrying a glob or a {placeholder} — quoting …/bin/* would suppress the glob, so those are left
for hand review. In the end no such token turned out to live in a shell fence.

Why it matters: it keeps the audit record honest. Everything the script did is mechanically
reproducible and idempotent (--check re-run is a no-op); everything else is in this journal.


[2026-08-15T14:20] STRUGGLE: the class-3 boundary rule silently skipped sentence-final invocations

type: struggle
timestamp: 2026-08-15T14:20
attempts: 2
resolved: true
tags: [polysemy, class-3, regex-boundary]

Problem: rewrite /setup/dev:setup only where it is an invocation, never where it is a path.
Expected: all 565 invocations rewritten.
Actual: 555 rewritten; 10 left bare.

Attempts:

  1. Trailing-boundary rule "the char after the name must not be [A-Za-z0-9_./-]". This correctly
    protects /develop.md (a file) but also rejects re-run /setup. (a sentence). The 10 misses were
    all sentence-final: integrate/SKILL.md:108,250, readiness-check.md:166,175,
    promote/SKILL.md:125, resolve/SKILL.md:138, develop/SKILL.md:437,495,545,
    setup/SKILL.md:114.
  2. Refined rule: a trailing . counts as sentence punctuation unless a word character follows it
    (which would make it an extension). Fixed in the script rather than by hand, so the script stays
    the complete record of the mechanical pass.

Key insight: the trailing-boundary set and the leading-boundary set are not symmetric. A / or
~ before the name always means path; a . after it usually means end of sentence.


[2026-08-15T14:45] DISCOVERY: the awk quoting fixup over-reached into prose, and had to be narrowed

type: discovery
timestamp: 2026-08-15T14:45
where: plugin/skills/_shared/procedures/phase-outcome.md
tags: [d12, consistency]

Context: 7 helper invocations sit in unlabeled fences or inside a blockquoted ```` >bash ```` fence, so neither the linter's tier-1 extractor nor the rewrite script's quoting
pass sees them. I hand-quoted them with a one-shot awk over three files.

Pattern: that awk matched the first occurrence per line, which also quoted backticked prose
mentions in the same lines (e.g. the phase-outcome.md API reference table, develop/SKILL.md:43),
leaving those lines half-quoted and inconsistent with the other ~300 prose mentions. Reverted the
whole quoting pass across those three files with a sed unquote, then re-applied quotes at the seven
known line numbers only.

Why it matters: the shipped convention is now uniform and mechanically checkable — quoted inside
shell, bare in prose — instead of "quoted wherever a script happened to reach first".


[2026-08-15T15:00] DISCOVERY: _lib.sh resolved adapters in the wrong precedence for D10 — sanctioned touch #2

type: discovery
timestamp: 2026-08-15T15:00
where: plugin/skills/_shared/procedures/bin/_lib.sh
claude_md_gap: false
tags: [d10, sanctioned-touch, helper-tier, r7, coexistence-window]

Ruled by the lead (2026-08-15): this is inside sanctioned restorative touch #2 (D10), not beyond
it.
$_SKILLS_ROOT is BASH_SOURCE-derived, so post-move it is the plugin-bundled scope, and the
swap implements D10's project → bundled → user exactly; S-4's desk-check verifies the actual
discovery order against D10, not merely the documented one, so the code had to move with the prose.
The truthful error message was ruled right too.

Context: the class-1 sweep found one ~/.claude/skills hit inside a helper script —
_lib.sh:97, the _die message naming the directories _resolve_adapter_bin() searched.

Pattern: the message was accurate, and that was the problem. _resolve_adapter_bin() ordered its
candidates project → user scope$_SKILLS_ROOT (the plugin-bundled adapter, resolved as a
sibling of _shared). D10 mandates project → plugin-bundled → user. Fixing only the string would
have made it describe an order the code does not implement, so I swapped the two candidate entries
and rewrote the message with the three real paths.

Why it matters: this is not cosmetic during Phase 4. The coexistence window keeps the 15 farm
links at ~/.claude/skills/<adapter> alive while ~/.claude/skills/dev points at plugin/. Under
the old order a helper invoked through the plugin form would have resolved the farm's
pre-migration adapter ahead of the plugin's own bundled one — a mixed-form defect squarely in R7's
class. Under the pure farm form the two candidates are the same directory, so the swap is a no-op
there; it only changes behavior when the two scopes genuinely differ, which is exactly the case it
is meant to disambiguate.

This is the only place I changed skill logic.

Verified empirically, not just by desk-check. This machine still carries the live farm link
~/.claude/skills/local-fs -> …/devwork-skills/.claude/skills/local-fs (the pre-migration main
checkout), so the two scopes genuinely differ here — this box is the coexistence-window scenario.
A probe that sources _lib.sh and calls _setup_adapter() against a scratch local-fs project
resolves …/devwork-skills-50/plugin/skills/local-fs/bin, the plugin-bundled adapter. Under the old
order it would have resolved the farm copy. All five helper-tier flow tests
(_shared/procedures/test/{smoke,disposition-flow,promotion-flow,release-flow,slot-flow}.sh) pass
through this path.


Scan-population ruling (lead, 2026-08-15) and what it required

Class Population Exemption
1 — ~/.claude/skills/ and $HOME/.claude/skills literals all files under plugin/skills/**, .md and .sh explicit anchor only
2 — bare _shared/ .md only, fences and prose .sh exempt as a class
3 — /dev:X with X outside the 14 all files none

Why .sh is exempt from class 2 (recording the reasoning, since it is the non-obvious one):
D3's zero-helper-change ruling. Helper code self-locates relative to BASH_SOURCE rather than
naming _shared/ as a path, and the ~20 remaining _shared/ mentions in .sh are developer-facing
header comments (# Spec: _shared/procedures/phase-outcome.md) that are accurate repo-relative
pointers. Prefixing them with ${CLAUDE_PLUGIN_ROOT} would be actively wrong — that variable does
not exist in a shell process (D12: it is load-time text substitution in markdown only), so the
comment would name a path no reader could resolve.

Allow-anchors placed — the complete allowlist (5)

All five document user scope as the third and last adapter-discovery location per D10. The
rewrite script skips any line carrying the anchor, which is what keeps it idempotent against these.
Markdown prose uses <!-- allow: user-scope-path -->; shell (including shell inside a fence) uses a
trailing # allow: user-scope-path.

File:line Form Why
_shared/procedures/phase-outcome.md:52 HTML Adapter dispatch precedence list, item 3. User scope is a real discovery location; naming it is the point of the list.
setup/SKILL.md:108 HTML /dev:setup Step 1 "validate the adapter is installed" precedence list, item 3 — /dev:setup must tell the operator every place it looked.
_shared/procedures/readiness-check.md:50 shell The local-fs probe_pin resolution loop's third candidate. It is inside a sh fence, so the shell anchor form is the syntactically valid one — an HTML comment would become literal text in the snippet.
_shared/procedures/bin/_lib.sh:84 shell _resolve_adapter_bin()'s third candidate — the executable form of the same precedence.
_shared/procedures/bin/_lib.sh:100 shell The _die message that names all three searched scopes.

Both shell anchor placements are unusual enough that I verified them functionally rather than
trusting bash -n: a comment inside a candidates=( … ) array literal still yields 3 elements, and
a trailing comment after a line-continued for … ; do still iterates all 3 items. Note for the
lint WU:
a line-level anchor scanner must accept the shell form in .md files too, because of
readiness-check.md:50.

Class-4 hand rewrites (D10 precedence, sanctioned restorative touch #2)

  • _shared/procedures/phase-outcome.md §Adapter dispatch item 4 — flat "project-level or user-level"
    prose replaced by the explicit three-scope, first-hit-wins list.
  • setup/SKILL.md Step 1 — same list, plus "If neither exists" → "If none exists" now that there are
    three.
  • _shared/procedures/readiness-check.md — the prose describing the precedence, and the local-fs
    probe_pin resolution loop, which gained "${CLAUDE_PLUGIN_ROOT}/skills/local-fs/bin" between the
    project-scope and $HOME entries. Still POSIX sh: a line-continued for list, every element
    quoted.
  • _shared/forge-contract.md §1 layering box — the rewritten paths and /dev: names blew the ASCII
    box's alignment. Re-padded to a 66-column inner width, with the schemas path moved to its own
    continuation line so the box stays readable.

Judgment calls (flagged for the lead)

  1. Adapter smoke-test paths → plugin-relative. tea-cli/SKILL.md:196 said
    .claude/skills/tea-cli/test/smoke.sh and glab-cli/SKILL.md:184 said
    .claude/skills/glab-cli/test/release-smoke.sh. Both read "run the test that ships with this
    adapter, from inside a real project" — the script is plugin content, not consumer-project content,
    and post-migration a consumer has no .claude/skills/tea-cli/. Rewritten to
    ${CLAUDE_PLUGIN_ROOT}/skills/<adapter>/test/<script> (quoted in tea-cli's fence, bare in
    glab-cli's prose). This is the one place I read a .claude/skills/<adapter> path as plugin rather
    than project.
  2. Left project-relative, deliberately — all of these mean the consumer's own tree:
    learning.v1.md:64,74 and learnings.md:73,81 (.claude/skills/stack-<name>/SKILL.md, the
    project-local stack skill the learnings procedure writes and commits); develop/SKILL.md:552
    (same); git-worktrees/SKILL.md:160,169,262,274 (symlinking a project's .claude/skills into a
    worktree — an explicitly project-scoped operation, and the surrounding text is about what the
    project commits); readiness-check.md:48 (./.claude/skills/local-fs/bin, project-scope override).
  3. local-fs/SKILL.md:280 points at a file that does not exist
    ${CLAUDE_PLUGIN_ROOT}/skills/_shared/schemas/link.v1.md. Verified pre-existing at 77a5f2f: it is
    a forward-looking TODO ("promote link-parent:v1/link-child:v1 to …when a second adapter needs
    it"), not a dangling reference this WU created. Rewritten as a plugin path because that is where it
    would land. Every one of the other 44 distinct .md targets resolves on disk.
  4. Helper-tier user-facing strings — raised, ruled, done. 10 _die messages printed run /setup,
    which is wrong post-cutover. The lead ruled them in scope; all 10 now read run /dev:setup:
    _shared/procedures/bin/_lib.sh:57,98; tea-cli/bin/_lib.sh:48,88; gh-cli/bin/_lib.sh:51,91;
    glab-cli/bin/_lib.sh:44,84; local-fs/bin/create_issue.sh:23; local-fs/bin/_lib.sh:216. These
    are the complete set — after the rewrite, zero non-comment bare /<shipped-skill> occurrences
    remain in any .sh under plugin/skills/**. The ~50 comment-only hits (# Owned by /promote.)
    stay, per the same ruling.

Gates

  • bash scripts/rewrite-plugin-refs.sh --check → 0 files would change (idempotent).
  • bash scripts/lint-conventions.shlint-conventions: clean.
  • bash -n over all six edited helper scripts → ok.
  • All five helper-tier flow tests pass:
    _shared/procedures/test/{smoke,disposition-flow,promotion-flow,release-flow,slot-flow}.sh.
  • Sweeps under the ruled populations, all empty: class-1 without anchor (all files); bare _shared/
    (.md); /dev: names outside the 14 (all files); unbraced $CLAUDE_PLUGIN_ROOT; escaped
    \${CLAUDE_PLUGIN_ROOT}; unquoted ${CLAUDE_PLUGIN_ROOT} inside a shell fence; non-comment bare
    /<shipped-skill> in .sh.
  • Non-suite slash names verified still bare: /simplify (21), /closeout (12), /qa-playbook (16),
    /qa-* (4), /qa-domain (5), /qa-report (4), /qa-validate (1), /vibecoding (4). No
    /meta-amend or /meta-retrospective occurs in the shipped tree at all.
  • S-5: git diff --stat 77a5f2f..HEAD touches only plugin/skills/** and
    scripts/rewrite-plugin-refs.sh — nothing under sdlc-*, sdlc-dogfood-run-reports/,
    sdlc-retrospectives/, README.md, .claude/skills/meta-*, .devwork/, or the manifests.

[2026-08-15T15:40] COMPLETE

type: complete
variant: development
timestamp: 2026-08-15T15:40
iterations: 3

Hardest part: deciding, for each .claude/skills/<something> path, whether it meant "inside the
shipped plugin" or "inside the consumer's project" — the two are textually identical and only the
surrounding paragraph distinguishes them.

If I did this again: I would write the fence-aware quoting pass into the script from the start
instead of bolting on a one-shot awk afterwards. That shortcut cost a full revert-and-redo across
three files, and it was the only step in the unit that damaged text I had already got right.

<!-- dev-journal:v1 wu=WU-50-3-2 skill=develop --> # Dev journal — WU-50-3-2: rewrite shipped-skill references to the `dev` plugin namespace Worktree: `/home/jochem/.claude/repos/devwork-skills-50` (branch `feature/50-ship-the-suite-as-a-claude-code-marketplace-plugin`) Baseline: `77a5f2f` (WU-50-3-1 restructure) ## Per-class counts (occurrences, not lines) over `plugin/skills/**/*.md` | Class | At 77a5f2f | After | | --- | --- | --- | | 1 — `~/.claude/skills/…` | 56 | **2** (both allow-anchored) | | 2 — bare `_shared/…` | 299 | **0** | | 3 — bare `/<shipped-skill>` | 565 | **0** | | `${CLAUDE_PLUGIN_ROOT}` introduced | 0 | 358 | | `/dev:<skill>` introduced | 0 | 565 | The arithmetic closes exactly: 358 = (56 − 2 class-1) + 299 class-2 + 5 hand-added occurrences (the readiness-check `for` list, the phase-outcome precedence list, the setup precedence list, and the two adapter smoke-test paths). Class 3 is 1:1 — 565 in, 565 out, and every `/dev:` name is one of the 14. Files changed: 69 under `plugin/skills/**` + `scripts/rewrite-plugin-refs.sh`. --- ## [2026-08-15T14:05] DISCOVERY: quoting hygiene is fence-state-dependent, so the mechanical pass had to track fences ```yaml type: discovery timestamp: 2026-08-15T14:05 where: scripts/rewrite-plugin-refs.sh tags: [d12, portability, awk] ``` **Context:** D12 requires `"${CLAUDE_PLUGIN_ROOT}/…"` double-quoted *at use sites* but permits the bare form in prose. A blind substitution can satisfy neither, because the same textual path is a prose cross-reference in one line and an executable argv[0] in the next. **Pattern:** the rewrite script carries the same CommonMark fence tracker `scripts/lint-conventions.sh` uses (a fence closes only on ≥ its own backtick count), and applies the quoting pass only while inside a `bash`/`sh`/`zsh`/`shell` fence. It also declines to quote a token carrying a glob or a `{placeholder}` — quoting `…/bin/*` would suppress the glob, so those are left for hand review. In the end no such token turned out to live in a shell fence. **Why it matters:** it keeps the audit record honest. Everything the script did is mechanically reproducible and idempotent (`--check` re-run is a no-op); everything else is in this journal. --- ## [2026-08-15T14:20] STRUGGLE: the class-3 boundary rule silently skipped sentence-final invocations ```yaml type: struggle timestamp: 2026-08-15T14:20 attempts: 2 resolved: true tags: [polysemy, class-3, regex-boundary] ``` **Problem:** rewrite `/setup` → `/dev:setup` only where it is an invocation, never where it is a path. **Expected:** all 565 invocations rewritten. **Actual:** 555 rewritten; 10 left bare. **Attempts:** 1. Trailing-boundary rule "the char after the name must not be `[A-Za-z0-9_./-]`". This correctly protects `/develop.md` (a file) but also rejects `re-run /setup.` (a sentence). The 10 misses were all sentence-final: `integrate/SKILL.md:108,250`, `readiness-check.md:166,175`, `promote/SKILL.md:125`, `resolve/SKILL.md:138`, `develop/SKILL.md:437,495,545`, `setup/SKILL.md:114`. 2. Refined rule: a trailing `.` counts as sentence punctuation **unless a word character follows it** (which would make it an extension). Fixed in the script rather than by hand, so the script stays the complete record of the mechanical pass. **Key insight:** the trailing-boundary set and the leading-boundary set are not symmetric. A `/` or `~` *before* the name always means path; a `.` *after* it usually means end of sentence. --- ## [2026-08-15T14:45] DISCOVERY: the awk quoting fixup over-reached into prose, and had to be narrowed ```yaml type: discovery timestamp: 2026-08-15T14:45 where: plugin/skills/_shared/procedures/phase-outcome.md tags: [d12, consistency] ``` **Context:** 7 helper invocations sit in **unlabeled** ``` fences or inside a blockquoted ```` > ```bash ```` fence, so neither the linter's tier-1 extractor nor the rewrite script's quoting pass sees them. I hand-quoted them with a one-shot awk over three files. **Pattern:** that awk matched the first occurrence per *line*, which also quoted backticked prose mentions in the same lines (e.g. the `phase-outcome.md` API reference table, `develop/SKILL.md:43`), leaving those lines half-quoted and inconsistent with the other ~300 prose mentions. Reverted the whole quoting pass across those three files with a `sed` unquote, then re-applied quotes at the seven known line numbers only. **Why it matters:** the shipped convention is now uniform and mechanically checkable — quoted inside shell, bare in prose — instead of "quoted wherever a script happened to reach first". --- ## [2026-08-15T15:00] DISCOVERY: `_lib.sh` resolved adapters in the wrong precedence for D10 — sanctioned touch #2 ```yaml type: discovery timestamp: 2026-08-15T15:00 where: plugin/skills/_shared/procedures/bin/_lib.sh claude_md_gap: false tags: [d10, sanctioned-touch, helper-tier, r7, coexistence-window] ``` **Ruled by the lead (2026-08-15): this is inside sanctioned restorative touch #2 (D10), not beyond it.** `$_SKILLS_ROOT` is `BASH_SOURCE`-derived, so post-move it *is* the plugin-bundled scope, and the swap implements D10's project → bundled → user exactly; S-4's desk-check verifies the **actual** discovery order against D10, not merely the documented one, so the code had to move with the prose. The truthful error message was ruled right too. **Context:** the class-1 sweep found one `~/.claude/skills` hit inside a helper script — `_lib.sh:97`, the `_die` message naming the directories `_resolve_adapter_bin()` searched. **Pattern:** the message was accurate, and that was the problem. `_resolve_adapter_bin()` ordered its candidates project → **user scope** → `$_SKILLS_ROOT` (the plugin-bundled adapter, resolved as a sibling of `_shared`). D10 mandates project → plugin-bundled → user. Fixing only the string would have made it describe an order the code does not implement, so I swapped the two candidate entries and rewrote the message with the three real paths. **Why it matters:** this is not cosmetic during Phase 4. The coexistence window keeps the 15 farm links at `~/.claude/skills/<adapter>` alive while `~/.claude/skills/dev` points at `plugin/`. Under the old order a helper invoked *through the plugin form* would have resolved the farm's pre-migration adapter ahead of the plugin's own bundled one — a mixed-form defect squarely in R7's class. Under the pure farm form the two candidates are the same directory, so the swap is a no-op there; it only changes behavior when the two scopes genuinely differ, which is exactly the case it is meant to disambiguate. This is the only place I changed skill *logic*. **Verified empirically, not just by desk-check.** This machine still carries the live farm link `~/.claude/skills/local-fs -> …/devwork-skills/.claude/skills/local-fs` (the pre-migration main checkout), so the two scopes genuinely differ here — this box *is* the coexistence-window scenario. A probe that sources `_lib.sh` and calls `_setup_adapter()` against a scratch `local-fs` project resolves `…/devwork-skills-50/plugin/skills/local-fs/bin`, the plugin-bundled adapter. Under the old order it would have resolved the farm copy. All five helper-tier flow tests (`_shared/procedures/test/{smoke,disposition-flow,promotion-flow,release-flow,slot-flow}.sh`) pass through this path. --- ## Scan-population ruling (lead, 2026-08-15) and what it required | Class | Population | Exemption | | --- | --- | --- | | 1 — `~/.claude/skills/` **and** `$HOME/.claude/skills` literals | **all** files under `plugin/skills/**`, `.md` and `.sh` | explicit anchor only | | 2 — bare `_shared/` | **`.md` only**, fences and prose | `.sh` exempt **as a class** | | 3 — `/dev:X` with X outside the 14 | all files | none | **Why `.sh` is exempt from class 2** (recording the reasoning, since it is the non-obvious one): D3's zero-helper-change ruling. Helper code self-locates relative to `BASH_SOURCE` rather than naming `_shared/` as a path, and the ~20 remaining `_shared/` mentions in `.sh` are developer-facing header comments (`# Spec: _shared/procedures/phase-outcome.md`) that are accurate repo-relative pointers. Prefixing them with `${CLAUDE_PLUGIN_ROOT}` would be actively wrong — that variable does not exist in a shell process (D12: it is load-time text substitution in markdown only), so the comment would name a path no reader could resolve. ## Allow-anchors placed — the complete allowlist (5) All five document user scope as the **third and last** adapter-discovery location per D10. The rewrite script skips any line carrying the anchor, which is what keeps it idempotent against these. Markdown prose uses `<!-- allow: user-scope-path -->`; shell (including shell inside a fence) uses a trailing `# allow: user-scope-path`. | File:line | Form | Why | | --- | --- | --- | | `_shared/procedures/phase-outcome.md:52` | HTML | Adapter dispatch precedence list, item 3. User scope is a real discovery location; naming it is the point of the list. | | `setup/SKILL.md:108` | HTML | `/dev:setup` Step 1 "validate the adapter is installed" precedence list, item 3 — `/dev:setup` must tell the operator every place it looked. | | `_shared/procedures/readiness-check.md:50` | shell | The `local-fs` `probe_pin` resolution loop's third candidate. It is *inside a `sh` fence*, so the shell anchor form is the syntactically valid one — an HTML comment would become literal text in the snippet. | | `_shared/procedures/bin/_lib.sh:84` | shell | `_resolve_adapter_bin()`'s third candidate — the executable form of the same precedence. | | `_shared/procedures/bin/_lib.sh:100` | shell | The `_die` message that names all three searched scopes. | Both shell anchor placements are unusual enough that I verified them functionally rather than trusting `bash -n`: a comment inside a `candidates=( … )` array literal still yields 3 elements, and a trailing comment after a line-continued `for … ; do` still iterates all 3 items. **Note for the lint WU:** a line-level anchor scanner must accept the shell form in `.md` files too, because of `readiness-check.md:50`. ## Class-4 hand rewrites (D10 precedence, sanctioned restorative touch #2) - `_shared/procedures/phase-outcome.md` §Adapter dispatch item 4 — flat "project-level or user-level" prose replaced by the explicit three-scope, first-hit-wins list. - `setup/SKILL.md` Step 1 — same list, plus "If neither exists" → "If none exists" now that there are three. - `_shared/procedures/readiness-check.md` — the prose describing the precedence, and the `local-fs` `probe_pin` resolution loop, which gained `"${CLAUDE_PLUGIN_ROOT}/skills/local-fs/bin"` between the project-scope and `$HOME` entries. Still POSIX `sh`: a line-continued `for` list, every element quoted. - `_shared/forge-contract.md` §1 layering box — the rewritten paths and `/dev:` names blew the ASCII box's alignment. Re-padded to a 66-column inner width, with the schemas path moved to its own continuation line so the box stays readable. ## Judgment calls (flagged for the lead) 1. **Adapter smoke-test paths → plugin-relative.** `tea-cli/SKILL.md:196` said `.claude/skills/tea-cli/test/smoke.sh` and `glab-cli/SKILL.md:184` said `.claude/skills/glab-cli/test/release-smoke.sh`. Both read "run the test that ships with this adapter, from inside a real project" — the script is plugin content, not consumer-project content, and post-migration a consumer has no `.claude/skills/tea-cli/`. Rewritten to `${CLAUDE_PLUGIN_ROOT}/skills/<adapter>/test/<script>` (quoted in tea-cli's fence, bare in glab-cli's prose). This is the one place I read a `.claude/skills/<adapter>` path as *plugin* rather than *project*. 2. **Left project-relative, deliberately** — all of these mean the consumer's own tree: `learning.v1.md:64,74` and `learnings.md:73,81` (`.claude/skills/stack-<name>/SKILL.md`, the project-local stack skill the learnings procedure writes and commits); `develop/SKILL.md:552` (same); `git-worktrees/SKILL.md:160,169,262,274` (symlinking a *project's* `.claude/skills` into a worktree — an explicitly project-scoped operation, and the surrounding text is about what the project commits); `readiness-check.md:48` (`./.claude/skills/local-fs/bin`, project-scope override). 3. **`local-fs/SKILL.md:280` points at a file that does not exist** — `${CLAUDE_PLUGIN_ROOT}/skills/_shared/schemas/link.v1.md`. Verified pre-existing at `77a5f2f`: it is a forward-looking TODO ("promote `link-parent:v1`/`link-child:v1` to …when a second adapter needs it"), not a dangling reference this WU created. Rewritten as a plugin path because that is where it would land. Every one of the other 44 distinct `.md` targets resolves on disk. 4. **Helper-tier user-facing strings — raised, ruled, done.** 10 `_die` messages printed `run /setup`, which is wrong post-cutover. The lead ruled them in scope; all 10 now read `run /dev:setup`: `_shared/procedures/bin/_lib.sh:57,98`; `tea-cli/bin/_lib.sh:48,88`; `gh-cli/bin/_lib.sh:51,91`; `glab-cli/bin/_lib.sh:44,84`; `local-fs/bin/create_issue.sh:23`; `local-fs/bin/_lib.sh:216`. These are the complete set — after the rewrite, zero non-comment bare `/<shipped-skill>` occurrences remain in any `.sh` under `plugin/skills/**`. The ~50 comment-only hits (`# Owned by /promote.`) stay, per the same ruling. ## Gates - `bash scripts/rewrite-plugin-refs.sh --check` → 0 files would change (idempotent). - `bash scripts/lint-conventions.sh` → `lint-conventions: clean`. - `bash -n` over all six edited helper scripts → ok. - All five helper-tier flow tests pass: `_shared/procedures/test/{smoke,disposition-flow,promotion-flow,release-flow,slot-flow}.sh`. - Sweeps under the ruled populations, all empty: class-1 without anchor (all files); bare `_shared/` (`.md`); `/dev:` names outside the 14 (all files); unbraced `$CLAUDE_PLUGIN_ROOT`; escaped `\${CLAUDE_PLUGIN_ROOT}`; unquoted `${CLAUDE_PLUGIN_ROOT}` inside a shell fence; non-comment bare `/<shipped-skill>` in `.sh`. - Non-suite slash names verified still bare: `/simplify` (21), `/closeout` (12), `/qa-playbook` (16), `/qa-*` (4), `/qa-domain` (5), `/qa-report` (4), `/qa-validate` (1), `/vibecoding` (4). No `/meta-amend` or `/meta-retrospective` occurs in the shipped tree at all. - S-5: `git diff --stat 77a5f2f..HEAD` touches only `plugin/skills/**` and `scripts/rewrite-plugin-refs.sh` — nothing under `sdlc-*`, `sdlc-dogfood-run-reports/`, `sdlc-retrospectives/`, `README.md`, `.claude/skills/meta-*`, `.devwork/`, or the manifests. ## [2026-08-15T15:40] COMPLETE ```yaml type: complete variant: development timestamp: 2026-08-15T15:40 iterations: 3 ``` **Hardest part:** deciding, for each `.claude/skills/<something>` path, whether it meant "inside the shipped plugin" or "inside the consumer's project" — the two are textually identical and only the surrounding paragraph distinguishes them. **If I did this again:** I would write the fence-aware quoting pass into the script from the start instead of bolting on a one-shot awk afterwards. That shortcut cost a full revert-and-redo across three files, and it was the only step in the unit that damaged text I had already got right.
Author
Owner
{
  "outcome": "complete",
  "summary": "Rewrote all shipped-skill references into the dev plugin namespace over 69 files: 54 class-1 path refs -> ${CLAUDE_PLUGIN_ROOT}, 299 class-2 bare _shared refs prefixed, 565 class-3 bare invocations -> /dev:<skill> (1:1, all within the 14), D10 discovery-order hand-rewrite incl. the sanctioned _lib.sh precedence fix (project -> bundled -> user, verified empirically against the live farm), 10 helper _die strings -> /dev:setup, 5 allow-anchored user-scope lines. Audit script scripts/rewrite-plugin-refs.sh idempotent; lint clean; helper flow tests pass; S-5 exclusion diff empty. Artifact-shape WU: Test Writer skipped; verified by sweeps + lead's independent re-sweep.",
  "commit_refs": ["2c138e3","8ead6e3","598d16b"],
  "journal_ref": "1013",
  "dependencies_satisfied": ["WU-50-3-1"],
  "dependencies_violated": []
}
<!-- work-unit-outcome:v1 id=WU-50-3-2 skill=develop --> ```json { "outcome": "complete", "summary": "Rewrote all shipped-skill references into the dev plugin namespace over 69 files: 54 class-1 path refs -> ${CLAUDE_PLUGIN_ROOT}, 299 class-2 bare _shared refs prefixed, 565 class-3 bare invocations -> /dev:<skill> (1:1, all within the 14), D10 discovery-order hand-rewrite incl. the sanctioned _lib.sh precedence fix (project -> bundled -> user, verified empirically against the live farm), 10 helper _die strings -> /dev:setup, 5 allow-anchored user-scope lines. Audit script scripts/rewrite-plugin-refs.sh idempotent; lint clean; helper flow tests pass; S-5 exclusion diff empty. Artifact-shape WU: Test Writer skipped; verified by sweeps + lead's independent re-sweep.", "commit_refs": ["2c138e3","8ead6e3","598d16b"], "journal_ref": "1013", "dependencies_satisfied": ["WU-50-3-1"], "dependencies_violated": [] } ```
Author
Owner

Dev journal — WU-50-3-3 (meta lane amendments)

Scope: .claude/skills/meta-amend/SKILL.md, .claude/skills/meta-retrospective/SKILL.md
in the worktree /home/jochem/.claude/repos/devwork-skills-50.

2026-08-15T00:00 DISCOVERY: the D7 root and the drain loop's cd are not in conflict — the order just had to invert

type: discovery
timestamp: 2026-08-15T00:00
where: .claude/skills/meta-retrospective/SKILL.md
tags: [D7, helper-root, adapter-resolution, meta-lane]

Context: the learnings drain loop previously read cd "$P" && B=.claude/skills/_shared/procedures/bin
— one statement doing two incompatible jobs. $P is another project's checkout, so after the cd
neither $(pwd) nor git rev-parse points at devwork-skills, and the relative B resolved against
the wrong repo (this is the already-broken path D7 calls sanctioned restorative touch #1).

Pattern: the cd is still required, but for a different reason than the helper path. _lib.sh
walks up from $PWD for a CLAUDE.md carrying a forge: block (_find_project_root) and resolves the
adapter from that — so the helper binary must come from devwork-skills while the cwd must be the
source project. Resolving B before the cd gives both:

B="$(git rev-parse --show-toplevel)/plugin/skills/_shared/procedures/bin"
cd "$P" || exit 1
"$B/issue-list.sh" --state all

Adapter dirs still resolve correctly too: _lib.sh derives _SKILLS_ROOT from BASH_SOURCE/../../..,
which under this B is plugin/skills, so the bundled adapters are the suite's own while a source
project's own project-scope adapter still wins (D10 precedence).

Why it matters: the skill text now states the reason for the ordering, so a future edit can't
"tidy" the cd back up next to the assignment and silently re-break the drain.

2026-08-15T00:10 DISCOVERY: writing the D7 rationale re-introduced the banned token

type: discovery
timestamp: 2026-08-15T00:10
tags: [D12, verification, prose-vs-code]

Context: my first draft of the rationale paragraph explained D7 by naming ${CLAUDE_PLUGIN_ROOT}
literally ("… is never substituted for the meta lane"). Correct prose, but the WU's verification
requires zero occurrences of that token anywhere in the meta lane, and D12's lint rules key on
the token without caring whether it sits in a fence or in backticked prose.

Pattern: name the concept, not the token — "the plugin-root variable the shipped skills use is
never substituted here". Keeps the explanation, keeps the grep at zero, keeps the D12 rule
mechanically unambiguous.

Why it matters: a scan whose green depends on humans distinguishing prose mentions from use
sites is a scan that will eventually be argued with. Zero means zero.

Call sites changed

D7 helper roots (3):

File Site Before After
meta-amend §5 outcome/close B=.claude/skills/_shared/procedures/bin B="$(git rev-parse --show-toplevel)/plugin/skills/_shared/procedures/bin"
meta-retrospective Inputs, drain loop cd "$P" && B=.claude/skills/… B="$(git rev-parse …)/plugin/skills/…" resolved before cd "$P"
meta-retrospective §5 issue-create inline .claude/skills/_shared/procedures/bin/issue-create.sh B=… + "$B/issue-create.sh"

Helper invocations also moved from "$B"/name.sh to the SREQ's "$B/name.sh" form (5 calls:
post-comment, issue-close, issue-list, comments-scan, issue-create).

Shipped-skill invocation names (4):

  • meta-amend, scope gate escalation: /requirements/dev:requirements
  • meta-retrospective, routing reminder: /requirements/technical-plan/develop
    became /dev:requirements/dev:technical-plan/dev:develop

Bare /meta-amend (routing reminder) and /meta-retrospective (meta-amend frontmatter) left bare.

Lane-gate / path references (2):

  • meta-amend scope gate item 1: the text-only population is now plugin/skills/**/*.md (shipped
    text, procedures, schemas, domains) or .claude/skills/**/*.md (the meta lane) — both are
    legitimate amendment targets, and naming both keeps the fence explicit rather than implied.
  • meta-retrospective preamble: "never touches .claude/skills/** text" → "never touches skill
    text — neither the shipped plugin/skills/** nor the meta lane's own .claude/skills/**".

Kept verbatim (judgment calls)

  1. develop: name the wait-discipline rule the WU brief must restate (meta-amend §5) — this is
    the commit-subject example for the repo's <area>: <imperative sentence> convention. develop
    there is a git commit area, not a slash invocation; commit areas are unchanged by this feature.
  2. scripts/lint-conventions.sh (4 mentions across both files) — repo-local scripts did not move.
  3. sdlc-process-observability-analysis.md §7, sdlc-dogfood-run-reports/*.md,
    sdlc-retrospectives/*
    — repo-local historical/state records, unchanged locations, and S-5
    forbids rewriting the historical set.
  4. "several #26-era findings", tracker refs #10 / #32 — historical citations.
  5. Source-access rung 2 (glab auth status, tea login list, gh auth status) — meta-lane
    read-only CLI exemption, unrelated to plugin layout.

No occurrence of ~/.claude/skills, $HOME/.claude/skills, or the plugin-root token existed in
either file before the change, so there was nothing of that class to remove.

Verification

  • S-4 (D7 pattern): B="$(git rev-parse --show-toplevel)/plugin/skills/_shared/procedures/bin"
    evaluated from (a) the worktree root, (b) plugin/skills, (c) plugin/skills/_shared/procedures
    followed by cd /tmp. All three yield
    /home/jochem/.claude/repos/devwork-skills-50/plugin/skills/_shared/procedures/bin, and
    phase-outcome-read-all.sh is executable there in each case (--help exits 0). git rev-parse --show-toplevel returns the worktree root inside a linked worktree, which is what the slot case
    needs.
  • End-to-end desk-check of the drain pattern (read-only, no forge mutation): resolved B at the
    worktree root, cd'd to a project root, ran "$B/issue-list.sh" --state all — real tea-cli JSON
    returned, rc 0. Confirms the helper resolves the project's adapter from cwd while being invoked
    from the D7 root.
  • Scans: zero hits for ~/.claude/skills, $HOME/.claude/skills, plugin-root token, bare
    /​<shipped-name> invocations, and .claude/skills/{_shared,<shipped-name>} paths. The four
    surviving .claude/skills strings are the meta lane's own scope statements.
  • bash scripts/lint-conventions.shlint-conventions: clean, rc 0.
  • git diff --name-only → exactly the two meta SKILL.md files.

2026-08-15T00:20 COMPLETE

type: complete
variant: development
timestamp: 2026-08-15T00:20
iterations: 2

Hardest part: seeing that the drain loop's cd and its helper path were solving two different
problems that had been collapsed into one line — the fix is an ordering change, not a path swap.
If I did this again: I'd grep for the banned tokens in my own new prose before running the
verification, instead of writing the rationale with the literal token and then rewording it.

<!-- dev-journal:v1 wu=WU-50-3-3 skill=develop --> # Dev journal — WU-50-3-3 (meta lane amendments) Scope: `.claude/skills/meta-amend/SKILL.md`, `.claude/skills/meta-retrospective/SKILL.md` in the worktree `/home/jochem/.claude/repos/devwork-skills-50`. ## 2026-08-15T00:00 DISCOVERY: the D7 root and the drain loop's `cd` are not in conflict — the order just had to invert ```yaml type: discovery timestamp: 2026-08-15T00:00 where: .claude/skills/meta-retrospective/SKILL.md tags: [D7, helper-root, adapter-resolution, meta-lane] ``` **Context:** the learnings drain loop previously read `cd "$P" && B=.claude/skills/_shared/procedures/bin` — one statement doing two incompatible jobs. `$P` is *another* project's checkout, so after the `cd` neither `$(pwd)` nor `git rev-parse` points at devwork-skills, and the relative `B` resolved against the wrong repo (this is the already-broken path D7 calls sanctioned restorative touch #1). **Pattern:** the `cd` is still required, but for a different reason than the helper path. `_lib.sh` walks up from `$PWD` for a CLAUDE.md carrying a `forge:` block (`_find_project_root`) and resolves the adapter from *that* — so the helper binary must come from devwork-skills while the cwd must be the source project. Resolving `B` before the `cd` gives both: ```sh B="$(git rev-parse --show-toplevel)/plugin/skills/_shared/procedures/bin" cd "$P" || exit 1 "$B/issue-list.sh" --state all ``` Adapter *dirs* still resolve correctly too: `_lib.sh` derives `_SKILLS_ROOT` from `BASH_SOURCE/../../..`, which under this `B` is `plugin/skills`, so the bundled adapters are the suite's own while a source project's own project-scope adapter still wins (D10 precedence). **Why it matters:** the skill text now states the reason for the ordering, so a future edit can't "tidy" the `cd` back up next to the assignment and silently re-break the drain. ## 2026-08-15T00:10 DISCOVERY: writing the D7 rationale re-introduced the banned token ```yaml type: discovery timestamp: 2026-08-15T00:10 tags: [D12, verification, prose-vs-code] ``` **Context:** my first draft of the rationale paragraph explained D7 by naming `${CLAUDE_PLUGIN_ROOT}` literally ("… is never substituted for the meta lane"). Correct prose, but the WU's verification requires **zero** occurrences of that token anywhere in the meta lane, and D12's lint rules key on the token without caring whether it sits in a fence or in backticked prose. **Pattern:** name the concept, not the token — "the plugin-root variable the shipped skills use is never substituted here". Keeps the explanation, keeps the grep at zero, keeps the D12 rule mechanically unambiguous. **Why it matters:** a scan whose green depends on humans distinguishing prose mentions from use sites is a scan that will eventually be argued with. Zero means zero. ## Call sites changed **D7 helper roots (3):** | File | Site | Before | After | |---|---|---|---| | meta-amend | §5 outcome/close | `B=.claude/skills/_shared/procedures/bin` | `B="$(git rev-parse --show-toplevel)/plugin/skills/_shared/procedures/bin"` | | meta-retrospective | Inputs, drain loop | `cd "$P" && B=.claude/skills/…` | `B="$(git rev-parse …)/plugin/skills/…"` resolved **before** `cd "$P"` | | meta-retrospective | §5 issue-create | inline `.claude/skills/_shared/procedures/bin/issue-create.sh` | `B=…` + `"$B/issue-create.sh"` | Helper invocations also moved from `"$B"/name.sh` to the SREQ's `"$B/name.sh"` form (5 calls: post-comment, issue-close, issue-list, comments-scan, issue-create). **Shipped-skill invocation names (4):** - meta-amend, scope gate escalation: `/requirements` → `/dev:requirements` - meta-retrospective, routing reminder: `/requirements` → `/technical-plan` → `/develop` became `/dev:requirements` → `/dev:technical-plan` → `/dev:develop` Bare `/meta-amend` (routing reminder) and `/meta-retrospective` (meta-amend frontmatter) left bare. **Lane-gate / path references (2):** - meta-amend scope gate item 1: the text-only population is now `plugin/skills/**/*.md` (shipped text, procedures, schemas, domains) **or** `.claude/skills/**/*.md` (the meta lane) — both are legitimate amendment targets, and naming both keeps the fence explicit rather than implied. - meta-retrospective preamble: "never touches `.claude/skills/**` text" → "never touches skill text — neither the shipped `plugin/skills/**` nor the meta lane's own `.claude/skills/**`". ## Kept verbatim (judgment calls) 1. **`develop: name the wait-discipline rule the WU brief must restate`** (meta-amend §5) — this is the *commit-subject* example for the repo's `<area>: <imperative sentence>` convention. `develop` there is a git commit area, not a slash invocation; commit areas are unchanged by this feature. 2. **`scripts/lint-conventions.sh`** (4 mentions across both files) — repo-local scripts did not move. 3. **`sdlc-process-observability-analysis.md` §7, `sdlc-dogfood-run-reports/*.md`, `sdlc-retrospectives/*`** — repo-local historical/state records, unchanged locations, and S-5 forbids rewriting the historical set. 4. **"several #26-era findings"**, tracker refs #10 / #32 — historical citations. 5. **Source-access rung 2** (`glab auth status`, `tea login list`, `gh auth status`) — meta-lane read-only CLI exemption, unrelated to plugin layout. No occurrence of `~/.claude/skills`, `$HOME/.claude/skills`, or the plugin-root token existed in either file before the change, so there was nothing of that class to remove. ## Verification - **S-4 (D7 pattern):** `B="$(git rev-parse --show-toplevel)/plugin/skills/_shared/procedures/bin"` evaluated from (a) the worktree root, (b) `plugin/skills`, (c) `plugin/skills/_shared/procedures` followed by `cd /tmp`. All three yield `/home/jochem/.claude/repos/devwork-skills-50/plugin/skills/_shared/procedures/bin`, and `phase-outcome-read-all.sh` is executable there in each case (`--help` exits 0). `git rev-parse --show-toplevel` returns the *worktree* root inside a linked worktree, which is what the slot case needs. - **End-to-end desk-check of the drain pattern (read-only, no forge mutation):** resolved `B` at the worktree root, `cd`'d to a project root, ran `"$B/issue-list.sh" --state all` — real tea-cli JSON returned, rc 0. Confirms the helper resolves the project's adapter from cwd while being invoked from the D7 root. - **Scans:** zero hits for `~/.claude/skills`, `$HOME/.claude/skills`, plugin-root token, bare `/​<shipped-name>` invocations, and `.claude/skills/{_shared,<shipped-name>}` paths. The four surviving `.claude/skills` strings are the meta lane's own scope statements. - **`bash scripts/lint-conventions.sh`** → `lint-conventions: clean`, rc 0. - **`git diff --name-only`** → exactly the two meta SKILL.md files. ## 2026-08-15T00:20 COMPLETE ```yaml type: complete variant: development timestamp: 2026-08-15T00:20 iterations: 2 ``` **Hardest part:** seeing that the drain loop's `cd` and its helper path were solving two different problems that had been collapsed into one line — the fix is an ordering change, not a path swap. **If I did this again:** I'd grep for the banned tokens in my *own* new prose before running the verification, instead of writing the rationale with the literal token and then rewording it.
Author
Owner
{
  "outcome": "complete",
  "summary": "Meta lane amended for the plugin layout: 3 helper-root call sites now resolve via the D7 pattern (git rev-parse --show-toplevel, before any cd — fixing the already-broken drain-loop path, sanctioned touch #1), 4 shipped-skill invocations -> /dev: names, lane-gate populations name plugin/skills/** vs .claude/skills/** explicitly; 5 historical/verbatim exceptions kept and cataloged. S-4 D7 check passes from root/subdir/slot; drain pattern desk-checked read-only against the live tea forge; lint clean; diff confined to the two meta SKILL.md files. Artifact-shape WU: Test Writer skipped.",
  "commit_refs": ["f12b2c4"],
  "journal_ref": "1016",
  "dependencies_satisfied": ["WU-50-3-1", "WU-50-3-2"],
  "dependencies_violated": []
}
<!-- work-unit-outcome:v1 id=WU-50-3-3 skill=develop --> ```json { "outcome": "complete", "summary": "Meta lane amended for the plugin layout: 3 helper-root call sites now resolve via the D7 pattern (git rev-parse --show-toplevel, before any cd — fixing the already-broken drain-loop path, sanctioned touch #1), 4 shipped-skill invocations -> /dev: names, lane-gate populations name plugin/skills/** vs .claude/skills/** explicitly; 5 historical/verbatim exceptions kept and cataloged. S-4 D7 check passes from root/subdir/slot; drain pattern desk-checked read-only against the live tea forge; lint clean; diff confined to the two meta SKILL.md files. Artifact-shape WU: Test Writer skipped.", "commit_refs": ["f12b2c4"], "journal_ref": "1016", "dependencies_satisfied": ["WU-50-3-1", "WU-50-3-2"], "dependencies_violated": [] } ```
Author
Owner

Dev Journal — WU-50-3-4 (gate-suite Test Writer)

Test Writer phase

type: complete
variant: development
timestamp: 2026-08-15T13:12
iterations: 1

Scope: Wrote scripts/test-plugin-gates.sh in the isolated worktree
(/home/jochem/.claude/repos/devwork-skills-50, branch
feature/50-ship-the-suite-as-a-claude-code-marketplace-plugin). No
production code touched — scripts/release.sh does not exist and
scripts/lint-conventions.sh was read-only reference.

Red-state table (this run, bash scripts/test-plugin-gates.sh, 24.5s, exit 1)

Scenario Result now Why
positive_control_release_dry_run FAIL release.sh missing (scripts/release.sh does not exist yet) — clean failure, not a crash
scenario_18_dirty_tree FAIL same — release.sh missing
scenario_19_version_disagreement FAIL same — release.sh missing
scenario_20_existing_tag FAIL same — release.sh missing
scenario_23_validator_negative PASS claude plugin validate --strict already exists and already rejects a corrupted name field by name, on both the plugin and marketplace manifests; pristine copies already validate clean
lint_nonvacuity_on_real_tree FAIL current lint-conventions.sh prints only lint-conventions: clean / N violation(s) — no examined-file count at all, so AC-3 non-vacuity is unproven
scenario_24_scan_negative_controls FAIL 14 of 16 sub-checks red: none of class 1 (~/.claude/skills/, $HOME/.claude/skills, both .md and .sh), class 2 (_shared/ unprefixed, fence and prose), class 3 (/dev: outside the 14), class 3b (bare /setup), AC-4 absence (meta-amend/ dir, plugin/scripts/), D4 (realpath), or D12 ($CLAUDE_PLUGIN_ROOT unbraced/escaped/unquoted) is caught — none of these rules exist yet in lint-conventions.sh. The two anchor-exemption sub-checks (class1_md_anchored_exempt, class1_sh_anchored_exempt) read as green today, but only because nothing is flagged at all yet — that's a coincidental green, not a verified exemption; it becomes a real assertion once the Implementer adds the class-1 rule. Post-cleanup non-vacuity also fails, for the same reason as lint_nonvacuity_on_real_tree.

7 scenarios: 1 pass, 6 fail, 0 skipped. Verified the loud-skip path separately
(env -i PATH=/usr/bin:/bin bash scripts/test-plugin-gates.sh): with claude
off PATH, scenario_23_validator_negative reports SKIP (LOUD): ... claude CLI not found on PATH instead of a silent pass, and the summary line counts
it under skipped, not passed.

Contract ambiguities pinned down

  • Refusal scenarios run without --dry-run. The WU brief restricts
    --dry-run to "exercise ONLY with --dry-run" for the green path; it
    says nothing about --dry-run for the three refusals. I run those without
    it (still only ever in a scratch clone) because with --dry-run a refusal
    test can't distinguish "release.sh's refusal logic caught this" from "dry-run
    suppressed all side effects regardless of logic" — e.g. scenario 20's "the
    existing tag still points at its original commit" assertion would be true
    under --dry-run even if release.sh's tag-reuse check were missing
    entirely. Real invocation in a scratch clone gives a meaningful test;
    --dry-run there would not.
  • Tag naming. Confirmed via claude plugin tag --help: {name}--v{version}.
    plugin/.claude-plugin/plugin.json .name is "dev", so the tag prefix is
    literally dev--v (TAG_PREFIX in the script). Not otherwise stated in
    the WU brief.
  • D6 (SREQ) says release.sh "commits the bump before tagging" and
    delegates tag enforcement to claude plugin tag.
    I did not model that
    internal mechanism in the tests — the suite treats release.sh [--dry-run] <version> as a black box against the five behaviors the WU brief spells
    out (exit codes, tag-set deltas, manifest-field deltas, message content).
    Whatever the Implementer's internal flow is (bump-then-tag vs. tag-only),
    these assertions hold either way.
  • Version-disagreement message assertion is an OR, not an AND-of-strings.
    "identifies the disagreement (both values or both field locations)" — I
    accept either (a) both version strings appear in the output, or (b) both
    plugin.json and marketplace (case-insensitive) appear. Whichever
    phrasing the Implementer's message uses, one of the two should hold
    naturally; I didn't want to force one specific wording.
  • lint_nonvacuity_on_real_tree and the post-cleanup half of scenario 24
    scrape the "examined N file(s)" count with a loose regex
    (extract_examined_count
    in the script) rather than requiring one exact phrasing — it matches either
    word order (N files examined / examined N files). Whatever line the
    Implementer adds, it just needs a number next to the word "examined" and
    the word "file(s)".
  • Fixture files for scenario 24 seeding: plugin/skills/setup/SKILL.md
    (has existing shell fences, safe to append after) and
    plugin/skills/_shared/procedures/bin/phase-outcome-read-latest.sh (a real
    helper under a bin/ dir, needed for the D4 canonicalization-ban check,
    which the WU brief scopes to plugin/skills/**/bin/). Both are
    snapshotted and restored between sub-checks so each seed is isolated.
  • AC-4 absence checks assert on substring match of the directory path
    (plugin/skills/meta-amend / plugin/scripts) rather than a path:line
    hit, since these are presence-of-a-path violations, not line-level ones —
    unlike every other class 1–3/D4/D12 check, which asserts an exact
    path:line: hit to avoid a false PASS from an unrelated violation
    elsewhere in the same file.
  • make_lint_copy() copies only scripts/lint-conventions.sh +
    plugin/
    (not the whole repo) into scratch — lint-conventions.sh
    computes REPO_ROOT from its own BASH_SOURCE, and only requires
    <root>/plugin/skills to exist (the .claude/skills root is optional,
    skipped if absent). This keeps each of the 16 scenario-24 lint runs at
    ~1.4s instead of copying .git history or the ~30 unrelated top-level
    sdlc-*.md docs.

Hardest part: Picking assertion granularity for scenario 24's "flagged"
checks — matching only the exact path:lineno: of the seeded line (not just
"the file appears anywhere in the output") so the checks stay meaningful
after the Implementer's rules exist, rather than trivially passing because
some other violation in the same fixture file got reported.

If I did this again: I'd pin the fixture files (setup/SKILL.md,
_shared/.../bin/phase-outcome-read-latest.sh) down earlier — I picked them
after already sketching the seed logic, which meant re-deriving expected
line numbers by hand once instead of parameterizing from the start.


Implementer phase

type: complete
variant: development
timestamp: 2026-08-15T16:05
iterations: 2

Scope: Extended scripts/lint-conventions.sh with a tier-3 shipped-tree
section and created scripts/release.sh, both in the isolated worktree
(/home/jochem/.claude/repos/devwork-skills-50). scripts/test-plugin-gates.sh
was read-only — no test was edited. Nothing under plugin/, .claude/, the
main checkout, or ~/.claude/skills/ was touched.

Green run: bash scripts/test-plugin-gates.sh — 7 passed, 0 failed,
0 skipped, exit 0, 32.6s (red baseline was 24.5s; the delta is the tier-3
scans, which scenario 24 invokes ~17 times).

Two iterations: iteration 1 added the lint rules (all 15 scenario-24 sub-checks
green on first run, 3/7 scenarios); iteration 2 added release.sh (7/7). The
refactor pass that followed changed no behavior.

[2026-08-15T15:20] DISCOVERY: claude plugin tag --dry-run enforces only one third of its trio

type: discovery
timestamp: 2026-08-15T15:20
where: scripts/release.sh
tags: [plugin-cli, release, D6, external-contract]

Context: SREQ D6 says release enforcement is "delegated to claude plugin tag", and the WU brief separately requires release.sh's own git-only refusals.
Before writing either, I probed the CLI in a scratch clone to see how much
--dry-run actually enforces.

Pattern: claude plugin tag --dry-run enforces tag uniqueness only. A
deliberately dirtied tree passed --dry-run with exit 0 and a full "would
create tag" report; the same tree with an existing dev--v0.1.0 tag exited 1
with a clear message. So the dirty-tree and version-agreement legs of the
enforcement trio are live only on the real (non-dry-run) tagging path.

Why it matters: this makes release.sh's three git-only refusals load-bearing
rather than a convenience layer — under --dry-run they are the only thing
standing between a dirty tree and a green gate report. Recorded here because
D6's wording ("enforcement delegated to the CLI") would otherwise read as
license to drop them. Also pinned down: claude plugin tag and plugin validate both take a path argument to the plugin root; with no argument
tag looks for <cwd>/.claude-plugin/plugin.json and fails, so every call site
passes plugin (or an absolute root) explicitly.

[2026-08-15T15:35] DISCOVERY: release.sh must resolve its repo from the cwd, not from its own location

type: discovery
timestamp: 2026-08-15T15:35
where: scripts/release.sh
tags: [portability, worktrees, test-harness]

Context: Every helper in this repo resolves its root from BASH_SOURCE.
Copying that idiom into release.sh would have been the natural move.

Pattern: REPO_ROOT="$(git rev-parse --show-toplevel)" — the repo we were
invoked in, not the one the script lives in.

Why it matters: the gate suite runs bash "$RELEASE_SH" <args> with
cwd set to a scratch clone while $RELEASE_SH points at the working-tree
copy. A BASH_SOURCE-derived root would have made all four release scenarios
operate on the real worktree — creating real tags and real bump commits in
the repo under development, while the tests dutifully reported that the scratch
clone was unchanged. Same property matters in production: a release must be
cuttable from a clone or a worktree. This is the inverse of D7 (the meta lane
also uses git rev-parse --show-toplevel, for the same class of reason: cwd
resets and worktree slots break location-derived roots).

[2026-08-15T15:45] STRUGGLE: D4's "zero violations today" was not zero

type: struggle
timestamp: 2026-08-15T15:45
affected: [lint-conventions.sh, slot-retire.sh]
attempts: 2
resolved: true
tags: [D4, rule-scope, false-positive]

Problem: The WU brief specifies the D4 canonicalization ban as realpath,
readlink -f, cd -P, pwd -P in shipped bin/ helpers, described as a pure
regression fence with "zero today".

Expected: a literal implementation would be clean on the real tree.

Actual: two live hits. plugin/skills/_shared/procedures/bin/slot-retire.sh:156
(canon_dir() { ... (cd "$1" && pwd -P); ... }) and the slot test fixture
test/fixtures/slot/bin/teardown-marker.sh:9 (pwd -P > .../teardown-ran-for-$name).
A literal rule would have failed lint_nonvacuity_on_real_tree, which requires
exit 0 on the real tree — and I cannot fix it by anchoring the lines, since
plugin/ is out of bounds for this work unit.

Attempts:

  1. Implement the ban literally over */bin/*.sh — two false positives.
  2. Split the rule by what D4 actually protects (below).

Solution: realpath and readlink -f stay banned outright — they have no
legitimate use in a shipped helper and are genuinely zero today. cd -P /
pwd -P are flagged only when the same line also references $0 or
BASH_SOURCE, i.e. when the script is canonicalizing its own location.

Key insight: D4 is not a ban on canonicalization, it is a ban on
self-canonicalization — "the loaded (symlink) path is canonical; helpers keep
logical BASH_SOURCE+dirname". slot-retire.sh canonicalizes a directory the
caller passed in, and its own 10-line comment explains why that is required
for correctness (comparing a slot's recorded parent against the slots dir when
one side is reached through a symlinked parent). Banning that would have been
enforcing the letter against the purpose. The narrowed rule still catches the
seeded regression (p="$(realpath "$0")") and would catch the realistic
regression (cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd). Known limit,
documented in the script: a self-location split across two lines
(d="$(dirname "${BASH_SOURCE[0]}")" then root="$(cd "$d" && pwd -P)")
escapes the line-scoped check.

[2026-08-15T15:52] STRUGGLE: the path prefix of a grep hit is part of the haystack

type: struggle
timestamp: 2026-08-15T15:52
affected: [lint-conventions.sh]
attempts: 2
resolved: true
tags: [class-2, grep, false-positive]

Problem: Class 2 flags _shared/ references that lack the
${CLAUDE_PLUGIN_ROOT}/skills/ prefix. The obvious implementation — grep for
_shared/, filter out the prefixed form — reported ~18 clean files.

Expected: zero hits on the real tree (a survey confirmed every prose and
fence reference is correctly prefixed).

Actual: every hit was a file living under plugin/skills/_shared/ — the
path:lineno: prefix that grep -rn prepends contains _shared/ itself, so
any line-level filter sees the string whether or not the content has it.

Solution: strip the ^[^:]+:[0-9]+: prefix in awk, do the prefixed-form
removal on the content only, and print the original line if _shared/
survives. Same shape reused for class 3b's .sh-comment exemption.

Key insight: it is worth writing the negative control for a scan before
trusting its green — but equally worth running the new rule against the real
tree first, because that is where this one showed up. The seeded fixture in
plugin/skills/setup/SKILL.md would never have caught it: setup/ has no
_shared in its path.

Rule-scope decisions taken beyond the brief

  • D4 split into two rules (above) — the only place I departed from the
    brief's literal population, and the only one that changes what is flagged.
  • Class 3b scans .sh as well as .md. The brief scopes it to "shipped
    .md text" but also says "exempt .sh comment lines", which only makes sense
    if .sh is in the population. Scanning both with the comment exemption is
    the stronger fence and is clean today: all ~40 bare /promote-style hits in
    helpers are pure comment lines naming an owning phase; non-comment hits are
    zero.
  • Exemption anchors are per-rule, not shared. Class 1 honours
    allow: user-scope-path (5 lines today); class 3b honours
    allow: bare-skill-name (0 today). A single blanket anchor would let a
    class-1 exemption silently suppress an unrelated class-3b hit.
  • Marketplace validation degrades to a loud skip when the manifest is
    absent
    , rather than a violation. make_lint_copy() builds a tree with
    plugin/ but no root .claude-plugin/, so a hard requirement there would
    fail scenario 24's post-cleanup clean-scan assertion. The skip names the
    manifest that went unchecked.
  • The claude-absent skip is a skip, the claude-absent release is a hard
    failure.
    Lint must stay usable without the CLI; a release without the
    validator is not a release (brief step 8). release.sh checks for the CLI
    after the three git-only refusals but before the bump, so the common
    failure modes still report their real cause and nothing is modified.

Note for QA

  • --dry-run still commits the bump when the version actually changes.
    This follows the brief (steps 5 + 7: gates run against committed state, and
    nothing is rolled back), and the Test Writer's positive control tolerates it.
    It only fires when the requested version differs from the current one — the
    suite's positive control passes the current version, which takes the
    idempotent-resume path and commits nothing. release.sh prints a loud line
    whenever a bump commit survives a --dry-run.
  • Exercised by hand in a scratch clone (with a scratch bare repo standing in for
    the github remote), beyond what the suite covers: full non-dry-run release
    including tag --push and the RELEASES.md create-then-append + record
    commit; idempotent resume after a --dry-run bump (HEAD verified unchanged on
    the second run); all four refusals with claude off PATH; argument errors.
    All refusals exit 1. The real worktree was verified untouched afterwards
    (no tags, versions still 0.1.0/0.1.0, only the two intended files modified).
  • Lint's two defensive branches were exercised directly: a tree with no
    plugin/skills reports examined 0 files and raises a violation
    (exit 1); the real tree with claude off PATH prints the loud skip and
    still exits 0.

Hardest part: Recognising that D4's stated population and the tree's actual
contents disagreed, and that the right response was to narrow the rule to what
the decision protects rather than to weaken it into a warning or to edit
plugin/.

If I did this again: I'd run each new rule against the real tree before
running it against the seeded fixtures. Both false positives (class 2's path
prefix, D4's two legitimate pwd -P uses) were invisible in the negative
controls and obvious the moment the rule met 199 real files.

<!-- dev-journal:v1 wu=WU-50-3-4 skill=develop --> # Dev Journal — WU-50-3-4 (gate-suite Test Writer) ## Test Writer phase ```yaml type: complete variant: development timestamp: 2026-08-15T13:12 iterations: 1 ``` **Scope:** Wrote `scripts/test-plugin-gates.sh` in the isolated worktree (`/home/jochem/.claude/repos/devwork-skills-50`, branch `feature/50-ship-the-suite-as-a-claude-code-marketplace-plugin`). No production code touched — `scripts/release.sh` does not exist and `scripts/lint-conventions.sh` was read-only reference. ### Red-state table (this run, `bash scripts/test-plugin-gates.sh`, 24.5s, exit 1) | Scenario | Result now | Why | |---|---|---| | `positive_control_release_dry_run` | FAIL | `release.sh missing (scripts/release.sh does not exist yet)` — clean failure, not a crash | | `scenario_18_dirty_tree` | FAIL | same — release.sh missing | | `scenario_19_version_disagreement` | FAIL | same — release.sh missing | | `scenario_20_existing_tag` | FAIL | same — release.sh missing | | `scenario_23_validator_negative` | **PASS** | `claude plugin validate --strict` already exists and already rejects a corrupted `name` field by name, on both the plugin and marketplace manifests; pristine copies already validate clean | | `lint_nonvacuity_on_real_tree` | FAIL | current `lint-conventions.sh` prints only `lint-conventions: clean` / `N violation(s)` — no examined-file count at all, so AC-3 non-vacuity is unproven | | `scenario_24_scan_negative_controls` | FAIL | 14 of 16 sub-checks red: none of class 1 (`~/.claude/skills/`, `$HOME/.claude/skills`, both `.md` and `.sh`), class 2 (`_shared/` unprefixed, fence and prose), class 3 (`/dev:` outside the 14), class 3b (bare `/setup`), AC-4 absence (`meta-amend/` dir, `plugin/scripts/`), D4 (`realpath`), or D12 (`$CLAUDE_PLUGIN_ROOT` unbraced/escaped/unquoted) is caught — none of these rules exist yet in `lint-conventions.sh`. The two anchor-exemption sub-checks (`class1_md_anchored_exempt`, `class1_sh_anchored_exempt`) read as green today, but only because nothing is flagged at all yet — that's a coincidental green, not a verified exemption; it becomes a real assertion once the Implementer adds the class-1 rule. Post-cleanup non-vacuity also fails, for the same reason as `lint_nonvacuity_on_real_tree`. | 7 scenarios: 1 pass, 6 fail, 0 skipped. Verified the loud-skip path separately (`env -i PATH=/usr/bin:/bin bash scripts/test-plugin-gates.sh`): with `claude` off PATH, `scenario_23_validator_negative` reports `SKIP (LOUD): ... claude CLI not found on PATH` instead of a silent pass, and the summary line counts it under `skipped`, not `passed`. ### Contract ambiguities pinned down - **Refusal scenarios run without `--dry-run`.** The WU brief restricts `--dry-run` to "exercise ONLY with `--dry-run`" for the green path; it says nothing about `--dry-run` for the three refusals. I run those without it (still only ever in a scratch clone) because with `--dry-run` a refusal test can't distinguish "release.sh's refusal logic caught this" from "dry-run suppressed all side effects regardless of logic" — e.g. scenario 20's "the existing tag still points at its original commit" assertion would be true under `--dry-run` even if release.sh's tag-reuse check were missing entirely. Real invocation in a scratch clone gives a meaningful test; `--dry-run` there would not. - **Tag naming.** Confirmed via `claude plugin tag --help`: `{name}--v{version}`. `plugin/.claude-plugin/plugin.json .name` is `"dev"`, so the tag prefix is literally `dev--v` (`TAG_PREFIX` in the script). Not otherwise stated in the WU brief. - **D6 (SREQ) says release.sh "commits the bump before tagging" and delegates tag enforcement to `claude plugin tag`.** I did not model that internal mechanism in the tests — the suite treats `release.sh [--dry-run] <version>` as a black box against the five behaviors the WU brief spells out (exit codes, tag-set deltas, manifest-field deltas, message content). Whatever the Implementer's internal flow is (bump-then-tag vs. tag-only), these assertions hold either way. - **Version-disagreement message assertion is an OR, not an AND-of-strings.** "identifies the disagreement (both values or both field locations)" — I accept either (a) both version strings appear in the output, or (b) both `plugin.json` and `marketplace` (case-insensitive) appear. Whichever phrasing the Implementer's message uses, one of the two should hold naturally; I didn't want to force one specific wording. - **`lint_nonvacuity_on_real_tree` and the post-cleanup half of scenario 24 scrape the "examined N file(s)" count with a loose regex** (`extract_examined_count` in the script) rather than requiring one exact phrasing — it matches either word order (`N files examined` / `examined N files`). Whatever line the Implementer adds, it just needs a number next to the word "examined" and the word "file(s)". - **Fixture files for scenario 24 seeding:** `plugin/skills/setup/SKILL.md` (has existing shell fences, safe to append after) and `plugin/skills/_shared/procedures/bin/phase-outcome-read-latest.sh` (a real helper under a `bin/` dir, needed for the D4 canonicalization-ban check, which the WU brief scopes to `plugin/skills/**/bin/`). Both are snapshotted and restored between sub-checks so each seed is isolated. - **AC-4 absence checks assert on substring match of the *directory* path** (`plugin/skills/meta-amend` / `plugin/scripts`) rather than a `path:line` hit, since these are presence-of-a-path violations, not line-level ones — unlike every other class 1–3/D4/D12 check, which asserts an exact `path:line:` hit to avoid a false PASS from an unrelated violation elsewhere in the same file. - **`make_lint_copy()` copies only `scripts/lint-conventions.sh` + `plugin/`** (not the whole repo) into scratch — `lint-conventions.sh` computes `REPO_ROOT` from its own `BASH_SOURCE`, and only requires `<root>/plugin/skills` to exist (the `.claude/skills` root is optional, skipped if absent). This keeps each of the 16 scenario-24 lint runs at ~1.4s instead of copying `.git` history or the ~30 unrelated top-level `sdlc-*.md` docs. **Hardest part:** Picking assertion granularity for scenario 24's "flagged" checks — matching only the exact `path:lineno:` of the seeded line (not just "the file appears anywhere in the output") so the checks stay meaningful after the Implementer's rules exist, rather than trivially passing because some *other* violation in the same fixture file got reported. **If I did this again:** I'd pin the fixture files (`setup/SKILL.md`, `_shared/.../bin/phase-outcome-read-latest.sh`) down earlier — I picked them after already sketching the seed logic, which meant re-deriving expected line numbers by hand once instead of parameterizing from the start. --- ## Implementer phase ```yaml type: complete variant: development timestamp: 2026-08-15T16:05 iterations: 2 ``` **Scope:** Extended `scripts/lint-conventions.sh` with a tier-3 shipped-tree section and created `scripts/release.sh`, both in the isolated worktree (`/home/jochem/.claude/repos/devwork-skills-50`). `scripts/test-plugin-gates.sh` was read-only — no test was edited. Nothing under `plugin/`, `.claude/`, the main checkout, or `~/.claude/skills/` was touched. **Green run:** `bash scripts/test-plugin-gates.sh` — 7 passed, 0 failed, 0 skipped, exit 0, 32.6s (red baseline was 24.5s; the delta is the tier-3 scans, which scenario 24 invokes ~17 times). Two iterations: iteration 1 added the lint rules (all 15 scenario-24 sub-checks green on first run, 3/7 scenarios); iteration 2 added `release.sh` (7/7). The refactor pass that followed changed no behavior. ## [2026-08-15T15:20] DISCOVERY: `claude plugin tag --dry-run` enforces only one third of its trio ```yaml type: discovery timestamp: 2026-08-15T15:20 where: scripts/release.sh tags: [plugin-cli, release, D6, external-contract] ``` **Context:** SREQ D6 says release enforcement is "delegated to `claude plugin tag`", and the WU brief separately requires release.sh's own git-only refusals. Before writing either, I probed the CLI in a scratch clone to see how much `--dry-run` actually enforces. **Pattern:** `claude plugin tag --dry-run` enforces **tag uniqueness only**. A deliberately dirtied tree passed `--dry-run` with exit 0 and a full "would create tag" report; the same tree with an existing `dev--v0.1.0` tag exited 1 with a clear message. So the dirty-tree and version-agreement legs of the enforcement trio are live **only on the real (non-dry-run) tagging path**. **Why it matters:** this makes release.sh's three git-only refusals load-bearing rather than a convenience layer — under `--dry-run` they are the *only* thing standing between a dirty tree and a green gate report. Recorded here because D6's wording ("enforcement delegated to the CLI") would otherwise read as license to drop them. Also pinned down: `claude plugin tag` and `plugin validate` both take a **path argument** to the plugin root; with no argument `tag` looks for `<cwd>/.claude-plugin/plugin.json` and fails, so every call site passes `plugin` (or an absolute root) explicitly. ## [2026-08-15T15:35] DISCOVERY: release.sh must resolve its repo from the cwd, not from its own location ```yaml type: discovery timestamp: 2026-08-15T15:35 where: scripts/release.sh tags: [portability, worktrees, test-harness] ``` **Context:** Every helper in this repo resolves its root from `BASH_SOURCE`. Copying that idiom into release.sh would have been the natural move. **Pattern:** `REPO_ROOT="$(git rev-parse --show-toplevel)"` — the repo we were *invoked in*, not the one the script lives in. **Why it matters:** the gate suite runs `bash "$RELEASE_SH" <args>` with `cwd` set to a scratch clone while `$RELEASE_SH` points at the working-tree copy. A `BASH_SOURCE`-derived root would have made all four release scenarios operate on the **real worktree** — creating real tags and real bump commits in the repo under development, while the tests dutifully reported that the scratch clone was unchanged. Same property matters in production: a release must be cuttable from a clone or a worktree. This is the inverse of D7 (the meta lane also uses `git rev-parse --show-toplevel`, for the same class of reason: cwd resets and worktree slots break location-derived roots). ## [2026-08-15T15:45] STRUGGLE: D4's "zero violations today" was not zero ```yaml type: struggle timestamp: 2026-08-15T15:45 affected: [lint-conventions.sh, slot-retire.sh] attempts: 2 resolved: true tags: [D4, rule-scope, false-positive] ``` **Problem:** The WU brief specifies the D4 canonicalization ban as `realpath`, `readlink -f`, `cd -P`, `pwd -P` in shipped `bin/` helpers, described as a pure regression fence with "zero today". **Expected:** a literal implementation would be clean on the real tree. **Actual:** two live hits. `plugin/skills/_shared/procedures/bin/slot-retire.sh:156` (`canon_dir() { ... (cd "$1" && pwd -P); ... }`) and the slot test fixture `test/fixtures/slot/bin/teardown-marker.sh:9` (`pwd -P > .../teardown-ran-for-$name`). A literal rule would have failed `lint_nonvacuity_on_real_tree`, which requires exit 0 on the real tree — and I cannot fix it by anchoring the lines, since `plugin/` is out of bounds for this work unit. **Attempts:** 1. Implement the ban literally over `*/bin/*.sh` — two false positives. 2. Split the rule by what D4 actually protects (below). **Solution:** `realpath` and `readlink -f` stay banned outright — they have no legitimate use in a shipped helper and are genuinely zero today. `cd -P` / `pwd -P` are flagged only when the same line also references `$0` or `BASH_SOURCE`, i.e. when the script is canonicalizing **its own location**. **Key insight:** D4 is not a ban on canonicalization, it is a ban on *self-canonicalization* — "the loaded (symlink) path is canonical; helpers keep logical `BASH_SOURCE`+`dirname`". `slot-retire.sh` canonicalizes a directory the *caller* passed in, and its own 10-line comment explains why that is required for correctness (comparing a slot's recorded parent against the slots dir when one side is reached through a symlinked parent). Banning that would have been enforcing the letter against the purpose. The narrowed rule still catches the seeded regression (`p="$(realpath "$0")"`) and would catch the realistic regression (`cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd`). Known limit, documented in the script: a self-location split across two lines (`d="$(dirname "${BASH_SOURCE[0]}")"` then `root="$(cd "$d" && pwd -P)"`) escapes the line-scoped check. ## [2026-08-15T15:52] STRUGGLE: the path prefix of a grep hit is part of the haystack ```yaml type: struggle timestamp: 2026-08-15T15:52 affected: [lint-conventions.sh] attempts: 2 resolved: true tags: [class-2, grep, false-positive] ``` **Problem:** Class 2 flags `_shared/` references that lack the `${CLAUDE_PLUGIN_ROOT}/skills/` prefix. The obvious implementation — grep for `_shared/`, filter out the prefixed form — reported ~18 clean files. **Expected:** zero hits on the real tree (a survey confirmed every prose and fence reference is correctly prefixed). **Actual:** every hit was a file *living under* `plugin/skills/_shared/` — the `path:lineno:` prefix that `grep -rn` prepends contains `_shared/` itself, so any line-level filter sees the string whether or not the content has it. **Solution:** strip the `^[^:]+:[0-9]+:` prefix in `awk`, do the prefixed-form removal on the **content only**, and print the original line if `_shared/` survives. Same shape reused for class 3b's `.sh`-comment exemption. **Key insight:** it is worth writing the negative control for a scan before trusting its green — but equally worth running the new rule against the real tree *first*, because that is where this one showed up. The seeded fixture in `plugin/skills/setup/SKILL.md` would never have caught it: `setup/` has no `_shared` in its path. ### Rule-scope decisions taken beyond the brief - **D4 split into two rules** (above) — the only place I departed from the brief's literal population, and the only one that changes what is flagged. - **Class 3b scans `.sh` as well as `.md`.** The brief scopes it to "shipped `.md` text" but also says "exempt `.sh` comment lines", which only makes sense if `.sh` is in the population. Scanning both with the comment exemption is the stronger fence and is clean today: all ~40 bare `/promote`-style hits in helpers are pure comment lines naming an owning phase; non-comment hits are zero. - **Exemption anchors are per-rule, not shared.** Class 1 honours `allow: user-scope-path` (5 lines today); class 3b honours `allow: bare-skill-name` (0 today). A single blanket anchor would let a class-1 exemption silently suppress an unrelated class-3b hit. - **Marketplace validation degrades to a loud skip when the manifest is absent**, rather than a violation. `make_lint_copy()` builds a tree with `plugin/` but no root `.claude-plugin/`, so a hard requirement there would fail scenario 24's post-cleanup clean-scan assertion. The skip names the manifest that went unchecked. - **The `claude`-absent skip is a skip, the `claude`-absent *release* is a hard failure.** Lint must stay usable without the CLI; a release without the validator is not a release (brief step 8). release.sh checks for the CLI *after* the three git-only refusals but *before* the bump, so the common failure modes still report their real cause and nothing is modified. ### Note for QA - **`--dry-run` still commits the bump when the version actually changes.** This follows the brief (steps 5 + 7: gates run against committed state, and nothing is rolled back), and the Test Writer's positive control tolerates it. It only fires when the requested version differs from the current one — the suite's positive control passes the *current* version, which takes the idempotent-resume path and commits nothing. release.sh prints a loud line whenever a bump commit survives a `--dry-run`. - Exercised by hand in a scratch clone (with a scratch bare repo standing in for the `github` remote), beyond what the suite covers: full non-dry-run release including `tag --push` and the `RELEASES.md` create-then-append + record commit; idempotent resume after a `--dry-run` bump (HEAD verified unchanged on the second run); all four refusals with `claude` off `PATH`; argument errors. All refusals exit 1. The real worktree was verified untouched afterwards (no tags, versions still 0.1.0/0.1.0, only the two intended files modified). - Lint's two defensive branches were exercised directly: a tree with no `plugin/skills` reports `examined 0 files` **and** raises a violation (exit 1); the real tree with `claude` off `PATH` prints the loud skip and still exits 0. **Hardest part:** Recognising that D4's stated population and the tree's actual contents disagreed, and that the right response was to narrow the rule to what the decision protects rather than to weaken it into a warning or to edit `plugin/`. **If I did this again:** I'd run each new rule against the real tree before running it against the seeded fixtures. Both false positives (class 2's path prefix, D4's two legitimate `pwd -P` uses) were invisible in the negative controls and obvious the moment the rule met 199 real files.
Author
Owner
{
  "outcome": "complete",
  "summary": "Full 2-phase TDD: Test Writer committed scripts/test-plugin-gates.sh red (1/7 pass, failing exactly on the unimplemented surface, loud-skip path verified); Implementer added the tier-3 shipped-tree lint rules (class 1/2/3/3b scans w/ per-rule anchors, AC-4 absence, D4 self-canonicalization fence, D12 hygiene, validate --strict rule, non-vacuity count: 199 files) and scripts/release.sh (git-only refusal trio ordered before CLI steps, bump-commit-before-tag, idempotent resume, RELEASES.md record). Suite green 7/7 in 33s, lint clean, refusal exit codes re-verified by lead. Two contract discoveries: tag --dry-run enforces only tag uniqueness (release.sh refusals are load-bearing); D4 narrowed to self-canonicalization after two legitimate pwd -P uses falsified the SREQ's zero-violations claim.",
  "commit_refs": ["ee2ea9b","46c405c"],
  "journal_ref": "1021",
  "dependencies_satisfied": ["WU-50-3-1", "WU-50-3-2"],
  "dependencies_violated": []
}
<!-- work-unit-outcome:v1 id=WU-50-3-4 skill=develop --> ```json { "outcome": "complete", "summary": "Full 2-phase TDD: Test Writer committed scripts/test-plugin-gates.sh red (1/7 pass, failing exactly on the unimplemented surface, loud-skip path verified); Implementer added the tier-3 shipped-tree lint rules (class 1/2/3/3b scans w/ per-rule anchors, AC-4 absence, D4 self-canonicalization fence, D12 hygiene, validate --strict rule, non-vacuity count: 199 files) and scripts/release.sh (git-only refusal trio ordered before CLI steps, bump-commit-before-tag, idempotent resume, RELEASES.md record). Suite green 7/7 in 33s, lint clean, refusal exit codes re-verified by lead. Two contract discoveries: tag --dry-run enforces only tag uniqueness (release.sh refusals are load-bearing); D4 narrowed to self-canonicalization after two legitimate pwd -P uses falsified the SREQ's zero-violations claim.", "commit_refs": ["ee2ea9b","46c405c"], "journal_ref": "1021", "dependencies_satisfied": ["WU-50-3-1", "WU-50-3-2"], "dependencies_violated": [] } ```
Author
Owner

Dev journal — WU-50-3-5 (cutover tooling: cutover-dev.sh, cutover-abort.sh)

2026-08-15T13:40 DISCOVERY: the state machine is what makes abort possible

type: discovery
timestamp: 2026-08-15T13:40
where: scripts/cutover-dev.sh
tags: [d11, state-machine, idempotency]

Context: designing the D11 sequence as a script rather than a checklist.
Pattern: one state file (~/.claude/cutover-dev-state.json, CUTOVER_STATE_FILE-overridable)
holding, per step, {status, at} plus the facts abort needs: the farm inventory
(name, target_raw, target_abs), the runtime-config backup path, the pre-merge integration SHA,
both projects-dir slugs, and where the repo was. Steps assert preconditions, act, then record.
Two rules do the real work: (1) step 2's inventory is written once and never overwritten — it is
abort's baseline, and rewriting it after step 3 would capture a world that had already changed;
(2) the state file is bound to a (home, repo) pair and refuses to run against a different one,
so a scratch rehearsal's state can never be replayed against the real machine.
Why it matters: AC-7 judges restoration against a record. Without a written record, "restore" is
a guess about what the machine should look like.

2026-08-15T14:05 STRUGGLE: <repo>/plugin does not exist at step 3

type: struggle
timestamp: 2026-08-15T14:05
attempts: 3
resolved: true
tags: [d11, ordering, plugin-link, spec-defect]

Problem: D11 (and the WU) say step 3 creates ~/.claude/skills/dev -> <repo>/plugin, restart,
verify the plugin form loads, and only then merge.
Expected: the link resolves and the plugin loads at the STOP.
Actual: pre-merge, the integration branch has no plugin/ at all — git ls-tree main on this repo
confirms the restructured tree exists only on the feature branch. So the link is dangling at
step 3, the "verify the plugin form loads" stop verifies nothing, and the first thing that makes
<repo>/plugin exist is the very merge the verification was supposed to precede.
Attempts:

  1. Point the link at <repo>/plugin as specified — dangling; STOP is vacuous.
  2. Check the feature branch out in the main checkout first — that removes .claude/skills/ from the
    working tree, i.e. it breaks the farm at exactly the moment D11 forbids, and buys nothing.
  3. Link to the plugin root that actually resolves right then: the feature worktree's.
    Solution: step 3 resolves the plugin source (CUTOVER_PLUGIN_SOURCE, else <repo>/plugin, else the
    worktree holding the feature branch) and links to that; step 4 retargets the link to <repo>/plugin
    immediately after the fast-forward, when the two are the same content, and says so. Consequently the
    feature's own worktree is not an S-2 blocker in preflight (slot worktrees still are), and step 6
    still refuses to move a repo with any worktree registered.
    Key insight: D11's guarantee is "the plugin form is loaded and verified before the tree changes under
    the farm", not the literal path <repo>/plugin. Implementing the path breaks the guarantee.

2026-08-15T14:20 STRUGGLE: the D7 helper root has the same hole

type: struggle
timestamp: 2026-08-15T14:20
attempts: 2
resolved: true
tags: [d7, s-2, forge, helper-root]

Problem: the S-2 forge check uses the D7 pattern
B="$(git rev-parse --show-toplevel)/plugin/skills/_shared/procedures/bin".
Expected: the helper is there and the check queries the tracker.
Actual: caught by the scratch rehearsal — pre-merge the repo is on the integration branch, which
carries the pre-restructure layout, so that path does not exist and the S-2 check reported
"cannot run" at exactly the moment it matters. The plain D7 root is the post-cutover answer.
Solution: resolve_helper_bin() tries, in order: the resolved plugin source's
skills/_shared/procedures/bin, then <repo>/plugin/skills/..., then <repo>/.claude/skills/...
(the layout the running suite uses today). It prints which one it used.
Key insight: D7 is correct for the meta lane after the cutover; the cutover script runs across the
boundary and cannot assume either side.

type: discovery
timestamp: 2026-08-15T15:53
where: scripts/cutover-abort.sh
tags: [ac-7, abort, honesty, merge]

Context: the first full scratch rehearsal — forward cutover, then abort.
Pattern: abort re-created all 15 farm links byte-identically from the record, restored the runtime
config, the projects-dir slug and the repo location, removed the plugin link — and every one of the
15 links dangled.
Abort deliberately does not undo the merge (step 4), and the merge is what moved
.claude/skills/ away, so the targets are gone. "Links restored" and "the old form works again" are
different claims, and only rewinding the merge closes the gap.
Why it matters: reporting "pre-cutover state restored" there would be false. Abort now counts
dangling links after re-creating them and, when any dangle, records it as an outstanding item — naming
the pre-merge SHA and the exact reset --hard command — and exits non-zero. status gained the same
awareness: a machine in pre-cutover shape whose links dangle gets its own verdict line
("PRE-CUTOVER SHAPE, BUT n FARM LINK(S) DANGLE … neither form resolves"), because that is precisely the
half-migrated confusion R8 exists to prevent. Operator consequence: aborting after step 4 is a
two-part act
— run cutover-abort.sh, then decide about the merge.

2026-08-15T15:20 DISCOVERY: rehearsability is an environment-override discipline

type: discovery
timestamp: 2026-08-15T15:20
tags: [scenario-8, testability, rehearsal]

Context: UAT Scenario 8 requires the abort path rehearsed on a scratch copy before the real cutover.
Pattern: every real-machine path is an environment override whose default is the real machine:
CUTOVER_HOME, CUTOVER_REPO, CUTOVER_NEW_HOME, CUTOVER_STATE_FILE, CUTOVER_LOG_FILE,
CUTOVER_INTEGRATION_BRANCH, CUTOVER_FEATURE_BRANCH, CUTOVER_PLUGIN_SOURCE, CUTOVER_FEATURE_ISSUE.
The forge leg of S-2 is the one thing a scratch home cannot fake, so it has an explicit, loudly-announced
CUTOVER_SKIP_FORGE_CHECK=1. claude plugin list is skipped (with the reason printed) whenever
CUTOVER_HOME != $HOME, because it reads the real $HOME and its answer would not describe the scratch.
Why it matters: the full sequence — including the destructive steps — was exercised end to end
against a scratch replica while the real ~/.claude.json, ~/.claude/skills/, ~/.claude/projects/
and both repos stayed byte-identical (verified by checksum + listing diff before/after).

2026-08-15T16:00 DISCOVERY: rehearsal results

type: discovery
timestamp: 2026-08-15T16:00
tags: [rehearsal, s-2, ac-6, ac-7, evidence]

Context: scratch replica of the dev machine's pre-cutover state — a clone at the old path with a
feature worktree, 15 relative farm links, a ~/.claude.json matching the recorded sample shape (this
repo's entry plus an unrelated project, to prove exactly one key moves), and a projects dir at the
old slug containing a canary memory file.

Forward (19 assertions, all pass). preflight clean → steps 2–3 → STOP → steps 4–5 → step 6 refused
while the feature worktree was still registered (named it) → worktree retired → step 6 → STOP → step 7
from the new location. End state: 0 of 15 farm links present; plugin link → <new home>/plugin; repo
moved; runtime config has the new key, lost the old one, kept the unrelated project (exactly 2 keys) and
preserved hasTrustDialogAccepted; projects dir renamed with the canary intact; integration branch at
the feature tip; state marked complete. The status verdict read CUTOVER COMPLETE.

Abort (7 assertions, all pass). All 15 links re-created with byte-identical raw targets; repo moved
back; ~/.claude.json byte-identical to the backup (the pre-abort file preserved alongside, since a
whole-file restore also rolls back anything else that changed in it); projects dir back at the old slug;
plugin link removed; merge untouched, with its pre-merge SHA printed. Exit 1 with the dangling-links
item outstanding — correct, and re-running abort was idempotent (0 re-created, 15 already in place).

S-2 negative. With a slot worktree added and a dirty tree, preflight reported both blockers
(it accumulates rather than failing fast) and exited 1 with no step marked done and nothing changed.
The forge leg was exercised against the real tracker read-only in both directions: with
CUTOVER_FEATURE_ISSUE=50 it passed (#50 is the cutover's own issue), and with CUTOVER_FEATURE_ISSUE=999
it correctly blocked naming #50 … phase/developing.

Edge cases. status works with no state file; a refused command creates no state file; run --step 5
out of order refuses naming the first pending step; a virgin home reports NO CUTOVER STATE rather than
the alarming HALF-MIGRATED.

Not covered by the rehearsal (remains UAT): that the plugin form actually loads at the STOP
(Scenario 9's interactive /dev: checks), and claude plugin list on the real machine.

2026-08-15T16:05 COMPLETE

type: complete
variant: development
timestamp: 2026-08-15T16:05
iterations: 4

Hardest part: discovering that the specified step-3 target cannot exist yet — the same pre-merge
layout gap then turned up a second time in the D7 forge-helper root, and both had to be resolved
without weakening D11's ordering guarantee.
If I did this again: I would rehearse the abort before polishing the forward path. The
dangling-links finding — that abort alone does not restore a working farm once the merge has run — is
the single most consequential thing in this work unit, and it only appears after a full round trip.

<!-- dev-journal:v1 wu=WU-50-3-5 skill=develop --> # Dev journal — WU-50-3-5 (cutover tooling: cutover-dev.sh, cutover-abort.sh) ## 2026-08-15T13:40 DISCOVERY: the state machine is what makes abort possible ```yaml type: discovery timestamp: 2026-08-15T13:40 where: scripts/cutover-dev.sh tags: [d11, state-machine, idempotency] ``` **Context:** designing the D11 sequence as a script rather than a checklist. **Pattern:** one state file (`~/.claude/cutover-dev-state.json`, `CUTOVER_STATE_FILE`-overridable) holding, per step, `{status, at}` plus the facts abort needs: the farm inventory (`name`, `target_raw`, `target_abs`), the runtime-config backup path, the pre-merge integration SHA, both projects-dir slugs, and where the repo was. Steps assert preconditions, act, then record. Two rules do the real work: (1) step 2's inventory is written **once** and never overwritten — it is abort's baseline, and rewriting it after step 3 would capture a world that had already changed; (2) the state file is bound to a `(home, repo)` pair and refuses to run against a different one, so a scratch rehearsal's state can never be replayed against the real machine. **Why it matters:** AC-7 judges restoration against a record. Without a written record, "restore" is a guess about what the machine should look like. ## 2026-08-15T14:05 STRUGGLE: `<repo>/plugin` does not exist at step 3 ```yaml type: struggle timestamp: 2026-08-15T14:05 attempts: 3 resolved: true tags: [d11, ordering, plugin-link, spec-defect] ``` **Problem:** D11 (and the WU) say step 3 creates `~/.claude/skills/dev -> <repo>/plugin`, restart, verify the plugin form loads, and only then merge. **Expected:** the link resolves and the plugin loads at the STOP. **Actual:** pre-merge, the integration branch has no `plugin/` at all — `git ls-tree main` on this repo confirms the restructured tree exists only on the feature branch. So the link is **dangling** at step 3, the "verify the plugin form loads" stop verifies nothing, and the first thing that makes `<repo>/plugin` exist is the very merge the verification was supposed to precede. **Attempts:** 1. Point the link at `<repo>/plugin` as specified — dangling; STOP is vacuous. 2. Check the feature branch out in the main checkout first — that removes `.claude/skills/` from the working tree, i.e. it breaks the farm at exactly the moment D11 forbids, and buys nothing. 3. Link to the plugin root that actually resolves right then: the feature worktree's. **Solution:** step 3 resolves the plugin source (`CUTOVER_PLUGIN_SOURCE`, else `<repo>/plugin`, else the worktree holding the feature branch) and links to that; step 4 retargets the link to `<repo>/plugin` immediately after the fast-forward, when the two are the same content, and says so. Consequently the feature's **own** worktree is not an S-2 blocker in preflight (slot worktrees still are), and step 6 still refuses to move a repo with any worktree registered. **Key insight:** D11's guarantee is "the plugin form is loaded and verified before the tree changes under the farm", not the literal path `<repo>/plugin`. Implementing the path breaks the guarantee. ## 2026-08-15T14:20 STRUGGLE: the D7 helper root has the same hole ```yaml type: struggle timestamp: 2026-08-15T14:20 attempts: 2 resolved: true tags: [d7, s-2, forge, helper-root] ``` **Problem:** the S-2 forge check uses the D7 pattern `B="$(git rev-parse --show-toplevel)/plugin/skills/_shared/procedures/bin"`. **Expected:** the helper is there and the check queries the tracker. **Actual:** caught by the scratch rehearsal — pre-merge the repo is on the integration branch, which carries the **pre-restructure** layout, so that path does not exist and the S-2 check reported "cannot run" at exactly the moment it matters. The plain D7 root is the post-cutover answer. **Solution:** `resolve_helper_bin()` tries, in order: the resolved plugin source's `skills/_shared/procedures/bin`, then `<repo>/plugin/skills/...`, then `<repo>/.claude/skills/...` (the layout the running suite uses today). It prints which one it used. **Key insight:** D7 is correct for the meta lane after the cutover; the cutover script runs across the boundary and cannot assume either side. ## 2026-08-15T15:53 DISCOVERY: abort restores the links, not the farm ```yaml type: discovery timestamp: 2026-08-15T15:53 where: scripts/cutover-abort.sh tags: [ac-7, abort, honesty, merge] ``` **Context:** the first full scratch rehearsal — forward cutover, then abort. **Pattern:** abort re-created all 15 farm links byte-identically from the record, restored the runtime config, the projects-dir slug and the repo location, removed the plugin link — and **every one of the 15 links dangled.** Abort deliberately does not undo the merge (step 4), and the merge is what moved `.claude/skills/` away, so the targets are gone. "Links restored" and "the old form works again" are different claims, and only rewinding the merge closes the gap. **Why it matters:** reporting "pre-cutover state restored" there would be false. Abort now counts dangling links after re-creating them and, when any dangle, records it as an outstanding item — naming the pre-merge SHA and the exact `reset --hard` command — and exits non-zero. `status` gained the same awareness: a machine in pre-cutover *shape* whose links dangle gets its own verdict line ("PRE-CUTOVER SHAPE, BUT n FARM LINK(S) DANGLE … neither form resolves"), because that is precisely the half-migrated confusion R8 exists to prevent. Operator consequence: **aborting after step 4 is a two-part act** — run `cutover-abort.sh`, then decide about the merge. ## 2026-08-15T15:20 DISCOVERY: rehearsability is an environment-override discipline ```yaml type: discovery timestamp: 2026-08-15T15:20 tags: [scenario-8, testability, rehearsal] ``` **Context:** UAT Scenario 8 requires the abort path rehearsed on a scratch copy *before* the real cutover. **Pattern:** every real-machine path is an environment override whose **default is the real machine**: `CUTOVER_HOME`, `CUTOVER_REPO`, `CUTOVER_NEW_HOME`, `CUTOVER_STATE_FILE`, `CUTOVER_LOG_FILE`, `CUTOVER_INTEGRATION_BRANCH`, `CUTOVER_FEATURE_BRANCH`, `CUTOVER_PLUGIN_SOURCE`, `CUTOVER_FEATURE_ISSUE`. The forge leg of S-2 is the one thing a scratch home cannot fake, so it has an explicit, loudly-announced `CUTOVER_SKIP_FORGE_CHECK=1`. `claude plugin list` is skipped (with the reason printed) whenever `CUTOVER_HOME != $HOME`, because it reads the real `$HOME` and its answer would not describe the scratch. **Why it matters:** the full sequence — including the destructive steps — was exercised end to end against a scratch replica while the real `~/.claude.json`, `~/.claude/skills/`, `~/.claude/projects/` and both repos stayed byte-identical (verified by checksum + listing diff before/after). ## 2026-08-15T16:00 DISCOVERY: rehearsal results ```yaml type: discovery timestamp: 2026-08-15T16:00 tags: [rehearsal, s-2, ac-6, ac-7, evidence] ``` **Context:** scratch replica of the dev machine's pre-cutover state — a clone at the old path with a feature worktree, 15 relative farm links, a `~/.claude.json` matching the recorded sample shape (this repo's entry **plus an unrelated project**, to prove exactly one key moves), and a projects dir at the old slug containing a canary memory file. **Forward (19 assertions, all pass).** preflight clean → steps 2–3 → STOP → steps 4–5 → step 6 refused while the feature worktree was still registered (named it) → worktree retired → step 6 → STOP → step 7 from the new location. End state: 0 of 15 farm links present; plugin link → `<new home>/plugin`; repo moved; runtime config has the new key, lost the old one, kept the unrelated project (exactly 2 keys) and preserved `hasTrustDialogAccepted`; projects dir renamed with the canary intact; integration branch at the feature tip; state marked complete. The `status` verdict read CUTOVER COMPLETE. **Abort (7 assertions, all pass).** All 15 links re-created with byte-identical raw targets; repo moved back; `~/.claude.json` byte-identical to the backup (the pre-abort file preserved alongside, since a whole-file restore also rolls back anything else that changed in it); projects dir back at the old slug; plugin link removed; merge untouched, with its pre-merge SHA printed. Exit 1 with the dangling-links item outstanding — correct, and re-running abort was idempotent (0 re-created, 15 already in place). **S-2 negative.** With a slot worktree added *and* a dirty tree, preflight reported **both** blockers (it accumulates rather than failing fast) and exited 1 with no step marked done and nothing changed. The forge leg was exercised against the real tracker read-only in both directions: with `CUTOVER_FEATURE_ISSUE=50` it passed (#50 is the cutover's own issue), and with `CUTOVER_FEATURE_ISSUE=999` it correctly blocked naming `#50 … phase/developing`. **Edge cases.** `status` works with no state file; a refused command creates no state file; `run --step 5` out of order refuses naming the first pending step; a virgin home reports NO CUTOVER STATE rather than the alarming HALF-MIGRATED. **Not covered by the rehearsal (remains UAT):** that the plugin form actually *loads* at the STOP (Scenario 9's interactive `/dev:` checks), and `claude plugin list` on the real machine. ## 2026-08-15T16:05 COMPLETE ```yaml type: complete variant: development timestamp: 2026-08-15T16:05 iterations: 4 ``` **Hardest part:** discovering that the specified step-3 target cannot exist yet — the same pre-merge layout gap then turned up a second time in the D7 forge-helper root, and both had to be resolved without weakening D11's ordering guarantee. **If I did this again:** I would rehearse the *abort* before polishing the forward path. The dangling-links finding — that abort alone does not restore a working farm once the merge has run — is the single most consequential thing in this work unit, and it only appears after a full round trip.
Author
Owner
{
  "outcome": "complete",
  "summary": "cutover-dev.sh (stepwise D11 state machine: S-2 hard block incl. read-only forge leg, timestamped runtime-config backup + once-only farm inventory, plugin-form link, ff-merge under plugin form, verified farm teardown, repo move w/ single-entry ~/.claude.json migration + projects-slug rename, R8 status probe) + cutover-abort.sh (reverse-order restore from the recorded state; honestly reports that post-merge abort leaves the 15 restored links dangling and exits non-zero with the pre-merge SHA — abort after step 4 is a two-part act). Fully rehearsed on a scratch replica: forward 19/19 assertions, abort 7/7 + idempotent re-run, S-2 negative blocks accumulating both blockers; real machine verified byte-identical. Two spec defects found and resolved without weakening D11: step-3 target <repo>/plugin cannot exist pre-merge (plugin-source resolution + post-ff retarget); D7 helper root absent pre-merge (layout-aware fallback). Artifact-shape WU: Test Writer skipped; rehearsal is the verification.",
  "commit_refs": ["5c6681c"],
  "journal_ref": "1023",
  "dependencies_satisfied": ["WU-50-3-1", "WU-50-3-4"],
  "dependencies_violated": []
}
<!-- work-unit-outcome:v1 id=WU-50-3-5 skill=develop --> ```json { "outcome": "complete", "summary": "cutover-dev.sh (stepwise D11 state machine: S-2 hard block incl. read-only forge leg, timestamped runtime-config backup + once-only farm inventory, plugin-form link, ff-merge under plugin form, verified farm teardown, repo move w/ single-entry ~/.claude.json migration + projects-slug rename, R8 status probe) + cutover-abort.sh (reverse-order restore from the recorded state; honestly reports that post-merge abort leaves the 15 restored links dangling and exits non-zero with the pre-merge SHA — abort after step 4 is a two-part act). Fully rehearsed on a scratch replica: forward 19/19 assertions, abort 7/7 + idempotent re-run, S-2 negative blocks accumulating both blockers; real machine verified byte-identical. Two spec defects found and resolved without weakening D11: step-3 target <repo>/plugin cannot exist pre-merge (plugin-source resolution + post-ff retarget); D7 helper root absent pre-merge (layout-aware fallback). Artifact-shape WU: Test Writer skipped; rehearsal is the verification.", "commit_refs": ["5c6681c"], "journal_ref": "1023", "dependencies_satisfied": ["WU-50-3-1", "WU-50-3-4"], "dependencies_violated": [] } ```
Author
Owner

Dev journal — WU-50-3-6 (README rewrite + P0.8 history scan)

Worktree: /home/jochem/.claude/repos/devwork-skills-50 (branch feature/50-ship-the-suite-as-a-claude-code-marketplace-plugin)
Files owned: README.md (rewrite), scripts/history-scan.sh (new), the P0.8 report in the feature folder (not committed).

2026-08-15T14:05 DISCOVERY: every plugin-CLI command in the README was verified against a real install, not against the help text alone

type: discovery
timestamp: 2026-08-15T14:05
where: README.md
tags: [plugin-cli, verification, ac-11]

Context: AC-11 makes the README the desktop cutover's only instruction set, so a command that
merely looks right is a defect. Two verification routes were used: claude plugin <sub> --help on
2.1.229 for flags, and a throwaway CLAUDE_CONFIG_DIR for behaviour — a scratch config dir where
marketplaces can be added and plugins installed without touching the operator's real
~/.claude/plugins.

Pattern: what was checked, and how:

Claim in the README How it was verified
plugin marketplace add <owner/repo> source form marketplace add --help (--scope, --sparse only) + the binary's own error string Invalid GitHub repository format: … Expected format: owner/repo; a file:// URL is rejected with Try: owner/repo, https://…, or ./path
marketplace name devwork-skills, plugin id dev@devwork-skills marketplace add <this worktree> + plugin install dev@devwork-skills in a scratch config: ✔ Successfully installed plugin: dev@devwork-skills (scope: user)
plugin list --json reports version 0.1.0 same scratch install: {"id":"dev@devwork-skills","version":"0.1.0","enabled":true}
plugin details shows the 14 skills Skills (14) develop, gh-cli, git-worktrees, glab-cli, integrate, local-fs, promote, requirements, resolve, setup, simplify-report, tea-cli, technical-plan, worktree-cleanup
plugin list --json --available has a different shape returns {installed: [...], available: [...]}, not a bare array — the README's verify commands use the bare list --json
a GitHub-sourced marketplace is a git clone marketplace add anthropics/claude-plugins-official in a scratch config → .git present, origin = the HTTPS URL, rev-list --count HEAD = 1, is-shallow-repository = true, 0 tags. Hence the README's fetch --unshallow --tags before any tag checkout
HTTPS vs SSH clone the CLI prints SSH not configured, cloning via HTTPS: https://github.com/…
release refusal messages quoted verbatim from scripts/release.sh (prefix release: refusing to release <v>:)
cutover STOP/abort/status strings quoted verbatim from scripts/cutover-dev.sh (stop_message, do_status verdicts) and scripts/cutover-abort.sh (COULD NOT RESTORE … DANGLE, the pre-merge SHA lines)
uat.mode enum in the consumer template plugin/skills/_shared/procedures/uat-surface.mdpreview-deploy | staging | local (an earlier draft had invented production)

Why it matters: three of these contradicted the obvious reading, and each would have cost the
desktop operator a failed step mid-cutover.

2026-08-15T14:10 DISCOVERY: install does not update, update goes downwards, and the @version suffix does not pin

type: discovery
timestamp: 2026-08-15T14:10
where: README.md
tags: [plugin-cli, versioning, ac-9, ac-7]

Context: AC-9 (the update ritual) and AC-7 (recover the previous release) both need a version to
be selectable. claude plugin install --help shows no version flag, so the question was how a
consumer lands on a specific release at all.

Pattern: built a scratch marketplace repo with two tagged versions (0.1.0, 0.2.0) and drove it in
an isolated CLAUDE_CONFIG_DIR:

  • plugin install <p>@<m>@0.1.0 succeeds and installs 0.2.0. The @version suffix parses (it is
    stripped, and playwright@claude-plugins-official@1.0.0 resolves the same as without it) but does
    not select a version. Do not trust it; verify with plugin list --json.
  • plugin install against an already-installed plugin is a no-op: Plugin "…" is already installed (scope: user). An update ritual built on install would silently do nothing.
  • plugin update <p>@<m> moves the install to whatever the marketplace source currently carries, in
    either direction: with the source rolled back to 0.1.0 it printed ✔ Plugin "scratchdev" updated from 0.2.0 to 0.1.0 for scope user. Restart to apply changes.

Why it matters: the version a consumer runs is a property of the marketplace snapshot, not of
the install command. That is what makes the recovery path "point the snapshot at the old tag, then
update" rather than "install the old version", and it is why the README's update ritual is two
commands, not one.

Not verified from here: the recovery sequence end-to-end against a private, GitHub-sourced
marketplace. Its two premises are verified separately (the snapshot is a git clone; update follows
the source downwards); the composition is a UAT item, and the README says so and names the fallback
(re-cut the previous content as a new release).

2026-08-15T14:17 STRUGGLE: the generic secret rule blocked on documentation the first time it ran

type: struggle
timestamp: 2026-08-15T14:17
affected: [scripts/history-scan.sh]
attempts: 2
resolved: true
tags: [p08, false-positive]

Problem: the first full-history run reported P0.8: BLOCK (15 hits).
Expected: a clean history — this repo has never carried a credential.
Actual: every hit was the same line of QA example code in _shared/domains/api.md:
token = response.json()["token"].

Attempts:

  1. Exclude placeholder-looking values (<your-token>, changeme, ${VAR}). That is the natural
    phrasing and it is the wrong shape: response.json is not a placeholder, so it still blocked.
  2. Invert the test — decide positively whether the value looks like credential material.

Solution: real_value() now requires the assigned value to survive a series of "this is not a
secret" shapes (a function call, a path, an ALL_CAPS env-var name, a dotted identifier, an
all-lowercase word run, a known placeholder word) and then look random: at least 16 characters
with both digits and letters, or 32+ opaque characters.

Key insight: an exclusion list answers "is this a known placeholder?", which is unbounded. The
question a scanner can actually answer is "does this look like credential material?" — and phrasing
it that way keeps a real credential in a documented example blocking, which the exclusion-list
version would eventually have let through.

2026-08-15T14:25 DISCOVERY: the scan's own non-vacuity control

type: discovery
timestamp: 2026-08-15T14:25
where: scripts/history-scan.sh
tags: [p08, non-vacuity]

Context: a clean scan of a clean history is indistinguishable from a scan that cannot find
anything — the same failure mode AC-3's negative controls exist for.

Pattern: built a scratch repo, planted one instance of each of the nine Class A shapes in a
commit, then deleted them all in a following commit so HEAD is clean and only history is not. The
scan reported all nine from history, with values redacted ([REDACTED 34 chars]), and exited 1.
Benign neighbours planted alongside (token: <your-token-here>, password: changeme,
TOKEN = os.environ["TOKEN"], a long absolute path) did not fire.

Two other guards fell out of the same reasoning and are in the script: the run dies if the awk in
use does not support ERE intervals (mawk 1.3.3 would silently reduce every token rule to a rule that
matches nothing), and it dies if the population comes out empty rather than reporting PASS over
zero blobs.

Why it matters: the P0.8 PASS below is evidence, not silence.

2026-08-15T14:35 DISCOVERY: P0.8 result on this repo's full history

type: discovery
timestamp: 2026-08-15T14:35
where: .devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/p08-history-scan-report.md
tags: [p08, d5, verdict]

Context: the gate on the first push to github.com/jbr870/devwork-skills (D5).

Pattern: run at the branch tip ca517f0 — 239 commits, 9 refs, 1191 blobs reachable, 1191
scanned (nothing skipped as binary or oversized; the object store holds no unreachable blobs).
Class A: 0 hits across all nine patterns — P0.8: PASS.

Class B (advisory, acceptable while the repo is private): internal-domain 80 hits / 75 blobs, of
which git.wihslon.com is 76 / 73; email-address 215 / 100 (mostly git@git.wihslon.com in the old
README's clone command and test@example.com in QA domain examples); operator-home-path 23 / 12;
tailscale 7 / 4; desktop-hostname 6 / 6; ssh-remote-url 5 / 5; private-ip 0.

Why it matters: the push is unblocked, and the visibility decision now has its inventory. Every
Class B pattern would be published verbatim if the repo were ever made public — the private-forge
hostname most of all, since it appears in run reports, the devcontainer files and .env.example.

2026-08-15T14:40 COMPLETE

type: complete
variant: development
timestamp: 2026-08-15T14:40
iterations: 2

Commits: d0b7e79 (scripts: history-scan.sh), ca517f0 (docs: README rewrite). Gates after both:
bash -n clean, scripts/lint-conventions.sh clean (199 files examined), scripts/test-plugin-gates.sh
7/7 PASS. S-5 holds — the two commits touch scripts/history-scan.sh and README.md and nothing else.

Hardest part: deciding how to document the version-selection path honestly once the probes showed
the @version suffix is accepted but ignored — the README had to be specific enough to execute and
explicit about which step has not been run end-to-end.

If I did this again: I would probe the CLI's install/update semantics in a scratch
CLAUDE_CONFIG_DIR before drafting the install section rather than after, since three sections had
to be rewritten around what the probes returned.

<!-- dev-journal:v1 wu=WU-50-3-6 skill=develop --> # Dev journal — WU-50-3-6 (README rewrite + P0.8 history scan) Worktree: `/home/jochem/.claude/repos/devwork-skills-50` (branch `feature/50-ship-the-suite-as-a-claude-code-marketplace-plugin`) Files owned: `README.md` (rewrite), `scripts/history-scan.sh` (new), the P0.8 report in the feature folder (not committed). ## 2026-08-15T14:05 DISCOVERY: every plugin-CLI command in the README was verified against a real install, not against the help text alone ```yaml type: discovery timestamp: 2026-08-15T14:05 where: README.md tags: [plugin-cli, verification, ac-11] ``` **Context:** AC-11 makes the README the desktop cutover's only instruction set, so a command that merely looks right is a defect. Two verification routes were used: `claude plugin <sub> --help` on 2.1.229 for flags, and a **throwaway `CLAUDE_CONFIG_DIR`** for behaviour — a scratch config dir where marketplaces can be added and plugins installed without touching the operator's real `~/.claude/plugins`. **Pattern:** what was checked, and how: | Claim in the README | How it was verified | | --- | --- | | `plugin marketplace add <owner/repo>` source form | `marketplace add --help` (`--scope`, `--sparse` only) + the binary's own error string `Invalid GitHub repository format: … Expected format: owner/repo`; a `file://` URL is rejected with `Try: owner/repo, https://…, or ./path` | | marketplace name `devwork-skills`, plugin id `dev@devwork-skills` | `marketplace add <this worktree>` + `plugin install dev@devwork-skills` in a scratch config: `✔ Successfully installed plugin: dev@devwork-skills (scope: user)` | | `plugin list --json` reports version `0.1.0` | same scratch install: `{"id":"dev@devwork-skills","version":"0.1.0","enabled":true}` | | `plugin details` shows the 14 skills | `Skills (14) develop, gh-cli, git-worktrees, glab-cli, integrate, local-fs, promote, requirements, resolve, setup, simplify-report, tea-cli, technical-plan, worktree-cleanup` | | `plugin list --json --available` has a different shape | returns `{installed: [...], available: [...]}`, not a bare array — the README's verify commands use the bare `list --json` | | a GitHub-sourced marketplace is a git clone | `marketplace add anthropics/claude-plugins-official` in a scratch config → `.git` present, `origin` = the HTTPS URL, `rev-list --count HEAD` = 1, `is-shallow-repository` = true, 0 tags. Hence the README's `fetch --unshallow --tags` before any tag checkout | | HTTPS vs SSH clone | the CLI prints `SSH not configured, cloning via HTTPS: https://github.com/…` | | release refusal messages | quoted verbatim from `scripts/release.sh` (prefix `release: refusing to release <v>:`) | | cutover STOP/abort/status strings | quoted verbatim from `scripts/cutover-dev.sh` (`stop_message`, `do_status` verdicts) and `scripts/cutover-abort.sh` (`COULD NOT RESTORE … DANGLE`, the pre-merge SHA lines) | | `uat.mode` enum in the consumer template | `plugin/skills/_shared/procedures/uat-surface.md` — `preview-deploy \| staging \| local` (an earlier draft had invented `production`) | **Why it matters:** three of these contradicted the obvious reading, and each would have cost the desktop operator a failed step mid-cutover. ## 2026-08-15T14:10 DISCOVERY: install does not update, update goes downwards, and the `@version` suffix does not pin ```yaml type: discovery timestamp: 2026-08-15T14:10 where: README.md tags: [plugin-cli, versioning, ac-9, ac-7] ``` **Context:** AC-9 (the update ritual) and AC-7 (recover the previous release) both need a version to be selectable. `claude plugin install --help` shows **no version flag**, so the question was how a consumer lands on a specific release at all. **Pattern:** built a scratch marketplace repo with two tagged versions (0.1.0, 0.2.0) and drove it in an isolated `CLAUDE_CONFIG_DIR`: - `plugin install <p>@<m>@0.1.0` **succeeds and installs 0.2.0.** The `@version` suffix parses (it is stripped, and `playwright@claude-plugins-official@1.0.0` resolves the same as without it) but does not select a version. **Do not trust it; verify with `plugin list --json`.** - `plugin install` against an already-installed plugin is a **no-op**: `Plugin "…" is already installed (scope: user)`. An update ritual built on `install` would silently do nothing. - `plugin update <p>@<m>` moves the install to whatever the marketplace source currently carries, in **either direction**: with the source rolled back to 0.1.0 it printed `✔ Plugin "scratchdev" updated from 0.2.0 to 0.1.0 for scope user. Restart to apply changes.` **Why it matters:** the version a consumer runs is a property of the **marketplace snapshot**, not of the install command. That is what makes the recovery path "point the snapshot at the old tag, then `update`" rather than "install the old version", and it is why the README's update ritual is two commands, not one. **Not verified from here:** the recovery sequence end-to-end against a *private, GitHub-sourced* marketplace. Its two premises are verified separately (the snapshot is a git clone; `update` follows the source downwards); the composition is a UAT item, and the README says so and names the fallback (re-cut the previous content as a new release). ## 2026-08-15T14:17 STRUGGLE: the generic secret rule blocked on documentation the first time it ran ```yaml type: struggle timestamp: 2026-08-15T14:17 affected: [scripts/history-scan.sh] attempts: 2 resolved: true tags: [p08, false-positive] ``` **Problem:** the first full-history run reported `P0.8: BLOCK (15 hits)`. **Expected:** a clean history — this repo has never carried a credential. **Actual:** every hit was the same line of QA example code in `_shared/domains/api.md`: `token = response.json()["token"]`. **Attempts:** 1. Exclude placeholder-looking values (`<your-token>`, `changeme`, `${VAR}`). That is the natural phrasing and it is the wrong shape: `response.json` is not a placeholder, so it still blocked. 2. Invert the test — decide **positively** whether the value looks like credential material. **Solution:** `real_value()` now requires the assigned value to survive a series of "this is not a secret" shapes (a function call, a path, an `ALL_CAPS` env-var name, a dotted identifier, an all-lowercase word run, a known placeholder word) **and** then look random: at least 16 characters with both digits and letters, or 32+ opaque characters. **Key insight:** an exclusion list answers "is this a known placeholder?", which is unbounded. The question a scanner can actually answer is "does this look like credential material?" — and phrasing it that way keeps a *real* credential in a documented example blocking, which the exclusion-list version would eventually have let through. ## 2026-08-15T14:25 DISCOVERY: the scan's own non-vacuity control ```yaml type: discovery timestamp: 2026-08-15T14:25 where: scripts/history-scan.sh tags: [p08, non-vacuity] ``` **Context:** a clean scan of a clean history is indistinguishable from a scan that cannot find anything — the same failure mode AC-3's negative controls exist for. **Pattern:** built a scratch repo, planted one instance of each of the nine Class A shapes in a commit, then **deleted them all in a following commit** so HEAD is clean and only history is not. The scan reported all nine from history, with values redacted (`[REDACTED 34 chars]`), and exited 1. Benign neighbours planted alongside (`token: <your-token-here>`, `password: changeme`, `TOKEN = os.environ["TOKEN"]`, a long absolute path) did not fire. Two other guards fell out of the same reasoning and are in the script: the run **dies** if the awk in use does not support ERE intervals (mawk 1.3.3 would silently reduce every token rule to a rule that matches nothing), and it **dies** if the population comes out empty rather than reporting PASS over zero blobs. **Why it matters:** the P0.8 PASS below is evidence, not silence. ## 2026-08-15T14:35 DISCOVERY: P0.8 result on this repo's full history ```yaml type: discovery timestamp: 2026-08-15T14:35 where: .devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/p08-history-scan-report.md tags: [p08, d5, verdict] ``` **Context:** the gate on the first push to `github.com/jbr870/devwork-skills` (D5). **Pattern:** run at the branch tip `ca517f0` — 239 commits, 9 refs, 1191 blobs reachable, 1191 scanned (nothing skipped as binary or oversized; the object store holds no unreachable blobs). **Class A: 0 hits across all nine patterns — `P0.8: PASS`.** Class B (advisory, acceptable while the repo is private): `internal-domain` 80 hits / 75 blobs, of which `git.wihslon.com` is 76 / 73; `email-address` 215 / 100 (mostly `git@git.wihslon.com` in the old README's clone command and `test@example.com` in QA domain examples); `operator-home-path` 23 / 12; `tailscale` 7 / 4; `desktop-hostname` 6 / 6; `ssh-remote-url` 5 / 5; `private-ip` 0. **Why it matters:** the push is unblocked, and the visibility decision now has its inventory. Every Class B pattern would be published verbatim if the repo were ever made public — the private-forge hostname most of all, since it appears in run reports, the devcontainer files and `.env.example`. ## 2026-08-15T14:40 COMPLETE ```yaml type: complete variant: development timestamp: 2026-08-15T14:40 iterations: 2 ``` Commits: `d0b7e79` (scripts: history-scan.sh), `ca517f0` (docs: README rewrite). Gates after both: `bash -n` clean, `scripts/lint-conventions.sh` clean (199 files examined), `scripts/test-plugin-gates.sh` 7/7 PASS. S-5 holds — the two commits touch `scripts/history-scan.sh` and `README.md` and nothing else. **Hardest part:** deciding how to document the version-selection path honestly once the probes showed the `@version` suffix is accepted but ignored — the README had to be specific enough to execute and explicit about which step has not been run end-to-end. **If I did this again:** I would probe the CLI's install/update semantics in a scratch `CLAUDE_CONFIG_DIR` *before* drafting the install section rather than after, since three sections had to be rewritten around what the probes returned.
Author
Owner
{
  "outcome": "complete",
  "summary": "README rewritten for the dev plugin (587 lines: prereqs w/ verbatim checks, consumer install from the private GitHub source w/ credential path, update ritual, previous-release recovery, failure modes quoting real refusal strings, 14-row old->new name table, desktop + dev-machine cutover runbooks incl. the two-part-abort caveat, release flow, consumer guidance-block template) — every CLI command verified against claude 2.1.229 via a scratch CLAUDE_CONFIG_DIR, surfacing that install@version does NOT pin (snapshot+update is the real ritual) and install-on-installed is a no-op. scripts/history-scan.sh added (full-history P0.8 gate w/ positive credential-shape matching, redaction, its own non-vacuity control incl. awk-interval and empty-population guards) and RUN: P0.8 PASS over 239 commits / 1191 blobs, zero push-blocking hits; Class B advisory inventory recorded for the visibility decision. Lint + gate suite green; S-5 holds. Artifact-shape WU: Test Writer skipped.",
  "commit_refs": ["ca517f0","d0b7e79"],
  "journal_ref": "1025",
  "dependencies_satisfied": ["WU-50-3-1", "WU-50-3-4", "WU-50-3-5"],
  "dependencies_violated": []
}
<!-- work-unit-outcome:v1 id=WU-50-3-6 skill=develop --> ```json { "outcome": "complete", "summary": "README rewritten for the dev plugin (587 lines: prereqs w/ verbatim checks, consumer install from the private GitHub source w/ credential path, update ritual, previous-release recovery, failure modes quoting real refusal strings, 14-row old->new name table, desktop + dev-machine cutover runbooks incl. the two-part-abort caveat, release flow, consumer guidance-block template) — every CLI command verified against claude 2.1.229 via a scratch CLAUDE_CONFIG_DIR, surfacing that install@version does NOT pin (snapshot+update is the real ritual) and install-on-installed is a no-op. scripts/history-scan.sh added (full-history P0.8 gate w/ positive credential-shape matching, redaction, its own non-vacuity control incl. awk-interval and empty-population guards) and RUN: P0.8 PASS over 239 commits / 1191 blobs, zero push-blocking hits; Class B advisory inventory recorded for the visibility decision. Lint + gate suite green; S-5 holds. Artifact-shape WU: Test Writer skipped.", "commit_refs": ["ca517f0","d0b7e79"], "journal_ref": "1025", "dependencies_satisfied": ["WU-50-3-1", "WU-50-3-4", "WU-50-3-5"], "dependencies_violated": [] } ```
Author
Owner

P0.8 — full-history sensitive-data scan

Produced by scripts/history-scan.sh on 2026-08-15T14:28:30Z.

This gate exists because pushing a repo publishes its history, not its tip
(SREQ D5). It runs before the first push to the GitHub remote.

Scope

Repository /home/jochem/.claude/repos/devwork-skills-50
HEAD ca517f0b49b4cd604340d03b434cdd6a58939a6d (feature/50-ship-the-suite-as-a-claude-code-marketplace-plugin)
Refs 9
Commits (all refs) 239
Blobs reachable from all refs 1191
Blobs scanned 1191
Blobs skipped — binary by extension 0
Blobs skipped — over 2097152 bytes 0
Blobs in the object store (incl. unreachable) 1191

Class A — secrets (push-blocking)

No hit on any of the 9 credential patterns below.

Pattern What it looks for Hits Blobs
private-key-block PEM private key material 0 0
github-token GitHub classic/OAuth/app token 0 0
github-fine-grained GitHub fine-grained PAT 0 0
gitlab-token GitLab personal access token 0 0
aws-access-key AWS access key id 0 0
slack-token Slack token 0 0
ai-api-key Anthropic/OpenAI-shaped API key 0 0
netrc-credentials .netrc-style credential line 0 0
assigned-secret a secret-looking name assigned a real-looking value 0 0

Class B — internal information (advisory)

Acceptable while the repo is PRIVATE (SREQ D5). This is the inventory to
read before any decision to make it public — none of it blocks the push.

Pattern What it looks for Hits Blobs
internal-forge-host the private Gitea instance 76 73
internal-domain the operator domain (superset of the row above) 80 75
desktop-hostname the desktop machine name 6 6
tailscale tailnet naming 7 4
operator-home-path absolute home paths carrying the username 23 12
email-address email addresses 215 100
private-ip RFC1918 addresses 0 0
ssh-remote-url ssh remote URLs 5 5

Class B samples

internal-forge-host — 76 hit(s) in 73 blob(s), first 3 shown:

internal-domain — 80 hit(s) in 75 blob(s), first 3 shown:

desktop-hostname — 6 hit(s) in 6 blob(s), first 3 shown:

  • .devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/test-plan.md:7 (af24779bef) — - the desktop (jochems-forge) — currently carries a second clone plus its own farm; becomes a pure consumer.
  • scripts/history-scan.sh:166 (cdc5d50322) — reg("B", "desktop-hostname", "jochems-forge", "the desktop machine name")
  • sdlc-dogfood-run-reports/verity-slot4-230.md:24 (8428495ed8) — from the deployed checkout at 'jochems-forge:~/.claude/repos/devwork-skills':

tailscale — 7 hit(s) in 4 blob(s), first 3 shown:

  • scripts/history-scan.sh:167 (cdc5d50322) — reg("B", "tailscale", "tailscale|\.ts\.net", "tailnet naming")
  • sdlc-retrospectives/2026-08-13.md:98 (1c921c427b) — ## Third pass (same day): transcript sweep over tailscale SSH
  • sdlc-retrospectives/2026-08-13.md:101 (1c921c427b) — tailscale peer with passwordless SSH from this box — transcripts, the verity checkout, and the

operator-home-path — 23 hit(s) in 12 blob(s), first 3 shown:

  • scripts/cutover-dev.sh:134 (f4d574c21f) — # /home/jochem/.claude/repos/devwork-skills -> -home-jochem--claude-repos-devwork-skills
  • scripts/cutover-dev.sh:135 (f4d574c21f) — # /home/jochem/Coding/devwork-skills -> -home-jochem-Coding-devwork-skills
  • scripts/history-scan.sh:168 (cdc5d50322) — reg("B", "operator-home-path", "/home/jochem", "absolute home paths carrying the username")

email-address — 215 hit(s) in 100 blob(s), first 3 shown:

  • plugin/skills/_shared/domains/api.md:350 (c36c691280) — "email": "test@example.com", # From CLAUDE.md test accounts
  • plugin/skills/_shared/domains/api.md:406 (c36c691280) — email: 'test@example.com', // From CLAUDE.md test accounts
  • plugin/skills/_shared/domains/code.md:408 (8f57b71848) — response = client.post("/api/users", json={"name": "", "email": "test@example.com"})

ssh-remote-url — 5 hit(s) in 5 blob(s), first 3 shown:

  • README.md:15 (2ac6d48bf7) — git clone ssh://git@git.wihslon.com:2222/jbr870/devwork-skills.git ~/devwork-skills
  • README.md:15 (ed90bf2052) — git clone ssh://git@git.wihslon.com:2222/jbr870/devwork-skills.git ~/devwork-skills
  • README.md:15 (1a5c8ee5f0) — git clone ssh://git@git.wihslon.com:2222/jbr870/devwork-skills.git ~/devwork-skills

Limitations

  • Reachable objects only. Unreachable objects are counted above but not
    scanned: git push transmits only what the pushed refs reach.
  • Shape-based, not entropy-based. A credential with no recognisable prefix
    and no secret-looking variable name is not detectable by these rules; the
    assigned-secret rule is the only generic one, and it excludes values that
    look like placeholders (so a documented token: <your-token> does not block,
    and a documented real value still does).
  • 40-hex Gitea/Forgejo tokens are not matched — that shape is
    indistinguishable from a git SHA, and this history is full of SHAs. A rule
    for it would be noise, not signal.
  • Class B is deliberately advisory. It is the input to the visibility
    decision, not a gate on the push.

Verdict

P0.8: PASS (no push-blocking findings)

<!-- probe-record:p0.8 skill=develop po=PO-50-3 --> # P0.8 — full-history sensitive-data scan Produced by `scripts/history-scan.sh` on 2026-08-15T14:28:30Z. This gate exists because pushing a repo publishes its **history**, not its tip (SREQ D5). It runs before the first push to the GitHub remote. ## Scope | | | | --- | --- | | Repository | `/home/jochem/.claude/repos/devwork-skills-50` | | HEAD | `ca517f0b49b4cd604340d03b434cdd6a58939a6d` (feature/50-ship-the-suite-as-a-claude-code-marketplace-plugin) | | Refs | 9 | | Commits (all refs) | 239 | | Blobs reachable from all refs | 1191 | | Blobs scanned | 1191 | | Blobs skipped — binary by extension | 0 | | Blobs skipped — over 2097152 bytes | 0 | | Blobs in the object store (incl. unreachable) | 1191 | ## Class A — secrets (push-blocking) No hit on any of the 9 credential patterns below. | Pattern | What it looks for | Hits | Blobs | | ------- | ----------------- | ---- | ----- | | `private-key-block` | PEM private key material | 0 | 0 | | `github-token` | GitHub classic/OAuth/app token | 0 | 0 | | `github-fine-grained` | GitHub fine-grained PAT | 0 | 0 | | `gitlab-token` | GitLab personal access token | 0 | 0 | | `aws-access-key` | AWS access key id | 0 | 0 | | `slack-token` | Slack token | 0 | 0 | | `ai-api-key` | Anthropic/OpenAI-shaped API key | 0 | 0 | | `netrc-credentials` | .netrc-style credential line | 0 | 0 | | `assigned-secret` | a secret-looking name assigned a real-looking value | 0 | 0 | ## Class B — internal information (advisory) Acceptable while the repo is PRIVATE (SREQ D5). This is the inventory to read before any decision to make it public — none of it blocks the push. | Pattern | What it looks for | Hits | Blobs | | ------- | ----------------- | ---- | ----- | | `internal-forge-host` | the private Gitea instance | 76 | 73 | | `internal-domain` | the operator domain (superset of the row above) | 80 | 75 | | `desktop-hostname` | the desktop machine name | 6 | 6 | | `tailscale` | tailnet naming | 7 | 4 | | `operator-home-path` | absolute home paths carrying the username | 23 | 12 | | `email-address` | email addresses | 215 | 100 | | `private-ip` | RFC1918 addresses | 0 | 0 | | `ssh-remote-url` | ssh remote URLs | 5 | 5 | ### Class B samples **internal-forge-host** — 76 hit(s) in 73 blob(s), first 3 shown: - .devcontainer/docker-compose.yml:11 (15b7c3cbcb06) — # SSH agent forwarding from host (git over SSH to git.wihslon.com): - .devcontainer/post-create.sh:22 (f811c0c793cf) — --url "https://git.wihslon.com" \ - .env.example:2 (39d28b4e53cc) — # Get one at https://git.wihslon.com/user/settings/applications **internal-domain** — 80 hit(s) in 75 blob(s), first 3 shown: - .devcontainer/docker-compose.yml:11 (15b7c3cbcb06) — # SSH agent forwarding from host (git over SSH to git.wihslon.com): - .devcontainer/post-create.sh:22 (f811c0c793cf) — --url "https://git.wihslon.com" \ - .env.example:2 (39d28b4e53cc) — # Get one at https://git.wihslon.com/user/settings/applications **desktop-hostname** — 6 hit(s) in 6 blob(s), first 3 shown: - .devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/test-plan.md:7 (af24779bef1d) — - the **desktop (jochems-forge)** — currently carries a second clone plus its own farm; becomes a pure consumer. - scripts/history-scan.sh:166 (cdc5d5032221) — reg("B", "desktop-hostname", "jochems-forge", "the desktop machine name") - sdlc-dogfood-run-reports/verity-slot4-230.md:24 (8428495ed82a) — from the deployed checkout at 'jochems-forge:~/.claude/repos/devwork-skills': **tailscale** — 7 hit(s) in 4 blob(s), first 3 shown: - scripts/history-scan.sh:167 (cdc5d5032221) — reg("B", "tailscale", "tailscale|\\.ts\\.net", "tailnet naming") - sdlc-retrospectives/2026-08-13.md:98 (1c921c427bdc) — ## Third pass (same day): transcript sweep over tailscale SSH - sdlc-retrospectives/2026-08-13.md:101 (1c921c427bdc) — tailscale peer with passwordless SSH from this box — transcripts, the verity checkout, and the **operator-home-path** — 23 hit(s) in 12 blob(s), first 3 shown: - scripts/cutover-dev.sh:134 (f4d574c21f7a) — # /home/jochem/.claude/repos/devwork-skills -> -home-jochem--claude-repos-devwork-skills - scripts/cutover-dev.sh:135 (f4d574c21f7a) — # /home/jochem/Coding/devwork-skills -> -home-jochem-Coding-devwork-skills - scripts/history-scan.sh:168 (cdc5d5032221) — reg("B", "operator-home-path", "/home/jochem", "absolute home paths carrying the username") **email-address** — 215 hit(s) in 100 blob(s), first 3 shown: - plugin/skills/_shared/domains/api.md:350 (c36c69128073) — "email": "test@example.com", # From CLAUDE.md test accounts - plugin/skills/_shared/domains/api.md:406 (c36c69128073) — email: 'test@example.com', // From CLAUDE.md test accounts - plugin/skills/_shared/domains/code.md:408 (8f57b71848be) — response = client.post("/api/users", json={"name": "", "email": "test@example.com"}) **ssh-remote-url** — 5 hit(s) in 5 blob(s), first 3 shown: - README.md:15 (2ac6d48bf76d) — git clone ssh://git@git.wihslon.com:2222/jbr870/devwork-skills.git ~/devwork-skills - README.md:15 (ed90bf20522c) — git clone ssh://git@git.wihslon.com:2222/jbr870/devwork-skills.git ~/devwork-skills - README.md:15 (1a5c8ee5f0f8) — git clone ssh://git@git.wihslon.com:2222/jbr870/devwork-skills.git ~/devwork-skills ## Limitations - **Reachable objects only.** Unreachable objects are counted above but not scanned: `git push` transmits only what the pushed refs reach. - **Shape-based, not entropy-based.** A credential with no recognisable prefix and no secret-looking variable name is not detectable by these rules; the `assigned-secret` rule is the only generic one, and it excludes values that look like placeholders (so a documented `token: <your-token>` does not block, and a documented real value still does). - **40-hex Gitea/Forgejo tokens are not matched** — that shape is indistinguishable from a git SHA, and this history is full of SHAs. A rule for it would be noise, not signal. - **Class B is deliberately advisory.** It is the input to the visibility decision, not a gate on the push. ## Verdict P0.8: PASS (no push-blocking findings)
Author
Owner
{
  "phase": "develop",
  "learnings": [
    {"id": "L-1", "scope": "stack:claude-plugin-cli", "status": "applied", "summary": "claude plugin validate --strict takes the plugin/marketplace ROOT directory (it appends .claude-plugin/<kind>.json itself); strict fails on a missing top-level marketplace description and on any unknown field; marketplace validation recurses into the referenced plugin's manifest", "evidence": "DISCOVERY entries (validator invocations, marketplace schema), WU-50-3-1 journal", "source_ref": "comment:1011", "applied_to": ".claude/skills/stack-claude-plugin-cli/SKILL.md"},
    {"id": "L-2", "scope": "stack:claude-plugin-cli", "status": "applied", "summary": "claude plugin tag --dry-run enforces only tag uniqueness — the dirty-tree and version-agreement legs of the enforcement trio run only on the real tagging path, so release tooling must own its git-side refusals", "evidence": "DISCOVERY (tag --dry-run probe), WU-50-3-4 journal", "source_ref": "comment:1021", "applied_to": ".claude/skills/stack-claude-plugin-cli/SKILL.md"},
    {"id": "L-3", "scope": "stack:claude-plugin-cli", "status": "applied", "summary": "plugin install <p>@<m>@<version> parses but does NOT pin (installs whatever the marketplace snapshot carries); install on an installed plugin is a no-op; plugin update follows the snapshot in either direction; a GitHub-sourced marketplace is a shallow git clone — version selection = point the snapshot at the tag (fetch --unshallow --tags), then update", "evidence": "DISCOVERY (scratch CLAUDE_CONFIG_DIR probes with a two-release scratch marketplace), WU-50-3-6 journal", "source_ref": "comment:1025", "applied_to": ".claude/skills/stack-claude-plugin-cli/SKILL.md"},
    {"id": "L-4", "scope": "project", "status": "applied", "summary": "Repo tools that scan or rewrite skill markdown must be fence-aware (CommonMark >= -backtick close rule; quoting rules differ between fence and prose) — reuse lint-conventions.sh's fence tracker, never line-regex", "evidence": "DISCOVERY (awk quoting over-reach, reverted and redone across three files), WU-50-3-2 journal", "source_ref": "comment:1013", "applied_to": "CLAUDE.md"}
  ]
}
<!-- learning:v1 issue=50 skill=develop po=PO-50-3 --> ```json { "phase": "develop", "learnings": [ {"id": "L-1", "scope": "stack:claude-plugin-cli", "status": "applied", "summary": "claude plugin validate --strict takes the plugin/marketplace ROOT directory (it appends .claude-plugin/<kind>.json itself); strict fails on a missing top-level marketplace description and on any unknown field; marketplace validation recurses into the referenced plugin's manifest", "evidence": "DISCOVERY entries (validator invocations, marketplace schema), WU-50-3-1 journal", "source_ref": "comment:1011", "applied_to": ".claude/skills/stack-claude-plugin-cli/SKILL.md"}, {"id": "L-2", "scope": "stack:claude-plugin-cli", "status": "applied", "summary": "claude plugin tag --dry-run enforces only tag uniqueness — the dirty-tree and version-agreement legs of the enforcement trio run only on the real tagging path, so release tooling must own its git-side refusals", "evidence": "DISCOVERY (tag --dry-run probe), WU-50-3-4 journal", "source_ref": "comment:1021", "applied_to": ".claude/skills/stack-claude-plugin-cli/SKILL.md"}, {"id": "L-3", "scope": "stack:claude-plugin-cli", "status": "applied", "summary": "plugin install <p>@<m>@<version> parses but does NOT pin (installs whatever the marketplace snapshot carries); install on an installed plugin is a no-op; plugin update follows the snapshot in either direction; a GitHub-sourced marketplace is a shallow git clone — version selection = point the snapshot at the tag (fetch --unshallow --tags), then update", "evidence": "DISCOVERY (scratch CLAUDE_CONFIG_DIR probes with a two-release scratch marketplace), WU-50-3-6 journal", "source_ref": "comment:1025", "applied_to": ".claude/skills/stack-claude-plugin-cli/SKILL.md"}, {"id": "L-4", "scope": "project", "status": "applied", "summary": "Repo tools that scan or rewrite skill markdown must be fence-aware (CommonMark >= -backtick close rule; quoting rules differ between fence and prose) — reuse lint-conventions.sh's fence tracker, never line-regex", "evidence": "DISCOVERY (awk quoting over-reach, reverted and redone across three files), WU-50-3-2 journal", "source_ref": "comment:1013", "applied_to": "CLAUDE.md"} ] } ```
Author
Owner
{
  "next_state": "qa",
  "produced": [
    {
      "kind": "wu-plan",
      "ref": "1010",
      "summary": "6 WUs in 4 waves, fully sequential in the isolated worktree; TDD pair on WU-4 only (artifact-shape exception elsewhere)"
    },
    {
      "kind": "work-unit-outcome",
      "ref": "1012",
      "summary": "WU-50-3-1: restructure into plugin/ + manifests + lint retarget (201 pure renames, one commit)"
    },
    {
      "kind": "work-unit-outcome",
      "ref": "1015",
      "summary": "WU-50-3-2: reference rewrite to the dev namespace (54+299+565 occurrences, D10 fix, audit script)"
    },
    {
      "kind": "work-unit-outcome",
      "ref": "1020",
      "summary": "WU-50-3-3: meta lane on the D7 helper root + /dev: names"
    },
    {
      "kind": "work-unit-outcome",
      "ref": "1022",
      "summary": "WU-50-3-4: TDD pair — test-plugin-gates.sh red then release.sh + tier-3 lint rules green (7/7)"
    },
    {
      "kind": "work-unit-outcome",
      "ref": "1024",
      "summary": "WU-50-3-5: cutover-dev.sh + cutover-abort.sh (D11 state machine, S-2 gate, scratch-rehearsed)"
    },
    {
      "kind": "work-unit-outcome",
      "ref": "1026",
      "summary": "WU-50-3-6: README runbooks (CLI-verified) + history-scan.sh; P0.8 run: PASS"
    },
    {
      "kind": "dev-journal",
      "ref": "1011",
      "summary": "WU-1 journal (validator invocation + strict-schema discoveries)"
    },
    {
      "kind": "dev-journal",
      "ref": "1013",
      "summary": "WU-2 journal (rewrite arithmetic, polysemy calls, allow-anchor list, D10 rationale)"
    },
    {
      "kind": "dev-journal",
      "ref": "1016",
      "summary": "WU-3 journal (D7 root vs drain-loop cd ordering)"
    },
    {
      "kind": "dev-journal",
      "ref": "1021",
      "summary": "WU-4 journal (both TDD phases; tag --dry-run and D4 discoveries)"
    },
    {
      "kind": "dev-journal",
      "ref": "1023",
      "summary": "WU-5 journal (pre-merge layout gap; abort-restores-links-not-farm)"
    },
    {
      "kind": "dev-journal",
      "ref": "1025",
      "summary": "WU-6 journal (CLI verification table; install@version does not pin; P0.8 result)"
    },
    {
      "kind": "probe-record",
      "ref": "1027",
      "summary": "P0.8 full-history sensitive-data scan: PASS (239 commits, 1191 blobs, 0 push-blocking; Class B advisory inventory for the visibility decision)"
    },
    {
      "kind": "learning",
      "ref": "comment:1028",
      "summary": "4 learnings (4 applied: 3 to stack-claude-plugin-cli skill, 1 to CLAUDE.md)"
    }
  ],
  "findings": [
    {
      "category": "in-scope-deferrable",
      "summary": "17 human-uat test-plan scenarios (AC-2, AC-5..AC-12 legs) execute in the post-develop phases 4-7: coexistence dogfood, rehearsed dev cutover, release 0.1.0 + desktop cutover, second release + update ritual",
      "reasoning": "The lane-annotated test plan (comment 978) routes these to human-uat by design - they are live external writes, interactive-session behavior, or machine cutovers a develop-phase run cannot perform. /develop delivered the tooling and runbooks they execute: cutover scripts scratch-rehearsed end-to-end (forward 19/19, abort 7/7, S-2 negative), release tooling with dry-run coverage, README runbooks with CLI-verified commands. This is the plan's lane split, not a develop-time deferral decision.",
      "proposed_action": "accept",
      "feature_value": "core",
      "requires_product_decision": false,
      "applied_disposition": "accept",
      "id": "F-PO-50-3-1"
    },
    {
      "category": "in-scope-deferrable",
      "summary": "Aborting the dev cutover AFTER the merge step is a two-part act: cutover-abort.sh restores the recorded state but the 15 re-created farm links dangle until the operator decides about the merge (script exits non-zero naming the pre-merge SHA)",
      "reasoning": "The merge moves .claude/skills/ away, so restored links point at nothing; auto-rewinding a possibly-pushed merge would be worse than reporting honestly. The abort script counts dangling links, reports PRE-CUTOVER SHAPE BUT LINKS DANGLE in status, and the README documents the two-part act. UAT Scenario 8 should evaluate AC-7 with this semantic in mind.",
      "proposed_action": "accept",
      "feature_value": "core",
      "requires_product_decision": false,
      "applied_disposition": "accept",
      "id": "F-PO-50-3-2"
    },
    {
      "category": "in-scope-deferrable",
      "summary": "claude plugin install's @version suffix does not pin (verified CLI 2.1.229): version selection is a property of the marketplace snapshot, so the README's install/update/recovery rituals are snapshot-then-update; the end-to-end recovery composition against the private GitHub source remains unverified from the dev machine",
      "reasoning": "Probed in a scratch CLAUDE_CONFIG_DIR with a two-release scratch marketplace: @version is stripped, install-on-installed is a no-op, update follows the snapshot bidirectionally, GitHub-sourced snapshots are shallow clones needing fetch --unshallow --tags. The README documents the verified ritual and names the unverified composition; UAT Scenarios 14/16/21 exercise it for real.",
      "proposed_action": "accept",
      "feature_value": "core",
      "requires_product_decision": false,
      "applied_disposition": "accept",
      "id": "F-PO-50-3-3"
    },
    {
      "category": "in-scope-deferrable",
      "summary": "SREQ D6 correction: claude plugin tag --dry-run enforces only tag uniqueness - the dirty-tree and version-agreement refusals live only on the real tagging path, so release.sh's own git-side refusal trio is load-bearing, not a convenience layer",
      "reasoning": "Discovered by probing before implementation (WU-4 journal). release.sh implements the trio itself, ordered before any CLI dependency; scenarios 18-20 of test-plugin-gates.sh pin the behavior mechanically. Nothing remains to do; recorded so QA and future maintainers do not re-read D6's delegation wording as license to drop the checks.",
      "proposed_action": "accept",
      "feature_value": "incidental",
      "requires_product_decision": false,
      "applied_disposition": "accept",
      "id": "F-PO-50-3-4"
    },
    {
      "category": "in-scope-deferrable",
      "summary": "SREQ D4 correction: the canonicalization ban is implemented as a ban on SELF-canonicalization - realpath/readlink -f banned outright, cd -P/pwd -P flagged only on lines also referencing $0/BASH_SOURCE - because two legitimate caller-directory uses (slot-retire.sh, a slot test fixture) falsified the SREQ's zero-current-violations claim",
      "reasoning": "D4 protects 'the loaded (symlink) path is canonical for the helper itself'; slot-retire.sh canonicalizes a caller-passed directory for slot-parent comparison, which the decision does not forbid. The narrowed rule still catches the seeded and realistic regressions (scenario 24 D4 control green); known limit (two-line self-location split) documented in the lint script. Plugin/ was out of bounds for the lint WU, so narrowing the rule to the decision's purpose was the correct resolution.",
      "proposed_action": "accept",
      "feature_value": "incidental",
      "requires_product_decision": false,
      "applied_disposition": "accept",
      "id": "F-PO-50-3-5"
    }
  ],
  "pending_decisions": []
}
<!-- phase-outcome:v1 id=PO-50-3 skill=develop --> ```json { "next_state": "qa", "produced": [ { "kind": "wu-plan", "ref": "1010", "summary": "6 WUs in 4 waves, fully sequential in the isolated worktree; TDD pair on WU-4 only (artifact-shape exception elsewhere)" }, { "kind": "work-unit-outcome", "ref": "1012", "summary": "WU-50-3-1: restructure into plugin/ + manifests + lint retarget (201 pure renames, one commit)" }, { "kind": "work-unit-outcome", "ref": "1015", "summary": "WU-50-3-2: reference rewrite to the dev namespace (54+299+565 occurrences, D10 fix, audit script)" }, { "kind": "work-unit-outcome", "ref": "1020", "summary": "WU-50-3-3: meta lane on the D7 helper root + /dev: names" }, { "kind": "work-unit-outcome", "ref": "1022", "summary": "WU-50-3-4: TDD pair — test-plugin-gates.sh red then release.sh + tier-3 lint rules green (7/7)" }, { "kind": "work-unit-outcome", "ref": "1024", "summary": "WU-50-3-5: cutover-dev.sh + cutover-abort.sh (D11 state machine, S-2 gate, scratch-rehearsed)" }, { "kind": "work-unit-outcome", "ref": "1026", "summary": "WU-50-3-6: README runbooks (CLI-verified) + history-scan.sh; P0.8 run: PASS" }, { "kind": "dev-journal", "ref": "1011", "summary": "WU-1 journal (validator invocation + strict-schema discoveries)" }, { "kind": "dev-journal", "ref": "1013", "summary": "WU-2 journal (rewrite arithmetic, polysemy calls, allow-anchor list, D10 rationale)" }, { "kind": "dev-journal", "ref": "1016", "summary": "WU-3 journal (D7 root vs drain-loop cd ordering)" }, { "kind": "dev-journal", "ref": "1021", "summary": "WU-4 journal (both TDD phases; tag --dry-run and D4 discoveries)" }, { "kind": "dev-journal", "ref": "1023", "summary": "WU-5 journal (pre-merge layout gap; abort-restores-links-not-farm)" }, { "kind": "dev-journal", "ref": "1025", "summary": "WU-6 journal (CLI verification table; install@version does not pin; P0.8 result)" }, { "kind": "probe-record", "ref": "1027", "summary": "P0.8 full-history sensitive-data scan: PASS (239 commits, 1191 blobs, 0 push-blocking; Class B advisory inventory for the visibility decision)" }, { "kind": "learning", "ref": "comment:1028", "summary": "4 learnings (4 applied: 3 to stack-claude-plugin-cli skill, 1 to CLAUDE.md)" } ], "findings": [ { "category": "in-scope-deferrable", "summary": "17 human-uat test-plan scenarios (AC-2, AC-5..AC-12 legs) execute in the post-develop phases 4-7: coexistence dogfood, rehearsed dev cutover, release 0.1.0 + desktop cutover, second release + update ritual", "reasoning": "The lane-annotated test plan (comment 978) routes these to human-uat by design - they are live external writes, interactive-session behavior, or machine cutovers a develop-phase run cannot perform. /develop delivered the tooling and runbooks they execute: cutover scripts scratch-rehearsed end-to-end (forward 19/19, abort 7/7, S-2 negative), release tooling with dry-run coverage, README runbooks with CLI-verified commands. This is the plan's lane split, not a develop-time deferral decision.", "proposed_action": "accept", "feature_value": "core", "requires_product_decision": false, "applied_disposition": "accept", "id": "F-PO-50-3-1" }, { "category": "in-scope-deferrable", "summary": "Aborting the dev cutover AFTER the merge step is a two-part act: cutover-abort.sh restores the recorded state but the 15 re-created farm links dangle until the operator decides about the merge (script exits non-zero naming the pre-merge SHA)", "reasoning": "The merge moves .claude/skills/ away, so restored links point at nothing; auto-rewinding a possibly-pushed merge would be worse than reporting honestly. The abort script counts dangling links, reports PRE-CUTOVER SHAPE BUT LINKS DANGLE in status, and the README documents the two-part act. UAT Scenario 8 should evaluate AC-7 with this semantic in mind.", "proposed_action": "accept", "feature_value": "core", "requires_product_decision": false, "applied_disposition": "accept", "id": "F-PO-50-3-2" }, { "category": "in-scope-deferrable", "summary": "claude plugin install's @version suffix does not pin (verified CLI 2.1.229): version selection is a property of the marketplace snapshot, so the README's install/update/recovery rituals are snapshot-then-update; the end-to-end recovery composition against the private GitHub source remains unverified from the dev machine", "reasoning": "Probed in a scratch CLAUDE_CONFIG_DIR with a two-release scratch marketplace: @version is stripped, install-on-installed is a no-op, update follows the snapshot bidirectionally, GitHub-sourced snapshots are shallow clones needing fetch --unshallow --tags. The README documents the verified ritual and names the unverified composition; UAT Scenarios 14/16/21 exercise it for real.", "proposed_action": "accept", "feature_value": "core", "requires_product_decision": false, "applied_disposition": "accept", "id": "F-PO-50-3-3" }, { "category": "in-scope-deferrable", "summary": "SREQ D6 correction: claude plugin tag --dry-run enforces only tag uniqueness - the dirty-tree and version-agreement refusals live only on the real tagging path, so release.sh's own git-side refusal trio is load-bearing, not a convenience layer", "reasoning": "Discovered by probing before implementation (WU-4 journal). release.sh implements the trio itself, ordered before any CLI dependency; scenarios 18-20 of test-plugin-gates.sh pin the behavior mechanically. Nothing remains to do; recorded so QA and future maintainers do not re-read D6's delegation wording as license to drop the checks.", "proposed_action": "accept", "feature_value": "incidental", "requires_product_decision": false, "applied_disposition": "accept", "id": "F-PO-50-3-4" }, { "category": "in-scope-deferrable", "summary": "SREQ D4 correction: the canonicalization ban is implemented as a ban on SELF-canonicalization - realpath/readlink -f banned outright, cd -P/pwd -P flagged only on lines also referencing $0/BASH_SOURCE - because two legitimate caller-directory uses (slot-retire.sh, a slot test fixture) falsified the SREQ's zero-current-violations claim", "reasoning": "D4 protects 'the loaded (symlink) path is canonical for the helper itself'; slot-retire.sh canonicalizes a caller-passed directory for slot-parent comparison, which the decision does not forbid. The narrowed rule still catches the seeded and realistic regressions (scenario 24 D4 control green); known limit (two-line self-location split) documented in the lint script. Plugin/ was out of bounds for the lint WU, so narrowing the rule to the decision's purpose was the correct resolution.", "proposed_action": "accept", "feature_value": "incidental", "requires_product_decision": false, "applied_disposition": "accept", "id": "F-PO-50-3-5" } ], "pending_decisions": [] } ```
Author
Owner
{
  "outcome": "issues-found",
  "summary": "41 distinct candidates from the 4-reviewer team + test-quality audit -> 37 verified (0 false positives, 4 below floor): 12 in-scope-blocking (1 Critical: history-scan redaction leaks adjacent secrets; population gap: commit/tag payloads unscanned; cutover preflight null-poisoning; abort clobbers ~/.claude.json; choreography + default-allow gates; release first-run/resume aliasing; gate-suite contract gaps), 20 trivial/small findings promoted to fix-now by the recommender, 5 accepted with reasoning. Full detail: .devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/code-report.md",
  "findings": [
    { "id": "CR-1", "category": "in-scope-blocking", "severity": "critical", "summary": "history-scan.sh redact() masks only the leftmost match; adjacent context incl. a second same-line secret is emitted verbatim into the P0.8 report (PoC verified)" },
    { "id": "CR-2", "category": "in-scope-blocking", "severity": "high", "summary": "history-scan.sh scans blobs only: commit messages, idents, annotated-tag payloads are pushed but never scanned; gap undisclosed in Limitations" },
    { "id": "CR-3", "category": "in-scope-blocking", "severity": "high", "summary": "cutover-dev.sh state_get prints literal 'null' with no state file; resolve_feature_branch treats it as a branch -> documented preflight entry point fails closed with two false blockers" },
    { "id": "CR-4", "category": "in-scope-blocking", "severity": "high", "summary": "cutover-abort.sh restores ~/.claude.json from the backup whenever it differs, without checking the repo-move/migration step ever ran -> early-stage abort rolls back unrelated runtime-config changes" },
    { "id": "CR-5", "category": "in-scope-blocking", "severity": "medium", "summary": "cutover-dev.sh has no STOP between merge and repo_move; a single run barrels merge->teardown->repo_move and hard-dies on the still-registered feature worktree" },
    { "id": "CR-6", "category": "in-scope-blocking", "severity": "medium", "summary": "preflight is done-once; S-2 no-feature-in-flight and farm/worktree gates never re-run at cutover run time" },
    { "id": "CR-7", "category": "in-scope-blocking", "severity": "medium", "summary": "git worktree list failure (silenced, unchecked) indistinguishable from 'no worktrees' -> default-allow in front of the destructive repo move and in the S-2 exemption" },
    { "id": "CR-8", "category": "in-scope-blocking", "severity": "medium", "summary": "release.sh first-ever release takes the resume branch: 'resuming a committed-but-untagged release' message and RELEASES.md records arbitrary HEAD as the bump commit" },
    { "id": "CR-9", "category": "in-scope-blocking", "severity": "medium", "summary": "test-plugin-gates.sh positive control omits require_claude -> hard FAIL instead of the loud SKIP the suite contract promises on claude-less boxes" },
    { "id": "CR-10", "category": "in-scope-blocking", "severity": "medium", "summary": "no scenario exercises release.sh's bump-and-commit path; a bump-corrupting bug turns nothing red" },
    { "id": "CR-11", "category": "in-scope-blocking", "severity": "medium", "summary": "tag-created-but-push-failed state hits refusal 3 with wrong guidance ('bump to a new version'); RELEASES.md record never written" },
    { "id": "CR-12", "category": "in-scope-blocking", "severity": "medium", "summary": "runtime-config backups made with plain cp; umask can widen a 0600 ~/.claude.json to 0644 in the backup copies" },
    { "id": "CR-13", "category": "in-scope-deferrable", "severity": "low", "summary": "release.sh assumes .plugins[0] is the dev entry, no name assertion", "reasoning": "latent until a second plugin exists; trivial jq assert", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "core" },
    { "id": "CR-14", "category": "in-scope-deferrable", "severity": "low", "summary": "release.sh never checks it runs on the integration branch; README enforces it in prose only", "reasoning": "a feature-branch release tags content the default branch never carries; small refusal closes it", "proposed_action": "fix-now", "fix_cost": "small", "adjacent_to_blocking": true, "feature_value": "core" },
    { "id": "CR-15", "category": "in-scope-deferrable", "severity": "low", "summary": "release.sh gate-3 comment claims tag --dry-run owns the enforcement trio, contradicting the repo's own probe record", "reasoning": "comment invites weakening the load-bearing refusals; one-line fix", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" },
    { "id": "CR-16", "category": "in-scope-deferrable", "severity": "medium", "summary": "refusal scenarios 18/19/20 never assert their fixture mutation landed before asserting the refusal", "reasoning": "18/20 run release.sh without --dry-run; a silently-failed fixture heads toward the real write path", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "core" },
    { "id": "CR-17", "category": "in-scope-deferrable", "severity": "low", "summary": "make_clone swallows clone failures", "reasoning": "misattributes env failures to release.sh", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" },
    { "id": "CR-18", "category": "in-scope-deferrable", "severity": "low", "summary": "scenario-24 line-number math assumes trailing newlines in fixtures", "reasoning": "false RED on a newline-less fixture; one guard", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" },
    { "id": "CR-19", "category": "in-scope-deferrable", "severity": "low", "summary": "only the tier-3 scan is non-vacuity-asserted; tier-1 fence-scratch and tier-2 helper populations can silently be empty", "reasoning": "same vacuity class AC-3 guards against, unguarded in two tiers", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "core" },
    { "id": "CR-20", "category": "in-scope-deferrable", "severity": "low", "summary": "class-1 scan misses the quoted \"$HOME\"/.claude/skills spelling", "reasoning": "an escapable spelling of the banned path class; regex + scenario-24 control", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "core" },
    { "id": "CR-21", "category": "in-scope-deferrable", "severity": "low", "summary": "release.sh uses static .tmp temp names (collision/symlink-follow)", "reasoning": "operator-owned dirs make exploitation unlikely; trivial hardening", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" },
    { "id": "CR-22", "category": "in-scope-deferrable", "severity": "low", "summary": "norm_path glob-expands path components (set -- $p without set -f) in both cutover scripts", "reasoning": "corrupts containment checks on glob-char paths; two-line guard", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" },
    { "id": "CR-23", "category": "in-scope-deferrable", "severity": "low", "summary": "trailing slash in CUTOVER_REPO/CUTOVER_HOME defeats every case containment check with a misdirecting diagnosis", "reasoning": "strip on ingest; one line", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" },
    { "id": "CR-24", "category": "in-scope-deferrable", "severity": "low", "summary": "cutover-abort --dry-run skips the dangling-farm check, so rehearsals under-report vs the real abort", "reasoning": "the check is read-only; no reason to skip it in dry-run", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "core" },
    { "id": "CR-25", "category": "in-scope-deferrable", "severity": "medium", "summary": "post-merge abort removes the plugin link — the only resolving form — while the restored farm is known-dangling", "reasoning": "keeping the working form with a loud note honors the no-dead-window intent better; small change, judged developer-decidable within the two-part-abort semantic", "proposed_action": "fix-now", "fix_cost": "small", "adjacent_to_blocking": true, "feature_value": "core" },
    { "id": "CR-26", "category": "in-scope-deferrable", "severity": "low", "summary": "rewrite-plugin-refs.sh honors allow: user-scope-path but not allow: bare-skill-name", "reasoning": "a re-run would silently defeat the lint's other exemption anchor; latent (no such anchors in tree)", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" },
    { "id": "CR-27", "category": "in-scope-deferrable", "severity": "medium", "summary": "rewrite-plugin-refs.sh quote_pr corrupts a use site already inside a double-quoted string (non-adjacent opening quote), and --check calls the corrupted output clean", "reasoning": "latent tool defect in a kept rerunnable tool; tree verified clean today; quote-parity guard closes it", "proposed_action": "fix-now", "fix_cost": "small", "adjacent_to_blocking": true, "feature_value": "incidental" },
    { "id": "CR-28", "category": "in-scope-deferrable", "severity": "low", "summary": "_lib.sh: failed adapter resolution exits via set -e before the crafted _die message — the new 'looked in' diagnostic is dead code", "reasoning": "failure shape pre-exists at merge-base but the feature edited exactly these lines; minimal guard completes sanctioned touch #2", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" },
    { "id": "CR-29", "category": "in-scope-deferrable", "severity": "low", "summary": "history-scan.sh lacks the executable bit", "reasoning": "inconsistent with every sibling in scripts/", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" },
    { "id": "CR-30", "category": "in-scope-deferrable", "severity": "low", "summary": "README consumer-runbook grep relies on GNU \\b word boundaries", "reasoning": "silently matches nothing on BSD grep; POSIX-safe form is a one-line doc fix", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" },
    { "id": "CR-31", "category": "in-scope-deferrable", "severity": "low", "summary": "README recovery runbook's git fetch --unshallow fails on any repeat; no second-run form given", "reasoning": "the runbook's own step 1 breaks on retry; one sentence fixes it", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" },
    { "id": "CR-32", "category": "in-scope-deferrable", "severity": "low", "summary": "cutover status subcommand dies on a mismatched state file instead of reporting it", "reasoning": "the R8 'run this when confused' probe refuses exactly when confused; make status report read-only", "proposed_action": "fix-now", "fix_cost": "small", "adjacent_to_blocking": true, "feature_value": "incidental" },
    { "id": "CR-33", "category": "in-scope-deferrable", "severity": "low", "summary": "D12's 'dynamic forms (string-built names)' ban has no dedicated lint rule or scenario-24 control", "reasoning": "no bounded mechanical signature exists for the class; the unbraced/escaped/unquoted bans cover every observed leak path; a dynamic form yields an empty value at runtime and is caught by D12's baked-path non-empty assertion; ${!var} indirection is non-POSIX and outside the emitted-glue baseline already", "proposed_action": "accept", "fix_cost": "substantial", "adjacent_to_blocking": false, "feature_value": "incidental" },
    { "id": "CR-34", "category": "in-scope-deferrable", "severity": "low", "summary": "cutover worktree parsing breaks on paths containing spaces (porcelain awk + unquoted iteration)", "reasoning": "both machines' repo/worktree roots are space-free by layout convention; the cutover tooling is repo-local and retires after Phase 6/7", "proposed_action": "accept", "fix_cost": "small", "adjacent_to_blocking": false, "feature_value": "none" },
    { "id": "CR-35", "category": "in-scope-deferrable", "severity": "low", "summary": "history-scan framing is not NUL-safe; a NUL-bearing text-extension blob could silently mis-frame", "reasoning": "repo history is own-authored markdown/shell (no UTF-16/NUL blobs); disclosed in Limitations as part of the CR-2 edit", "proposed_action": "accept", "fix_cost": "substantial", "adjacent_to_blocking": false, "feature_value": "incidental" },
    { "id": "CR-36", "category": "in-scope-deferrable", "severity": "low", "summary": "plugin link dangles between step 3 and the merge when the main checkout itself sits on the feature branch", "reasoning": "layout not used: the main checkout stays on main and the plugin source resolves to the feature worktree; the step-3 STOP verifies before merge", "proposed_action": "accept", "fix_cost": "small", "adjacent_to_blocking": false, "feature_value": "none" },
    { "id": "CR-37", "category": "in-scope-deferrable", "severity": "low", "summary": "do_backup re-runs overwrite integration_sha_at_backup and the slug fields", "reasoning": "audit nit only; abort keys off pre_merge_integration_sha which is write-once", "proposed_action": "accept", "fix_cost": "trivial", "adjacent_to_blocking": false, "feature_value": "none" }
  ],
  "artifacts": {
    "report_path": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/code-report.md",
    "journal_path": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/code-journal.md"
  }
}
<!-- qa-report:v1 issue=50 skill=qa domain=code phase=validate --> ```json { "outcome": "issues-found", "summary": "41 distinct candidates from the 4-reviewer team + test-quality audit -> 37 verified (0 false positives, 4 below floor): 12 in-scope-blocking (1 Critical: history-scan redaction leaks adjacent secrets; population gap: commit/tag payloads unscanned; cutover preflight null-poisoning; abort clobbers ~/.claude.json; choreography + default-allow gates; release first-run/resume aliasing; gate-suite contract gaps), 20 trivial/small findings promoted to fix-now by the recommender, 5 accepted with reasoning. Full detail: .devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/code-report.md", "findings": [ { "id": "CR-1", "category": "in-scope-blocking", "severity": "critical", "summary": "history-scan.sh redact() masks only the leftmost match; adjacent context incl. a second same-line secret is emitted verbatim into the P0.8 report (PoC verified)" }, { "id": "CR-2", "category": "in-scope-blocking", "severity": "high", "summary": "history-scan.sh scans blobs only: commit messages, idents, annotated-tag payloads are pushed but never scanned; gap undisclosed in Limitations" }, { "id": "CR-3", "category": "in-scope-blocking", "severity": "high", "summary": "cutover-dev.sh state_get prints literal 'null' with no state file; resolve_feature_branch treats it as a branch -> documented preflight entry point fails closed with two false blockers" }, { "id": "CR-4", "category": "in-scope-blocking", "severity": "high", "summary": "cutover-abort.sh restores ~/.claude.json from the backup whenever it differs, without checking the repo-move/migration step ever ran -> early-stage abort rolls back unrelated runtime-config changes" }, { "id": "CR-5", "category": "in-scope-blocking", "severity": "medium", "summary": "cutover-dev.sh has no STOP between merge and repo_move; a single run barrels merge->teardown->repo_move and hard-dies on the still-registered feature worktree" }, { "id": "CR-6", "category": "in-scope-blocking", "severity": "medium", "summary": "preflight is done-once; S-2 no-feature-in-flight and farm/worktree gates never re-run at cutover run time" }, { "id": "CR-7", "category": "in-scope-blocking", "severity": "medium", "summary": "git worktree list failure (silenced, unchecked) indistinguishable from 'no worktrees' -> default-allow in front of the destructive repo move and in the S-2 exemption" }, { "id": "CR-8", "category": "in-scope-blocking", "severity": "medium", "summary": "release.sh first-ever release takes the resume branch: 'resuming a committed-but-untagged release' message and RELEASES.md records arbitrary HEAD as the bump commit" }, { "id": "CR-9", "category": "in-scope-blocking", "severity": "medium", "summary": "test-plugin-gates.sh positive control omits require_claude -> hard FAIL instead of the loud SKIP the suite contract promises on claude-less boxes" }, { "id": "CR-10", "category": "in-scope-blocking", "severity": "medium", "summary": "no scenario exercises release.sh's bump-and-commit path; a bump-corrupting bug turns nothing red" }, { "id": "CR-11", "category": "in-scope-blocking", "severity": "medium", "summary": "tag-created-but-push-failed state hits refusal 3 with wrong guidance ('bump to a new version'); RELEASES.md record never written" }, { "id": "CR-12", "category": "in-scope-blocking", "severity": "medium", "summary": "runtime-config backups made with plain cp; umask can widen a 0600 ~/.claude.json to 0644 in the backup copies" }, { "id": "CR-13", "category": "in-scope-deferrable", "severity": "low", "summary": "release.sh assumes .plugins[0] is the dev entry, no name assertion", "reasoning": "latent until a second plugin exists; trivial jq assert", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "core" }, { "id": "CR-14", "category": "in-scope-deferrable", "severity": "low", "summary": "release.sh never checks it runs on the integration branch; README enforces it in prose only", "reasoning": "a feature-branch release tags content the default branch never carries; small refusal closes it", "proposed_action": "fix-now", "fix_cost": "small", "adjacent_to_blocking": true, "feature_value": "core" }, { "id": "CR-15", "category": "in-scope-deferrable", "severity": "low", "summary": "release.sh gate-3 comment claims tag --dry-run owns the enforcement trio, contradicting the repo's own probe record", "reasoning": "comment invites weakening the load-bearing refusals; one-line fix", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" }, { "id": "CR-16", "category": "in-scope-deferrable", "severity": "medium", "summary": "refusal scenarios 18/19/20 never assert their fixture mutation landed before asserting the refusal", "reasoning": "18/20 run release.sh without --dry-run; a silently-failed fixture heads toward the real write path", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "core" }, { "id": "CR-17", "category": "in-scope-deferrable", "severity": "low", "summary": "make_clone swallows clone failures", "reasoning": "misattributes env failures to release.sh", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" }, { "id": "CR-18", "category": "in-scope-deferrable", "severity": "low", "summary": "scenario-24 line-number math assumes trailing newlines in fixtures", "reasoning": "false RED on a newline-less fixture; one guard", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" }, { "id": "CR-19", "category": "in-scope-deferrable", "severity": "low", "summary": "only the tier-3 scan is non-vacuity-asserted; tier-1 fence-scratch and tier-2 helper populations can silently be empty", "reasoning": "same vacuity class AC-3 guards against, unguarded in two tiers", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "core" }, { "id": "CR-20", "category": "in-scope-deferrable", "severity": "low", "summary": "class-1 scan misses the quoted \"$HOME\"/.claude/skills spelling", "reasoning": "an escapable spelling of the banned path class; regex + scenario-24 control", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "core" }, { "id": "CR-21", "category": "in-scope-deferrable", "severity": "low", "summary": "release.sh uses static .tmp temp names (collision/symlink-follow)", "reasoning": "operator-owned dirs make exploitation unlikely; trivial hardening", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" }, { "id": "CR-22", "category": "in-scope-deferrable", "severity": "low", "summary": "norm_path glob-expands path components (set -- $p without set -f) in both cutover scripts", "reasoning": "corrupts containment checks on glob-char paths; two-line guard", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" }, { "id": "CR-23", "category": "in-scope-deferrable", "severity": "low", "summary": "trailing slash in CUTOVER_REPO/CUTOVER_HOME defeats every case containment check with a misdirecting diagnosis", "reasoning": "strip on ingest; one line", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" }, { "id": "CR-24", "category": "in-scope-deferrable", "severity": "low", "summary": "cutover-abort --dry-run skips the dangling-farm check, so rehearsals under-report vs the real abort", "reasoning": "the check is read-only; no reason to skip it in dry-run", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "core" }, { "id": "CR-25", "category": "in-scope-deferrable", "severity": "medium", "summary": "post-merge abort removes the plugin link — the only resolving form — while the restored farm is known-dangling", "reasoning": "keeping the working form with a loud note honors the no-dead-window intent better; small change, judged developer-decidable within the two-part-abort semantic", "proposed_action": "fix-now", "fix_cost": "small", "adjacent_to_blocking": true, "feature_value": "core" }, { "id": "CR-26", "category": "in-scope-deferrable", "severity": "low", "summary": "rewrite-plugin-refs.sh honors allow: user-scope-path but not allow: bare-skill-name", "reasoning": "a re-run would silently defeat the lint's other exemption anchor; latent (no such anchors in tree)", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" }, { "id": "CR-27", "category": "in-scope-deferrable", "severity": "medium", "summary": "rewrite-plugin-refs.sh quote_pr corrupts a use site already inside a double-quoted string (non-adjacent opening quote), and --check calls the corrupted output clean", "reasoning": "latent tool defect in a kept rerunnable tool; tree verified clean today; quote-parity guard closes it", "proposed_action": "fix-now", "fix_cost": "small", "adjacent_to_blocking": true, "feature_value": "incidental" }, { "id": "CR-28", "category": "in-scope-deferrable", "severity": "low", "summary": "_lib.sh: failed adapter resolution exits via set -e before the crafted _die message — the new 'looked in' diagnostic is dead code", "reasoning": "failure shape pre-exists at merge-base but the feature edited exactly these lines; minimal guard completes sanctioned touch #2", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" }, { "id": "CR-29", "category": "in-scope-deferrable", "severity": "low", "summary": "history-scan.sh lacks the executable bit", "reasoning": "inconsistent with every sibling in scripts/", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" }, { "id": "CR-30", "category": "in-scope-deferrable", "severity": "low", "summary": "README consumer-runbook grep relies on GNU \\b word boundaries", "reasoning": "silently matches nothing on BSD grep; POSIX-safe form is a one-line doc fix", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" }, { "id": "CR-31", "category": "in-scope-deferrable", "severity": "low", "summary": "README recovery runbook's git fetch --unshallow fails on any repeat; no second-run form given", "reasoning": "the runbook's own step 1 breaks on retry; one sentence fixes it", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" }, { "id": "CR-32", "category": "in-scope-deferrable", "severity": "low", "summary": "cutover status subcommand dies on a mismatched state file instead of reporting it", "reasoning": "the R8 'run this when confused' probe refuses exactly when confused; make status report read-only", "proposed_action": "fix-now", "fix_cost": "small", "adjacent_to_blocking": true, "feature_value": "incidental" }, { "id": "CR-33", "category": "in-scope-deferrable", "severity": "low", "summary": "D12's 'dynamic forms (string-built names)' ban has no dedicated lint rule or scenario-24 control", "reasoning": "no bounded mechanical signature exists for the class; the unbraced/escaped/unquoted bans cover every observed leak path; a dynamic form yields an empty value at runtime and is caught by D12's baked-path non-empty assertion; ${!var} indirection is non-POSIX and outside the emitted-glue baseline already", "proposed_action": "accept", "fix_cost": "substantial", "adjacent_to_blocking": false, "feature_value": "incidental" }, { "id": "CR-34", "category": "in-scope-deferrable", "severity": "low", "summary": "cutover worktree parsing breaks on paths containing spaces (porcelain awk + unquoted iteration)", "reasoning": "both machines' repo/worktree roots are space-free by layout convention; the cutover tooling is repo-local and retires after Phase 6/7", "proposed_action": "accept", "fix_cost": "small", "adjacent_to_blocking": false, "feature_value": "none" }, { "id": "CR-35", "category": "in-scope-deferrable", "severity": "low", "summary": "history-scan framing is not NUL-safe; a NUL-bearing text-extension blob could silently mis-frame", "reasoning": "repo history is own-authored markdown/shell (no UTF-16/NUL blobs); disclosed in Limitations as part of the CR-2 edit", "proposed_action": "accept", "fix_cost": "substantial", "adjacent_to_blocking": false, "feature_value": "incidental" }, { "id": "CR-36", "category": "in-scope-deferrable", "severity": "low", "summary": "plugin link dangles between step 3 and the merge when the main checkout itself sits on the feature branch", "reasoning": "layout not used: the main checkout stays on main and the plugin source resolves to the feature worktree; the step-3 STOP verifies before merge", "proposed_action": "accept", "fix_cost": "small", "adjacent_to_blocking": false, "feature_value": "none" }, { "id": "CR-37", "category": "in-scope-deferrable", "severity": "low", "summary": "do_backup re-runs overwrite integration_sha_at_backup and the slug fields", "reasoning": "audit nit only; abort keys off pre_merge_integration_sha which is write-once", "proposed_action": "accept", "fix_cost": "trivial", "adjacent_to_blocking": false, "feature_value": "none" } ], "artifacts": { "report_path": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/code-report.md", "journal_path": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/code-journal.md" } } ```
Author
Owner
{ "outcome": "skipped", "summary": "No browser e2e surface. The PREQ-derived validation run executed instead via the shell harness: lint-conventions.sh clean (199 shipped files examined, non-vacuous) and test-plugin-gates.sh 7/7 PASS (scenarios 1, 2, 18, 19, 20, 23, 24 incl. all 15 per-class scan controls) — doubling as the integration-covered lane-boot check. The 17 human-uat scenarios (3-17, 21, 22) carry to UAT by plan.", "findings": [], "not_applicable_reason": "markdown+shell skill suite — no application or browser surface to drive end-to-end; validation is the shell test harness plus dogfooding" }
<!-- qa-report:v1 issue=50 skill=qa domain=e2e phase=validate --> ```json { "outcome": "skipped", "summary": "No browser e2e surface. The PREQ-derived validation run executed instead via the shell harness: lint-conventions.sh clean (199 shipped files examined, non-vacuous) and test-plugin-gates.sh 7/7 PASS (scenarios 1, 2, 18, 19, 20, 23, 24 incl. all 15 per-class scan controls) — doubling as the integration-covered lane-boot check. The 17 human-uat scenarios (3-17, 21, 22) carry to UAT by plan.", "findings": [], "not_applicable_reason": "markdown+shell skill suite — no application or browser surface to drive end-to-end; validation is the shell test harness plus dogfooding" } ```
Author
Owner
{ "outcome": "skipped", "summary": "No rendered UI in this project.", "findings": [], "not_applicable_reason": "no rendered UI — the deliverables are markdown skill text and shell helpers" }
<!-- qa-report:v1 issue=50 skill=qa domain=a11y phase=validate --> ```json { "outcome": "skipped", "summary": "No rendered UI in this project.", "findings": [], "not_applicable_reason": "no rendered UI — the deliverables are markdown skill text and shell helpers" } ```
Author
Owner
{ "outcome": "skipped", "summary": "No browser surface in this project.", "findings": [], "not_applicable_reason": "no browser surface — nothing is served or rendered" }
<!-- qa-report:v1 issue=50 skill=qa domain=security-browser phase=validate --> ```json { "outcome": "skipped", "summary": "No browser surface in this project.", "findings": [], "not_applicable_reason": "no browser surface — nothing is served or rendered" } ```
Author
Owner
{ "outcome": "skipped", "summary": "Project declares api_invocation: mode: none.", "findings": [], "not_applicable_reason": "project declares api_invocation: { mode: none } — no API; markdown+shell skill suite" }
<!-- qa-report:v1 issue=50 skill=qa domain=api phase=validate --> ```json { "outcome": "skipped", "summary": "Project declares api_invocation: mode: none.", "findings": [], "not_applicable_reason": "project declares api_invocation: { mode: none } — no API; markdown+shell skill suite" } ```
Author
Owner
{ "outcome": "skipped", "summary": "Project declares api_invocation: mode: none.", "findings": [], "not_applicable_reason": "project declares api_invocation: { mode: none } — no API surface to attack; runtime security coverage is the code domain static review" }
<!-- qa-report:v1 issue=50 skill=qa domain=security-api phase=validate --> ```json { "outcome": "skipped", "summary": "Project declares api_invocation: mode: none.", "findings": [], "not_applicable_reason": "project declares api_invocation: { mode: none } — no API surface to attack; runtime security coverage is the code domain static review" } ```
Author
Owner
{
  "outcome": "clean",
  "summary": "Every red-testable finding got a TDD-red gate scenario in scripts/test-plugin-gates.sh (commit 4576252): CR-1 (scenario_25 redaction adjacent-secrets), CR-2 (scenario_26 commit-message population), CR-3 (scenario_27 cutover preflight first-run), CR-8 (scenario_29), CR-11 (scenario_32 — refusal STANDS per AC-10, the test asserts the resume guidance), CR-13 (scenario_31), CR-14 (scenario_30), CR-20 (scenario_24 sub-check class1_sh_quoted_home); CR-10 is closed by a green coverage scenario_28 (bump path). Recorded red state: 8 failed / 7 passed. Non-red-tested with reasoning: cutover behavioral findings CR-4/5/6/7/12/22/23/24/25/32 (live-machine choreography — owned by the UAT Scenario 8 scratch rehearsal lane; scenario_27 covers the entry point), suite-self findings CR-9/16/17/18/19 (fixes to the test harness itself), doc/comment findings CR-15/29/30/31, latent-tool findings CR-26/27 (rewriter retired from rewrite duty; --check remains), CR-21/28 (trivial hardening, verified by re-run + lint). Red-state note: scenario_31 proved the decoy plugins[0] passes BOTH claude gates and release.sh exits 0 against the wrong entry.",
  "findings": [],
  "artifacts": {
    "test_files": ["scripts/test-plugin-gates.sh"],
    "test_commit": "45762523db1a84d291986efcc09c13ed7f433878",
    "test_marker": { "runner": "bespoke bash harness (test-plugin-gates.sh)", "write": "scenario asserts the FIXED behavior and reports FAIL while the defect stands (recorded red run)", "promote": "no marker edit — the fix turns the scenario green; the suite exit code is the gate" }
  }
}
<!-- qa-report:v1 issue=50 skill=qa domain=code phase=tests --> ```json { "outcome": "clean", "summary": "Every red-testable finding got a TDD-red gate scenario in scripts/test-plugin-gates.sh (commit 4576252): CR-1 (scenario_25 redaction adjacent-secrets), CR-2 (scenario_26 commit-message population), CR-3 (scenario_27 cutover preflight first-run), CR-8 (scenario_29), CR-11 (scenario_32 — refusal STANDS per AC-10, the test asserts the resume guidance), CR-13 (scenario_31), CR-14 (scenario_30), CR-20 (scenario_24 sub-check class1_sh_quoted_home); CR-10 is closed by a green coverage scenario_28 (bump path). Recorded red state: 8 failed / 7 passed. Non-red-tested with reasoning: cutover behavioral findings CR-4/5/6/7/12/22/23/24/25/32 (live-machine choreography — owned by the UAT Scenario 8 scratch rehearsal lane; scenario_27 covers the entry point), suite-self findings CR-9/16/17/18/19 (fixes to the test harness itself), doc/comment findings CR-15/29/30/31, latent-tool findings CR-26/27 (rewriter retired from rewrite duty; --check remains), CR-21/28 (trivial hardening, verified by re-run + lint). Red-state note: scenario_31 proved the decoy plugins[0] passes BOTH claude gates and release.sh exits 0 against the wrong entry.", "findings": [], "artifacts": { "test_files": ["scripts/test-plugin-gates.sh"], "test_commit": "45762523db1a84d291986efcc09c13ed7f433878", "test_marker": { "runner": "bespoke bash harness (test-plugin-gates.sh)", "write": "scenario asserts the FIXED behavior and reports FAIL while the defect stands (recorded red run)", "promote": "no marker edit — the fix turns the scenario green; the suite exit code is the gate" } } } ```
Author
Owner
{
  "outcome": "clean",
  "summary": "SUPERSEDES comment 1038 (same content, new baseline SHA): the red commit 4576252 was replaced by e049e3f before any push existed, because it carried credential-shaped fixture literals that the strengthened P0.8 full-history scan itself flags — fixture values are now split string concatenations, runtime-identical, with no matchable span in any committed blob. Red scenarios and recorded red state (8 failed / 7 passed) unchanged from 1038; scenario_33 (CR-38, uppercase assigned-secret — found during the fix round) was added red and its red state verified directly (scan rc=0 pre-fix). Final suite: 16 scenarios.",
  "findings": [],
  "artifacts": {
    "test_files": ["scripts/test-plugin-gates.sh"],
    "test_commit": "e049e3f",
    "test_marker": { "runner": "bespoke bash harness (test-plugin-gates.sh)", "write": "scenario asserts the FIXED behavior and reports FAIL while the defect stands (recorded red run)", "promote": "no marker edit — the fix turns the scenario green; the suite exit code is the gate" }
  }
}
<!-- qa-report:v1 issue=50 skill=qa domain=code phase=tests --> ```json { "outcome": "clean", "summary": "SUPERSEDES comment 1038 (same content, new baseline SHA): the red commit 4576252 was replaced by e049e3f before any push existed, because it carried credential-shaped fixture literals that the strengthened P0.8 full-history scan itself flags — fixture values are now split string concatenations, runtime-identical, with no matchable span in any committed blob. Red scenarios and recorded red state (8 failed / 7 passed) unchanged from 1038; scenario_33 (CR-38, uppercase assigned-secret — found during the fix round) was added red and its red state verified directly (scan rc=0 pre-fix). Final suite: 16 scenarios.", "findings": [], "artifacts": { "test_files": ["scripts/test-plugin-gates.sh"], "test_commit": "e049e3f", "test_marker": { "runner": "bespoke bash harness (test-plugin-gates.sh)", "write": "scenario asserts the FIXED behavior and reports FAIL while the defect stands (recorded red run)", "promote": "no marker edit — the fix turns the scenario green; the suite exit code is the gate" } } } ```
Author
Owner
{
  "outcome": "clean",
  "summary": "All 33 fix-now findings resolved (CR-1..CR-32 + CR-38, found mid-fix) in commit 861e496 on top of tests commit e049e3f. Gate suite 16/16 PASS; lint clean with all three population counts asserted; rewrite --check idempotent. Notable: CR-1 needed a second fix — masking every pattern match still leaked (an adjacent secret matching NO rule cannot be masked by rules), so Class-A samples now carry locations only, never line content; the red test caught the insufficient first fix. CR-38: assigned-secret rule was lowercase-only; now case-insensitive, red-tested (scenario_33). P0.8 re-run with the strengthened scanner (commit+tag objects, case-insensitive generic rule): PASS over the full history at HEAD 861e496 — after replacing the never-pushed red commit whose fixture literals the scan itself flagged. Documented deviations: the tests file also carries the harness-fix findings CR-9/16/17/18 (the findings ARE test code) and RELEASE_INTEGRATION_BRANCH plumbing in three green-path scenarios required by the new refusal 4; no red-test assertion was modified. Accepts (CR-33..CR-37) unchanged; CR-35 acceptance upgraded from assumption to verified fact (zero NUL-bearing text-extension blobs in history).",
  "findings": [],
  "artifacts": { "report_path": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/code-report.md" }
}
<!-- qa-report:v1 issue=50 skill=qa domain=code phase=fix --> ```json { "outcome": "clean", "summary": "All 33 fix-now findings resolved (CR-1..CR-32 + CR-38, found mid-fix) in commit 861e496 on top of tests commit e049e3f. Gate suite 16/16 PASS; lint clean with all three population counts asserted; rewrite --check idempotent. Notable: CR-1 needed a second fix — masking every pattern match still leaked (an adjacent secret matching NO rule cannot be masked by rules), so Class-A samples now carry locations only, never line content; the red test caught the insufficient first fix. CR-38: assigned-secret rule was lowercase-only; now case-insensitive, red-tested (scenario_33). P0.8 re-run with the strengthened scanner (commit+tag objects, case-insensitive generic rule): PASS over the full history at HEAD 861e496 — after replacing the never-pushed red commit whose fixture literals the scan itself flagged. Documented deviations: the tests file also carries the harness-fix findings CR-9/16/17/18 (the findings ARE test code) and RELEASE_INTEGRATION_BRANCH plumbing in three green-path scenarios required by the new refusal 4; no red-test assertion was modified. Accepts (CR-33..CR-37) unchanged; CR-35 acceptance upgraded from assumption to verified fact (zero NUL-bearing text-extension blobs in history).", "findings": [], "artifacts": { "report_path": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/code-report.md" } } ```
Author
Owner

P0.8 re-verification (QA round 1) — supersedes the probe record in comment 1027.

The QA fix round strengthened scripts/history-scan.sh (CR-1: Class-A samples carry locations only, never line content; CR-2: commit and annotated-tag objects join the scan population — messages and idents are pushed too; CR-38: the generic assigned-secret rule now matches case-insensitively), so the recorded PASS no longer described the current scanner and was re-established:

  • Interim result: the strengthened scan came back BLOCK (3 Class A hits) — all three were the QA gate suite's own committed fixture literals (an AWS doc-example key, a glpat- seed, one assigned-secret assignment) introduced by the never-pushed red-tests commit 4576252.
  • Resolution: fixture literals are now split string concatenations (runtime-identical, no matchable span in any committed blob), and the red commit was replaced (e049e3f) before any remote ever saw it — the literal-bearing blobs are unreachable and are not transmitted by a push.
  • Final result: P0.8 PASS at HEAD 861e496 — 242 commits, 1202 blobs scanned, 242 commit objects (messages + idents), 0 annotated tags, 0 push-blocking findings. Class B advisory inventory unchanged in character (internal hostnames/paths — acceptable while the repo is private, per D5).
  • Report: $SCRATCHPAD/p08-rerun-report.md (local; regenerate any time with scripts/history-scan.sh).

The first-push gate therefore still holds as PASS, now against the stronger scanner and the exact history that will be pushed.

**P0.8 re-verification (QA round 1) — supersedes the probe record in comment 1027.** The QA fix round strengthened `scripts/history-scan.sh` (CR-1: Class-A samples carry locations only, never line content; CR-2: commit and annotated-tag objects join the scan population — messages and idents are pushed too; CR-38: the generic `assigned-secret` rule now matches case-insensitively), so the recorded PASS no longer described the current scanner and was re-established: - **Interim result:** the strengthened scan came back **BLOCK (3 Class A hits)** — all three were the QA gate suite's *own committed fixture literals* (an AWS doc-example key, a `glpat-` seed, one assigned-secret assignment) introduced by the never-pushed red-tests commit 4576252. - **Resolution:** fixture literals are now split string concatenations (runtime-identical, no matchable span in any committed blob), and the red commit was replaced (`e049e3f`) before any remote ever saw it — the literal-bearing blobs are unreachable and are not transmitted by a push. - **Final result: P0.8 PASS** at HEAD `861e496` — 242 commits, 1202 blobs scanned, 242 commit objects (messages + idents), 0 annotated tags, 0 push-blocking findings. Class B advisory inventory unchanged in character (internal hostnames/paths — acceptable while the repo is private, per D5). - Report: `$SCRATCHPAD/p08-rerun-report.md` (local; regenerate any time with `scripts/history-scan.sh`). The first-push gate therefore still holds as PASS, now against the stronger scanner and the exact history that will be pushed.
Author
Owner
{
  "outcome": "issues-found",
  "summary": "Round 2 = the sweep adversarial pass (fable Bug Hunter, fresh context) + a fresh-context re-validation over the round-1 fix commits e049e3f+861e496. Re-validator: all 10 round-1 fixes verified OK. Bug Hunter: 24 candidates -> 7 verified (1 High: CR-39 Class A leaks through the Class B sample channel on dual-match lines; CR-40 leftmost-placeholder shielding; CR-41 no stream-completeness guard; CR-42 the CR-4 abort gate re-opens via the has_new branch; CR-43 the CR-25 keep-guard misses failed re-creations; CR-44 refusal docs drift; CR-45 low-severity batch), 12 floored/accepted with reasoning, remainder cleared. Full detail: code-report.md Round 2 section.",
  "findings": [
    { "id": "CR-39", "category": "in-scope-blocking", "severity": "high", "summary": "history-scan withholding was per-rule not per-line: Class A material printed verbatim through the Class B sample channel on dual-match lines" },
    { "id": "CR-40", "category": "in-scope-blocking", "severity": "medium", "summary": "leftmost placeholder assignment shielded a later real secret on the same line" },
    { "id": "CR-41", "category": "in-scope-blocking", "severity": "medium", "summary": "no stream-completeness guard: a dying cat-file --batch could yield PASS over a partial population" },
    { "id": "CR-42", "category": "in-scope-blocking", "severity": "medium", "summary": "abort restored the config backup over an untouched file when step 6 took the has_new branch (CR-4 regression path)" },
    { "id": "CR-43", "category": "in-scope-blocking", "severity": "medium", "summary": "keep-plugin-link guard keyed on DANGLING only; failed farm-link re-creations still removed the only resolving form" },
    { "id": "CR-44", "category": "in-scope-blocking", "severity": "medium", "summary": "README + release.sh header documented three refusals; refusals 0/4 and the tag-at-HEAD resume guidance were missing" },
    { "id": "CR-45", "category": "in-scope-deferrable", "severity": "low", "summary": "batch: tag-object double-count, multi-slash strip, test git-identity pinning, over-broad resum grep, fragile test-var placement, rewriter write-mode-on-typo", "reasoning": "six trivial hardenings, all adjacent to the blocking fixes this round", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" }
  ],
  "artifacts": { "report_path": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/code-report.md" }
}
<!-- qa-report:v1 issue=50 skill=qa domain=code phase=validate --> ```json { "outcome": "issues-found", "summary": "Round 2 = the sweep adversarial pass (fable Bug Hunter, fresh context) + a fresh-context re-validation over the round-1 fix commits e049e3f+861e496. Re-validator: all 10 round-1 fixes verified OK. Bug Hunter: 24 candidates -> 7 verified (1 High: CR-39 Class A leaks through the Class B sample channel on dual-match lines; CR-40 leftmost-placeholder shielding; CR-41 no stream-completeness guard; CR-42 the CR-4 abort gate re-opens via the has_new branch; CR-43 the CR-25 keep-guard misses failed re-creations; CR-44 refusal docs drift; CR-45 low-severity batch), 12 floored/accepted with reasoning, remainder cleared. Full detail: code-report.md Round 2 section.", "findings": [ { "id": "CR-39", "category": "in-scope-blocking", "severity": "high", "summary": "history-scan withholding was per-rule not per-line: Class A material printed verbatim through the Class B sample channel on dual-match lines" }, { "id": "CR-40", "category": "in-scope-blocking", "severity": "medium", "summary": "leftmost placeholder assignment shielded a later real secret on the same line" }, { "id": "CR-41", "category": "in-scope-blocking", "severity": "medium", "summary": "no stream-completeness guard: a dying cat-file --batch could yield PASS over a partial population" }, { "id": "CR-42", "category": "in-scope-blocking", "severity": "medium", "summary": "abort restored the config backup over an untouched file when step 6 took the has_new branch (CR-4 regression path)" }, { "id": "CR-43", "category": "in-scope-blocking", "severity": "medium", "summary": "keep-plugin-link guard keyed on DANGLING only; failed farm-link re-creations still removed the only resolving form" }, { "id": "CR-44", "category": "in-scope-blocking", "severity": "medium", "summary": "README + release.sh header documented three refusals; refusals 0/4 and the tag-at-HEAD resume guidance were missing" }, { "id": "CR-45", "category": "in-scope-deferrable", "severity": "low", "summary": "batch: tag-object double-count, multi-slash strip, test git-identity pinning, over-broad resum grep, fragile test-var placement, rewriter write-mode-on-typo", "reasoning": "six trivial hardenings, all adjacent to the blocking fixes this round", "proposed_action": "fix-now", "fix_cost": "trivial", "adjacent_to_blocking": true, "feature_value": "incidental" } ], "artifacts": { "report_path": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/code-report.md" } } ```
Author
Owner
{
  "outcome": "clean",
  "summary": "Round-2 red fixtures committed as caef69b: scenario_25 now dual-matches Class A + Class B on the secret line (CR-39 red: fragment leaked via B sample), scenario_33 leads with a placeholder assignment (CR-40 red: scan exited 0). Assertions unchanged. CR-41/42/43/44/45 are not red-harnessed with reasoning: stream-kill injection and abort environment-failure simulation exceed the harness lane (owned by the UAT Scenario 8 rehearsal + the loud non-zero exits verified by inspection); CR-44 is docs.",
  "findings": [],
  "artifacts": { "test_files": ["scripts/test-plugin-gates.sh"], "test_commit": "caef69b", "test_marker": { "runner": "bespoke bash harness (test-plugin-gates.sh)", "write": "scenario asserts the FIXED behavior and reports FAIL while the defect stands", "promote": "no marker edit — the fix turns the scenario green" } }
}
<!-- qa-report:v1 issue=50 skill=qa domain=code phase=tests --> ```json { "outcome": "clean", "summary": "Round-2 red fixtures committed as caef69b: scenario_25 now dual-matches Class A + Class B on the secret line (CR-39 red: fragment leaked via B sample), scenario_33 leads with a placeholder assignment (CR-40 red: scan exited 0). Assertions unchanged. CR-41/42/43/44/45 are not red-harnessed with reasoning: stream-kill injection and abort environment-failure simulation exceed the harness lane (owned by the UAT Scenario 8 rehearsal + the loud non-zero exits verified by inspection); CR-44 is docs.", "findings": [], "artifacts": { "test_files": ["scripts/test-plugin-gates.sh"], "test_commit": "caef69b", "test_marker": { "runner": "bespoke bash harness (test-plugin-gates.sh)", "write": "scenario asserts the FIXED behavior and reports FAIL while the defect stands", "promote": "no marker edit — the fix turns the scenario green" } } } ```
Author
Owner
{
  "outcome": "clean",
  "summary": "All 7 round-2 findings fixed in 3d63f58. Direct red->green probes for CR-39 (0 secret fragments in the report, rc=1) and CR-40 (rc=1). Full verification at HEAD 3d63f58: gate suite 16/16 PASS, P0.8 PASS over the complete history with the twice-strengthened scanner, lint clean, rewrite --check idempotent. Floored/accepted round-2 candidates (12) carry reasoning in the report; none security-relevant in direction.",
  "findings": [],
  "artifacts": { "report_path": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/code-report.md" }
}
<!-- qa-report:v1 issue=50 skill=qa domain=code phase=fix --> ```json { "outcome": "clean", "summary": "All 7 round-2 findings fixed in 3d63f58. Direct red->green probes for CR-39 (0 secret fragments in the report, rc=1) and CR-40 (rc=1). Full verification at HEAD 3d63f58: gate suite 16/16 PASS, P0.8 PASS over the complete history with the twice-strengthened scanner, lint clean, rewrite --check idempotent. Floored/accepted round-2 candidates (12) carry reasoning in the report; none security-relevant in direction.", "findings": [], "artifacts": { "report_path": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/code-report.md" } } ```
Author
Owner
{
  "outcome": "issues-found",
  "summary": "Round 3 = adversarial pass (fable, fresh, empirical probes) on the round-2 fixes. 6 candidates -> 3 verified fix-now (CR-46 completeness guard bypassable via truncation inside the last batch header — false PASS reproduced; CR-47 assigned-secret walk guard=32 fails open on 33+ placeholder matches — reproduced; CR-48 a rejected match swallows a nested real assignment — reproduced) + 3 nits folded (quiet corrupt-state abort branch, README tag-at-HEAD fragment, rewriter empty/extra-arg gaps). Cleared with evidence: per-line taint hygiene, guard plumbing, tag dedup, strip_slashes, FARM_RESTORE_FAILED joins, harness fixes.",
  "findings": [
    { "id": "CR-46", "category": "in-scope-blocking", "severity": "medium", "summary": "a stream cut inside the LAST object header still counted toward nobj with remaining=0 — the completeness guard blessed a partial population (false PASS reproduced)" },
    { "id": "CR-47", "category": "in-scope-blocking", "severity": "medium", "summary": "any_real_assigned guard=32 failed OPEN: 33+ rule-matching placeholders on one line shielded a real secret (reproduced)" },
    { "id": "CR-48", "category": "in-scope-blocking", "severity": "low", "summary": "advancing past a rejected match whole-span swallowed a real assignment nested in its value (reproduced)" }
  ],
  "artifacts": { "report_path": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/code-report.md" }
}
<!-- qa-report:v1 issue=50 skill=qa domain=code phase=validate --> ```json { "outcome": "issues-found", "summary": "Round 3 = adversarial pass (fable, fresh, empirical probes) on the round-2 fixes. 6 candidates -> 3 verified fix-now (CR-46 completeness guard bypassable via truncation inside the last batch header — false PASS reproduced; CR-47 assigned-secret walk guard=32 fails open on 33+ placeholder matches — reproduced; CR-48 a rejected match swallows a nested real assignment — reproduced) + 3 nits folded (quiet corrupt-state abort branch, README tag-at-HEAD fragment, rewriter empty/extra-arg gaps). Cleared with evidence: per-line taint hygiene, guard plumbing, tag dedup, strip_slashes, FARM_RESTORE_FAILED joins, harness fixes.", "findings": [ { "id": "CR-46", "category": "in-scope-blocking", "severity": "medium", "summary": "a stream cut inside the LAST object header still counted toward nobj with remaining=0 — the completeness guard blessed a partial population (false PASS reproduced)" }, { "id": "CR-47", "category": "in-scope-blocking", "severity": "medium", "summary": "any_real_assigned guard=32 failed OPEN: 33+ rule-matching placeholders on one line shielded a real secret (reproduced)" }, { "id": "CR-48", "category": "in-scope-blocking", "severity": "low", "summary": "advancing past a rejected match whole-span swallowed a real assignment nested in its value (reproduced)" } ], "artifacts": { "report_path": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/code-report.md" } } ```
Author
Owner
{
  "outcome": "clean",
  "summary": "Round-3 findings are guarded by direct probes rather than new harness scenarios, with reasoning: CR-46 requires stream-kill injection inside the scanner pipeline (exceeds the black-box harness; verified by the finder empirical repro pre-fix and by the healthy-path + missing-response probes post-fix); CR-47/CR-48 verified red pre-fix by the finder and green post-fix by direct scratch-repo probes (33-placeholder line rc=1, nested-assignment line rc=1, placeholder-only line rc=0 — no false positive from the +1 advance). Existing 16-scenario suite unchanged and green.",
  "findings": [],
  "artifacts": { "test_files": ["scripts/test-plugin-gates.sh"], "test_commit": "caef69b" }
}
<!-- qa-report:v1 issue=50 skill=qa domain=code phase=tests --> ```json { "outcome": "clean", "summary": "Round-3 findings are guarded by direct probes rather than new harness scenarios, with reasoning: CR-46 requires stream-kill injection inside the scanner pipeline (exceeds the black-box harness; verified by the finder empirical repro pre-fix and by the healthy-path + missing-response probes post-fix); CR-47/CR-48 verified red pre-fix by the finder and green post-fix by direct scratch-repo probes (33-placeholder line rc=1, nested-assignment line rc=1, placeholder-only line rc=0 — no false positive from the +1 advance). Existing 16-scenario suite unchanged and green.", "findings": [], "artifacts": { "test_files": ["scripts/test-plugin-gates.sh"], "test_commit": "caef69b" } } ```
Author
Owner
{
  "outcome": "clean",
  "summary": "CR-46/47/48 + nits fixed in d976759. Independent fresh-eyes review (sonnet, empirical): all four change groups OK with evidence — header validation does not false-trip and catches the missing-response shape loud (closing the previously floored desync finding as a side effect); any_real_assigned provably terminates, fails closed, no suffix false positive; abort elif chain intact across 6 simulated states; rewriter arg cases all refuse correctly; README fragments match the die strings verbatim. Full verification at HEAD d976759: gates 16/16, P0.8 PASS (full history, thrice-strengthened scanner), lint clean.",
  "findings": [],
  "artifacts": { "report_path": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/code-report.md" }
}
<!-- qa-report:v1 issue=50 skill=qa domain=code phase=fix --> ```json { "outcome": "clean", "summary": "CR-46/47/48 + nits fixed in d976759. Independent fresh-eyes review (sonnet, empirical): all four change groups OK with evidence — header validation does not false-trip and catches the missing-response shape loud (closing the previously floored desync finding as a side effect); any_real_assigned provably terminates, fails closed, no suffix false positive; abort elif chain intact across 6 simulated states; rewriter arg cases all refuse correctly; README fragments match the die strings verbatim. Full verification at HEAD d976759: gates 16/16, P0.8 PASS (full history, thrice-strengthened scanner), lint clean.", "findings": [], "artifacts": { "report_path": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/code-report.md" } } ```
Author
Owner
{
  "next_state": "integrating",
  "produced": [
    {
      "kind": "qa-report",
      "ref": "comments 1032-1037 (round-1 validate: code + 5 declared skips)"
    },
    {
      "kind": "qa-report",
      "ref": "comments 1039/1040 (round-1 tests/fix, superseding 1038)"
    },
    {
      "kind": "qa-report",
      "ref": "comments 1042-1044 (round-2 validate/tests/fix)"
    },
    {
      "kind": "qa-report",
      "ref": "comments 1045-1047 (round-3 validate/tests/fix)"
    },
    {
      "kind": "probe-record",
      "ref": "comment 1041 (P0.8 re-verification: PASS, strengthened scanner, superseding 1027)"
    },
    {
      "kind": "commits",
      "ref": "e049e3f, 861e496, caef69b, 3d63f58, d976759 (QA rounds 1-3 on feature/50-ship-the-suite-as-a-claude-code-marketplace-plugin; sweep HEAD d976759)"
    },
    {
      "kind": "uat-prerequisites",
      "ref": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/test-prerequisites.md (local card; credentials verified: tea jbr870 admin, gh jbr870)"
    }
  ],
  "findings": [
    {
      "id": "F-PO-50-4-1",
      "category": "in-scope-deferrable",
      "severity": "low",
      "summary": "D12's 'dynamic forms (string-built names)' ban has no dedicated lint rule or scenario-24 control (CR-33)",
      "reasoning": "no bounded mechanical signature exists for the class; the unbraced/escaped/unquoted bans cover every observed leak path; a dynamic form yields an empty value at runtime and is caught by D12's baked-path non-empty assertion; ${!var} indirection is non-POSIX and already outside the emitted-glue baseline",
      "requires_product_decision": false,
      "applied_disposition": "accept"
    },
    {
      "id": "F-PO-50-4-2",
      "category": "in-scope-deferrable",
      "severity": "low",
      "summary": "cutover worktree parsing breaks on paths containing spaces (CR-34)",
      "reasoning": "both machines' repo/worktree roots are space-free by layout convention; the cutover tooling is repo-local and retires after Phase 6/7",
      "requires_product_decision": false,
      "applied_disposition": "accept"
    },
    {
      "id": "F-PO-50-4-3",
      "category": "in-scope-deferrable",
      "severity": "low",
      "summary": "history-scan framing is not NUL-safe for NUL-bearing text-extension blobs (CR-35)",
      "reasoning": "verified fact this round: zero NUL-bearing text-extension blobs exist anywhere in this repo's history; the limitation is disclosed in the report's Limitations section",
      "requires_product_decision": false,
      "applied_disposition": "accept"
    },
    {
      "id": "F-PO-50-4-4",
      "category": "in-scope-deferrable",
      "severity": "low",
      "summary": "plugin link dangles between step 3 and the merge when the main checkout itself sits on the feature branch (CR-36)",
      "reasoning": "layout not used: the main checkout stays on main and the plugin source resolves to the feature worktree; the step-3 STOP verifies before merge",
      "requires_product_decision": false,
      "applied_disposition": "accept"
    },
    {
      "id": "F-PO-50-4-5",
      "category": "in-scope-deferrable",
      "severity": "low",
      "summary": "do_backup re-runs overwrite integration_sha_at_backup and the slug fields (CR-37)",
      "reasoning": "audit nit only; abort keys off pre_merge_integration_sha, which is write-once",
      "requires_product_decision": false,
      "applied_disposition": "accept"
    },
    {
      "id": "F-PO-50-4-6",
      "category": "in-scope-deferrable",
      "severity": "low",
      "summary": "residual low-severity floor set from the round-2/3 adversarial passes (unreachable missing-sha desync — since made loud by CR-46; safe-direction diagnostics on detached HEAD and malformed marketplace JSON; quote-switch class-1 spellings; escaped-quote parity in the retired rewriter's quote pass; class3b non-isolation in scenario 24)",
      "reasoning": "each is low severity, low confidence-of-occurrence, or fails in the safe/loud direction; itemized with evidence in code-report.md rounds 2-3 filter sections",
      "requires_product_decision": false,
      "applied_disposition": "accept"
    }
  ],
  "pending_decisions": []
}
<!-- phase-outcome:v1 id=PO-50-4 skill=qa --> ```json { "next_state": "integrating", "produced": [ { "kind": "qa-report", "ref": "comments 1032-1037 (round-1 validate: code + 5 declared skips)" }, { "kind": "qa-report", "ref": "comments 1039/1040 (round-1 tests/fix, superseding 1038)" }, { "kind": "qa-report", "ref": "comments 1042-1044 (round-2 validate/tests/fix)" }, { "kind": "qa-report", "ref": "comments 1045-1047 (round-3 validate/tests/fix)" }, { "kind": "probe-record", "ref": "comment 1041 (P0.8 re-verification: PASS, strengthened scanner, superseding 1027)" }, { "kind": "commits", "ref": "e049e3f, 861e496, caef69b, 3d63f58, d976759 (QA rounds 1-3 on feature/50-ship-the-suite-as-a-claude-code-marketplace-plugin; sweep HEAD d976759)" }, { "kind": "uat-prerequisites", "ref": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/test-prerequisites.md (local card; credentials verified: tea jbr870 admin, gh jbr870)" } ], "findings": [ { "id": "F-PO-50-4-1", "category": "in-scope-deferrable", "severity": "low", "summary": "D12's 'dynamic forms (string-built names)' ban has no dedicated lint rule or scenario-24 control (CR-33)", "reasoning": "no bounded mechanical signature exists for the class; the unbraced/escaped/unquoted bans cover every observed leak path; a dynamic form yields an empty value at runtime and is caught by D12's baked-path non-empty assertion; ${!var} indirection is non-POSIX and already outside the emitted-glue baseline", "requires_product_decision": false, "applied_disposition": "accept" }, { "id": "F-PO-50-4-2", "category": "in-scope-deferrable", "severity": "low", "summary": "cutover worktree parsing breaks on paths containing spaces (CR-34)", "reasoning": "both machines' repo/worktree roots are space-free by layout convention; the cutover tooling is repo-local and retires after Phase 6/7", "requires_product_decision": false, "applied_disposition": "accept" }, { "id": "F-PO-50-4-3", "category": "in-scope-deferrable", "severity": "low", "summary": "history-scan framing is not NUL-safe for NUL-bearing text-extension blobs (CR-35)", "reasoning": "verified fact this round: zero NUL-bearing text-extension blobs exist anywhere in this repo's history; the limitation is disclosed in the report's Limitations section", "requires_product_decision": false, "applied_disposition": "accept" }, { "id": "F-PO-50-4-4", "category": "in-scope-deferrable", "severity": "low", "summary": "plugin link dangles between step 3 and the merge when the main checkout itself sits on the feature branch (CR-36)", "reasoning": "layout not used: the main checkout stays on main and the plugin source resolves to the feature worktree; the step-3 STOP verifies before merge", "requires_product_decision": false, "applied_disposition": "accept" }, { "id": "F-PO-50-4-5", "category": "in-scope-deferrable", "severity": "low", "summary": "do_backup re-runs overwrite integration_sha_at_backup and the slug fields (CR-37)", "reasoning": "audit nit only; abort keys off pre_merge_integration_sha, which is write-once", "requires_product_decision": false, "applied_disposition": "accept" }, { "id": "F-PO-50-4-6", "category": "in-scope-deferrable", "severity": "low", "summary": "residual low-severity floor set from the round-2/3 adversarial passes (unreachable missing-sha desync — since made loud by CR-46; safe-direction diagnostics on detached HEAD and malformed marketplace JSON; quote-switch class-1 spellings; escaped-quote parity in the retired rewriter's quote pass; class3b non-isolation in scenario 24)", "reasoning": "each is low severity, low confidence-of-occurrence, or fails in the safe/loud direction; itemized with evidence in code-report.md rounds 2-3 filter sections", "requires_product_decision": false, "applied_disposition": "accept" } ], "pending_decisions": [] } ```
Author
Owner
{
  "next_state": "uat",
  "produced": [
    {
      "kind": "uat-prerequisites",
      "ref": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/test-prerequisites.md (worktree ~/.claude/repos/devwork-skills-50, HEAD d976759; also summarized on PO-50-4 / comment 1048)",
      "summary": "UAT card: 17 human-uat scenarios (phases 4-7), credentials verified (tea jbr870 admin, gh jbr870), ordering constraints, scenario-specific test data"
    },
    {
      "kind": "verification",
      "ref": "integration test at d976759, 2026-08-16",
      "summary": "rebase drift-catch no-op (merge-base = main tip = 1e6bd78); lint-conventions clean (114 helper scripts, 689 fence lines, 199 shipped files); gate suite 16/16; rewrite-plugin-refs --check idempotent; tree clean"
    }
  ],
  "findings": [],
  "pending_decisions": [
    {
      "id": "D-PO-50-5-1",
      "type": "approval",
      "blocking": true,
      "question": "UAT: does the plugin-form suite work end-to-end (coexistence, cutover, release, update), and is it what we wanted?",
      "options": [
        "approve",
        "reject"
      ],
      "recommended": "approve",
      "reasoning": "REDUCED integrate run — hand-off only, by design. This feature replaces the skill-farm deployment mechanism itself: the 15 links in ~/.claude/skills/ point into the main checkout, so a vanilla fast-forward merge to main would mutate the live suite mid-session. The merge is therefore step 4 of scripts/cutover-dev.sh (a STOP step), run under the plugin form during UAT Phase 5 — this run deliberately performs NO merge, NO branch/worktree cleanup, and records NO code-bundle (the bundle records what landed; the acceptance run posts it after the cutover merge). The feature-branch push was also skipped: the standing no-push constraint holds until the P0.8-gated first push (Phase 6; scan currently green, comment 1041). What DID run: rebase drift-catch (no-op — merge-base = main tip = 1e6bd78), full integration test at d976759 (lint clean, 16/16 gate scenarios, rewriter idempotent). UAT surface: the plugin loaded from the worktree source ~/.claude/repos/devwork-skills-50 (coexistence Scenarios 3-7 BEFORE any cutover, farm intact), then the cutover/release/update phases per the UAT card. Keep the worktree until the merge STOP retires it. approve = a second /dev:integrate run posts accepted (queued for /dev:promote) and records the code-bundle from the by-then-merged main. reject = record findings; the UAT return path (qa-playbook.md section 9) classifies how far back to re-enter; the feature branch still exists as the rework base."
    }
  ]
}
<!-- phase-outcome:v1 id=PO-50-5 skill=integrate --> ```json { "next_state": "uat", "produced": [ { "kind": "uat-prerequisites", "ref": ".devwork/feature-ship-the-suite-as-a-claude-code-marketplace-plugin/test-prerequisites.md (worktree ~/.claude/repos/devwork-skills-50, HEAD d976759; also summarized on PO-50-4 / comment 1048)", "summary": "UAT card: 17 human-uat scenarios (phases 4-7), credentials verified (tea jbr870 admin, gh jbr870), ordering constraints, scenario-specific test data" }, { "kind": "verification", "ref": "integration test at d976759, 2026-08-16", "summary": "rebase drift-catch no-op (merge-base = main tip = 1e6bd78); lint-conventions clean (114 helper scripts, 689 fence lines, 199 shipped files); gate suite 16/16; rewrite-plugin-refs --check idempotent; tree clean" } ], "findings": [], "pending_decisions": [ { "id": "D-PO-50-5-1", "type": "approval", "blocking": true, "question": "UAT: does the plugin-form suite work end-to-end (coexistence, cutover, release, update), and is it what we wanted?", "options": [ "approve", "reject" ], "recommended": "approve", "reasoning": "REDUCED integrate run — hand-off only, by design. This feature replaces the skill-farm deployment mechanism itself: the 15 links in ~/.claude/skills/ point into the main checkout, so a vanilla fast-forward merge to main would mutate the live suite mid-session. The merge is therefore step 4 of scripts/cutover-dev.sh (a STOP step), run under the plugin form during UAT Phase 5 — this run deliberately performs NO merge, NO branch/worktree cleanup, and records NO code-bundle (the bundle records what landed; the acceptance run posts it after the cutover merge). The feature-branch push was also skipped: the standing no-push constraint holds until the P0.8-gated first push (Phase 6; scan currently green, comment 1041). What DID run: rebase drift-catch (no-op — merge-base = main tip = 1e6bd78), full integration test at d976759 (lint clean, 16/16 gate scenarios, rewriter idempotent). UAT surface: the plugin loaded from the worktree source ~/.claude/repos/devwork-skills-50 (coexistence Scenarios 3-7 BEFORE any cutover, farm intact), then the cutover/release/update phases per the UAT card. Keep the worktree until the merge STOP retires it. approve = a second /dev:integrate run posts accepted (queued for /dev:promote) and records the code-bundle from the by-then-merged main. reject = record findings; the UAT return path (qa-playbook.md section 9) classifies how far back to re-enter; the feature branch still exists as the rework base." } ] } ```
Author
Owner

UAT report — plugin migration (#50)

Executed across both machines, 2026-08-13 → 2026-08-22. Full per-scenario evidence with commands and
output: .devwork/feature-…/uat-log.md (gitignored, dev machine). This is the summary the decision
rests on.

What was proved

Phase Scenarios Result
4 — coexistence 3–7 PASS — plugin form and farm coexisted; /dev: and bare names both resolved in the pre-cutover window
5 — dev cutover 8–11 PASS — rehearsed, then executed; 7/7 steps; historical records unchanged
6 — release + desktop 12, 13, 14, 15, 22 PASS — 0.1.0 cut and pushed (first ever push to GitHub, P0.8-gated); desktop installed from the marketplace and ran a real forge-mutating phase
7 — update, purity, fallback 16, 17, 21 16 PASS; 17 PASS on purity, FAIL on attribution; 21 FAIL

Two releases exist: dev--v0.1.0ee15687, dev--v0.1.18c9a94e.

The core claim holds. A consumer machine with no clone, no symlink farm and no local copy of the
suite installs the plugin from a private GitHub marketplace, resolves /dev: skills, and runs
helper-invoking phases that write to a live forge — proved end to end on verity (glab-cli), where
/dev:requirements created issue #239, posted a valid PO-239-1, and advanced its phase label. The
update ritual then moved that machine 0.1.0 → 0.1.1 and a subsequent run resolved every plugin file
through the newer tree.

Findings: 34 recorded, 21 fixed

  • Fixed in 0.1.1 (README + tooling): F13–F32 less the non-actionable ones — the whole Scenario 13
    defect set, the two Scenario 22 findings, the headless-consumer gap (F28), and the release script's
    remote mismatch (F13).
  • Fixed earlier / operator-directed: F9.
  • No action, recorded with reasoning: F30 (upstream CLI message quality), F10, F11 (test-plan
    wording, not product).
  • Pre-existing, overlap other issues: F1 (cf. #35), F2, F3, F4 (cf. #16 — re-checked at HEAD: now 6 tracked files under .devwork/, not 4; .gitignore lists the directory but never untracked them), F5, F12 — exposed by
    this work, not caused by it. These want deduping against their siblings before separate filing.
  • Cutover-script findings, still open: F6, F7, F8 — all on the abort path of a one-shot script
    that has now served its purpose. Worth a decision on whether they are worth fixing at all.

The two that bear on acceptance

UAT-F34 — AC-7's documented recovery path does not work. "The desktop's previous release remains
installable from the remote as its recovery path." Run verbatim: fetch --unshallow --tags and
checkout dev--v0.1.0 both succeed, then claude plugin update reports "already at the latest
version (0.1.1)"
. Measured cause: the snapshot sat at ee15687/0.1.0 immediately before the
command and 8081969/0.1.1 immediately after — plugin update refreshes the marketplace itself,
undoing the pin before it compares.
uninstall + install behaves identically. The README already
anticipated this and names the working alternative (re-cut the previous content as a new release,
consumer takes the normal update ritual), which needs nothing but the remote. That alternative is
currently untestable: 0.1.0 and 0.1.1 ship byte-identical content, so a re-cut could not be
distinguished from a no-op.

UAT-F33 — AC-12's attribution half is unimplemented. "Desktop run records are attributable to the
installed release version." PO-239-1, the one real consumer-machine record, carries no version
anywhere; nothing in phase-outcome.md's schema or phase-outcome-post.sh stamps one. Attribution
today depends on the operator hand-pasting the version per the README's consumer-runbook step 8 — and
in the one real run, it was not done. Records either side of an update are not distinguishable. The
fix (stamp the version from ${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json into the outcome
header) is a helper behaviour change, so it is full-pipeline work, not a meta-lane amendment.

Neither is a defect in the plugin form itself; both are gaps in acceptance criteria this feature
claimed. They are the reason this report does not simply say "approve".

Recommendation

Approve the migration and split the two gaps out as siblings — the deployment-mechanism change is
done, proved, and already in daily use on both machines, and holding it open behind a helper change
(F33) and an upstream CLI behaviour (F34) would keep a finished thing unfinished. The alternative,
rejecting to fold F33 into this feature, is defensible if AC-12 is considered load-bearing rather
than aspirational; it costs a full pipeline pass on the helper tier.

One cross-finding worth carrying to the siblings

F1 and #35 are the same trap from opposite ends. #35's fix direction is to replace
~/.claude/skills/… in domain docs with the plugin-root variable — but F1 is precisely that the
plugin-root variable is not expanded when a subagent reads a procedure file off disk (it is
expanded only in the injected SKILL.md). Applying #35's fix naively would swap one unresolvable path
for another and look correct in review. Whichever is picked up first should be told about the other.


Addendum (2026-08-22) — the cold re-walk, a retracted verdict, and where F33/F34 sit

Releases now: 0.1.0, 0.1.1, 0.1.2. The desktop runs 0.1.2.

Scenario 13 re-walked cold, and still fails

A fresh-context executor, given only the 0.1.1 README and no knowledge of the cutover or the earlier
walk, found 4 blocking and 11 non-blocking defects. All are fixed in 0.1.2. AC-11 demands zero,
so Scenario 13 does not pass yet; a second cold re-walk against 0.1.2 is running.

The worst finding retracts a verdict I had already recorded. The consumer-guidance sweep used
~/Coding/*/CLAUDE.md ~/Coding/*/*/CLAUDE.md — two levels deep, no dot-directories. On the desktop
that glob sees 4 of 34 CLAUDE.md files. Re-run with find: 67 stale bare invocations and 6
dangling ~/.claude/skills/glab-cli/ paths
, where the glob reported zero.

My Scenario 17 result ("all consumer projects are /dev:-only — PASS") was produced with that same
glob
, so it inherited the defect it was checking for. That PASS is withdrawn. Six live
CLAUDE.md files in verity's slot worktrees were still instructing agents to invoke bare names that
no longer resolve and to read an adapter path that no longer exists — a cutover recorded as complete
that was not. Verifying a fix with the instrument that produced the defect proves nothing.

Now actually fixed (operator-approved, slots idle): all six rewritten to /dev: with the adapter
described as bundled and no path named; pre-existing uncommitted work in slot1 preserved and
verified; backups in /tmp/slot-claude-backup/; left uncommitted for the repo owner. The machine-wide
sweep is now 0 files with bare invocations, 0 with farm paths. Note the durable fix is committing
the corrected guidance on verity's own branches — the slot worktrees are detached-HEAD checkouts of
three older commits, so their working-tree fixes do not survive a re-pin.

A methodological finding, recorded against my own testing

The re-walk filed one false finding: that the "SSH first, then HTTPS" ordering was contradicted
by the marketplace clone's reflog. That reflog was residue from my own Scenario 21 and 22 tests,
which had forced an HTTPS re-clone hours earlier; the pre-test evidence shows the original clone was
SSH. The executor reasoned correctly from evidence the tester had polluted, and had no way to see it.
A UAT that mutates a machine contaminates every later cold read of that machine, invisibly. The
second re-walk brief now says so explicitly.

F33 and F34 — folded into this issue, not split out

Per operator decision, both stay in scope here rather than becoming siblings:

  • F34 (AC-7) is closed by documentation, and that documentation is shipped in 0.1.2. The
    tag-pin recovery cannot be made to work — plugin update refreshes the marketplace before it
    compares, and uninstall+install does the same, so no ordering of CLI commands preserves the
    pin. The README now leads with the route that does work (re-cut the old content as a new release),
    explains why the obvious one fails, and shows how to discover which releases exist. AC-7's
    substance — the previous release remains installable from the remote, needing nothing but the
    remote
    — holds via that route.
  • F33 (AC-12) is not closed and is real remaining work: nothing stamps the release version
    into a phase outcome, so records either side of an update are indistinguishable. 0.1.2 makes the
    README honest about it (it now says plainly that nothing stamps this for you, and treats the
    operator paste as a stopgap), but honesty is not implementation. The fix belongs in
    phase-outcome-post.sh and is a helper behaviour change — full pipeline per this repo's own
    conventions.

Consequence for the gate. With F33 folded in, this feature has implementation work outstanding,
so D-PO-50-5-1 cannot honestly be answered "approve" today. The deployment-mechanism change itself
is done, proved on two machines, and in daily use; what remains is one helper change and one more
cold re-walk.

## UAT report — plugin migration (#50) Executed across both machines, 2026-08-13 → 2026-08-22. Full per-scenario evidence with commands and output: `.devwork/feature-…/uat-log.md` (gitignored, dev machine). This is the summary the decision rests on. ### What was proved | Phase | Scenarios | Result | |---|---|---| | 4 — coexistence | 3–7 | PASS — plugin form and farm coexisted; `/dev:` and bare names both resolved in the pre-cutover window | | 5 — dev cutover | 8–11 | PASS — rehearsed, then executed; 7/7 steps; historical records unchanged | | 6 — release + desktop | 12, 13, 14, 15, 22 | PASS — `0.1.0` cut and pushed (first ever push to GitHub, P0.8-gated); desktop installed from the marketplace and ran a real forge-mutating phase | | 7 — update, purity, fallback | 16, 17, 21 | 16 PASS; 17 PASS on purity, FAIL on attribution; **21 FAIL** | Two releases exist: `dev--v0.1.0` → `ee15687`, `dev--v0.1.1` → `8c9a94e`. **The core claim holds.** A consumer machine with no clone, no symlink farm and no local copy of the suite installs the plugin from a private GitHub marketplace, resolves `/dev:` skills, and runs helper-invoking phases that write to a live forge — proved end to end on verity (glab-cli), where `/dev:requirements` created issue #239, posted a valid `PO-239-1`, and advanced its phase label. The update ritual then moved that machine 0.1.0 → 0.1.1 and a subsequent run resolved every plugin file through the newer tree. ### Findings: 34 recorded, 21 fixed - **Fixed in 0.1.1** (README + tooling): F13–F32 less the non-actionable ones — the whole Scenario 13 defect set, the two Scenario 22 findings, the headless-consumer gap (F28), and the release script's remote mismatch (F13). - **Fixed earlier / operator-directed:** F9. - **No action, recorded with reasoning:** F30 (upstream CLI message quality), F10, F11 (test-plan wording, not product). - **Pre-existing, overlap other issues:** F1 (cf. #35), F2, F3, F4 (cf. #16 — re-checked at HEAD: now **6** tracked files under `.devwork/`, not 4; `.gitignore` lists the directory but never untracked them), F5, F12 — exposed by this work, not caused by it. These want deduping against their siblings before separate filing. - **Cutover-script findings, still open:** F6, F7, F8 — all on the *abort* path of a one-shot script that has now served its purpose. Worth a decision on whether they are worth fixing at all. ### The two that bear on acceptance **UAT-F34 — AC-7's documented recovery path does not work.** "The desktop's previous release remains installable from the remote as its recovery path." Run verbatim: `fetch --unshallow --tags` and `checkout dev--v0.1.0` both succeed, then `claude plugin update` reports *"already at the latest version (0.1.1)"*. Measured cause: the snapshot sat at `ee15687`/`0.1.0` immediately before the command and `8081969`/`0.1.1` immediately after — **`plugin update` refreshes the marketplace itself, undoing the pin before it compares.** `uninstall` + `install` behaves identically. The README already anticipated this and names the working alternative (re-cut the previous content as a new release, consumer takes the normal update ritual), which needs nothing but the remote. That alternative is **currently untestable**: 0.1.0 and 0.1.1 ship byte-identical content, so a re-cut could not be distinguished from a no-op. **UAT-F33 — AC-12's attribution half is unimplemented.** "Desktop run records are attributable to the installed release version." `PO-239-1`, the one real consumer-machine record, carries no version anywhere; nothing in `phase-outcome.md`'s schema or `phase-outcome-post.sh` stamps one. Attribution today depends on the operator hand-pasting the version per the README's consumer-runbook step 8 — and in the one real run, it was not done. Records either side of an update are not distinguishable. The fix (stamp the version from `${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json` into the outcome header) is a helper **behaviour** change, so it is full-pipeline work, not a meta-lane amendment. Neither is a defect in the plugin form itself; both are gaps in acceptance criteria this feature claimed. They are the reason this report does not simply say "approve". ### Recommendation Approve the migration and split the two gaps out as siblings — the deployment-mechanism change is done, proved, and already in daily use on both machines, and holding it open behind a helper change (F33) and an upstream CLI behaviour (F34) would keep a finished thing unfinished. The alternative, rejecting to fold F33 into this feature, is defensible if AC-12 is considered load-bearing rather than aspirational; it costs a full pipeline pass on the helper tier. ### One cross-finding worth carrying to the siblings **F1 and #35 are the same trap from opposite ends.** #35's fix direction is to replace `~/.claude/skills/…` in domain docs with the plugin-root variable — but F1 is precisely that the plugin-root variable **is not expanded when a subagent reads a procedure file off disk** (it is expanded only in the injected SKILL.md). Applying #35's fix naively would swap one unresolvable path for another and look correct in review. Whichever is picked up first should be told about the other. --- ## Addendum (2026-08-22) — the cold re-walk, a retracted verdict, and where F33/F34 sit **Releases now: `0.1.0`, `0.1.1`, `0.1.2`.** The desktop runs `0.1.2`. ### Scenario 13 re-walked cold, and still fails A fresh-context executor, given only the 0.1.1 README and no knowledge of the cutover or the earlier walk, found **4 blocking and 11 non-blocking defects**. All are fixed in `0.1.2`. AC-11 demands zero, so Scenario 13 does not pass yet; a second cold re-walk against `0.1.2` is running. **The worst finding retracts a verdict I had already recorded.** The consumer-guidance sweep used `~/Coding/*/CLAUDE.md ~/Coding/*/*/CLAUDE.md` — two levels deep, no dot-directories. On the desktop that glob sees **4 of 34** `CLAUDE.md` files. Re-run with `find`: **67 stale bare invocations and 6 dangling `~/.claude/skills/glab-cli/` paths**, where the glob reported zero. My Scenario 17 result ("all consumer projects are `/dev:`-only — PASS") was produced with *that same glob*, so it inherited the defect it was checking for. **That PASS is withdrawn.** Six live `CLAUDE.md` files in verity's slot worktrees were still instructing agents to invoke bare names that no longer resolve and to read an adapter path that no longer exists — a cutover recorded as complete that was not. Verifying a fix with the instrument that produced the defect proves nothing. **Now actually fixed** (operator-approved, slots idle): all six rewritten to `/dev:` with the adapter described as bundled and no path named; pre-existing uncommitted work in slot1 preserved and verified; backups in `/tmp/slot-claude-backup/`; left uncommitted for the repo owner. The machine-wide sweep is now **0 files with bare invocations, 0 with farm paths**. Note the durable fix is committing the corrected guidance on verity's own branches — the slot worktrees are detached-HEAD checkouts of three older commits, so their working-tree fixes do not survive a re-pin. ### A methodological finding, recorded against my own testing The re-walk filed one **false** finding: that the "SSH first, then HTTPS" ordering was contradicted by the marketplace clone's reflog. That reflog was **residue from my own Scenario 21 and 22 tests**, which had forced an HTTPS re-clone hours earlier; the pre-test evidence shows the original clone was SSH. The executor reasoned correctly from evidence the tester had polluted, and had no way to see it. **A UAT that mutates a machine contaminates every later cold read of that machine, invisibly.** The second re-walk brief now says so explicitly. ### F33 and F34 — folded into this issue, not split out Per operator decision, both stay in scope here rather than becoming siblings: - **F34 (AC-7)** is closed by documentation, and that documentation is shipped in `0.1.2`. The tag-pin recovery cannot be made to work — `plugin update` refreshes the marketplace before it compares, and `uninstall`+`install` does the same, so no ordering of CLI commands preserves the pin. The README now leads with the route that does work (re-cut the old content as a new release), explains why the obvious one fails, and shows how to discover which releases exist. AC-7's substance — *the previous release remains installable from the remote, needing nothing but the remote* — holds via that route. - **F33 (AC-12)** is **not** closed and is real remaining work: nothing stamps the release version into a phase outcome, so records either side of an update are indistinguishable. `0.1.2` makes the README honest about it (it now says plainly that nothing stamps this for you, and treats the operator paste as a stopgap), but honesty is not implementation. The fix belongs in `phase-outcome-post.sh` and is a helper **behaviour** change — full pipeline per this repo's own conventions. **Consequence for the gate.** With F33 folded in, this feature has implementation work outstanding, so `D-PO-50-5-1` cannot honestly be answered "approve" today. The deployment-mechanism change itself is done, proved on two machines, and in daily use; what remains is one helper change and one more cold re-walk.
Author
Owner

AC-11's bar, restated — operator decision (2026-08-22)

Three cold re-walks have now been run against successive releases, each by a fresh executor with no
knowledge of the cutover, the previous walks, or their findings.

Round Against Blocking Non-blocking
1 0.1.1 4 11
2 0.1.2 4 10
3 0.1.3 2 12

Every finding was fixed; releases 0.1.2, 0.1.3 and 0.1.4 carry them.

AC-11 as written — zero log entries needing outside knowledge — is not converging. Blocking
findings are drying up; non-blocking are not. And a consistent share of each round's findings are
defects introduced by the previous round's fixes (a deleted section heading, orphaned edit residue,
and one piece of advice that would have silently broken nine unrelated user-level skills). Pursuing
zero-of-everything with the same editor is a treadmill rather than a gate.

Restated bar, by operator decision: Scenario 13 passes when a cold re-walk returns zero BLOCKING
findings.
Blocking = the reader would be stopped or misdirected, which is what "needed outside
knowledge" was always about. Non-blocking findings are recorded and triaged as ordinary tracker
items; nobody gets stuck on imprecise wording.

Recorded as a deliberate narrowing of the criterion, with the evidence above, rather than a quiet
reinterpretation. Round 4 against 0.1.4 decides Scenario 13 under this bar.

## AC-11's bar, restated — operator decision (2026-08-22) Three cold re-walks have now been run against successive releases, each by a fresh executor with no knowledge of the cutover, the previous walks, or their findings. | Round | Against | Blocking | Non-blocking | |---|---|---|---| | 1 | 0.1.1 | 4 | 11 | | 2 | 0.1.2 | 4 | 10 | | 3 | 0.1.3 | 2 | 12 | Every finding was fixed; releases `0.1.2`, `0.1.3` and `0.1.4` carry them. **AC-11 as written — zero log entries needing outside knowledge — is not converging.** Blocking findings are drying up; non-blocking are not. And a consistent share of each round's findings are defects introduced by the *previous* round's fixes (a deleted section heading, orphaned edit residue, and one piece of advice that would have silently broken nine unrelated user-level skills). Pursuing zero-of-everything with the same editor is a treadmill rather than a gate. **Restated bar, by operator decision: Scenario 13 passes when a cold re-walk returns zero BLOCKING findings.** Blocking = the reader would be stopped or misdirected, which is what "needed outside knowledge" was always about. Non-blocking findings are recorded and triaged as ordinary tracker items; nobody gets stuck on imprecise wording. Recorded as a deliberate narrowing of the criterion, with the evidence above, rather than a quiet reinterpretation. Round 4 against `0.1.4` decides Scenario 13 under this bar.
Author
Owner

UAT note — issue #50, Scenario 13 closure. Not a Phase Outcome. The original first line
carried a phase-outcome:v1 HTML header, which made scan_comments absorb this comment into
the #50 Phase Outcome ledger: phase-outcome-read-all.sh hard-failed on it and the next PO
would have been minted at the wrong ordinal. Header removed 2026-08-22; body unchanged below.

Scenario 13 (README sufficiency) — CLOSED, and the bar restated

Released 0.1.5 (dev--v0.1.5, release commit 5a64031), pushed to github; consumer machine
updated and verified on 0.1.5 / user / enabled. Gates at release: lint clean, README structure
audit clean (its first run as release gate 2/4), plugin validate --strict both roots, tag dry-run,
and P0.8 full-history scan PASS with no push-blocking findings.

What this scenario is closed against

Not "zero findings from adversarial cold readers." That bar was adopted after round 3 and it is
unachievable by construction: it is an absolute claim about natural language, judged by fresh readers
explicitly instructed to hunt for gaps. Three such readers will find something in any prose document.
Recommending that bar was a mistake; it produced a treadmill, not a decision procedure.

Closed instead against a standard that can actually be met and checked:

  1. The documented consumer path was executed end-to-end on a real machine and worked — the
    desktop was migrated by following it, farm removed, plugin resolving, no bare names.
  2. No known blocking-class defect remains. Rounds 4–6 surfaced 15 distinct blocking findings
    across 8 independent cold readers; all are closed at ecc30b1.
  3. Mechanical gates now enforce what prose discipline did not — structure audit at release time,
    plus (this round) an internal-anchor check and a per-fence bash -n pass with placeholders
    substituted.

Why we stopped, with the measurement

Round README lines after fix Readers Distinct blocking Found by all readers
1–3 599 → 601 1 each small, cheap n/a
4 1038 2 5 1
5 1223 3 4 0
6 1385 3 6 2

The process was generating its own findings. Each round I fixed by adding text: +437, +185,
+162 lines. More text is more audit surface, and round 6's blocking count went up. Rounds 1–3 added
two lines total and found little — that correlation is the explanation, and I should have acted on it
at round 5 rather than reporting each round as an independent measurement.

My fix rate was roughly one new blocking defect per defect closed. Two of round 6's three
unanimous blocking findings were the round-5 fixes written for round 5's blocking findings. A loop
in which the fixer is the principal defect source does not terminate on its own.

What the six rounds were actually worth

Genuinely valuable (would have bitten a real operator):

  • backup filenames derived from basename collided — seven paths here share api, so each backup
    destroyed the last, of exactly the untracked files the step exists to protect
  • rm -rf guarded by git status --short, which is blind to ignored files, i.e. .devwork/
  • the back-up-first caution sat 68 lines after the edit instruction it protects
  • plugin update silently updating nothing at project scope (-s defaults to user; enable
    auto-detects, update does not)
  • enabled: false diagnosed with no cure given
  • two successive completion tests that reported "done" having examined nothing

The remainder is polish on a document whose realistic readership is the operator, on two machines
that are already migrated.

Known and accepted

  • At 1385 lines the document is arguably worse as a document than at 600 — its job is to be read
    end to end. Trimming is the honest fix and is deliberately not done now: it is more work in the
    same direction. Revisit only if it causes friction in practice.
  • Findings rated non-blocking by all readers in round 6 are accepted as-is, not fixed.
  • A real remnant was found on the operator's machine by the new per-project sweep:
    ~/Coding/castellan/.claude/settings.json still grants dead farm paths
    (Bash(/home/jochem/.claude/skills/_shared/...)). Left for the operator — another project's
    settings are not this ticket's to edit.

Still open on #50

F33 / AC-12 — nothing stamps the plugin version into a phase outcome. Helper behaviour change
(phase-outcome-post.sh), so it takes the full pipeline, not the meta lane. D-PO-50-5-1 cannot
honestly resolve to "approve" until that is settled or explicitly deferred.

**UAT note — issue #50, Scenario 13 closure.** Not a Phase Outcome. The original first line carried a `phase-outcome:v1` HTML header, which made `scan_comments` absorb this comment into the #50 Phase Outcome ledger: `phase-outcome-read-all.sh` hard-failed on it and the next PO would have been minted at the wrong ordinal. Header removed 2026-08-22; body unchanged below. ## Scenario 13 (README sufficiency) — CLOSED, and the bar restated **Released 0.1.5** (`dev--v0.1.5`, release commit `5a64031`), pushed to `github`; consumer machine updated and verified on `0.1.5 / user / enabled`. Gates at release: lint clean, README structure audit clean (its first run as release gate 2/4), `plugin validate --strict` both roots, tag dry-run, and P0.8 full-history scan PASS with no push-blocking findings. ### What this scenario is closed against **Not** "zero findings from adversarial cold readers." That bar was adopted after round 3 and it is unachievable by construction: it is an absolute claim about natural language, judged by fresh readers explicitly instructed to hunt for gaps. Three such readers will find something in any prose document. Recommending that bar was a mistake; it produced a treadmill, not a decision procedure. Closed instead against a standard that can actually be met and checked: 1. **The documented consumer path was executed end-to-end on a real machine and worked** — the desktop was migrated by following it, farm removed, plugin resolving, no bare names. 2. **No known blocking-class defect remains.** Rounds 4–6 surfaced 15 distinct blocking findings across 8 independent cold readers; all are closed at `ecc30b1`. 3. **Mechanical gates now enforce what prose discipline did not** — structure audit at release time, plus (this round) an internal-anchor check and a per-fence `bash -n` pass with placeholders substituted. ### Why we stopped, with the measurement | Round | README lines after fix | Readers | Distinct blocking | Found by all readers | |---|---|---|---|---| | 1–3 | 599 → 601 | 1 each | small, cheap | n/a | | 4 | 1038 | 2 | 5 | 1 | | 5 | 1223 | 3 | 4 | 0 | | 6 | 1385 | 3 | 6 | 2 | **The process was generating its own findings.** Each round I fixed by *adding* text: +437, +185, +162 lines. More text is more audit surface, and round 6's blocking count went *up*. Rounds 1–3 added two lines total and found little — that correlation is the explanation, and I should have acted on it at round 5 rather than reporting each round as an independent measurement. **My fix rate was roughly one new blocking defect per defect closed.** Two of round 6's three unanimous blocking findings were the round-5 fixes written *for round 5's blocking findings*. A loop in which the fixer is the principal defect source does not terminate on its own. ### What the six rounds were actually worth Genuinely valuable (would have bitten a real operator): - backup filenames derived from `basename` collided — seven paths here share `api`, so each backup destroyed the last, of exactly the untracked files the step exists to protect - `rm -rf` guarded by `git status --short`, which is blind to **ignored** files, i.e. `.devwork/` - the back-up-first caution sat 68 lines *after* the edit instruction it protects - `plugin update` silently updating nothing at project scope (`-s` defaults to `user`; `enable` auto-detects, `update` does not) - `enabled: false` diagnosed with no cure given - two successive completion tests that reported "done" having examined nothing The remainder is polish on a document whose realistic readership is the operator, on two machines that are already migrated. ### Known and accepted - At 1385 lines the document is arguably worse *as a document* than at 600 — its job is to be read end to end. Trimming is the honest fix and is **deliberately not done now**: it is more work in the same direction. Revisit only if it causes friction in practice. - Findings rated non-blocking by all readers in round 6 are accepted as-is, not fixed. - A real remnant was found on the operator's machine by the new per-project sweep: `~/Coding/castellan/.claude/settings.json` still grants dead farm paths (`Bash(/home/jochem/.claude/skills/_shared/...)`). Left for the operator — another project's settings are not this ticket's to edit. ### Still open on #50 **F33 / AC-12** — nothing stamps the plugin version into a phase outcome. Helper *behaviour* change (`phase-outcome-post.sh`), so it takes the full pipeline, not the meta lane. `D-PO-50-5-1` cannot honestly resolve to "approve" until that is settled or explicitly deferred.
Author
Owner

Linked: this issue is relates_to #34 (recorded by the devwork pipeline).

Linked: this issue is **relates_to** #34 (recorded by the devwork pipeline).
Author
Owner
{
  "status": "resolved",
  "chosen_option": "approve",
  "rationale": "UAT complete on #50. Executed end-to-end across both machines: coexistence Scenarios 3-7 under the intact farm, dev-machine cutover, release + update ritual (0.1.0 through 0.1.5), and Scenario 13 README sufficiency, closed at 0.1.5 after six independent cold-reader walks. Two findings were settled rather than fixed here. UAT-F34 (a version tag pinned in the marketplace snapshot cannot survive the plugin update/install that consumes it) is a property of the Claude Code CLI, not of this feature; the re-cut route the README already names is intact. UAT-F33 (nothing stamps the plugin version into a run record) is outside this feature's own Out of Scope (\"any skill logic change: layout + references + packaging + tooling only\"), so AC-12's third clause was amended to claim only the manual runbook mechanism #50 actually built, and tooling-enforced attribution is deferred to #34 -- which now carries the UAT evidence and design input measured on both machines rather than assumed (the installed plugin root is not a git work tree, the dev checkout root is, so one probe separates them). Checkable: main @ 23fd477 clean and equal to github/main; tags dev--v0.1.0..dev--v0.1.5; consumer verified at 0.1.5/user/enabled; lint, audit-doc-structure, validate --strict, tag dry-run and the P0.8 history scan all green at release. No blocking-class defect remains in what #50 delivered. Authority: operator decision in this session, taken against the two options below.",
  "rejected_alternative": "Hold #50 in UAT until #34 ships the enforced version stamp, so AC-12 could pass as literally written. Turned down: the fix is barred by #50's own Out of Scope, so holding would block a released, dogfooded, working 0.1.5 behind a separate feature's full pipeline without changing anything #50 delivers. Amending the AC to claim only what was built, and moving the capability to the issue that can carry it, is the honest record."
}
<!-- decision-resolution:v1 ref=D-PO-50-5-1 --> ```json { "status": "resolved", "chosen_option": "approve", "rationale": "UAT complete on #50. Executed end-to-end across both machines: coexistence Scenarios 3-7 under the intact farm, dev-machine cutover, release + update ritual (0.1.0 through 0.1.5), and Scenario 13 README sufficiency, closed at 0.1.5 after six independent cold-reader walks. Two findings were settled rather than fixed here. UAT-F34 (a version tag pinned in the marketplace snapshot cannot survive the plugin update/install that consumes it) is a property of the Claude Code CLI, not of this feature; the re-cut route the README already names is intact. UAT-F33 (nothing stamps the plugin version into a run record) is outside this feature's own Out of Scope (\"any skill logic change: layout + references + packaging + tooling only\"), so AC-12's third clause was amended to claim only the manual runbook mechanism #50 actually built, and tooling-enforced attribution is deferred to #34 -- which now carries the UAT evidence and design input measured on both machines rather than assumed (the installed plugin root is not a git work tree, the dev checkout root is, so one probe separates them). Checkable: main @ 23fd477 clean and equal to github/main; tags dev--v0.1.0..dev--v0.1.5; consumer verified at 0.1.5/user/enabled; lint, audit-doc-structure, validate --strict, tag dry-run and the P0.8 history scan all green at release. No blocking-class defect remains in what #50 delivered. Authority: operator decision in this session, taken against the two options below.", "rejected_alternative": "Hold #50 in UAT until #34 ships the enforced version stamp, so AC-12 could pass as literally written. Turned down: the fix is barred by #50's own Out of Scope, so holding would block a released, dogfooded, working 0.1.5 behind a separate feature's full pipeline without changing anything #50 delivers. Amending the AC to claim only what was built, and moving the capability to the issue that can carry it, is the honest record." } ```
Author
Owner
{
  "integration_branch": "main",
  "feature_branch": "feature/50-ship-the-suite-as-a-claude-code-marketplace-plugin",
  "repos": [
    {
      "repo": "devwork-skills",
      "base_sha": "1e6bd78fbb8a08e2a869d0c666a07174be37e771",
      "tip_sha": "23fd477006e96a2d04d0deb8a8c761cfbc324014",
      "commit_count": 41,
      "commits": [
        {
          "sha": "23fd477006e96a2d04d0deb8a8c761cfbc324014",
          "subject": "README: point consumer-runbook step 8 at #34 as the enforced replacement"
        },
        {
          "sha": "bc6214df81eee1017a5a5d92963ac96fef24a550",
          "subject": "release: record 0.1.5"
        },
        {
          "sha": "5a64031a249b1ac2cf2b64c0a8c1b84b4959ade7",
          "subject": "release: bump version to 0.1.5"
        },
        {
          "sha": "ecc30b1746773bbe62a565b0fab8b50621b53e8e",
          "subject": "README: [QA-50-r6 fix] close the round-6 findings; stop trusting my own snippets"
        },
        {
          "sha": "a6c6935e390b879592632067623e7770731e175c",
          "subject": "README: [QA-50-r5 fix] close the round-5 findings from three parallel cold readers"
        },
        {
          "sha": "e30499ece07606bea309f25dba2467c87dafe88c",
          "subject": "README,scripts: [QA-50-r4 fix] close the round-4 blocking findings; gate README structure"
        },
        {
          "sha": "e0d503545e077139dbf02f1aa12b016063aa522b",
          "subject": "release: record 0.1.4"
        },
        {
          "sha": "ee12950cd973cef199d9b397eab332675bbf8c93",
          "subject": "release: bump version to 0.1.4"
        },
        {
          "sha": "543f29995e38489d27d8aa5bc39911c64cfb32d7",
          "subject": "README: fix the third re-walk's findings, including advice that would break the machine"
        },
        {
          "sha": "b06e8f716cf7d5dfb87e2f4b6d3131f17081538e",
          "subject": "release: record 0.1.3"
        },
        {
          "sha": "c81b0a8703a596b9564b13ae38d0ebfd7d602451",
          "subject": "release: bump version to 0.1.3"
        },
        {
          "sha": "b73d4818df113086ec395e859975e816be542b65",
          "subject": "README: fix what a second cold re-walk found, including a heading I had deleted"
        },
        {
          "sha": "9cfb755e1d86843bf308e05fdb36407fd6eac2d7",
          "subject": "release: record 0.1.2"
        },
        {
          "sha": "f628362db2b256a89f43c582608ac0b3ac8c83bb",
          "subject": "release: bump version to 0.1.2"
        },
        {
          "sha": "2d0e5995a0221a8e9c3e62f1f68fd8003522b8d8",
          "subject": "README: fix the four blocking defects a cold re-walk found, and ten smaller ones"
        },
        {
          "sha": "c44cae1a2913c7e6efe3841de79b60821fa9e40a",
          "subject": "lint,scripts: scan every shipped file for user-scope paths, and make the count auditable"
        },
        {
          "sha": "8081969e6478bc81f484d2b93121ca639c2e0c0d",
          "subject": "release: record 0.1.1"
        },
        {
          "sha": "8c9a94ec853500edafe20ec423ed204a59142556",
          "subject": "release: bump version to 0.1.1"
        },
        {
          "sha": "8a985c7c5963725da11ab74c8cfc2cbd394deff9",
          "subject": "README: apply the 0.1.1 fixes from the Scenario 13 walk and Scenario 22"
        },
        {
          "sha": "a3529334d64cb2b05395071d394e3f147789f046",
          "subject": "scripts: make the release and cutover gate fixtures stage their own preconditions"
        },
        {
          "sha": "7c4f87e34d789145c0b4f33b64ee19d8b2bc7847",
          "subject": "release: record 0.1.0"
        },
        {
          "sha": "ee15687e9cd55780054d7fe92230b9de92f9b467",
          "subject": "glab-cli: name the two observed quick-reference traps"
        },
        {
          "sha": "c7d9e2002dbf747fc5017067761c49d421c08ea8",
          "subject": "CLAUDE.md: name the full-pipeline phases by their /dev: plugin names"
        },
        {
          "sha": "d9767590ad27fd07113a44540d2d0654445a384c",
          "subject": "scripts,README: [QA-50-r3 fix] close the round-3 adversarial findings CR-46..CR-48 + nits"
        },
        {
          "sha": "3d63f589ee0b37695be89658a097f8073c4a9837",
          "subject": "scripts,README: [QA-50-r2 fix] resolve the sweep findings CR-39..CR-45"
        },
        {
          "sha": "caef69b4c07ed05bacccb64af7e32265f79e5ba7",
          "subject": "scripts: [QA-50-r2 tests] harden scan fixtures for CR-39/CR-40 (red)"
        },
        {
          "sha": "861e496774bf10a4db85cdd8f3e02490a13d675a",
          "subject": "scripts,plugin,README: [QA-50-r1 fix] resolve the 33 fix-now findings CR-1..CR-32, CR-38"
        },
        {
          "sha": "e049e3fa0a51e801818741480769b57cf67c9638",
          "subject": "scripts: [QA-50-r1 tests] gate scenarios for the round-1 code findings"
        },
        {
          "sha": "923a25b7997117a4949d262f27204db907e35055",
          "subject": "docs: capture learning — 4 learnings from develop (plugin-CLI semantics, fence-aware authoring)"
        },
        {
          "sha": "ca517f0b49b4cd604340d03b434cdd6a58939a6d",
          "subject": "docs: [WU-50-3-6] rewrite README: install/update/cutover runbooks, name table, failure modes"
        },
        {
          "sha": "d0b7e79168ad9cc1e7f9a09feb145753b1cb35a4",
          "subject": "scripts: [WU-50-3-6] add history-scan.sh (P0.8 full-history sensitive-data gate)"
        },
        {
          "sha": "5c6681c27e7d9b9d3f71609f3a38cae286a24844",
          "subject": "scripts: [WU-50-3-5] add cutover-dev.sh and cutover-abort.sh (D11 sequence, S-2 gate, R8 probe)"
        },
        {
          "sha": "ee2ea9be13c5d31d5add545003fac9ff7218b172",
          "subject": "scripts: [WU-50-3-4] implement release.sh and plugin gate lint rules (suite green)"
        },
        {
          "sha": "46c405cee24aa068c46e52e319039d0350139a67",
          "subject": "scripts: [WU-50-3-4] add test-plugin-gates.sh (red: release+scan gates unimplemented)"
        },
        {
          "sha": "f12b2c4932e6c882e78abb9c73546717fa42b4bc",
          "subject": "meta: [WU-50-3-3] resolve helpers via the D7 root and address shipped skills as /dev:<name>"
        },
        {
          "sha": "2c138e33425824c0d4989295e9782e594899703d",
          "subject": "plugin: [WU-50-3-2] point helper error strings at /dev:setup and anchor the user-scope paths"
        },
        {
          "sha": "8ead6e3220c32e342b230cfd20f45c41f2d7ba81",
          "subject": "plugin: [WU-50-3-2] rewrite shipped-skill references to the dev plugin namespace"
        },
        {
          "sha": "598d16b3a0f0898966389a35065617e5d45262ca",
          "subject": "scripts: [WU-50-3-2] add rewrite-plugin-refs.sh (mechanical-pass audit record)"
        },
        {
          "sha": "77a5f2f6a8be7e032be9dbd0fb173036326f16e4",
          "subject": "plugin: [WU-50-3-1] restructure shipped skills into plugin/ with manifests and lint retarget"
        },
        {
          "sha": "5ef8f8557c003cfb953fd2a3e7a69ed70df374dc",
          "subject": "docs(ship-the-suite-as-a-claude-code-marketplace-plugin): test plan"
        },
        {
          "sha": "91b4451f51dba25b828966fc561672fdc2dff16f",
          "subject": "docs(ship-the-suite-as-a-claude-code-marketplace-plugin): software requirements"
        }
      ],
      "compare_url": null,
      "pushed": true,
      "remote_url": "ssh://git@forge-devwork/jbr870/devwork-skills.git"
    }
  ]
}
<!-- code-bundle:v1 issue=50 skill=integrate --> ```json { "integration_branch": "main", "feature_branch": "feature/50-ship-the-suite-as-a-claude-code-marketplace-plugin", "repos": [ { "repo": "devwork-skills", "base_sha": "1e6bd78fbb8a08e2a869d0c666a07174be37e771", "tip_sha": "23fd477006e96a2d04d0deb8a8c761cfbc324014", "commit_count": 41, "commits": [ { "sha": "23fd477006e96a2d04d0deb8a8c761cfbc324014", "subject": "README: point consumer-runbook step 8 at #34 as the enforced replacement" }, { "sha": "bc6214df81eee1017a5a5d92963ac96fef24a550", "subject": "release: record 0.1.5" }, { "sha": "5a64031a249b1ac2cf2b64c0a8c1b84b4959ade7", "subject": "release: bump version to 0.1.5" }, { "sha": "ecc30b1746773bbe62a565b0fab8b50621b53e8e", "subject": "README: [QA-50-r6 fix] close the round-6 findings; stop trusting my own snippets" }, { "sha": "a6c6935e390b879592632067623e7770731e175c", "subject": "README: [QA-50-r5 fix] close the round-5 findings from three parallel cold readers" }, { "sha": "e30499ece07606bea309f25dba2467c87dafe88c", "subject": "README,scripts: [QA-50-r4 fix] close the round-4 blocking findings; gate README structure" }, { "sha": "e0d503545e077139dbf02f1aa12b016063aa522b", "subject": "release: record 0.1.4" }, { "sha": "ee12950cd973cef199d9b397eab332675bbf8c93", "subject": "release: bump version to 0.1.4" }, { "sha": "543f29995e38489d27d8aa5bc39911c64cfb32d7", "subject": "README: fix the third re-walk's findings, including advice that would break the machine" }, { "sha": "b06e8f716cf7d5dfb87e2f4b6d3131f17081538e", "subject": "release: record 0.1.3" }, { "sha": "c81b0a8703a596b9564b13ae38d0ebfd7d602451", "subject": "release: bump version to 0.1.3" }, { "sha": "b73d4818df113086ec395e859975e816be542b65", "subject": "README: fix what a second cold re-walk found, including a heading I had deleted" }, { "sha": "9cfb755e1d86843bf308e05fdb36407fd6eac2d7", "subject": "release: record 0.1.2" }, { "sha": "f628362db2b256a89f43c582608ac0b3ac8c83bb", "subject": "release: bump version to 0.1.2" }, { "sha": "2d0e5995a0221a8e9c3e62f1f68fd8003522b8d8", "subject": "README: fix the four blocking defects a cold re-walk found, and ten smaller ones" }, { "sha": "c44cae1a2913c7e6efe3841de79b60821fa9e40a", "subject": "lint,scripts: scan every shipped file for user-scope paths, and make the count auditable" }, { "sha": "8081969e6478bc81f484d2b93121ca639c2e0c0d", "subject": "release: record 0.1.1" }, { "sha": "8c9a94ec853500edafe20ec423ed204a59142556", "subject": "release: bump version to 0.1.1" }, { "sha": "8a985c7c5963725da11ab74c8cfc2cbd394deff9", "subject": "README: apply the 0.1.1 fixes from the Scenario 13 walk and Scenario 22" }, { "sha": "a3529334d64cb2b05395071d394e3f147789f046", "subject": "scripts: make the release and cutover gate fixtures stage their own preconditions" }, { "sha": "7c4f87e34d789145c0b4f33b64ee19d8b2bc7847", "subject": "release: record 0.1.0" }, { "sha": "ee15687e9cd55780054d7fe92230b9de92f9b467", "subject": "glab-cli: name the two observed quick-reference traps" }, { "sha": "c7d9e2002dbf747fc5017067761c49d421c08ea8", "subject": "CLAUDE.md: name the full-pipeline phases by their /dev: plugin names" }, { "sha": "d9767590ad27fd07113a44540d2d0654445a384c", "subject": "scripts,README: [QA-50-r3 fix] close the round-3 adversarial findings CR-46..CR-48 + nits" }, { "sha": "3d63f589ee0b37695be89658a097f8073c4a9837", "subject": "scripts,README: [QA-50-r2 fix] resolve the sweep findings CR-39..CR-45" }, { "sha": "caef69b4c07ed05bacccb64af7e32265f79e5ba7", "subject": "scripts: [QA-50-r2 tests] harden scan fixtures for CR-39/CR-40 (red)" }, { "sha": "861e496774bf10a4db85cdd8f3e02490a13d675a", "subject": "scripts,plugin,README: [QA-50-r1 fix] resolve the 33 fix-now findings CR-1..CR-32, CR-38" }, { "sha": "e049e3fa0a51e801818741480769b57cf67c9638", "subject": "scripts: [QA-50-r1 tests] gate scenarios for the round-1 code findings" }, { "sha": "923a25b7997117a4949d262f27204db907e35055", "subject": "docs: capture learning — 4 learnings from develop (plugin-CLI semantics, fence-aware authoring)" }, { "sha": "ca517f0b49b4cd604340d03b434cdd6a58939a6d", "subject": "docs: [WU-50-3-6] rewrite README: install/update/cutover runbooks, name table, failure modes" }, { "sha": "d0b7e79168ad9cc1e7f9a09feb145753b1cb35a4", "subject": "scripts: [WU-50-3-6] add history-scan.sh (P0.8 full-history sensitive-data gate)" }, { "sha": "5c6681c27e7d9b9d3f71609f3a38cae286a24844", "subject": "scripts: [WU-50-3-5] add cutover-dev.sh and cutover-abort.sh (D11 sequence, S-2 gate, R8 probe)" }, { "sha": "ee2ea9be13c5d31d5add545003fac9ff7218b172", "subject": "scripts: [WU-50-3-4] implement release.sh and plugin gate lint rules (suite green)" }, { "sha": "46c405cee24aa068c46e52e319039d0350139a67", "subject": "scripts: [WU-50-3-4] add test-plugin-gates.sh (red: release+scan gates unimplemented)" }, { "sha": "f12b2c4932e6c882e78abb9c73546717fa42b4bc", "subject": "meta: [WU-50-3-3] resolve helpers via the D7 root and address shipped skills as /dev:<name>" }, { "sha": "2c138e33425824c0d4989295e9782e594899703d", "subject": "plugin: [WU-50-3-2] point helper error strings at /dev:setup and anchor the user-scope paths" }, { "sha": "8ead6e3220c32e342b230cfd20f45c41f2d7ba81", "subject": "plugin: [WU-50-3-2] rewrite shipped-skill references to the dev plugin namespace" }, { "sha": "598d16b3a0f0898966389a35065617e5d45262ca", "subject": "scripts: [WU-50-3-2] add rewrite-plugin-refs.sh (mechanical-pass audit record)" }, { "sha": "77a5f2f6a8be7e032be9dbd0fb173036326f16e4", "subject": "plugin: [WU-50-3-1] restructure shipped skills into plugin/ with manifests and lint retarget" }, { "sha": "5ef8f8557c003cfb953fd2a3e7a69ed70df374dc", "subject": "docs(ship-the-suite-as-a-claude-code-marketplace-plugin): test plan" }, { "sha": "91b4451f51dba25b828966fc561672fdc2dff16f", "subject": "docs(ship-the-suite-as-a-claude-code-marketplace-plugin): software requirements" } ], "compare_url": null, "pushed": true, "remote_url": "ssh://git@forge-devwork/jbr870/devwork-skills.git" } ] } ```
Author
Owner
{
  "next_state": "accepted",
  "produced": [
    {
      "kind": "code-bundle",
      "ref": "comment:1087",
      "summary": "Merged 41 commit(s) to main (1e6bd78..23fd477), pushed to origin and github. The merge itself was step 4 of scripts/cutover-dev.sh during UAT (this feature replaces the deployment mechanism, so a vanilla /dev:integrate merge would have mutated the live suite mid-session); this run records the bundle from the by-then-merged main, as the approval decision anticipated."
    }
  ],
  "findings": [],
  "pending_decisions": []
}
<!-- phase-outcome:v1 id=PO-50-6 skill=integrate --> ```json { "next_state": "accepted", "produced": [ { "kind": "code-bundle", "ref": "comment:1087", "summary": "Merged 41 commit(s) to main (1e6bd78..23fd477), pushed to origin and github. The merge itself was step 4 of scripts/cutover-dev.sh during UAT (this feature replaces the deployment mechanism, so a vanilla /dev:integrate merge would have mutated the live suite mid-session); this run records the bundle from the by-then-merged main, as the approval decision anticipated." } ], "findings": [], "pending_decisions": [] } ```
Author
Owner

Cut by this promote run. dev--v0.1.6 is the first release containing 23fd477, the
last outstanding deliverable of this issue (the consumer-runbook fix pointing step 8 at
#34). The other 40 commits of the code-bundle:v1 shipped incrementally across
dev--v0.1.0 .. dev--v0.1.5 while the feature was in flight.

status: tag-only — release.sh pushes tags to the github remote only; the Gitea forge
has no tag to hang a Release object on, so there is no release URL to record.

{
  "tag": "dev--v0.1.6",
  "status": "tag-only",
  "repos": [
    {
      "repo": ".",
      "tag": "dev--v0.1.6",
      "target_sha": "6f0249f223be8e95838b89afe3ab7084a011a0bb"
    }
  ]
}
<!-- release-link:v1 issue=50 skill=promote tag=dev--v0.1.6 --> Cut by this promote run. `dev--v0.1.6` is the first release containing `23fd477`, the last outstanding deliverable of this issue (the consumer-runbook fix pointing step 8 at #34). The other 40 commits of the `code-bundle:v1` shipped incrementally across `dev--v0.1.0` .. `dev--v0.1.5` while the feature was in flight. `status: tag-only` — release.sh pushes tags to the `github` remote only; the Gitea forge has no tag to hang a Release object on, so there is no release URL to record. ```json { "tag": "dev--v0.1.6", "status": "tag-only", "repos": [ { "repo": ".", "tag": "dev--v0.1.6", "target_sha": "6f0249f223be8e95838b89afe3ab7084a011a0bb" } ] } ```
Author
Owner
{
  "next_state": "deployed",
  "produced": [
    {
      "kind": "release-link",
      "ref": "comment:1102",
      "summary": "Shipped in dev--v0.1.6 (release commit 6f0249f) — the first release containing 23fd477, the last outstanding deliverable. Four release gates green (lint, audit-doc-structure, claude plugin validate --strict both roots, plugin tag --dry-run); deploy check PASS: tag resolves on github at 6f0249f and validates on both roots."
    }
  ],
  "findings": [],
  "pending_decisions": []
}
<!-- phase-outcome:v1 id=PO-50-7 skill=promote --> ```json { "next_state": "deployed", "produced": [ { "kind": "release-link", "ref": "comment:1102", "summary": "Shipped in dev--v0.1.6 (release commit 6f0249f) — the first release containing 23fd477, the last outstanding deliverable. Four release gates green (lint, audit-doc-structure, claude plugin validate --strict both roots, plugin tag --dry-run); deploy check PASS: tag resolves on github at 6f0249f and validates on both roots." } ], "findings": [], "pending_decisions": [] } ```
Sign in to join this conversation.
No description provided.