diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 81863116..1b66d0cb 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -100,6 +100,18 @@ }, "category": "Developer Tools" }, + { + "name": "han-linear", + "source": { + "source": "local", + "path": "./han-linear" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Developer Tools" + }, { "name": "han-plugin-builder", "source": { diff --git a/docs/plans/han-publishing-cleanup/build-phase-outline.md b/docs/plans/han-publishing-cleanup/build-phase-outline.md new file mode 100644 index 00000000..727ae4a0 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/build-phase-outline.md @@ -0,0 +1,546 @@ +--- +title: "Han Publishing Cleanup: Build Phase Outline" +source_artifact: "./source-han-cleanup-plan.md" +audience: "mixed engineering, product, and leadership" +generated: "2026-07-21" +generated_by: "han-planning:plan-a-phased-build" +--- + + + +# Han Publishing Cleanup: Build Phase Outline + +This document describes the order in which the Han publishing cleanup will be built. The work is broken into a sequence +of **phases**. Each phase is a thin end-to-end deliverable that can be demonstrated to a real person, and each phase +builds on the one before it. + +The cleanup repairs how Han is published to the people who install it: a missing plugin, work items that vanish without +a trace, frozen version numbers, untrue dependency declarations, plugins that never say which versions of each other +they work with, and a release process that only sees half the places it ships to. + +This document is the companion to [source-han-cleanup-plan.md](./source-han-cleanup-plan.md). The source artifact +describes _what is broken in Han's publishing pipeline today and the seven fixes it recommends, in order_. This +document describes _the order in which the work will be built to close that picture_. Every phase below cites the +source-artifact sections it covers, so anyone can trace a phase back to source. + +## Table of Contents + +- [Executive Summary](#executive-summary) +- [Build Phase Index](#build-phase-index) +- [How This Build Differs from the Cleanup Analysis](#departures) +- [Phase Kinds](#phase-kinds) +- [Build Phases](#build-phases) + - [Phase 1: Publish the Linear plugin to the second channel](#phase-1) + - [Phase 2: Label every tracker's marks and close the silent gap](#phase-2) + - [Phase 3: Unfreeze the second channel's version numbers](#phase-3) + - [Phase 4: Remove the untrue dependency declarations](#phase-4) + - [Phase 5: Declare the plugin versions that work together](#phase-5) + - [Phase 6: Teach the release process about all four publishing surfaces](#phase-6) + - [Phase 7: Turn on the automated completeness check](#phase-7) + - [Phase 8 (Deferred): Consolidate the duplicated rule documents](#phase-8) +- [Open Questions](#open-questions) + - [OQ-1 (resolved): How far does the ticket-file fix go in Phase 2?](#oq-1) + - [OQ-2 (resolved): Should plugins declare which versions of each other they work with?](#oq-2) + - [OQ-3 (resolved): Does the format-checking step really catch a mismatched ticket file?](#oq-3) + +--- + +## Executive Summary {#executive-summary} + +**The goal:** Every Han plugin is published, current, and honestly described on both of the channels people install it +from. An automated check makes it impossible to quietly break that again. + +Han ships through two install channels. Following the source artifact, this document calls them the first channel and +the second channel. The first channel is healthy; the second channel is the one that has been quietly rotting, and it +is where most of this cleanup lands. + +**The shape of the build (plain language):** + +- Phases 1 through 3 repair what users hit today: a plugin that is advertised but missing, work items that vanish from + a publishing run or get mistakenly skipped, and version numbers so stale that nobody is ever offered an update. +- Phases 4 and 5 make the declarations honest and complete: the dependency claims that are not true are removed (two + named by the source, a third found in Phase 4's review), and every plugin then states which versions of its + companions it works with. +- Phase 6 teaches the release process to start from what is really in the repository and to update all four publishing + surfaces instead of two. +- Phase 7 turns on the automated check that keeps every earlier fix fixed. It comes last on purpose. +- One piece of work is deferred: consolidating the duplicated rule documents, with a named trigger for reopening it. + +**Sequencing rationale, in plain language:** + +The order comes from the source artifact, and the source is explicit that it is not negotiable. The user-facing harms +come first because someone following the documented instructions hits an error today. + +The record cleanup and the release-process fix come next because the automated check depends on them. If the check were +turned on first, it would fail on almost every plugin from day one, someone would disable it, and it would protect +nothing. Fixed first, then guarded: that is the whole sequencing argument. + +**Departures from the source artifact:** + +- The source's fifth fix, correcting two documents that describe a behavior the system does not have, is dropped: the + source never names the documents, so the work cannot be picked up. +- Version compatibility between plugins, which the source left as an open question with no proposed fix, is now + committed work: Phase 5 has every plugin state which versions of its companions it works with. +- The ticket-file fix is built at full width: all three tracker publishers label their marks, not only the GitHub + repair the source's ordered list named. + +The [departures section](#departures) explains each one. + +**Phases deliberately deferred:** + +Consolidating the three plugins' duplicated copies of the same two rule documents is listed as Phase 8 but not built +now. The source artifact strengthens the case for consolidation, yet it is not one of the seven ordered fixes, and none +of the seven depend on it. It reopens the next time one of those shared rules changes. + +**Where to look next:** The [Build Phase Index](#build-phase-index) lists every phase in order. Detailed write-ups +follow under [Build Phases](#build-phases). Decisions the team must resolve before work starts are at +[Open Questions](#open-questions). + +--- + +## Build Phase Index {#build-phase-index} + +> The scan view. One row per phase, in build order. Each "Outcome" cell is one short sentence. Detailed write-ups +> follow under [Build Phases](#build-phases); use the link in the Phase column. + +| # | Phase | Kind | Outcome (one sentence) | +| --- | -------------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------- | +| 1 | [Publish the Linear plugin to the second channel](#phase-1) | Feature slice | Following the second channel's setup instructions for Linear now works. | +| 2 | [Label every tracker's marks and close the silent gap](#phase-2) | Feature slice | No publisher can lose or mistakenly skip another tracker's work items. | +| 3 | [Unfreeze the second channel's version numbers](#phase-3) | Feature slice | People on the second channel are offered updates again. | +| 4 | [Remove the untrue dependency declarations](#phase-4) | Feature slice | Installing Reporting, Feedback, or Linear no longer drags in a plugin they never use. | +| 5 | [Declare the plugin versions that work together](#phase-5) | Feature slice | Every plugin states which versions of its companions it works with. | +| 6 | [Teach the release process about all four publishing surfaces](#phase-6) | Feature slice | A release starts from the repository and updates all four surfaces. | +| 7 | [Turn on the automated completeness check](#phase-7) | Feature slice | Every release and change is blocked if any plugin is missing anywhere. | +| 8 | [Consolidate the duplicated rule documents (deferred)](#phase-8) | Deferred | Three plugins share one copy of each rule instead of hand-synced copies. | + +> Numbers are assigned in build order and are stable for the life of this outline. Cite them as `Phase N` in tickets, +> comments, and follow-up reports. + +--- + +## How This Build Differs from the Cleanup Analysis {#departures} + +The build deliberately departs from [source-han-cleanup-plan.md](./source-han-cleanup-plan.md) in three ways, decided +on 2026-07-21. Each departure is summarized once here so the rest of the document can refer to it by name. + +### 1. The document-correction fix is dropped + +The source's fifth ordered fix corrects two documents that describe a behavior the system does not have. The source +names the count and the harm but never the documents themselves, so there is no detail to work from. Rather than carry +an unworkable phase, this build drops it. If the underlying analysis ever surfaces which two documents were meant, the +fix comes back as its own small piece of work. + +### 2. Version declarations are promoted from open question to committed work + +The source deliberately proposed no fix for version compatibility between plugins, holding that the right answer needs +a real decision. That decision has now been made: every plugin will state explicitly which versions of its companions +it works with. The work lands as [Phase 5](#phase-5), immediately after the dependency declarations are made truthful +in [Phase 4](#phase-4). + +### 3. The ticket-file fix is built at full width + +The source's ordered list committed only to repairing the GitHub publisher's silent gap, while its prose named the +fuller fix: make every publisher's marks say which tracker they came from. This build adopts the fuller fix in +[Phase 2](#phase-2), so all three publishers change together and old-format files get an upgrade path that stops and +asks rather than guesses. + +--- + +## Phase Kinds {#phase-kinds} + +Every phase is tagged with one of four kinds. The taxonomy is used in the Build Phase Index and on each phase entry's +`**Kind.**` line. + +- **Foundation:** A capability that does not deliver new user-facing features on its own, but is required for later + phases. Must still be demoable in its own right (e.g., "an admin can edit and persist a new setting"). No phase in + this plan uses the Foundation kind. +- **Feature slice:** A thin end-to-end strip of new behavior that a real user can experience. +- **Polish:** Branding, refinement, observability, or quality-of-life work that enriches a working core. +- **Deferred:** Listed for traceability; not built in the current plan. Slotted at the end of the index. + +--- + +## Build Phases {#build-phases} + +### Phase 1: Publish the Linear plugin to the second channel {#phase-1} + +**Kind.** Feature slice. + +**Builds on.** Nothing. This is the starting phase. + +**What we build.** The Linear integration is published to the second install channel, the one place it is advertised +but missing. Today the setup instructions for that channel tell people to install it, and anyone who follows them gets +an error. After this phase, the instructions work as written. + +**Why this is Phase 1.** This is the only fix where a new user hits a hard error in the first thing they try, so it +carries the most immediate harm. It needs nothing else in place first. The source artifact ranks it first for the same +reason: the plugin exists and works, and only the publishing step was missed. + +**Outcome to demonstrate.** + +1. Open the second channel's setup instructions for the Linear integration. +2. Follow them exactly, as a new user would. +3. The installation succeeds instead of returning an error. +4. Run the installed integration's work-item publishing skill and confirm it is the current version. + +**Source citations.** + +- ["The publishing pipeline: the actual problem"](./source-han-cleanup-plan.md#the-publishing-pipeline-the-actual-problem), + the "One plugin is not there at all" finding. +- ["What I would do, in order"](./source-han-cleanup-plan.md#what-i-would-do-in-order), item 1. + +**Connects to.** + +- [Phase 7](#phase-7): the automated check cannot land green while this plugin is missing from the channel. + +**Preconditions to verify before starting.** + +- Confirm the second channel accepts a first-time publication of a plugin that was never listed there, rather than only + updates to existing listings. + +--- + +### Phase 2: Label every tracker's marks and close the silent gap {#phase-2} + +**Kind.** Feature slice. + +**Builds on.** Nothing from Phase 1. It holds position 2 because the source ranks the remaining fixes by user harm, and +this is the only one where work silently disappears. + +**What we build.** Three plugins publish work items to three different trackers, and all three record what they +published by marking up the same shared file. Today the GitHub publisher can silently lose items marked by another +tracker, and the other two publishers can mistake each other's marks and skip work that was never published to their +tracker. After this phase, every publisher's marks say which tracker they came from, so no publisher can misread +another's, and every work item in any publishing run is accounted for: published, skipped with a count, or reported as +belonging to another tracker. Files marked up in the old format get an upgrade path that stops and asks rather than +guesses. This is the full fix chosen in [OQ-1](#oq-1) and named as [departure 3](#departures). + +**Why this is Phase 2.** Silent data loss outranks every remaining problem: the other two tracker publishers at least +report a skipped count that an attentive person would notice, while the GitHub one leaves no trace at all. The source +calls it "the one worth fixing first" among the ticket-file problems. It depends on nothing else in this plan. + +**Outcome to demonstrate.** + +1. Take a work-items file that another tracker's publisher has already marked up. +2. Run the GitHub publisher against it. +3. Every item in the file appears in the run's output: published, skipped, or reported as belonging to another + tracker, and the run says what to do about the reported ones. +4. Repeat with the other two publishers and confirm neither skips items that were never published to its tracker. +5. Feed in a file marked up in the old format and confirm the run stops and asks instead of guessing. + +**Source citations.** + +- ["The shared ticket file: a real bug, described more carefully than I first described it"](./source-han-cleanup-plan.md#the-shared-ticket-file-a-real-bug-described-more-carefully-than-i-first-described-it). +- ["What I would do, in order"](./source-han-cleanup-plan.md#what-i-would-do-in-order), item 2. + +**Connects to.** + +- No later phase depends on it. It is sequenced here purely by severity. + +**Preconditions to verify before starting.** + +- Run the safety-net trial from [OQ-3](#oq-3): feed a mismatched file through the GitHub publisher in a throwaway + project and confirm the run stops at its checking step, so this phase starts from tested facts about today's + behavior. +- Confirm no known user files in the old mark format would be stranded by the upgrade path's stop-and-ask behavior. + +--- + +### Phase 3: Unfreeze the second channel's version numbers {#phase-3} + +**Kind.** Feature slice. + +**Builds on.** Nothing from earlier phases. It holds position 3 in the source's harm-first ordering. + +**What we build.** The second channel decides whether an update is available by reading a version number. That number +has not moved since the day it was written, so it never offers anyone an update. This phase corrects every frozen version +number on that channel to match the versions that shipped, so people stuck on old copies start being offered +updates again. + +**Why this is Phase 3.** Everyone on the second channel is affected, but nothing errors: they quietly run old skills. +That makes it less urgent than a hard install failure (Phase 1) or silent data loss (Phase 2), and more urgent than +record cleanup. It is also one of the named prerequisites for the automated check: turned on before this fix, the check +would fail on almost every plugin. + +**Outcome to demonstrate.** + +1. Set up a machine with an older copy of a Han plugin installed from the second channel. +2. Check for updates on that channel. +3. An update is offered, where before this phase none ever was. +4. Accept the update and confirm the installed version now matches the latest release. + +**Source citations.** + +- ["The publishing pipeline: the actual problem"](./source-han-cleanup-plan.md#the-publishing-pipeline-the-actual-problem), + the "People on the second channel are stuck on old versions and cannot tell" finding. +- ["What I would do, in order"](./source-han-cleanup-plan.md#what-i-would-do-in-order), item 3. + +**Connects to.** + +- [Phase 6](#phase-6): once the release process owns all four surfaces, it keeps these numbers moving. +- [Phase 7](#phase-7): the check cannot land green while these numbers are stale. + +**Preconditions to verify before starting.** + +- Confirm which released version each plugin on the second channel should be corrected to, so the fix does not guess. + +--- + +### Phase 4: Remove the untrue dependency declarations {#phase-4} + +**Kind.** Feature slice. + +**Builds on.** Nothing from earlier phases. The user-facing repairs come first; this begins the record cleanup. + +**What we build.** Two plugins, Reporting and Feedback, declare that they need the Core plugin and never touch it. +Reporting's declaration is a leftover from a capability that moved to the Communication plugin; Feedback's cannot be +true because that plugin is not permitted to call other plugins at all. This phase deletes both declarations, plus a +third found during this phase's own review: the Linear plugin's declaration on the Core plugin fails the same +evidence test, and the team chose on 2026-07-21 to remove it in the same pass. Three lines removed, nothing added, +and every documentation surface repeating the claims is corrected in the same change. + +**Why this is Phase 4.** The direct cost is small: people installing either plugin quietly get a large plugin they +never use. The real damage is to trust in the declarations as a whole, since nobody can rely on them to answer "what +breaks if I change this?" while two of them are decorative. It lands after the user-facing repairs because nobody hits +an error from it, and immediately before Phase 5, which adds version statements to the declarations this phase makes +truthful. + +**Outcome to demonstrate.** + +1. On a clean machine, install the Reporting plugin. +2. Confirm the Core plugin is not pulled in alongside it. +3. Run a Reporting skill end to end and confirm it still works, including its writing pass. +4. Repeat steps 1 through 3 for the Feedback plugin and the Linear plugin. + +**Source citations.** + +- ["The dependency graph: two threads to snip"](./source-han-cleanup-plan.md#the-dependency-graph-two-threads-to-snip). +- ["What I would do, in order"](./source-han-cleanup-plan.md#what-i-would-do-in-order), item 4. + +**Connects to.** + +- [Phase 5](#phase-5): version statements are added on top of the declarations this phase makes truthful. + +**Preconditions to verify before starting.** + +- Re-confirm none of the three plugins reaches the Core plugin through any path the analysis did not cover, so + removing the declarations cannot break a working flow. + +--- + +### Phase 5: Declare the plugin versions that work together {#phase-5} + +**Kind.** Feature slice. + +**Builds on.** Phase 4: version statements are only worth adding to declarations that are true. + +**What we build.** Every plugin states explicitly which versions of the other plugins it works with. Today plugins are +installed and updated one at a time, so someone can update one plugin while running a months-old copy of another, and +nothing anywhere notices or complains. After this phase, each plugin's requirements are written down where the person +installing it can see them. This work is [departure 2](#departures): the source left the question open, and the team +decided it on 2026-07-21. + +**Why this is Phase 5.** It follows directly from Phase 4: first the dependency declarations are made truthful, then +each one gains a version statement, so the record is corrected once rather than twice. It lands before the release +process work because Phase 6 is where the statements start being kept current as new versions ship. + +**Outcome to demonstrate.** + +1. Open any plugin's listing and see which versions of its companion plugins it states it works with. +2. On a clean machine, install that plugin and confirm compatible companions arrive with it. +3. Set up a machine with an out-of-date companion and confirm the mismatch is surfaced instead of silently accepted. + +**Source citations.** + +- ["Two places the reviewer changed my mind entirely"](./source-han-cleanup-plan.md#two-places-the-reviewer-changed-my-mind-entirely), + the "On version compatibility between plugins" finding. +- ["How much of this to trust"](./source-han-cleanup-plan.md#how-much-of-this-to-trust), the caveat that this finding + rests on the project's description of how installation works. + +**Connects to.** + +- [Phase 6](#phase-6): the release process keeps these version statements current as new versions ship. + +**Preconditions to verify before starting.** + +- Confirm whether the install channels read and enforce version statements. If they do not, decide whether the + statements start as visible information for people until the channels can act on them. +- Decide how strict the statements are: an exact version, a minimum version, or a range. + +--- + +### Phase 6: Teach the release process about all four publishing surfaces {#phase-6} + +**Kind.** Feature slice. + +**Builds on.** Phases 1 and 3 in practice: teaching the release process to see the second channel is far simpler when +that channel's contents are already correct. It also picks up upkeep of the version statements Phase 5 introduces. + +**What we build.** Today the release process updates two publishing surfaces and does not know the other two exist, +which is why roughly twenty releases went by without anyone noticing the rot. After this phase the release process +starts from the plugins as they exist in the repository, rather than trusting a list that can go stale, and +updates all four surfaces. It keeps the companion version statements from Phase 5 current as new versions ship. It +also knows the one deliberate exception permanently: the all-in-one bundle cannot be published to the second channel +because that channel does not support bundles yet. + +**Why this is Phase 6.** This is the root-cause fix: every earlier publishing problem grew from a release process that +could not see half the world. It lands after Phases 1 and 3 so it maintains a correct state instead of inheriting a +broken one, and immediately before Phase 7 because the automated check enforces exactly the behavior this phase +teaches. + +**Outcome to demonstrate.** + +1. Run the release process in a rehearsal mode against the current repository. +2. Confirm it lists every plugin found in the repository as its starting point. +3. Confirm it updates, or reports it would update, all four publishing surfaces. +4. Confirm the all-in-one bundle's absence from the second channel is reported as a known, documented exception rather + than an error. + +**Source citations.** + +- ["The publishing pipeline: the actual problem"](./source-han-cleanup-plan.md#the-publishing-pipeline-the-actual-problem), + the "Before", "After", and root-cause findings. +- ["What I would do, in order"](./source-han-cleanup-plan.md#what-i-would-do-in-order), item 6. + +**Connects to.** + +- [Phase 7](#phase-7): the check turns the behavior this phase teaches into something that cannot regress. + +**Preconditions to verify before starting.** + +- Confirm the full list of four publishing surfaces and what "up to date" means for each. +- Confirm the bundle exception is recorded somewhere durable the release process and the check can both consult. + +--- + +### Phase 7: Turn on the automated completeness check {#phase-7} + +**Kind.** Feature slice. + +**Builds on.** Phases 1, 3, and 6. The source is explicit that this order is not negotiable: the check must land on a +tree that already passes it. + +**What we build.** An automated check that asks one question on every release and every proposed change: does every +plugin appear everywhere it should, at the right version? If anything is missing, it stops the release or flags the +change and says exactly what is absent. It knows about the all-in-one bundle exception permanently, so that one +deliberate gap is never flagged. + +**Why this is Phase 7.** The first six fixes are small; this is what makes them stay fixed. Turned on before the tree +is ready, the check fails on almost every plugin from day one, someone disables it, and it protects nothing. Turned on +last, it lands green, stays green, and makes every problem above impossible to reintroduce, including the "plugin added +today lands invisible by default" failure that caused the Linear gap. + +**Outcome to demonstrate.** + +1. Run the check against the current repository and watch it pass. +2. On a throwaway branch, add a new plugin without publishing it anywhere. +3. Run the check again and watch it fail, naming the new plugin and each surface it is missing from. +4. Confirm the all-in-one bundle's known exception is not flagged in either run. + +**Source citations.** + +- ["The order matters, and getting it wrong stops everything"](./source-han-cleanup-plan.md#the-order-matters-and-getting-it-wrong-stops-everything). +- ["What I would do, in order"](./source-han-cleanup-plan.md#what-i-would-do-in-order), item 7 and its closing + paragraph. + +**Connects to.** + +- This is the final phase. It guards the outcomes of [Phase 1](#phase-1), [Phase 3](#phase-3), and + [Phase 6](#phase-6) from regressing. Phase 7's spec review corrected an earlier claim here: the check does not + examine dependency declarations, so [Phase 4](#phase-4)'s outcome is not among its guards. + +**Preconditions to verify before starting.** + +- Confirm Phases 1, 3, and 6 are complete and the check's first run against the real tree passes before it is allowed + to block anything. + +--- + +### Phase 8 (Deferred): Consolidate the duplicated rule documents {#phase-8} + +**Kind.** Deferred. + +**Builds on.** Not applicable until built. + +**What we build.** Three plugins each carry their own copy of the same two rule documents. When one rule changed, a +person had to remember to make the identical edit in three separate places, and so far has gotten it right. This phase +would consolidate each rule into one shared copy so that manual synchronization stops being a job. + +**Why this is deferred.** The source artifact strengthens the case for consolidation, reversing the earlier analysis +that said to leave the copies alone. But it is not one of the seven ordered fixes, none of the seven depend on it, and +the copies are correct today. Building it now would be work the cleanup does not need; listing it here gives the team a +place to slot it when the trigger fires. + +**Reopen when.** The next time either of the two shared rules needs an edit, consolidate before or alongside that edit +instead of hand-copying the change into three places again. + +**Outcome to demonstrate (when or if built).** + +1. Edit the shared rule in its one canonical location. +2. Confirm all three plugins that follow the rule pick up the change. +3. Confirm no plugin still carries a private copy that could drift. + +**Source citations.** + +- ["Two places the reviewer changed my mind entirely"](./source-han-cleanup-plan.md#two-places-the-reviewer-changed-my-mind-entirely), + the "On duplicated rule files" finding. + +--- + +## Open Questions {#open-questions} + +> All three questions raised while shaping this plan were resolved on 2026-07-21. The decisions are recorded here, with +> their original anchors, so citations of `OQ-N` in tickets and threads still resolve. + +### OQ-1 (resolved). How far does the ticket-file fix go in Phase 2? {#oq-1} + +**Blocked phase(s).** Phase 2. **Decision: the full fix.** + +The source described two related problems with the shared ticket file: the GitHub publisher silently drops items +marked by another tracker, and the other two publishers can mistake each other's marks and skip work that was never +published to their tracker. The choice was between the fix the source itself named, making every publisher's marks say +which tracker they came from, and a smaller change that only stopped the GitHub publisher's silent loss. + +The team chose the full fix. All three publishers label their marks, and files marked up the old way get an upgrade +path that stops and asks rather than guesses. The decision is folded into [Phase 2](#phase-2) and recorded as +[departure 3](#departures). + +### OQ-2 (resolved). Should plugins declare which versions of each other they work with? {#oq-2} + +**Blocked phase(s).** None when raised. **Decision: yes, explicitly.** + +The source reversed the earlier analysis that closed this question: plugins are installed and updated one at a time, +so someone can update the Core plugin today while running a months-old Coding plugin, and nothing anywhere notices or +complains. The source deliberately proposed no fix because the right one is not obvious. + +The team decided that all plugins should state which versions of each other they depend on, explicitly. The work lands +as [Phase 5](#phase-5) and is recorded as [departure 2](#departures). The open sub-questions, whether the install +channels enforce the statements and how strict each statement is, live on Phase 5's preconditions list. + +### OQ-3 (resolved). Does the format-checking step really catch a mismatched ticket file? {#oq-3} + +**Blocked phase(s).** None when raised. **Decision: test it right away, before the phases start.** + +The source's reviewer could not confirm that the step meant to catch a mismatched ticket file works in practice, +believing it rested on in-the-moment judgment rather than anything written down. A first inspection after the decision +found the rule is written down after all: the GitHub publisher's instructions explicitly say a file carrying another +tracker's marks must stop the run and be reported to the user, never repaired, because repairing it would publish +duplicates. + +What remains untested is whether a live run follows that instruction. The remaining trial, feeding a mismatched file +through the GitHub publisher in a throwaway project, is now the first precondition on [Phase 2](#phase-2). If the run +stops as written, the note closes; if not, the gap becomes part of Phase 2's fix. + +--- + +_End of outline. If you need to cite a specific phase elsewhere, use its `Phase N` number: those numbers are stable +for the life of this document. If you need to cite a specific open question, use its `OQ-N` ID._ diff --git a/docs/plans/han-publishing-cleanup/phase-1-linear-publishing/artifacts/decision-log.md b/docs/plans/han-publishing-cleanup/phase-1-linear-publishing/artifacts/decision-log.md new file mode 100644 index 00000000..5cb8a0f3 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-1-linear-publishing/artifacts/decision-log.md @@ -0,0 +1,60 @@ +# Decision Log: Publish the Linear Plugin to the Second Channel + +## Trivial decisions + +- D1: Spec location — the spec lives at `docs/plans/han-publishing-cleanup/phase-1-linear-publishing/`, nested beside + the build phase outline that spawned it (considered a standalone top-level plan folder; rejected because the repo's + convention is one folder per plan and this work belongs to the han-publishing-cleanup plan). — Referenced in spec: + none (organizational). +- D4: Version parity across channels at ship time — the channel manifest's stated version equals the first channel's + released version, and the installed manifest is the observable check for "current version" (considered a runtime + version signal from the skill itself; rejected because the skill surfaces no version at runtime). — Driven by + findings: F5, F9. — Referenced in + spec: Outcome; Primary Flow; Maintainer verification before ship; Edge Cases and Failure Modes. + +## Full decisions + +### D2: Definition of done + +- **Question:** The listing fix already exists on the working branch, but users install from the default branch. What + does "done" mean for this phase? +- **Decision:** Done means the existing listing entry and channel manifest are verified correct, one real end-to-end + install is validated, and the fix reaches users when the working branch merges to the default branch. No separate + hotfix ships ahead of the merge. +- **Rationale:** The fix was authored on this branch (commit `25e7bd2`, "feat(han-linear): publish han-linear to the + Codex marketplace"), so the remaining work is verification and shipping, not authoring. The user chose to ride the + branch's merge rather than hotfix the default branch. +- **Evidence:** User input (2026-07-21). Branch state: `.agents/plugins/marketplace.json` on `plugin-cleanup` lists + `han-linear`; `origin/main`'s copy does not. `han-linear/.codex-plugin/plugin.json` exists at version 1.0.2, matching + `han-linear/.claude-plugin/plugin.json`. +- **Rejected alternatives:** + - Hotfix the default branch now — rejected because it adds a second shipping motion for a fix that arrives with the + branch anyway; the user accepted the merge timeline. + - Listing presence only, no install validation — rejected because the build outline's Phase 1 demo requires a + working install, not a present listing entry. +- **Linked technical notes:** T1 +- **Driven by findings:** F2 (the first-time-publication precondition was made explicit inside this decision's + verification scope) +- **Dependent decisions:** D3, D4 +- **Referenced in spec:** Outcome; Actors and Triggers; Primary Flow; Out of Scope + +### D3: No companion-install instruction + +- **Question:** The second channel resolves no dependencies. Does the Linear plugin's install instruction need a + companion-install note, the way the Atlassian plugin's does? +- **Decision:** No new companion note. The documented instructions stay as they are, and verification confirms the + plugin's skill runs standalone. +- **Rationale:** The Atlassian plugin's note exists because its wrapped skills source the shared readability standard + from the communication plugin. The Linear plugin's skill content references no other plugin. +- **Evidence:** Codebase: `grep -rn "han-core\|han-communication" han-linear/skills/` returns nothing, while + `han-linear/.claude-plugin/plugin.json` declares a first-channel dependency on `han-core`. README lines 87-89 already + name `han-linear` as an opt-in install and reserve the companion note for `han-atlassian`. +- **Rejected alternatives:** + - Add "install han-core alongside" to the instructions — rejected because no skill content sources it; the + declaration exists only on the first channel, where the channel itself resolves it. +- **Linked technical notes:** — +- **Driven by findings:** F1 (the plugin's own README contradicts the standalone claim — `han-linear/README.md` lines + 8 and 16 say its skills dispatch `han-core` and `han-communication` agents, while + `han-linear/skills/work-items-to-linear/SKILL.md` dispatches none; the discrepancy became OI-2) +- **Dependent decisions:** — +- **Referenced in spec:** Primary Flow; Open Items diff --git a/docs/plans/han-publishing-cleanup/phase-1-linear-publishing/artifacts/feature-technical-notes.md b/docs/plans/han-publishing-cleanup/phase-1-linear-publishing/artifacts/feature-technical-notes.md new file mode 100644 index 00000000..b0bf2280 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-1-linear-publishing/artifacts/feature-technical-notes.md @@ -0,0 +1,13 @@ +# Feature Technical Notes: Publish the Linear Plugin to the Second Channel + +## T1: Second channel serves the default branch + +- **Context:** The Outcome section promises the fix reaches users at merge time, not when the listing change was + authored. That timing is only correct because of how the second channel resolves the marketplace. +- **Technical detail:** The Codex channel registers this repository itself as the marketplace + (`codex plugin marketplace add testdouble/han`) and reads the listing from the repository's default branch. A listing + entry that exists only on a working branch is invisible to every user until that branch merges to `main`. This is + external channel behavior, not discoverable from the repo's own code. +- **Supports decisions:** D2 +- **Driven by findings:** — +- **Referenced in spec:** Outcome; Edge Cases and Failure Modes; Coordinations diff --git a/docs/plans/han-publishing-cleanup/phase-1-linear-publishing/artifacts/gap-analysis-scratch.md b/docs/plans/han-publishing-cleanup/phase-1-linear-publishing/artifacts/gap-analysis-scratch.md new file mode 100644 index 00000000..8773d723 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-1-linear-publishing/artifacts/gap-analysis-scratch.md @@ -0,0 +1,125 @@ +# Gap Analysis: Phase 1 Build Outline Entry vs. Phase 1 Feature Specification + +## Comparison Direction + +Current state (artifact under review): the draft feature specification at +`docs/plans/han-publishing-cleanup/phase-1-linear-publishing/feature-specification.md`, with companion artifacts +`artifacts/decision-log.md` and `artifacts/feature-technical-notes.md`. + +Desired state (reference): the Phase 1 entry `{#phase-1}` ("Publish the Linear plugin to the second channel") of +`docs/plans/han-publishing-cleanup/build-phase-outline.md`, comprising its What we build, Why, Outcome to demonstrate +(4-step demo script), Source citations, Connects to, and Preconditions to verify subsections. + +This is checked current-state-toward-desired-state (does the spec cover everything the outline's Phase 1 entry +commits to), per the task's request. Additions the spec makes beyond the outline are also reported, as requested, in a +dedicated non-gap section, since the taxonomy's four categories describe desired-state coverage gaps, not current-state +scope creep. + +## Scope + +Comparison areas analyzed, matching the outline Phase 1 entry's own subsection structure: + +1. What we build (feature description) +2. Why this is Phase 1 (rationale — checked only for behavioral claims restated or contradicted by the spec, not for + narrative framing) +3. Outcome to demonstrate — all 4 demo steps, checked individually +4. Connects to (the Phase 7 dependency) +5. Preconditions to verify (the first-time-publication precondition) +6. Source citations (excluded from gap findings — see Areas Needing Separate Analysis) + +Excluded from findings: implementation mechanics (channel tooling commands, file paths, commit hashes) per the task's +constraint to behavioral-level findings only. These appear in the spec's technical notes and decision log as supporting +evidence, not as gap subject matter. + +## Actors and Modes Observed + +The outline's Phase 1 entry addresses one behavioral actor type: "anyone... who follows [the setup instructions]" / +"a new user," in an interactive, self-service installation mode. It names no sub-roles, no automated/batch mode, and no +API or agent surface for this phase (those show up only later, in Phase 7's automated check). The spec extends this +with a second actor the outline does not name for Phase 1: a "maintainer verifying the listing before ship," operating +in a distinct pre-ship verification mode. No API or agent surface is observed in either document for this phase. + +## Summary + +Compared the Phase 1 entry of the build phase outline (desired state) against the Phase 1 feature specification and its +decision log and technical notes (current state), current state toward desired state. All four demo steps are covered +without contradiction; the gaps found are narrower — one precondition that is exercised in practice but never named as +a distinct check, and one cross-phase dependency that the outline frames as a blocking relationship but the spec +records only as an exclusion. + +| Category | Count | Description | +|----------|-------|-------------| +| Missing | 0 | Elements in desired state with no current state correspondence | +| Partial | 2 | Elements present in both but incompletely covered | +| Divergent | 0 | Elements addressing same concern in incompatible ways | +| Implicit | 0 | Assumed capabilities neither confirmed nor denied | + +Full analysis written to: `docs/plans/han-publishing-cleanup/phase-1-linear-publishing/artifacts/gap-analysis-scratch.md` + +## Findings + +**GAP-001: First-time-publication precondition is never named as a distinct check** +- **Category:** Partial +- **Feature/Behavior:** Confirming, before work starts, that the second channel's publishing mechanism accepts a + brand-new listing (a plugin "that was never listed there") rather than only accepting updates to existing listings. +- **Current State:** `feature-specification.md`, "Actors and Triggers" → Preconditions ("The user has the second + channel's tooling installed and can reach the repository") and "Alternate Flows and States" → Maintainer verification + before ship (three confirmations: listing entry shape, manifest version match, one real end-to-end install). None of + these three confirmations is framed as answering the specific question the outline raises — whether the channel's + mechanism handles a plugin that has no prior listing at all, as opposed to an update path. The end-to-end install + step would incidentally exercise this, but the spec never names it as a distinct risk to verify, and the decision log + (`decision-log.md` D2) treats the remaining work as "verification and shipping, not authoring," without calling out + first-time-listing acceptance as one of the things being verified. +- **Desired State:** `build-phase-outline.md` {#phase-1}, "Preconditions to verify before starting": "Confirm the + second channel accepts a first-time publication of a plugin that was never listed there, rather than only updates to + existing listings." + +**GAP-002: Phase 7 dependency is recorded as an exclusion, not as the outline's blocking relationship** +- **Category:** Partial +- **Feature/Behavior:** The relationship between this phase and the later automated completeness check — specifically, + that the check cannot pass while the Linear plugin is missing from the second channel, making this phase a + precondition for Phase 7's success. +- **Current State:** `feature-specification.md`, "Out of Scope": "Teaching the release process about all four + publishing surfaces (Phase 6) and the automated completeness check (Phase 7)." This states only that Phase 7's own + work is excluded from this spec; it does not state that Phase 7 depends on this phase's outcome, or that this + phase's success is a precondition for Phase 7 landing green. +- **Desired State:** `build-phase-outline.md` {#phase-1}, "Connects to": "[Phase 7](#phase-7): the automated check + cannot land green while this plugin is missing from the channel." + +## Additions Beyond the Outline (Not Gaps) + +These are current-state elements in the spec that the outline's Phase 1 entry does not address. They are not gap +findings under the taxonomy — the taxonomy measures desired-state coverage, not current-state scope creep — but the +task asked that they be flagged, with a note on whether the decision log evidences each one. + +1. **Definition of done: verify + ship at branch merge, no hotfix.** The outline's Phase 1 entry is silent on shipping + mechanics — it describes the fix as something to build, not a timeline for reaching users. The spec's Outcome and + Out of Scope sections commit to a specific timeline (ships at branch merge; no hotfix ships ahead of it). This is + evidenced: `decision-log.md` D2 records it as a decision made by the user on 2026-07-21, with rationale (the fix is + already authored on the working branch; the user chose to ride the merge rather than hotfix) and a rejected + alternative (hotfix now). This is the legitimate, user-sourced addition the task anticipated. +2. **No companion-install instruction (D3).** The spec adds a decision that the Linear plugin's install instructions + need no note about installing a companion plugin alongside it, unlike the Atlassian plugin's install note. The + outline's Phase 1 entry does not raise or foreclose this question. This is evidenced: `decision-log.md` D3 cites a + codebase check (`grep -rn "han-core|han-communication" han-linear/skills/` returns nothing) and the plugin's + existing first-channel dependency declaration, supporting the claim that no companion note is needed. +3. **Second-channel manifest version must match the first channel's released version.** The spec's Coordinations table + and one Edge Case row commit to a specific rule beyond the outline's demo step 4 ("confirm it is the current + version"): that the second channel's manifest version must match the first channel's, and that a mismatch holds the + ship. This is not tied to any decision-log entry (no D-numbered decision cites it as evidence), so it is a + lower-confidence addition — reasonable given the outline's general "current version" framing, but not + independently evidenced the way D2 and D3 are. + +## Areas Needing Separate Analysis + +- **Source citation traceability.** The outline's Phase 1 entry cites two source-artifact sections + (`source-han-cleanup-plan.md`) to let the fix be traced back to the original analysis. The spec does not cite the + source artifact at all — only the build outline and its own artifacts folder. Because the spec and the outline sit + at different abstraction levels (a feature spec vs. a phase-tracing document), this is a structural/format + difference rather than a behavioral gap, and was excluded from the numbered findings per the task's constraint to + behavioral-level findings. A documentation-conventions review (whether feature specs are expected to carry + source-artifact citations) would need separate, focused analysis. +- **"Why this is Phase 1" rationale.** The outline's rationale (this is the only fix causing a hard error on a new + user's first action) is narrative framing for sequencing, not a behavioral commitment the spec needs to restate. It + was checked only for claims that the spec might contradict (none found) and excluded from further analysis as + out-of-scope for a feature specification's format. diff --git a/docs/plans/han-publishing-cleanup/phase-1-linear-publishing/artifacts/team-findings.md b/docs/plans/han-publishing-cleanup/phase-1-linear-publishing/artifacts/team-findings.md new file mode 100644 index 00000000..23b2ed22 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-1-linear-publishing/artifacts/team-findings.md @@ -0,0 +1,120 @@ +# Team Findings: Publish the Linear Plugin to the Second Channel + + + +## Major findings + +### F1: The plugin's own README contradicts the standalone claim + +- **Agent:** junior-developer +- **Finding:** The spec's Outcome promised "the documented instructions all agree" and Primary Flow step 5 claims the + plugin works standalone, but `han-linear/README.md` (lines 8 and 16) says the plugin depends on `han-core` and that + its skills dispatch shared agents from `han-core` and `han-communication`. The skill's own content + (`han-linear/skills/work-items-to-linear/SKILL.md`) dispatches no agent and references neither plugin, so the README + appears stale and the spec's evidence (a grep of `skills/` only) was narrower than the claim it supported. +- **Resolution:** The Outcome's agreement claim was narrowed to the second channel's setup instructions, which are + accurate. The README discrepancy became Open Item OI-2, routed toward the cleanup's dependency-truth work (Phase 4 + of the outline), and D3's evidence field now records both sides of the contradiction. +- **Resolved by:** evidence +- **Affected decisions:** D3 +- **Affected tech-notes:** — +- **Changed in spec:** Outcome; Primary Flow; Open Items + +### F2: The first-time-publication precondition was silently dropped + +- **Agent:** junior-developer (also gap-analyzer, GAP-001) +- **Finding:** The parent outline's Phase 1 preconditions require confirming the second channel accepts a first-time + publication of a never-before-listed plugin, not only updates. The draft spec assumed install-by-name resolves once + the entry exists and never named this as a distinct check. +- **Resolution:** The precondition is now explicit in Actors and Triggers, the maintainer-verification install is + specified to start from an already-registered marketplace in which the plugin was previously absent (proving + first-time resolution and answering OI-1 in the same run), and a new edge-case row covers the channel refusing a + first-time listing. +- **Resolved by:** evidence +- **Affected decisions:** D2 +- **Affected tech-notes:** — +- **Changed in spec:** Actors and Triggers; Maintainer verification before ship; Edge Cases and Failure Modes + +### F3: Edge-case row restated untouched existing behavior (YAGNI) + +- **Agent:** junior-developer +- **Finding:** The row about running the skill without a configured Linear connection described pre-existing behavior + this feature does not touch. Completeness anti-pattern. +- **Resolution:** Row removed; replaced by a single Out of Scope bullet stating the plugin's runtime behavior is + untouched. +- **Resolved by:** evidence +- **Affected decisions:** — +- **Affected tech-notes:** — +- **Changed in spec:** Edge Cases and Failure Modes; Out of Scope + +### F4: Bundle edge-case row duplicated Out of Scope (YAGNI) + +- **Agent:** junior-developer +- **Finding:** The all-in-one bundle limitation appeared both as an edge-case row and an Out of Scope bullet, and it + belongs to the release-process phase, not the Linear listing. +- **Resolution:** Edge-case row removed; the single Out of Scope mention remains. +- **Resolved by:** evidence +- **Affected decisions:** — +- **Affected tech-notes:** — +- **Changed in spec:** Edge Cases and Failure Modes + +### F5: "Operates at the current released version" had no observable signal + +- **Agent:** junior-developer +- **Finding:** Running the skill surfaces no version, so the runtime half of the version claim could not be observed. +- **Resolution:** The version check is restated as the observable comparison it is: the installed manifest's stated + version equals the first channel's released version. Recorded as trivial decision D4. +- **Resolved by:** evidence +- **Affected decisions:** D4 (added) +- **Affected tech-notes:** — +- **Changed in spec:** Outcome; Primary Flow; Maintainer verification before ship + +### F6: Sibling-matching was the wrong correctness standard + +- **Agent:** junior-developer +- **Finding:** "Matches its sibling entries field for field" risks copying a known defect, since the outline's Phase 3 + exists precisely because sibling entries carry stale data. +- **Resolution:** The criterion is rephrased: the entry conforms to the channel's required entry shape, with sibling + similarity demoted to a sanity check. +- **Resolved by:** evidence +- **Affected decisions:** — +- **Affected tech-notes:** — +- **Changed in spec:** Maintainer verification before ship + +### F7: The pre-registration alternate flow rested on an unevidenced population (YAGNI) + +- **Agent:** junior-developer +- **Finding:** No documented evidence exists of users who registered the marketplace before the fix; verification + effort committed to that scenario was speculative on its own. +- **Resolution:** Simpler version kept: the maintainer's single end-to-end run starts from an already-registered + marketplace, so the scenario is covered at zero extra cost and OI-1 resolves in the same run. +- **Resolved by:** evidence +- **Affected decisions:** — +- **Affected tech-notes:** — +- **Changed in spec:** Alternate Flows and States; Open Items + +### F8: The Phase 7 dependency was downgraded to an exclusion + +- **Agent:** gap-analyzer (GAP-002) +- **Finding:** The outline states the automated completeness check cannot land green while this plugin is missing from + the channel; the draft spec listed the check only as out-of-scope work, losing the blocking relationship. +- **Resolution:** The Outcome now states that the cleanup's later automated completeness check can only land green once + this listing entry is in place. +- **Resolved by:** evidence +- **Affected decisions:** — +- **Affected tech-notes:** — +- **Changed in spec:** Outcome + +### F9: The version-parity commitment lacked a decision entry + +- **Agent:** gap-analyzer +- **Finding:** The spec committed to version parity between channels with no corresponding decision-log entry, leaving + the commitment lower-confidence than its siblings. +- **Resolution:** Added trivial decision D4 and linked it from every spec sentence that carries the commitment. +- **Resolved by:** evidence +- **Affected decisions:** D4 (added) +- **Affected tech-notes:** — +- **Changed in spec:** Outcome; Primary Flow; Edge Cases and Failure Modes + +## Minor edits diff --git a/docs/plans/han-publishing-cleanup/phase-1-linear-publishing/feature-specification.md b/docs/plans/han-publishing-cleanup/phase-1-linear-publishing/feature-specification.md new file mode 100644 index 00000000..ad7b7b71 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-1-linear-publishing/feature-specification.md @@ -0,0 +1,131 @@ +# Feature Specification: Publish the Linear Plugin to the Second Channel + +The Linear work-item plugin becomes installable from the second install channel. Today the channel advertises the +plugin but does not carry it, so a new user following the documented setup instructions hits an error instead of +succeeding; this fix closes that gap. + +## Outcome + +A new user who follows the second channel's setup instructions for the Linear plugin completes the installation and can +run its work-item publishing skill. The listing entry, the plugin's channel manifest, and the second channel's setup +instructions all agree with each other. The installed version equals the version released on the first channel +([D4](artifacts/decision-log.md#trivial-decisions)). + +The fix ships to users as soon as the current working branch merges to the default branch, because the second channel +serves the repository's default branch ([T1](artifacts/feature-technical-notes.md#t1-second-channel-serves-the-default-branch)) +([D2](artifacts/decision-log.md#d2-definition-of-done)). The cleanup's later automated completeness check can only land +green once this listing entry is in place. + +## Actors and Triggers + +- **Actors**: A new user installing Han's Linear plugin from the second channel; a maintainer verifying the listing + before ship. +- **Triggers**: The user follows the setup instructions in the project's front-door documentation: register the Han + marketplace, then install the Linear plugin by name. +- **Preconditions**: The user has the second channel's tooling installed and can reach the repository. The second + channel must accept a first-time publication of a plugin that was never listed there, not only updates to existing + listings. The maintainer's verification install proves this rather than assuming it + ([D2](artifacts/decision-log.md#d2-definition-of-done)). + +## Primary Flow + +1. The user registers the Han marketplace with the second channel's tooling, per the documented instructions. +2. The user installs the Linear plugin by name from that marketplace. +3. The installation succeeds: the plugin is listed in the channel's storefront listing and carries a complete channel + manifest, so the channel can resolve it ([D2](artifacts/decision-log.md#d2-definition-of-done)). +4. The user runs the plugin's work-item publishing skill and it works. The observable version check is the installed + manifest: its stated version equals the version released on the first channel + ([D4](artifacts/decision-log.md#trivial-decisions)). +5. The plugin works standalone: its skill content references no other plugin, so no companion installation is required + on a channel that resolves no dependencies + ([D3](artifacts/decision-log.md#d3-no-companion-install-instruction)). The plugin's own introduction page currently + overstates its reliance on other plugins. Reconciling that page is routed to [OI-2](#open-items). + +## Alternate Flows and States + +### Maintainer verification before ship + +- **Entry condition:** The listing entry and channel manifest exist on the working branch and are about to ship. +- **Sequence:** The maintainer takes three steps. First, the maintainer confirms the listing entry conforms to the + channel's required entry shape, using sibling entries only as a sanity check rather than the correctness standard. + Second, the maintainer confirms the channel manifest's version matches the first channel's released version + ([D4](artifacts/decision-log.md#trivial-decisions)). Third, the maintainer performs one real end-to-end install + following the documented instructions exactly, from a branch state equivalent to what will merge. That install + starts from an already-registered marketplace in which this plugin was previously absent. The single run proves + both that the channel resolves a first-time listing and that existing registrations pick it up, answering + [OI-1](#open-items). +- **Exit:** All three confirmations pass, and the branch is cleared to carry the fix to the default branch. + +### User registered the marketplace before the fix shipped + +- **Entry condition:** A user added the Han marketplace while the Linear plugin was still missing from the listing. +- **Sequence:** After the fix reaches the default branch, the user refreshes or re-reads the marketplace and installs + the Linear plugin by name. +- **Exit:** The installation succeeds. The exact refresh step the channel requires is Open Item + [OI-1](#open-items). + +## Edge Cases and Failure Modes + +| Condition | Required Behavior | +| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| The install is attempted before the fix reaches the default branch | The user still sees today's not-found error; the spec's outcome is only promised from the merge onward ([T1](artifacts/feature-technical-notes.md#t1-second-channel-serves-the-default-branch)). | +| The channel manifest's version drifts from the first channel's version | Verification fails and the ship is held until the two versions match; today both channels state the same released version ([D4](artifacts/decision-log.md#trivial-decisions)). | +| The channel refuses a first-time listing and accepts only updates | The maintainer's verification install surfaces this before ship, and the ship holds until the channel resolves the new entry. | + +## User Interactions + +- **Affordances:** The front-door documentation's setup instructions for the second channel, which already name the + Linear plugin as an opt-in install. +- **Feedback:** The channel's tooling confirms a successful installation; the plugin's skill then appears by name. +- **Error states:** Today the same instructions end in a not-found error; after ship, that error no longer occurs for + this plugin. + +## Coordinations + +| Coordinating System | Direction | Interaction | Ordering / Consistency Requirement | +| ------------------------------------ | --------- | ------------------------------------------------------------------ | --------------------------------------------------------------------- | +| Second channel's storefront listing | outbound | Lists the Linear plugin so the channel's tooling can resolve it | Entry must be present and conform to the channel's required entry shape; sibling entries are a sanity check only. | +| Repository default branch | outbound | Serves the listing and manifest to every user of the second channel | The fix is user-visible only after merge ([T1](artifacts/feature-technical-notes.md#t1-second-channel-serves-the-default-branch)). | +| First install channel | inbound | Supplies the released version the second channel must match | Versions agree at ship time; both state the same released version. | + +## Out of Scope + +- Correcting the other plugins' frozen version numbers on the second channel. That is Phase 3 of the + [build phase outline](../build-phase-outline.md#phase-3). +- Teaching the release process about all four publishing surfaces (Phase 6) and the automated completeness check + (Phase 7). +- Publishing the all-in-one bundle to the second channel. The channel does not support bundles; the limitation is + documented and permanent until the channel changes. +- A hotfix that carries the listing fix to the default branch ahead of the working branch's merge + ([D2](artifacts/decision-log.md#d2-definition-of-done)). +- Changing any of the Linear plugin's runtime behavior. This phase publishes what already exists; the skill itself is + untouched. + +## Open Items + +- **OI-1:** Confirm what an already-registered marketplace user must do, if anything, for the newly listed plugin to + appear: an explicit refresh command, or automatic pickup on next read. + - **Resolves when:** The maintainer's end-to-end verification runs from an already-registered marketplace, per the + maintainer-verification flow, or the channel's documentation answers it first. + - **Blocks implementation:** No. It shapes the verification script, not the listing change, which already exists. +- **OI-2:** The Linear plugin's introduction page says its skills rely on other plugins. Its skill content references + none. Reconcile the page with the truth so the standalone claim + ([D3](artifacts/decision-log.md#d3-no-companion-install-instruction)) and the documentation agree. + - **Resolves when:** The page is corrected, or evidence surfaces that the reliance is real. Feeds the cleanup's + dependency-truth work ([Phase 4 of the outline](../build-phase-outline.md#phase-4)). + - **Blocks implementation:** No. The listing fix and install verification stand regardless. + +## Summary + +- **Outcome delivered:** The Linear plugin installs successfully from the second channel, at the current released + version, once the working branch merges. +- **Primary actors:** A new user on the second channel; a maintainer verifying before ship. +- **Decisions settled by evidence:** 2. See [artifacts/decision-log.md](artifacts/decision-log.md) +- **Decisions settled by user input:** 2. See [artifacts/decision-log.md](artifacts/decision-log.md) +- **Sub-agents consulted:** junior-developer, gap-analyzer. See + [artifacts/team-findings.md](artifacts/team-findings.md) +- **Key adjustments from review:** The first-time-publication precondition was made explicit and folded into the + verification install. The version check was restated as the observable manifest comparison. A contradiction in the + plugin's own introduction page became OI-2. See [artifacts/team-findings.md](artifacts/team-findings.md) +- **Remaining open items:** 2 +- **Technical notes:** 1. See [artifacts/feature-technical-notes.md](artifacts/feature-technical-notes.md) diff --git a/docs/plans/han-publishing-cleanup/phase-2-tracker-labeled-marks/artifacts/decision-log.md b/docs/plans/han-publishing-cleanup/phase-2-tracker-labeled-marks/artifacts/decision-log.md new file mode 100644 index 00000000..3caee435 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-2-tracker-labeled-marks/artifacts/decision-log.md @@ -0,0 +1,141 @@ +# Decision Log: Label Every Tracker's Marks and Close the Silent Gap + +## Trivial decisions + +- D1: Spec location — the spec lives at `docs/plans/han-publishing-cleanup/phase-2-tracker-labeled-marks/`, nested + beside the build phase outline that spawned it. — Referenced in spec: none (organizational). +- D5: Closing report accounts for every slice by category — published, skipped as already published here, another + tracker's, or upgraded from the old format (considered keeping each publisher's existing count-only report; rejected + because the outline's Phase 2 outcome requires every item accounted for, not only a skip count). — Referenced in + spec: Primary Flow. +- D8: The stop-on-marking-failure protection becomes a required behavior of all three publishers — an item created on + a tracker whose mark cannot be written stops the run with the orphan reported (generalizes the Linear publisher's + existing written rule; the GitHub and Jira publishers have no such rule today, so for them this is a new commitment, + not a preserved one). — Driven by findings: F4. — Referenced in spec: Alternate Flows and States; Coordinations. +- D9: A dependency pointing at a slice published to a different tracker is skipped as a single relation and reported; + the linking pass never fails the whole run over it and never drops it silently (considered failing the run; + rejected because one cross-tracker relation would then block every within-tracker link). — Driven by findings: F10. + — Referenced in spec: Edge Cases and Failure Modes. + +## Full decisions + +### D2: Full-fix scope + +- **Question:** Does this phase adopt tracker-labeled marks across all three publishers, or only close the GitHub + publisher's silent gap? +- **Decision:** The full fix. All three publishers write marks that name their tracker, and all three change together + because the mark format is one shared contract on the work-items file. +- **Rationale:** The user resolved this as OQ-1 in the build phase outline on 2026-07-21, choosing the fix the source + analysis itself named. A GitHub-only change would leave the Jira and Linear publishers able to mistake each other's + marks and skip work that was never published to their tracker. +- **Evidence:** User input (OQ-1, `../build-phase-outline.md#oq-1`, resolved 2026-07-21). Codebase: the Jira + publisher's annotation shape (`## () — `, `han-atlassian/skills/work-items-to-jira/SKILL.md` + lines 127-128) and the Linear publisher's (`## <SYM-N> (<LINEAR-ID>) — <title>`, + `han-linear/skills/work-items-to-linear/SKILL.md` lines 117-118) are the same letter-key-number shape and cannot be + told apart; the GitHub publisher's is `(#NNN)` + (`han-github/skills/work-items-to-issues/SKILL.md` lines 71-75). +- **Rejected alternatives:** + - GitHub-only detection fix — rejected by the user in OQ-1 because it leaves the cross-tracker skip trap between the + other two publishers in place. +- **Linked technical notes:** — +- **Driven by findings:** — +- **Dependent decisions:** D3, D4, D5, D6, D7 +- **Referenced in spec:** Outcome; Primary Flow; Coordinations + +### D3: Mark-recognition taxonomy + +- **Question:** What must a publisher do with each kind of mark it can meet in a file? +- **Decision:** Every slice is classified before anything is created: no mark (publish and mark), own current mark + (skip and count), own old-format mark (upgrade path), another tracker's mark (report, never repair, never publish + without the person's explicit go-ahead), unrecognizable mark (treated exactly like another tracker's mark). Nothing + falls outside these categories, so nothing can vanish from a run. +- **Rationale:** The GitHub publisher already has a written rule that a foreign annotation is "a distinct category from + a malformed heading and is never repaired" with stop-and-report handling; this decision generalizes that rule to all + three publishers and makes the unrecognized case explicit, closing the hole where unmatched marks fell through every + pattern. +- **Evidence:** Codebase: `han-github/skills/work-items-to-issues/SKILL.md` lines 73-75 and 93-100 (the + never-repaired, stop-and-report rule and the note that an unparseable heading "may be another tracker's annotation in + a shape you do not know"). Source analysis: the silent-vanish finding in + `../source-han-cleanup-plan.md` ("The shared ticket file"). +- **Rejected alternatives:** + - Treating unrecognized marks as malformed headings to repair — rejected because repairing an annotation that + records a publication elsewhere would publish duplicates; the existing written rule names exactly this danger. +- **Linked technical notes:** — +- **Driven by findings:** F3 (the classification is asymmetric: only the tracker whose old shape is unique can be + recognized as "own old-format"; the two shape-identical trackers always reach the ask), F7 (a near-miss annotation + gets a proposed manual correction, never an automatic repair) +- **Dependent decisions:** D4, D7 +- **Referenced in spec:** Primary Flow; Alternate Flows and States; Edge Cases and Failure Modes + +### D4: Migration behavior + +- **Question:** What happens to files marked up in the old, unlabeled format? +- **Decision:** Old marks whose shape identifies their tracker without doubt are upgraded in place, with each upgrade + reported. Old marks whose shape could belong to more than one tracker stop the run, and the publisher asks the + person which tracker owns each one. The publisher never assigns a tracker by guess. +- **Rationale:** The outline commits the migration path to "stops and asks rather than guesses." Upgrading an + unambiguous mark is not guessing: the old number-sign shape belongs to exactly one tracker, while the old + letter-key shape is shared by two trackers and is exactly the ambiguity that caused the original bug. +- **Evidence:** User input (OQ-1 resolution and the outline's Phase 2 entry, `../build-phase-outline.md#phase-2`). + Codebase: the three mark shapes cited under D2 show the asymmetry — `(#NNN)` is unique to the GitHub publisher; + `(<KEY>)` and `(<LINEAR-ID>)` are the same shape. +- **Rejected alternatives:** + - Stop and ask for every old mark, including unambiguous ones — rejected because it adds interruptions without + protecting anything; the simpler version satisfies the same evidence. This deliberately refines the outline's + blanket "stops and asks" wording: upgrading a mark whose owner is certain is not guessing, which is what the + outline's wording guards against. + - Auto-assign ambiguous marks by probing both trackers — rejected because a lookup that happens to match in both + trackers still cannot decide ownership, and the outline requires asking over guessing. + - Only the owning tracker's publisher may upgrade its old marks — rejected because a team that stopped using that + tracker would then carry old-format marks forever; naming a certain owner is safe from any publisher. +- **Linked technical notes:** — +- **Driven by findings:** F3, F6 (migration is the expected first encounter for every file published before this + phase, not an edge case), F7 (key text that collides with a tracker name or another tracker's key is treated as + ambiguous and held for the ask), F12 (the divergence from the outline's blanket wording is recorded here) +- **Dependent decisions:** — +- **Referenced in spec:** Outcome; Alternate Flows and States; Edge Cases and Failure Modes + +### D6: One shared publishing record + +- **Question:** The GitHub publisher records its marks only in derived per-repo copies today, leaving the shared file + untouched; the other two publishers mark the shared file in place. Where does the publishing record live? +- **Decision:** The shared work-items file is the complete publishing record for all three trackers. The GitHub + publisher changes to also write its tracker-labeled marks into the shared file; its derived per-repo copies stay + internal. +- **Rationale:** The whole fix rests on one contract all three publishers read and write. With two carriers, the + shared file could never answer "was this published to the GitHub tracker?", and cross-tracker classification would + stay asymmetric forever. +- **Evidence:** User input (2026-07-21). Codebase: `han-github/skills/work-items-to-issues/SKILL.md` Step 5 states the + source file is not modified by the publish step, while the Jira and Linear skills edit the source file in place + (their Step 6 annotation passes). +- **Rejected alternatives:** + - Model two carriers (shared file plus derived copies) — rejected by the user because the shared file could never + answer whether a slice was published to the GitHub tracker, defeating the one-contract goal. +- **Linked technical notes:** — +- **Driven by findings:** F1 +- **Dependent decisions:** D7 +- **Referenced in spec:** Outcome; Primary Flow; Coordinations + +### D7: Pause and ask + +- **Question:** When a run finds marks belonging to another tracker (or ambiguous or unrecognized marks), does it + pause for an in-run decision or stop outright for a hand-edit and re-run? +- **Decision:** The run classifies everything first, pauses once, presents the full picture with each raw mark text, + and asks one question. The person chooses: publish only the unmarked slices, also publish another tracker's slices + here, or stop. Nothing is created before the answer. +- **Rationale:** The publishers run inside a conversation, so an informed in-run question is natural and cheaper than + a hand-edit-and-re-run loop. Presenting the whole picture before anything is created keeps the safety of the old + hard stop while removing its friction. Showing the raw mark text lets the person double-check before an + irreversible answer. +- **Evidence:** User input (2026-07-21). Codebase: the GitHub publisher's current hard-stop rule + (`han-github/skills/work-items-to-issues/SKILL.md` Step 3: only Stop is offered for foreign annotations) is the + behavior this deliberately replaces. +- **Rejected alternatives:** + - Keep the hard stop with hand-edit and re-run — rejected by the user for friction; the pause preserves the same + protection (nothing created before a decision) without the re-run loop. + - Publish unmarked slices unconditionally and only report the rest — rejected because it removes the person's + chance to catch a wrong classification before items are created. +- **Linked technical notes:** — +- **Driven by findings:** F2 +- **Dependent decisions:** — +- **Referenced in spec:** Primary Flow; Edge Cases and Failure Modes diff --git a/docs/plans/han-publishing-cleanup/phase-2-tracker-labeled-marks/artifacts/gap-analysis-scratch.md b/docs/plans/han-publishing-cleanup/phase-2-tracker-labeled-marks/artifacts/gap-analysis-scratch.md new file mode 100644 index 00000000..97d309e1 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-2-tracker-labeled-marks/artifacts/gap-analysis-scratch.md @@ -0,0 +1,129 @@ +# Gap Analysis: Phase 2 Build-Phase Outline vs. Draft Feature Specification + +## Comparison Direction + +Current state: the draft feature specification at +`docs/plans/han-publishing-cleanup/phase-2-tracker-labeled-marks/feature-specification.md`, read together with its +`artifacts/decision-log.md`. Desired state: the Phase 2 entry (`{#phase-2}`) of +`docs/plans/han-publishing-cleanup/build-phase-outline.md` — "Label every tracker's marks and close the silent gap" — +including its "What we build," 5-step "Outcome to demonstrate," "Source citations," and two "Preconditions to verify +before starting," plus the outline's OQ-1 resolution (`{#oq-1}`) and departure 3 (`{#departures}`). + +Comparison direction used: desired state (outline) toward current state (spec), per protocol default. A secondary, +explicitly requested bidirectional check (spec additions not present in the outline, and whether the decision log +evidences them) is reported separately in "Additions Beyond the Outline" and is not counted in the taxonomy table. + +## Scope + +Comparison areas analyzed: the Phase 2 "What we build" narrative, all 5 numbered demonstration steps, both source +citations, the two preconditions, the OQ-1 resolution text, and departure 3's text. These were walked against the +spec's Outcome, Actors and Triggers, Primary Flow, Alternate Flows and States, Edge Cases, Coordinations, Out of Scope, +and Open Items sections, cross-referenced against every decision (D1-D5) in the decision log. + +Excluded from analysis: the outline's "Why this is Phase 2," "Builds on," and "Connects to" prose, since these are +sequencing rationale rather than behavior the spec is required to implement. Phases 1, 3-8 and OQ-2 are out of scope; +they do not bear on Phase 2. Analysis is behavioral only — no code was read or assessed. + +## Actors and Modes Observed + +The outline and spec both address a single actor type: "a person" (or "a real user") running one of three named +work-item publisher skills (GitHub, Jira, Linear) against a shared work-items file, interactively, one run at a time. +No sub-roles are named. No batch/automated mode is described — every run is a person-invoked skill. No API or +agent-to-agent surface is named; the file itself is described as a passive, shared coordination point between runs, +not an actor. None of this differs between the two documents. + +## Summary + +Compared the Phase 2 entry of the build-phase outline (desired state) against the draft feature specification and its +decision log (current state), walking all 5 demonstration steps, both preconditions, the OQ-1 resolution, and +departure 3. Comparison direction: desired state (outline) toward current state (spec), per protocol default. + +| Category | Count | Description | +|----------|-------|-------------| +| Missing | 1 | Elements in desired state with no current state correspondence | +| Partial | 0 | Elements present in both but incompletely covered | +| Divergent | 1 | Elements addressing same concern in incompatible ways | +| Implicit | 0 | Assumed capabilities neither confirmed nor denied | + +A separate, explicitly requested check of spec additions beyond the outline (bidirectional) found 3 items; see +"Additions Beyond the Outline" below. + +Full analysis written to: `/Users/riverbailey/dev/testdouble/han/docs/plans/han-publishing-cleanup/phase-2-tracker-labeled-marks/artifacts/gap-analysis-scratch.md` + +## Findings + +**GAP-001: Old-format migration is bifurcated in the spec but stated as unconditional in the outline** + +- **Category:** Divergent +- **Feature/Behavior:** What happens when a publisher meets a file marked in the old, unlabeled format. +- **Current State:** `feature-specification.md`, "Alternate Flows and States" > "Old-format marks found in the file" + (lines 46-54): "Marks whose old shape identifies their tracker without doubt are upgraded in place to the labeled + format, and the run reports each upgrade" (no stop, no ask) — only "marks whose old shape could belong to more than + one tracker stop the run" and prompt the person. `artifacts/decision-log.md` D4 ("Migration behavior," lines 60-80) + makes this an explicit, evidenced decision: unambiguous old marks are silently auto-upgraded; only ambiguous marks + stop and ask. +- **Desired State:** `build-phase-outline.md#phase-2`, "What we build" (line 223): "Files marked up in the old format + get an upgrade path that stops and asks rather than guesses" — stated without qualification. The same unconditional + claim is repeated twice more: demo step 5 (line 237), "Feed in a file marked up in the old format and confirm the + run stops and asks instead of guessing," and OQ-1's resolution (`#oq-1`, lines 509-510), "files marked up the old + way get an upgrade path that stops and asks rather than guesses," and departure 3 (`#departures`, lines 152-153), + "old-format files get an upgrade path that stops and asks rather than guesses." +- **Note:** The decision log offers a rationale for the bifurcation (D4: "Upgrading an unambiguous mark is not + guessing"), and this rationale is not unreasonable. But taken literally, demo step 5 as written cannot be + demonstrated on a file carrying only unambiguous old marks (e.g., a GitHub-only old-format file, per D2/D4's own + evidence that the GitHub shape `(#NNN)` is unique and unambiguous): such a file would be silently auto-upgraded, not + stopped and asked about. Three separate places in the outline state the "stops and asks" behavior without the + ambiguous/unambiguous distinction the spec introduces, so this is a same-concern, incompatible-approach conflict, + not merely an implementation detail. + +**GAP-002: Second Phase 2 precondition is absent from the spec** + +- **Category:** Missing +- **Feature/Behavior:** Pre-start verification that no known user files would be stranded by the old-format upgrade + path. +- **Current State:** `feature-specification.md`, "Actors and Triggers" > "Preconditions" (lines 22-25) states only one + precondition — the OQ-3 safety-net trial. `Open Items` (OI-1, lines 99-105) likewise addresses only the safety-net + trial. No section of the spec or the decision log (`artifacts/decision-log.md`) mentions stranded files, orphaned + old-format files, or a check of known user files against the upgrade path. +- **Desired State:** `build-phase-outline.md#phase-2`, "Preconditions to verify before starting" (line 253): "Confirm + no known user files in the old mark format would be stranded by the upgrade path's stop-and-ask behavior." +- **Note:** Validated by re-reading the full spec and decision log for any indirect coverage (e.g., under "Edge Cases," + "Out of Scope," or "Open Items"); no correspondence was found anywhere. Given GAP-001's bifurcation, this precondition + is also harder to satisfy as originally worded, since "stranded by stop-and-ask" no longer describes every old-format + file under the spec's design — but its absence is a Missing gap regardless of that complication. + +## Additions Beyond the Outline + +Explicitly requested bidirectional check: current-state (spec) elements with no desired-state (outline) correspondence, +each with its decision-log evidence status. + +1. **Unrecognizable-mark classification category** — `feature-specification.md`, "Alternate Flows and States" > "A + mark the publisher cannot recognize" (lines 56-62), and D3 in the decision log (lines 37-58). The outline names only + two ticket-file problems (GitHub's silent drop, and the other two publishers mistaking each other's marks); it does + not name a third case of an unrecognized annotation shape. **Evidenced:** yes — D3 cites the GitHub publisher's + existing written rule (`han-github/skills/work-items-to-issues/SKILL.md` lines 73-75, 93-100) as the basis for + generalizing this category to all three publishers. + +2. **"Upgraded from the old format" as a fourth closing-report category** — `feature-specification.md`, Primary Flow + step 6 (lines 40-42), and D5 in the decision log (lines 7-10). The outline's Phase 2 "What we build" names three + report categories (published, skipped with a count, reported as belonging to another tracker); the spec's closing + report adds a fourth. **Evidenced:** yes — D5 records the decision and rejects a count-only report as insufficient + against the outline's own accounting requirement. + +3. **"Publishing succeeds but marking fails" recovery flow** — `feature-specification.md`, "Alternate Flows and + States" > "Publishing succeeds but marking fails" (lines 64-70). This describes new behavior (stopping the run and + reporting an orphaned tracker item when a mark write fails after a tracker accepts the item) that has no + correspondence anywhere in the outline's Phase 2 entry, OQ-1, or departure 3. **Evidenced:** no — this section cites + no decision-log entry (no `D`-number reference) and no `artifacts/decision-log.md` entry addresses mark-write + failure. This is an un-evidenced addition. + +## Areas Needing Separate Analysis + +- **Source citations fidelity.** The spec's decision log cites the same source-artifact sections the outline's Phase 2 + entry cites (`the shared ticket file` finding, `what I would do, in order` item 2). A full check of whether every + claim in the spec traces back to those source sections (versus the outline's own text) was not performed line by + line and would benefit from a dedicated citation audit if the team wants that level of rigor. +- **OI-1's "blocks implementation: No" claim.** The spec states the pre-start safety-net trial does not block + implementation because "the target behavior in this spec is the same either way." Given GAP-001, this claim may need + re-examination once the stranded-files precondition (GAP-002) is addressed, since the trial's outcome and the + stranding question are related but were not evaluated together here. diff --git a/docs/plans/han-publishing-cleanup/phase-2-tracker-labeled-marks/artifacts/team-findings.md b/docs/plans/han-publishing-cleanup/phase-2-tracker-labeled-marks/artifacts/team-findings.md new file mode 100644 index 00000000..b3f75855 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-2-tracker-labeled-marks/artifacts/team-findings.md @@ -0,0 +1,161 @@ +# Team Findings: Label Every Tracker's Marks and Close the Silent Gap + +<!-- Findings from the review team, recorded as F# entries. Major findings carry the full field set; minor edits are +one-line bullets. The F# counter is shared across both sections. --> + +## Major findings + +### F1: The single-shared-file model did not hold for the GitHub publisher + +- **Agent:** junior-developer +- **Finding:** The draft treated one shared file as every publisher's mark carrier, but the GitHub publisher's publish + step does not modify the source file; it marks derived per-repo copies. The classification story rested on a + shared-carrier assumption true for only two of three publishers. +- **Resolution:** The user chose to unify: the GitHub publisher will also write its labeled marks into the shared + file, which becomes the complete record for all three trackers. Recorded as D6. +- **Resolved by:** user input +- **Affected decisions:** D6 (added), D2 +- **Affected tech-notes:** — +- **Changed in spec:** Outcome; Primary Flow; Coordinations + +### F2: Foreign marks — pause for a decision, or hard stop + +- **Agent:** junior-developer (with edge-case-explorer's stop-scope contradiction) +- **Finding:** The draft was ambiguous between a partial run and the GitHub publisher's current whole-run hard stop, + and its alternate flow and edge table used conflicting stop language. +- **Resolution:** The user chose pause-and-ask: classify everything, present the full picture once with raw mark + texts, and take one answer before creating anything. Recorded as D7; flow and edge table made consistent. +- **Resolved by:** user input +- **Affected decisions:** D7 (added), D3 +- **Affected tech-notes:** — +- **Changed in spec:** Primary Flow; Alternate Flows and States; Edge Cases and Failure Modes + +### F3: The "own old-format mark" branch was unreachable for two publishers + +- **Agent:** junior-developer (with edge-case-explorer's foreign-unambiguous question) +- **Finding:** The two trackers with shape-identical old marks can never classify an old mark as their own, so the + five-way taxonomy was asymmetric without saying so; and it was undefined whether a publisher may upgrade another + tracker's unambiguous old mark. +- **Resolution:** The asymmetry is stated in the migration flow, and any publisher may upgrade an unambiguous old mark + regardless of owner, since naming a certain owner is not guessing; without this, a team that left a tracker would + carry old marks forever. +- **Resolved by:** evidence +- **Affected decisions:** D3, D4 +- **Affected tech-notes:** — +- **Changed in spec:** Alternate Flows and States + +### F4: The marking-failure stop was framed as preserved but exists only in one publisher + +- **Agent:** junior-developer (gap-analyzer flagged the same flow as unevidenced) +- **Finding:** Stop-on-marking-failure is written down only in the Linear publisher; for the GitHub and Jira + publishers it is a new commitment, not a preserved one, and it had no decision entry. +- **Resolution:** Reworded as a required behavior generalizing the Linear publisher's rule; recorded as trivial + decision D8. +- **Resolved by:** evidence +- **Affected decisions:** D8 (added) +- **Affected tech-notes:** — +- **Changed in spec:** Alternate Flows and States + +### F5: The silent vanish was asserted as fact while OI-1 calls it unverified + +- **Agent:** junior-developer +- **Finding:** The outcome asserted items vanish silently; the GitHub publisher's written instructions already mandate + stop-and-report, and the pre-start trial is what settles reality. If the trial shows the stop works, the phase + shrinks to labeling, the unified record, and migration. +- **Resolution:** OI-1 now says exactly that; the outcome describes the target behavior without asserting today's + failure mode. +- **Resolved by:** evidence +- **Affected decisions:** — +- **Affected tech-notes:** — +- **Changed in spec:** Outcome; Open Items + +### F6: Migration is the default first encounter, not an edge case + +- **Agent:** junior-developer (F8), gap-analyzer (GAP-002) +- **Finding:** Every file published before this phase carries old-format marks, so migration is the expected path for + all existing files; and the outline's second precondition (confirm no old-format user files would be stranded) was + missing from the spec. +- **Resolution:** The migration flow is reframed as the expected first encounter; the stranded-files check is now a + precondition and OI-2, resolved by accepting ask-first as the safe default for files that cannot be counted. +- **Resolved by:** evidence +- **Affected decisions:** D4 +- **Affected tech-notes:** — +- **Changed in spec:** Actors and Triggers; Alternate Flows and States; Open Items + +### F7: Key text can collide with tracker names and near-miss labels can loop + +- **Agent:** edge-case-explorer +- **Finding:** Tracker keys are user-chosen, so an old mark's key can read as a tracker's name or another tracker's + key, risking misclassification and a consented duplicate; and a hand-typed near-miss label would be reported forever + with no path out under a strict never-repair rule. +- **Resolution:** Colliding or two-ways-readable text is treated as ambiguous and held for the ask; near-miss + annotations get a proposed manual correction in the report, never an automatic repair. +- **Resolved by:** evidence +- **Affected decisions:** D3, D4 +- **Affected tech-notes:** — +- **Changed in spec:** Alternate Flows and States; Edge Cases and Failure Modes + +### F8: Mark loss by hand-edit or merge silently produces duplicates + +- **Agent:** edge-case-explorer +- **Finding:** The mark is the only record of publication; a mark lost to a hand edit or merge resolution makes the + slice look unpublished, and the next run duplicates it with no signal. +- **Resolution:** Named as an accepted limitation of a file-carried record, in Out of Scope, rather than left + implicit. +- **Resolved by:** evidence +- **Affected decisions:** — +- **Affected tech-notes:** — +- **Changed in spec:** Out of Scope + +### F9: Diverged copies published concurrently produce undetectable orphans + +- **Agent:** edge-case-explorer +- **Finding:** Two people publishing diverged copies of the same file and merging afterward leaves one tracker's + created items orphaned, with nothing in the file to detect them. +- **Resolution:** Named as an accepted limitation in Out of Scope. +- **Resolved by:** evidence +- **Affected decisions:** — +- **Affected tech-notes:** — +- **Changed in spec:** Out of Scope + +### F10: Cross-tracker dependencies had no defined linking behavior + +- **Agent:** edge-case-explorer +- **Finding:** When a slice's blocker was published to a different tracker, the linking pass had no stated behavior: + a crash or a silently dropped relation, depending on publisher. +- **Resolution:** The linking pass skips that one relation and reports it; it never fails the run and never drops the + relation silently. Recorded as trivial decision D9. +- **Resolved by:** evidence +- **Affected decisions:** D9 (added) +- **Affected tech-notes:** — +- **Changed in spec:** Edge Cases and Failure Modes + +### F11: Stale marks pointing at deleted tracker items keep slices skipped forever + +- **Agent:** edge-case-explorer +- **Finding:** Classification trusts a mark's presence without checking the item still exists, so a deleted tracker + item leaves its slice permanently unpublished with no signal outside the one publisher's link-pass check. +- **Resolution:** Named as an accepted limitation in Out of Scope; classification-time liveness checks are not added. +- **Resolved by:** evidence +- **Affected decisions:** — +- **Affected tech-notes:** — +- **Changed in spec:** Out of Scope + +### F12: The migration behavior diverges from the outline's blanket wording + +- **Agent:** gap-analyzer (GAP-001) +- **Finding:** The outline says three times, unqualified, that old files "get an upgrade path that stops and asks"; + the spec upgrades unambiguous marks without asking, so the outline's demo step cannot be shown on a file carrying + only unambiguous old marks. +- **Resolution:** The divergence is deliberate and now recorded inside D4's rejected alternatives: upgrading a mark + whose owner is certain is not guessing, which is what the outline's wording protects against. +- **Resolved by:** evidence +- **Affected decisions:** D4 +- **Affected tech-notes:** — +- **Changed in spec:** Alternate Flows and States + +## Minor edits + +- F13: The shared file was listed as an actor; removed, it is a coordinating system — junior-developer — Actors and + Triggers +- F14: Summary placeholders replaced with the consulted agents and key adjustments — junior-developer — Summary diff --git a/docs/plans/han-publishing-cleanup/phase-2-tracker-labeled-marks/feature-specification.md b/docs/plans/han-publishing-cleanup/phase-2-tracker-labeled-marks/feature-specification.md new file mode 100644 index 00000000..9f215882 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-2-tracker-labeled-marks/feature-specification.md @@ -0,0 +1,154 @@ +# Feature Specification: Label Every Tracker's Marks and Close the Silent Gap + +All three work-item publishers record tracker-identifying marks in the one shared work-items file. This way, no +publisher can mistake another tracker's published work for its own, and no work item can leave a publishing run +unaccounted for. + +## Outcome + +A person can publish the same work-items file to any of the three trackers, in any order. Every slice in the file is +accounted for on every run. + +Each publisher writes marks that name its own tracker. The shared file becomes the complete publishing record for all +three trackers, including the GitHub publisher, which today records its marks only in derived copies +([D6](artifacts/decision-log.md#d6-one-shared-publishing-record)). + +Every run classifies all marks before creating anything, then tells the person what it published, what it skipped, and +what belongs to another tracker ([D2](artifacts/decision-log.md#d2-full-fix-scope)). + +Files marked before this phase shipped get a migration path that asks rather than guesses +([D4](artifacts/decision-log.md#d4-migration-behavior)). + +## Actors and Triggers + +- **Actors**: A person running any of the three work-item publishers (GitHub, Jira, or Linear) against a shared + work-items file. +- **Triggers**: The person invokes a publisher skill against a work-items file. The file may be unpublished, already + published to this tracker, already published to another tracker, marked in the format that predates this phase, or + any mix. +- **Preconditions**: The work-items file exists in the format the planning skill produces. Two checks run before this + phase starts. First is the safety-net trial from the outline's resolved open question: feed a file marked by another + tracker through the GitHub publisher in a throwaway project, and record whether the run stops at its checking step, + as its instructions say ([OI-1](#open-items)). Second, confirm no known user files in the old mark format would be + stranded by the migration path's ask-first behavior ([OI-2](#open-items)). + +## Primary Flow + +1. The person runs a publisher against a work-items file. +2. The publisher classifies every slice's mark before creating anything: no mark, its own labeled mark, an old-format + mark, another tracker's labeled mark, or a mark it cannot recognize + ([D3](artifacts/decision-log.md#d3-mark-recognition-taxonomy)). +3. If classification finds anything other than "no mark" or "its own labeled mark", the run pauses and presents the + full picture in one place. That picture shows what would publish, what is already published here, what belongs to + another tracker, and what is ambiguous or unrecognized, each with the raw mark text so the person can double-check. + The person answers once: publish only the unmarked slices, also publish another tracker's slices here, or stop. + Nothing is created before the answer ([D7](artifacts/decision-log.md#d7-pause-and-ask)). +4. Slices approved for publishing are created on this tracker, and each published slice's heading in the shared file + gains a mark naming this tracker ([D6](artifacts/decision-log.md#d6-one-shared-publishing-record)). +5. Slices with this tracker's own labeled mark are skipped, and the skip count is reported, preserving each + publisher's resume-on-re-run behavior. +6. The run's closing report accounts for every slice in the file by category: published, skipped as already published + here, another tracker's, or upgraded from the old format ([D5](artifacts/decision-log.md#trivial-decisions)). + +## Alternate Flows and States + +### First run against a file published before this phase shipped + +Every file published before this phase carries old-format marks, so this flow is the expected first encounter for +existing files, not a rarity. + +- **Entry condition:** The file carries marks in the old, unlabeled format. +- **Sequence:** Old marks whose shape identifies their tracker without doubt are upgraded in place to the labeled + format by whichever publisher meets them, and the run reports each upgrade. Naming the owner outright is not + guessing ([D4](artifacts/decision-log.md#d4-migration-behavior)). Old marks whose shape could belong to more than + one tracker are held for the pause-and-ask step: the publisher shows each ambiguous mark's raw text and asks the + person which tracker owns it. This classification is asymmetric by nature. Only the tracker whose old shape is + unique can be identified without asking; the two trackers whose old shapes are identical always require the ask. +- **Exit:** Every mark in the file is in the labeled format, and the run proceeds with the primary flow's + classification. + +### A mark the publisher cannot recognize + +- **Entry condition:** A slice heading carries an annotation that matches no known mark shape, current or old. +- **Sequence:** The publisher treats it exactly like another tracker's mark: never repaired automatically, shown to + the person with the annotation text it found, and excluded from publishing until the person decides. When the + annotation is a near-miss of a known shape, the report proposes the manual correction, but never applies it + ([D3](artifacts/decision-log.md#d3-mark-recognition-taxonomy)). +- **Exit:** The person resolves the annotation; the run never guesses. + +### Publishing succeeds but marking fails + +This protection exists today only in the Linear publisher. This phase makes it a required behavior of all three +([D8](artifacts/decision-log.md#trivial-decisions)). + +- **Entry condition:** A tracker accepts a new item, but writing the mark back to the shared file fails. +- **Sequence:** The run stops immediately and reports the orphaned tracker item so the person can mark the heading by + hand or delete the item. It does not continue creating items. +- **Exit:** The person reconciles the file with the tracker before re-running. + +## Edge Cases and Failure Modes + +| Condition | Required Behavior | +| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| The file carries marks from two or three different trackers at once | Each mark is classified by its named tracker; the pause presents them grouped per tracker; nothing is dropped. | +| The same file is published to a second tracker on purpose | The pause step's explicit go-ahead publishes the other tracker's slices here as well; without it, they are left alone. | +| A file mixes old-format and labeled marks | Migration runs first, so classification always operates on labeled marks only. | +| An old mark's key text collides with a tracker's name or another tracker's key | The mark is treated as ambiguous and held for the ask; text that could read two ways is never classified on shape alone ([D4](artifacts/decision-log.md#d4-migration-behavior)). | +| A slice's dependency points at a slice published to a different tracker | The dependency-linking pass skips that one relation and reports it; it never fails the run and never drops the relation silently ([D9](artifacts/decision-log.md#trivial-decisions)). | +| A re-run after a partial failure | Already-marked slices are skipped with a count; approved unmarked slices publish; the file ends consistent with the tracker. | +| The person cannot say which tracker owns an ambiguous mark | That slice stays unresolved and unpublished; the publisher never assigns a tracker by guess. | + +## Coordinations + +| Coordinating System | Direction | Interaction | Ordering / Consistency Requirement | +| ---------------------------------------- | --------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| The shared work-items file | both | Carries the complete publishing record all three publishers read and write | The mark format is one contract shared by all three publishers; they change together ([D2](artifacts/decision-log.md#d2-full-fix-scope), [D6](artifacts/decision-log.md#d6-one-shared-publishing-record)). | +| The three tracker services | outbound | Receive published work items | An item is created before its mark is written; a failed mark stops the run. | +| The planning skill that writes the file | inbound | Produces unmarked slices in the agreed heading shape | Unmarked headings remain valid input to every publisher, unchanged. | + +## Out of Scope + +- Changing the planning skill's authoring format. Unmarked slice headings stay exactly as they are today. +- Changing what any publisher sends to its tracker: titles, bodies, and dependency links within one tracker are + untouched. +- Detecting a mark that was deleted by hand or lost in a merge. A slice whose mark disappears looks unpublished, and + the next run will create a duplicate. This is a named, accepted limitation of a file-carried record. +- Detecting duplicates created by two people publishing diverged copies of the same file and merging afterward. Also a + named, accepted limitation. +- Verifying at classification time that a marked item still exists on its tracker. A mark pointing at a deleted + tracker item keeps its slice skipped. Only the dependency-linking pass checks liveness today, and that stays as it + is. Named, accepted limitation. +- The publishing-channel work of the other phases: the Linear listing (Phase 1), version numbers (Phase 3), the + release process (Phase 6), and the automated check (Phase 7). + +## Open Items + +- **OI-1:** The pre-start safety-net trial. The GitHub publisher's written instructions say a file carrying another + tracker's marks must stop the run and be reported, while the source analysis observed those slices vanishing + silently. The trial settles which is true today. If the trial shows the publisher already stops as written, this + phase's scope shrinks to tracker labeling, the unified shared-file record, and migration. Either way, the target + behavior in this spec stays the same. + - **Resolves when:** The trial runs in a throwaway project, per the outline's resolved open question OQ-3. + - **Blocks implementation:** No. +- **OI-2:** Confirm no known user files in the old mark format would be stranded by the migration path's ask-first + behavior. Files annotated by earlier releases are believed to exist only in users' own repositories, where they + cannot be inspected from here. So this resolves by choice of default rather than by census. + - **Resolves when:** The team accepts the ask-first default as the protection for unknown files, or evidence of a + stranded file arrives. + - **Blocks implementation:** No. Asking is the safe default for exactly the files that cannot be counted. + +## Summary + +- **Outcome delivered:** One shared file records all three trackers' publishing, marks name their tracker, every run + classifies before creating and accounts for every slice, and old files migrate by asking. +- **Primary actors:** A person running any of the three work-item publishers. +- **Decisions settled by evidence:** 5. See [artifacts/decision-log.md](artifacts/decision-log.md) +- **Decisions settled by user input:** 3. See [artifacts/decision-log.md](artifacts/decision-log.md) +- **Sub-agents consulted:** junior-developer, edge-case-explorer, gap-analyzer. See + [artifacts/team-findings.md](artifacts/team-findings.md) +- **Key adjustments from review:** the shared-file record was unified across all three publishers after review showed + the GitHub publisher never marks the shared file today. The foreign-mark response became a single pause-and-ask + step. Migration was reframed as the expected first encounter for every existing published file. Three + file-carried-record limitations were named and accepted. See + [artifacts/team-findings.md](artifacts/team-findings.md) +- **Remaining open items:** 2 diff --git a/docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/artifacts/decision-log.md b/docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/artifacts/decision-log.md new file mode 100644 index 00000000..b691b970 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/artifacts/decision-log.md @@ -0,0 +1,35 @@ +# Decision Log: Unfreeze the Second Channel's Version Numbers + +## Trivial decisions + +- D1: Spec location — the spec lives at `docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/`, nested beside + the build phase outline that spawned it. — Referenced in spec: none (organizational). + +## Full decisions + +### D2: Align to the first channel's versions + +- **Question:** What version should each plugin on the second channel be corrected to? +- **Decision:** Each plugin's second-channel version is set to the version of its last published release, as the first + channel records it. Plugins already matching are untouched. A second-channel version found ahead of the first + channel's is never moved backward without a person deciding; that guard is kept as a cheap safety rail for a + one-time human action even though no plugin is ahead today. +- **Rationale:** The source analysis's target picture labels the second channel's version numbers as "copied from + channel one", and the outline's Phase 3 precondition requires the fix not to guess. The first channel's versions are + the released record, maintained by the release process, so copying them is the no-guess answer. +- **Evidence:** Source analysis: the "After" diagram in + [`../source-han-cleanup-plan.md`](../source-han-cleanup-plan.md#the-publishing-pipeline-the-actual-problem) + ("Version numbers, copied from channel one"). Codebase: current drift measured on the working branch — for example + `han-core/.codex-plugin/plugin.json` states 1.2.0 while `han-core/.claude-plugin/plugin.json` states 2.2.1; 8 of 10 + plugins lag, and `han-communication` (1.0.0) and `han-linear` (1.0.2) already match. +- **Rejected alternatives:** + - Independent second-channel versioning, bumped at each release — rejected because two version histories for one + plugin re-creates the drift this cleanup exists to end, and the source's target picture copies from channel one. + - Restarting every second-channel version at the same fresh number — rejected because it discards the released + history and could move ahead of or behind the true release state; it is exactly the guessing the precondition + forbids. +- **Linked technical notes:** T1 +- **Driven by findings:** F3 (the source of truth is pinned to the last published release), F5 (the ahead-guard is a + deliberate safety rail, mirrored as a pre-start check) +- **Dependent decisions:** — +- **Referenced in spec:** Outcome; Actors and Triggers; Primary Flow; Coordinations diff --git a/docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/artifacts/feature-technical-notes.md b/docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/artifacts/feature-technical-notes.md new file mode 100644 index 00000000..1ae3779f --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/artifacts/feature-technical-notes.md @@ -0,0 +1,14 @@ +# Feature Technical Notes: Unfreeze the Second Channel's Version Numbers + +## T1: How the second channel offers updates + +- **Context:** The Outcome promises update offers resume once the numbers are corrected, and only after the merge. + Both halves rest on how the second channel evaluates freshness. +- **Technical detail:** The Codex channel registers this repository as the marketplace and reads each plugin's version + from its `.codex-plugin/plugin.json` on the repository's default branch. Update availability is a comparison of that + served version against the locally installed one, so a served version that never changes means no update is ever + offered, and corrections on a working branch are invisible until merged to `main`. External channel behavior, not + discoverable from this repo's own code. +- **Supports decisions:** D2 +- **Driven by findings:** F1 +- **Referenced in spec:** Outcome; Actors and Triggers; Primary Flow; Edge Cases and Failure Modes; Coordinations diff --git a/docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/artifacts/gap-analysis-scratch.md b/docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/artifacts/gap-analysis-scratch.md new file mode 100644 index 00000000..79e71700 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/artifacts/gap-analysis-scratch.md @@ -0,0 +1,117 @@ +# Gap Analysis: Phase 3 outline entry vs. Phase 3 draft feature specification + +## Comparison Direction + +Current state: `docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/feature-specification.md`, with +`artifacts/decision-log.md` and `artifacts/feature-technical-notes.md` as supporting evidence. + +Desired state: the Phase 3 entry (`{#phase-3}`, "Unfreeze the second channel's version numbers") of +`docs/plans/han-publishing-cleanup/build-phase-outline.md`. + +## Scope + +Comparison areas: What we build, Why this is Phase 3 (context only, not a commitment), the 4-step Outcome to +demonstrate, Source citations, Connects to (Phase 6, Phase 7), and the Preconditions to verify before starting. +Analysis is behavioral only — no code, file format, or implementation-level comparison. `team-findings.md` was read +and contains no entries (review pending), so it contributes no evidence either way. + +## Actors and Modes Observed + +The outline and spec both name two actors: the person installing Han plugins from the second channel (an end user +checking for and accepting an update) and the maintainer performing the one-time correction. No API, agent, or batch +automation surface is named for this phase — that capability (an automated release process and a completeness check) +belongs to Phases 6 and 7, which this phase only connects to. No sub-roles observed. + +## Summary + +Compared the Phase 3 outline entry (desired state) against the draft feature specification and its two artifact +files (current state), current state toward desired state. The spec fully covers the outline's What-we-build, +Outcome-to-demonstrate, precondition, and its Connects-to relationship with Phase 6, and adds several behaviors +beyond the outline's text that the decision log and technical notes evidence. It is missing any acknowledgment of +the outline's second Connects-to relationship (Phase 7). + +| Category | Count | Description | +|----------|-------|-------------| +| Missing | 1 | Elements in desired state with no current state correspondence | +| Partial | 0 | Elements present in both but incompletely covered | +| Divergent | 0 | Elements addressing same concern in incompatible ways | +| Implicit | 0 | Assumed capabilities neither confirmed nor denied | + +Full analysis written to: /Users/riverbailey/dev/testdouble/han/docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/artifacts/gap-analysis-scratch.md + +## Findings + +**GAP-001: Connects-to Phase 7 relationship absent from the spec** + +- **Category:** Missing +- **Feature/Behavior:** The outline commits Phase 3 to a specific downstream relationship with the automated + completeness check (Phase 7): "the check cannot land green while these numbers are stale." This is one of two + Connects-to entries the outline lists for Phase 3. +- **Current State:** `feature-specification.md` has no "Connects to" section and no mention of Phase 7 or an + automated/completeness check anywhere in the file. The Coordinations table and Out of Scope section mention only + the release process (Phase 6): "Ongoing upkeep is the release-process phase's job, building on this one" (line 58) + and "That is the release-process phase (Phase 6 of the outline)" (lines 62-63). A repository-wide search of the + spec and both artifact files for "phase 7", "phase-7", "completeness check", and "automated check" returns no + matches. +- **Desired State:** `build-phase-outline.md` lines 286-289, "Connects to." — "[Phase 6](#phase-6): once the release + process owns all four surfaces, it keeps these numbers moving. [Phase 7](#phase-7): the check cannot land green + while these numbers are stale." Phase 7 itself lists Phase 3 among the phases it "guards... from regressing" (line + 454), confirming the relationship is bidirectional and load-bearing, not incidental narrative. + +## Validation Notes (Step 5) + +- **Outcome-to-demonstrate (4 steps):** All four outline steps (set up an old install, check for updates, confirm an + update is offered where none was before, accept and confirm the version now matches) have corresponding behavior in + the spec's Outcome paragraph (lines 6-14) and Primary Flow steps 4-5 (lines 32-34). No gap found; searched the full + spec body, not just the Outcome section, before concluding coverage. +- **Precondition:** The outline's precondition ("Confirm which released version each plugin... should be corrected + to, so the fix does not guess") is resolved by decision D2 and carried into the spec's own "Preconditions" bullet + (lines 21-24: "The target version for each plugin is read from the first channel's released versions, not guessed + (D2)"). No gap found. +- **Connects to Phase 6:** Covered by the spec's Coordinations table row for "The release process" (line 58) and the + Out of Scope entry citing Phase 6 by name (lines 62-63). No gap found. +- **Source citations:** Both outline citations (the "People on the second channel are stuck..." finding and "What I + would do, in order" item 3) are consistent with the spec's content; the spec does not restate citations verbatim + (expected — citations are an outline-level artifact) and nothing in the spec contradicts the cited source material. + No gap found; this area is a citation-format check, not a behavioral gap, so it is not scored. +- Disproof attempt for GAP-001: searched `feature-specification.md`, `decision-log.md`, and `feature-technical-notes.md` + for any indirect reference to a completeness check, a regression guard, or "everything must be green" framing that + might satisfy the Phase 7 connection under different wording. None found. The gap survives validation. + +## Additions Beyond the Outline (current state has no desired-state correspondence) + +These are current-state elements with no counterpart in the outline's Phase 3 entry. None contradict the outline; +each is evidenced by the decision log or technical notes, so none are flagged as unevidenced scope creep. + +1. **Backward-move safety rule** — "A second-channel version is found ahead of the first channel's version: the + correction stops for that plugin and the discrepancy is raised to the maintainer; a version is never moved + backward without a person deciding" (Edge Cases table, line 48). Not stated in the outline. Evidenced by decision + D2's rationale and rejected-alternatives list (`decision-log.md` lines 24-29), which explicitly considers and + rejects moving versions backward. +2. **"Already matching" no-op behavior** — the Alternate Flow "A plugin's two versions already match" (lines 38-42) + and the Edge Cases row "A user is on the corrected version already" (line 50). Not stated in the outline. Evidenced + by D2's decision text ("Plugins already matching are left untouched," `decision-log.md` line 14) and by the + codebase drift measurement cited as D2's evidence (`han-communication` and `han-linear` already match, line 23). +3. **Merge-timing edge case** — "A user checks for updates before the correction reaches the default branch... the + outcome is promised from the merge onward" (Edge Cases table, line 49). Not stated in the outline. Evidenced by T1 + (`feature-technical-notes.md` lines 7-11), which explains the second channel reads from the default branch. +4. **Explicit exclusion of the Linear plugin and the all-in-one bundle** — Out of Scope, lines 64: "The Linear + plugin's listing (Phase 1) and the all-in-one bundle, which the second channel does not carry." Not stated in the + outline's Phase 3 entry. Not contradictory: cross-checked against the outline's Phase 1 entry ("Publish the Linear + plugin to the second channel"), which confirms the Linear plugin is not yet on the second channel, and Phase 6's + text, which confirms the bundle "cannot be published to the second channel because that channel does not support + bundles yet." Evidenced by the outline itself (cross-phase), not by the decision log. +5. **Open Item OI-1** — verifying the second channel offers updates purely by version comparison, with no other + freshness signal (lines 69-72). Not stated in the outline as an open question. Evidenced by T1's note that this is + "external channel behavior, not discoverable from this repo's own code" (`feature-technical-notes.md` line 11). + +## Areas Needing Separate Analysis + +- **Phase 1 and Phase 6 draft specs (if/when written)** — this analysis only validates Phase 3's own document set. + Whether Phase 6's eventual spec correctly inherits "keeps these numbers moving" from Phase 3, and whether Phase 7's + eventual spec correctly treats Phase 3 as a regression-guard precondition, both require separate gap analyses once + those specs exist. +- **Source-artifact fidelity** — this analysis compared the spec against the outline only. Whether the outline's own + citations faithfully represent `source-han-cleanup-plan.md` was spot-checked (item 3 of "What I would do, in + order" and the "stuck on old versions" finding) but not exhaustively re-verified against the full source document, + since the outline itself is the desired-state artifact of record for this comparison. diff --git a/docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/artifacts/team-findings.md b/docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/artifacts/team-findings.md new file mode 100644 index 00000000..5248f375 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/artifacts/team-findings.md @@ -0,0 +1,93 @@ +# Team Findings: Unfreeze the Second Channel's Version Numbers + +<!-- Findings from the review team, recorded as F# entries. Major findings carry the full field set; minor edits are +one-line bullets. The F# counter is shared across both sections. --> + +## Major findings + +### F1: The update-offer outcome rested on an unverified external assumption + +- **Agent:** junior-developer +- **Finding:** The outcome promised update offers as a certainty while T1 concedes the channel's freshness rule is + external and unverified, and OI-1 was still open. The reviewer also verified from the repository that the channel + serves plugin content and version from the same directory on the default branch, so a bump plus merge should + deliver real content, with content-caching by version as the one residual unknown. +- **Resolution:** The outcome now names the OI-1 demonstration as what confirms the payoff, and OI-1 gained the + cache question and a split blocking answer: the correction is unblocked, the payoff sign-off is the demonstration. +- **Resolved by:** evidence +- **Affected decisions:** — +- **Affected tech-notes:** T1 +- **Changed in spec:** Outcome; Open Items + +### F2: An "older copy" was undefined because version numbers are decoupled from content + +- **Agent:** junior-developer +- **Finding:** Content is served fresh from the branch, so a copy can hold an old number with current content; a demo + from such a machine would show only the number ticking up. +- **Resolution:** The demonstration now requires a copy that is older in content, and confirms an observable content + difference after accepting, not only a higher number. +- **Resolved by:** evidence +- **Affected decisions:** — +- **Affected tech-notes:** — +- **Changed in spec:** Primary Flow + +### F3: The source of truth was ambiguous between "released" and "current manifest value" + +- **Agent:** junior-developer +- **Finding:** "The first channel's released versions" could mean the last published release or whatever the working + branch's manifest says mid-release; the two can differ. +- **Resolution:** Pinned to the version of the last published release as the first channel records it; today those + equal the first-channel manifest values on the branch. +- **Resolved by:** evidence +- **Affected decisions:** D2 +- **Affected tech-notes:** — +- **Changed in spec:** Actors and Triggers + +### F4: "One version per plugin across both channels" read as an ongoing guarantee + +- **Agent:** junior-developer +- **Finding:** The alignment is a point-in-time snapshot; future releases re-drift until the release-process phase. +- **Resolution:** Reworded: after the correction the numbers match; keeping them matched is the release-process + phase's job. +- **Resolved by:** evidence +- **Affected decisions:** — +- **Affected tech-notes:** — +- **Changed in spec:** Outcome + +### F5: The ahead-of-first-channel guard covers a condition no plugin meets today + +- **Agent:** junior-developer +- **Finding:** No second-channel version is currently ahead, so the stop-and-raise rule guards a hypothetical + (`Category: YAGNI candidate`). +- **Resolution:** Kept deliberately as a cheap safety rail for a one-time human action, consistent with the plan's + ask-over-guess rule, and mirrored as a pre-start check in the preconditions so it is confirmed rather than assumed. +- **Resolved by:** evidence +- **Affected decisions:** D2 +- **Affected tech-notes:** — +- **Changed in spec:** Actors and Triggers; Edge Cases and Failure Modes + +### F6: The release-process coordination row overreached into Phase 6 + +- **Agent:** junior-developer +- **Finding:** The row framed this phase as doing something with the release process; that behavior is Phase 6's. +- **Resolution:** Reframed as a hand-off note: the aligned state is the baseline Phase 6 maintains. +- **Resolved by:** evidence +- **Affected decisions:** — +- **Affected tech-notes:** — +- **Changed in spec:** Coordinations + +### F7: The Phase 7 dependency was missing + +- **Agent:** gap-analyzer (GAP-001) +- **Finding:** The outline states the automated completeness check cannot land green while these numbers are stale; + the spec never mentioned it. +- **Resolution:** The outcome now names this phase as a prerequisite of the cleanup's later automated completeness + check. +- **Resolved by:** evidence +- **Affected decisions:** — +- **Affected tech-notes:** — +- **Changed in spec:** Outcome + +## Minor edits + +- F8: Summary placeholders replaced with consulted agents and key adjustments — junior-developer — Summary diff --git a/docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/feature-specification.md b/docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/feature-specification.md new file mode 100644 index 00000000..ec13b38c --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-3-unfreeze-versions/feature-specification.md @@ -0,0 +1,102 @@ +# Feature Specification: Unfreeze the Second Channel's Version Numbers + +Every plugin's stated version on the second install channel is corrected to match the version released on the first +channel, so people installing from the second channel start being offered updates again. + +## Outcome + +A person running an older copy of any Han plugin from the second channel checks for updates and is offered one, where +today none is ever offered. + +That payoff rests on the second channel offering an update when the version it serves is newer than the installed +one. The demonstration in [OI-1](#open-items) is what confirms it +([T1](artifacts/feature-technical-notes.md#t1-how-the-second-channel-offers-updates)). + +The correction itself sets each plugin's second-channel version to the version last released on the first channel +([D2](artifacts/decision-log.md#d2-align-to-the-first-channels-versions)). After the correction the two numbers match +for every plugin. Keeping them matched on future releases belongs to the release-process phase, not this one. + +The cleanup's later automated completeness check cannot land green while these numbers are stale, so this phase is one +of its prerequisites. + +## Actors and Triggers + +- **Actors:** People with Han plugins installed from the second channel; the maintainer applying the correction. +- **Triggers:** The maintainer corrects the stated versions once. Users' channel tooling then compares versions + whenever it checks for updates. +- **Preconditions:** The target version for each plugin is the version of its last published release, as the first + channel records it. Nothing is guessed + ([D2](artifacts/decision-log.md#d2-align-to-the-first-channels-versions)). Before starting, confirm no + second-channel version is ahead of its first-channel counterpart. The corrections reach users when the working + branch merges to the default branch, which the second channel serves + ([T1](artifacts/feature-technical-notes.md#t1-how-the-second-channel-offers-updates)). + +## Primary Flow + +1. The maintainer reads each plugin's released version from the first channel. +2. For every plugin whose second-channel version lags, the second-channel version is set to the released version + ([D2](artifacts/decision-log.md#d2-align-to-the-first-channels-versions)). Plugins already matching are left + untouched. +3. The change ships with the working branch's merge to the default branch. +4. A person with a genuinely older installed copy, older in content and not only in number, checks for updates on the + second channel and is offered the released version. +5. They accept, and the installed version matches the version released on the first channel. The demonstration + confirms an observable content difference after accepting, not only a higher number. A version number on this + channel does not by itself prove fresh content + ([T1](artifacts/feature-technical-notes.md#t1-how-the-second-channel-offers-updates)). + +## Alternate Flows and States + +### A plugin's two versions already match + +- **Entry condition:** A plugin's second-channel version already equals the first channel's released version. +- **Sequence:** No change is made for that plugin, and no update is offered to users already current. +- **Exit:** The plugin is simply confirmed aligned. + +## Edge Cases and Failure Modes + +| Condition | Required Behavior | +| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | +| A second-channel version is found ahead of the first channel's version | The correction stops for that plugin and the discrepancy is raised to the maintainer; a version is never moved backward without a person deciding. | +| A user checks for updates before the correction reaches the default branch | They are still offered nothing; the outcome is promised from the merge onward ([T1](artifacts/feature-technical-notes.md#t1-how-the-second-channel-offers-updates)). | +| A user is on the corrected version already | No update is offered; the check is quiet. | + +## Coordinations + +| Coordinating System | Direction | Interaction | Ordering / Consistency Requirement | +| -------------------------- | --------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| First install channel | inbound | Supplies each plugin's released version as the source of truth | The correction copies from it, never invents versions ([D2](artifacts/decision-log.md#d2-align-to-the-first-channels-versions)). | +| Second install channel | outbound | Serves the corrected versions and offers updates by comparison | Corrections are user-visible only after the merge to the default branch ([T1](artifacts/feature-technical-notes.md#t1-how-the-second-channel-offers-updates)). | +| The release process | none yet | Hands off: the aligned state is the baseline the release-process phase maintains | This phase does nothing with the release process; the hand-off lands in Phase 6. | + +## Out of Scope + +- Automating version updates on future releases. That is the release-process phase + ([Phase 6 of the outline](../build-phase-outline.md#phase-6)); this phase is the one-time correction it builds on. +- The Linear plugin's listing (Phase 1) and the all-in-one bundle, which the second channel does not carry. +- Changing how either channel decides that an update is available. + +## Open Items + +- **OI-1:** Confirm the second channel offers an update purely by comparing the served version against the installed + one, with no other freshness signal. Also confirm that accepting an update re-reads the plugin's content rather than + serving something cached by version. + - **Resolves when:** A real update is offered and accepted during this phase's demonstration, with a confirmed + content difference. + - **Blocks implementation:** No for the correction itself, which is right regardless. Yes for signing off the + user-visible payoff this phase promises. The demonstration is that sign-off. + +## Summary + +- **Outcome delivered:** Second-channel versions match the first channel's released versions, and update offers + resume. +- **Primary actors:** Second-channel users; the maintainer applying the one-time correction. +- **Decisions settled by evidence:** 1. See [artifacts/decision-log.md](artifacts/decision-log.md) +- **Decisions settled by user input:** 0. See [artifacts/decision-log.md](artifacts/decision-log.md) +- **Sub-agents consulted:** junior-developer, gap-analyzer. See + [artifacts/team-findings.md](artifacts/team-findings.md) +- **Key adjustments from review:** The update-offer payoff is now explicitly gated on the OI-1 demonstration. The + demo requires a content difference, not only a higher number. The source of truth is pinned to the last published + release. The Phase 7 dependency is restored. See [artifacts/team-findings.md](artifacts/team-findings.md) +- **Remaining open items:** 1 +- **Technical notes:** 1. See [artifacts/feature-technical-notes.md](artifacts/feature-technical-notes.md) diff --git a/docs/plans/han-publishing-cleanup/phase-4-untrue-dependencies/artifacts/decision-log.md b/docs/plans/han-publishing-cleanup/phase-4-untrue-dependencies/artifacts/decision-log.md new file mode 100644 index 00000000..e00eeeb7 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-4-untrue-dependencies/artifacts/decision-log.md @@ -0,0 +1,90 @@ +# Decision Log: Remove the Untrue Dependency Declarations + +## Trivial decisions + +- D1: Spec location — the spec lives at `docs/plans/han-publishing-cleanup/phase-4-untrue-dependencies/`, nested + beside the build phase outline that spawned it. — Referenced in spec: none (organizational). + +## Full decisions + +### D2: What is removed and what stays + +- **Question:** Which declarations are deleted, and what evidence gates the deletion? +- **Decision:** Three declarations are removed: the Reporting plugin's, the Feedback plugin's, and the Linear plugin's + claims on the Core plugin. The Reporting plugin keeps its Communication dependency; the Feedback and Linear plugins + are left depending on nothing, a supported state with two shipped precedents. A pre-start check re-confirms none of + the three reaches the Core plugin through any path; if a real path is found, the removal stops for that plugin. +- **Rationale:** The source analysis found the first two claims decorative. Review of this phase applied the same + evidence test to the Linear plugin and found the same absence, plus its own long-form doc stating the skill + dispatches no agents; leaving the declaration while correcting the page that claims it would create a fresh + docs-versus-declaration contradiction. The user chose to remove it in the same pass (2026-07-21). +- **Evidence:** Codebase: `han-reporting/.claude-plugin/plugin.json` declares `["han-communication", "han-core"]` + while `grep -rn "han-core" han-reporting/skills/` returns nothing; `han-feedback/.claude-plugin/plugin.json` + declares `["han-core"]` while the only `han-core` strings in `han-feedback/skills/` are naming-convention text; + `han-linear/.claude-plugin/plugin.json` declares `["han-core"]` while + `han-linear/skills/work-items-to-linear/SKILL.md` references no other plugin and + `han-linear/docs/skills/work-items-to-linear.md` line 156 states "The skill dispatches no agents." Zero-dependency + precedent: `han-communication` and `han-plugin-builder` both declare no dependencies and ship. Source analysis: + "The dependency graph: two threads to snip" in + [`../source-han-cleanup-plan.md`](../source-han-cleanup-plan.md#the-dependency-graph-two-threads-to-snip). User + input: the third removal (2026-07-21). +- **Rejected alternatives:** + - Exactly two removals per the outline, Linear deferred to its own decision — rejected by the user; the evidence was + already in hand and deferral would ship a known contradiction. + - Keeping the Linear declaration and documenting why it is real — rejected because the review found no positive + evidence of reality to document. + - Also auditing and trimming other plugins' declarations — rejected because the source analysis verified the rest + of the graph as true and this phase's review surfaced no further decorative claim. +- **Linked technical notes:** — +- **Driven by findings:** F3 (the Linear contradiction), F6 (the zero-dependency precedent citation) +- **Dependent decisions:** D3, D4 +- **Referenced in spec:** Outcome; Actors and Triggers; Primary Flow + +### D3: Declarations and documentation change together + +- **Question:** Is deleting the declaration lines enough, or do the documentation surfaces that repeat them change + too, and which are they? +- **Decision:** They change in the same commit. The audited claiming surfaces are twenty-one locations across eight + files, and completion is verified by searching the documentation for the Core plugin's name and reviewing every + hit: each must be a true statement about a plugin keeping the dependency, or one of the Feedback plugin's excluded + naming-convention examples. Grouped sentences covering removed and kept dependencies are edited so the kept ones + stay correctly described. +- **Rationale:** The phase's payoff is trust. The initial enumeration (seven surfaces) missed fourteen; the audit + found claims in the contributor guide, the concepts doc, the versioning doc, the dependency how-to guide, the + marketplace listing descriptions, and second untrue dispatch claims on two front-door pages. A search-and-review + acceptance check is repeatable; a memorized list is not. +- **Evidence:** Content audit (this phase's review): claims at `han-reporting/README.md:8,20`, + `han-feedback/README.md:8,15`, `han-linear/README.md:8,16`, the three `.claude-plugin/plugin.json` declarations + (and `han-linear`'s description field), `CLAUDE.md:21,100,107,142`, `CONTRIBUTING.md:129-130`, + `docs/concepts.md:224,232`, `docs/semantic-versioning.md:100`, + `docs/how-to/extend-han-with-plugin-dependencies.md:136,164,169,192`, `docs/choosing-a-han-plugin.md:43,45,50,64`, + and `.claude-plugin/marketplace.json:67`. Exclusion: the naming-convention examples in + `han-feedback/skills/han-feedback/SKILL.md` must not change. +- **Rejected alternatives:** + - Declarations first, documentation in a follow-up — rejected because the interim state reintroduces exactly the + docs-say-one-thing-system-does-another problem this cleanup exists to end. + - Verifying by the enumerated list alone — rejected because the list was already proven incomplete once; the search + is the acceptance check, the list is the work plan. +- **Linked technical notes:** — +- **Driven by findings:** F1, F2, F4, F5, F8 +- **Dependent decisions:** — +- **Referenced in spec:** Outcome; Primary Flow; Edge Cases and Failure Modes; Coordinations + +### D4: Linear page reconciliation + +- **Question:** The Phase 1 spec handed off an open item: the Linear plugin's front-door page says its skills dispatch + shared agents from other plugins, while its skill content dispatches none. Where does that get fixed? +- **Decision:** Here, paired with the removal of the Linear plugin's declaration under D2, so the page and the + declaration end the phase agreeing with each other and with the skill's real behavior. +- **Rationale:** This phase is the cleanup's dependency-truth work, and the Phase 1 spec routed the item here + explicitly. Fixing the page alone would have contradicted the surviving declaration; D2's third removal is what + makes the reconciliation clean. +- **Evidence:** Phase 1 spec, Open Item OI-2 (`../phase-1-linear-publishing/feature-specification.md`). Codebase: + `han-linear/README.md` lines 8 and 16; `han-linear/docs/skills/work-items-to-linear.md` line 156. +- **Rejected alternatives:** + - Leaving it as a standing open item — rejected because the fix is a documentation edit inside a phase already + editing the same class of documentation. +- **Linked technical notes:** — +- **Driven by findings:** F3 +- **Dependent decisions:** — +- **Referenced in spec:** Primary Flow; Open Items diff --git a/docs/plans/han-publishing-cleanup/phase-4-untrue-dependencies/artifacts/team-findings.md b/docs/plans/han-publishing-cleanup/phase-4-untrue-dependencies/artifacts/team-findings.md new file mode 100644 index 00000000..67bbdd85 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-4-untrue-dependencies/artifacts/team-findings.md @@ -0,0 +1,89 @@ +# Team Findings: Remove the Untrue Dependency Declarations + +<!-- Findings from the review team, recorded as F# entries. Major findings carry the full field set; minor edits are +one-line bullets. The F# counter is shared across both sections, so the content-auditor's later major finding is F8 +even though it sits above the earlier minor edits F6 and F7 by severity. --> + +## Major findings + +### F1: The contributor guide repeats the claims and was missing from the plan + +- **Agent:** junior-developer +- **Finding:** The contributor guide states "Every plugin depends on `han-core`" and names the Reporting plugin among + core-dependents; the sentence is already false for two shipped plugins and becomes false for three more after this + phase. The spec's surface list did not include it. +- **Resolution:** The contributor guide joined the corrected surfaces, and its generalization gets rewritten rather + than patched. +- **Resolved by:** evidence +- **Affected decisions:** D3 +- **Affected tech-notes:** — +- **Changed in spec:** Outcome; Coordinations + +### F2: Both front-door pages carry a second untrue claim beyond the dependency line + +- **Agent:** junior-developer +- **Finding:** The Reporting and Feedback front-door pages also say their skills dispatch the Core plugin's shared + reviewers; Reporting dispatches only the Communication plugin's editor and Feedback dispatches nothing. +- **Resolution:** The correction widens beyond the dependency line to the dispatch claims on both pages. +- **Resolved by:** evidence +- **Affected decisions:** D3 +- **Affected tech-notes:** — +- **Changed in spec:** Primary Flow + +### F3: The Linear plugin's declaration fails the same evidence test + +- **Agent:** junior-developer +- **Finding:** Reconciling the Linear page while keeping its Core declaration would create a fresh + docs-versus-declaration contradiction: by the identical evidence test, the Linear plugin's declaration is equally + decorative, and its own long-form doc says the skill dispatches no agents. +- **Resolution:** The user chose to remove the Linear declaration in the same pass (2026-07-21), gated by the same + stop-if-real check; the outline gains a note recording the third removal as an evidence-driven addition. +- **Resolved by:** user input +- **Affected decisions:** D2, D4 +- **Affected tech-notes:** — +- **Changed in spec:** Outcome; Primary Flow; Out of Scope; Coordinations + +### F4: The repository map edit is shared-sentence surgery + +- **Agent:** junior-developer +- **Finding:** The repository map states the claims inside grouped sentences that also cover plugins keeping the + dependency; a careless edit leaves the claim half-intact or breaks a true statement. +- **Resolution:** The spec now requires grouped sentences to be edited so kept dependencies stay correctly described, + with a matching edge-case row. +- **Resolved by:** evidence +- **Affected decisions:** D3 +- **Affected tech-notes:** — +- **Changed in spec:** Primary Flow; Edge Cases and Failure Modes + +### F5: The completion check was not a repeatable acceptance criterion + +- **Agent:** junior-developer +- **Finding:** "Verified by searching for the claim" named no search or exclusion, so it would miss the surfaces the + audit found and could flag the Feedback plugin's legitimate naming-convention examples. +- **Resolution:** The check is specified: search the documentation for the Core plugin's name, review every hit as + either a true kept-dependency statement or an excluded naming-convention example. +- **Resolved by:** evidence +- **Affected decisions:** D3 +- **Affected tech-notes:** — +- **Changed in spec:** Primary Flow; Edge Cases and Failure Modes + +### F8: The audited surface count is twenty-one across eight files, not seven + +- **Agent:** content-auditor +- **Finding:** The enumeration missed fourteen surfaces, including the concepts doc, the versioning doc, the + dependency how-to guide, the marketplace listing description, several repository-map lines, and the Linear + manifest's description text. +- **Resolution:** The full audited list is recorded in D3's evidence and drives the work plan; the search-and-review + step is the acceptance check so future misses cannot hide. +- **Resolved by:** evidence +- **Affected decisions:** D3 +- **Affected tech-notes:** — +- **Changed in spec:** Outcome; Coordinations + +## Minor edits + +- F6: "Installs alone" now rests on the two shipped zero-dependency precedents — junior-developer — Primary Flow +- F7: The channels stay unnamed in behavioral prose by the plain-language rule; the reviewer's request to name them is + met in the decision log's evidence instead — junior-developer — — +</content> +</invoke> diff --git a/docs/plans/han-publishing-cleanup/phase-4-untrue-dependencies/feature-specification.md b/docs/plans/han-publishing-cleanup/phase-4-untrue-dependencies/feature-specification.md new file mode 100644 index 00000000..421e45a9 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-4-untrue-dependencies/feature-specification.md @@ -0,0 +1,116 @@ +# Feature Specification: Remove the Untrue Dependency Declarations + +The Reporting, Feedback, and Linear plugins stop declaring a dependency on the Core plugin none of them uses, and +every documentation surface that repeats those claims is corrected in the same change, so the dependency record can be +trusted again. + +## Outcome + +Installing the Reporting, Feedback, or Linear plugin no longer pulls in the Core plugin, and all three keep working +exactly as before. + +The source analysis named two decorative declarations. Review of this phase found a third, the Linear plugin's, using +the same evidence test, and the team chose to remove it in the same pass +([D2](artifacts/decision-log.md#d2-what-is-removed-and-what-stays)). + +Every surface that states a removed dependency changes together with the declaration: front-door pages, the repository +map, the contributor guide, the concepts and versioning docs, the dependency how-to guide, the plugin index, and the +marketplace listing descriptions +([D3](artifacts/decision-log.md#d3-declarations-and-documentation-change-together)). + +The wider payoff is trust: with the decorative claims gone, the remaining declarations answer "what breaks if I change +this?" honestly. + +## Actors and Triggers + +- **Actors:** People installing the Reporting, Feedback, or Linear plugin; maintainers who rely on the dependency + record when changing plugins. +- **Triggers:** The maintainer removes the three declarations and corrects the documentation in one change. +- **Preconditions:** Re-confirm none of the three plugins reaches the Core plugin through any path the analysis did + not cover: no skill content, no dispatched agent, no shared reference sourced from it + ([D2](artifacts/decision-log.md#d2-what-is-removed-and-what-stays)). The Feedback plugin's mentions of the Core + plugin's name are naming-convention text inside captured feedback, not invocations, and stay as they are. + +## Primary Flow + +1. The maintainer re-verifies the evidence for each of the three plugins. The Reporting plugin's skills source their + writing standard from the Communication plugin and reference the Core plugin nowhere. The Feedback plugin invokes + no other plugin at all. The Linear plugin's skill dispatches no agents and references no other plugin, as its own + long-form documentation states ([D2](artifacts/decision-log.md#d2-what-is-removed-and-what-stays)). +2. The three declarations are deleted. The Reporting plugin keeps its Communication dependency. The Feedback and + Linear plugins are left depending on nothing, a supported state with two shipped precedents among the existing + plugins ([D2](artifacts/decision-log.md#d2-what-is-removed-and-what-stays)). +3. Every documentation surface that repeats a removed claim is corrected in the same change. This includes a second + untrue claim on the Reporting and Feedback front-door pages, which say their skills dispatch the Core plugin's + shared reviewers when they do not. It also includes the grouped sentences in the repository map and guides, which + must be edited so the plugins that keep the dependency stay correctly described + ([D3](artifacts/decision-log.md#d3-declarations-and-documentation-change-together)). +4. The Linear plugin's front-door page is reconciled with what its skill content really does, closing the open item + handed off from the Phase 1 spec. This reconciliation is now paired with the declaration's removal, so no new + contradiction between the docs and the declaration is created + ([D4](artifacts/decision-log.md#d4-linear-page-reconciliation)). +5. Completion is verified by searching the documentation for the Core plugin's name and reviewing every hit. Each hit + is either a true statement about a plugin that keeps the dependency, or one of the Feedback plugin's + naming-convention examples, which are excluded on purpose + ([D3](artifacts/decision-log.md#d3-declarations-and-documentation-change-together)). +6. On a clean machine, installing the Reporting plugin brings in the Communication plugin, not the Core plugin. A + Reporting skill then runs end to end, including its writing pass. The same check passes for the Feedback and + Linear plugins, which now install alone. + +## Alternate Flows and States + +### A user already has the plugins installed + +- **Entry condition:** Someone installed the Reporting, Feedback, or Linear plugin before this change and has the + Core plugin alongside it. +- **Sequence:** Nothing changes at runtime for them; the Core plugin simply stops being required and can be removed + if it is not otherwise wanted. +- **Exit:** Their setup keeps working with no action needed. + +## Edge Cases and Failure Modes + +| Condition | Required Behavior | +| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| The pre-start check finds a real path from any of the three to the Core plugin | The removal stops for that plugin and the finding goes back to the team; a true dependency is never deleted. | +| A documentation surface still claims a removed dependency after the change | The change is not complete; the search-and-review step in the primary flow is the acceptance check. | +| A grouped sentence covers both removed and kept dependencies | The sentence is edited so the plugins that keep the dependency stay correctly described; a half-edited grouping is a failure. | +| The second channel's manifests | Nothing to change there: they declare no dependencies at all, on any plugin. | + +## Coordinations + +| Coordinating System | Direction | Interaction | Ordering / Consistency Requirement | +| ------------------------- | --------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------ | +| First install channel | outbound | Reads each plugin's declarations to resolve installs | After the change, installing any of the three no longer pulls the Core plugin. | +| Documentation surfaces | outbound | Eight documentation files describe the dependencies today | They change in the same commit as the declarations, never separately. | +| The phased-build outline | outbound | Records this phase's scope | The outline gains a note that a third removal was added on review evidence. | + +## Out of Scope + +- The versions each plugin states for its remaining dependencies. That is the next phase + ([Phase 5 of the outline](../build-phase-outline.md#phase-5)), which adds version statements on top of the truthful + declarations this phase leaves behind. +- Every other plugin's declarations. The source analysis verified the rest of the graph as true, and this phase's + review found no further decorative claim beyond the Linear plugin's. +- Changing any skill's behavior in the three plugins. + +## Open Items + +- **OI-1:** None remain. The Phase 1 spec's open item about the Linear plugin's front-door page resolves inside this + phase's step 4. + - **Resolves when:** Already scheduled within this phase. + - **Blocks implementation:** No. + +## Summary + +- **Outcome delivered:** The three decorative dependency claims are gone, the documentation agrees on every surface, + and all three plugins install and run without the Core plugin. +- **Primary actors:** People installing the three plugins; maintainers relying on the dependency record. +- **Decisions settled by evidence:** 2. See [artifacts/decision-log.md](artifacts/decision-log.md) +- **Decisions settled by user input:** 1. See [artifacts/decision-log.md](artifacts/decision-log.md) +- **Sub-agents consulted:** junior-developer, content-auditor. See + [artifacts/team-findings.md](artifacts/team-findings.md) +- **Key adjustments from review:** The Linear plugin's declaration joined the removal by user decision, after the same + evidence test flagged it. The documentation surface list grew from seven claimed surfaces to the audited twenty-one + across eight files. The completion check became a specified search-and-review step. See + [artifacts/team-findings.md](artifacts/team-findings.md) +- **Remaining open items:** 0 diff --git a/docs/plans/han-publishing-cleanup/phase-5-version-declarations/artifacts/decision-log.md b/docs/plans/han-publishing-cleanup/phase-5-version-declarations/artifacts/decision-log.md new file mode 100644 index 00000000..d5e61f9d --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-5-version-declarations/artifacts/decision-log.md @@ -0,0 +1,125 @@ +# Decision Log: Declare the Plugin Versions That Work Together + +## Trivial decisions + +- D1: Spec location — the spec lives at `docs/plans/han-publishing-cleanup/phase-5-version-declarations/`, nested + beside the build phase outline that spawned it. — Referenced in spec: none (organizational). + +## Full decisions + +### D2: Same-major version statements + +- **Question:** How strict is each statement: an exact version, a minimum, or a range? +- **Decision:** Same-major, at-or-above: a companion satisfies the statement when its version shares the stated major + line and is at or above the stated release. Pre-release forms are excluded by default; when a companion's baseline + version is itself a pre-release, its statements deliberately opt into the pre-release form, because a plain range + would exclude the only marker that exists. Enforcement reaches an existing installation only after the depending + plugin upgrades to a statement-carrying version, and the shipping release says so. +- **Rationale:** The project's versioning policy reserves major bumps for breaking changes, so "same major, at or + above" is exactly the range the policy promises compatible. An exact pin would break on every patch release; a bare + minimum would silently admit the next breaking major. The pre-release clause is load-bearing on the current alpha + line, not speculative. The enforcement-reach limit follows from statements shipping inside the depending plugin's + own manifest. +- **Evidence:** Codebase: `docs/semantic-versioning.md` (major = breaking; minor/patch = compatible); current branch + context `han-v5.0.0-alpha-1` (a pre-release baseline is plausible, making the opt-in clause load-bearing). + Platform: semver range support on dependency entries per T1. +- **Rejected alternatives:** + - Exact version pins — rejected: every patch release of a companion would break resolution and demand a suite-wide + edit. + - Bare minimum with no major cap — rejected: it admits the next major, which the versioning policy defines as + allowed to break users. + - Dropping the pre-release opt-in clause as speculative — rejected after review: on an alpha line the clause is the + difference between a working statement and a suite-wide install break. +- **Linked technical notes:** T1 +- **Driven by findings:** F1 (enforcement-reach limit), F9 (pre-release baseline hazard) +- **Dependent decisions:** D4, D5 +- **Referenced in spec:** Outcome; Actors and Triggers; Edge Cases and Failure Modes + +### D3: First channel only + +- **Question:** Do version statements appear on the second channel in any form? +- **Decision:** No. Statements live where a mechanism reads them: the first channel's dependency declarations. The + second channel resolves no dependencies, so it gets no statements; its companion guidance stays in the documented + install instructions, unchanged. This deliberately narrows the outline's channel-unqualified "every plugin states" + wording, and deliberately closes the outline's "visible but unenforced" fallback: if the verification trial + disproves enforcement, the phase pauses and the question returns to the team rather than shipping decoration. +- **Rationale:** A statement nothing reads is decoration, the same class of untruth Phase 4 removed. +- **Evidence:** Codebase: `README.md` lines 74-89 ("Codex ... resolves no dependencies"); the `.codex-plugin` + manifests carry no dependency field. Outline: OQ-2 resolution and Phase 5 preconditions + (`../build-phase-outline.md#oq-2`). +- **Rejected alternatives:** + - Informational statements in second-channel manifests — rejected: no tooling reads them, and unenforced claims + drift into the docs-versus-reality gap this cleanup exists to close. + - Shipping statements as visible information if enforcement fails — rejected in favor of pausing and re-deciding; + recorded because the outline's precondition had left that option open (gap-analysis divergence). +- **Linked technical notes:** — +- **Driven by findings:** F13, F14 (the two recorded divergences from the outline's wording) +- **Dependent decisions:** — +- **Referenced in spec:** Outcome; Alternate Flows and States; Coordinations; Out of Scope; Open Items + +### D4: Per-plugin release markers + +- **Question:** The first channel resolves a version statement by finding a matching per-plugin release marker, and + this repository marks releases only at the suite level today. How do statements become resolvable? +- **Decision:** This phase backfills one release marker per companion plugin — only the plugins that appear on the + depended-upon side of a statement — at its latest published release, pointing at the exact content that shipped + under that release, never at the working branch's in-flight state. "Latest published release" means the per-plugin + versions as of the newest suite release the marketplace serves. Markers are confirmed present on the shared + repository before any statement stage ships. Until more markers accumulate, each range resolves to that single + marked version in practice. Phase 6 takes over producing markers for every plugin on every future release. +- **Rationale:** The channel disables a depending plugin when no marker matches, so marker-before-statement is a hard + gate. Markers travel separately from merged changes, so presence on the shared repository is confirmed, not + assumed. Marking only companions is the strictly simpler version that satisfies the same evidence; markers for + plugins nothing resolves against would do no work this phase. Pointing markers at released content preserves what a + version name means: a marker on unreleased content would serve wrong code under a released number. +- **Evidence:** Platform: resolution against per-plugin markers, disable on missing match, per T1. Codebase: + `git tag --list` shows 27 suite-level `vX.Y.Z` tags and zero per-plugin markers; `docs/semantic-versioning.md` + ("the git tag for a release tracks the parent version" and its per-plugin tag note). +- **Rejected alternatives:** + - Backfill a marker for every plugin — rejected: the simpler companion-only set satisfies the same evidence; Phase + 6 covers the rest going forward. + - Backfill additional markers for older versions each range nominally allows — rejected: installs resolve to the + newest in range, and history accumulates naturally from Phase 6 onward. + - Marking the working branch's current content — rejected: it binds released version names to unreleased content, a + silent provenance defect. +- **Linked technical notes:** T1 +- **Driven by findings:** F2 (single-marker point resolution stated), F6 (companion-only simpler version), F7 + (baseline pinned to the latest published suite release), F10 (markers confirmed on the shared repository) +- **Dependent decisions:** D5 +- **Referenced in spec:** Outcome; Actors and Triggers; Primary Flow; Alternate Flows and States; Out of Scope + +### D5: Staged, verified, reversible rollout + +- **Question:** How do the statements land without risking a suite-wide install break on a mechanism this repository + cannot inspect? +- **Decision:** Four safeguards, in order. First, a live verification trial on a clean machine before any statement + reaches general installers: one edge resolves correctly, an unsatisfiable statement produces the channel's named + error, an out-of-range companion is refused; the trial is the corroboration for the channel's externally-documented + behavior, and if it disproves that behavior the phase pauses. Second, staging: one low-traffic edge first, then the + remaining single edges, the all-in-one bundle's six statements last. Third, a pre-ship check per stage that the + authored ranges admit a common solution for every supported install combination. Fourth, reversibility: statements + start deliberately wide; rollback is reverting the statement manifests, with markers left in place as harmless; + corrections reach already-resolved installations at their next update, a latency the wide-start posture is chosen + to make survivable. The Phase 5-to-Phase 6 gap is covered by a named manual marker step on every release in the + window. +- **Rationale:** Every enforcing behavior rests on a single external documentation source; the trial converts that + single-source claim into observed behavior before users can be hurt. The bundle is the most-installed path and + fails whole on one bad marker among six, so it lands last. A self-inflicted refused combination would present as + the suite's own bug, so it is checked before ship rather than left to users. A release in the gap would otherwise + pin users to stale versions or disable dependents, reproducing the harms Phases 2 and 3 exist to end. +- **Evidence:** Review findings of this phase (the rollout-safety review's five blocking findings and the + single-source evidence flag). Platform mechanics per T1, single-source, corroborated by the trial this decision + mandates. Sibling precedent: the cleanup's other phases already gate risky behavior on throwaway-project trials + (outline OQ-3 resolution). +- **Rejected alternatives:** + - Landing all statements as one change across the suite — rejected: a single bad marker or misread mechanic breaks + every install at once with no incremental signal. + - Shipping without a live trial — rejected: the mechanics are not discoverable from this repository, and the + evidence rule does not let a single web source carry a suite-wide behavioral commitment. + - Narrow statements from day one — rejected: a bad narrow statement pins users below later fixes and takes a full + release cycle to correct. +- **Linked technical notes:** T1 +- **Driven by findings:** F3, F4, F8, F10, F11, F12 +- **Dependent decisions:** — +- **Referenced in spec:** Outcome; Actors and Triggers; Primary Flow; Alternate Flows and States; Edge Cases and + Failure Modes; Open Items diff --git a/docs/plans/han-publishing-cleanup/phase-5-version-declarations/artifacts/feature-technical-notes.md b/docs/plans/han-publishing-cleanup/phase-5-version-declarations/artifacts/feature-technical-notes.md new file mode 100644 index 00000000..77134457 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-5-version-declarations/artifacts/feature-technical-notes.md @@ -0,0 +1,22 @@ +# Feature Technical Notes: Declare the Plugin Versions That Work Together + +## T1: How the first channel resolves version statements + +- **Context:** The Outcome and Primary Flow promise enforcement (resolved companions, refused conflicts) and the + preconditions demand markers-before-statements. All of it rests on the channel's resolution mechanics, which are + external and not discoverable from this repository. +- **Technical detail:** Claude Code plugin `dependencies` entries accept either bare names or + `{ "name": ..., "version": <semver range> }` objects, with any Node-semver expression (`^`, `~`, `>=`, exact); + pre-releases are excluded unless the range opts in (e.g. `^2.0.0-0`). The installer resolves a constraint to the + highest git tag matching the range, in the tag format `{plugin-name}--v{version}`. No matching tag disables the + dependent plugin with a `no-matching-tag` error. When multiple installed plugins constrain the same dependency, the + ranges are intersected; an empty intersection fails with `range-conflict`. Auto-update fetches within the + constrained range rather than the marketplace's latest. Source: official plugin-dependencies documentation + (code.claude.com/docs/en/plugin-dependencies), fetched 2026-07-21 — a single web source; the live verification + trial mandated by D5 is the corroboration before any statement ships. This repository currently has only + suite-level `vX.Y.Z` tags, so per-plugin `{name}--v{version}` tags must be backfilled before any constraint lands. + Operational caveat: git tags do not travel with branch merges — they are pushed separately — so "statements merged, + tags unpushed" is the primary partial-failure state and is gated explicitly in the spec's primary flow. +- **Supports decisions:** D2, D4, D5 +- **Driven by findings:** F3 (single-source flag), F10 (tags travel separately from merges) +- **Referenced in spec:** Outcome; Primary Flow; Alternate Flows and States; Coordinations diff --git a/docs/plans/han-publishing-cleanup/phase-5-version-declarations/artifacts/gap-analysis-scratch.md b/docs/plans/han-publishing-cleanup/phase-5-version-declarations/artifacts/gap-analysis-scratch.md new file mode 100644 index 00000000..d2d30f23 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-5-version-declarations/artifacts/gap-analysis-scratch.md @@ -0,0 +1,139 @@ +# Gap Analysis: Phase 5 outline entry vs. Phase 5 feature specification + +## Comparison Direction + +Current state: the draft spec at +`docs/plans/han-publishing-cleanup/phase-5-version-declarations/feature-specification.md`, with its supporting +`artifacts/decision-log.md` and `artifacts/feature-technical-notes.md`. + +Desired state (reference): the Phase 5 entry (`{#phase-5}`) of +`docs/plans/han-publishing-cleanup/build-phase-outline.md` — "Declare the plugin versions that work together" — +including What we build, Why, the 3-step Outcome to demonstrate, Source citations, Connects to (Phase 6), both +preconditions, plus the OQ-2 resolution (`{#oq-2}`) and departure 2 (`{#departures}`). + +A secondary, explicitly requested reverse-direction check is also performed: anything the spec adds beyond what the +outline names, and whether the decision log evidences each addition. This does not change the primary direction above; +it is reported separately under "Spec Additions Beyond the Outline." + +Analysis is behavioral only: no file paths, function names, or implementation mechanics are treated as gaps in +themselves; they are cited only as evidence. + +## Scope + +Comparison areas, drawn from the user's explicit scope: + +1. Phase 5 "What we build" / "Why this is Phase 5" narrative. +2. The 3-step "Outcome to demonstrate." +3. "Source citations." +4. "Connects to" (Phase 6). +5. Both "Preconditions to verify before starting." +6. The OQ-2 resolution text. +7. Departure 2 text. +8. (Reverse check) Spec elements — Primary Flow, Alternate Flows, Edge Cases, Coordinations, Out of Scope — that go + beyond anything named in the seven areas above, and whether `artifacts/decision-log.md` evidences each. + +Excluded: the source artifact (`source-han-cleanup-plan.md`) itself. The outline is treated as the desired state +directly; the source is one level further removed and out of scope for this comparison. + +## Actors and Modes Observed + +The outline's Phase 5 entry addresses two actor types without naming them as roles: the person installing or updating +a plugin ("the person installing it," "a machine with an out-of-date companion") and, implicitly, the maintainer who +writes the version statements (never named directly, only implied by "every plugin states"). It describes only +interactive, one-machine-at-a-time install/update scenarios — no batch, API, or agent-driven install surface is named. +It distinguishes two install channels only in the preconditions ("whether the install channels read and enforce"), +not in the main body. The spec's "Actors and Triggers" section makes the maintainer role explicit and scopes the +person-actor to "the first channel" specifically — a narrowing addressed in Findings below. + +## Summary + +Compared the Phase 5 entry of the build-phase outline (desired state) against the current draft feature spec and its +decision log (current state), current-state-toward-desired-state. Both named preconditions are addressed and two of +three outcome-demonstration steps line up cleanly; one gap narrows the outline's channel-agnostic commitment to a +first-channel-only guarantee, and one gap closes an option the outline's own precondition had left open. The reverse +check found two spec additions beyond the outline, both evidenced in the decision log. + +| Category | Count | Description | +|----------|-------|-------------| +| Missing | 0 | Elements in desired state with no current state correspondence | +| Partial | 1 | Elements present in both but incompletely covered | +| Divergent | 1 | Elements addressing same concern in incompatible ways | +| Implicit | 0 | Assumed capabilities neither confirmed nor denied | + +Full analysis written to: `/Users/riverbailey/dev/testdouble/han/docs/plans/han-publishing-cleanup/phase-5-version-declarations/artifacts/gap-analysis-scratch.md` + +## Findings + +**GAP-001: "Every plugin states" narrows to "every first-channel-resolvable plugin states"** + +- **Category:** Partial +- **Feature/Behavior:** Whether a person can see a plugin's stated companion-version requirements regardless of which + install channel they use. +- **Current State:** `feature-specification.md` Outcome (lines 3–14) and Alternate Flows "The second channel" (lines + 58–64): "The second channel resolves no dependencies at all, so it neither reads nor enforces the statements... a + named limitation, not a gap in this phase ([D3])." `decision-log.md` D3 (lines 32–49): "Statements live where a + mechanism reads them: the first channel's dependency declarations... it gets no statements." `Out of Scope` (lines + 83–88): "Any statement on the second channel, which has no dependency mechanism to read one." +- **Desired State:** `build-phase-outline.md` Phase 5 "What we build" (lines 346–350): "Every plugin states explicitly + which versions of the other plugins it works with... each plugin's requirements are written down where the person + installing it can see them" — stated without a channel qualifier, unlike Phases 1, 3, 4, and 6, which name "the + second channel" explicitly whenever the commitment is channel-specific. Outcome-to-demonstrate step 1 (line 358): + "Open any plugin's listing and see which versions of its companion plugins it states it works with" — "any plugin's + listing," not "any plugin's first-channel listing." The OQ-2 resolution (lines 524–526) repeats the same unqualified + phrasing: "all plugins should state which versions of each other they depend on, explicitly." +- **Note:** The spec's narrowing is a deliberate, evidenced decision (D3 cites the second channel's manifest format + lacking a dependency field), not an oversight. It still falls short of the outline's repeated unqualified "every + plugin" / "any plugin's listing" language: a person installing from the second channel gets no visible version + statement at all, whereas the outline's plain-language commitment does not carve that channel out. + +**GAP-002: Spec closes the "visible-but-unenforced" option the outline's own precondition left open** + +- **Category:** Divergent +- **Feature/Behavior:** What happens to version statements on a channel that does not enforce them. +- **Current State:** `decision-log.md` D3, "Rejected alternatives" (lines 43–45): "Informational statements in + second-channel manifests — rejected: no tooling reads them, and unenforced claims drift into the docs-versus-reality + gap this cleanup exists to close." +- **Desired State:** `build-phase-outline.md` Phase 5 "Preconditions to verify before starting," first bullet (lines + 375–376): "Confirm whether the install channels read and enforce version statements. If they do not, decide whether + the statements start as visible information for people until the channels can act on them." The precondition frames + "visible but unenforced" as a live, acceptable path forward for a non-enforcing channel; the spec's decision rules + that path out entirely for the second channel rather than choosing it or leaving it open. +- **Note:** Both sides are documented decisions, not oversights, so this is Divergent rather than Missing: the + concern (what to do when a channel can't enforce) is addressed in both, but the resolutions are opposite. The + outline names an open branch; the spec eliminates that branch. Validated against the spec's own rationale (D3) and + found no other spec section that reopens or partially restores the informational option. + +## Spec Additions Beyond the Outline + +Per the explicit request, this section checks the reverse direction: capabilities the spec commits to that the +outline's Phase 5 entry does not name, and whether the decision log evidences each one. + +1. **Per-plugin release-marker backfill.** `feature-specification.md` lines 16–19, Primary Flow step 1 (lines 34–35), + and the Edge Cases table (line 69) commit to a one-time backfill of per-plugin release markers as a prerequisite to + any version statement landing. The outline's Phase 5 entry never mentions release markers; Phase 6 is where the + outline discusses the release process, and Phase 5's "Connects to" section only says Phase 6 "keeps these version + statements current," not that Phase 5 itself must first create markers to make enforcement possible. **Evidenced:** + yes — `decision-log.md` D4 (lines 51–72) cites codebase evidence (`git tag --list` showing 27 suite-level tags and + zero per-plugin markers; `docs/semantic-versioning.md`) and platform evidence (official plugin-dependencies + documentation) for why the backfill is a necessary, in-scope precondition rather than scope creep. + +2. **Range-conflict refusal between two installed plugins' statements for the same companion.** Primary Flow step 5 + (lines 43–45): "When two installed plugins state ranges for the same companion that cannot both be satisfied, the + channel refuses the combination with a named error." The outline's three demo steps describe only a single + plugin/companion pair going out of date (step 3), not two installed plugins disagreeing about a shared companion's + acceptable range. **Evidenced:** yes — `decision-log.md` T1 reference and `feature-technical-notes.md` T1 (lines + 3–19) cite the official plugin-dependencies documentation's `range-conflict` behavior as the platform mechanism + this follows from; the addition is a documented consequence of the enforcement the outline commits to, not an + independent invention. + +## Areas Needing Separate Analysis + +- **Whether "every plugin's requirements... where the person installing it can see them" (outline) is satisfiable for + the second channel in some non-statement form** — e.g., documentation-only guidance, as D3 gestures at ("its + companion guidance stays in the documented install instructions, unchanged"). Confirming whether that documentation + actually names version ranges today, or only install steps, would need a check of the second channel's current + install-instructions content, which is out of scope here. +- **The Phase 4 dependency-edge list in OI-1** — the spec's own open item flags that the exact set of edges receiving + statements is not yet confirmed against Phase 4's merged state. This is a spec-internal open item, not an + outline-vs-spec gap, but it will affect whether GAP-001's channel-scope finding applies uniformly across all edges + once Phase 4 lands. diff --git a/docs/plans/han-publishing-cleanup/phase-5-version-declarations/artifacts/team-findings.md b/docs/plans/han-publishing-cleanup/phase-5-version-declarations/artifacts/team-findings.md new file mode 100644 index 00000000..59c7bac9 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-5-version-declarations/artifacts/team-findings.md @@ -0,0 +1,174 @@ +# Team Findings: Declare the Plugin Versions That Work Together + +<!-- Findings from the review team, recorded as F# entries. Major findings carry the full field set; minor edits are +one-line bullets. The F# counter is shared across both sections. --> + +## Major findings + +### F1: Enforcement does not reach pre-existing installations until the depending plugin upgrades + +- **Agent:** junior-developer +- **Finding:** Statements ship inside the depending plugin's own manifest, so a person who updates only a companion + while keeping a pre-statement copy of the depending plugin gets no protection: the exact mismatch the phase exists + to catch. +- **Resolution:** The reach limit is stated in the Outcome, an edge-case row covers the companion-updated case, and + the shipping release recommends a full-suite update. +- **Resolved by:** evidence +- **Affected decisions:** D2 +- **Affected tech-notes:** — +- **Changed in spec:** Outcome; Edge Cases and Failure Modes + +### F2: A single backfilled marker collapses each range to one resolvable version + +- **Agent:** junior-developer +- **Finding:** With one marker per companion, every "at-or-above" range resolves only to that single current version; + older versions the range nominally allows have no marker. +- **Resolution:** Stated plainly in the alternate flow: ranges are effectively single-valued until markers accumulate + from Phase 6 onward. No extra historical markers are backfilled. +- **Resolved by:** evidence +- **Affected decisions:** D4 +- **Affected tech-notes:** — +- **Changed in spec:** Alternate Flows and States + +### F3: The entire payoff rested on one uncorroborated external source + +- **Agent:** junior-developer (and devops-engineer, who rated the missing verification gate the most load-bearing + fix) +- **Finding:** Every enforcing behavior traced to a single documentation fetch; the outline's own precondition + (confirm the channel enforces) was treated as settled without a trial. +- **Resolution:** A live clean-machine verification trial is now a gate in the primary flow before any statement + reaches installers; OI-2 records that a failed trial pauses the phase. T1 carries the single-source flag. +- **Resolved by:** evidence +- **Affected decisions:** D5 (added) +- **Affected tech-notes:** T1 +- **Changed in spec:** Primary Flow; Open Items + +### F4: The bundle's six statements and range intersection need atomic co-movement on major bumps + +- **Agent:** junior-developer (and devops-engineer on pre-ship intersection checking) +- **Finding:** A future companion major bump must move every edge to that companion, including the bundle's, in one + release, or the intersection goes empty and the bundle refuses to install; and nothing required proving the + authored ranges admit a common solution before ship. +- **Resolution:** The major-bump edge row names the bundle's co-movement; a pre-ship common-solution check per stage + is part of D5. +- **Resolved by:** evidence +- **Affected decisions:** D5 +- **Affected tech-notes:** — +- **Changed in spec:** Edge Cases and Failure Modes; Primary Flow + +### F6: Markers narrowed to companion plugins only + +- **Agent:** junior-developer +- **Finding:** Backfilling a marker for every plugin included plugins no statement resolves against + (`Category: YAGNI candidate`); the strictly simpler companion-only set satisfies the same evidence. +- **Resolution:** The backfill covers only the depended-upon plugins; Phase 6 produces markers for all plugins going + forward. Recorded in D4 and Out of Scope. +- **Resolved by:** evidence +- **Affected decisions:** D4 +- **Affected tech-notes:** — +- **Changed in spec:** Primary Flow; Out of Scope + +### F7: "Current released version" pinned to the latest published suite release + +- **Agent:** junior-developer (and devops-engineer on marker commit provenance) +- **Finding:** On the working branch, manifest versions can be ahead of the last published release; an undefined + baseline could mark unreleased numbers, and a marker on the wrong content would serve wrong code under a released + name. +- **Resolution:** The baseline is defined as the per-plugin versions of the newest suite release the marketplace + serves, and each marker points at the content that shipped under that release. +- **Resolved by:** evidence +- **Affected decisions:** D4 +- **Affected tech-notes:** — +- **Changed in spec:** Actors and Triggers; Primary Flow + +### F8: No recovery story for a bad statement + +- **Agent:** junior-developer (and devops-engineer on rollback latency) +- **Finding:** A mis-authored statement was not hot-fixable: corrections need a new release plus a fresh marker, and + auto-update confined to the bad range can pin users below later fixes. +- **Resolution:** A dedicated alternate flow states the rollback (revert the manifests; markers stay), the correction + path and its latency, and the wide-start posture that makes the latency survivable. +- **Resolved by:** evidence +- **Affected decisions:** D5 +- **Affected tech-notes:** — +- **Changed in spec:** Alternate Flows and States + +### F9: The pre-release baseline hazard on the alpha line + +- **Agent:** devops-engineer (junior-developer had flagged the opt-in clause as YAGNI; the alpha-line context + reversed that) +- **Finding:** If a companion's baseline version is a pre-release, a plain same-major statement excludes the very + marker it points at, disabling every dependent even when everything else is done correctly. +- **Resolution:** The precondition and an edge-case row require statements to opt into the pre-release form whenever + the baseline is one; the opt-in clause is retained as load-bearing, reversing the earlier YAGNI call. +- **Resolved by:** evidence +- **Affected decisions:** D2 +- **Affected tech-notes:** — +- **Changed in spec:** Actors and Triggers; Edge Cases and Failure Modes + +### F10: Markers travel separately from merged changes + +- **Agent:** devops-engineer +- **Finding:** The spec guarded only authoring order and called statement-before-marker unreachable; the real hazard + is the reverse partial state, statements merged while tags were never pushed, which disables every dependent at + once. +- **Resolution:** Marker presence on the shared repository is a confirmed gate before any stage ships, a post-ship + check confirms every referenced marker resolves, and the edge table names this as the primary failure mode. +- **Resolved by:** evidence +- **Affected decisions:** D4, D5 +- **Affected tech-notes:** T1 +- **Changed in spec:** Primary Flow; Edge Cases and Failure Modes + +### F11: The big-bang landing was replaced with staged rollout + +- **Agent:** devops-engineer +- **Finding:** "Statements land as one change across the suite" converted the bundle from no-markers-required to + six-markers-required atomically, with no canary and no incremental signal. +- **Resolution:** D5 stages the rollout: one low-traffic edge, then single edges, the bundle last; the one-change + approach is a recorded rejected alternative. +- **Resolved by:** evidence +- **Affected decisions:** D5 +- **Affected tech-notes:** — +- **Changed in spec:** Primary Flow; Coordinations + +### F12: The Phase 5-to-6 gap had no marker production + +- **Agent:** devops-engineer +- **Finding:** A release landing between the phases would produce no new markers, silently pinning users to the + backfilled versions or disabling dependents on a major bump: reproducing the frozen-version harm Phase 3 removes. +- **Resolution:** A named gap plan is a precondition: either Phase 6's mechanism is in place first, or a manual + marker step is part of every release in the window, with a matching edge-case row. +- **Resolved by:** evidence +- **Affected decisions:** D5 +- **Affected tech-notes:** — +- **Changed in spec:** Actors and Triggers; Edge Cases and Failure Modes; Out of Scope + +### F13: The channel-unqualified outline wording is deliberately narrowed + +- **Agent:** gap-analyzer (GAP-001) +- **Finding:** The outline says "every plugin states" without a channel qualifier; the spec restricts statements to + the first channel. +- **Resolution:** The narrowing is stated in the Outcome and recorded in D3 as deliberate: the second channel has no + mechanism to read a statement. +- **Resolved by:** evidence +- **Affected decisions:** D3 +- **Affected tech-notes:** — +- **Changed in spec:** Outcome + +### F14: The outline's "visible but unenforced" fallback is deliberately closed + +- **Agent:** gap-analyzer (GAP-002) +- **Finding:** The outline's precondition left open statements starting as visible information on a non-enforcing + channel; the spec rejects that outright. +- **Resolution:** D3 records the closure and the replacement: if the verification trial disproves enforcement, the + phase pauses and the question returns to the team rather than shipping decoration. OI-2 carries it. +- **Resolved by:** evidence +- **Affected decisions:** D3 +- **Affected tech-notes:** — +- **Changed in spec:** Open Items + +## Minor edits + +- F5: OI-1's edge list corrected: Planning depends on Communication and Core, not Core alone — junior-developer — + Open Items +- F15: Summary placeholders replaced with consulted agents and key adjustments — junior-developer — Summary \ No newline at end of file diff --git a/docs/plans/han-publishing-cleanup/phase-5-version-declarations/feature-specification.md b/docs/plans/han-publishing-cleanup/phase-5-version-declarations/feature-specification.md new file mode 100644 index 00000000..65a413f0 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-5-version-declarations/feature-specification.md @@ -0,0 +1,160 @@ +# Feature Specification: Declare the Plugin Versions That Work Together + +Every plugin that depends on another now states which versions of that companion it works with. The first install +channel enforces those statements at install and update time. The rollout is staged and verified, so the statements +can never disable installs on their way in. + +## Outcome + +Each dependency a plugin declares now carries a version statement: the companion works when its version is in the +same major line at or above the stated release +([D2](artifacts/decision-log.md#d2-same-major-version-statements)). + +The first install channel enforces these statements. Installs and updates resolve companions within the stated +range. A combination that cannot be satisfied is refused with a named error, instead of being assembled silently +([T1](artifacts/feature-technical-notes.md#t1-how-the-first-channel-resolves-version-statements)). + +Enforcement reaches an existing installation only after the depending plugin itself upgrades to a statement-carrying +version. Someone who updates only a companion, while keeping a pre-statement copy of the plugin that depends on it, +gets no protection yet. The release that ships this phase says so and recommends a full-suite update +([D2](artifacts/decision-log.md#d2-same-major-version-statements)). + +The second channel resolves no dependencies at all, so it neither reads nor enforces the statements. That is a named +limitation, and it deliberately narrows the outline's channel-unqualified wording +([D3](artifacts/decision-log.md#d3-first-channel-only)). + +Enforcement only works if the channel can find each companion version by its release marker. Today, releases are +marked at the suite level only. This phase backfills per-plugin release markers for the companion plugins, each +one pointing at the exact content that shipped as that released version +([D4](artifacts/decision-log.md#d4-per-plugin-release-markers)). + +The rollout itself is staged, gated by a live verification trial, and reversible +([D5](artifacts/decision-log.md#d5-staged-verified-reversible-rollout)). + +## Actors and Triggers + +- **Actors**: People installing or updating Han plugins from the first channel; maintainers declaring the statements + and running the rollout. +- **Triggers**: The maintainer backfills markers, verifies resolution live, then lands statements in stages. The + channel enforces on every subsequent install and update. +- **Preconditions**: Phase 4 is complete, so every remaining declaration is true. The baseline for every marker and + statement is the set of per-plugin versions in the latest published suite release, not the working branch's + in-flight numbers ([D4](artifacts/decision-log.md#d4-per-plugin-release-markers)). When a companion's baseline is a + pre-release form, its statements deliberately opt into that form. A plain same-major range would exclude + pre-releases and strand the marker it points at + ([D2](artifacts/decision-log.md#d2-same-major-version-statements)). Marker production for future releases must be + covered before statements reach installers. Either the release-process phase's mechanism is in place, or a manual + marker step is a named part of every release in the gap + ([D5](artifacts/decision-log.md#d5-staged-verified-reversible-rollout)). + +## Primary Flow + +1. The maintainer backfills one release marker per companion plugin, at its latest published release, pointing at the + content that shipped under that release. The markers are confirmed present on the shared repository before + anything else proceeds. Markers travel separately from merged changes, and a statement served without its + marker disables the depending plugin + ([D4](artifacts/decision-log.md#d4-per-plugin-release-markers)) + ([T1](artifacts/feature-technical-notes.md#t1-how-the-first-channel-resolves-version-statements)). +2. A live verification trial runs before any statement reaches general installers. On a clean machine, one + statement-bearing edge is installed from the channel against the pushed markers. This confirms three things: a + compatible companion resolves, an unsatisfiable statement produces the channel's named missing-marker error, and + an out-of-range companion is refused. The trial is what corroborates the channel's externally-documented behavior + ([D5](artifacts/decision-log.md#d5-staged-verified-reversible-rollout)). +3. Statements then land in stages: one low-traffic edge first, verified end to end, then the remaining single edges, + and the all-in-one bundle's six statements last. A single bad marker among six would break the suite's + most-installed path ([D5](artifacts/decision-log.md#d5-staged-verified-reversible-rollout)). +4. Before each stage ships, the authored ranges are checked to admit a common solution for every supported install + combination. So a refused combination can only come from a user mixing outside versions, never from the suite's + own statements ([D5](artifacts/decision-log.md#d5-staged-verified-reversible-rollout)). +5. From then on, installing a plugin resolves each companion to the newest marked version satisfying the statement. + Updates are offered within the allowed range, and an unsatisfiable combination is refused with a named error that + the person resolves deliberately + ([T1](artifacts/feature-technical-notes.md#t1-how-the-first-channel-resolves-version-statements)). + +## Alternate Flows and States + +### A person runs a companion older than a plugin's statement allows + +- **Entry condition:** A statement-carrying plugin is installed while its companion sits below the stated range. +- **Sequence:** The channel resolves the companion up into the stated range as part of the install, or refuses with a + named error if it cannot. The mismatch is surfaced instead of silently accepted. Until more release markers + accumulate, each range resolves to a single marked version in practice, since the backfill marks only the latest + published release ([D4](artifacts/decision-log.md#d4-per-plugin-release-markers)). +- **Exit:** The person ends with a compatible pair or an explicit error naming what to update. + +### A statement turns out wrong after people have resolved it + +- **Entry condition:** A shipped statement is too strict, names the wrong line, or otherwise misfires. +- **Sequence:** The statements are file content, so the rollback is reverting them. The backfilled markers are + additive and safe to leave. For people who already resolved the bad statement, the correction arrives as a new + release of the depending plugin, which itself needs a fresh marker. So recovery latency is a release cycle. That + is why statements start deliberately wide and tighten only after the verification trial proves the mechanics + ([D5](artifacts/decision-log.md#d5-staged-verified-reversible-rollout)). +- **Exit:** New installs stop seeing the bad statement immediately; already-resolved installations pick up the + correction at their next update. + +### The second channel + +- **Entry condition:** A person installs plugins from the second channel, which resolves no dependencies. +- **Sequence:** Nothing changes there. Companion guidance for that channel remains the documented install + instructions, unchanged by this phase ([D3](artifacts/decision-log.md#d3-first-channel-only)). +- **Exit:** Second-channel behavior is exactly as before. + +## Edge Cases and Failure Modes + +| Condition | Required Behavior | +| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | +| Statements are served while their markers are missing from the shared repository | This is the primary failure mode, not an unreachable one: the marker-confirmation gate in the flow blocks the stage, and a post-ship check confirms every referenced marker resolves. | +| A release happens after this phase ships but before the release process produces markers | The named gap plan applies: a manual marker step is part of every release in the window, so dependents are neither pinned to stale versions nor disabled. | +| A companion updates while the depending plugin still runs a pre-statement version | No enforcement fires yet; protection begins when the depending plugin upgrades. The shipping release says so. | +| A future release bumps a companion's major version | Every edge to that companion, including the all-in-one bundle's, moves in the same release, or the intersection goes empty and the bundle refuses to install. | +| A companion's baseline version is a pre-release form | Its statements opt into the pre-release form deliberately; a plain range would exclude the only marker that exists. | +| A person already runs a combination outside the new statements | Nothing breaks retroactively; the statements apply when the depending plugin next installs or updates. | + +## Coordinations + +| Coordinating System | Direction | Interaction | Ordering / Consistency Requirement | +| ------------------------ | --------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | +| First install channel | outbound | Reads statements, resolves companions by release marker, enforces ranges | Markers confirmed on the shared repository before any statement stage ships ([T1](artifacts/feature-technical-notes.md#t1-how-the-first-channel-resolves-version-statements)). | +| The release process | outbound | Hands off: produces per-plugin release markers on every future release | The backfill and the gap plan are this phase's; ongoing production is Phase 6's ([Phase 6 of the outline](../build-phase-outline.md#phase-6)). | +| Second install channel | none | Resolves no dependencies; unaffected | Named limitation, documented, nothing to change. | + +## Out of Scope + +- Teaching the release process to produce the markers and update statements on future releases. That is Phase 6; this + phase hands it a working baseline and covers the gap with a named manual step. +- Any statement on the second channel, which has no dependency mechanism to read one. +- Changing which dependencies exist. Phase 4 settled the edges; this phase only adds version statements to them. +- Release markers for plugins no statement resolves against. Only companions need markers for this phase; Phase 6 + produces markers for every plugin going forward ([D4](artifacts/decision-log.md#d4-per-plugin-release-markers)). + +## Open Items + +- **OI-1:** Confirm the exact set of dependency edges receiving statements by reading the post-Phase-4 tree. The + expected list: Core to Communication; Planning to Communication and Core; Coding to Communication and Core; GitHub + to Communication and Core; Reporting to Communication and Core; Atlassian's four; and the all-in-one bundle's six. + - **Resolves when:** Phase 4 merges and the edge list is read from the tree, not from this spec. + - **Blocks implementation:** No. It fixes the work list, not the behavior. +- **OI-2:** The verification trial doubles as the unresolved outline precondition: whether the channel enforces + statements as its documentation describes. If the trial disproves the documented enforcement, the phase pauses and + the question returns to the team. Unenforced statements do not ship as decoration. + - **Resolves when:** The trial in the primary flow runs. + - **Blocks implementation:** Yes for landing statements; no for the marker backfill, which is additive and safe. + +## Summary + +- **Outcome delivered:** Every true dependency carries an enforced version statement, mismatches surface as named + errors or resolved upgrades, markers exist for the channel to resolve, and the rollout is staged, verified live, + and reversible. +- **Primary actors:** People installing or updating from the first channel; the maintainer running the rollout. +- **Decisions settled by evidence:** 4. See [artifacts/decision-log.md](artifacts/decision-log.md) +- **Decisions settled by user input:** 0 (the phase itself was commissioned by the user's OQ-2 decision in the + outline). See [artifacts/decision-log.md](artifacts/decision-log.md) +- **Sub-agents consulted:** junior-developer, devops-engineer, gap-analyzer. See + [artifacts/team-findings.md](artifacts/team-findings.md) +- **Key adjustments from review:** The rollout became staged, live-verified, and reversible after review showed a + suite-wide install break was one forgotten step away. The pre-release baseline hazard on the current alpha line was + named and handled. The enforcement-reach limit for existing installations was made explicit. Markers narrowed to + companion plugins only. See [artifacts/team-findings.md](artifacts/team-findings.md) +- **Remaining open items:** 2 +- **Technical notes:** 1. See [artifacts/feature-technical-notes.md](artifacts/feature-technical-notes.md) diff --git a/docs/plans/han-publishing-cleanup/phase-6-release-process/artifacts/decision-log.md b/docs/plans/han-publishing-cleanup/phase-6-release-process/artifacts/decision-log.md new file mode 100644 index 00000000..ea2e0f42 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-6-release-process/artifacts/decision-log.md @@ -0,0 +1,166 @@ +# Decision Log: Teach the Release Process About All Four Publishing Surfaces + +## Trivial decisions + +- D1: Spec location — the spec lives at `docs/plans/han-publishing-cleanup/phase-6-release-process/`, nested beside + the build phase outline that spawned it. — Referenced in spec: none (organizational). + +## Full decisions + +### D2: The repository is the source of truth + +- **Question:** Where does a release get its list of plugins, and what counts as a plugin? +- **Decision:** From the repository itself: a plugin is a top-level directory carrying the first channel's manifest, + and the all-in-one bundle is the one plugin whose role is to install the others. The listing files become outputs + the release maintains, never inputs it trusts. A discovered plugin missing from a surface it belongs on stops the + release with a named report, and so does a listing entry naming a plugin that no longer exists, kept as a cheap + symmetric rail without a claimed incident behind it. +- **Rationale:** The root cause of the publishing rot is that the release reads its world from one listing file, so + it structurally cannot see what that file omits. Roughly twenty releases missed the second channel's decay because + the problem was invisible from inside the release. Discovery needs a deterministic discriminator or it is not + implementable. +- **Evidence:** Codebase: the release skill reads its plugin list from the first channel's storefront listing + (`.claude/skills/han-release/SKILL.md` lines 37-48) and updates only the first channel's manifests and listing + (lines 234, 326); eleven top-level directories carry `.claude-plugin/plugin.json`, distinguishing them from + `docs/`, `images/`, and the rest. Source analysis: "The release process only knows about half the world" + ([`../source-han-cleanup-plan.md`](../source-han-cleanup-plan.md#the-publishing-pipeline-the-actual-problem)). +- **Rejected alternatives:** + - Keeping the listing as input and adding the missing surfaces to it — rejected: a stale-able input stays + stale-able; the Linear gap happened exactly this way. +- **Linked technical notes:** — +- **Driven by findings:** F2 (the plugin discriminator), F7 (the orphan rail's honest labeling) +- **Dependent decisions:** D3, D4 +- **Referenced in spec:** Outcome; Primary Flow; Alternate Flows and States; Edge Cases and Failure Modes + +### D3: The four surfaces and what current means + +- **Question:** Which surfaces does a release update, what does "up to date" mean on each, and how is the target + version decided? +- **Decision:** Four surfaces. On the first channel: each plugin's version record, and the storefront listing with + its per-plugin versions. On the second channel: each plugin's version record, and the storefront listing, which + carries no version numbers, so current there means complete presence. The release keeps today's bump-proposal step: + it proposes a semantic bump per changed plugin, the maintainer confirms once, and that one confirmed target is what + every surface receives. Where the two channels' records disagree beforehand, the first channel's record is the + baseline, because the second channel's records are the ones that froze. +- **Rationale:** These are the four places the source analysis counted, each with a distinct staleness mode. A single + confirmed target per plugin is what makes "both channels state the same version" achievable; two baselines would + reintroduce drift at the moment of sync. +- **Evidence:** Codebase: `.claude-plugin/plugin.json` per plugin and `.claude-plugin/marketplace.json` (versioned + entries); `.codex-plugin/plugin.json` per plugin (versioned) and `.agents/plugins/marketplace.json` (entries carry + no version field); the bump-proposal interaction in `.claude/skills/han-release/SKILL.md` (its steps 3b-3d). + Phase 3 spec: the second channel's records are the frozen ones + (`../phase-3-unfreeze-versions/feature-specification.md`). +- **Rejected alternatives:** + - Treating the second channel's listing as versioned like the first's — rejected: it has no version field, and + inventing one duplicates the per-plugin records. + - Deciding targets per channel — rejected: two baselines drift; one confirmed target is the sync. +- **Linked technical notes:** — +- **Driven by findings:** F4 (the bump-proposal step and the drift baseline made explicit) +- **Dependent decisions:** — +- **Referenced in spec:** Outcome; Primary Flow; Coordinations + +### D4: The bundle exception lives in one durable place + +- **Question:** The all-in-one bundle cannot be published to the second channel. How do the release process and the + later automated check both know that, permanently, without either flagging it forever? +- **Decision:** One durable exception record, read by both the release process and the Phase 7 check, holding a + single named allowance: the bundle's absence from the second channel. Only that record silences a missing-plugin + report; an undocumented absence always stops the release. A record that exists but cannot be read fails closed and + names itself as the fault, distinctly from a missing-plugin report. No general multi-exception format is designed + now; that question reopens if a concrete second exception ever arises. +- **Rationale:** Two independent copies would eventually disagree, and a hardcoded allowance inside either tool is + invisible to the other. Exactly one exception exists, so a general format is flexibility without evidence. The + fail-closed-and-name-itself rule keeps a corrupted record from either blocking releases with a misleading gap + report or, worse, silencing real gaps. +- **Evidence:** Source analysis: "the check needs to know about it permanently rather than flagging it forever". + Codebase: the `han` directory has no second-channel manifest, uniquely among the eleven; `README.md` lines 74-76 + document the channel's bundle limitation. Outline: Phase 6 preconditions (`../build-phase-outline.md#phase-6`). +- **Rejected alternatives:** + - Hardcoding the exception in the release process and again in the check — rejected: duplicated knowledge drifts. + - A general exception format designed now — rejected: one real exception exists; the simpler single allowance + satisfies the same evidence, with the reopening trigger named. +- **Linked technical notes:** — +- **Driven by findings:** F3 (the default membership rule and naming the bundle), F6 (single allowance, general + format deferred), F11 (fail closed and name itself), F14 (the precondition's requirement-versus-placement split recorded as OI-1) +- **Dependent decisions:** — +- **Referenced in spec:** Outcome; Actors and Triggers; Primary Flow; Edge Cases and Failure Modes; Coordinations; + Out of Scope; Open Items + +### D5: Markers, statements, and the publish boundary + +- **Question:** Phase 5 hands this phase the markers and statements. Where does a release actually break, and what + order and verification protect it? +- **Decision:** Three commitments. Ordering: per-plugin markers reach the shared repository before, or atomically + with, any change that moves version statements, because the four surface files land together in one change while + markers and the release record travel separately; the wrong order can refuse every install of the bundle. Re-run + safety: the checks read the shared repository's real state, including marker presence and the release record, not + only local file content, so a half-landed publish is detected rather than invisibly skipped. Reconciliation: after + publishing, the process reads every surface, marker, and the release record back from the shared repository and + fails loudly with a named report on anything that did not land. Every release also produces a per-plugin marker for + each released plugin, new behavior relative to today's suite-only marker, and moves every edge to a companion in + the same release as that companion's major bump. +- **Rationale:** The review relocated the real partial-failure boundary: the four files are effectively atomic under + one change, while the markers and the release record are separate acts that can half-land. A file-state re-run + check passes cleanly over a missing marker, reproducing the silent decay this cleanup exists to end, one axis over. +- **Evidence:** Phase 5 spec: statement-without-marker disables the depending plugin, and its gap plan expects this + phase to take over marker production (`../phase-5-version-declarations/feature-specification.md`). Codebase: the + release skill's publish sequence pushes the change before its markers today + (`.claude/skills/han-release/SKILL.md` line 335), and its final report recounts local actions without reading the + shared state back (lines 350-356). +- **Rejected alternatives:** + - Treating the four surfaces as the partial-failure unit — rejected: they land together; the boundary that breaks + is markers and the release record. + - Leaving marker production manual indefinitely — rejected: a forgotten manual step recreates the + disable-at-install failure Phase 5 guards against. +- **Linked technical notes:** — +- **Driven by findings:** F5 (in-spec definitions; markers are new), F8, F9, F10 (the three publish-boundary + commitments) +- **Dependent decisions:** D6, D7 +- **Referenced in spec:** Outcome; Primary Flow; Alternate Flows and States; Edge Cases and Failure Modes; + Coordinations + +### D6: Rehearsal mode + +- **Question:** Is rehearsal an existing capability, and when is it required? +- **Decision:** Rehearsal is new behavior. It reports the discovered plugin list, per-surface results, the version + plan, and the bundle exception as a known allowance, and it writes no surface, publishes nothing, pushes nothing, + and produces no markers. It still shows the version plan for review. Whenever a release moves any version + statement or includes a major bump, the maintainer sees and acknowledges the rehearsal view before anything + publishes: the preview is opt-out on risky releases, not opt-in. +- **Rationale:** The release process today has no dry run, and its publish gate defaults to continuing without a + pause. A release that can refuse every bundle install must not reach that state through a default path with no + human checkpoint; making the preview opt-out for exactly those releases puts the guardrail where the risk is. +- **Evidence:** Codebase: the release skill's draft path still commits, tags, and publishes a draft release, and its + pause option defaults off (`.claude/skills/han-release/SKILL.md` steps 8-9 and its `pause_before_publish` + default). Review: the rollout-safety finding that the default path was deploy-without-preview. +- **Rejected alternatives:** + - Rehearsal as an always-optional trigger — rejected for statement-moving and major-bump releases: recovery from a + bad statement takes a release cycle, so skipping the preview is the expensive mistake. + - Requiring rehearsal before every release — rejected: routine releases with no statement movement carry none of + the refusal risk, and a mandatory step people resent gets skipped or hollowed out. +- **Linked technical notes:** — +- **Driven by findings:** F1 (rehearsal named as new and bounded), F12 (opt-out preview on risky releases) +- **Dependent decisions:** — +- **Referenced in spec:** Actors and Triggers; Primary Flow; Edge Cases and Failure Modes + +### D7: A loud, recorded hotfix override + +- **Question:** The release stops on any gap. What happens when an urgent fix must ship while an unrelated gap is + open? +- **Decision:** A deliberate, single-use override ships the release and names every deferred gap in the release's own + notes, so the bypass is loud and auditable. It is never the default, and the next ordinary release stops on the + same gaps again. +- **Rationale:** Without an override, the completeness policy blocks incident recovery on unrelated bookkeeping, + which pressures people to disable the whole gate: the exact failure the source analysis warns about. A loud, + recorded door keeps the paved path easier than the shortcut while giving urgency a way through. +- **Evidence:** Review: the operational finding that gap-stops couple incident recovery to unrelated completeness. + Source analysis: "turn the check on first and it fails on everything, someone disables it, and you are back where + you started" names the disable-under-pressure failure this door prevents. +- **Rejected alternatives:** + - No override — rejected: it converts the safety gate into the thing that gets switched off during the first real + incident. + - A silent skip flag — rejected: an unrecorded bypass is how gaps go invisible for twenty releases. +- **Linked technical notes:** — +- **Driven by findings:** F13 +- **Dependent decisions:** — +- **Referenced in spec:** Alternate Flows and States; Summary diff --git a/docs/plans/han-publishing-cleanup/phase-6-release-process/artifacts/gap-analysis-scratch.md b/docs/plans/han-publishing-cleanup/phase-6-release-process/artifacts/gap-analysis-scratch.md new file mode 100644 index 00000000..827cb404 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-6-release-process/artifacts/gap-analysis-scratch.md @@ -0,0 +1,126 @@ +# Gap Analysis: Build Phase Outline Phase 6 vs. Phase 6 Draft Feature Specification + +## Comparison Direction + +Current state: `docs/plans/han-publishing-cleanup/phase-6-release-process/feature-specification.md` (plus its +`artifacts/decision-log.md`) — the draft spec that must satisfy the outline's commitment. + +Desired state: the Phase 6 entry (`{#phase-6}`, "Teach the release process about all four publishing surfaces") of +`docs/plans/han-publishing-cleanup/build-phase-outline.md`. + +Comparison performed at the behavioral level only (no implementation, data-type, or file-format comparison beyond what +both documents themselves name). + +## Scope + +Comparison areas: the Phase 6 outline entry's What we build (including the Phase 5 companion-version-statement +hand-off), Why, four-step Outcome to demonstrate, Source citations, Connects to (Phase 7), and both Preconditions to +verify — checked against the spec's Outcome, Actors and Triggers, Primary Flow, Alternate Flows and States, Edge Cases, +Coordinations, Out of Scope, and Open Items, corroborated against `decision-log.md`'s D2–D5. + +Excluded: Source citations were checked only for whether they name a behavior the spec must still cover; the source +artifact itself (`source-han-cleanup-plan.md`) was not re-read, since the outline already distills it and the task +scope is outline-vs-spec. Phases 1, 3, 5, and 7 were read only as far as needed to verify Phase 6's stated dependencies +on them (their own specs were not separately audited). + +## Actors and Modes Observed + +The outline names no actors or sub-roles for Phase 6 itself; it speaks only in terms of "the release process" and "the +automated check" as systems. The spec adds two named actors (the maintainer cutting a release; every user of both +channels) and two triggers (an ordinary release run; a rehearsal run that reports without publishing) — an +interactive/batch distinction the outline implies (its demo opens in "rehearsal mode") but does not spell out as a +mode. No API or agent/integration surface is named in either document. + +## Summary + +Compared the Phase 6 entry of the build-phase outline against the phase-6 draft feature specification and its decision +log, current state (spec) toward desired state (outline). All four outcome-demonstration steps, both named connections +to earlier/later phases, and the first precondition have direct, evidenced correspondence in the spec; one precondition +is only partially settled. The spec also adds three behaviors the outline does not mention, one backed by decision-log +evidence and two without a decision-log citation. + +| Category | Count | Description | +|----------|-------|-------------| +| Missing | 0 | Elements in desired state with no current state correspondence | +| Partial | 1 | Elements present in both but incompletely covered | +| Divergent | 0 | Elements addressing same concern in incompatible ways | +| Implicit | 0 | Assumed capabilities neither confirmed nor denied | + +Full analysis written to: /Users/riverbailey/dev/testdouble/han/docs/plans/han-publishing-cleanup/phase-6-release-process/artifacts/gap-analysis-scratch.md + +## Findings + +**GAP-001: Bundle-exception precondition confirmed as a requirement, not as a resolved durable location** + +- **Category:** Partial +- **Feature/Behavior:** The outline's second precondition: "Confirm the bundle exception is recorded somewhere durable + the release process and the check can both consult" — a confirmation the outline expects done *before starting* + Phase 6. +- **Current State:** `feature-specification.md` Open Items, OI-1: "Decide where the durable bundle-exception record + lives so both the release process and the Phase 7 check read the same one... **Blocks implementation:** No — it is a + placement choice inside a settled behavior." `decision-log.md` D4 settles the *requirement* (one durable record, read + by both, silences the report) but explicitly leaves the *location* open, with the same "does not block + implementation" framing. +- **Desired State:** `build-phase-outline.md#phase-6`, "Preconditions to verify before starting": "Confirm the bundle + exception is recorded somewhere durable the release process and the check can both consult." The outline frames this + as something to verify pre-start, not a placement detail deferrable into implementation planning. +- **Note:** The behavioral requirement itself (single durable record, consulted by both systems, silences the report) + is fully and correctly captured — this is not a Missing or Divergent gap. The gap is that the outline's precondition + asks for a confirmed, existing durable location before work starts, while the spec explicitly defers that + confirmation and marks it non-blocking, which waters down the urgency the outline assigns to it. + +## Outline Elements Confirmed Covered (evidence of no gap) + +- **What we build — repository as source of truth:** Outline: "the release process starts from the plugins as they + exist in the repository, rather than trusting a list that can go stale." Spec: Outcome paragraph 1 and Primary Flow + step 1, backed by `decision-log.md` D2. +- **What we build — four surfaces, not two:** Outline: "updates all four surfaces." Spec: Outcome paragraph 1 and + Primary Flow step 4, backed by D3, which also answers outline precondition 1 ("Confirm the full list of four + publishing surfaces and what 'up to date' means for each") by naming and defining all four. +- **What we build — Phase 5 hand-off:** Outline: "It keeps the companion version statements from Phase 5 current as + new versions ship." Spec: Outcome paragraph 2 and Primary Flow step 5, backed by D5. +- **What we build — bundle exception known permanently:** Outline: "it also knows the one deliberate exception + permanently: the all-in-one bundle cannot be published to the second channel." Spec: Outcome paragraph 1 sentence 3, + Primary Flow step 2, Edge Cases table row 2, backed by D4. +- **Why — sequencing after Phases 1 and 3:** Outline: "It lands after Phases 1 and 3 so it maintains a correct state + instead of inheriting a broken one." Spec: Actors and Triggers, Preconditions: "Phases 1 and 3 are complete." +- **Outcome step 1 (rehearsal mode against the current repository):** Spec Actors and Triggers ("or runs it in + rehearsal mode") and Primary Flow step 6. +- **Outcome step 2 (lists every plugin found in the repository):** Spec Primary Flow step 1 and step 6 ("including the + discovered plugin list"). +- **Outcome step 3 (updates, or reports it would update, all four surfaces):** Spec Primary Flow step 4 and step 6 + ("the per-surface check results" / "reports everything it would do"). +- **Outcome step 4 (bundle absence reported as a known, documented exception, not an error):** Spec Primary Flow step 2 + and step 6 ("the bundle exception as a known allowance"), Edge Cases table row 2. +- **Connects to Phase 7:** Outline: "the check turns the behavior this phase teaches into something that cannot + regress." Spec: Coordinations table row "The automated completeness check (Phase 7)," backed by D4's shared durable + record. +- **Precondition 1 (four surfaces and what "current" means for each):** Fully resolved by D3, cited above. + +## Spec Additions Beyond the Outline + +The outline does not mention these three behaviors. Listed per task instructions, not counted in the gap taxonomy above +(they are current-state surface area beyond the desired state's scope, not gaps against it). + +1. **Per-plugin release marker production.** Spec Outcome paragraph 2 and Primary Flow step 5: "produces a per-plugin + release marker for every released plugin." The outline's What we build names only "keeps the companion version + statements from Phase 5 current," not markers. **Decision-log evidence:** D5 covers this, but its own evidence cites + Phase 5's spec ("the release-process hand-off in its Coordinations and D4/D5") and a codebase file + (`docs/semantic-versioning.md`) — not the outline. The addition is evidenced, but by a downstream document, not by + the desired-state artifact under comparison here. +2. **Orphan storefront-listing entry (a plugin that no longer exists in the repository but is still listed).** Spec + Edge Cases table row 1. Not named in the outline's What we build or Outcome steps. **Decision-log evidence:** none — + no D-entry's "Referenced in spec" field cites the Edge Cases table, and D2's rationale (listings become outputs, not + inputs) implies but does not name this scenario. +3. **Interrupted-release re-run safety.** Spec Edge Cases table row 3. Not named in the outline. **Decision-log + evidence:** none — no D-entry cites this edge case. + +## Areas Needing Separate Analysis + +- **Source citations accuracy.** The outline's Phase 6 source citations point into `source-han-cleanup-plan.md`; this + analysis did not re-verify those citations against the source document itself, since the task scope was outline vs. + spec. A separate pass could confirm the source material still supports the citations as the spec has evolved them. +- **Phase 5 and Phase 7 spec consistency.** Phase 6's D5 and Coordinations depend on claims about Phase 5's spec + (markers, version-statement enforcement) and Phase 7's spec (shared exception record, same rule). Those two specs + were not independently audited here; a cross-phase consistency check would confirm Phase 6's characterization of + their content is accurate rather than assumed. diff --git a/docs/plans/han-publishing-cleanup/phase-6-release-process/artifacts/team-findings.md b/docs/plans/han-publishing-cleanup/phase-6-release-process/artifacts/team-findings.md new file mode 100644 index 00000000..be7f24c6 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-6-release-process/artifacts/team-findings.md @@ -0,0 +1,181 @@ +# Team Findings: Teach the Release Process About All Four Publishing Surfaces + +<!-- Findings from the review team, recorded as F# entries. Major findings carry the full field set; minor edits are +one-line bullets. The F# counter is shared across both sections. --> + +## Major findings + +### F1: Rehearsal mode was presented as existing behavior and left undefined at its boundary + +- **Agent:** junior-developer +- **Finding:** The release skill has no dry run today; even its draft path commits, tags, and publishes a draft + release. The spec introduced rehearsal as a plain trigger with no decision behind it and no statement of what it + skips. +- **Resolution:** Rehearsal is now decision D6: named as new behavior, with a defined boundary (no surface writes, no + pushes, no publication, no markers; the version plan still shown). +- **Resolved by:** evidence +- **Affected decisions:** D6 (added) +- **Affected tech-notes:** — +- **Changed in spec:** Actors and Triggers; Primary Flow; Edge Cases and Failure Modes + +### F2: "Discovers every plugin" had no behavioral definition of a plugin + +- **Agent:** junior-developer +- **Finding:** Without a discriminator, disk discovery is not deterministically implementable; the repo root also + holds non-plugin directories. +- **Resolution:** A plugin is a top-level directory carrying the first channel's manifest; the bundle is the one + plugin whose role is to install the others. Recorded in D2. +- **Resolved by:** evidence +- **Affected decisions:** D2 +- **Affected tech-notes:** — +- **Changed in spec:** Outcome; Primary Flow + +### F3: The surface-membership default was circular + +- **Agent:** junior-developer +- **Finding:** The spec said only where a plugin does not belong; the default rule and the bundle's identity were + never stated. +- **Resolution:** Every discovered plugin belongs on all four surfaces by default; only the durable exception record + subtracts one; the bundle is named. Recorded in D4. +- **Resolved by:** evidence +- **Affected decisions:** D4 +- **Affected tech-notes:** — +- **Changed in spec:** Outcome + +### F4: The version-bump proposal step and drift baseline were missing + +- **Agent:** junior-developer +- **Finding:** The skill's core interaction, proposing bumps and gating on one confirmation, was absent, and the spec + never said which channel's record feeds the bump when the two disagree. +- **Resolution:** The proposal-and-confirm step is retained explicitly; one confirmed target per plugin feeds every + surface; the first channel's record is the baseline because the second channel's records are the frozen ones. + Recorded in D3. +- **Resolved by:** evidence +- **Affected decisions:** D3 +- **Affected tech-notes:** — +- **Changed in spec:** Primary Flow + +### F5: Marker and statement were load-bearing but undefined here + +- **Agent:** junior-developer +- **Finding:** A reader of this spec alone could not tell what a marker or a statement is, or that per-plugin markers + are new relative to today's suite-only marker. +- **Resolution:** One-line in-spec definitions added in the Outcome, with the newness stated. +- **Resolved by:** evidence +- **Affected decisions:** D5 +- **Affected tech-notes:** — +- **Changed in spec:** Outcome + +### F6: The exception record's future-flexibility clause was YAGNI + +- **Agent:** junior-developer +- **Finding:** Designing a general multi-exception format for a record with exactly one exception is flexibility + without evidence. +- **Resolution:** The record holds a single named allowance; the general-format question reopens if a concrete second + exception arises. Recorded in D4 and Out of Scope. +- **Resolved by:** evidence +- **Affected decisions:** D4 +- **Affected tech-notes:** — +- **Changed in spec:** Out of Scope; Open Items + +### F7: The orphan-entry check had no incident behind it + +- **Agent:** junior-developer +- **Finding:** The documented failure is a missing plugin; the orphan-listing case is its unevidenced symmetric + opposite (`Category: YAGNI candidate`). +- **Resolution:** Kept, because it rides the same per-surface comparison at no extra cost, and now honestly labeled + in the edge table as a symmetric rail without a claimed incident. +- **Resolved by:** evidence +- **Affected decisions:** D2 +- **Affected tech-notes:** — +- **Changed in spec:** Edge Cases and Failure Modes + +### F8: The marker-versus-statement publish order was unspecified and today's order is unsafe + +- **Agent:** devops-engineer +- **Finding:** The four surface files land together in one change, but markers travel separately, and the skill today + pushes the change before its markers; on a major bump that order can refuse every install of the bundle. +- **Resolution:** The ordering invariant is a primary-flow step: markers reach the shared repository before, or + atomically with, any statement-moving change. Recorded in D5. +- **Resolved by:** evidence +- **Affected decisions:** D5 +- **Affected tech-notes:** — +- **Changed in spec:** Primary Flow; Coordinations + +### F9: The re-run safety claim could not see the states that actually break + +- **Agent:** devops-engineer +- **Finding:** A file-state re-run check passes cleanly over a half-landed marker push or missing release record, + leaving dependents disabled indefinitely while reporting clean. +- **Resolution:** Re-run checks read the shared repository's real state, including markers and the release record; + the edge row now says exactly that. +- **Resolved by:** evidence +- **Affected decisions:** D5 +- **Affected tech-notes:** — +- **Changed in spec:** Edge Cases and Failure Modes + +### F10: Nothing verified that a publish actually landed + +- **Agent:** devops-engineer +- **Finding:** The process had a pre-publish gate but no post-publish confirmation; a transient failure between acts + leaves a split-brain release invisible until a user reports it. +- **Resolution:** A post-publish reconciliation step reads everything back from the shared repository and fails + loudly with a named report. Recorded in D5. +- **Resolved by:** evidence +- **Affected decisions:** D5 +- **Affected tech-notes:** — +- **Changed in spec:** Outcome; Primary Flow + +### F11: The exception record's unreadable state was undefined + +- **Agent:** devops-engineer +- **Finding:** A malformed record could either block releases with a misleading gap report or silence real gaps, + depending on parser charity. +- **Resolution:** Unreadable fails closed and names the record itself as the fault, distinct from a gap report. + Recorded in D4 with a matching edge row. +- **Resolved by:** evidence +- **Affected decisions:** D4 +- **Affected tech-notes:** — +- **Changed in spec:** Edge Cases and Failure Modes + +### F12: The default path published with no human checkpoint on risky releases + +- **Agent:** devops-engineer +- **Finding:** Rehearsal was opt-in and the skill's pause gate defaults off, so a release that moves statements or + majors a companion could publish with no preview, and recovery takes a release cycle. +- **Resolution:** The preview is opt-out for exactly those releases: the maintainer sees and acknowledges the + rehearsal view before anything publishes. Recorded in D6. +- **Resolved by:** evidence +- **Affected decisions:** D6 +- **Affected tech-notes:** — +- **Changed in spec:** Primary Flow + +### F13: The gap-stop had no incident door + +- **Agent:** devops-engineer +- **Finding:** With no override, an unrelated open gap blocks an urgent fix, pressuring someone to disable the whole + gate: the exact failure the source analysis warns about. +- **Resolution:** A deliberate, single-use, loud override ships the release with every deferred gap named in the + release notes. Recorded as D7 with an alternate flow. +- **Resolved by:** evidence +- **Affected decisions:** D7 (added) +- **Affected tech-notes:** — +- **Changed in spec:** Alternate Flows and States + +### F14: The bundle-exception precondition is settled in requirement, open in placement + +- **Agent:** gap-analyzer (GAP-001) +- **Finding:** The outline's precondition asks to confirm the durable record before starting; the spec settles every + behavioral requirement but defers the record's location to implementation planning. +- **Resolution:** Recorded as the deliberate split it is: OI-1 carries the placement choice, and the requirement + (one record, read by both, fails closed, single allowance) is fixed here. +- **Resolved by:** evidence +- **Affected decisions:** D4 +- **Affected tech-notes:** — +- **Changed in spec:** Open Items + +## Minor edits + +- F15: The precondition now says presence-complete and version-aligned rather than the blanket "already correct", so + the first run reads as ordinary sync, not gap repair — junior-developer — Actors and Triggers +- F16: Summary placeholders replaced with consulted agents and key adjustments — junior-developer — Summary diff --git a/docs/plans/han-publishing-cleanup/phase-6-release-process/feature-specification.md b/docs/plans/han-publishing-cleanup/phase-6-release-process/feature-specification.md new file mode 100644 index 00000000..4beea998 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-6-release-process/feature-specification.md @@ -0,0 +1,155 @@ +# Feature Specification: Teach the Release Process About All Four Publishing Surfaces + +The release process now starts from the plugins as they exist in the repository, and updates all four publishing +surfaces instead of two. It stops with a named report when anything is missing, and verifies after publishing that +everything really landed. It treats the all-in-one bundle's absence from the second channel as a permanent, documented +exception. + +## Outcome + +A maintainer cutting a release can no longer ship around a gap without seeing it. + +The release starts by discovering every plugin directly from the repository. A plugin is any top-level directory +carrying the first channel's manifest, and the all-in-one bundle is the one plugin whose role is to install the others +([D2](artifacts/decision-log.md#d2-the-repository-is-the-source-of-truth)). + +Every discovered plugin belongs on all four surfaces by default. Only a durable exception record subtracts a surface, +and today exactly one exception exists: the bundle's absence from the second channel +([D4](artifacts/decision-log.md#d4-the-bundle-exception-lives-in-one-durable-place)). + +The four surfaces are the first channel's per-plugin version records and storefront listing, and the second channel's +per-plugin version records and storefront listing +([D3](artifacts/decision-log.md#d3-the-four-surfaces-and-what-current-means)). + +When a plugin is missing from a surface it belongs on, the release stops and reports exactly what is absent, instead of +finishing quietly. + +The release also carries forward work from the previous phase. + +It produces a per-plugin release marker, the released-version record the install channel resolves against, for every +plugin it releases. Per-plugin markers are new behavior: today only a suite-level marker exists. + +It also keeps companion version statements current, the declared version ranges plugins state for each other. When a +companion's major version bumps, every edge to that companion moves in the same release +([D5](artifacts/decision-log.md#d5-markers-statements-and-the-publish-boundary)). + +Publishing is verified, not assumed. After the release publishes, the process reads every surface, marker, and the +release record back from the shared repository. It fails loudly, naming anything that did not land +([D5](artifacts/decision-log.md#d5-markers-statements-and-the-publish-boundary)). + +## Actors and Triggers + +- **Actors**: The maintainer cutting a release; every user of both channels, who receives what the release + publishes. +- **Triggers**: The maintainer runs the release process, or runs its rehearsal mode, a new capability that reports + everything without publishing anything ([D6](artifacts/decision-log.md#d6-rehearsal-mode)). +- **Preconditions**: Phases 1 and 3 are complete, so every surface is presence-complete and the version records are + aligned; the first real run performs ordinary version sync, not gap repair. The bundle + exception's durable record exists where both this process and the later automated check read it + ([D4](artifacts/decision-log.md#d4-the-bundle-exception-lives-in-one-durable-place)). + +## Primary Flow + +1. The maintainer starts a release. The process discovers the set of plugins from the repository itself + ([D2](artifacts/decision-log.md#d2-the-repository-is-the-source-of-truth)). +2. For each discovered plugin, the process checks its presence on every surface it belongs on, honoring the bundle + exception ([D3](artifacts/decision-log.md#d3-the-four-surfaces-and-what-current-means)) + ([D4](artifacts/decision-log.md#d4-the-bundle-exception-lives-in-one-durable-place)). If anything is missing, the + release stops before publishing and reports each absent plugin and surface. +3. The process proposes a version bump per changed plugin, and the maintainer confirms the plan, as today. The one + confirmed target per plugin is what every surface receives. Where the two channels' records disagree beforehand, + the first channel's record is the baseline, because the second channel's records are the ones that froze + ([D3](artifacts/decision-log.md#d3-the-four-surfaces-and-what-current-means)). +4. When the release moves any version statement or includes a major bump, the maintainer sees and acknowledges the + rehearsal view of what will change, before anything publishes. The preview is opt-out, not opt-in, for exactly the + releases that can refuse installs ([D6](artifacts/decision-log.md#d6-rehearsal-mode)). +5. The process updates the version records and listings on all four surfaces so both channels state the same released + version for every plugin. +6. The process publishes in the safe order. Per-plugin markers reach the shared repository before, or atomically + with, any change that moves version statements. A statement served without its marker disables the depending + plugin ([D5](artifacts/decision-log.md#d5-markers-statements-and-the-publish-boundary)). +7. After publishing, the process reconciles. It reads all four surfaces, every marker, and the release record back + from the shared repository. It fails loudly with a named report if anything did not land + ([D5](artifacts/decision-log.md#d5-markers-statements-and-the-publish-boundary)). + +## Alternate Flows and States + +### A new plugin exists in the repository but nowhere else + +- **Entry condition:** A plugin directory was added since the last release and no surface lists it yet. +- **Sequence:** Discovery finds it ([D2](artifacts/decision-log.md#d2-the-repository-is-the-source-of-truth)), the per-surface check reports it missing from every surface it belongs on, and + the release stops with that report. Nothing ships until the plugin is listed everywhere or deliberately excluded + with a durable record like the bundle's. +- **Exit:** The failure that created the Linear gap, a new plugin landing invisible by default, can no longer happen + silently. + +### A companion's major version bumps in this release + +- **Entry condition:** The release includes a breaking change to a plugin that others depend on. +- **Sequence:** The process updates every depending plugin's version statement to the new line, in the same release. + The acknowledged preview from the primary flow shows the moves, and the marker-first publish order holds + ([D5](artifacts/decision-log.md#d5-markers-statements-and-the-publish-boundary)). +- **Exit:** The release ships with statements and markers agreeing everywhere. + +### An urgent fix must ship while an unrelated gap is open + +- **Entry condition:** A severe problem needs a release now, and the presence check is stopping on a gap unrelated to + the fix. +- **Sequence:** The maintainer uses a deliberate, single-use override. The release ships, and every deferred gap is + named in the release's own notes, so the bypass is loud and auditable, never silent + ([D7](artifacts/decision-log.md#d7-a-loud-recorded-hotfix-override)). +- **Exit:** Users get the urgent fix; the named gaps remain visibly open until fixed, and the next ordinary release + stops on them again. + +## Edge Cases and Failure Modes + +| Condition | Required Behavior | +| -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| The storefront listing names a plugin that no longer exists in the repository | The check reports the orphan entry and the release stops; kept as a cheap symmetric rail beside the missing-plugin check, without a claimed incident behind it. | +| A plugin is deliberately absent from a surface | Only the durable exception record silences the report; an undocumented absence always stops the release. | +| The exception record exists but cannot be read | The release fails closed and names the record itself as the fault, distinctly from a missing-plugin report, so the maintainer fixes the record instead of chasing a phantom gap. | +| The release is interrupted partway | Re-running is safe because the checks read the shared repository's real state, including markers and the release record, not only the local files; a half-landed publish is detected, reported, and completed. | +| Rehearsal mode | Reports everything, including the discovered plugin list, the per-surface results, the version plan, and the bundle exception as a known allowance; writes no surface, publishes nothing, pushes nothing, and produces no markers ([D6](artifacts/decision-log.md#d6-rehearsal-mode)). | + +## Coordinations + +| Coordinating System | Direction | Interaction | Ordering / Consistency Requirement | +| ------------------------------ | --------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------ | +| Both install channels | outbound | Receive updated version records and listings on every release | The presence check passes before anything publishes; both channels end stating the same versions; the post-publish reconciliation confirms it. | +| The automated completeness check (Phase 7) | outbound | Enforces the same rule this process follows | Both read the same durable exception record, so they can never disagree about the bundle ([D4](artifacts/decision-log.md#d4-the-bundle-exception-lives-in-one-durable-place)). | +| Version statements and markers (Phase 5) | inbound | The baseline this process maintains going forward | Markers reach the shared repository before or atomically with statement-moving changes ([D5](artifacts/decision-log.md#d5-markers-statements-and-the-publish-boundary)). | + +## Out of Scope + +- Building the automated completeness check itself. That is Phase 7; this phase gives it a tree that passes and a + shared exception record. +- Correcting today's stale surfaces. Phases 1 and 3 do that; this phase keeps them correct. +- A general multi-exception record format. Exactly one exception exists; the record holds that single named + allowance. The format question reopens if a concrete second exception ever arises + ([D4](artifacts/decision-log.md#d4-the-bundle-exception-lives-in-one-durable-place)). +- Changing what a release publishes to either channel beyond versions, listings, markers, and statements. + +## Open Items + +- **OI-1:** Decide where the durable bundle-exception record lives, so both the release process and the Phase 7 check + read the same one. The behavioral requirements are already settled: one record, read by both. It fails closed, names + itself when unreadable, and holds a single named allowance. + - **Resolves when:** Implementation planning picks the location. + - **Blocks implementation:** No. It is a placement choice inside a settled behavior. + +## Summary + +- **Outcome delivered:** Releases start from the repository, cover all four surfaces, and stop loudly on gaps. They + publish in the marker-first safe order, verify afterward that everything landed, honor one durable exception, and + carry a loud override for urgent fixes. +- **Primary actors:** The maintainer cutting releases; users of both channels. +- **Decisions settled by evidence:** 6. See [artifacts/decision-log.md](artifacts/decision-log.md) +- **Decisions settled by user input:** 0. See [artifacts/decision-log.md](artifacts/decision-log.md) +- **Sub-agents consulted:** junior-developer, devops-engineer, gap-analyzer. See + [artifacts/team-findings.md](artifacts/team-findings.md) +- **Key adjustments from review:** The real publish boundary was relocated from the four files, which land together, + to the markers and release record, which travel separately. That move drives the marker-first order, the + shared-state re-run check, and the post-publish reconciliation. Rehearsal mode was named as new behavior with a + defined boundary and an opt-out preview on risky releases. A loud, recorded hotfix override was added. Discovery + and surface membership got behavioral definitions. See [artifacts/team-findings.md](artifacts/team-findings.md) +- **Remaining open items:** 1 diff --git a/docs/plans/han-publishing-cleanup/phase-7-automated-check/artifacts/decision-log.md b/docs/plans/han-publishing-cleanup/phase-7-automated-check/artifacts/decision-log.md new file mode 100644 index 00000000..617563f5 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-7-automated-check/artifacts/decision-log.md @@ -0,0 +1,101 @@ +# Decision Log: Turn On the Automated Completeness Check + +## Trivial decisions + +- D1: Spec location — the spec lives at `docs/plans/han-publishing-cleanup/phase-7-automated-check/`, nested beside + the build phase outline that spawned it. — Referenced in spec: none (organizational). + +## Full decisions + +### D2: One verdict, two enforcement moments + +- **Question:** Where does the check run, what exactly does it verify, and how does it relate to the release + process's gate without contradicting it? +- **Decision:** The guarantee is verdict equivalence, not a shared machine: given the same tree and the same + exception record, the change-time check and the release-time gate return the same verdict. The verdict evaluates + the proposed tree's end state: every plugin present on every surface it belongs on, and both channels stating the + same version per plugin. A release passes because it moves all four surfaces together in one change; a change that + moves one surface without its partners is precisely what gets blocked. The check runs automatically on every + proposed change, gates every release, and can be run on demand against any working tree. Its scope is presence and + version agreement, guarding Phases 1, 3, and 6; it does not examine ticket-file marks, dependency-declaration + truth, or version statements and markers, which the release reconciliation owns; the reopening trigger is a + statement-marker mismatch escaping that reconciliation. The deleted-plugin orphan case is kept for verdict parity + with the release process's symmetric rail. The release's loud recorded override is the only sanctioned bypass at + release time. +- **Rationale:** The two moments run in different worlds, one non-interactive beside the repository's automatic + checks and one inside an interactive release, so "one rule" could only honestly mean same-inputs-same-verdict. + Defining the verdict on the proposed end state dissolves the apparent collision with the release's tolerance of + pre-existing drift: the release fixes flagged drift by syncing, the check blocks changes that create drift, and + both call the same tree state a failure. All four surfaces are files in this repository, which is the fact that + makes a change-time verdict possible. Narrowing the guard claim keeps the spec from promising Phase 4 and Phase 5 + protection its mechanism does not provide; the outline's Phase 7 guard list was corrected to match. +- **Evidence:** Source analysis: the After diagram's gate and "Only now, turn on the automated check" + ([`../source-han-cleanup-plan.md`](../source-han-cleanup-plan.md#what-i-would-do-in-order)). Outline: Phase 7's + outcome and demo (`../build-phase-outline.md#phase-7`). Codebase: the repository runs automatic checks on proposed + changes (a continuous-integration workflow and pre-commit hooks exist), and all four surfaces are repository + files. Phase 6 spec: the release-side gate, the one-change surface sync, and the override + (`../phase-6-release-process/feature-specification.md`). +- **Rejected alternatives:** + - Release-time only — rejected: a plugin added today would still land invisible until the next release. + - A literally shared implementation across both moments — rejected as an overclaim across two runtimes; the + behavioral equivalence is the commitment, the machinery is implementation planning's. + - Extending the check to dependency-declaration truth and statement-marker consistency — rejected: declaration + truth is not mechanically checkable, and reconciliation already owns markers and statements; widening the check + duplicates a guard that exists. +- **Linked technical notes:** — +- **Driven by findings:** F1 (the verdict collision dissolved on the end-state definition), F2 (equivalence replaces + "one rule"), F3 (the guard claim narrowed and the outline corrected), F5 (all four surfaces are repository files, + stated), F6 (on-demand invocation granted), F8 (the orphan rail's parity evidence and carried caveat) +- **Dependent decisions:** D3, D4 +- **Referenced in spec:** Outcome; Actors and Triggers; Primary Flow; Edge Cases and Failure Modes; Coordinations; + Out of Scope + +### D3: Shared exception record + +- **Question:** How does the check know about the bundle exception without flagging it forever? +- **Decision:** The check reads the same durable exception record the release process reads, holding the single named + allowance. An unreadable record fails closed and names the record itself as the fault. Editing the record is itself + a change the check evaluates, so exceptions are added or removed visibly, under review. +- **Rationale:** Phase 6 settled the one-record requirement precisely so the two consumers can never disagree; this + phase inherits it rather than restating it. Making record edits reviewed changes keeps the exception list from + becoming a quiet side door. +- **Evidence:** Phase 6 spec and decision log: the durable record, its single allowance, and its fail-closed rule + (`../phase-6-release-process/artifacts/decision-log.md`, its D4). Source analysis: "the check needs to know about + it permanently rather than flagging it forever". +- **Rejected alternatives:** + - The check carrying its own copy of the exception — rejected: duplicated knowledge drifts; Phase 6 already + rejected the same alternative for the same reason. +- **Linked technical notes:** — +- **Driven by findings:** — +- **Dependent decisions:** — +- **Referenced in spec:** Outcome; Actors and Triggers; Primary Flow; Alternate Flows and States; Edge Cases and + Failure Modes; Coordinations + +### D4: Green-first arrival, and the repair door + +- **Question:** When does the check start blocking, and what happens when the check itself is the broken thing? +- **Decision:** The check begins blocking only after an observable turn-on condition: a recorded green run against + the real tree, owned by the maintainer. If that first run fails, the check does not begin blocking and the failure + goes back to the earlier phases' owners. Once on, the check is never simply disabled. Instead, a scoped, recorded + repair door exists for the one deadlock a no-disable rule creates: a change whose purpose is fixing the check or + its exception record may land past the failing check, with the bypass named visibly in that change. +- **Rationale:** The source is explicit that the order is not negotiable: turned on before the tree is ready, the + check fails on almost everything, someone disables it, and it protects nothing. But an absolute no-disable rule + wedges the repository when the check false-fails, since the fixing change would be blocked by the bug it fixes. + The repair door keeps the no-disable spirit, loud and auditable like the release override, while making the check + repairable. +- **Evidence:** Source analysis: "The order matters, and getting it wrong stops everything" + ([`../source-han-cleanup-plan.md`](../source-han-cleanup-plan.md#the-order-matters-and-getting-it-wrong-stops-everything)). + Outline: Phase 7's builds-on line and precondition (`../build-phase-outline.md#phase-7`). Phase 6 spec: the + loud-recorded override pattern the door mirrors. +- **Rejected alternatives:** + - Landing the check early in report-only mode — rejected: a warning nobody must act on trains everyone to ignore + it. + - An absolute no-disable rule with no door — rejected: it deadlocks on a false-failing check and invites the + out-of-band disable it forbids. +- **Linked technical notes:** — +- **Driven by findings:** F4 (the repair door), F9 (the observable turn-on condition and its owner) +- **Dependent decisions:** — +- **Referenced in spec:** Outcome; Actors and Triggers; Alternate Flows and States; Edge Cases and Failure Modes; + Coordinations +</content> diff --git a/docs/plans/han-publishing-cleanup/phase-7-automated-check/artifacts/gap-analysis-scratch.md b/docs/plans/han-publishing-cleanup/phase-7-automated-check/artifacts/gap-analysis-scratch.md new file mode 100644 index 00000000..4e721b95 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-7-automated-check/artifacts/gap-analysis-scratch.md @@ -0,0 +1,123 @@ +# Gap Analysis: Phase 7 Build-Phase Outline Entry vs. Phase 7 Feature Specification + +## Comparison Direction + +Desired state (reference): the Phase 7 entry ({#phase-7}) of +`/Users/riverbailey/dev/testdouble/han/docs/plans/han-publishing-cleanup/build-phase-outline.md`. + +Current state: `/Users/riverbailey/dev/testdouble/han/docs/plans/han-publishing-cleanup/phase-7-automated-check/feature-specification.md`, +cross-checked against `artifacts/decision-log.md` for evidence of any spec content beyond the outline. + +Comparison direction used: outline (desired state) toward spec (current state) for coverage gaps; spec toward outline +for scope-creep/addition checks, per explicit task instruction (bidirectional, scoped to this one phase entry). + +## Scope + +Comparison area is limited to the Phase 7 outline entry (What we build, Why, Outcome to demonstrate's 4 steps, Source +citations, Connects to, Preconditions) against the full Phase 7 feature-specification.md and its decision-log.md. +Excluded: Phases 1–6 and 8 outline entries in full (referenced only where Phase 7 cites or connects to them), and the +source-han-cleanup-plan.md (referenced only where the spec or outline cite it directly). + +## Actors and Modes Observed + +The outline names three implicit actors for Phase 7: a person running the check against the repository (interactive/ +demo mode), a contributor proposing a change (automated, triggered mode), and the maintainer cutting a release (gated, +automated mode). The spec's "Actors and Triggers" section names the same three roles explicitly: "Anyone proposing a +change," "the maintainer cutting releases," and "the future contributor who adds a plugin." Two triggers are named: +automatic on every proposed change, and as a release-process gate. No API/agent/integration surface is named in either +document beyond the repository's own automatic-check and release-process machinery. + +## Summary + +Compared the Phase 7 outline entry (desired state) to the Phase 7 feature specification and decision log (current +state). Direction: outline toward spec for coverage, spec toward outline for additions. The spec covers three of the +outline's four guarded phases and all 4 demo steps and the precondition verbatim, but the outline's explicit claim that +Phase 7 "guards the outcomes of... Phase 4" has no corresponding behavior anywhere in the spec — the spec never +mentions dependency declarations. The spec also adds two behaviors (an orphan-listing edge case and an out-of-scope +carve-out for Phase 5's version statements) that the decision log does not evidence. + +| Category | Count | Description | +|----------|-------|-------------| +| Missing | 1 | Elements in desired state with no current state correspondence | +| Partial | 0 | Elements present in both but incompletely covered | +| Divergent | 0 | Elements addressing same concern in incompatible ways | +| Implicit | 0 | Assumed capabilities neither confirmed nor denied | + +Full analysis written to: /Users/riverbailey/dev/testdouble/han/docs/plans/han-publishing-cleanup/phase-7-automated-check/artifacts/gap-analysis-scratch.md + +## Correspondence Map: Outcome-to-Demonstrate (4 steps) + +| Outline step | Spec correspondence | Verdict | +|---|---|---| +| 1. "Run the check against the current repository and watch it pass." | Spec D4 + Edge Cases row "The check's very first run against the real tree fails": the check "begins blocking only after its first green run against the real tree" (Outcome, Preconditions, Edge Cases). | Covered — the single initial passing run is addressed as the arrival condition, not as a separate ad hoc invocation, but the behavior matches. | +| 2. "On a throwaway branch, add a new plugin without publishing it anywhere." | Spec Alternate Flow "A new plugin is added without being published everywhere": entry condition "adds a plugin directory but does not list it on every surface it belongs on" (a superset of the outline's "not published anywhere" case). | Covered. | +| 3. "Run the check again and watch it fail, naming the new plugin and each surface it is missing from." | Spec Primary Flow step 3 and the same Alternate Flow: "fails the change, naming the new plugin and each surface it is missing from." | Covered — near-verbatim match. | +| 4. "Confirm the all-in-one bundle's known exception is not flagged in either run." | Spec Outcome and D3: the check "reads the same durable exception record... and never flags it," in both the pass and fail cases. | Covered. | + +Precondition: outline's "Confirm Phases 1, 3, and 6 are complete and the check's first run against the real tree +passes before it is allowed to block anything" (Phase 7, Preconditions) matches the spec's "Preconditions" bullet +verbatim in substance: "Phases 1, 3, and 6 are complete... The check's first run against the real tree passes before +it is allowed to block anything" (feature-specification.md, Preconditions). No gap. + +## Findings + +**GAP-001: Phase 7 spec does not guard Phase 4's outcome, despite the outline's explicit commitment** + +- **Category:** Missing +- **Feature/Behavior:** The check's coverage of the untrue-dependency-declaration fix (Phase 4) as one of the things + Phase 7 prevents from regressing. +- **Current State:** `feature-specification.md` contains no reference to dependency declarations anywhere — not in + Outcome, Primary Flow, Alternate Flows, Edge Cases, Coordinations, or Out of Scope. A full-text search for "depend" + in the spec and decision log returns no behavioral content (only the unrelated "Dependent decisions" metadata field + in `artifacts/decision-log.md`, lines 35, 55, 77). The spec's entire behavioral surface is presence-across-four- + surfaces and version agreement (Outcome: "does every plugin appear everywhere it should, at the right version?"). +- **Desired State:** `build-phase-outline.md`, Phase 7, "Connects to": "This is the final phase. It guards the + outcomes of [Phase 1](#phase-1), [Phase 3](#phase-3), [Phase 4](#phase-4), and [Phase 6](#phase-6) from + regressing." Phase 4's outcome is removing untrue dependency declarations (Reporting, Feedback, and Linear on + Core); nothing in the check as specified would catch a regression of that fix (e.g., a plugin re-declaring a false + dependency), because the check's discovery rule and surface list are about publishing presence, not the + dependency graph. +- **Note on internal consistency:** the spec explicitly and traceably scopes out a different phase's outcome — Phase + 5's version-compatibility statements — in its "Out of Scope" section, and that exclusion is consistent with the + outline (the outline's "Connects to" for Phase 7 never claims to guard Phase 5). But Phase 4 receives no equivalent + explicit exclusion; it is simply absent, with no decision-log entry (D2, D3, D4) explaining or ratifying the + omission. This makes the scoping inconsistent: one outline-adjacent phase (5) is deliberately and visibly excluded, + the other (4), which the outline explicitly claims is guarded, is silently dropped. + +## Additions Beyond the Outline (spec toward outline direction) + +The outline's Phase 7 entry describes the check at the level of "does every plugin appear everywhere it should, at +the right version," the 4 demo steps, and the guarded-phases list. The spec adds detail beyond that. Per-addition +decision-log evidence check: + +1. **Single shared rule enforced at two moments (change-time block, release-time gate), with the release's override + as the only sanctioned bypass.** Evidenced: `decision-log.md` D2, citing the source analysis's "Only now, turn on + the automated check," the outline's Phase 7 outcome and demo, and the Phase 6 spec's release-side gate. +2. **Bundle exception read from a durable record shared with Phase 6, failing closed if unreadable.** Evidenced: + `decision-log.md` D3, citing Phase 6's spec and decision log (its D4) and the source analysis's "know about it + permanently" language. +3. **No-disable-once-on rule; a disputed report is resolved by fixing the tree or the record, never by turning the + check off.** Evidenced: `decision-log.md` D4, citing the source analysis's "order matters" section and the + outline's builds-on/precondition line. +4. **Edge case: a change deletes a plugin directory but leaves its listings behind ("orphan entries"), and the check + fails on the orphan the same way it fails on a missing listing.** Not evidenced. This row in the Edge Cases and + Failure Modes table has no D2/D3/D4 citation, and no decision log entry addresses plugin deletion or orphaned + listings. The outline's 4 demo steps only cover a plugin being *added* without being published, never removed. +5. **Out-of-Scope carve-out: "Checking companion version statements and release markers on every proposed change... + this check owns presence and version agreement across the four surfaces. Reopen if a statement-marker mismatch + ever escapes the release reconciliation."** Not evidenced. No decision-log entry (D2, D3, or D4) discusses this + scoping choice or the delegation to Phase 6's "post-publish reconciliation." This is the same scoping decision + flagged in GAP-001 as inconsistent with the silent Phase 4 omission — it is itself unevidenced, even though it + happens to be consistent with what the outline claims Phase 7 guards. + +Additions 1–3 are legitimate elaborations with a documented evidence trail. Additions 4 and 5 are unevidenced by the +decision log; 5 is also the source of the Phase 4/Phase 5 scoping asymmetry identified in GAP-001. + +## Areas Needing Separate Analysis + +- **Phase 6 feature-specification.md and its decision log**, referenced repeatedly by Phase 7's D2/D3 as the source + of the shared discovery rule, surface list, and exception record. Not read in this analysis; a correctness check of + Phase 7's claims about what Phase 6 provides would require it. +- **source-han-cleanup-plan.md**, cited by both the outline and the spec's decision log as the origin of the + "order matters" and "know about it permanently" language. Not read in this analysis; confirming those citations + quote the source accurately would require it. diff --git a/docs/plans/han-publishing-cleanup/phase-7-automated-check/artifacts/team-findings.md b/docs/plans/han-publishing-cleanup/phase-7-automated-check/artifacts/team-findings.md new file mode 100644 index 00000000..5466a4a1 --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-7-automated-check/artifacts/team-findings.md @@ -0,0 +1,101 @@ +# Team Findings: Turn On the Automated Completeness Check + +<!-- Findings from the review team, recorded as F# entries. Major findings carry the full field set; minor edits are +one-line bullets. The F# counter is shared across both sections. --> + +## Major findings + +### F1: The change-time and release-time verdicts collided on version disagreement + +- **Agent:** junior-developer +- **Finding:** The change-time check blocked on any cross-channel version disagreement, while Phase 6's release + tolerates pre-existing drift and reconciles it, so the same tree state produced opposite verdicts, contradicting + the phase's central never-disagree promise. +- **Resolution:** The verdict is defined on the proposed tree's end state: a release passes because it moves all four + surfaces together in one change; a change creating drift is blocked; the release's sync is how flagged drift gets + fixed. Both moments call the same state the same thing. +- **Resolved by:** evidence +- **Affected decisions:** D2 +- **Affected tech-notes:** — +- **Changed in spec:** Outcome; Primary Flow + +### F2: "One rule" overclaimed a shared implementation across two runtimes + +- **Agent:** junior-developer +- **Finding:** The change-time check and the interactive release gate cannot literally be one executable path; the + spec should own behavioral equivalence, not machinery. +- **Resolution:** The commitment is now verdict equivalence: same tree and same exception record, same verdict, at + both moments. +- **Resolved by:** evidence +- **Affected decisions:** D2 +- **Affected tech-notes:** — +- **Changed in spec:** Outcome; Coordinations + +### F3: The guard claim was broader than the mechanism, and the outline overclaimed Phase 4 + +- **Agent:** junior-developer (gap-analyzer's GAP-001 found the same asymmetry) +- **Finding:** "Makes every problem the earlier phases fixed impossible to reintroduce" swept in Phase 2's marks, + Phase 4's declaration truth, and Phase 5's statements, none of which the check examines; the outline's Connects-to + said the check guards Phase 4. +- **Resolution:** The claim is narrowed to presence and version agreement, guarding Phases 1, 3, and 6; the + not-guarded outcomes are named in the Outcome and Out of Scope with the reopening trigger; the outline's Phase 7 + guard list was corrected to drop Phase 4. +- **Resolved by:** evidence +- **Affected decisions:** D2 +- **Affected tech-notes:** — +- **Changed in spec:** Outcome; Out of Scope (and the parent outline's Phase 7 entry) + +### F4: An absolute no-disable rule deadlocked on a false-failing check + +- **Agent:** junior-developer +- **Finding:** A check whose own logic false-fails would block every change, including its fix, with no change-time + escape; the repository wedges. +- **Resolution:** A scoped, recorded repair door: a change fixing the check or its record may land past the failing + check, with the bypass named visibly, mirroring the release override's loud-and-recorded principle. +- **Resolved by:** evidence +- **Affected decisions:** D4 +- **Affected tech-notes:** — +- **Changed in spec:** Alternate Flows and States; Edge Cases and Failure Modes + +### F5: The four-surfaces-visible-at-change-time assumption was unstated + +- **Agent:** junior-developer +- **Finding:** A change-time check can only see the repository tree; whether all four surfaces live in the tree was + load-bearing and unsaid. +- **Resolution:** Stated as fact with evidence: all four surfaces are files in this repository, which is what makes + the change-time verdict possible. +- **Resolved by:** evidence +- **Affected decisions:** D2 +- **Affected tech-notes:** — +- **Changed in spec:** Outcome + +### F6: The demonstration needed on-demand invocation the spec never granted + +- **Agent:** junior-developer +- **Finding:** The outline's demo runs the check ad hoc against a throwaway branch; the spec only defined automatic + and release-time runs. +- **Resolution:** On-demand invocation against any working tree is now a stated trigger. +- **Resolved by:** evidence +- **Affected decisions:** D2 +- **Affected tech-notes:** — +- **Changed in spec:** Outcome; Actors and Triggers + +### F9: "Arrives green" had no observable done-criterion or owner + +- **Agent:** junior-developer +- **Finding:** Who runs the first run, what records that it passed, and what "turn on" means were unstated, leaving + the phase's definition of done as an intention. +- **Resolution:** The turn-on condition is a recorded green run against the real tree, owned by the maintainer; + until it exists, the check does not block. +- **Resolved by:** evidence +- **Affected decisions:** D4 +- **Affected tech-notes:** — +- **Changed in spec:** Actors and Triggers; Outcome + +## Minor edits + +- F7: OI-1's "fully settled" claim now lists what was settled, including the version verdict F1 defined — + junior-developer — Open Items +- F8: The orphan-listing row carries Phase 6's honest caveat and its parity evidence — junior-developer — Edge Cases + and Failure Modes +- F10: Summary placeholders replaced with consulted agents and key adjustments — junior-developer — Summary diff --git a/docs/plans/han-publishing-cleanup/phase-7-automated-check/feature-specification.md b/docs/plans/han-publishing-cleanup/phase-7-automated-check/feature-specification.md new file mode 100644 index 00000000..284c0d7b --- /dev/null +++ b/docs/plans/han-publishing-cleanup/phase-7-automated-check/feature-specification.md @@ -0,0 +1,142 @@ +# Feature Specification: Turn On the Automated Completeness Check + +An automated check asks one question on every proposed change and every release: does every plugin appear everywhere +it should, at agreed versions? It lands only after the tree already passes it, so it arrives green, stays green, and +keeps the publishing repairs of Phases 1, 3, and 6 from regressing quietly. + +## Outcome + +A plugin can no longer land invisible on a publishing surface by default. On every proposed change, the check runs +automatically and blocks the change when any plugin is missing from a surface it belongs on. It also blocks the +change when the proposed tree's two channels disagree about a plugin's version. Its failure report names each +missing plugin, the surface it is missing from, and each version disagreement +([D2](artifacts/decision-log.md#d2-one-verdict-two-enforcement-moments)). + +The check's guarantee is verdict equivalence, not a shared machine: given the same tree and the same exception record, +the change-time check and the release-time gate return the same verdict. All four surfaces are files in this +repository, which is what makes the change-time verdict possible at all +([D2](artifacts/decision-log.md#d2-one-verdict-two-enforcement-moments)). The check can also be run on demand against +any working tree, which is how its demonstration works. + +The check guards presence and version agreement: the outcomes of Phases 1, 3, and 6. It does not examine ticket-file +marks (Phase 2), the truth of dependency declarations (Phase 4), or companion version statements and release markers +(Phase 5). The release process's own reconciliation owns markers and statements instead +([D2](artifacts/decision-log.md#d2-one-verdict-two-enforcement-moments)). + +The check knows the all-in-one bundle's absence from the second channel permanently, by reading the same durable +exception record the release process reads, and never flags it +([D3](artifacts/decision-log.md#d3-shared-exception-record)). + +The order of arrival is not negotiable: the check turns on only after a recorded green run against the real tree, +because a check that lands red gets disabled and then protects nothing +([D4](artifacts/decision-log.md#d4-green-first-arrival-and-the-repair-door)). + +## Actors and Triggers + +- **Actors**: Anyone proposing a change to the repository; the maintainer cutting releases; the future contributor + who adds a plugin and is caught before it lands half-published. +- **Triggers**: Automatically on every proposed change; as the release process's presence gate; on demand against + any working tree ([D2](artifacts/decision-log.md#d2-one-verdict-two-enforcement-moments)). +- **Preconditions**: Phases 1, 3, and 6 are complete: the Linear plugin is listed, the version records agree, and + the release process maintains all four surfaces. The turn-on condition is observable: a recorded green run against + the real tree, owned by the maintainer; until it exists, the check does not block + ([D4](artifacts/decision-log.md#d4-green-first-arrival-and-the-repair-door)). The durable exception record exists + and is readable ([D3](artifacts/decision-log.md#d3-shared-exception-record)). + +## Primary Flow + +1. Someone proposes a change. The check runs alongside the repository's other automatic checks. It discovers every + plugin in the proposed tree the same way the release process discovers them. Then it evaluates the proposed + tree's end state: every plugin present on every surface it belongs on, and both channels stating the same version + ([D2](artifacts/decision-log.md#d2-one-verdict-two-enforcement-moments)). +2. When everything is present and agreed, the check passes quietly and the change proceeds. A release-preparation + change passes the same way, because a release moves all four surfaces together in one change. A change that moves + one surface and not its partners is exactly what the check exists to block + ([D2](artifacts/decision-log.md#d2-one-verdict-two-enforcement-moments)). +3. When anything is missing or mismatched, the check fails. Its report names each missing plugin, the surface it + is missing from, and each version disagreement, so the fix needs no investigation. +4. The bundle's absence from the second channel is read from the exception record and never reported + ([D3](artifacts/decision-log.md#d3-shared-exception-record)). +5. At release time, the same verdict gates the release, and the release's loud, recorded override is the only + sanctioned way past it ([D2](artifacts/decision-log.md#d2-one-verdict-two-enforcement-moments)). + +## Alternate Flows and States + +### A new plugin is added without being published everywhere + +- **Entry condition:** A proposed change adds a plugin directory but does not list it on every surface it belongs on. +- **Sequence:** The check fails the change, naming the new plugin and each surface it is missing from. The + contributor adds the listings, or a deliberate durable exception, and the check passes. +- **Exit:** The failure that created the Linear gap is caught at the moment it is introduced, not twenty releases + later. + +### The check itself is wrong + +- **Entry condition:** The check false-fails because its own logic is broken, so it would block every change, + including the change that fixes it. +- **Sequence:** A scoped, recorded repair door exists for this case. A change whose purpose is fixing the check or + its exception record may land past the failing check. The bypass must be named visibly in that change, mirroring + the release override's loud-and-recorded principle. The check is never simply disabled + ([D4](artifacts/decision-log.md#d4-green-first-arrival-and-the-repair-door)). +- **Exit:** The fixed check resumes blocking; the bypass remains auditable. + +### A disputed report + +- **Entry condition:** The check reports a gap a person judges wrong, or the exception record cannot be read. +- **Sequence:** An unreadable exception record fails closed and names the record itself as the fault, mirroring the + release process's rule ([D3](artifacts/decision-log.md#d3-shared-exception-record)). A disputed report is resolved + by fixing the tree, the record, or, through the repair door, the check + ([D4](artifacts/decision-log.md#d4-green-first-arrival-and-the-repair-door)). +- **Exit:** The check stays on, and the fix ends the dispute. + +## Edge Cases and Failure Modes + +| Condition | Required Behavior | +| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| The check's very first run against the real tree fails | The check does not begin blocking; the failure goes back to the earlier phases' owners, because arriving red is the disable-me failure mode ([D4](artifacts/decision-log.md#d4-green-first-arrival-and-the-repair-door)). | +| A change deletes a plugin directory but leaves its listings | The check fails naming the orphan entries; kept for verdict parity with the release process's symmetric rail, which itself carries no claimed incident ([D2](artifacts/decision-log.md#d2-one-verdict-two-enforcement-moments)). | +| The exception record is edited in a proposed change | The check evaluates the change with the edited record, so adding or removing an exception is itself a reviewed, visible change ([D3](artifacts/decision-log.md#d3-shared-exception-record)). | +| An urgent release must ship past a failing check | Only the release process's loud, recorded override applies; the change-time check's only bypass is the scoped repair door for fixing the check itself ([D2](artifacts/decision-log.md#d2-one-verdict-two-enforcement-moments)) ([D4](artifacts/decision-log.md#d4-green-first-arrival-and-the-repair-door)). | + +## Coordinations + +| Coordinating System | Direction | Interaction | Ordering / Consistency Requirement | +| ----------------------- | --------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| The release process (Phase 6) | both | Shares the discovery rule, the surface list, and the exception record | Verdict equivalence: same tree and record, same verdict, at both moments ([D2](artifacts/decision-log.md#d2-one-verdict-two-enforcement-moments)) ([D3](artifacts/decision-log.md#d3-shared-exception-record)). | +| The repository's automatic change checks | outbound | The completeness check joins them and blocks failing changes | It begins blocking only after the recorded green run against the real tree ([D4](artifacts/decision-log.md#d4-green-first-arrival-and-the-repair-door)). | + +## Out of Scope + +- Guarding what the check does not examine: ticket-file marks (Phase 2), the truth of dependency declarations + (Phase 4), and companion version statements and release markers (Phase 5). The release process's post-publish + reconciliation owns markers and statements. Reopen if a statement-marker mismatch ever escapes that + reconciliation ([D2](artifacts/decision-log.md#d2-one-verdict-two-enforcement-moments)). +- Repairing gaps automatically. The check names problems; people fix them. +- Any new publishing surface. Four surfaces and one exception are what exist. + +## Open Items + +- **OI-1:** None. The version-agreement verdict, the enforcement moments, the exception record, the turn-on + condition, and the repair door are all settled here; the check's placement among the repository's existing + automatic checks is an implementation choice. + - **Resolves when:** Not applicable. + - **Blocks implementation:** No. + +## Summary + +- **Outcome delivered:** Every proposed change and every release is gated on complete, version-agreed publishing + across all four surfaces, with one permanent shared exception, a recorded green arrival, and a scoped repair door + instead of an off switch. +- **Primary actors:** Contributors proposing changes; the maintainer releasing; future plugin authors. +- **Decisions settled by evidence:** 3. See [artifacts/decision-log.md](artifacts/decision-log.md) +- **Decisions settled by user input:** 0. See [artifacts/decision-log.md](artifacts/decision-log.md) +- **Sub-agents consulted:** junior-developer, gap-analyzer. See + [artifacts/team-findings.md](artifacts/team-findings.md) +- **Key adjustments from review:** "one rule" became verdict equivalence with the release gate; the version verdict + was reconciled with how releases move surfaces together; the guard claim was narrowed to Phases 1, 3, and 6, and + the outline's Phase 4 guard claim was corrected; a repair door replaced the unbreakable no-disable deadlock; the + turn-on condition became an observable recorded green run. See + [artifacts/team-findings.md](artifacts/team-findings.md) +- **Remaining open items:** 0 +</content> +</invoke> diff --git a/docs/plans/han-publishing-cleanup/source-han-cleanup-plan.md b/docs/plans/han-publishing-cleanup/source-han-cleanup-plan.md new file mode 100644 index 00000000..abcbdf3e --- /dev/null +++ b/docs/plans/han-publishing-cleanup/source-han-cleanup-plan.md @@ -0,0 +1,247 @@ +# Cleaning up Han: what to change, and what to leave alone + +> Local copy of the source artifact for the han-publishing-cleanup build phase outline. +> Original: <https://claude.ai/code/artifact/1dfdb467-8593-4675-96b1-3e541a1f3b77> ("han-cleanup-plan.md"). +> Captured 2026-07-21. The build phase outline cites the section headings below. + +## The short version + +Han's dependency structure is in better shape than it looks, and needs almost no cleanup. The real mess is somewhere +else entirely: in how Han gets published to the people who install it. + +That is the surprise in this plan. I went looking for a tangled dependency graph to straighten out. What I found was a +graph with exactly two loose threads, sitting next to a publishing pipeline that has been quietly broken for months. + +Every suggestion below was checked twice. First by the analysis that found it, then by a separate reviewer whose only +job was to prove each one wrong. That second pass killed two of my original suggestions outright and forced me to +rewrite three more. What is left is what survived being attacked. + +--- + +## The dependency graph: two threads to snip + +### Before + +Each arrow is one plugin saying "I need this other plugin installed." Red arrows are the two that are not true. + +```mermaid +graph TD + COMM["Communication<br/>the shared writing standard"] + CORE["Core<br/>the specialist reviewers"] + PLAN["Planning"] + CODE["Coding"] + GH["GitHub"] + REP["Reporting"] + FB["Feedback"] + LIN["Linear"] + ATL["Atlassian"] + PB["Plugin Builder"] + META["Han<br/>the all-in-one bundle"] + + CORE --> COMM + PLAN --> CORE + CODE --> COMM + CODE --> CORE + GH --> COMM + GH --> CORE + REP --> COMM + ATL --> COMM + ATL --> CORE + ATL --> PLAN + ATL --> CODE + LIN --> CORE + + REP -.->|"claims to need Core<br/>but never uses it"| CORE + FB -.->|"claims to need Core<br/>but never uses it"| CORE + + META -.-> COMM + META -.-> CORE + META -.-> PLAN + META -.-> CODE + META -.-> GH + META -.-> REP + + linkStyle 12 stroke:#c0392b,stroke-width:3px + linkStyle 13 stroke:#c0392b,stroke-width:3px +``` + +Two plugins declare that they need the Core plugin, and then never touch it. Both are leftovers. Reporting used to +reach into Core for its writing pass, that capability moved to the Communication plugin, the code was updated, and +nobody removed the old line. Feedback's case is even clearer: it is not permitted to call other plugins at all, so its +claim cannot possibly be true. + +The cost is not dramatic. Anyone installing either plugin quietly gets a large plugin they will never use. The real +damage is to trust: once two of these declarations are decorative, nobody can rely on any of them to answer "what +actually breaks if I change this?" + +### After + +That is the entire dependency cleanup. Two lines deleted. Nothing added. + +I want to be honest about how small that is, because I expected a bigger answer and went looking for one. The graph has +no loops. Every arrow points the right way, from the plugins that use things toward the plugins that provide them. The +foundation sits at the bottom exactly where it was designed to sit. This part of the system was built carefully and it +held up. + +### Two changes I proposed and then withdrew + +I originally wanted to add arrows, not just remove them. + +**Wiring Planning into the writing standard.** Planning produces more prose than any other plugin, and it is the only +one not connected to the shared writing standard. That looked like an obvious oversight. It is not. The team already +considered exactly this, wrote down the reasoning, listed it explicitly as out of scope, had it challenged during a +plan review, and confirmed the exclusion anyway. I was about to reopen a settled decision and present it as a bug. If +the team wants to revisit it, that is a fresh decision to make on purpose, not a gap to patch. + +**Declaring the hidden contract between Planning and the three ticket publishers.** Three plugins read a file that +Planning writes, and only one of them admits it. My instinct was to make them all declare it. The reviewer pointed out +this would backfire: declaring a dependency in this system does not annotate a relationship, it forces an install. +Every GitHub user would be made to install all of Planning to document a connection that no code actually follows. +Worse, it is the same move the team already rejected for good reasons elsewhere. The relationship is real and worth +writing down. It belongs in the documentation, not in the install instructions. + +--- + +## The publishing pipeline: the actual problem + +Han ships to two different places. One of them has been slowly rotting, and the process that would notice is +structurally incapable of seeing it. + +### Before + +Three things are going wrong here at once, and they share one root cause. + +**The release process only knows about half the world.** It updates two places. There are four. It does not read the +other two, which means it cannot even detect that they have fallen behind. This is why roughly twenty releases went by +without anyone catching it. No amount of care during a release would have surfaced the problem, because the problem is +invisible from inside the release. + +**People on the second channel are stuck on old versions and cannot tell.** That channel decides whether an update is +available by reading a version number that has not moved since the day it was written. So it never offers anyone an +update. They are running old skills and have no way to know. + +**One plugin is not there at all.** The Linear integration exists, works, and is advertised in the setup instructions +for that channel. It was never actually published there. Someone following the documented instructions gets an error. +That is a broken promise in the first thing a new user tries. + +The Linear gap is the tell. It was added two days before the pass that would have published it, and that pass simply +missed it. Which means this is not a one-time slip. Any plugin added today lands invisible on that channel by default. + +### After + +The release starts by looking at what is actually in the repository rather than trusting a list that can go stale. It +checks that every plugin appears everywhere it is supposed to. If something is missing, it stops and says so instead of +shipping around the gap. And it updates all four places rather than two. + +One deliberate exception stays: the all-in-one bundle genuinely cannot be published to the second channel, because that +channel does not support bundles yet. That is a real limitation, it is documented, and the check needs to know about it +permanently rather than flagging it forever. + +### The order matters, and getting it wrong stops everything + +This is the part I got wrong first, and the reviewer caught it. + +If the automated check lands before the underlying problems are fixed, it fails immediately, on almost every plugin, +and blocks every release and every pull request from day one. The check is correct. The tree is not ready for it. + +So: fix the Linear gap, correct the stale version numbers, and teach the check about the bundle exception. Then turn +the check on. In that order, the check lands green and stays green. In the other order, it lands red and someone turns +it off, and then it protects nothing. + +--- + +## The shared ticket file: a real bug, described more carefully than I first described it + +Three plugins publish work items to three different trackers, and they all mark up the same shared file to record what +they published. Two of them mark it up in a way that is indistinguishable from each other. + +Publish to one tracker, then feed the same file to the other, and the second one sees the first one's marks, concludes +the work is already published, and skips it. + +I originally called this silent. The reviewer pushed back, and was right. Both of those plugins do report a count of +what they skipped, twice. A person paying attention would notice. It is a trap, not a disappearance. + +But the reviewer also found something worse that I had not. The third plugin, the GitHub one, has a genuine hole. If it +meets a file marked up by a different tracker, the marks match none of the patterns it looks for. The work items do not +get published, and they do not show up in the skipped count either. They simply vanish from the run, with no error and +no signal. That one really is silent, and it is the one worth fixing first. + +The fix is to make each plugin's marks say which tracker they came from, so no plugin can mistake another's marks for +its own. It is a format change, so files already marked up the old way need a migration path. There is one, and it errs +toward stopping and asking rather than guessing. + +--- + +## Things that are fine, that I checked anyway + +Worth stating plainly, because "we looked and it was fine" is a real result: + +- **The dependency graph has no loops and points the right way.** It matches its own documentation exactly. +- **No two skills or reviewers anywhere in the system have colliding names.** Not one, across eleven plugins. +- **Every plugin that talks to an outside service checks that the service is reachable before doing any work.** All six + of them, consistently, before spending any of your time. +- **The feedback plugin's error handling is the best in the codebase.** It specifically prevents a retry from creating + a duplicate. +- **Nobody built machinery they did not need.** A reviewer went looking specifically for over-engineering and found + none. The versioning documentation was singled out as a model of restraint: it names the tooling the project + deliberately does not use and the exact circumstance that would justify revisiting. + +--- + +## Two places the reviewer changed my mind entirely + +These matter because they reverse conclusions from the earlier analysis, and I would rather flag that than quietly ship +the old answer. + +**On duplicated rule files.** Three plugins each carry their own copy of the same two rule documents. The earlier +analysis said: leave them alone, they have never actually drifted apart, and consolidating them is more disruption than +it prevents. The reviewer checked the history properly and found that one of those rules _was_ edited, meaningfully, +and somebody had to remember to make the identical change in three separate places to keep them aligned. That is not +evidence that consolidation is unnecessary. That is evidence that a human is currently doing the job by hand and +getting it right so far. It strengthens the case for consolidating rather than weakening it. + +**On version compatibility between plugins.** The earlier analysis argued that plugins never need to declare which +versions of each other they work with, because everything ships together from a single snapshot. The reviewer checked +how installation actually works and refuted it. Plugins are installed and updated one at a time. Someone can update the +Core plugin today while running a Coding plugin from months ago, and nothing anywhere would notice or complain. The +single-snapshot argument only holds for a fresh install of everything at once, which is not how the system is used over +time. I am not proposing a fix here, because the right fix is not obvious and this deserves a real decision. But the +question is open, and the earlier analysis closed it too early. + +--- + +## What I would do, in order + +1. **Publish the Linear plugin to the second channel.** It is advertised, it is missing, and someone following the + instructions hits an error today. +2. **Fix the GitHub publisher's hole**, where work items marked up by another tracker disappear from a run with no + trace. +3. **Correct the frozen version numbers**, so people on the second channel start being offered updates again. +4. **Delete the two dependency lines that are not true.** +5. **Fix the two documents that describe a behavior the system does not have**, so nobody makes a change based on them + and gets it wrong. +6. **Teach the release process about all four places**, and to start from what is really in the repository. +7. **Only now, turn on the automated check.** It will be green, and it will stay green, and it will make every problem + above impossible to reintroduce. + +The first six are small. Most are a few lines. The seventh is what makes them stay fixed, and it is why the order is +not negotiable: turn the check on first and it fails on everything, someone disables it, and you are back where you +started with an extra broken thing. + +--- + +## How much of this to trust + +Every claim here was verified against the actual files rather than inherited from the earlier analysis. That mattered: +a meaningful fraction did not survive. Two proposed fixes were withdrawn, three descriptions were corrected, and two +earlier conclusions were reversed. The reviewer's own confidence rating was medium, and the things it could not check +are worth naming. + +It could not test whether the format-checking step that is supposed to catch a mismatched ticket file actually catches +one in practice, because that depends on judgment at the time rather than anything written down. It could not measure +the impact of the format change on files already sitting in people's repositories, because none exist here to check. +And its conclusion about version compatibility rests on the project's own description of how installation works being +accurate to the real thing. + +Those are honest gaps, not hedging. Everything else in this plan was checked against the files, and the parts that +could not survive that are not in it. diff --git a/han-github/docs/skills/work-items-to-issues.md b/han-github/docs/skills/work-items-to-issues.md index 3e2d93c2..c8d08b52 100644 --- a/han-github/docs/skills/work-items-to-issues.md +++ b/han-github/docs/skills/work-items-to-issues.md @@ -41,6 +41,11 @@ and _how_ to use the skill. For what the skill does internally, read the skill d without evidence are surfaced as gaps, not applied silently. - **Idempotent publish.** The publish pipeline resumes cleanly after a partial failure. Items already annotated with their issue number are skipped, and screenshot uploads overwrite in place. +- **Every item is accounted for.** In every run, each item in the file is published, skipped-and-counted, or surfaced. + An item another tracker already published is surfaced rather than passed over, and the run stops before creating + anything — including in the other repos, since a file published elsewhere is usually annotated across all of them. + The annotation is never stripped to make the item publishable: doing so would duplicate work you already track + somewhere else. - **No label, no assignee by default.** Issues are created unlabeled and unassigned. You can pass an optional `--label` and `--assignee` when you want them. @@ -159,14 +164,16 @@ The skill walks a six-step process: 3. **Validate the format with evidence-based repair.** Check the file against the format invariants the publish scripts depend on (heading shape, `Depends on` syntax, within-repo blockers, screenshot URL scheme, references present, no process artifacts). When a check fails, propose a fix backed by a concrete source and give you three actions: - continue with the fills, correct them, or stop. + continue with the fills, correct them, or stop. An item another tracker already published is the exception — it has + no fill, so the skill reports what it found and stops. 4. **Show the item-to-repo map for confirmation.** Present the table and wait. Nothing is written or created until you confirm. 5. **Write the per-repo work-items files.** For each target repo, write a filtered `<repo-name>.work-items.md` next to - the source. This is the file the publish scripts read. + the source. This is the file the publish scripts read. Then account for every item across all of those files at once; + if any item cannot be placed, nothing publishes to any repo. 6. **Publish each per-repo file.** Run the publish pipeline, which runs three idempotent scripts in order. First, upload - the screenshots into the target repo. Then create one issue per item, annotating each heading with its `(#NNN)`. - Finally, post the within-repo `blocked_by` links. + the screenshots into the target repo. Then examine the whole file and create one issue per item, annotating each + heading with its `(#NNN)`. Finally, post the within-repo `blocked_by` links. The publish pipeline is three scripts behind one wrapper. @@ -176,9 +183,11 @@ request when the default branch is protected. The fallback runs entirely through is never touched (no local git is involved). On re-run, it reuses the assets branch only when that branch already carries this feature's `issue-assets/<feature-slug>/` tree; it refuses a same-named branch it did not create. -`create-issues.sh` creates one issue per item in file order, captures the returned number, and rewrites the heading in -place so the next script can resolve symbolic IDs to issue numbers. It applies a label and an assignee only when you -pass them. +`create-issues.sh` accounts for every item in the file before it creates anything, then creates one issue per item in +file order, captures the returned number, and rewrites the heading in place so the next script can resolve symbolic IDs +to issue numbers. It applies a label and an assignee only when you pass them. Examining the whole file first is what +keeps an item another tracker published, sitting late in the file, from being preceded by issues already created — and +the check runs ahead of label creation too, since a label is also a change to your repo. `link-blockers.sh` reads the recorded numbers and posts a native `blocked_by` relationship per blocker. It errors out if a `Depends on` line names an item that is not in the same repo, because a cross-repo dependency belongs in the diff --git a/han-github/skills/work-items-to-issues/SKILL.md b/han-github/skills/work-items-to-issues/SKILL.md index 003ef227..e990b2ae 100644 --- a/han-github/skills/work-items-to-issues/SKILL.md +++ b/han-github/skills/work-items-to-issues/SKILL.md @@ -70,6 +70,9 @@ Check the work-items file against the format invariants in - **Heading shape.** Every slice heading matches `## <SYM-N> — <title>` with an em-dash separator (already-published headings annotated as `## <SYM-N> (#NNN) — <title>` are valid too). +- **Annotated by another tracker.** A slice heading carrying an annotation that is not `(#NNN)` — `## <SYM-N> (ACME-142) + — <title>`, `## <SYM-N> (ENG-99) — <title>` — means this file was already published somewhere else. This is a distinct + category from a malformed heading and is **never repaired**. See the repair rules below. - **`Depends on` line.** Literal bold marker `**Depends on.**`, trailing period, `None.` or comma-separated SYMs. - **Within-repo blockers.** Every SYM named in a `Depends on` line maps to the same target repo as the dependent slice (under the map from Step 2). @@ -86,7 +89,15 @@ When a check fails, attempt evidence-based repair. Pull evidence from the source referenced in its intro, the feature spec in the same folder, sibling files in the plan folder, and the target repo's ADRs / coding standards / docs: -- **Malformed heading** — propose the corrected shape based on the surrounding text. Cite the line number. +- **Annotated by another tracker** — **never repair this.** Do not propose stripping the annotation, and do not treat it + as a malformed heading to be corrected into shape. The annotation is evidence that these work items already exist + somewhere else, and tidying it away here would publish duplicates of work that is already tracked. Stop and report + every such heading with the annotation it carries, so the user decides whether this file was already published + elsewhere. Only the user resolves this category. +- **Malformed heading** — propose the corrected shape based on the surrounding text. Cite the line number. This covers a + heading nobody else annotated: a hand edit with the wrong dash, a missing separator, a mistyped symbolic ID. If the + heading carries an annotation that is not `(#NNN)`, it belongs to the category above instead — a heading you cannot + parse may be another tracker's annotation in a shape you do not know, and the two need different answers. - **Missing `Depends on` line** — propose `None.` if no blockers are evident in the slice's prose. Cite the absence. - **Cross-repo `Depends on`** — propose moving the relationship to the cross-repo work-order prose at the top of the file and replacing the line with `None.` or remaining within-repo SYMs. Cite the SYM→repo map entries that prove the @@ -112,6 +123,12 @@ Then give the user three actions: - **Correct the fills** — user provides the right values; apply those and proceed. - **Stop** — exit without publishing. User edits the file by hand and re-runs. +**A heading annotated by another tracker is the exception: it has no fills, so only Stop is offered.** Report every such +heading and what it appears to be annotated by, tell the user their file looks like it was already published to that +tracker, and exit without publishing. Do not offer to strip the annotation and continue — that is the one "repair" that +silently duplicates work the user already has. If they genuinely want these items in GitHub as well, they say so and +edit the file themselves; that is a decision to make on purpose, not a default. + If validation passes with no findings, proceed silently to Step 4. ### 4. Show the SYM→repo map for confirmation @@ -140,6 +157,13 @@ source `work-items.md`. The per-repo file is a filtered view of the source — i The source `work-items.md` is not modified by the publish step. The per-repo files are what carry the `(#NNN)` issue-number annotations after publishing. +Once every per-repo file is written, account for every slice heading across all of them at once by running +`${CLAUDE_SKILL_DIR}/scripts/check-annotations.sh <per-repo-file-1> <per-repo-file-2> ...` with every per-repo file in +one invocation. If it exits non-zero, report what it names and stop — publish nothing, to any repo. A file that another +tracker already published is usually annotated across every repo it touches, so checking one repo at a time would create +issues in the clean repos before reaching the annotated one. Nothing publishes anywhere until every file is accounted +for. + ### 6. Publish each per-repo file to GitHub For each per-repo file, publish it by running @@ -162,7 +186,9 @@ The wrapper runs three idempotent scripts in order: PR merges — the issues are still created immediately. Overwrites existing files cleanly and, on re-run, reuses the assets branch and open PR — but only a branch it created for this feature (one already carrying the feature's `issue-assets/<feature-slug>/` tree); a same-named branch it does not own is refused rather than committed onto. -2. **`scripts/create-issues.sh`** — creates one GitHub issue per `## <SYM-N>` slice in file order (blocker-first), +2. **`scripts/create-issues.sh`** — accounts for every slice heading in the file before creating anything (via + `scripts/check-annotations.sh`, the same check Step 5 runs across every per-repo file), so a heading it cannot place + stops the run rather than being passed over. Then creates one GitHub issue per `## <SYM-N>` slice in file order (blocker-first), unassigned and unlabeled by default. When `--label <name>` is passed, it applies that label to every issue, creating it on the repo only if it does not already exist (an existing label's color and description are left intact); when `--assignee <user>` is passed, it assigns each issue to that user. Captures each returned issue number and rewrites diff --git a/han-github/skills/work-items-to-issues/references/work-items-file-format.md b/han-github/skills/work-items-to-issues/references/work-items-file-format.md index 80e7c288..319a086b 100644 --- a/han-github/skills/work-items-to-issues/references/work-items-file-format.md +++ b/han-github/skills/work-items-to-issues/references/work-items-file-format.md @@ -87,3 +87,30 @@ source file does not. The slice-body invariants are documented in [issue-template.md](./issue-template.md). The per-repo file's preamble (title, intro, cross-repo prose, shared references) is for the human reviewer — the scripts ignore everything before the first `## <SYM-N>` heading. + +## Every slice heading is accounted for + +`scripts/check-annotations.sh` classifies every heading that carries a symbolic ID, and every one lands in exactly one +of three buckets: + +| Heading | Meaning | What happens | +| -------------------------------- | --------------------------- | ------------------------- | +| `## W-1 — <title>` | Publishable here | An issue is created | +| `## W-1 (#12) — <title>` | Already published here | Skipped and counted | +| `## W-1 (ACME-142) — <title>` | Published to another tracker | Surfaced; the run stops | +| `## W-1 - <title>` (wrong dash) | Cannot be placed | Surfaced; the run stops | + +A heading is in scope when it carries a symbolic ID, **whatever follows it**. That is what lets the check see another +tracker's annotation and a hand-edited heading alike, and it is why preamble prose (`## Shared reference artifacts`) +never triggers a stop: it carries no symbolic ID. + +Two properties make the promise real rather than circular: + +- **The whole file is examined before the first issue is created**, so an annotation late in the file is never preceded + by issues already created. The check runs ahead of label creation too, since creating a label also changes the target + repo. +- **Every per-repo file is examined before any repo is published to.** A file another tracker published is usually + annotated across all of its repos, so a per-file check would create issues in the clean repos first. + +A heading annotated by another tracker is never repaired into shape. Stripping the annotation would publish duplicates +of work that already exists somewhere else. diff --git a/han-github/skills/work-items-to-issues/scripts/check-annotations.sh b/han-github/skills/work-items-to-issues/scripts/check-annotations.sh new file mode 100755 index 00000000..fc0be038 --- /dev/null +++ b/han-github/skills/work-items-to-issues/scripts/check-annotations.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Usage: check-annotations.sh <work-items-file>... +# +# Accounts for every slice heading in every file given: each one is publishable +# here, already published here, or unrecognized. Exits non-zero naming every +# unrecognized heading at once, so a run can stop before creating anything +# anywhere. +# +# A heading is in scope when it carries a symbolic ID (`## W-1 ...`), whatever +# follows it. That admits headings annotated by another tracker and headings a +# hand edit left malformed, and it never matches preamble prose like +# `## Shared reference artifacts`, which carries no symbolic ID. +# +# Takes every per-repo file at once because a file published to another tracker +# is usually annotated across all of its repos: checking one repo at a time +# would create issues in the clean repos before reaching the annotated one. + +set -euo pipefail + +[ $# -gt 0 ] || { + echo "usage: check-annotations.sh <work-items-file>..." >&2 + exit 2 +} + +# Symbolic ID: any uppercase prefix plus a number, per the documented format. +sym='[A-Z][A-Z0-9]*-[0-9]+' + +publishable=0 +already=0 +unrecognized=0 +findings=() + +for work_items in "$@"; do + [ -f "$work_items" ] || { + echo "ERROR: work-items file not found: $work_items" >&2 + exit 2 + } + + lineno=0 + while IFS= read -r line || [ -n "$line" ]; do + lineno=$((lineno + 1)) + + # In scope only if the heading carries a symbolic ID. + [[ "$line" =~ ^##[[:space:]]+($sym)([[:space:]]|$) ]] || continue + id="${BASH_REMATCH[1]}" + + if [[ "$line" =~ ^##[[:space:]]+${sym}[[:space:]]+—[[:space:]] ]]; then + publishable=$((publishable + 1)) + elif [[ "$line" =~ ^##[[:space:]]+${sym}[[:space:]]+\(#[0-9]+\)[[:space:]]+—[[:space:]] ]]; then + already=$((already + 1)) + else + unrecognized=$((unrecognized + 1)) + if [[ "$line" =~ \(([^\)]+)\) ]]; then + findings+=("$work_items:$lineno: $id is annotated by ${BASH_REMATCH[1]} — already published to another tracker?") + else + findings+=("$work_items:$lineno: $id heading shape not recognized") + fi + fi + done < "$work_items" +done + +total=$((publishable + already + unrecognized)) +echo "examined $total slice heading(s): $publishable publishable, $already already published here, $unrecognized unrecognized" + +if [ "$unrecognized" -gt 0 ]; then + { + echo + echo "ERROR: $unrecognized slice heading(s) could not be placed. Nothing has been created." + for finding in "${findings[@]}"; do + echo " $finding" + done + echo + echo "Publishing a file that another tracker already published would duplicate that work." + echo "Resolve each heading above, then re-run." + } >&2 + exit 1 +fi diff --git a/han-github/skills/work-items-to-issues/scripts/create-issues.sh b/han-github/skills/work-items-to-issues/scripts/create-issues.sh index 6c6d07be..ae5b3a75 100755 --- a/han-github/skills/work-items-to-issues/scripts/create-issues.sh +++ b/han-github/skills/work-items-to-issues/scripts/create-issues.sh @@ -14,6 +14,10 @@ # are optional. # # Idempotent: slices whose heading already includes `(#NNN)` are skipped. +# +# Accounts for every slice heading before creating anything: each one is +# publishable, already published here, or surfaced by check-annotations.sh, +# which stops the run. Nothing is silently passed over. set -euo pipefail @@ -33,6 +37,15 @@ done [ -f "$WORK_ITEMS" ] || { echo "work-items file not found: $WORK_ITEMS" >&2; exit 1; } +DIR="$(cd "$(dirname "$0")" && pwd)" + +# Account for every slice heading before creating anything — including the +# label, which is itself a change to the target repo. A heading annotated by +# another tracker, or left malformed by a hand edit, stops the run here rather +# than being passed over silently. Examining the whole file first is what keeps +# an annotation late in the file from being preceded by issues already created. +"$DIR/check-annotations.sh" "$WORK_ITEMS" + if [ -n "$LABEL" ]; then # Create the label without `--force`, which would reset an existing label's # color/description. A create that fails on an existing label leaves it diff --git a/han-linear/.codex-plugin/plugin.json b/han-linear/.codex-plugin/plugin.json new file mode 100644 index 00000000..a946c907 --- /dev/null +++ b/han-linear/.codex-plugin/plugin.json @@ -0,0 +1,28 @@ +{ + "name": "han-linear", + "version": "1.0.2", + "description": "Linear-facing Han skill for publishing work items to a Linear team as issues.", + "author": { + "name": "Test Double", + "url": "https://testdouble.com" + }, + "homepage": "https://github.com/testdouble/han#readme", + "repository": "https://github.com/testdouble/han", + "license": "MIT", + "keywords": ["han", "linear", "issues", "work-items", "planning"], + "skills": "./skills/", + "interface": { + "displayName": "Han Linear", + "shortDescription": "Turn a work-items file into Linear issues, one per slice.", + "longDescription": "Linear-facing Han skill for publishing work items to Linear. Creates one issue per slice in a single target team, resolving states, labels, Projects, and members against the team's real configuration before creating anything. Links dependencies between issues as native blocked-by relations. Requires a configured Linear MCP server.", + "developerName": "Test Double", + "category": "Developer Tools", + "capabilities": ["Skills"], + "websiteURL": "https://github.com/testdouble/han", + "defaultPrompt": [ + "Create Linear issues from these work items.", + "Publish this work-items file to my Linear team.", + "Turn my plan into Linear tickets." + ] + } +} diff --git a/test/check-annotations.bats b/test/check-annotations.bats new file mode 100755 index 00000000..b73a145e --- /dev/null +++ b/test/check-annotations.bats @@ -0,0 +1,193 @@ +#!/usr/bin/env bats +# +# check-annotations.sh accounts for every slice heading in a work-items file: +# each one is publishable here, already published here, or surfaced. Nothing +# is silently passed over. +# +# A heading is in scope when it carries a symbolic ID (`## W-1 ...`), whatever +# follows it. Preamble prose headings never match that shape, so they cannot +# cause a false stop. + +setup() { + CHECK="${BATS_TEST_DIRNAME}/../han-github/skills/work-items-to-issues/scripts/check-annotations.sh" + TMP="$(mktemp -d)" +} + +teardown() { + rm -rf "$TMP" +} + +# --- clean files pass --------------------------------------------------------- + +@test "passes a file whose slices are all unannotated" { + cat > "$TMP/wi.md" <<'EOF' +# Work Items — Example + +## W-1 — First slice +body +## W-2 — Second slice +body +EOF + run "$CHECK" "$TMP/wi.md" + [ "$status" -eq 0 ] +} + +@test "passes a file whose slices were already published here" { + cat > "$TMP/wi.md" <<'EOF' +## W-1 (#12) — Already published +body +## W-2 — Not yet published +body +EOF + run "$CHECK" "$TMP/wi.md" + [ "$status" -eq 0 ] +} + +@test "ignores preamble headings that carry no symbolic ID" { + cat > "$TMP/wi.md" <<'EOF' +# Work Items — Example + +Work items are numbered W-N for cross-reference only. + +## Shared reference artifacts + +- [API contract](./contract.md#envelope) + +## W-1 — First slice +body +EOF + run "$CHECK" "$TMP/wi.md" + [ "$status" -eq 0 ] +} + +# --- foreign annotations are surfaced ----------------------------------------- + +@test "stops on a heading annotated by another tracker" { + cat > "$TMP/wi.md" <<'EOF' +## W-1 — Fine +body +## W-2 (ACME-142) — Published to another tracker +body +EOF + run "$CHECK" "$TMP/wi.md" + [ "$status" -ne 0 ] + [[ "$output" == *"W-2"* ]] + [[ "$output" == *"ACME-142"* ]] +} + +@test "names what the heading appears to be annotated by" { + cat > "$TMP/wi.md" <<'EOF' +## W-1 (ENG-99) — Published to a different tracker +body +EOF + run "$CHECK" "$TMP/wi.md" + [ "$status" -ne 0 ] + [[ "$output" == *"ENG-99"* ]] +} + +@test "stops on a heading it cannot place even when it looks like nobody else's" { + cat > "$TMP/wi.md" <<'EOF' +## W-1 - Hand-edited with the wrong dash +body +EOF + run "$CHECK" "$TMP/wi.md" + [ "$status" -ne 0 ] + [[ "$output" == *"W-1"* ]] +} + +@test "names every unrecognized heading, not just the first" { + cat > "$TMP/wi.md" <<'EOF' +## W-1 (ACME-1) — First foreign +body +## W-2 — Fine +body +## W-3 (ENG-2) — Second foreign +body +## W-4 - Wrong dash +body +EOF + run "$CHECK" "$TMP/wi.md" + [ "$status" -ne 0 ] + [[ "$output" == *"W-1"* ]] + [[ "$output" == *"W-3"* ]] + [[ "$output" == *"W-4"* ]] +} + +@test "stops on a file published entirely to another tracker" { + # The case the whole phase exists for: every heading is foreign-annotated, so + # there is no recognizable unannotated slice to anchor on. This must surface, + # not read as a file with nothing in it. + cat > "$TMP/wi.md" <<'EOF' +# Work Items — Example + +## W-1 (ACME-1) — First +body +## W-2 (ACME-2) — Second +body +EOF + run "$CHECK" "$TMP/wi.md" + [ "$status" -ne 0 ] + [[ "$output" == *"W-1"* ]] + [[ "$output" == *"W-2"* ]] +} + +# --- every heading is accounted for ------------------------------------------- + +@test "reports counts that add up to every slice heading in the file" { + cat > "$TMP/wi.md" <<'EOF' +## W-1 — Publishable +body +## W-2 (#7) — Already published here +body +## W-3 (ACME-3) — Foreign +body +EOF + run "$CHECK" "$TMP/wi.md" + [ "$status" -ne 0 ] + [[ "$output" == *"1 publishable"* ]] + [[ "$output" == *"1 already published"* ]] + [[ "$output" == *"1 unrecognized"* ]] +} + +# --- multi-file: nothing publishes anywhere ----------------------------------- + +@test "checks every file it is given, not just the first" { + cat > "$TMP/a.md" <<'EOF' +## W-1 — Clean repo file +body +EOF + cat > "$TMP/b.md" <<'EOF' +## W-2 (ACME-9) — Dirty repo file +body +EOF + run "$CHECK" "$TMP/a.md" "$TMP/b.md" + [ "$status" -ne 0 ] + [[ "$output" == *"W-2"* ]] + [[ "$output" == *"b.md"* ]] +} + +@test "passes only when every file it is given is clean" { + cat > "$TMP/a.md" <<'EOF' +## W-1 — Clean +body +EOF + cat > "$TMP/b.md" <<'EOF' +## W-2 (#3) — Also clean +body +EOF + run "$CHECK" "$TMP/a.md" "$TMP/b.md" + [ "$status" -eq 0 ] +} + +# --- input handling ----------------------------------------------------------- + +@test "fails loudly when a file does not exist" { + run "$CHECK" "$TMP/nope.md" + [ "$status" -ne 0 ] + [[ "$output" == *"not found"* ]] +} + +@test "requires at least one file" { + run "$CHECK" + [ "$status" -ne 0 ] +} diff --git a/test/create-issues-guard.bats b/test/create-issues-guard.bats new file mode 100755 index 00000000..91ccfd12 --- /dev/null +++ b/test/create-issues-guard.bats @@ -0,0 +1,127 @@ +#!/usr/bin/env bats +# +# create-issues.sh examines the whole file before creating the first issue. +# A heading it cannot place stops the run before anything reaches the tracker, +# so an annotation late in the file is never preceded by issues already created. +# +# `gh` is stubbed on PATH: these tests assert what the script would send, and +# must never reach the network. + +setup() { + SCRIPT="${BATS_TEST_DIRNAME}/../han-github/skills/work-items-to-issues/scripts/create-issues.sh" + TMP="$(mktemp -d)" + GH_LOG="$TMP/gh-calls.log" + export GH_LOG + + # Stub gh: log every invocation, and answer `issue create` with a plausible URL. + mkdir -p "$TMP/bin" + cat > "$TMP/bin/gh" <<'EOF' +#!/usr/bin/env bash +echo "$*" >> "$GH_LOG" +if [ "${1:-}" = "issue" ] && [ "${2:-}" = "create" ]; then + echo "https://github.com/acme/repo/issues/501" +fi +EOF + chmod +x "$TMP/bin/gh" + PATH="$TMP/bin:$PATH" + export PATH + : > "$GH_LOG" +} + +teardown() { + rm -rf "$TMP" +} + +gh_call_count() { + if [ -s "$GH_LOG" ]; then wc -l < "$GH_LOG" | tr -d ' '; else echo 0; fi +} + +@test "creates nothing when a heading is annotated by another tracker" { + cat > "$TMP/wi.md" <<'EOF' +## W-1 — Would publish fine +body +## W-2 (ACME-142) — Published to another tracker +body +EOF + run "$SCRIPT" "$TMP/wi.md" acme/repo + [ "$status" -ne 0 ] + [ "$(gh_call_count)" -eq 0 ] +} + +@test "the file is left untouched when the run stops" { + cat > "$TMP/wi.md" <<'EOF' +## W-1 — Would publish fine +body +## W-2 (ACME-142) — Foreign +body +EOF + before="$(cat "$TMP/wi.md")" + run "$SCRIPT" "$TMP/wi.md" acme/repo + [ "$status" -ne 0 ] + [ "$(cat "$TMP/wi.md")" = "$before" ] +} + +@test "a foreign annotation last in the file still stops the first item being created" { + # The ordering the guard exists for: without examine-first, W-1 and W-2 would + # already be issues by the time the publisher reached W-3. + cat > "$TMP/wi.md" <<'EOF' +## W-1 — First +body +## W-2 — Second +body +## W-3 (ENG-7) — Foreign, last in the file +body +EOF + run "$SCRIPT" "$TMP/wi.md" acme/repo + [ "$status" -ne 0 ] + [ "$(gh_call_count)" -eq 0 ] +} + +@test "names the offending heading when it stops" { + cat > "$TMP/wi.md" <<'EOF' +## W-9 (ACME-3) — Foreign +body +EOF + run "$SCRIPT" "$TMP/wi.md" acme/repo + [ "$status" -ne 0 ] + [[ "$output" == *"W-9"* ]] + [[ "$output" == *"ACME-3"* ]] +} + +@test "still publishes a clean file" { + cat > "$TMP/wi.md" <<'EOF' +## W-1 — A clean slice +body +EOF + run "$SCRIPT" "$TMP/wi.md" acme/repo + [ "$status" -eq 0 ] + grep -q "issue create" "$GH_LOG" + grep -q '^## W-1 (#501) — A clean slice$' "$TMP/wi.md" +} + +@test "still skips a file already published here without creating anything" { + cat > "$TMP/wi.md" <<'EOF' +## W-1 (#12) — Already published here +body +EOF + run "$SCRIPT" "$TMP/wi.md" acme/repo + [ "$status" -eq 0 ] + run grep -c "issue create" "$GH_LOG" + [ "$output" -eq 0 ] +} + +@test "does not stop on preamble headings that carry no symbolic ID" { + cat > "$TMP/wi.md" <<'EOF' +# Work Items — Example + +## Shared reference artifacts + +- [contract](./c.md) + +## W-1 — A clean slice +body +EOF + run "$SCRIPT" "$TMP/wi.md" acme/repo + [ "$status" -eq 0 ] + grep -q "issue create" "$GH_LOG" +}