Plan K: Canopy xtask + Hook Backports

On this page

Status

Step Description Status

1

Plan filing — body lands in the docs-only Plan D refresh MR alongside Plans D/G/H/I/J. nav.adoc + CHANGELOG. No code changes.

Done (2026-05-15 via !307)

2

F-040 + F-041 (combined — both compose-level): port canopy/xtask/src/docker.rs::acquire_lock (~30 lines) → CRAIG’s xtask/src/docker.rs. Exclusive file lock at $TMPDIR/craig-xtask.lock; polls 60s with 1s interval before timing out. Wire into compose-lifecycle xtask commands. ALSO add --remove-orphans flag to every docker::compose(["up", …​]) and compose(["down", …​]) call in xtask/src/devstack.rs + xtask/src/cmd/dev.rs (6 call sites total). Lock placement adapted from plan-text (bring_up) to canopy’s actual pattern (cmd-level in cmd/dev.rs::run) — covers Start/Stop/Reload/Restart/Clean/Reseed/RestartService/MultibackendUp/MultibackendDown variants; Status/Logs/{Kanidm,Zitadel}Bootstrap stay unlocked (read-only or IdP-HTTP-API-only).

Done (2026-05-15)

3

F-042 implementation: port reconcile_ports_env from canopy/xtask/src/docker.rs:412-495. Reads .ports.env, queries docker compose port for each service, rewrites .ports.env if drift detected. Call from xtask’s pre-start path + before any port-dependent commands (test, e2e, validate).

