HOMELAB-1729: LD4.9 plan-freshness — boundary trigger, deterministic diff, receipts #575

Merged
aaron merged 5 commits from plane/HOMELAB-1729-planfresh into epic/LD4 2026-10-02 06:48:43 +00:00
Owner

Implements LD4.9 · plan-freshness re-derivation per .rpiv/artifacts/epic-ld4/design-LD4.9-plan-freshness.md, under the orchestrator rulings:

  • boundary scope = every epic in the homelab project (any ticket that reaches a terminal state group and has a parent);
  • plan of record = the epic description's <h2>Scope</h2> checklist; unmappable prose → advisory only;
  • event-driven — no cadence/quiet-week pressure anywhere in the diff;
  • drift → a queued dispatch issue, never an immediate spawn.

Built

Piece Where
watch.Issue.Parent decode (flex: UUID string or expanded object) watch/client.go, watch/planeapi.go
The plan of record: Scope checklist parse (tags/entities/three citation shapes) planfresh/scope.go
The deterministic 4-class diff (D1–D4) planfresh/diff.go
One pass + receipts: epic comment, one queued dispatch issue on drift planfresh/pass.go, planfresh/comment.go
The ticket.done boundary arm (own watermark, read-only Plane client) cmd/dispatcher/boundary.go
Config: arms.boundary, boundary: block, templates.plan-drift config/config.go, config/load.go, config/example.go
Registry: R-25 registered, paused routines/registry.yaml

The asymmetry is the cost brake. D1 (planned-but-unticketed) and D2 (unplanned child) are identifier mismatches: they raise the drift verdict and are the only classes that create work. D3 (stale tick) and D4 (uncited scope item) are advisory lines in the same epic comment — prose can never spend an LLM. A pass with only advisories is a visible success-noop.

Receipts (R-25 design): a no-change pass returns a visible success-noop carrying the epic-comment ref, and the comment opens with no change. A pass that could not look returns an empty receipt → missed, never success-noop (P-ROUT-004); the read failure is folded into the receipt rather than returned as an error precisely so the runner records missed and not failed. A pass whose disposition failed returns an error (failed) and keeps the comment ref as evidence.

R-25 ships paused with pausedReason: dep:routine-executor. Honest reason: running an event-scheduled routine needs a trigger threaded through routines.Runner.Run, a live ExecutionPlane and an Executor; none are wired in this build. The arm still proves the trigger exists — the default sink resolves the registry, names the claimant, logs its paused reason, and says in as many words that NO RUN WAS STARTED. An active claim with no executor is a WARN, never a silent success. The mechanism itself (planfresh.Pass) is complete and fixture-tested; only the runner seam is missing.

Stop condition — answered, not engineered around

The per-epic child scan does not blow past a project-scan-per-poll design, so no per-epic watermark is needed: the arm scans the project, once per interval, shared by every epic. pages_steady = 1 + ceil(overlap / span-per-page) (proved by TestBoundaryArmSteadyStateReadsTheOverlapOnly: 3 pages cold, 2 for one new boundary), so on the brief's measured project (1497 issues / 15 pages) it is 15 requests cold, 1–2 in steady state, no model call. The only LLM spend in the feature is the architect lane, downstream of a drift finding.

Shared-file hunks (siblings: please skim)

  • cmd/dispatcher/app.go — the boundary arm wiring + newBoundaryArm + loadRoutinesRegistry. No other arm touched.
  • config/config.go / load.go / example.go — only Arms.Boundary, the Boundary block, TemplateKindPlanDrift, and their validation.
  • watch/client.go + watch/planeapi.go — one additive field each.
  • routines/registry.yaml — R-25 only.
  • Test adjustments made growth-tolerant on purpose (R-24 or any other 4.x registrant needs no further edit): the census count is a floor plus a named R-25 shape assertion (routines/registry_test.go); an entry outside R-01…R-22 must name its owning ticket (routines/parity_test.go); routines -check's expected total is derived from the registry (cmd/routines/main_test.go).

Not touched: direction/, intake/, notifier/, persona/, director/, comms/, or any other routines/ internals.

