plan: four pre-flight corrections before execution
Scanning the plan for cross-task conflicts before dispatching found four, all mine: The gate tests skipped on `docs/site` existing, but Task 4 creates `docs/site/anchors.json` — so the skips would stop firing four tasks early and every gate would fail on a corpus that does not arrive until Task 8. They test `nav.json` now. `fencedCarlosVerbs` could not produce `adopt` from `/opt/carlos/carlos adopt`, which the plan's own test asserted it does. It now matches a field ending in `/carlos` too — the on-box runbooks invoke the binary by absolute path, and those examples go stale the same way bare ones do. A test named for nesting that checked fence languages is renamed to what it checks, and Task 8's orientation step now runs a command that exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+29
−22
docs/superpowers/plans/2026-08-25-docs-pipeline-and-gates.md+29 −22
diff --git a/docs/superpowers/plans/2026-08-25-docs-pipeline-and-gates.md b/docs/superpowers/plans/2026-08-25-docs-pipeline-and-gates.md| index 57d3be9e1..fa9425d55 100644 |
| --- a/docs/superpowers/plans/2026-08-25-docs-pipeline-and-gates.md |
| +++ b/docs/superpowers/plans/2026-08-25-docs-pipeline-and-gates.md |
| @@ -441,13 +441,15 @@ Expected: `TestNavAndFilesAgreeBothWays` FAILS with `load: read nav.json: ... do |
| Add to the top of `TestNavAndFilesAgreeBothWays` only: |
| ```go |
| - if _, err := os.Stat(root); os.IsNotExist(err) { |
| - t.Skip("no corpus yet — Task 8 creates docs/site") |
| + if _, err := os.Stat(root + "/nav.json"); os.IsNotExist(err) { |
| + t.Skip("no corpus yet — Task 8 creates docs/site/nav.json") |
| } |
| ``` |
| Add the same three lines to every gate test that reads `root` in Tasks 3–7. Task 8 deletes all of them in one commit. Import `"os"`. |
| +The condition tests `nav.json`, not the directory. Task 4 creates `docs/site/anchors.json`, so a bare directory test would stop skipping one task early and every gate would then fail on a corpus that does not exist until Task 8. |
| + |
| - [ ] **Step 6: Run to verify all pass** |
| Same command. Expected: PASS (one skipped). |
| @@ -495,8 +497,8 @@ import ( |
| // exists, the link still looks right in review, and the reader lands at |
| // the top of a long page wondering what they missed. |
| func TestInternalLinksResolve(t *testing.T) { |
| - if _, err := os.Stat(root); os.IsNotExist(err) { |
| - t.Skip("no corpus yet — Task 8 creates docs/site") |
| + if _, err := os.Stat(root + "/nav.json"); os.IsNotExist(err) { |
| + t.Skip("no corpus yet — Task 8 creates docs/site/nav.json") |
| } |
| site, err := Load(root) |
| if err != nil { |
| @@ -553,8 +555,8 @@ func TestLinkCheckCatchesADeadFragment(t *testing.T) { |
| // the second one. The link then goes to the wrong section rather than |
| // nowhere, which is harder to notice. |
| func TestAnchorsAreUnique(t *testing.T) { |
| - if _, err := os.Stat(root); os.IsNotExist(err) { |
| - t.Skip("no corpus yet — Task 8 creates docs/site") |
| + if _, err := os.Stat(root + "/nav.json"); os.IsNotExist(err) { |
| + t.Skip("no corpus yet — Task 8 creates docs/site/nav.json") |
| } |
| site, err := Load(root) |
| if err != nil { |
| @@ -660,7 +662,7 @@ func TestAnchorMatchesFixture(t *testing.T) { |
| Add `"encoding/json"` and `"path/filepath"` to the imports. |
| -Note this test does **not** skip on a missing corpus: `anchors.json` is created by this task, so from here on `docs/site/` exists. Tasks 2, 3 and 5–7 keep their skips until Task 8, because they need the `.md` pages, not just the fixture. |
| +Note this test does **not** skip: `anchors.json` is created by this task, so it is always there to read. The other gates keep skipping until Task 8 because they need the `.md` pages — which is why their condition tests `nav.json` rather than the `docs/site` directory this task brings into existence. |
| - [ ] **Step 3: Run to verify it passes** |
| @@ -718,8 +720,8 @@ var knownFenceLangs = map[string]bool{ |
| } |
| func TestFenceLanguagesAreDeclared(t *testing.T) { |
| - if _, err := os.Stat(root); os.IsNotExist(err) { |
| - t.Skip("no corpus yet — Task 8 creates docs/site") |
| + if _, err := os.Stat(root + "/nav.json"); os.IsNotExist(err) { |
| + t.Skip("no corpus yet — Task 8 creates docs/site/nav.json") |
| } |
| site, err := Load(root) |
| if err != nil { |
| @@ -739,10 +741,10 @@ func TestFenceLanguagesAreDeclared(t *testing.T) { |
| } |
| } |
| -// TestFenceParsingHandlesNesting proves the parser does not close an |
| -// outer fence on an inner one, which would split one block into two and |
| -// hide the second half from every gate. |
| -func TestFenceParsingHandlesNesting(t *testing.T) { |
| +// TestTestdataFencesDeclareLanguages runs the same rule against the |
| +// testdata corpus, so the gate above is proven to work on a corpus that |
| +// exists before docs/site does. |
| +func TestTestdataFencesDeclareLanguages(t *testing.T) { |
| site, err := Load(testCorpus) |
| if err != nil { |
| t.Fatalf("load: %v", err) |
| @@ -855,8 +857,8 @@ func contains(xs []string, s string) bool { |
| // someone can forget to update, so gating against it would only prove |
| // the docs match a second copy of the docs. The switch is the CLI. |
| func TestCLICommandsAreDocumented(t *testing.T) { |
| - if _, err := os.Stat(root); os.IsNotExist(err) { |
| - t.Skip("no corpus yet — Task 8 creates docs/site") |
| + if _, err := os.Stat(root + "/nav.json"); os.IsNotExist(err) { |
| + t.Skip("no corpus yet — Task 8 creates docs/site/nav.json") |
| } |
| site, err := Load(root) |
| if err != nil { |
| @@ -1026,8 +1028,8 @@ Append to `internal/docsite/fences_test.go`: |
| // prose. A renamed verb leaves every fence containing it copy-pasteable |
| // and wrong, and nothing else in this package would notice. |
| func TestFencedCarlosCommandsAreReal(t *testing.T) { |
| - if _, err := os.Stat(root); os.IsNotExist(err) { |
| - t.Skip("no corpus yet — Task 8 creates docs/site") |
| + if _, err := os.Stat(root + "/nav.json"); os.IsNotExist(err) { |
| + t.Skip("no corpus yet — Task 8 creates docs/site/nav.json") |
| } |
| site, err := Load(root) |
| if err != nil { |
| @@ -1067,7 +1069,13 @@ func fencedCarlosVerbs(body string) []string { |
| } |
| fields := strings.Fields(line) |
| for i, f := range fields { |
| - if f != "carlos" || i+1 >= len(fields) { |
| + // Bare "carlos", or the binary called by an absolute path — |
| + // runbooks on a box invoke /opt/carlos/carlos, and those |
| + // examples go stale the same way. |
| + if f != "carlos" && !strings.HasSuffix(f, "/carlos") { |
| + continue |
| + } |
| + if i+1 >= len(fields) { |
| continue |
| } |
| next := fields[i+1] |
| @@ -1116,7 +1124,7 @@ GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal |
| Expected: `TestFencedCarlosCommandsAreReal` SKIP, `TestFencedCarlosVerbsIgnoresNoise` PASS. |
| -The `/opt/carlos/carlos adopt` case yields `adopt` — the path segment is one field, so `carlos` matches only the binary name. That is the behaviour we want, and the test pins it. |
| +The `/opt/carlos/carlos adopt` case yields `adopt`: a field ending in `/carlos` is the binary called by absolute path, which is how the on-box runbooks invoke it, and those examples go stale the same way bare ones do. `echo carlos` yields nothing because no word follows, and `carlos -h` yields nothing because a flag is not a verb. The test pins all three. |
| - [ ] **Step 3: Commit** |
| @@ -1149,11 +1157,10 @@ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>" |
| - [ ] **Step 1: Get the command list** |
| ```bash |
| -GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go run ./scripts/... 2>/dev/null || \ |
| - go doc ./internal/docsite TopLevelCommands |
| +grep -o 'case "[a-z-]*"' main.go | sort -u |
| ``` |
| -Simpler: read `main.go:138-204`, the `usage` function. It already groups every command by role — your apps, your deployment, on the host — with a one-line description each. That grouping is the CLI page's structure. Do not copy its wording; it is terse to fit a terminal, and the page has room to say what a command is for. |
| +That is the list the gate will hold you to. Then read `main.go:138-204`, the `usage` function. It already groups every command by role — your apps, your deployment, on the host — with a one-line description each. That grouping is the CLI page's structure. Do not copy its wording; it is terse to fit a terminal, and the page has room to say what a command is for. |
| - [ ] **Step 2: Write `docs/site/nav.json`** |