carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1# Seeded Instances, Plan 1: Copy Deck and Gate Implementation Plan
2
3> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
5**Goal:** Pin every user-facing string of the seeded-instances series in one reviewed deck, get Paul's copy review on all of it before any code uses a string, and add the gate that keeps the code and the deck in step.
6
7**Architecture:** The deck is a markdown table (`docs/superpowers/plans/2026-10-08-seeded-instances-copy.md`, written with this plan) in the moves copy record's format. A new repo-root test file, `approved_seeded_copy_test.go`, parses it with the moves record's parser (generalised to take an id pattern), pins the inventory, refuses em dashes, requires every row approved, and checks that every row a later plan has wired appears in the file the row names. The review itself runs through the `copy-review` skill.
8
9**Tech Stack:** Go 1.25 tests at the repo root (`package main`), the existing `approved_moves_copy_test.go` helpers (`copyRow`, `checkMovesInventory`, `checkMovesApplied`, `checkMovesNoEmDash`, `movesTextApplied`).
10
11**Spec:** `docs/superpowers/specs/2026-10-08-seeded-instances-design.md` rev 7, §7 ("Every CLI and console string goes through copy review before it's written into the code"). Index: `2026-10-08-seeded-instances-0-index.md`.
12
13## Global Constraints
14
15- Every user-facing string (CLI output, errors, console messages, docs prose) is copy-reviewed by Paul before it is written into code; implementation tasks use only strings from the copy deck. Copy voice: no em dashes; full stops and new sentences instead.
16- CI cross-compiles darwin and windows: linux-only code needs `//go:build linux` plus a `!linux` stub.
17- Local test commands: targeted tests (`go test ./internal/<pkg>/ -run TestX`), not the full suite in a loop. Inside the sandbox the default Go build cache may be read-only; if so use `GOCACHE=$TMPDIR/gocache` re-exported per command. Socket tests need a short TMPDIR (`TMPDIR=/tmp/g`). The local gate may need `GOFLAGS=-buildvcs=false`.
18- Heavy loops run under `nice`; no -race full suite until the end of a plan.
19- Production use needs nested boxes; the claim-armed path is refused here and left to #250 (spec §3.4).
20- Commits end with `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
21- Do not change any moves row, id or sentence. The moves gate's own tests (`go test . -run 'TestMoves|TestParseMovesCopyTable|TestCopyRowPath'`) pass unchanged after Task 1.
22
23## Review Focus
24
251. A deck row whose text holds a pipe (`a | b`) must parse as one text cell, as the moves parser already does. Test in Task 1 (`TestParseSeededCopyTableKeepsPipes`).
262. A row deleted from the deck by mistake, or a new row added without pinning it, must fail the inventory check both ways. Test in Task 1 (`TestSeededCopyInventoryBitesBothWays`).
273. A wired id whose text was later reworded in code (or never written) must fail, naming the id and the file. Test in Task 1 (`TestSeededWiredCheckBites`).
284. Paul's edits are applied verbatim: a reworded row keeps its id, and a row he deletes is removed from both the deck and `seededCopyIDs` in the same commit. Checked by Task 2 Step 5's inventory run.
295. A deck that still holds a `pending` row after the review must fail `TestSeededCopyIsApproved`, so no later plan can start on unreviewed text. Test in Task 2.
30
31---
32
33## File structure
34
35| path | responsibility |
36|---|---|
37| `docs/superpowers/plans/2026-10-08-seeded-instances-copy.md` | the deck (already written; Task 2 updates statuses and text) |
38| `approved_moves_copy_test.go` | `parseMovesCopyTable` becomes a wrapper over the new `parseCopyTable` |
39| `approved_seeded_copy_test.go` | the seeded gate: inventory, em dash, approval, wired rows applied |
40
41---
42
43### Task 1: The seeded copy gate
44
45**Files:**
46- Modify: `approved_moves_copy_test.go:105-164` (`parseMovesCopyTable`)
47- Create: `approved_seeded_copy_test.go`
48
49**Interfaces:**
50- Consumes: `copyRow`, `checkMovesInventory(rows []copyRow, want []string) []string`, `checkMovesApplied(rows []copyRow, root string) []string`, `checkMovesNoEmDash(rows []copyRow) []string`, `reportAll(t, problems)` (all in `approved_moves_copy_test.go`).
51- Produces: `parseCopyTable(src, name string, ids *regexp.Regexp, idKind string) ([]copyRow, error)`; `seededCopyRecordPath`; `seededCopyIDs []string` (SD-N1 to SD-N116); `seededWiredIDs []string` (empty here; each later plan's wiring task appends to it); `mustParseSeededRecord(t) []copyRow`; `checkSeededWired(rows []copyRow, wired []string, root string) []string`; `checkSeededApproved(rows []copyRow) []string`.
52
53- [ ] **Step 1: Write the failing tests**
54
55Create `approved_seeded_copy_test.go`:
56
57```go
58// approved_seeded_copy_test.go
59package main
60
61import (
62 "regexp"
63 "strconv"
64 "strings"
65 "testing"
66)
67
68// The copy gate of the seeded-instances series (#260, #261). The deck is
69// reviewed BEFORE any code speaks a sentence (the reverse of the moves
70// record, where code came first), so the gate has two lists:
71//
72// - seededCopyIDs: every row the deck holds, pinned both ways;
73// - seededWiredIDs: the rows code already speaks. Each plan's wiring task
74// appends the ids it wired, and the gate checks each one's text is in
75// the file its row names.
76
77const seededCopyRecordPath = "docs/superpowers/plans/2026-10-08-seeded-instances-copy.md"
78
79var seededCopyIDPattern = regexp.MustCompile(`^SD-N[0-9]+$`)
80
81var seededCopyIDs = func() []string {
82 ids := make([]string, 0, 116)
83 for n := 1; n <= 116; n++ {
84 ids = append(ids, "SD-N"+strconv.Itoa(n))
85 }
86 return ids
87}()
88
89// seededWiredIDs grows as the plans wire rows. Plan 1 wires none.
90var seededWiredIDs = []string{}
91
92func mustParseSeededRecord(t *testing.T) []copyRow {
93 t.Helper()
94 rows, err := parseSeededCopyRecord(seededCopyRecordPath)
95 if err != nil {
96 t.Fatal(err)
97 }
98 if len(rows) == 0 {
99 t.Fatalf("%s has no rows", seededCopyRecordPath)
100 }
101 return rows
102}
103
104func TestSeededCopyRecordIsComplete(t *testing.T) {
105 reportAll(t, checkMovesInventory(mustParseSeededRecord(t), seededCopyIDs))
106}
107
108func TestSeededCopyHasNoEmDash(t *testing.T) {
109 reportAll(t, checkMovesNoEmDash(mustParseSeededRecord(t)))
110}
111
112func TestSeededWiredCopyIsApplied(t *testing.T) {
113 reportAll(t, checkSeededWired(mustParseSeededRecord(t), seededWiredIDs, "."))
114}
115
116func TestParseSeededCopyTableKeepsPipes(t *testing.T) {
117 src := "| id | file | text | status |\n|---|---|---|---|\n" +
118 "| SD-N1 | a.go | one | two | pending |\n"
119 rows, err := parseCopyTable(src, "x.md", seededCopyIDPattern, "an SD-N<n>")
120 if err != nil {
121 t.Fatal(err)
122 }
123 if len(rows) != 1 || rows[0].Text != "one | two" {
124 t.Fatalf("rows = %+v", rows)
125 }
126 if _, err := parseCopyTable(strings.Replace(src, "SD-N1", "MV-N1", 1), "x.md", seededCopyIDPattern, "an SD-N<n>"); err == nil ||
127 !strings.Contains(err.Error(), "is not an SD-N<n> id") {
128 t.Fatalf("a moves id in the seeded deck: err = %v", err)
129 }
130}
131
132func TestSeededCopyInventoryBitesBothWays(t *testing.T) {
133 rows := []copyRow{{"SD-N1", "a.go", "x", "pending"}, {"SD-N3", "a.go", "y", "pending"}}
134 got := checkMovesInventory(rows, []string{"SD-N1", "SD-N2"})
135 if len(got) != 2 {
136 t.Fatalf("problems = %q, want a missing SD-N2 and an extra SD-N3", got)
137 }
138}
139
140func TestSeededWiredCheckBites(t *testing.T) {
141 dir := t.TempDir()
142 writeFile(t, dir, "a.go", "package a\n\nvar s = \"hello there\"\n")
143 rows := []copyRow{
144 {"SD-N1", "a.go", "hello there", "approved"},
145 {"SD-N2", "a.go", "goodbye", "approved"},
146 {"SD-N3", "a.go", "hello there", "pending"},
147 }
148 if got := checkSeededWired(rows, []string{"SD-N1"}, dir); len(got) != 0 {
149 t.Errorf("a wired, applied row: problems = %q", got)
150 }
151 if got := checkSeededWired(rows, []string{"SD-N2"}, dir); len(got) != 1 || !strings.Contains(got[0], "SD-N2") {
152 t.Errorf("a wired row whose text is missing: problems = %q", got)
153 }
154 if got := checkSeededWired(rows, []string{"SD-N3"}, dir); len(got) != 1 || !strings.Contains(got[0], "pending") {
155 t.Errorf("a wired pending row: problems = %q", got)
156 }
157 if got := checkSeededWired(rows, []string{"SD-N9"}, dir); len(got) != 1 || !strings.Contains(got[0], "no row") {
158 t.Errorf("a wired id with no row: problems = %q", got)
159 }
160}
161```
162
163If the repo root has no `writeFile(t, dir, name, body string)` test helper yet (`grep -n 'func writeFile' *_test.go`), add this one at the end of the new file:
164
165```go
166func writeFile(t *testing.T, dir, name, body string) {
167 t.Helper()
168 if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
169 t.Fatal(err)
170 }
171}
172```
173
174with `"os"` and `"path/filepath"` added to the imports. If a helper of that name already exists with another signature, name this one `writeSeededFile` and use that name in `TestSeededWiredCheckBites`.
175
176- [ ] **Step 2: Run the tests to verify they fail**
177
178Run: `go test . -run 'TestSeeded|TestParseSeeded' -count=1`
179Expected: FAIL to compile: `undefined: parseSeededCopyRecord`, `undefined: parseCopyTable`, `undefined: checkSeededWired`.
180
181- [ ] **Step 3: Generalise the parser and add the seeded helpers**
182
183In `approved_moves_copy_test.go`, rename the body of `parseMovesCopyTable` to `parseCopyTable` with the id pattern as a parameter, and keep `parseMovesCopyTable` as a wrapper so every moves caller and every moves error message is unchanged:
184
185```go
186func parseMovesCopyTable(src, name string) ([]copyRow, error) {
187 return parseCopyTable(src, name, movesCopyIDPattern, "an M-C<n>, MV-N<n> or HC5-N<n>")
188}
189
190// parseCopyTable reads a copy record's tables: the moves record and the
191// seeded deck share the format. ids is the record's id pattern and idKind
192// names it in the refusal.
193func parseCopyTable(src, name string, ids *regexp.Regexp, idKind string) ([]copyRow, error) {
194 var rows []copyRow
195 seen := map[string]int{}
196 inTable := false
197 for n, line := range strings.Split(src, "\n") {
198 // ... the existing loop body, unchanged, except the id check:
199 if !ids.MatchString(r.ID) {
200 return nil, fmt.Errorf("%s: %q is not %s id", at, r.ID, idKind)
201 }
202 // ... rest unchanged
203 }
204 return rows, nil
205}
206```
207
208The only edit inside the loop is that one `if`: the old line read `if !movesCopyIDPattern.MatchString(r.ID) { return nil, fmt.Errorf("%s: %q is not an M-C<n>, MV-N<n> or HC5-N<n> id", at, r.ID) }`, and the new one produces the same bytes for the moves record.
209
210Append to `approved_seeded_copy_test.go`:
211
212```go
213func parseSeededCopyRecord(path string) ([]copyRow, error) {
214 raw, err := os.ReadFile(path)
215 if err != nil {
216 return nil, err
217 }
218 return parseCopyTable(string(raw), path, seededCopyIDPattern, "an SD-N<n>")
219}
220
221// checkSeededWired checks every wired id: it has a row, the row is
222// approved, and its text is in the file the row names (the moves gate's own
223// matcher, so a Go file must carry it as one literal, concatenation or
224// format, and any other file raw).
225func checkSeededWired(rows []copyRow, wired []string, root string) []string {
226 byID := map[string]copyRow{}
227 for _, r := range rows {
228 byID[r.ID] = r
229 }
230 var out []string
231 var applyRows []copyRow
232 for _, id := range wired {
233 r, ok := byID[id]
234 switch {
235 case !ok:
236 out = append(out, id+": wired, but the deck has no row for it")
237 case r.Status != "approved":
238 out = append(out, id+": wired, but its row is "+r.Status+": only approved text may reach code")
239 default:
240 applyRows = append(applyRows, r)
241 }
242 }
243 return append(out, checkMovesApplied(applyRows, root)...)
244}
245
246// checkSeededApproved fails on any row that is not approved. Plan 1 Task 2
247// adds the test that calls it, once Paul's review has landed.
248func checkSeededApproved(rows []copyRow) []string {
249 var out []string
250 for _, r := range rows {
251 if r.Status != "approved" {
252 out = append(out, r.ID+" is "+r.Status+": the deck is reviewed before any plan wires a string")
253 }
254 }
255 return out
256}
257```
258
259Add `"os"` to the new file's imports.
260
261- [ ] **Step 4: Run the tests to verify they pass**
262
263Run: `go test . -run 'TestSeeded|TestParseSeeded|TestMoves|TestParseMovesCopyTable|TestCopyRowPath' -count=1`
264Expected: PASS. `TestSeededCopyRecordIsComplete` passes against the deck written with this plan (116 rows, all `pending`).
265
266- [ ] **Step 5: Commit**
267
268```bash
269git add approved_moves_copy_test.go approved_seeded_copy_test.go
270git commit -m "copy gate: the seeded-instances deck, pinned (Plan 1 Task 1)
271
272Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
273```
274
275### Task 2: Paul's copy review of the deck
276
277**Files:**
278- Modify: `docs/superpowers/plans/2026-10-08-seeded-instances-copy.md`
279- Modify: `approved_seeded_copy_test.go` (add `TestSeededCopyIsApproved`; adjust `seededCopyIDs` only if Paul deletes or adds rows)
280
281**Interfaces:**
282- Consumes: the deck; `checkSeededApproved`.
283- Produces: a deck whose every row is `approved`, which every later plan's wiring tasks read.
284
285- [ ] **Step 1: Write the failing test**
286
287Append to `approved_seeded_copy_test.go`:
288
289```go
290// Every row is reviewed before any plan of the series wires one (Plan 1
291// Task 2). A row added later is reviewed before the plan that adds it
292// merges, and this test holds that line.
293func TestSeededCopyIsApproved(t *testing.T) {
294 reportAll(t, checkSeededApproved(mustParseSeededRecord(t)))
295}
296```
297
298- [ ] **Step 2: Run it to verify it fails**
299
300Run: `go test . -run TestSeededCopyIsApproved -count=1`
301Expected: FAIL with 116 lines of the form `SD-N1 is pending: the deck is reviewed before any plan wires a string`.
302
303- [ ] **Step 3: Run the copy review**
304
305Invoke the `copy-review` skill with the deck. Before relaunching its server for any reason, read the review's `result.json` first (a relaunch deletes a submitted review). Give it every row's id, the file it will live in, and the text, and the context line from the deck's heading (which door or command speaks it). Wait for Paul's submitted edits. Do not continue this task until they arrive.
306
307- [ ] **Step 4: Apply the edits verbatim**
308
309For each row Paul changed, replace the text cell with his text exactly, keeping the id. For each row he deleted, delete the row and remove its id from `seededCopyIDs` (replace the generator with an explicit literal list if the ids are no longer contiguous). For each row he added, give it the next free `SD-N<n>` id, add it to the deck under the plan heading he named, and add it to `seededCopyIDs`. Then set every row's status to `approved`.
310
311Check the result has no em dash: `grep -n $'—' docs/superpowers/plans/2026-10-08-seeded-instances-copy.md` prints nothing.
312
313- [ ] **Step 5: Run the gate**
314
315Run: `go test . -run 'TestSeeded|TestParseSeeded' -count=1`
316Expected: PASS, including `TestSeededCopyIsApproved` and `TestSeededCopyRecordIsComplete`.
317
318- [ ] **Step 6: Commit**
319
320```bash
321git add docs/superpowers/plans/2026-10-08-seeded-instances-copy.md approved_seeded_copy_test.go
322git commit -m "copy: Paul's review of the seeded-instances deck, every row approved (Plan 1 Task 2)
323
324Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
325```
326
327---
328
329## Self-review
330
331- Spec coverage: §7's copy rule is the whole of this plan; every string §7 lists (list states, audit rows, each error, the swap's refusal reasons) has a deck row: list states SD-N43, SD-N46, SD-N83 to SD-N88; audit SD-N12, SD-N41, SD-N62 to SD-N66, SD-N106; errors SD-N2 to SD-N10, SD-N20 to SD-N40, SD-N48 to SD-N61, SD-N100, SD-N101, SD-N103; swap reasons SD-N91 to SD-N98.
332- The moves gate's behaviour is unchanged: the only edit there is the parser's id check, which produces the same message for the moves record.
333- Review Focus: lines 1 to 3 are tests in Task 1; 4 is Task 2 Step 5; 5 is Task 2's test.
334