Scope substantiated 2026-05-25 (ADR-030 §3): Plan body’s "~30 lines" undercounted — actual port is ~500 LOC because canopy’s reconcile_ports_env depends on a chain of helpers CRAIG didn’t have (read_ports_env_file, read_recorded_ports, detect_port_drift, PortDrift, ReconcileOutcome, ReconcileResult, is_running, build_env_for_ports, compose_cmd_with_env) AND because canopy’s safer per-subprocess env pattern was migrated AWAY from process-env-mutation — CRAIG still had the old export_port_env_vars + load_and_export_ports_env antipattern via unsafe env::set_var, exactly what canopy’s doc-comment at line 427-437 flagged as causing the compose-replace drift cascade. This MR adopts canopy’s pattern wholesale: all subprocess env passed explicitly via Command::envs(…​) from the ReconcileResult.env vec. KEYCLOAK_REALM_AFFECTING_SERVICES is intentionally empty for CRAIG (realm uses wildcard http://localhost:*/http://host.docker.internal:* redirectUris so no host port is baked at import time) — surface kept aligned with canopy in case CRAIG ever tightens its realm.

Done (2026-05-25) — Shipped. +501/-134 in xtask/src/docker.rs (helpers + types + new tests); +30/-11 cmd/test.rs; +37/-5 cmd/e2e.rs; +59/-7 cmd/validate.rs; +24/-9 dsn.rs; +9/-2 devstack.rs; +2/-2 cmd/security.rs. Removed export_port_env_vars + load_and_export_ports_env (unsafe-env-mutation antipattern). Hardened get_host_port to fall back to .ports.env between process env and docker discovery. 6 new unit tests (detect_port_drift_* × 5, build_env_for_ports_* × 1); 241/241 xtask tests pass; workspace cargo clippy --workspace --all-targets — -D warnings clean. auto_heal_keycloak_realm (canopy:513) intentionally NOT ported — dormant-by-construction in CRAIG’s wildcard-realm posture. Closes #436.

4

F-043 implementation: cargo xtask dev refresh command. Mirrors canopy’s Action::Refresh (devstack_guard.rs:544-644). Reads SHA-256 staleness markers (source / deps / Dockerfile / compose / migrations / rulesets / seed); skips rebuild if unchanged, conditionally down only if compose drifted, reuses .ports.env. Additive change to CRAIG’s existing devstack_guard.rs (19KB → ~28KB; mirror canopy’s structure).

Done (2026-05-25) — Shipped. +159 LOC in devstack_guard.rs (auto_refresh + reconcile_ports_env_and_heal_drift helper); +23 LOC in cmd/dev.rs (Refresh variant + dispatch arm with lock acquisition via F-040); +4 LOC in devstack.rs (promoted init_garage + wait_for_health to pub(crate)); +24 LOC in .claude/docs/local-dev.md (13-row "When to use reload vs restart vs refresh`" decision table). Build + test + clippy + fmt clean (241/241 xtask tests). `cargo xtask dev refresh --help works.

Scope notes (ADR-030 §3): SOPS branch (canopy’s decrypt_secrets_to_env_pairs call at devstack_guard.rs:640) intentionally skipped — CRAIG has no SOPS-equivalent and manages secrets via .env + env-file mounts which compose loads itself. Rationale comment in-place at the call site. CRAIG-specific reseed escalation: when seed == Reseed && volume != Wipe, calls devstack::reseed() (tear_down keeping images + bring_up) because CRAIG’s seed runs as a one-shot craig-seed container — heavier than canopy’s standalone cargo xtask seed path but the cheapest valid path given CRAIG’s seed architecture. Documented inline. Closes #437.

5

F-044 implementation: cargo xtask coverage command. Port canopy/xtask/src/cmd/coverage.rs (~75 lines). Wraps cargo llvm-cov --workspace, enforces a configurable line-coverage floor. Initial threshold: measure-and-record (no gate); promote to hard gate (--fail-under-lines) after baseline established and a quarter of stable coverage. Add cargo-llvm-cov to CI image.

Done (2026-05-25) — Shipped. xtask/src/cmd/coverage.rs NEW (96 LOC verbatim port w/ flag semantics flipped per cross-cutting invariant #3 — default is measure-only, --threshold N opts into gate, --baseline writes .coverage-baseline.json). xtask/src/cmd/mod.rs + xtask/src/main.rs register Coverage subcommand. docs/modules/ROOT/pages/test-coverage-scorecard.adoc gets a new "Line coverage (Plan K F-044)" section with the soft-start contract + promotion checklist + empty per-crate baseline table awaiting first run. .gitlab-ci.yml install deferred to the promotion MR (when the gate is actually wired into CI); local prereq documented as cargo install cargo-llvm-cov --locked in the scorecard. Closes #438.

6

F-045 implementation: cargo xtask migrate snapshot|rollback command. Port canopy/xtask/src/cmd/migrate.rs (~220 lines). snapshot runs pg_dump -F c on each CRAIG service DB into .devstack/snapshots/<timestamp>/; rollback runs pg_restore --clean --if-exists. Manifest captures git HEAD SHA + _sqlx_migrations rows per DB. Add --db flag for per-database granularity.

Done (2026-05-25) — Shipped. xtask/src/cmd/migrate.rs NEW (798 LOC; canopy was 593 — +205 LOC concentrated in CRAIG-side schema-version-aware additions per cross-cutting invariant #4). Three sub-commands: snapshot (all 7 stateful DBs at once — manifest semantics keep the snapshot workspace-coherent; selective-snapshot omitted as low-value), rollback --db <name> (per-DB granularity with --force override), list. CRAIG-side beyond canopy: schema-version-ahead bail — manifest stores _sqlx_migrations head per DB; rollback bails with clear error when live > snapshot’s recorded head unless --force. pg_dump/pg_restore/psql invoked via docker exec against the shared postgres container — no host pg-binary dependency. The 7 CRAIG stateful DBs (craig_rules/cases/placement/exchange/financial/reporting/security) settled per devstack/postgres/init.sql:4-12 minus craig_intake (stateless edge per ADR-017; asserted by database_list_excludes_intake test). 14 new unit tests (filter logic × 5, db-list × 2, schema-ahead × 4, manifest + container × 3); xtask test count 241 → 255. F-040 lock acquired at dispatch level; F-042 reconcile not called (migrate only docker exec`s, never `compose up`s, so port-env reconciliation isn’t load-bearing). `.devstack/snapshots/ added to .gitignore for grep-discoverability (already covered by .devstack/ parent). New "Local DB snapshot/rollback (Plan K F-045)" section in .claude/docs/local-dev.md documents the workflow. Closes #439.

7

F-046 implementation: plan-lifecycle doctrine. New CRAIG ADR-030 mirroring canopy ADR-013 — canonical Status vocabulary (Not started / In progress / Done (YYYY-MM-DD) / Deferred (…) / Blocked (…) / N/A), plan archival rule, pre-commit Q4 reframe ("update Design/Scope, not errata"), pre-commit Q6 reframe ("file GitLab issue, not 'Potential Improvements' section"). Updates .githooks/pre-commit Q4 + Q6 wording to match. No enforcement tooling yet — that’s Step 8.

Done (2026-05-16)

8

F-047 implementation: cargo xtask docs plan-lint. Validates every file under docs/modules/ROOT/pages/plans/ (excluding archive/) — every Status-table row’s Status cell starts with one of the canonical tokens from F-046’s ADR. File-level report lists violations with plan filename + row description. Wire into cargo xtask validate with allow_failure: true at first (grandfathers existing drift); promote to blocking in a follow-up.

Done (2026-05-16)

9

F-048 implementation: cargo xtask docs plan-archive. Developer-invoked one-shot — scans plans for all-Done/N/A Status tables and git mv`s them into `docs/modules/ROOT/pages/plans/archive/. Not run in CI.

Done (2026-05-16)

10

F-049 implementation: cargo xtask e2e --no-refresh flag. Mirrors canopy’s pre-push usage — prevents ensure_ready from wiping just-loaded seed if markers drift during a long local session. Update .githooks/pre-push to use the flag after cargo xtask seed (or dev reseed).

Done (2026-05-25) — Shipped. xtask/src/cmd/e2e.rs: new --no-refresh flag on E2eArgs; when set, the function (a) bails with a clear error if devstack is down rather than auto-bootstrapping (caller is responsible for a healthy stack with current seed) and (b) skips the seed-manifest regeneration block (lines 67-87 pre-port) so cargo xtask dev reseed’s freshly-loaded data isn’t overwritten by a re-generated manifest with different env values. `.githooks/pre-push:24 updated to invoke cargo xtask e2e --no-refresh after the existing cargo xtask dev reseed step. CRAIG-side adaptation: canopy’s --no-refresh skips a call to ensure_ready; CRAIG’s e2e has no ensure_ready call (only an is_healthy short-circuit and a seed-manifest regeneration), so the flag targets the manifest regeneration which is the actual fixture-clobbering risk. Closes #457.

