Skip to content

writ reconcile: the command is renamed, and its renderings go through the sink - #778

Merged
David-Noble-at-work merged 5 commits into
developfrom
feature/774-writ-reconcile
Sep 2, 2026
Merged

David-Noble-at-work merged 5 commits into
developfrom
feature/774-writ-reconcile

Conversation

@David-Noble-at-work

@David-Noble-at-work David-Noble-at-work commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Closes #774

writ status is retired and writ reconcile takes its place. All 30 of writ's stdout call sites route
through the sink, and the five globals the root registers are consumed, not merely present. This is
phase 2 of #762 as well as the first item of #740; both plans record that they are one piece of work.

Acceptance criteria

The canonical copy is the plan's own section —
774-writ-reconcile.md § Acceptance criteria.
Every box is checked before merge.

Goals

  • writ status does not exist and has no alias — TestReconcile_StatusIsGone
  • Every stdout byte in cmd/writ goes through the sink — the only os.Stdout left is a .Stat() TTY
    check; 30 of 30 sites gone
  • Every global is consumed, not merely registered — five scenario assertions, each by observed effect

Test rows

  • 1 — writ status does not exist: TestReconcile_StatusIsGone (unit)
  • 2 — each of eight -o values renders on writ reconcile: TestReport_EveryFormatRenders (8 subtests)
    + the scenario loop
  • 3 — --jq '.entries' returns the delta alone: scenario
  • 4 — --store <dir> reads that store: scenario — an empty store is refused for its missing run index
  • 5 — --dry-run yields the plan, rendered by -o: scenario deploy --dry-run -o json is a JSON array
    + two unit tests
  • 6 — no fmt.Print* in cmd/writ/writ/reconcile: 0 in four files; the tree-wide test is Enforcement: two tests that fail when a command leaves the output convention #776's by
    design
  • 7 — the scenario suite passes on all five platforms: darwin-arm64 locally, and on the pull
    request scenario (darwin-arm64), scenario (linux-amd64), scenario (linux-arm64), scenario (windows-amd64), scenario (windows-arm64) — all green

Gates, every phase: make build, make vet, make test, make test-scenario, gofmt -l empty,
Test-GuideFrontmatter.sh.

The four phases

  1. The rename, with history — git mv cmd/writ/writ/status → reconcile/, the package surface, the
    scenario test, sixteen documents changed outright and two step-47 records given a note.
  2. The report is the result. 22 fmt.Print* deleted; BuildReport hands the Report to emitResult;
    -o, --jq, --filter apply to it like any result. The one-bool --json bridge goes.
  3. The plan is the result under --dry-run. Four Executes return ([]*op.Graph, error); Graph's
    own marshalers make the value its document, so no helper. migrate --format retired; its session emits
    through an Options.Emit hook.
  4. The globals proved by effect, and the rename pinned by a test.

Found on the way, logged where each belongs

Also carried

The plans for #775 and #776, committed here before their work starts, per the process. This PR closes
#774 alone.

https://claude.ai/code/session_01SqQVFZz7AdiLZkGoAcUqRW

David Noble added 5 commits September 1, 2026 15:14
Three of the four items left on #740 had no issue and no plan. Now #774, #775 and #776 each have both,
cross-referenced in both directions -- each issue links its plan, each plan names its issue, and every
plan links the other two and the epic's.

The plans land together on #774's branch, following the precedent #768 set: plans for work that is not
yet started ride with the first piece that is. This pull request will close #774 alone when its work is
done; #775 and #776 close when theirs is.

#774 -- writ reconcile. The rename and the 30 stdout call sites are ONE piece of work, not a rename
followed by a refactor: the renderings being rewritten live in the package being renamed. This is also
#762's phase 2, and both plans say so. The surface is the one #772 settled -- a report, no command
flags, the globals used faithfully.

#775 -- lore. The search table moves onto the shared TableFormatter, three hand-rolled flag sets go, and
thirteen fmt.Print calls are triaged. #741 is kept separate so a user-visible defect is not buried in a
refactor, and closes under #775 only if a multi-byte rune test -- written red first -- proves the fix.

#776 -- enforcement. Three tests, sequenced LAST because they are red until the other items land and
they certify those items rather than precede them. Each of #774, #775 and #743 turns exactly one test
green, which makes each of those pull requests' "done" observable rather than asserted. The third test
is the one that would have caught #753 and #754: a root that registers the flags and leaves that ignore
them looks like compliance to the other two.