Verification

  • go vet ./... clean.
  • go test -race -count=1 -p 1 ./... green (23/23 packages). -p 1 only because the VM's disk is at 100% — an unconstrained parallel run fails to link (no space left on device), which is environmental, not a test failure.
  • go run ./cmd/routines -check -registry ./routines/registry.yaml → exit 0, 23 routine(s), 3 active, R-25 paused on dep:routine-executor.
  • No test opens a socket — planfresh and the boundary arm are fixture-driven (watch.FixtureClient); the only net/http in a test file is the pre-existing in-process roundTripFunc in watch/planeapi_test.go.
  • Coverage: planfresh 92.9% of statements.

Deferred (named, not silently dropped)

  1. The runner seam: routines.Runner with a Trigger, a live ExecutionPlane, an Executor bound to planfresh.Pass, and the plan-drift branch in laneForIssue. Until then R-25 stays paused and arms.boundary stays off.
  2. Durable boundary watermark (a mounted watch.FileWatermarkStore), which is what makes coldStartPolicy: fail viable; both observers share this follow-up.
  3. A prod manifest mount of the routines registry for the dispatcher pod (the arm reads it best-effort today).
  4. emitsWork on R-25 must be set when the executor lands, with an edge to whichever routine consumes plan-drift items — inventing an edge to R-12 (a calendar sweep) today would be a fiction.