11

Plan completion audit + archive.

Done (2026-05-25) — Audit verified all 10 prior steps via the plan-completion-audit subagent per .claude/docs/delivery-protocol.md. Per-step file:line evidence cross-checked against git log + CHANGELOG.adoc; all Closes #N trailers verified. Plan body archived via cargo xtask docs plan-archive (the F-048 command Plan K Step 9 shipped — eats its own dogfood). nav.adoc Active entry removed; archive.adoc Code Quality section gains a new Plan K row; CLAUDE.md sibling-plans row updated to mark K complete + archived. Closes #440.

Epic: &33 (epic: Canopy xtask Backports (Plan K))
Issues: #435 (Step 2) · #436 (Step 3) · #437 (Step 4) · #438 (Step 5) · #439 (Step 6) · #454 (Step 7 — F-046 plan-lifecycle ADR + Q4/Q6) · #455 (Step 8 — F-047 plan-lint) · #456 (Step 9 — F-048 plan-archive) · #457 (Step 10 — F-049 e2e --no-refresh) · #440 (Step 11 — plan completion)
Branch prefix: feat/canopy-backport-
*Milestone
: TBD

Context

Canopy (sibling project, same author) has shipped six xtask patterns that solve real CRAIG pain points. Substantiated 2026-05-15 by side-by-side comparison of the two xtask trees:

  • F-040 (P0): Concurrent cargo xtask dev start invocations can race; CRAIG has no lock. Canopy’s acquire_lock (~30 lines) is a small, high-value port that prevents the race entirely.

  • F-041 (P0): CRAIG’s docker compose up invocations omit --remove-orphans. When old containers from prior runs still exist with prior-run port allocations, Docker Compose reuses them and binds to new ports — exactly the "port already allocated" leak symptom we’ve hit repeatedly (memory feedback_devstack_port_collision). Canopy passes --remove-orphans everywhere.

  • F-042 (P1): Canopy’s reconcile_ports_env (file:412-495) reads .ports.env, queries docker compose port for each service, rewrites the file when drift detected. CRAIG has no equivalent — operators using stale .ports.env get confusing failures (e.g. tests connect to a port from a previous devstack run that’s no longer bound).

  • F-043 (P1): Canopy has cargo xtask dev refresh (a third lifecycle command alongside reload and restart). It reads SHA-256 staleness markers + conditionally rebuilds only what changed. Reuses .ports.env when no rebuild needed — sidesteps the port-reassignment cascade.

  • F-044 (P2): Canopy enforces a workspace-wide line-coverage floor via cargo xtask coverage. CRAIG measures coverage in CI but doesn’t gate locally.

  • F-045 (P2): Canopy’s cargo xtask migrate snapshot/rollback lets developers safely test migrations locally; CRAIG has no equivalent.

  • F-046 (P1): Canopy ADR-013 (plan-lifecycle-and-status-vocabulary, Accepted 2026-04-23) defines a canonical Status vocabulary, plan archival rule, and reframes precommit Q4 + Q6 against the "plans are living specs, not backlogs/errata dumps" doctrine. CRAIG’s pre-commit Q4 still says "document why you deviated in an errata section" and Q6 still says "document it in the plan in a 'potential improvements' section" — both actively push the anti-patterns canopy ADR-013 enumerates. CRAIG’s ADR-013 slot is taken (web-session-strategy), so file as the next available number.

  • F-047 (P1): Without enforcement, F-046’s vocabulary will drift the same way canopy’s did pre-ADR (canopy measured 610 "Complete" / 191 "✓" / 85 "Done" / 45 "complete" / 0 "COMPLETE" across 95 plans before the lint landed). cargo xtask docs plan-lint is the enforcement; ships soft-fail at first to grandfather existing drift.

  • F-048 (P2): cargo xtask docs plan-archive is the operator-driven companion — scans for all-Done Status tables + git mv`s to `archive/. Today CRAIG plans get archived ad-hoc via manual git mv; canopy’s command makes it idempotent.

  • F-049 (P3): Canopy’s pre-push uses cargo xtask e2e --no-refresh; CRAIG’s xtask/src/cmd/e2e.rs has no such flag. The motivation per canopy’s pre-push header: "prevents ensure_ready from wiping the just-loaded seed if markers drift during a long local session." Small ergonomics fix.