740's table of remaining items now names the issue and plan for each, and the paragraph that said three
needed filing says they are filed.

Claude-Session: https://claude.ai/code/session_01SqQVFZz7AdiLZkGoAcUqRW
Phase 1 of #774, and phase 2 of #762: the two plans record that these are one piece of work.

THE PACKAGE MOVES WITH ITS HISTORY. git mv cmd/writ/writ/status -> cmd/writ/writ/reconcile, then
status.go -> reconcile.go and the integration test to match, so `git log --follow` crosses the rename.
report.go keeps its name. The package clause, the doc comments, and the surface follow: Config, Execute,
BuildReport; StatusConfig -> ReconcileConfig; parseStatusConfig -> parseReconcileConfig, whose doc comment
already said parseReconcileConfig -- someone started this rename before and got one line in;
newStatusCmd -> newReconcileCmd; runStatus -> runReconcile.

THE COMMAND IS `writ reconcile`. Use, Long, and Example say so; "Status indicators" becomes "Entry
states"; "Status is report-only" becomes "Reconcile produces a report", which is the surface #772
settled -- a report, no command flags, the globals used faithfully. No alias: `writ status` stops
existing.

The package doc records why the name went away and came back: step 47 named it status because
"reconcile" promised a mutation the command did not perform; #762 returned the name once repair was
chartered.

THE SCENARIO TEST invokes the new name and asserts under it. Six Go comments outside the package that
described defects in `writ status` now say `writ reconcile`; the one that says the command "shipped for
months" answering -o yaml with a dashboard says it shipped as `writ status` then, because otherwise the
sentence is false.

THE DOCUMENTS. Seven architecture and guide documents and nine plans changed outright -- the two
user-facing guides among them also lose a `--json` that no longer exists, in favor of `-o json`. Two
documents are the record of the original step-47 rename, and "reconcile replaces reconcile" is
nonsense, so those carry a note after their H1 and keep their text. The remaining `writ status` in the
tree is in history notes and in the plans that name the retired command as the thing retired.

FOUND ON THE WAY: three of #762's phase-1 boxes were completed by #767 and never ticked. Ticked now and
recorded as stale rather than quietly corrected, per the process.

The 30 stdout call sites are NOT touched here. That is phase 2, and it is the reason this is one piece of
work rather than two: the 22 in report.go are rewritten next, in the package that now has its final name.

make build, make vet, make test, gofmt -l, and Test-GuideFrontmatter.sh are all green.

Claude-Session: https://claude.ai/code/session_01SqQVFZz7AdiLZkGoAcUqRW
Phase 2 of #774. The 22 fmt.Print* calls in reconcile/report.go are deleted, not routed: the human
rendering is `-o table` or `-o list` applied to the report's JSON, per the two-stage model, so there is
no text renderer to route and no format decision left in the package.

WHAT GOES. presentJSON, presentText, presentEntries, and State.String() -- the glyph the text report
drew, which no output now carries. reconcile.Execute goes with them; BuildReport is the entry, and it
returns the Report to the command. Config.JSON goes, and with it the one-bool bridge at config.go:160
that read outputOptions.Format == "json" so that a boolean could stand in for a format -- the thing #740
retired --json to stop.

WHAT REMAINS. runReconcile builds the report and hands it to emitResult, exactly as verify does. Every
--output value, --jq and --filter now apply to it the way they apply to any other command's result,
because the report reaches BuildPipeline instead of bypassing it. The help text's "Entry states" lists
the labels JSON emits -- linked, copied, missing -- rather than glyphs, and says that --jq '.entries'
selects the delta alone.

THE TEST, AT TWO LEVELS. TestReport_EveryFormatRenders pins the Report's shape under the shared
pipeline: State marshals as its label, the four sections survive stage-1 normalization, none of the
eight formats is silently rejected. TestWritDeployScenario_Deploy gains a loop over the seven non-JSON
formats against the real binary and fails on the human dashboard or on silence -- and that is the
assertion this phase exists to turn green, because the defect was in the command's wiring, not in the
pipeline. make test skips the scenario suite; make test-scenario ran it, and it passed.

On "write the failing test first": the scenario assertion is red on the previous tree by
construction -- presentText's first statement was fmt.Println("Layers:"), and the assertion fails on
that string. It was written and the wiring changed in one pass, so red was argued rather than observed.
The plan says so, at the right size.

Eight of the thirty stdout sites remain: four SerializeGraphs dry-run dumps and two in migrate, which
are phase 3, and two in verify already gone.

