diff --git a/CHANGELOG.md b/CHANGELOG.md index bef362e..09d0007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,27 +40,12 @@ ### Compatibility - **`released` rows written before 1.1.9 still do not release anything, and this release does not change that.** 1.1.9 deliberately stopped matching five-field rows because the task that wrote one is recorded nowhere. The worktree these changes were made in held three such rows, written by the installed 1.1.8 plugin before the checkout's own code was in use; they were rewritten by hand to six fields with the task name, which was sound there because exactly one task had ever run in that worktree. That is a one-off repair of a store, not a migration path — nothing in the shipped code reads a five-field row, and a store without one needs no action. -- **Five archived Plans read as unfinished under the `done`-only rule and are not rewritten** (unchanged from the entry below): `achieved/` is read-only in place, and editing history so it passes a check that no longer applies to it would make the record say something untrue. +- **Five archived Plans read as unfinished under the new rule, and are not rewritten.** `achieved/` is read-only in place, so nothing here changes them and no task is blocked by them. They are: `2026-09-07-repository-standards-and-doc-root` (three Steps say `done (v1)`), `2026-10-11-gate-host-coverage` (four say `completed`), `2026-09-08-token-efficiency` and `2026-09-14-personal-docs-git-boundary` (their Steps still say `pending` while the Plan header says `completed` — the same disagreement running the other way), and `2026-10-22-taskflow-drift-guard`, whose `## Verification / Review` section reuses `### Step 1` as an ordinary subheading for a table. Recorded rather than repaired: editing history so it passes a check that no longer applies to it would make the record say something untrue. Only two of the five change verdict when the real gate is run over all 91 — the other three were already refused on `main` for reasons that have nothing to do with Steps, so this release does not newly refuse them. ## [1.1.9] — 2026-10-26 ### Added -- **`hooks/release-check` now reads the `$schema` a manifest declares, which nothing had ever inspected.** Two manifests point at a JSON Schema (`.claude-plugin/marketplace.json`, `.claude-plugin/plugin.json`) and measured 2026-10-29 **both of those URLs resolve to 404**: each `301`s to `www.anthropic.com/...` and stops there. Six candidate replacements were probed and none returned a real schema - `docs.anthropic.com/claude-code/plugin.schema.json` answers 200 with a 296 KB HTML error page. The assertion therefore judges only what the repository can decide by itself: a declared `$schema` must carry a non-empty value, must be an absolute `https` URL, may appear once per file (a duplicate key's winner is parser-dependent), and the set of files declaring one is pinned so adding or removing a declaration is a visible event rather than a silent edit. **Reachability is deliberately not checked**: every other check in this file is offline, and a `curl` here would redden on a DNS blip while still being wrong the moment the remote moved - which is exactly how these two went unnoticed. The pin is enforced only once something declares, so a repository that has not adopted `$schema` is not told it dropped one it never had. -- **`hooks/smoke-test` covers the new assertion, in the section that already drives `release-check` from a fixture.** It has to live there rather than only in the check itself: `hooks/*` is in the smoke path set, while the `release` job runs only when a manifest, `CHANGELOG.md` or a README changes - so a change to `release-check` alone would never execute it in CI. Verified by mutation: removing the empty-value branch, the `https` test, the duplicate count, and the pin each reddens its own named case, and restoring the file byte-identically returns the suite to `ALL SMOKE PASSED`. -- **A Step counts as closed only when it says `- Status: done`, so a word the gate did not know can no longer archive a Plan that is not finished.** `hooks/completion-gate` read the field as a blacklist — `pending`, `in_progress`, `blocked` were named and everything else was finished — so the word `completed`, a misspelling, an empty value, and a Step with no `- Status:` line at all all passed. Measured 2026-10-27 by driving the gate directly: `completed`, `complted`, `skipped`, `wip`, `DONE`, an empty value and a missing line each returned exit 0 and "finished". `hooks/task progress` has only ever accepted `pending|in_progress|done|blocked`, so those spellings could not have been written by the tool — they are the result of hand edits made precisely because the two sides disagreed. Closed is now a whitelist of one word, and it is the same word the writer accepts. The block is located by its `### Step ` heading anywhere in the Plan rather than only under `## Steps`, because that is how `task progress` finds the block it edits; a reader that skipped the other sections would see a different set of Steps than the writer changes. **Knowingly still open, and pinned by an assertion so it stays a decision**: a Plan with no `### Step` block at all is not refused, because "must a Plan have Steps" is a separate rule this change does not make. -- **The Windows smoke suite no longer stops dead when its stdin is open and silent.** `hooks/smoke-test-windows.ps1` called `run-hook.cmd session-start` with no stdin of its own, so the hook inherited the runner's — and `session-start` reads its event with `cat`. From a terminal that read never reaches EOF, so the suite printed `WINDOWS LAUNCHER ARGS PASSED` and then hung indefinitely; a red assertion reached a person as an unexplained freeze rather than a message. The cause is the inherited stdin, not the throw the report named: probing with `throw` at eight positions, every one before that call printed and exited 1 in under 14s, while every one after it hung — the boundary is exactly `Invoke-SessionStart 'pr'`. The bash suite already carried this lesson for its own `session-start` call (`hooks/smoke-test`, `< /dev/null`); the PowerShell one is now given an empty event the same way, through `cmd /c "... < nul"` rather than `'' |`. **An empty PowerShell string is not an empty stdin** — it is encoded and written as a line, and which line depends on `$OutputEncoding`: measured 0x0d 0x0a on the author's machine and a BOM on the CI runner, where `session-start` rejected it as invalid JSON and the suite failed ([run 40712165629](https://github.com/hkwuks/TaskFlow/actions/runs/40712165629)). A redirection is zero bytes on every host. Measured 2026-10-28: before, still silent at 180s and killed; after, four `PASSED` banners in 27s. **CI could never have caught it** — a runner's stdin is empty, so `cat` sees EOF at once and every windows-latest job has been green regardless. -- **The Windows suite now checks this itself, by re-running with an inherited open stdin and failing if it does not finish.** The check has to live there rather than in `hooks/smoke-test`, because that suite does not run on the Windows runner at all — the workflow gates it on `runner.os != 'Windows'` — so an assertion written there is skipped by every CI job, and the one platform that can reproduce the defect is the one platform that never executes the check. `hooks/smoke-test-windows.ps1` is the only TaskFlow check a Windows runner runs, which makes it the only place this can be caught automatically. It spawns itself with `RedirectStandardInput` set and never written: a pipe that is open and silent, the shape a terminal gives a command a person typed and the shape CI does not have. `TASKFLOW_STDIN_SELFCHECK` bounds the recursion to one child. Verified by mutation: with the `session-start` call reverted to inheriting, the child is killed at the 90s deadline and the suite fails naming the read; with `< nul` in place it exits 0 in about 28s, and the whole suite takes 51s. -- **`hooks/smoke-test-windows.ps1` must stay 7-bit ASCII, and now asserts it.** Windows PowerShell 5.1 reads a `.ps1` with no BOM using the system ANSI code page rather than UTF-8, so a literal non-ASCII character is decoded into bytes the parser does not expect: measured 2026-10-28, an em dash in a `throw` message arrived as three UTF-8 bytes, the third swallowed the closing quote, and the whole file failed to parse with "The string is missing the terminator" ([run 40717070366](https://github.com/hkwuks/TaskFlow/actions/runs/40717070366)) -- after the suite had passed on Linux and macOS, which read the same bytes as UTF-8. The file was already ASCII-only for this reason and builds its Chinese fixtures from `[char]` codes; the rule was known and unenforced. The first thing the suite does now is read itself and refuse to run if any non-ASCII character is present, naming the line and code point. Verified by mutation: injecting one em dash makes it fail at the guard. -- **The same inherited stdin reached one more `session-start` call, in `hooks/smoke-test` itself.** Its `== Windows launcher finds Git Bash ==` section ran `cmd.exe //d //c ... session-start` with no stdin, so it had the identical failure mode: measured 2026-10-28, it never returned against a held-open stdin and returned at once with `< /dev/null`. Given the empty stdin it now has, the same way and for the same reason. - - -### Compatibility - -- **Five archived Plans read as unfinished under the new rule, and are not rewritten.** `achieved/` is read-only in place, so nothing here changes them and no task is blocked by them. They are: `2026-09-07-repository-standards-and-doc-root` (three Steps say `done (v1)`), `2026-10-11-gate-host-coverage` (four say `completed`), `2026-09-08-token-efficiency` and `2026-09-14-personal-docs-git-boundary` (their Steps still say `pending` while the Plan header says `completed` — the same disagreement running the other way), and `2026-10-22-taskflow-drift-guard`, whose `## Verification / Review` section reuses `### Step 1` as an ordinary subheading for a table. Recorded rather than repaired: editing history so it passes a check that no longer applies to it would make the record say something untrue. Only two of the five change verdict when the real gate is run over all 91 — the other three were already refused on `main` for reasons that have nothing to do with Steps, so this release does not newly refuse them. - -### Added - - **`hooks/task recover` repairs a capability store instead of requiring it to be thrown away.** `consumed` is the highest sequence in `released` and the queue length is counted in `evidence`, so a store whose evidence was truncated or deleted reads as further along than it is: the gate denies every stage, `unaided` refuses to widen the gap, and `approve` now refuses too. The only recovery was `rm -rf` on the whole store, which discarded every release whose record still existed along with the ones that had none. The repair is arithmetic rather than reconstruction — a release at a sequence past the end of `evidence` points at a record that is gone, and that is the only kind identifiable without guessing — so `recover` drops exactly those rows, keeps the rest, and appends what it dropped to `released.discarded`. It never adds a release and accepts no argument naming rows, because a command that could write a release would be a way to edit the ledger. A healthy store is a read-only no-op: nothing is written and no discard file appears. ### Fixed