Together these close CRAIG’s most-painful infra gap (port collision is a recurring frustration) + add two genuinely useful developer-ergonomics commands (coverage + migrate) + close the plan-lifecycle drift that the canopy ADR-013 retrospective catches before it becomes a 95-plan cleanup.

Cross-cutting invariants

  1. Verbatim port where possible. Canopy code is the reference; CRAIG adapts paths/project-names only. Each Step’s MR cites the canopy source file in the body with a one-line "ported from canopy:<path>:<lines>" attribution.

  2. Step 4 is additive, not replacement. CRAIG already has reload (full rebuild + restart) + restart (cold teardown + cold build). refresh slots in as the smartest-of-three. Document precedence in .claude/docs/local-dev.md as a decision table ("I changed X → run Y").

  3. Step 5 starts soft. cargo xtask coverage measures and prints; CI gate added only after baseline written to a tracked file (docs/modules/ROOT/pages/test-coverage-scorecard.adoc already exists; promote it to a contract). Avoids a flag-day "coverage CI broken" event.

  4. Step 6 schema-version-aware. Manifest captures SELECT version FROM _sqlx_migrations per DB at snapshot time; rollback warns + bails if current schema is ahead of snapshot’s schema (data restore against newer schema = silent corruption). Per-DB granularity via --db <name>.

  5. Lock TTL handling. If xtask crashes mid-operation the lock stays held until manual rm; canopy’s acquire_lock polls 60s before timing out with a clear error message. Port that retry+timeout logic verbatim; don’t roll your own.

  6. Doctrine-before-enforcement (Step 7 → Step 8). F-046’s ADR lands before F-047’s lint — the lint cites the ADR as its source of truth. Promote the lint to blocking only after the existing-drift cleanup MR has landed (separate follow-up).

  7. F-046 wording is verbatim from canopy ADR-013 §3. The Q4 + Q6 replacement text in canopy’s pre-commit hook (lines 83 + 85-86) is the authoritative form. CRAIG’s port adapts only the file:line example in the meta-protocol’s bullet (a) to a CRAIG path.

Scope