Implements **LD4.9 · plan-freshness re-derivation** per `.rpiv/artifacts/epic-ld4/design-LD4.9-plan-freshness.md`, under the orchestrator rulings: - **boundary scope** = every epic in the homelab project (any ticket that reaches a terminal state group *and* has a parent); - **plan of record** = the epic description's `<h2>Scope</h2>` checklist; unmappable prose → **advisory only**; - **event-driven** — no cadence/quiet-week pressure anywhere in the diff; - **drift → a queued dispatch issue**, never an immediate spawn. ## Built | Piece | Where | |---|---| | `watch.Issue.Parent` decode (flex: UUID string or expanded object) | `watch/client.go`, `watch/planeapi.go` | | The plan of record: Scope checklist parse (tags/entities/three citation shapes) | `planfresh/scope.go` | | The deterministic 4-class diff (D1–D4) | `planfresh/diff.go` | | One pass + receipts: epic comment, one queued dispatch issue on drift | `planfresh/pass.go`, `planfresh/comment.go` | | The `ticket.done` boundary arm (own watermark, read-only Plane client) | `cmd/dispatcher/boundary.go` | | Config: `arms.boundary`, `boundary:` block, `templates.plan-drift` | `config/config.go`, `config/load.go`, `config/example.go` | | Registry: R-25 registered, `paused` | `routines/registry.yaml` | **The asymmetry is the cost brake.** D1 (planned-but-unticketed) and D2 (unplanned child) are *identifier* mismatches: they raise the drift verdict and are the only classes that create work. D3 (stale tick) and D4 (uncited scope item) are advisory lines in the same epic comment — prose can never spend an LLM. A pass with only advisories is a visible `success-noop`. **Receipts** (R-25 design): a no-change pass returns a visible `success-noop` carrying the epic-comment ref, and the comment *opens* with `no change`. A pass that **could not look** returns an **empty** receipt → `missed`, never `success-noop` (P-ROUT-004); the read failure is folded into the receipt rather than returned as an error precisely so the runner records `missed` and not `failed`. A pass whose *disposition* failed returns an error (`failed`) and keeps the comment ref as evidence. **R-25 ships `paused`** with `pausedReason: dep:routine-executor`. Honest reason: running an event-scheduled routine needs a trigger threaded through `routines.Runner.Run`, a live `ExecutionPlane` and an `Executor`; none are wired in this build. The arm still proves the trigger exists — the default sink resolves the registry, names the claimant, logs its paused reason, and says in as many words that **NO RUN WAS STARTED**. An *active* claim with no executor is a WARN, never a silent success. The mechanism itself (`planfresh.Pass`) is complete and fixture-tested; only the runner seam is missing. ## Stop condition — answered, not engineered around The per-epic child scan does **not** blow past a project-scan-per-poll design, so no per-epic watermark is needed: the arm scans the *project*, once per interval, shared by every epic. `pages_steady = 1 + ceil(overlap / span-per-page)` (proved by `TestBoundaryArmSteadyStateReadsTheOverlapOnly`: 3 pages cold, 2 for one new boundary), so on the brief's measured project (1497 issues / 15 pages) it is **15 requests cold, 1–2 in steady state, no model call**. The only LLM spend in the feature is the architect lane, downstream of a drift finding. ## Shared-file hunks (siblings: please skim) - `cmd/dispatcher/app.go` — the boundary arm wiring + `newBoundaryArm` + `loadRoutinesRegistry`. No other arm touched. - `config/config.go` / `load.go` / `example.go` — only `Arms.Boundary`, the `Boundary` block, `TemplateKindPlanDrift`, and their validation. - `watch/client.go` + `watch/planeapi.go` — one additive field each. - `routines/registry.yaml` — R-25 only. - **Test adjustments made growth-tolerant on purpose** (R-24 or any other 4.x registrant needs no further edit): the census count is a floor plus a named R-25 shape assertion (`routines/registry_test.go`); an entry outside R-01…R-22 must name its owning ticket (`routines/parity_test.go`); `routines -check`'s expected total is derived from the registry (`cmd/routines/main_test.go`). Not touched: `direction/`, `intake/`, `notifier/`, `persona/`, `director/`, `comms/`, or any other `routines/` internals. ## Verification - `go vet ./...` clean. - `go test -race -count=1 -p 1 ./...` **green** (23/23 packages). `-p 1` only because the VM's disk is at 100% — an unconstrained parallel run fails to *link* (`no space left on device`), which is environmental, not a test failure. - `go run ./cmd/routines -check -registry ./routines/registry.yaml` → exit 0, `23 routine(s), 3 active`, R-25 paused on `dep:routine-executor`. - **No test opens a socket** — `planfresh` and the boundary arm are fixture-driven (`watch.FixtureClient`); the only `net/http` in a test file is the pre-existing in-process `roundTripFunc` in `watch/planeapi_test.go`. - Coverage: `planfresh` 92.9% of statements. ## Deferred (named, not silently dropped) 1. The runner seam: `routines.Runner` with a `Trigger`, a live `ExecutionPlane`, an `Executor` bound to `planfresh.Pass`, and the `plan-drift` branch in `laneForIssue`. Until then R-25 stays paused and `arms.boundary` stays off. 2. Durable boundary watermark (a mounted `watch.FileWatermarkStore`), which is what makes `coldStartPolicy: fail` viable; both observers share this follow-up. 3. A prod manifest mount of the routines registry for the dispatcher pod (the arm reads it best-effort today). 4. `emitsWork` on R-25 must be set when the executor lands, with an edge to whichever routine consumes `plan-drift` items — inventing an edge to R-12 (a calendar sweep) today would be a fiction.
planeIssue gains a flex `parent` field (UUID string or expanded object) and
Issue.Parent carries it, so a caller that needs an issue's CHILDREN can scan
pages and match client-side. That is the only correct read: the self-hosted
proxy ignores a `?parent=` query filter (verified live 2026-10-02: the filtered
page is the unfiltered newest-first page and `count` is the page length, not a
total). Fixture and both wire shapes (list + detail) are covered.
The LD4.9 mechanism as a pure, fixture-testable package:

  scope.go  the plan of record: the epic description's <h2>Scope</h2>
            checklist, parsed and normalised (entities, tags, three citation
            shapes; a citation key must be >=3 chars so `R-25` cannot become
            a manufactured finding)
  diff.go   the 4-class diff. D1 (planned, unticketed) and D2 (unplanned
            child) are identifier mismatches -> drift. D3 (stale tick) and D4
            (uncited scope item) are advisory lines only. That asymmetry is
            the cost brake: prose can never dispatch the architect lane.
  pass.go   one pass over one epic: read, diff, record on the epic, and on
            drift create exactly ONE queued dispatch issue (Todo + label
            plan-drift + `ld:plan-drift` marker and JSON payload). R-25 never
            drafts a ticket body.

Receipts follow the R-25 design: a no-change pass returns a VISIBLE
success-noop carrying the epic comment ref (the comment itself opens with
"no change"); a pass that could not LOOK returns an EMPTY receipt which is
`missed`, never success-noop. A failed disposition returns an error so the
runner records `failed`, keeping the epic comment ref as evidence.