Also riding here, unrelated to writ and logged where it was asked: a question for the configuration
design (#441), recorded in configuration.md's Open questions and in thread 4's plan -- should a config
system be instantiated from the flags cobra has parsed and viper has bound, rather than binding its own?
Source 5 of resolution says "the parsed pflag set, read once" without saying whose.

Claude-Session: https://claude.ai/code/session_01SqQVFZz7AdiLZkGoAcUqRW
Phase 3 of #774: the last eight of writ's thirty stdout sites.

THE FOUR DRY-RUN DUMPS. deploy, decommission, upgrade and secret each wrote their graphs to os.Stdout
with SerializeGraphs and returned. Each Execute now returns ([]*op.Graph, error) -- the plan under
--dry-run, nil otherwise -- following verify.Execute, and the command hands a non-nil plan to
emitResult. Every --output value, --jq and --filter apply to a dry run the way they apply to any other
result, and `-o yaml` on a dry run is a parseable plan rather than a dump.

No new helper was needed. Graph has its own MarshalJSON and MarshalYAML, so the Go value's document form
is SerializeGraphs's -- as a JSON array or YAML sequence rather than a multi-document stream, which is
the one visible difference and a better one: `--jq '.[0]'` works.

28 test call sites now discard the plan they never asserted on. Two dry-run tests assert it comes back,
and the scenario suite runs `deploy --dry-run -o json` against the real binary and requires a non-empty
JSON array -- red before this, when -o json yielded YAML.

MIGRATE. Its own --format retires for -o. FormatMigrationPlan and the eleven text helpers under it are
deleted; NewMigrationView builds the value the pipeline renders, and the human rendering is -o table or
-o list on that JSON. Options.Format was read by nothing and goes with it. The session's one os.Stdout
write was its result -- the analysis and graph at completion -- and is emitted through an Options.Emit
hook the command supplies, since the session has no writer of its own.

THE DOCUMENTS. 10-command-line-interface.status.md ticks writ's renderings box: all 30 sites, with the
phase that took each. The conformance table in 10-command-line-interface.md moves writ to "none".

FOUND ON THE WAY, logged where each belongs. manage-environments.md teaches `writ inspect --format`, a
command that does not exist (#777, not this work). Two lore guides teach --format, recorded in #775's
file list and resolved there.

make build, make vet, make test (106 ok, 0 FAIL) and make test-scenario are green; gofmt -l is empty.

Claude-Session: https://claude.ai/code/session_01SqQVFZz7AdiLZkGoAcUqRW
Phase 4 of #774, the last. Three more assertions in the scenario suite, against the real binary, so
that every global writ registers is shown to be consumed rather than merely present -- a flag on a root
that no leaf reads looks like compliance, which is how #753 and #754 shipped.

--jq '.entries' returns the delta as a JSON array. --filter with a malformed expression is refused, and
the refusal names the expression: were the flag ignored, the command would print the report and exit 0,
a typo honored as a request for the default. --store pointed at a store that was never written is
refused for its missing run index rather than reporting on the default store as though it had complied.

With -o (phase 2, eight formats) and --dry-run (phase 3, the plan as a JSON array), that is all five.

AND THE RENAME IS PINNED. TestReconcile_StatusIsGone executes the root with `status` and requires
"unknown command", then executes `reconcile --help` so the test cannot pass by the root being broken.
Until now the old name's absence was verified by grep; the plan's own test row 1 asked for a test.

THE ACCEPTANCE CRITERIA are written into the plan as a checklist -- three goals, seven test rows, the
gates -- and the pull request carries a copy. Every box is checked before merge. One cannot be checked
from this branch: row 7, the scenario suite on all five platforms, is four-fifths CI, and it is checked
on the pull request when every scenario leg is green.

docs/cli regenerates cleanly: reconcile.md present, status.md gone, and no --format on migrate.md. It is
gitignored here and published from develop by docs-publish.yaml, so there is nothing to commit for it.

The plan is complete. 740's table marks this item landed; #775 is next.

Claude-Session: https://claude.ai/code/session_01SqQVFZz7AdiLZkGoAcUqRW
@David-Noble-at-work David-Noble-at-work self-assigned this Sep 2, 2026
@David-Noble-at-work
David-Noble-at-work merged commit d4eb623 into develop Sep 2, 2026
12 checks passed
@David-Noble-at-work
David-Noble-at-work deleted the feature/774-writ-reconcile branch September 2, 2026 01:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

writ reconcile: the command is renamed, and its renderings go through the sink

1 participant