In scope (10 findings):

  • F-040 xtask exclusive lock

  • F-041 --remove-orphans in all compose calls

  • F-042 .ports.env reconciliation

  • F-043 cargo xtask dev refresh

  • F-044 cargo xtask coverage

  • F-045 cargo xtask migrate snapshot/rollback

  • F-046 plan-lifecycle doctrine (new ADR + pre-commit Q4/Q6 reframe)

  • F-047 cargo xtask docs plan-lint

  • F-048 cargo xtask docs plan-archive

  • F-049 cargo xtask e2e --no-refresh flag + pre-push wire-up

Out of scope:

  • Canopy’s secrets SOPS workflow — separate decision (CRAIG hasn’t picked a secrets management approach; defer)

  • Canopy’s validate-in-network — useful, but defer until F-042 lands (port reconciliation may obviate the need)

  • Pre-commit AI meta-protocol port — shipped 2026-05-15 via branch chore/precommit-ai-verification-protocol (verbatim from canopy .githooks/pre-commit:44-78). Lives in CRAIG’s .githooks/pre-commit today; out of scope here

Steps

Step 2: F-040 + F-041 (xtask lock + --remove-orphans)

Files:

  • xtask/src/docker.rs — add pub fn acquire_lock(project: &str) → Result<LockGuard> mirroring canopy/xtask/src/docker.rs:610-637. File lock at env::temp_dir().join(format!("{project}-xtask.lock")); try_lock_exclusive() with 60s polling timeout.

  • xtask/src/devstack.rs — call acquire_lock at the start of bring_up, restart, reload, reseed. The LockGuard drops at end-of-function, releasing the lock.

  • xtask/src/docker.rs::compose — extend signature to accept extra args via the existing args: &[&str] parameter; ensure call sites in xtask/src/devstack.rs (lines 282-293, 343-345, 159-160, 168-169) pass --remove-orphans on every up and down.

Branch: feat/canopy-backport-step2-xtask-lock-and-remove-orphans

MR title: feat(xtask): exclusive lock + --remove-orphans in all compose calls [Step 2 of canopy-backports]

Verification:

  1. Open two terminals; cargo xtask dev start in one, immediately run same in the other → second one polls + reports "another xtask process is running" rather than racing

  2. cargo xtask dev start --force twice in succession; no "port is already allocated" errors (was the recurring failure mode)

  3. cargo nextest run --workspace — no regression

Step 3: F-042 reconcile_ports_env drift detection

Files:

  • xtask/src/docker.rs — port reconcile_ports_env() from canopy:412-495. Logs each port drift with before/after values.

  • xtask/src/cmd/{test,e2e,validate}.rs — call reconcile_ports_env at the start of each command (before tests run that rely on .ports.env).

  • xtask/src/devstack.rs::ensure_ready — call after compose up to capture freshly-bound ports.

Branch: feat/canopy-backport-step3-port-reconciliation

MR title: feat(xtask): reconcile .ports.env against actual docker compose port bindings [Step 3 of canopy-backports]

Verification:

  1. Manually bind a port to something other than the expected service; run reconcile; see the drift logged + .ports.env rewritten

  2. Run cargo xtask dev start --force twice; .ports.env is consistent with docker compose port output both times

  3. cargo xtask test against a stale .ports.env triggers reconciliation transparently

Step 4: F-043 cargo xtask dev refresh

Files:

  • xtask/src/devstack_guard.rs — extend with SHA-256 staleness markers per category (source / deps / Dockerfile / compose / migrations / rulesets / seed). Mirror canopy:devstack_guard.rs:544-644.

  • xtask/src/cmd/dev.rs — add Refresh action to the DevCommand enum + match arm. Calls devstack_guard::auto_refresh().

  • .claude/docs/local-dev.md — new "When to use reload vs restart vs refresh" decision table.

Branch: feat/canopy-backport-step4-dev-refresh

MR title: feat(xtask): cargo xtask dev refresh — smart minimum rebuild [Step 4 of canopy-backports]

Verification:

  1. Edit a services//src/.rs file; run cargo xtask dev refresh → rebuilds + reloads only the affected service

  2. Edit docker-compose.yml; run refresh → re-runs full lifecycle (because compose changed)

  3. No edits since last dev start; run refresh → reports "already up to date"

Step 5: F-044 cargo xtask coverage