Child enumeration is modelled as `Children(ctx, epicID)` rather than a query
because the self-hosted proxy ignores `?parent=` (see the watch commit).

92.9% statement coverage; every test is fixture-driven and none opens a socket.
A second, read-only Plane observer with its OWN watermark (never the watch's:
one watermark answering two questions is why the arm exists separately). It
publishes one `ticket.done` event per issue that reached a terminal state group
AND has a parent — a parentless Done ticket has no epic whose plan could go
stale, so it produces nothing.

It mirrors the watch's discipline verbatim because that discipline is already
reviewed: order_by=-updated_at with a stop-below-the-watermark cutoff, an
overlap rescan window, (ticketID, revisionAt) dedup with retention pruning,
publish-before-save, a loud refusal on a truncated page, a page-budget error,
and the SAME cold-start refusal type (no policy is a decision, not a guess).
Every failure is logged and retried, never fatal; a cold-start refusal is
returned so the deployment crashloops visibly.

The default sink resolves the event against the routines registry. R-25 ships
paused (dep:routine-executor: the runner's event-trigger seam, the live
ExecutionPlane and an Executor are not wired), so the sink logs the boundary,
names the claimant and its paused reason, and says in as many words that NO RUN
WAS STARTED. An active claim with no executor is a WARN, not a silent success.

