settings-3: apply Paul's approved copy and pin it (2026-10-11)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+112
−62
docs/superpowers/plans/2026-10-07-instance-settings-3-copy.json+30 −0internal/updatewords/words_approved_test.go+79 −36internal/updatewords/words_console.go+3 −26
diff --git a/docs/superpowers/plans/2026-10-07-instance-settings-3-copy.json b/docs/superpowers/plans/2026-10-07-instance-settings-3-copy.json| new file mode 100644 |
| index 000000000..54a2c060a |
| --- /dev/null |
| +++ b/docs/superpowers/plans/2026-10-07-instance-settings-3-copy.json |
| @@ -0,0 +1,30 @@ |
| +{ |
| + "approved_by": "Paul", |
| + "approved_on": "2026-10-11", |
| + "entries": [ |
| + {"id": "settings.console.bad-updates", "file": "internal/updatewords/words_console.go", "text": "updates must be auto or manual"}, |
| + {"id": "settings.console.bad-major", "file": "internal/updatewords/words_console.go", "text": "major must be a whole number, 0 or more, or empty to clear it"}, |
| + {"id": "settings.console.major-off-stable", "file": "internal/updatewords/words_console.go", "text": "a major hold applies only to instances on the stable channel. %s is on %s"}, |
| + {"id": "settings.console.named-unit", "file": "internal/updatewords/words_console.go", "text": "%s is run by its own systemd unit, so it always takes its channel's builds. Update settings do not apply to it"}, |
| + {"id": "settings.console.not-instance", "file": "internal/updatewords/words_console.go", "text": "update settings apply to instances. %s is a %s route"}, |
| + {"id": "settings.console.channel-elsewhere", "file": "internal/updatewords/words_console.go", "text": "change the channel with carlos instances set --channel, which applies the app's pipeline rules"}, |
| + {"id": "settings.console.bad-pin", "file": "internal/updatewords/words_console.go", "text": "%s is not a build version"}, |
| + {"id": "settings.console.pin-unpromoted", "file": "internal/updatewords/words_console.go", "text": "%s cannot be pinned: it has no promotion record. Pick a newer build, or ask an operator to authorize this one"}, |
| + {"id": "settings.console.pin-check-failed", "file": "internal/updatewords/words_console.go", "text": "could not check the promotion record of %s. Try again"}, |
| + {"id": "settings.console.not-enforced", "file": "internal/updatewords/words_console.go", "text": "pin, manual and major can be turned on once every box runs a release that applies them. %d boxes are not ready yet"}, |
| + {"id": "settings.console.enforcement-unknown", "file": "internal/updatewords/words_console.go", "text": "could not check whether every box applies update settings, so pin, manual and major stay off for now. Try again"}, |
| + {"id": "settings.console.pinned", "file": "internal/updatewords/words_console.go", "text": "%s is pinned to %s, so Update now would not move it. Clear the pin first"}, |
| + {"id": "settings.console.fast-track-owner", "file": "internal/updatewords/words_console.go", "text": "only an account owner can skip the safety delay"}, |
| + {"id": "settings.console.app-keeps", "file": "internal/updatewords/words_console.go", "text": "%s is kept by the app for itself. Ask someone who can change the app's settings"}, |
| + {"id": "settings.console.no-manifest", "file": "internal/updatewords/words_console.go", "text": "this app does not create instances yet. Run carlos instances enable first"}, |
| + {"id": "settings.console.unknown-policy-key", "file": "internal/updatewords/words_console.go", "text": "%s is not an app update setting. The app sets instance_control, default_updates, default_channel and default_major; there is no default pin"}, |
| + {"id": "settings.console.bad-control", "file": "internal/updatewords/words_console.go", "text": "instance_control may name updates, channel, major and pin, each once"}, |
| + {"id": "settings.console.changed", "file": "internal/updatewords/words_console.go", "text": "this instance changed while the request was in flight. Try again"}, |
| + {"id": "settings.console.store-failed", "file": "internal/updatewords/words_console.go", "text": "could not save the update settings. Try again"}, |
| + {"id": "settings.console.read-failed", "file": "internal/updatewords/words_console.go", "text": "could not read this instance. Try again"}, |
| + {"id": "settings.console.policy-read-failed", "file": "internal/updatewords/words_console.go", "text": "could not read the app's update policy. Try again"}, |
| + {"id": "settings.console.audit-set", "file": "internal/updatewords/words_console.go", "text": "changed the update settings of %s: %s"}, |
| + {"id": "settings.console.audit-install", "file": "internal/updatewords/words_console.go", "text": "asked %s to update now (request %d)"}, |
| + {"id": "settings.console.audit-policy", "file": "internal/updatewords/words_console.go", "text": "changed the instance update policy of %s: %s"} |
| + ] |
| +} |
diff --git a/internal/updatewords/words_approved_test.go b/internal/updatewords/words_approved_test.go| index 64aecbb5d..7e73ab107 100644 |
| --- a/internal/updatewords/words_approved_test.go |
| +++ b/internal/updatewords/words_approved_test.go |
| @@ -8,57 +8,100 @@ import ( |
| "testing" |
| ) |
| -// The settings-2 copy Paul approved (2026-10-09). The record must name exactly |
| -// the IDs of All(), each text must be what Text returns byte for byte, and the |
| -// CLI docs page must carry the authorize paragraph verbatim. |
| -func TestApprovedCopyIsApplied(t *testing.T) { |
| - raw, err := os.ReadFile("../../docs/superpowers/plans/2026-10-07-instance-settings-2-copy.json") |
| +type approvedEntry struct{ ID, File, Text string } |
| + |
| +type approvedRecord struct { |
| + ApprovedBy string `json:"approved_by"` |
| + ApprovedOn string `json:"approved_on"` |
| + Entries []approvedEntry |
| +} |
| + |
| +func loadApproved(t *testing.T, path string) approvedRecord { |
| + t.Helper() |
| + raw, err := os.ReadFile(path) |
| if err != nil { |
| t.Fatal(err) |
| } |
| - var rec struct { |
| - ApprovedBy string `json:"approved_by"` |
| - ApprovedOn string `json:"approved_on"` |
| - Entries []struct{ ID, File, Text string } |
| - } |
| + var rec approvedRecord |
| if err := json.Unmarshal(raw, &rec); err != nil { |
| t.Fatal(err) |
| } |
| - if rec.ApprovedBy == "" || rec.ApprovedOn == "" { |
| - t.Fatal("the record has no approval") |
| + return rec |
| +} |
| + |
| +// The copy Paul approved lives in two records: plan 2's (2026-10-09, the |
| +// authorize strings) and plan 3's (2026-10-11, the settings.console.* |
| +// strings). Each record must name exactly its own IDs, each text must be what |
| +// Text returns byte for byte, each entry must name the file that holds it, and |
| +// together the two records must name exactly the IDs of All(), so a new draft |
| +// with no approved record fails. |
| +func TestApprovedCopyIsApplied(t *testing.T) { |
| + const console = "settings.console." |
| + records := []struct { |
| + path string |
| + file string |
| + in func(id string) bool |
| + }{ |
| + {"../../docs/superpowers/plans/2026-10-07-instance-settings-2-copy.json", |
| + "internal/updatewords/words_authorize.go", |
| + func(id string) bool { return !strings.HasPrefix(id, console) }}, |
| + {"../../docs/superpowers/plans/2026-10-07-instance-settings-3-copy.json", |
| + "internal/updatewords/words_console.go", |
| + func(id string) bool { return strings.HasPrefix(id, console) }}, |
| } |
| - var got, want []string |
| + var union []string |
| docs := "" |
| - for _, e := range rec.Entries { |
| - got = append(got, e.ID) |
| - if e.Text == "" { |
| - t.Errorf("%s: the approved text is empty", e.ID) |
| - continue |
| - } |
| - if e.File != "internal/updatewords/words_authorize.go" { |
| - t.Errorf("%s: unexpected file %s", e.ID, e.File) |
| + for _, r := range records { |
| + rec := loadApproved(t, r.path) |
| + if rec.ApprovedBy == "" || rec.ApprovedOn == "" { |
| + t.Fatalf("%s: the record has no approval", r.path) |
| } |
| - if e.ID == "settings.authorize.docs" { |
| - docs = e.Text |
| + var got, want []string |
| + seen := map[string]bool{} |
| + for _, e := range rec.Entries { |
| + got = append(got, e.ID) |
| + if seen[e.ID] { |
| + t.Errorf("%s: %s appears twice", r.path, e.ID) |
| + } |
| + seen[e.ID] = true |
| + if e.Text == "" { |
| + t.Errorf("%s: the approved text is empty", e.ID) |
| + continue |
| + } |
| + if e.File != r.file { |
| + t.Errorf("%s: unexpected file %s, want %s", e.ID, e.File, r.file) |
| + } |
| + if e.ID == "settings.authorize.docs" { |
| + docs = e.Text |
| + } |
| + if !has(e.ID) { |
| + continue // reported by the ID comparison below |
| + } |
| + if txt := Text(e.ID); txt != e.Text { |
| + t.Errorf("%s: text is %q, approved %q", e.ID, txt, e.Text) |
| + } |
| } |
| - if !has(e.ID) { |
| - continue // reported by the ID comparison below |
| + for _, d := range All() { |
| + if r.in(d.ID) { |
| + want = append(want, d.ID) |
| + } |
| } |
| - if txt := Text(e.ID); txt != e.Text { |
| - t.Errorf("%s: text is %q, approved %q", e.ID, txt, e.Text) |
| + sort.Strings(got) |
| + sort.Strings(want) |
| + if strings.Join(got, "\n") != strings.Join(want, "\n") { |
| + t.Errorf("%s: record IDs %v, want %v", r.path, got, want) |
| } |
| + union = append(union, got...) |
| } |
| + |
| + var all []string |
| for _, d := range All() { |
| - // Plan 3's settings.console.* drafts get their own approved record after Paul's copy review. |
| - if strings.HasPrefix(d.ID, "settings.console.") { |
| - continue |
| - } |
| - want = append(want, d.ID) |
| + all = append(all, d.ID) |
| } |
| - sort.Strings(got) |
| - sort.Strings(want) |
| - if strings.Join(got, "\n") != strings.Join(want, "\n") { |
| - t.Errorf("record IDs %v, want the IDs of All() %v", got, want) |
| + sort.Strings(union) |
| + sort.Strings(all) |
| + if strings.Join(union, "\n") != strings.Join(all, "\n") { |
| + t.Errorf("the approved records name %v, want exactly the IDs of All() %v", union, all) |
| } |
| if docs == "" { |
diff --git a/internal/updatewords/words_console.go b/internal/updatewords/words_console.go| index 71d7a92f9..d4c2e749f 100644 |
| --- a/internal/updatewords/words_console.go |
| +++ b/internal/updatewords/words_console.go |
| @@ -1,80 +1,57 @@ |
| package updatewords |
| // Plan 3's member-facing strings: the console's update settings doors, the |
| -// install request and the app policy. Every one is a draft until Paul's copy |
| -// review; plan 3's approved record pins them after that. |
| +// install request and the app policy. Paul approved every one on 2026-10-11, |
| +// and docs/superpowers/plans/2026-10-07-instance-settings-3-copy.json pins |
| +// them. Do not edit a text here without a new copy review. |
| func init() { |
| add( |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.bad-updates", Text: "updates must be auto or manual", |
| Context: "Returned by carlos instances set and the console when updates is anything else."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.bad-major", Text: "major must be a whole number, 0 or more, or empty to clear it", |
| Context: "Returned when the major hold is not a number."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.bad-pin", Text: "%s is not a build version", |
| Context: "Returned when the pin is not a build version: not text, or text no build could be named. %s is the value sent, printable characters only, or the word that when none are left."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.major-off-stable", Text: "a major hold applies only to instances on the stable channel. %s is on %s", |
| Context: "Returned when major is set on an instance that follows another channel. Host, channel."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.pin-unpromoted", Text: "%s cannot be pinned: it has no promotion record. Pick a newer build, or ask an operator to authorize this one", |
| Context: "Returned when the pin names a build that was never promoted, or was promoted before records existed. %s is the version. Only an operator can run carlos releases authorize."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.pin-check-failed", Text: "could not check the promotion record of %s. Try again", |
| Context: "Returned when the console could not read or verify the promotion record of the build being pinned. %s is the version."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.not-enforced", Text: "pin, manual and major can be turned on once every box runs a release that applies them. %d boxes are not ready yet", |
| Context: "Returned when someone turns on pin, manual or major before every box is upgraded. %d is how many boxes are still on an older release. Clearing these settings is always allowed."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.enforcement-unknown", Text: "could not check whether every box applies update settings, so pin, manual and major stay off for now. Try again", |
| Context: "Returned when someone turns on pin, manual or major and the console could not read which release each box runs. Clearing these settings is always allowed."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.named-unit", Text: "%s is run by its own systemd unit, so it always takes its channel's builds. Update settings do not apply to it", |
| Context: "Returned for an instance whose process the platform does not start, such as a console. %s is the host."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.not-instance", Text: "update settings apply to instances. %s is a %s route", |
| Context: "Returned for a static site or a service route. Host, kind."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.channel-elsewhere", Text: "change the channel with carlos instances set --channel, which applies the app's pipeline rules", |
| Context: "Returned when a request to the settings door names channel. The channel has its own door with passkey and approval rules."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.app-keeps", Text: "%s is kept by the app for itself. Ask someone who can change the app's settings", |
| Context: "Returned, once instances can act for themselves, when an instance tries to change a setting the app's policy keeps. %s is the setting. Not reachable until instance credentials ship."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.pinned", Text: "%s is pinned to %s, so Update now would not move it. Clear the pin first", |
| Context: "Returned by carlos instances update and the Update now button on a pinned instance. Host, pinned version."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.fast-track-owner", Text: "only an account owner can skip the safety delay", |
| Context: "Returned when someone who is not an account owner asks Update now to skip the bake window."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.changed", Text: "this instance changed while the request was in flight. Try again", |
| Context: "Returned when two people changed the same instance at once and this request lost."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.store-failed", Text: "could not save the update settings. Try again", |
| Context: "Returned when the console could not write the record."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.read-failed", Text: "could not read this instance. Try again", |
| Context: "Returned by the update settings doors when the console could not read the instance's record."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.policy-read-failed", Text: "could not read the app's update policy. Try again", |
| Context: "Returned by the update settings doors when the console could not read the app's instances manifest, which holds its update policy."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.no-manifest", Text: "this app does not create instances yet. Run carlos instances enable first", |
| Context: "Returned by carlos apps set's update flags for an app with no instance provisioning."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.unknown-policy-key", Text: "%s is not an app update setting. The app sets instance_control, default_updates, default_channel and default_major; there is no default pin", |
| Context: "Returned by the app policy door for any other key, for example a default pin. %s is the key."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.bad-control", Text: "instance_control may name updates, channel, major and pin, each once", |
| Context: "Returned by carlos apps set --instance-control for anything else."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.audit-set", Text: "changed the update settings of %s: %s", |
| Context: "Activity line after a settings change. Host, then what changed, for example updates manual, pin cleared."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.audit-install", Text: "asked %s to update now (request %d)", |
| Context: "Activity line after Update now. Host, request number."}, |
| - // TODO-COPY: draft, awaiting Paul's copy review |
| Draft{ID: "settings.console.audit-policy", Text: "changed the instance update policy of %s: %s", |
| Context: "Activity line after an app policy change. App, then what changed."}, |
| ) |