Files:

  • xtask/src/cmd/coverage.rs (NEW) — port canopy:cmd/coverage.rs. Wraps cargo llvm-cov --workspace --json; parses output; checks against floor.

  • xtask/src/cmd/mod.rs — register subcommand.

  • xtask/Cargo.toml — no new deps (cargo-llvm-cov is invoked as a subcommand, not a library dep)

  • .gitlab-ci.yml — add cargo install cargo-llvm-cov to the CI image build OR install pre-baked

  • docs/modules/ROOT/pages/test-coverage-scorecard.adoc — promote to contract: list per-crate baselines

Branch: feat/canopy-backport-step5-coverage

MR title: feat(xtask, ci): cargo xtask coverage — line-coverage measurement + (soft) gate [Step 5 of canopy-backports]

Verification:

  1. cargo xtask coverage produces per-crate line-coverage summary

  2. CI runs the command and reports results; soft-fails only if --fail-under-lines <pct> flag is set

  3. docs/…​/test-coverage-scorecard.adoc lists current per-crate baselines

Step 6: F-045 cargo xtask migrate snapshot|rollback

Files:

  • xtask/src/cmd/migrate.rs (NEW) — port canopy:cmd/migrate.rs. snapshot enumerates the 8 CRAIG service DBs (craig_rules, craig_cases, craig_placement, craig_exchange, craig_financial, craig_reporting, craig_security, craig_intake … wait, intake is stateless per ADR-017; verify the actual DB list at branch time). For each DB: pg_dump -F c -h localhost -p $CRAIG_PORT_POSTGRES_5432 -U craig <db> > .devstack/snapshots/<ts>/<db>.dump. Manifest captures git HEAD SHA + SELECT version FROM _sqlx_migrations per DB.

  • xtask/src/cmd/mod.rs — register subcommand.

  • .devstack/snapshots/ — added to .gitignore.

  • .claude/docs/local-dev.md — document the snapshot/rollback workflow.

Branch: feat/canopy-backport-step6-migrate

MR title: feat(xtask): cargo xtask migrate snapshot/rollback — DB lifecycle safety net [Step 6 of canopy-backports]

Verification:

  1. cargo xtask migrate snapshot creates .devstack/snapshots/<ts>/{<each-db>.dump, manifest.json}

  2. Modify schema (e.g. add a column via psql); cargo xtask migrate rollback --snapshot <ts> restores

  3. Schema-version-ahead detection: snapshot at version A, advance schema to B, attempt rollback to A → warns + bails

Step 7: F-046 plan-lifecycle doctrine (ADR + pre-commit wording)

Files:

  • docs/modules/ROOT/pages/adrs/adr-030-plan-lifecycle-and-status-vocabulary.adoc (NEW). CRAIG’s previous ADR slots: ADR-028 = Service Identity (Plan E), ADR-029 = Introspection-mode token validation (Plan F). Plan L’s ADR will be 031 if needed. Section structure (CRAIG convention): Status / Context / Decision / Consequences (4 sections per .claude/docs/coding-conventions.md:157). Canopy ADR-013 uses 5 (adds Enforcement); roll Enforcement into Decision §4 to fit CRAIG convention. Adapt canopy ADR-013 verbatim; substitute "canopy" → "CRAIG" + update example MR refs to CRAIG MRs.

  • Q4 + Q6 replacement wording (inlined verbatim from canopy .githooks/pre-commit:83,85-86):

    4. Have you deviated from the plan? If so: (a) UPDATE the plan's Design/Scope
       section to reflect what was built — plans are living specs, not immutable
       records; (b) file a GitLab issue for any follow-up work. Errata is for
       post-hoc corrections only (typos, citation errors), not a dumping ground
       for "I built it differently" (see ADR-030).
    
    6. Can this feature be improved in any way? If so, FILE A GITLAB ISSUE and
       link it. Plans are specifications, not backlogs — do NOT add ideas to a
       plan's "Potential Improvements" section (see ADR-030).

    Q1-Q3, Q5, Q7-Q8 are unchanged from the current craig pre-commit hook.

  • Canonical Status vocabulary (inlined verbatim — the ADR’s Decision §1):

    • Not started — default state for rows added to a new plan

    • In progress — step is being actively worked in an open MR

    • Done (YYYY-MM-DD) — … — step shipped; date + freeform detail required. Optional MR reference (MR !N)

    • Deferred (…) — step explicitly descoped; reason required in parentheses

    • Blocked (…) — step cannot proceed; blocker (external partner, spec pending, dependent plan) required

    • N/A — row added to the table for structural reasons but doesn’t apply

      Case-insensitive first-token match; whitespace-preserved. Anything else is a lint violation.

  • .githooks/pre-commit — apply the Q4 / Q6 wording above.

  • docs/modules/ROOT/nav.adoc — register ADR-030.