Fixture-driven throughout (watch.FixtureClient): first-pass emission for
completed and cancelled children, multi-page walking, transition-not-state
dedup, adopt-baseline and rescan, the page-budget refusal, and the
publish-before-save recovery after a mid-scan failure.
The trigger source, the disposition lane and the registry record:

  * `arms.boundary` (OFF by default) plus a `boundary:` block — interval,
    coldStartPolicy, pageSize/maxPages, overlap, emittedRetention. The
    cold-start policy has NO default and is REQUIRED when the arm is on: a
    scan that finds no watermark must be told whether the downtime was
    absorbed or lost, and the loader will not choose that for the operator
    (the watch's rule, restated for the second observer).
  * `templates.plan-drift` — the LD4.9 item kind, alongside LD4.1's two. It
    answers "which lane serves this item kind", not "how risky is this
    ticket", so it is a template key and not a class letter.
  * R-25 registers in the committed registry as `onEvent: ticket.done` +
    `scope: epic`, `cadencePeriod: week`, outputContract planeComment, status
    PAUSED with `pausedReason: dep:routine-executor` — the honest reason: the
    event-trigger seam through routines.Runner, a live ExecutionPlane and an
    Executor are not wired. `emitsWork` is deliberately unset (the entry is
    paused, so it creates nothing); when the executor lands, the drift dispatch
    issue must be declared there as an edge to whichever routine consumes
    `plan-drift` items — inventing an edge to R-12 (a calendar sweep) would be a
    fiction.
  * app.go wires the arm as a NON-FATAL runner and builds its OWN in-process
    watermark store (never the watch's). It reads the routines registry from
    ROUTINES_REGISTRY_PATH best-effort: a missing mount is a WARN and the sink
    logs `registry_loaded=false`, so "no mount" is never confused with "no
    routine wants this event".

Three sibling-facing test adjustments, all growth-tolerant so the other 4.x
registrants (R-24 next) do not have to touch them again: the registry census
count is now a floor plus a named R-25 shape assertion; the corpus-parity rule
now requires an entry outside R-01..R-22 to name its owning ticket; and
`routines -check`'s expected total is derived from the registry instead of
hardcoded.

`routines -check` is green (23 routines, 3 active, R-25 paused on
dep:routine-executor).
HOMELAB-1729: test(dispatcher): bound the boundary scan's steady-state cost
All checks were successful
0/0 projects applied successfully.
CI Review / receiver-test (pull_request) Successful in 6s
CI Review / helm-validate (pull_request) Successful in 9s
CI Review / alert-receiver-test (pull_request) Successful in 21s
CI Review / dispatcher-test (pull_request) Successful in 2m25s
968fe73348
Answers the stop condition with arithmetic rather than prose: the arm scans the
PROJECT, not the epics, so a per-epic watermark design is not needed. Steady
state is pages = 1 + ceil(overlap / span-per-page): the fixture (3 pages cold,
one hour between issues) shows 2 pages for one new boundary versus 3 for a cold
scan, and a cold start (or the `rescan` policy) is the only case that pays the
full page count. On the brief's measured project (1497 issues / 15 pages) that
is 15 requests cold and 1-2 in steady state, with no model call either way.
Author
Owner

Landing review — verdict: pass (merge)

Reviewed head 968fe7334852f18272bb84358bd5b9c525a4240b (base epic/LD4 @ 1519dd2, unmoved). Fresh context; primary sources only: the diff (23 files), the ticket's Verify lines, the design brief (design-LD4.9-plan-freshness.md), and the HOMELAB-1729 orchestrator adjudication.

CI

forgejo_ci_status Homelab/infra-core #575 → run #1689 @ 968fe73348: pass (alert-receiver-test, dispatcher-test 2m25s, helm-validate, receiver-test). The job runs go vet ./... && go test -race ./... in a fresh golang:1.24 container with no -p 1 — so the PR body's -p 1 / disk-at-100% note is confirmed environmental: CI is not affected (it ran the unconstrained parallel race suite green). Race is enforced by CI, not only locally.

Verify lines — real tests, behaviour not implementation

  1. Planted gap → TestPassCatchesPlantedGapAtABoundary (with TestDiffPlantedGap at the pure layer): the epic comment goes to the epic issue, body asserts drift/D1/HOMELAB-1799/epic revision; exactly one dispatch issue, state Todo, labels lab-director,plan-drift, ld:plan-drift marker whose <pre> payload round-trips (ParseDispatchDescription) and names the D1 finding + the comment ref; plus an explicit negative assertion that the pass wrote nothing else (1 comment, 1 issue — R-25 drafts no ticket body).
  2. No-change → TestPassNoChangeIsVisible: the comment opens with <p>no change — (structural prefix assert, not a substring), receipt success-noop + Noop, refs = exactly the comment, 0 dispatch issues. The anti-confusion half is real too: TestPassReadFailureIsMissed proves a pass that could not look returns an empty receipt → missed, never success-noop — checked against routines.classify (err→failed, Empty()→missed, Noop→success-noop), which matches the three failure shapes documented in pass.go.

Other review dimensions

  • The perf claim is asserted, not asserted-in-prose. TestBoundaryArmSteadyStateReadsTheOverlapOnly counts actual calls: planes.Calls("Issues") delta == 2 in steady state vs 3 pages cold, plus a second.Pages >= first.Pages fail-fast. The arithmetic and pages_steady = 1 + ceil(overlap/span-per-page) hold; 15 pages on the measured project follows from pageSize 100 / 1497 issues.
  • D3/D4 advisory stays quiet: TestPassAdvisoryOnlyPassIsNoopButRecorded (noop, 0 dispatches, D3+D4 lines in the same comment) and TestDiffAdvisoryOnlyIsNotDrift. TestDiffTextMatchedProseIsNotD4 correctly keeps text-matching (which only suppresses D4) from manufacturing a citation.
  • No sockets: grep confirms no net/http/httptest/net.Dial in any new test; planfresh and the arm are fixture-driven (fixtureReader/fixtureSink, watch.FixtureClient). Reproduced locally: go test -race -run 'Boundary|DoneTriggers|ShippedRegistry' ./cmd/dispatcher/ and go test ./planfresh/ green; go vet on all five touched packages clean; coverage 92.9% reproduced exactly.
  • Plane write surface: the arm holds only the read-only watch.PlaneClient (5 read methods, no writes); the sole write seam is planfresh.Sink's two methods — epic comment + dispatch issue. Structurally true, not just claimed.
  • Deviations verified in the diff, and all ratified: R-25 ships paused (dep:routine-executor), emitsWork unset, no scriptRef pin (the orchestrator comment of 2026-10-02 records "R-25 registers as paused until the executor lands"); the mechanism lives in planfresh/ rather than routines/planfresh.go; laneForIssue/context_plane.go untouched (only the plan-drift template key added); the three growth-tolerant test edits are present as described. go run ./cmd/routines -check → exit 0, 23 routine(s), 3 active, R-25 paused — reproduced.
  • Watch decode: parent is a planeRef (pre-existing string-or-object unmarshaler), one field in client.go + one mapping in planeapi.go; the new test covers string, expanded-object, null and absent shapes — the absent case matters because a phantom parent would emit a boundary for a ticket with no epic.

Non-blocking concerns (may merge; no blocker findings)

  1. D2 is the false-positive surface, not D1. D1 is a citation that names nothing, but D2 fires solely on the absence of a citation. A prose-only Scope — exactly the live HOMELAB-1713 shape (intake (4.1), routines registry (4.4), fully uncited items) — yields one D2 per child → drift → one architect dispatch. The "prose can never spend an LLM" brake holds for D3/D4 but not for D2 against a plan that cites by section number. Design-ratified (§3 table, D2 = high) and dormant today (arm off, executor unwired), so not a blocker — but the first real run should be watched.
  2. PR body says "1–2 requests steady state"; the test pins 2. Fine as a bound, but the observed number is 2.
  3. The arm treats any set parent as an epic scope (no work-item-type check). This matches the ratified ruling verbatim ("terminal state group and has a parent"), and a non-epic parent degrades to no plan of record → visible no-change; noted only because the code comments say "inside an epic".
  4. DefaultBoundaryWatermarkPath is declared and intentionally unused (documented); TemplateKindOf's doc comment still says "LD4.1 item-kind TEMPLATE keys" after growing to three keys — one stale word.
  5. The example config uses coldStartPolicy: adopt-baseline because the store is in-process, so a dispatcher restart silently absorbs Done boundaries. Documented as the durable-store follow-up; fine while the arm ships off.

Incidental tooling finding (not this PR)

forgejo_comment_on_pr with repo_ref: aaron/infra-core reported success and wrote nothing: the owner path 301-redirects to Homelab/infra-core, and node fetch follows a 301 by rewriting POST→GET, dropping the body. The canonical Homelab/infra-core ref works (this comment). Worth a guard in the extension: never treat a 3xx as a successful write, or normalize the owner.

No blocker findings open. Merging.

## Landing review — verdict: pass (merge) Reviewed head `968fe7334852f18272bb84358bd5b9c525a4240b` (base `epic/LD4` @ `1519dd2`, unmoved). Fresh context; primary sources only: the diff (23 files), the ticket's Verify lines, the design brief (`design-LD4.9-plan-freshness.md`), and the HOMELAB-1729 orchestrator adjudication. ### CI `forgejo_ci_status Homelab/infra-core #575` → run #1689 @ `968fe73348`: **pass** (alert-receiver-test, dispatcher-test 2m25s, helm-validate, receiver-test). The job runs `go vet ./... && go test -race ./...` in a fresh `golang:1.24` container with **no `-p 1`** — so the PR body's `-p 1` / disk-at-100% note is confirmed environmental: **CI is not affected** (it ran the unconstrained parallel race suite green). Race is enforced by CI, not only locally. ### Verify lines — real tests, behaviour not implementation 1. **Planted gap** → `TestPassCatchesPlantedGapAtABoundary` (with `TestDiffPlantedGap` at the pure layer): the epic comment goes to the **epic** issue, body asserts `drift`/`D1`/`HOMELAB-1799`/epic revision; **exactly one** dispatch issue, state `Todo`, labels `lab-director,plan-drift`, `ld:plan-drift` marker whose `<pre>` payload round-trips (`ParseDispatchDescription`) and names the D1 finding + the comment ref; plus an explicit negative assertion that the pass wrote *nothing else* (1 comment, 1 issue — R-25 drafts no ticket body). 2. **No-change** → `TestPassNoChangeIsVisible`: the comment **opens** with `<p>no change —` (structural prefix assert, not a substring), receipt `success-noop` + `Noop`, refs = exactly the comment, 0 dispatch issues. The anti-confusion half is real too: `TestPassReadFailureIsMissed` proves a pass that *could not look* returns an **empty** receipt → `missed`, never `success-noop` — checked against `routines.classify` (`err→failed`, `Empty()→missed`, `Noop→success-noop`), which matches the three failure shapes documented in `pass.go`. ### Other review dimensions - **The perf claim is asserted, not asserted-in-prose.** `TestBoundaryArmSteadyStateReadsTheOverlapOnly` counts actual calls: `planes.Calls("Issues")` delta == **2** in steady state vs 3 pages cold, plus a `second.Pages >= first.Pages` fail-fast. The arithmetic and `pages_steady = 1 + ceil(overlap/span-per-page)` hold; 15 pages on the measured project follows from pageSize 100 / 1497 issues. - **D3/D4 advisory stays quiet:** `TestPassAdvisoryOnlyPassIsNoopButRecorded` (noop, 0 dispatches, D3+D4 lines in the *same* comment) and `TestDiffAdvisoryOnlyIsNotDrift`. `TestDiffTextMatchedProseIsNotD4` correctly keeps text-matching (which only suppresses D4) from manufacturing a citation. - **No sockets:** grep confirms no `net/http`/`httptest`/`net.Dial` in any new test; `planfresh` and the arm are fixture-driven (`fixtureReader`/`fixtureSink`, `watch.FixtureClient`). Reproduced locally: `go test -race -run 'Boundary|DoneTriggers|ShippedRegistry' ./cmd/dispatcher/` and `go test ./planfresh/` green; `go vet` on all five touched packages clean; coverage **92.9%** reproduced exactly. - **Plane write surface:** the arm holds only the read-only `watch.PlaneClient` (5 read methods, no writes); the sole write seam is `planfresh.Sink`'s two methods — epic comment + dispatch issue. Structurally true, not just claimed. - **Deviations verified in the diff, and all ratified:** R-25 ships `paused` (`dep:routine-executor`), `emitsWork` unset, no `scriptRef` pin (the orchestrator comment of 2026-10-02 records "R-25 registers as paused until the executor lands"); the mechanism lives in `planfresh/` rather than `routines/planfresh.go`; `laneForIssue`/`context_plane.go` untouched (only the `plan-drift` template key added); the three growth-tolerant test edits are present as described. `go run ./cmd/routines -check` → exit 0, `23 routine(s), 3 active`, R-25 paused — reproduced. - **Watch decode:** `parent` is a `planeRef` (pre-existing string-or-object unmarshaler), one field in `client.go` + one mapping in `planeapi.go`; the new test covers string, expanded-object, `null` and **absent** shapes — the absent case matters because a phantom parent would emit a boundary for a ticket with no epic. ### Non-blocking concerns (may merge; no blocker findings) 1. **D2 is the false-positive surface, not D1.** D1 is a citation that names nothing, but D2 fires solely on the *absence* of a citation. A prose-only Scope — exactly the live HOMELAB-1713 shape (`intake (4.1)`, `routines registry (4.4)`, fully uncited items) — yields one D2 per child → drift → one architect dispatch. The "prose can never spend an LLM" brake holds for D3/D4 but not for D2 against a plan that cites by section number. Design-ratified (§3 table, D2 = high) and dormant today (arm off, executor unwired), so not a blocker — but the first real run should be watched. 2. PR body says "1–2 requests steady state"; the test pins **2**. Fine as a bound, but the observed number is 2. 3. The arm treats *any* set `parent` as an epic scope (no work-item-type check). This matches the ratified ruling verbatim ("terminal state group *and* has a parent"), and a non-epic parent degrades to `no plan of record` → visible no-change; noted only because the code comments say "inside an epic". 4. `DefaultBoundaryWatermarkPath` is declared and intentionally unused (documented); `TemplateKindOf`'s doc comment still says "LD4.1 item-kind TEMPLATE keys" after growing to three keys — one stale word. 5. The example config uses `coldStartPolicy: adopt-baseline` because the store is in-process, so a dispatcher restart silently absorbs Done boundaries. Documented as the durable-store follow-up; fine while the arm ships off. ### Incidental tooling finding (not this PR) `forgejo_comment_on_pr` with `repo_ref: aaron/infra-core` reported success and wrote **nothing**: the owner path 301-redirects to `Homelab/infra-core`, and node `fetch` follows a 301 by rewriting POST→GET, dropping the body. The canonical `Homelab/infra-core` ref works (this comment). Worth a guard in the extension: never treat a 3xx as a successful write, or normalize the owner. No blocker findings open. Merging.
aaron merged commit 118d250032 into epic/LD4 2026-10-02 06:48:43 +00:00
Sign in to join this conversation.
No reviewers
No labels
operator-review
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Homelab/infra-core!575
No description provided.