Implement provision plan: spec, resolved state, and the diff

Go, per the toolchain decision. cmd/provision with internal/spec,
internal/state and internal/plan; plan computation is pure and takes live
observation through an interface, so the refusal logic is testable without
a Telegram account. It runs against the real campaign spec today.

Separates two refusals the design had treated as one. A deferral is the
design working -- the public channel waiting on a checked rendering, normal
on every first run. A block is the world disagreeing with the state file:
drifted rights, a taken-over username, a bot that is no longer reachable.
Collapsed together, a first run could never apply anything, because it
always defers the public channel.

The rights clamp and the private/public username rule are enforced in Go
and asserted against the same cases the JSON schema rejects, since mirroring
the schema in code is a drift risk worth a test rather than a comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172sgCZEEDJcnQmr4SGDvKa

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1361245@bnt-lap001
Assistant-Session: b3b428ef-f3e6-4688-b091-01f71461d66a
This commit is contained in:
tegwick 2026-09-04 21:25:49 +02:00
parent 52723bd8ad
commit 10018a99b6
10 changed files with 1108 additions and 1 deletions

View file

@ -93,7 +93,7 @@ permanent.
```task
id: FT-WP-0002-T03
status: todo
status: progress
priority: high
state_hub_task_id: "c3923422-7f2e-5bf9-906c-6d59572b09c0"
```
@ -110,6 +110,20 @@ cheapest place to catch someone asking for one.
knowable without attempting it, so the plan says "will attempt, with fallbacks"
rather than promising an outcome.
**Implemented (2026-09-04), less live observation.** Go, `cmd/provision`, with
`internal/spec`, `internal/state` and `internal/plan`. Runs against the real
campaign spec today; live observation is behind a `plan.Live` interface that
returns an offline stub until T01 mints a session, and the command says so rather
than presenting an unobserved plan as a drift check.
One thing the design got wrong and the implementation surfaced: refusals are two
different things. A **deferral** is the design working — the public channel
waiting on a checked rendering, which is normal on every first run. A **block**
is the world disagreeing with the state file — drifted rights, a taken-over
username, an unreachable bot. Collapsing them meant a first run could never apply
anything, since it always defers the public channel. They are now distinct
kinds: a deferral holds one action, a block stops the run.
## T04 — Implement `provision apply`
```task