Branch: feat/canopy-backport-step7-plan-lifecycle-adr

MR title: feat(adr, .githooks): plan-lifecycle doctrine — canonical Status vocabulary + Q4/Q6 reframe [Step 7 of canopy-backports]

Verification:

  1. ADR renders cleanly (cargo xtask check-docs for Tier 2 docs)

  2. .githooks/pre-commit shows the new Q4/Q6 wording when the token-gate fires

  3. No retroactive plan migration this MR; F-047’s lint lands separately + soft-fails first

Step 8: F-047 cargo xtask docs plan-lint

Design note: this lint operates on AsciiDoc Status tables, NOT Rust source. syn::visit::Visit (used by Plan D F-024 silent-skip lint and Plan I F-054 struct-method-count lint) does NOT apply here. AsciiDoc table parsing approach:

  • Option A (regex; simpler): match the |=== block delimiters; per-row |\s*(\d+)\s*\|\s*(.?)\s*\|\s*(.?)\s*$ shape. Brittle if any row uses | literally inside a cell (escape with \| per AsciiDoc rules).

  • Option B (pulldown-cmark-style AsciiDoc parser; sturdier): use the asciidoctor Rust binding (if available) or a small hand-written parser. Higher-effort but handles cell escapes correctly.

Pick Option A initially (regex) since plan bodies don’t currently use escaped pipes. Promote to a parser-backed implementation if false-positives appear.

Files:

  • xtask/src/cmd/docs.rs (NEW or EDIT if exists) — port canopy’s plan-lint implementation. Iterates docs/modules/ROOT/pages/plans/*.adoc (excluding archive/); parses the Status table; validates each row’s Status cell begins with a canonical token from the F-046 ADR (case-insensitive first-token match).

  • xtask/src/cmd/validate.rs — wire into validate with allow_failure: true initial behavior (print violations but exit 0). Promote to blocking via a follow-up MR after existing-drift cleanup.

Branch: feat/canopy-backport-step8-plan-lint

MR title: feat(xtask): cargo xtask docs plan-lint — Status vocabulary enforcement (soft-fail) [Step 8 of canopy-backports]

Verification:

  1. cargo xtask docs plan-lint runs against the full plans tree

  2. Output cites every plan with a non-canonical Status cell (e.g. bare "Complete", "✓", lowercase "done")

  3. cargo xtask validate runs the lint but does not block on violations

  4. Follow-up issue filed for "promote plan-lint to blocking" after the cleanup MR

Step 9: F-048 cargo xtask docs plan-archive

Files:

  • xtask/src/cmd/docs.rs — add Action::Archive subcommand. Scans plans for all-Done/N/A Status tables; for each candidate, runs git mv <plan>.adoc archive/<plan>.adoc + prints the move. Operator-driven; not run in CI.

  • docs/modules/ROOT/pages/plans/archive/ — directory may already exist; verify before first move.

Branch: feat/canopy-backport-step9-plan-archive

MR title: feat(xtask): cargo xtask docs plan-archive — idempotent plan archival [Step 9 of canopy-backports]

Verification:

  1. cargo xtask docs plan-archive --dry-run lists candidates without moving

  2. cargo xtask docs plan-archive performs the moves; git status shows renames

  3. xref updates to archived plans tracked manually (the command does not rewrite xrefs)

Step 10: F-049 cargo xtask e2e --no-refresh flag

Files:

  • xtask/src/cmd/e2e.rs — add --no-refresh flag to the E2eArgs struct (clap). When set, ensure_ready skips its staleness-marker check + does not wipe-and-reseed.

  • xtask/src/devstack_guard.rs::ensure_ready — accept a skip_refresh: bool parameter; gate the marker-check branch on it.

  • .githooks/pre-push — after cargo xtask dev reseed, run cargo xtask e2e --no-refresh (so the reseed isn’t wiped). Update comment to mirror canopy’s pre-push header explaining the flag’s purpose.

Branch: feat/canopy-backport-step10-e2e-no-refresh

MR title: feat(xtask, .githooks): cargo xtask e2e --no-refresh + wire into pre-push [Step 10 of canopy-backports]

Verification:

  1. cargo xtask e2e --no-refresh against a freshly-reseeded devstack: Playwright sees the seed fixtures (no wipe)

  2. cargo xtask e2e (default behavior) still re-runs ensure_ready

  3. Pre-push end-to-end: git push triggers the hook → reseed runs → e2e runs against the reseeded data

Step 11: Plan completion audit + archive

Mirror Plan B Step 8 / Plan C Step 18 / Plan F Step 6 pattern.

Files Touched

File Step Change

xtask/src/docker.rs

2,3

EDIT (acquire_lock, --remove-orphans, reconcile_ports_env)

xtask/src/devstack.rs

2,4

EDIT (lock integration, refresh action)

xtask/src/devstack_guard.rs

4,10

EDIT (staleness markers; skip_refresh param)

xtask/src/cmd/dev.rs

4

EDIT (Refresh action)

xtask/src/cmd/coverage.rs

5

NEW

xtask/src/cmd/migrate.rs

6

NEW

xtask/src/cmd/docs.rs

8,9

NEW or EDIT (plan-lint + plan-archive)

xtask/src/cmd/e2e.rs

10

EDIT (--no-refresh flag)

xtask/src/cmd/validate.rs

8

EDIT (wire plan-lint as soft-fail)

xtask/src/cmd/mod.rs

5,6,8,9

EDIT (subcommand registration)

docs/modules/ROOT/pages/adrs/adr-NNN-plan-lifecycle-and-status-vocabulary.adoc

7

NEW

docs/modules/ROOT/nav.adoc

7

EDIT (register ADR)

.githooks/pre-commit

7

EDIT (Q4 + Q6 wording)

.githooks/pre-push

10

EDIT (wire --no-refresh)

.gitlab-ci.yml

5

EDIT (install cargo-llvm-cov)

.gitignore

6

EDIT (snapshots dir)

.claude/docs/local-dev.md

4,6

EDIT (decision tables)

docs/modules/ROOT/pages/test-coverage-scorecard.adoc

5

EDIT (per-crate baselines)

Verification

After every step: cargo xtask validate --skip-docker + cargo nextest run --workspace.

Risks

Risk Mitigation

F-043 dev refresh semantics may diverge from reload/restart in subtle ways

Decision table in .claude/docs/local-dev.md. Code-level: a debug-log line at refresh-time stating "skipping rebuild because <category> unchanged"

F-044 cargo-llvm-cov adds a heavyweight dep on llvm-tools-preview rustup component; CI image needs update

Bake the install into the CI image before adopting the lint; verify in a CI dry-run

F-045’s pg_dump -F c requires the host to have pg_dump installed at a compatible version

Mitigation: docker-shell pg_dump via the postgres container itself (canopy does this via its devtools module — port the pattern too)

F-040 lock TTL: if xtask crashes mid-operation the lock stays held until manual rm

Port canopy’s 60s polling + clear-error-message; lock file lives in $TMPDIR so reboot cleans it up

F-042 reconciliation rewrites .ports.env when an operator is intentionally pinning specific ports

Drift-detected log line is loud; consider an --ignore-drift flag if this becomes a pain

After this plan lands

  • Concurrent xtask invocations no longer race (F-040)

  • "Port already allocated" failure mode eliminated (F-041 prevents stale containers from holding ports)

  • .ports.env drift auto-detected + corrected (F-042)

  • cargo xtask dev refresh is the daily-driver: rebuilds only what changed (F-043)

  • Workspace coverage measured + (soft) gated via cargo xtask coverage (F-044)

  • DB snapshot/rollback in developer flow via cargo xtask migrate (F-045)

  • Plan-lifecycle doctrine codified — canonical Status vocabulary, archival rule, pre-commit Q4/Q6 reframed against "plans are living specs, not backlogs/errata dumps" (F-046)

  • cargo xtask docs plan-lint enforces the vocabulary (soft-fail initially; promoted to blocking after cleanup MR) (F-047)

  • cargo xtask docs plan-archive makes plan archival idempotent (F-048)

  • cargo xtask e2e --no-refresh prevents seed-wipe during long local sessions; pre-push uses it (F-049)

Edit this page · latest