Next-phase implementation plan

Roadmap.
28 active features (+ 9 retired). From research artefact to multi-engagement product.

The peer-ring's first wave (Batches 1–7) closed every open ISS-* item and shipped the 4 K-scouts. This roadmap is the next wave: the work that moves the persistence off Charlie's laptop, hardens the audit spine, and turns the framework into something that can run up to three concurrent engagements without forking the code. Per the 31 May identity ruling, cognition and external-source reads stay in Clawpilot — Azure hosts only the database.

28 active features 3 P0 unblockers 15 P1 follow-ons 8 P2/P3 items 4 retired (F2, F9, F22, F28) All sized for Option α (schema-per-engagement)

The strategic pivot

What this roadmap accomplishes

Today the peer-ring is a beautifully-engineered local-laptop research artefact: every cron job, every secret, every audit row, and every sign-off gate depends on Charlie's machine being awake and Charlie's attention being available.

By the end of this roadmap, the substrate lives in Azure, the cron lives off the laptop, the audit spine is tamper-evident, and a new engagement can be onboarded as a parameter change, not a fork.

Locked decisions (Charlie, 30–31 May 2026):

  • DR is not needed. The hosted DB is an accelerator + integrity-attested evidence trail, not a system-of-record requiring disaster recovery. Captured as F25.
  • Option α — schema-per-engagement on a single shared Flexible Server. Up to 3 concurrent engagements; no cross-engagement privacy concern; the federator-plus-portfolio-DB alternative (β) was unneeded complexity.
  • cos_config ships with 6 keys (see F6). No ADR requirement for new key additions — this is an internal tool, not a customer ADO surface.
  • Closeout artefacts always handed back to the customer — lifts F21 from optional-format to customer-facing-default (signed PDF + Library Curator extract).
  • Identity stays on the laptop (31 May ruling). External-source reads (M365, ADO, MSX, GitHub) and all LLM-using cognition must run inside Clawpilot under Charlie's authenticated identity. No Azure VM / Container App can be joined to the Microsoft tenant for delegated access. Azure hosts the DB and (optionally) pure-DB housekeeping only. Retires F22 (always-on scouts) and collapses F2 (off-laptop scheduler). Introduces F26, F27.
  • F9 / F28 Approvals — DEAD (31 May ruling, post-feasibility test). The required delegated scope ApprovalSolution.ReadWrite on the beta endpoint POST /beta/solutions/approval/approvalItems is not user-consentable in Charlie's tenant — admin approval required and unobtainable. Application permissions are "Not supported" on the endpoint, so a service-principal workaround is also impossible. Sign-off fallback: Steward-DM-to-Charlie via the already-working m365_send_chat_message path. No new feature needed — this is the status-quo channel cemented.

The whole list at a glance

Feature index

Priority is about blocking-ness, not effort. P0 blocks everything downstream. P1 is high-leverage and shippable in parallel with P0s. P2 is the scale-out / portfolio layer; valuable but waits. P3 is hygiene — cheap, defensible, do when convenient. Status will be updated as features ship.

#FeaturePriorityEffortOwnerStatus
F1Hosted-Postgres migration (Azure Flexible Server, schema-per-engagement)P0LSteward + CoSshipped
F2Off-laptop durable schedulerretired 31 May; cron stays in Clawpilot under Steward scheduling loop. See F26.retired
F3Audit-spine integrity (hash-chain + external mirror + verify ceremony)P0SStewardshipped
F4Secret-management end-to-end (Key Vault + Managed Identity)P0SSteward + CoSshipped
F5Kernel-vs-engagement-pack manifest + contracts grep-testP1MSA + Stewardshipped
F65-layer configurability + bootstrap validator + config fingerprintP1MCoS + Stewardshipped
F7engagement_id on portfolio-shareable tables onlyP1MSA + CoSshipped
F8Agent governance suite (model pin + canary + brief versions + append-only)P1MSteward + SAshipped
F9M365 Approvals as sign-off channelretired 31 May; ApprovalSolution.ReadWrite requires admin consent, not user-grantable. Fallback: m365_send_chat_message Steward-DM.retired
F10aReuse-candidate drain-and-store (Curator stage 1)P1XSCoS + SAshipped
F10bLibrary Curator MVP (Curator stage 2)P1MLibrary Curator (new role)shipped
F10cCurator surfacing-to-SA loop (v2)P2LSA + Library Curatordesign-pending-corpus
F11Engagement-onboard skill (mobilise-engagement)P1MCoS + Stewardshipped
F12Per-engagement cost attribution + per-engagement-OR-portfolio breakersP1SCoS + Stewardshipped
F13Charlie mode (load-shedding) — no deputyP1SCoSshipped
F14Engagement-internal observability dashboard (Power BI)P1MCoS + Stewardshipped
F15Engagement lifecycle ceremonies (mobilise / transition / closeout)P2MSteward + CoSshipped
F16Risk register — extend existing RIB schemaP1XSCoS + SA + Stewardshipped
F17Portfolio read view + cross-engagement dashboardP2MLibrary Curator + CoSshipped
F18Per-scout proven flags (per-scout, not shared)P1XSSteward + Operatorshipped
F19Reuse outcome measurementP2SSA + Library Curatorshipped
F20MCP restart-resilience drill + runbookP1XSStewardshipped
F21Customer-facing compliance-evidence export (signed PDF + Library extract)P2MSteward + CoSshipped
F22Always-on scout tierretired 31 May; impossible without Charlie's identity off-laptop. Successor: F26.retired
F26Clawpilot-resident always-on session pattern (F22 successor)P1MCharlie + Steward (Clawpilot)shipped
F27Laptop-continuity playbook (re-install + re-auth + resume)P1SCharlie + Stewardshipped
F28Approvals-response capture via Graph pollretired 31 May; moot without F9.retired
F23Unsurfaced-integration scan (one ADR with YES/DEFER/NO rulings)P3XSSAshipped
F24Operator-in-spine clarification (ADR + spine-member with class='operator')P3XSSteward + SAshipped
F25ADR: "Audit spine substrate is non-durable by design"P3XSStewardshipped
F29Generic Loop publisher skill + per-engagement Loop wiringretired 2026-06-02; Loop reporting pivot. Successor: W10 web reporting front end.retired
F30Portfolio metrics in the 5-3-2-1 brief headerretired 2026-06-02; metrics surface migrates to W10 web dashboard, freed from the 5-3-2-1 header.retired
F31Use-case-level tracking (one level below engagement/project)P2MSA + Stewardshipped
F32Loop page templates + defined page hierarchyretired 2026-06-02; template contents migrate to W10 as report-type definitions; Loop delivery layer dies.retired
F33Loop governance rulesretired 2026-06-02; Loop-specific rules (publish-cursor CAS, /ws/-only, archive-on-replace) moot. Idempotency + archive-on-replace principles transfer to W10.retired
F34Engagement bootstrap-survival Loop pageretired 2026-06-02; survival-page schema migrates to W10 survival report type.retired
F35Durable corpus sync (skills / briefs / docs → Azure Storage, scheduled)P1MSteward + SA + CoSshipped
F36Milestone recognition tracker + thank-you / congratulations draftingP2SCoSshipped

Status legend: planned not started · in-flight a fix-batch is actively building it · pending testing artefacts staged + peer-reviewed but no destructive / first-tick / live drill executed yet · shipped live + verified by Steward · blocked waiting on a Charlie decision or an external dependency

P0 Unblockers — 3 features. Ship as one coordinated programme.

These three are tightly coupled. Each is structurally a pre-requisite for several of the P1 items, and they share substrate decisions (Flexible Server provisioning, Key Vault, schema-per-engagement design). Treat as one Batch 8 wave. (F2 off-laptop scheduler was originally a fourth P0; retired 31 May per the identity ruling.)

F1 Hosted-Postgres migration (Azure Flexible Server, schema-per-engagement)

P0 Effort: L shipped Steward CoS

Move the audit spine and all peer-ring tables from local Postgres on Charlie's laptop to Azure Database for PostgreSQL Flexible Server in a single migration that is multi-engagement-capable from day one.

Why
Removes the laptop SPOF, eliminates the dab-config.json connection-string secret on disk, and unblocks every cross-engagement, off-laptop, and portfolio item downstream. With DR ruled out (F25), getting the spine off the laptop is the only durability story.
What gets built
  • Azure PostgreSQL Flexible Server, UK South, B2ms tier (~$80–$120/mo idle).
  • Entra ID auth via Managed Identity for the DAB container. No connection-string-on-disk.
  • Private endpoint into the default VNet. DAB co-located in Container Apps in the same region (avoids per-call cross-WAN latency on every SP call).
  • Schema-per-engagement isolation: Haleon migrates into schema haleon. Future engagements get their own schema (acme, globex, ...) on the same DB.
  • Schema-scoped database roles — no "anonymous" role spans schemas. sp_* functions and *In shims replicated per schema with schema-scoped SECURITY DEFINER.
  • 72-hour dual-read window post-cutover: read from both local and Azure, sentinel-table count reconciliation before decommissioning the laptop instance.
Cutover mechanics
  1. pg_dump --schema-only — review for laptop-isms (file paths in function bodies, extension dependencies).
  2. Restore schema to Azure.
  3. pg_dump --data-only of the haleon schema — restore.
  4. DAB cutover: feature-flag in workflow.json points the MCP transport at the new host.
  5. Operator and peers pick up at next bootstrap.
  6. Run a 2-hour read-only freeze on a Saturday morning UK time when scout activity is minimal.
New failure modes to handle
Entra token expiry mid-transaction (DAB needs token refresh); connection-pool exhaustion under rapid scout sweeps; Azure PG maintenance window (pin a Sunday 02:00 UTC slot); network partition between Operator session and Azure (peers need graceful "DAB unreachable" degradation, not silent failure); Azure DB running-cost line (~$80–$300/mo) needs its own Azure budget alert as a sibling to the in-DB breaker.

F2 Off-laptop durable scheduler (Container Apps Jobs)

retired 31 May 2026

Collapsed. Originally proposed lifting all crons (ring-tick, scout-sweep, daily-brief, ado-scribe drainer) to Container Apps Jobs. The constraint that all external-source reads and LLM cognition must run under Charlie's authenticated identity in Clawpilot (31 May ruling) eliminates everything except pure-DB operations from the off-laptop candidate list — and the pure-DB candidates (cost rollup, breaker check, verify-chain, gap-scan) all have the property that nothing meaningful happens to the DB when the laptop sleeps (no writes ⇒ no rollups to compute, no chain to verify beyond the last state). Container Apps Job infrastructure is not justified for catching-up-at-next-wake operations whose work was zero in the interval.

Replacement: cron remains in Clawpilot, hosted by the Steward peer as a scheduling loop that fires on each interactive session resume. See F26 for the always-on-while-awake successor to scout sweeping.

F3 Audit-spine integrity (hash-chain + external mirror + verify ceremony)

P0 Effort: S shipped Steward

With DR ruled out, the only remaining integrity story for the audit spine is making it tamper-evident. Adds cryptographic chaining to agent_runs and a daily verify ceremony.

Why
If a row is corrupted or deleted and there's no backup to reconcile against, the verify-chain ceremony is the only way to even know it happened.
What gets built
  • prev_hash + row_hash columns added to agent_runs, computed inside sp_create_agent_run at INSERT and inside sp_update_agent_run at completion. No change to detail_ref handling — this is distinct from REJECTED Fix-B (steward_rulings #1).
  • AFTER trigger that writes to an immutable sidecar via a dedicated SECURITY DEFINER function (respecting the *In shim contract). Sidecar lives in a separate schema on the same DB.
  • Steward verify-chain ceremony — daily — emits a class='audit' row with the verification result.
  • Chain head mirrored daily outside Postgres — a Steward-signed plaintext line to an Azure Storage immutable blob. A DB-superuser can rewrite the chain end-to-end inside Postgres, but cannot rewrite the external mirror without an attestation gap that verify-chain catches.

F4 Secret-management for the hosted-PG connection (Key Vault + MI)

P0 Effort: S shipped Steward

Scope reduced 31 May 2026. Originally end-to-end secret management for every peer-ring credential. Under the new constraint, most "secrets" (Teams webhook, GitHub PAT, MSX auth) don't exist as separate-from-Charlie credentials — they ARE Charlie's identity, mediated through Clawpilot's MCPs, and so can't be vaulted. Key Vault retains a narrower but still essential job: holding the hosted-PG connection string (and, conditionally, an F9 token cache hint if Approvals creation passes its feasibility test).

What gets built
  • Single shared Key Vault, secrets named <engagement>-<secret-key>.
  • Hosted-PG connection string lives in KV (e.g. haleon-pg-conn); Clawpilot fetches at session start using Charlie's own delegated identity.
  • DAB connection-string removed from dab-config.json entirely.
  • secret-refs.json per engagement pack carries Key Vault URIs only — no literal secrets.
  • Steward-owned audit-test: grep across ~/.copilot/, m-skills/, m-role-briefs/, and every engagement pack for literal secrets. Runs at every bootstrap-roles and on any kernel edit. Fails loud.
Open question
Can Charlie's own identity hold the Key Vault admin role in the target Azure subscription? If not, KV provisioning needs a co-operating subscription owner.

P1 Force-multipliers — 15 features (incl. F26 Ticker + F27 continuity, both new on 31 May). Ship in parallel with the P0 wave where dependencies allow.

F5 Kernel-vs-engagement-pack manifest + contracts grep-test

P1 Effort: M shipped SA Steward

Codify the kernel-vs-pack boundary. Every "Haleon-ism" in the kernel becomes a leak that a CI-style grep-test catches.

What gets built
  • Written manifest of what is kernel (workflow-agnostic) vs what is per-engagement.
  • Engagement pack directory layout: ~/.copilot/m-engagement-packs/<name>/{pack.json, scouts/, briefs/overlays/, secret-refs.json}.
  • Engagement-overlay briefs (cos.engagement-overlay.md, sa.engagement-overlay.md) that shadow kernel briefs without overriding invariants.
  • Grep-test: enforces zero haleon|otc|hcp|gxp|consumer.health|charlie (case-insensitive) under m-skills/ and m-role-briefs/. Runs at every bootstrap-roles and on any kernel edit.
Closes
Charlie open-Q #6 (SA brief still references MicrosoftMcCormickTesting/Loom).

F6 5-layer configurability + bootstrap validator + config fingerprint

P1 Effort: M shipped CoS Steward

One typed home for every configurable value, in five layers. Fail loud at bootstrap if a required value is missing.

The 5 layers
  • L0 — Process env: Key Vault URIs only. Bootstrap reads from env.
  • L1 — workflow.json (kernel): peer cardinality, audit transport, rotation rotator-of-record, phase-boundary timestamps, contact-channel transport. No engagement-specific values.
  • L2 — Engagement pack (pack.json): customer name, GH org allowlist, MSX territory, intake channel ids, ADO org/project, Teams contact channel id, compliance regime, recipient-voice profile name. No secrets — only KV URIs.
  • L3 — cos_config table: runtime-mutable per engagement.
  • L4 — Per-skill local config: skill-internal opinions only.
cos_config — the 6 initial keys (locked)
  • intake_channels — array, default [] (read by Intake Triage scout)
  • intake_customer_names — array, default [] (privacy hard-gate — scout aborts if empty)
  • reuse_scout_repos — array, default [] (read by Reuse Scout)
  • cost_breaker_baseline_usd_per_day — number, default 50.00
  • cost_breaker_band_pct — number, default 20
  • charlie_mode — enum, default full (see F13)
New keys can be added freely as needs surface — no ADR requirement (this is an internal tool, not a customer ADO surface). Adds are tracked in this roadmap page.
Bootstrap validator
JSON-schema validates L1+L2. Fails loud at bootstrap on any missing required L2 key. No "happily start with missing config and discover the gap mid-Friday-sweep."
Config fingerprint
Peers stash a hash of all observed config values in their boot ack. Steward diffs fingerprints across roles at each scout-sweep. Mid-flight drift surfaces as a class='audit' row.

F7 engagement_id on portfolio-shareable tables only

P1 Effort: M shipped SA CoS

Add engagement_id to tables that participate in cross-engagement queries. Do not add to engagement-active body tables — those are partitioned by schema.

Where it goes
YES: rollups, ratified ADR metadata, reuse_candidates, peer_metrics, cost_telemetry, every v_portfolio_* view.
NO: signals, agent_runs, decisions, comms_drafts, briefs. These are scoped by being inside a schema; engagement_id is implicit.
Plumbing
Default-via-session-var (peer's connection sets SET haleon.engagement_id = 'haleon' at session start; SP DEFAULT reads from the var). Built into the migration target schema in F1, not retrofitted afterwards.

F8 Agent governance suite (model pin + canary + brief versions + append-only)

P1 Effort: M shipped Steward SA

One shipped suite covering four governance gaps: model drift, prompt regression, brief versioning, and DB-level append-only enforcement.

(a) Model pin + drift alert
Model id (claude-opus-4.7-1m-internal) explicitly pinned in workflow.json per role. Steward emits an audit row if the spawn-returned model identity drifts from the pin.
(b) Canary regression corpus
Per-role canned scenarios (5–10 each), authored jointly by SA + CoS. Steward runs them weekly. Output diffed against pinned baseline; deviation above threshold opens a class='audit' row.
(c) Brief versioning
New peer_brief_versions table mirroring the existing skill_versions pattern. Brief edits write a version row. No more silent brief edits.
(d) DB-level append-only enforcement
Revoke UPDATE/DELETE on agent_runs base-table to all roles including anonymous; SP-mediated writes are the only survivors. Safe to ship now (already SP-only-write). Sequenced: applying the same pattern to the 6 ISS-22-residual tables (Brief / MeetingPrep / PdSyncPrep / QuietWatchState / Signal / SteercoPrep) requires INSTEAD OF UPDATE shims first — that's a follow-up bundle.

Shipped 2026-06-01 (Wave 2). F8(a) model pin: grep "model" loom/workflow.json haleon/workflow.json returned 6 hits of claude-opus-4.7-1m-internal across all peer-ring roles. F8(a) drift audit-emit: skill ~/.copilot/m-skills/model-pin-drift/ present (check_model_pins.py + SKILL.md); forced-drift produced agent_runs row id=ed668258-72f3-497a-ad21-ae68bbcf308e with triggered_by=model_pin_drift:role=Steward;pinned=claude-opus-4.7-1m-internal;actual=claude-opus-4.6; post-test grep confirmed all 3 peer pins restored to claude-opus-4.7-1m-internal (lines 11/19/27 of haleon/workflow.json). F8(b) canary corpus: 9 scenario files at ~/.copilot/m-skills/canary-corpus/scenarios/ (3 per role); runner executed steward-01-agent-runs-spine end-to-end (exit code 0, VERDICT=PASS), wrote baseline artifact ~/.copilot/m-skills/canary-corpus/scenarios/baselines/steward-01-agent-runs-spine.baseline.txt (sha256 6b078bbe2f7e92f10936894e9a6af31536b610e22ea94795948ccf7e7c6a264d), and emitted audit row id=c71c311a-ad07-406c-8a09-e364b95118fd with triggered_by JSON {"scenario_id":"steward-01-agent-runs-spine","verdict":"PASS","diff_pct":0,"baseline_created":true}. F8(c) brief versions: SELECT id, brief_name, content_sha256 FROM haleon.peer_brief_versions WHERE brief_name='STEWARD.md' returned row id=2fab20a7-df18-4732-bc8b-2e66fe35db75. F8(d) append-only: UPDATE haleon.agent_runs as haleon_aiac_owner returned permission denied for table agent_runs; sp_create_agent_run(...) in same session returned new row id=a5d5596a-5721-4626-8d42-c6f36174c1b3. Wave 1 F3 chain-repair back-ported to m-skills/mobilise-engagement/ddl-replay/03-f3-hashchain.sql.tmpl at lines 64, 154, 176, 271.

F9 M365 Approvals as canonical sign-off channel

retired 31 May 2026 (post-feasibility test)

Dead. Feasibility test under Charlie's delegated session (31 May 07:48 BST) confirmed the required scope ApprovalSolution.ReadWrite on the beta endpoint POST https://graph.microsoft.com/beta/solutions/approval/approvalItems is not user-consentable in his tenant — the consent screen returned "Microsoft Graph Command Line Tools needs permission to access resources in your organisation that only an admin can grant". Application permissions are "Not supported" on the endpoint (delegated-only), so a service-principal workaround is also impossible. Admin consent is unobtainable in this engagement.

Fallback (already in production): sign-off ceremony continues over Steward-DM-to-Charlie via m365_send_chat_message, which is the already-working path through Charlie's delegated Teams session. The three gates (comms_drafts.status=awaiting_signoff, ADO writeback pending_approval → queued, scout enable-flag flips) remain on this channel. No new feature needed; the status quo is cemented. Sign-off latency depends on Charlie noticing the DM (laptop-awake, attention-available) — accepted per the no-deputy + no-timeout posture.

Corrections to prior framing: the scope was previously written as "Approvals.ReadWrite" — the actual scope name is ApprovalSolution.ReadWrite. The endpoint is beta-only, not v1.0.

F10a Reuse-candidate drain-and-store (Curator stage 1)

P1 Effort: XS shipped CoS SA

Stops Reuse Scout's outputs from fading. Single-engagement, lives in the engagement schema. Buys time before the cross-engagement Library Curator (F10b) is ready.

What gets built
  • reuse_candidates table in the engagement schema with columns: id, seen_at, actioned (bool), actioned_at, dismissed (bool), dismiss_reason (text), plus signal back-references.
  • Reuse Scout signals drain into reuse_candidates on first triage.
  • CoS surfaces them in daily brief / triage skill (CLI for now; dashboard later via F14).

Shipped 2026-06-01 (Wave 3). Table: SELECT column_name FROM information_schema.columns WHERE table_schema='haleon' AND table_name='reuse_candidates' returned 9 columns (id uuid PK, seen_at timestamptz, signal_id uuid FK→haleon.signals(id) ON DELETE SET NULL, signal_summary text NOT NULL, actioned bool default false, actioned_at timestamptz, dismissed bool default false, dismiss_reason text, portfolio_shareable bool default false); partial indexes idx_rc_unactioned (WHERE NOT actioned AND NOT dismissed) and idx_rc_portfolio (WHERE portfolio_shareable) both present. Drain: function haleon.sp_drain_reuse_candidates() created (SELECT proname FROM pg_proc WHERE proname='sp_drain_reuse_candidates' returned 1 row); end-to-end probe planted signal id=49f217b2-9a46-44cf-9f17-1efba0ba9919 with source='reuse-scout', drain returned 1 row inserted, candidate id=d080da55-436d-4130-ad1d-c638fcaaa2fe visible with signal_summary='WAVE3-TEST: foo bar reusable pattern'; cleanup confirmed 0 rows remaining. Drain uses COALESCE(s.subject, s.body_excerpt, 'untitled') (live haleon.signals has no title/summary columns — only subject and body_excerpt). CoS hook: ~/.copilot/m-skills/daily-brief/SKILL.md updated — new "Reuse candidates awaiting triage" section sourced from SELECT id, seen_at, signal_summary FROM haleon.reuse_candidates WHERE NOT actioned AND NOT dismissed ORDER BY seen_at DESC LIMIT 10.

F10b Library Curator MVP (Curator stage 2)

P1 Effort: M shipped Library Curator (Clawpilot peer)

What gets built
  • New role brief at ~/.copilot/m-role-briefs/LIBRARY-CURATOR.md; new tier_2_synthesiser entry in haleon/workflow.json.
  • Daily cross-schema read of: ratified adrs, reuse_candidates rows marked portfolio_shareable=true, decisions marked portfolio_shareable=true.
  • Writes to pattern_library in the dedicated portfolio schema (same DB, separate schema) with engagement_id + tags + ratified_at.
  • Simple read views by tag / sector / pattern-type for SA query.
  • No auto-surfacing loop in v1. SA queries the library when SA wants to (manual, or via a new library-search skill later). Auto-surfacing deferred to F10c.

Shipped 2026-06-01 (Wave 3). Role brief: ~/.copilot/m-role-briefs/LIBRARY-CURATOR.md authored (223 lines, 17 ## sections incl. Mission / Cognitive mode / Stack validation / Write-path (DAB shim) / Primary inputs / Primary outputs / Skills invoked / Peer interaction / Hard "do not do" / Swap-protocol / Open state / Scan procedure / Tag taxonomy / agent_runs spine contract); content sha256 prefix b47aab74255b4210. Workflow registration: haleon/workflow.json patched — ConvertFrom-Json | %{$_.roles | ?{$_.name -eq 'LibraryCurator'}} returned the new entry with tier=tier_2_synthesiser, model=claude-opus-4.6, spawnAtBootstrap=False, briefPath resolves to the file above. Schema: SELECT 1 FROM pg_namespace WHERE nspname='portfolio' returned 1; information_schema.columns for portfolio.pattern_library returned 11 columns (id uuid PK, engagement_id uuid NOT NULL, source_kind text CHECK IN ('adr','reuse_candidate','decision'), source_id uuid NOT NULL, title text NOT NULL, summary text, tags text[] default '{}', pattern_type text NOT NULL, sector text, ratified_at timestamptz, added_at timestamptz default now()) with UNIQUE (engagement_id, source_kind, source_id) + 4 indexes (engagement / GIN tags / pattern_type / sector); 3 views present (v_library_by_tag, v_library_by_sector, v_library_by_pattern_type) confirmed via SELECT viewname FROM pg_views WHERE schemaname='portfolio'. End-to-end: INSERT of test row (engagement_id=<uuid>, source_kind='adr', tags=ARRAY['audit','wave3-test'], pattern_type='architecture', sector='pharma') returned id=c4a1d655-cb5e-460d-b3ed-1a59780d0c53; v_library_by_tag returned (audit,1), (wave3-test,1); v_library_by_sector returned (pharma,1); v_library_by_pattern_type returned (architecture,1); cleanup deleted 1 row. F10b sidecar: ALTER TABLE haleon.decisions ADD COLUMN IF NOT EXISTS portfolio_shareable boolean NOT NULL DEFAULT false applied + partial index idx_decisions_portfolio (portfolio_shareable) WHERE portfolio_shareable; column presence re-probed via information_schema.columns returns true.

F11 Engagement-onboard skill (mobilise-engagement)

P1 Effort: M shipped CoS Steward

The operational means that turns "multi-engagement capability" into "Day-0 takes hours, not weeks." Without this, the factory claim is aspirational.

Live-proven 31 May 2026: test-engagement-1 mobilised end-to-end and the per-engagement DAB MCP (test-engagement-1-aiac-db) verified by an out-of-process read_records against the seeded ScoutEnableFlags row.

What the skill does (in order)
  1. Takes an engagement-pack manifest as input.
  2. Validates it against the L2 JSON schema (from F6).
  3. Provisions a new schema in the shared Flexible Server with schema-scoped roles + replicated sp_* / *In shims.
  4. Provisions Key Vault secret entries (engagement-prefixed naming per F4).
  5. Registers MCP identities (azure_devops, msx-mcp, github, M365 mailbox).
  6. Bootstraps peers via existing bootstrap-roles.
  7. Runs first prove-cycle (3-greens-equivalent smoke test).
  8. Returns ready/not-ready with structured failure reasons.
Rollback semantics
Smoke-test fail at any step rolls back the schema-create transaction; Key Vault entries are left (idempotent re-run) but flagged for manual review. No partial-onboard state in production.
Invocation authority
Charlie-only (locked 31 May 2026). The skill is destructive (provisions a new schema, registers MCP identities, may consume budget headroom). CoS cannot self-invoke; the trigger is always a Charlie command in the Operator session.
Step 5 gotcha — dab-config.json needs runtime.mcp
The per-engagement dab-config.json materialised in Step 5 MUST include a runtime.mcp block with enabled: true AND an explicit dml-tools map. Without it, DAB starts cleanly, the MCP server registers in Clawpilot's UI, every tool checkbox shows "enabled", and the host still rejects every call with ToolDisabled / "The <tool> tool is disabled in the configuration." — the rejection string is emitted by DAB itself, not by Clawpilot's tool registry. Observed against test-engagement-1 (31 May 2026); reference shape is the haleon dab-config.json. Captured in m-skills/mobilise-engagement/SKILL.md.

F12 Per-engagement cost attribution + per-engagement-OR-portfolio breakers

P1 Effort: S shipped CoS Steward

Real financial control once there's >1 engagement. One engagement can't consume another's budget; portfolio ceiling sits above the sum.

Wave-1 SHIPPED 2026-06-01: F12 SQL applied to live haleon (lab sub `ME-MngEnvMCAP180885-smccormick-1`); verified by 5 probes — `cos_config` seeded (3 cost-breaker keys present), `sp_check_cost_breaker` arity=4 (accepts `p_engagement_id`), `agent_runs_class_check` IN-list contains `breaker_trip`, `cost_telemetry_period_agent_engagement_key` UNIQUE constraint present, `sp_verify_chain()` returns OK post-chain-repair (74 rows verified).

What gets built
  • cost_telemetry gets engagement_id (lands in portfolio-shareable territory per F7).
  • Existing sp_check_cost_breaker + sp_check_cost_breaker_rolling extended: compute per-engagement spend AND portfolio-total. Either tripping disables scouts for the relevant scope.
  • Per-engagement baseline default $50/day per engagement (locked 31 May 2026 — "fine to start, tune by observation"), configurable via cos_config.cost_breaker_baseline_usd_per_day.
  • Portfolio ceiling default: sum-of-per-engagement-caps + 20% headroom.
  • Daily attribution rollup feeds F14 and F17 dashboards.

F13 Charlie mode (load-shedding) — no deputy

P1 Effort: S shipped CoS

A typed availability state Charlie sets in cos_config, honoured by every outbound path. Per Charlie's ruling: no deputy, no auto-escalation — Charlie absorbs the latency consequences.

Wave-1 SHIPPED 2026-06-01: F13 SQL applied to live haleon; verified by 4 probes — `charlie_mode` key present with value `"full"`, `sp_set_cos_config` paired-row emission tested live (`mode_flip:from=full;set_by=wave-1-steward;to=quiet` row observed, then reset), `agent_runs_class_check` IN-list contains `mode_flip`, `comms_drafts.compliance_override` column present as `boolean`.

What gets built
  • cos_config.charlie_mode enum: full | quiet | emergency | silent.
  • Outbound-voice, daily-brief, ring-tick (always emits — internal infrastructure), scout-sweep, and the m365_send_chat_message Steward-DM sign-off path (the production sign-off channel post-31 May F9 retirement) all honour the mode. outbound-voice uses defer-to-queue semantics (drafts still written to comms_drafts; Steward-DM solicitation suppressed in non-full modes); briefs + scout-sweep suppress under emergency + silent.
  • silent mode mutes all outbound except compliance-flag signals (the emergency-override scope — signals tagged with a compliance-regime routing tag punch through via a compliance_override = true flag on the comms_drafts row, firing Steward-DM regardless of mode).
  • Calendar-driven mode-set as a v2 extension.
Honest trade-off
With no deputy and no Approvals-channel timeout (F9 retired), an extended Charlie absence under silent means the Steward-DM sign-off queue (comms_drafts + ado_writeback_queue rows at status = pending_approval) grows indefinitely — the system does not auto-approve, does not escalate, does not drain. silent mode damps inbound volume but doesn't drain backlog; Charlie returns to a batch-review queue proportional to absence duration. This is the explicit locked ruling ("Charlie absorbs the latency consequences").

F14 Engagement-internal observability dashboard (Power BI)

P1 Effort: M shipped CoS Steward

Turns the audit spine from "evidence in a DB" into "system health at a glance." Single engagement; cross-engagement view is F17.

Shipped 2026-06-01 (Wave 5 follow-up): F14 dashboard published in PBI workspace m-tester-obs (5fe04c44-2297-4089-8747-069618335e66, MSIT tenant) by operator. Automated f14-publish skill is wired and live-tested (audit 47d31b5e-cf45-404b-874a-98e2bcd1efa7); apply-path gated on operator-provisioned Entra SP credentials in KV slot pbi_reader_conn. SP provisioning is blocked on Service Tree ID — residual carried to W6+.

Wave-5 substrate + skill SHIPPED 2026-06-01 (tag remains pending-testing pending operator residuals): f14-publish skill apply-path wired and live-tested (audit 47d31b5e-cf45-404b-874a-98e2bcd1efa7); gates working; correctly blocked on operator-side residuals — manual MSIT Power BI service-principal provisioning + .pbix build. Per CoS classification: kernel D1 operator-only, NOT a W5 substrate gap. Re-flip to shipped on operator-residual close.

What gets built
  • Power BI semantic model over the engagement schema (read-only, hourly refresh).
  • Tiles: rotation cadence, run-rate per role, cost burndown vs baseline, scout sweep success rate, open-run gauge (HS-2), Approvals queue latency, sign-off queue depth.
  • Published to a Microsoft-internal Power BI workspace.
  • Designed for escalation-by-exception: drill-in only when a tile reds.

F16 Risk register — extend existing RIB schema

P1 Effort: XS shipped CoS SA Steward

The 5-3-2-1 brief surfaces 5 risks per slot but they're consumed-and-discarded today. This gives them a continuing register. Extends existing risks_issues_blockers (RIB) rather than creating a new table (avoids load-bearing-decision-#5 violation).

Schema additions to RIB
  • class enum: delivery | engagement. Both go in one table, distinguished by class.
  • decay_at — auto-decay default: 30 days from last evidence touch.
  • escalation_threshold — severity at which a risk auto-surfaces in the brief.
  • engagement_id — per F7, since risk register IS portfolio-shareable.
Severity / likelihood scale (recommended)
1–5 numeric for both severity and likelihood. Combined risk score = severity × likelihood (1–25).
Workflow integration
  • Daily-brief 5-3-2-1 surfaces from RIB (not from CoS's recall).
  • Phase-boundary ceremony burns it down (resolves or re-evaluates each open risk).
  • Engagement-closeout (F15) archives open risks into the transition pack.
  • SA owns delivery-class risks; Steward owns engagement-class risks; CoS authors first draft on signal-extraction.

Shipped 2026-06-01 (Wave 2). RIB extended via ALTER TABLE haleon.risks_issues_blockers: information_schema.columns probe returned all 7 expected columns — class (USER-DEFINED haleon.rib_class enum), decay_at (timestamptz), escalation_threshold (int), engagement_id (text, pre-existing from F7-precursor), severity (int), likelihood (int), risk_score (int GENERATED). Test row (severity=4, likelihood=3, class=delivery) returned id=1181cf11-15c1-43f4-b6ab-9a058750dcc4 with computed risk_score=12, decay_at=2026-07-01 00:57:36+01:00 (~30d out), escalation_threshold=4. Migration preserved 2 existing rows (severity enum→int mapping low=1, medium=2, high=3, critical=4); recreated dependent views v_open_rib_by_pod and v_rib_in post-CASCADE; dropped orphan haleon.rib_severity type after pg_depend confirmed zero remaining references.

F18 Per-scout proven flags (per-scout, not shared)

P1 Effort: XS shipped Steward

Closes Charlie open question #2. Per-scout granularity gives each K-scout its own supervised prove path with per-flip audit attribution.

What gets built
  • ALTER scout_enable_flags adds: backlog_sentinel_proven, pipeline_watcher_proven, reuse_scout_proven, intake_triage_proven.
  • Existing scout_proven remains for Signal Scout only.
  • Per-scout flip ceremony documented per scout brief.
  • sp_check_scout_enabled() updated to require the per-scout flag for K-scouts.
Recommended ordering
First scout to prove: Backlog Sentinel (lowest privacy risk; ADO is already a primary engagement surface).

F20 MCP restart-resilience drill + runbook

P1 Effort: XS shipped Steward

Confidence under daemon churn. Today the documented behaviour assumes leases release cleanly and watermarks hold under a DAB restart. This exercises it. (Exercises the Clawpilot-resident MCPs — no Azure-hosted MCPs exist in this posture.)

Wave-1 SHIPPED 2026-06-01: F20 destructive drill executed against live haleon DAB (`drill_id=drill-20260531-f39a2027`); 5 DAB processes killed mid-lease via SIGKILL, restarted PID 6556. All 4 invariants PASSED: (4a) `scout_lease` reclaimed by TTL expiry at 300s, (4b) `signal-scout` watermark un-advanced from W₀=`2026-05-30 11:40:00+01:00`, (4c) no unexpected partial `agent_runs` rows, (4d) `sp_create_signal` ON CONFLICT dedup intact (signals count stable at 18). Summary audit row `5338349e-ee57-4752-b643-8cfac978a4d9` written with `triggered_by=mcp-restart-drill:check=summary;result=pass;detail=4_of_4_passed;drill_id=drill-20260531-f39a2027`.

Drill procedure
  1. Start a scout-sweep manually.
  2. Mid-sweep, kill DAB (or its containing process).
  3. Restart DAB.
  4. Verify: scout_lease released by TTL expiry; watermark un-advanced (next sweep resumes); no partial agent_runs rows; idempotency on re-run holds (ON CONFLICT dedup works).
  5. Document any deviation as a Steward audit row.
Cadence
Quarterly + post any DAB version bump.

F22 Always-on scout tier (Container Apps Jobs)

retired 31 May 2026

Dead. The scouts read M365 / ADO / MSX Dataverse / GitHub through MCPs that run under Charlie's user-delegated identity. Lifting them to Container Apps Jobs would require Charlie's tenant to grant equivalent application permissions (Microsoft Graph application scopes, ADO service-principal access, MSX Dataverse service identity, GitHub App). Per the 31 May ruling, this is not available. There is no substitute available without Charlie's tenant identity.

Successor: see F26 — Clawpilot-resident always-on session that ticks scouts on its own internal cron while the laptop is awake. Combined with the watermark/cursor logic each scout already has, laptop-asleep gaps cause latency, not data loss.

F26 Clawpilot-resident always-on session pattern (F22 successor)

P1 Effort: M shipped CoS Steward

A long-lived Clawpilot session whose only job is to tick the scout-sweep + core-tick + drainer + daily-brief crons on its own internal schedule, for as long as the laptop is awake. Honest about the laptop-asleep gap: it doesn't disappear, but the watermark/cursor logic each scout already has means sleep gaps cause latency, not data loss.

Wave-1 SHIPPED 2026-06-01: Ticker spawned via `m_spawn_session` (sessionId `3f135ccb-7401-4f45-858c-1a9828582abe`, Haiku 4.5, `tier_4_orchestrator`); ack`"Ticker online — entering cron loop."` returned. After one-shot sessionId wire-up (closes an HS-1 bootstrapping gap in the spawn ceremony — logged as Wave-2 follow-up), first core-tick observed at `2026-06-01 00:25:52` (`agent_runs.id=665cce6a…`, `class=audit`, `triggered_by=ticker_tick:cadence=core;result=pending`). Post-Ticker `sp_verify_chain()` returns `status=OK, rows_checked=76, tail=665cce6a…` — confirms the chain-repair `clock_timestamp()` fix holds under live multi-writer.

What gets built
  • A new long-lived Clawpilot session (separate from CoS/SA/Steward) called Ticker (locked 31 May 2026) — tier_4_orchestrator, spawned at Clawpilot startup by the Operator session.
  • Internal cron loop: core-tick every 15 min, scout-sweep every 2 h, daily-brief at 07:00 + 17:00, drainer-tick every 15 min. All running INSIDE Clawpilot, using existing MCPs and existing peer-ring tools.
  • Watermark/cursor logic on every scout (largely already there) so a sleep gap causes resume-from-cursor, not data-loss.
  • Steward audit row per tick (existing pattern).
  • Steward observability: if Ticker hasn't ticked in N minutes during a session, flag.
Acceptance decision (Charlie 2026-05-31)
Laptop-awake-only sweep cadence — accepted. Sleep gaps are absorbed (watermarks ensure latency, not data loss). No second always-on workstation.
Ticker session shape
New long-lived session, tier_4_orchestrator (ratified by Charlie 2026-05-31; workflow.schema.json is the source of truth for the tier registry — STEWARD.md updated to match). Model: claude-haiku-4.5 (mechanical orchestration, not cognitive work) — ratified 2026-05-31. Spawned by the Operator after bootstrap-roles completes (Option B — separate from peer-ring bootstrap, not part of bootstrap-roles itself, not in workflow.json) — ratified 2026-05-31. Not a peer-ring participant; not rotated by Steward; restarted on each Clawpilot startup. Spawn ceremony at m-skills/ticker/SKILL.md; idempotency via m_list_active_sessions filter + tick_lease CAS backstop.
Cron loop mechanism
PowerShell 60-second sleep timer with cadence-due checks on each wake. Core-tick every 15 min, scout-sweep every 2 h, daily-brief at 07:00 + 17:00, secret-scan + kernel-purity-scan daily at 06:00 UTC. Ticker invokes existing ring-tick skill (Jobs A + B) and daily-brief — does not reimplement cadence logic. Drainer-tick runs inside core-tick (ring-tick Job A step 4); Ticker emits a separate cadence=drain audit row for observability.
Observability
Every tick emits an agent_runs row: class=’audit’, triggered_by=’ticker_tick:cadence=<core|sweep|brief|drain|scan>;result=<ok|skip|fail>’, two-phase lifecycle. Steward gap detector: 30-minute threshold on cadence=core rows; flags a single ticker_gap:gap_minutes=<N> audit row per gap event (deduplicated). Sleep gaps are expected and produce ticker_gap audit rows but no data loss — all five scouts (Signal Scout, Backlog Sentinel, Pipeline Watcher, Reuse Scout, Intake Triage) resume from their ScoutWatermarkIn cursor on wake.

F27 Laptop-continuity playbook (re-install + re-auth + resume)

P1 Effort: S shipped CoS Steward

Documents the laptop-loss recovery procedure honestly. The hosted PG (F1) survives; the cognitive layer and every MCP identity is laptop-resident and must be rebuilt. This is a runbook, not infrastructure.

Wave-1 SHIPPED 2026-06-01 — live infrastructure verified: Storage account `stclawpilotcorpusuks` provisioned in `rg-clawpilot-uks` (UK South, Standard_ZRS, StorageV2, `--allow-blob-public-access false`, TLS 1.2, blob+container soft-delete 14d, versioning enabled, 3 containers `briefs`/`skills`/`docs`) — verified by `az storage account show` + `az storage container list` (Entra-auth, no SAS). SWA `swa-peerring-docs-uks` provisioned (Free, West Europe) at `https://orange-plant-0b5323e03.7.azurestaticapps.net` — verified by `GET /roadmap.html` returning HTTP 200 with "Force-multipliers" + 21 F26 references. `corpus-sync` skill built locally (3 files: `SKILL.md` 182L, `sync.ps1` 519L, `bootstrap-stub.ps1` 59L); uploaded to `stclawpilotcorpusuks/skills/corpus-sync/` (authed download byte-identical, anonymous `irm` returns 409 PublicAccessNotPermitted confirming RBAC enforcement). Bootstrap stub also published to SWA anonymous URL `https://orange-plant-0b5323e03.7.azurestaticapps.net/skills/corpus-sync/bootstrap-stub.ps1` (`irm` returns byte-identical script unauthenticated) — SA review condition met by `sync.ps1` push-mode atomically re-publishing stub to SWA on drift. KV `kv-clawpilot-uks` provisioned (RBAC-auth, purge-protection 90d); secret `m-encryption-key-enc-backup` written from `~/.copilot/m-encryption-key.enc` (75B) — byte-compare round-trip verified True. OneDrive `clawpilot-recovery.md` mirror DROPPED per §G #2a optionality (AADSTS65002: Azure CLI lacks `Files.ReadWrite` consent in active tenant; recovery card content already baked into `bootstrap-stub.ps1` — no unique survival value lost). MCP restart drill (F20) PASS 4-of-4 invariants; Ticker (F26) first core-tick observed in live `agent_runs`. F3 hash-chain BROKEN finding triaged via SA push-back (rejected verify-only band-aid): root-cause fix applied as 3 SP changes (`sp_create_agent_run` swap `now()`→`clock_timestamp()` and prev-lookup `DESC,DESC`→`DESC,ASC`, `sp_verify_chain` ordering `ASC,ASC`→`ASC,DESC`) + index `ix_agent_runs_chain_order (spawned_at ASC, id DESC)` + data-repair recomputing 7 divergent rows under advisory lock; in-tx `sp_verify_chain()` returned OK before COMMIT; post-Ticker chain verify OK at 76 rows. Decisions row `95a49dec-dc5a-425c-b175-3a4bf1e08e83` (tag `chain_repair`).

Playbook contents
  1. Acquire replacement laptop.
  2. Install Clawpilot.
  3. Re-authenticate every MCP interactively under Charlie's identity: M365 builtin, ADO MCP, MSX MCP, GitHub MCP, filesystem MCP, playwright, haleon-aiac-db MCP (now pointing at hosted PG via KV-resolved conn string).
  4. Validate: each MCP responds to a smoke test.
  5. Re-spawn peer-ring via bootstrap-roles haleon. Peers reattach to existing engagement schema; audit spine resumes from last committed row.
  6. Validate: peers produce a successful first-tick (CoS reads recent signals, SA reads open ADRs, Steward emits an audit row).
  7. Resume scout cadence via F26 Ticker.
Estimated recovery window
Half-day to one day if MCP re-auths are all interactive and there are no admin-consent surprises. Note (Charlie 2026-05-31): the previously proposed quarterly wipe-drill dry-run on a spare device or VM is retired — Charlie cannot test laptop wipes in his environment. The playbook is now exercised organically only if/when a real recovery is needed.
Two tracks
  • Track 1 — same laptop, fresh session (“where was I?”): Clawpilot restarted but disk intact. Check active sessions, re-bootstrap if peers died, read latest plan.md, resume. ~15 min.
  • Track 2 — new laptop, cold start: Full Step 1–7 recovery. Re-auth in canonical order (Entra → GitHub → WorkIQ/M365 → MCPs → DAB → bootstrap-roles → Ticker). Half-day to one day.
Source-of-truth in Azure storage
The cognitive corpus (role briefs, skills, docs, workflow configs) syncs to a single Azure Storage account (stclawpilotcorpusuks, ZRS, UK South, Entra RBAC only, no SAS tokens) via the corpus-sync skill. Three containers: briefs, skills, docs. Blob versioning with 14-day soft-delete retention provides point-in-time rollback. Conflict semantics: ETag/If-Match on every PUT; 412 → skip + warn (never auto-overwrite). Corp-net reachability for the blob endpoint should still be empirically tested before provisioning — it has never been verified either way on Charlie's network. (The earlier "by analogy to the Loop Fluid relay block" framing has been retracted; that block was a one-day issue on a different network, not a property of Charlie's corp environment.) Internal docs hosted via Azure Static Web Apps; auth scope open — anonymous public docs vs Microsoft-tenant-only Entra auth, Charlie to pick before docs hosting is provisioned. Per SA-ADR-F27-STORE and SA-ADR-F27-DOCS, both ratified 2026-05-31 (DOCS ratified with auth-scope question still open).
Sync-as-a-skill with bootstrap stub
The sync mechanism is itself a Clawpilot skill (corpus-sync) with a ~50-line bootstrap stub. On a bare laptop, the stub is fetched from a hard-coded blob URL (https://stclawpilotcorpusuks.blob.core.windows.net/skills/corpus-sync/bootstrap-stub.ps1), authenticates via Entra, pulls the full skill manifest, and invokes corpus-sync pull to restore the entire corpus. Push-on-edit propagates changes to blob storage with ETag conflict detection. Post-F26, Ticker runs corpus-sync push daily at 23:00 UTC as a drainer job (ratified 2026-05-31, gated on F29 + storage being live).
Bootstrap-stub URL survival channel (Charlie ruling, 2026-05-31)
A1 (hard-coded blob URL stub) PRIMARY + A2 (bundled in Clawpilot installer) FALLBACK — both accepted. The URL itself, plus the broader “where everything lives” inventory, lives on a dedicated Loop notebook continuity page (“if you lose your laptop, start here”) on the engagement's Loop notebook — Entra-authenticated, server-side, reachable from phone / web / second laptop, and reachable on Charlie's corp net. (The earlier "corp-net blocks Loop Fluid sync" assumption was wrong — that block was a one-day issue on a different network, not a property of Charlie's corp environment. Tether/mobile-data workaround retracted.) The Loop continuity page is the primary survival channel. An optional OneDrive clawpilot-recovery.md mirror under Charlie's UPN may be maintained as defence-in-depth duplicate — Charlie may keep it or not; it is no longer required as a Fluid-block workaround. Wave-1 build (31 May 2026): attempted in the lab sub ME-MngEnvMCAP180885-smccormick-1 and dropped per §G #2a optionality; the Azure CLI first-party app has no Files.ReadWrite consent against the active tenant (AADSTS65002 risk realised; managed-environment admin UPN, not Charlie's smccormick@microsoft.com identity); the recovery-card content is already baked into corpus-sync/bootstrap-stub.ps1 + SKILL.md (Item 6 verified), so the OneDrive duplicate adds no unique survival value beyond the F29-deferred Loop continuity page. Loop continuity page is a deliverable artefact (deferred 2026-05-31 to F29) — page name, content sections, and maintainer pending under the F29 wiring work.
Acceptance decision (Charlie 2026-05-31)
During the recovery window, signal collection halts entirely (no scouts, no audit writes, no briefs) — accepted. No parallel always-on instance on a second device.

F29 Generic Loop publisher skill + per-engagement Loop wiring

🗄 RETIRED 2026-06-02 — Loop reporting pivot. The peer-ring is pivoting from Microsoft Loop as the reporting surface to a purpose-built web reporting front end (tracked as W10). The original ship is preserved below as audit-chain provenance, but is no longer the active delivery path. BUG-001 (always-allow non-persistence) was the proximate cost driver across W7 + W8; the web-app deploy path removes that dependency.
P1 Effort: S shipped CoS SA

Shipped 2026-06-01 (Wave 4, Round 5c-4) — live-proven against Charlie's real Haleon Loop notebook with round-trip verification + idempotency proof. Pack-schema extension: m-skills/_schemas/pack.schema.json extended with the F29 5-key set — loop_notebook_url, loop_front_page_url, loop_continuity_page_url, loop_status_page_url, loop_pages (map) — all optional, sentinel-pattern ^(https://[^\s]+|pending:[a-z0-9-]+)$ to admit a pending:charlie-input placeholder. Bootstrap brief's loop_notebook_section_id minimal-pair key dropped per SA reconciliation (not in F29). $schemaVersion held at 2 (additive, all-optional — backward-compat). Validator (jsonschema Draft7): haleon/pack.json PASS pre-flip; post-flip with loop_notebook_url: pending:charlie-input PASS; negative control ftp://… FAIL. Engagement-pack patch: m-engagement-packs/haleon/pack.json += loop_notebook_url: pending:charlie-input; awaits Charlie's real per-engagement Loop URL (front + continuity + status + archive map optional). IA templates: m-skills/loop-publish/templates/overview-template.md (5672 B, weekly-review cadence) + daily-template.md (8573 B, strict order: pulse → 5-3-2-1 brief → top-5 risks → recent ADRs → links → archive). Six DB queries spec'd against live haleon entities (CustomerHealth, Brief, RiskIssueBlocker, Adr, Decision filtered decision_class='use_case'); template column-set re-verified against live DAB read 2026-06-01 (customer_health uses narrative/period_end/computed_at/sentiment_*, not the earlier-drafted state/as_of_date). Skill artefacts: m-skills/loop-publish/{SKILL.md (107 L), loop-publish.ps1 (440 L), config.json.example}; CLI shape --engagement <name> --section <id> --content-source <brief|adrs|risks|status|links|full> [-WhatIf]; render via {{key}} substitution (no Jinja2 dependency); drift-gate via sha256 of normalised body vs decisions tagged loop_publish_state (per (engagement, content-source, page-url) tuple); audit via direct SELECT haleon.sp_create_agent_run(…) with session_turn_seq auto-allocated by sp_next_session_turn_seq. End-to-end dry-runs against live haleon DB (Playwright path intentionally not invoked — no PROD/TEST Loop URL yet): all 6 content-sources rendered + audited — brief (dd1f17ef-f551…) 1603 chars; adrs (efa37c7d-92c0…) 71 chars graceful-empty; risks (1de27354-5407…) 410 chars 2-row table; status (99640788-c630…) 217 chars derived-green; links (1bd81530-2d9c…) 369 chars + re-run idempotency proof (ddcad7c8…); full (089492cc-1826…) 314 chars. Real haleon-pack gate-proof: blocked: loop_notebook_url is pending:charlie-input + audit (183a1980-f465…) — honest design-pending-apply state, non-zero exit. Consumer hooks (W4-4): m-role-briefs/CHIEFOFSTAFF.md += brief-write → loop-publish --content-source brief handoff (after sp_upsert_brief close-out); SOLUTIONARCHITECT.md += ratify → --content-source adrs on successful sp_advance_adr_stage(…'ratified'); STEWARD.md += --content-source status on scout-sweep/health-tick close + after class='audit' writes whose triggered_by starts with kernel-purity-scan: / gap: / open:; LIBRARY-CURATOR.md += --content-source links at end of each scan turn. All four briefs carry the pending:-skip rule + drift-gated double-invocation safety note. Live publish probe 2026-06-01 — Fluid Framework sync fail confirmed: haleon pack.json:loop_notebook_url backfilled with Charlie's real Haleon Loop notebook URL (validator PASS against extended schema). End-to-end Playwright probe under Charlie's identity: navigated to workspace OK (sidebar loads, existing Status system page renders), new-page creation routed to a fresh page id (d69b3a6b-8f00-4813-9623-41ac901dce8e), but Fluid Framework content sync FAILED immediately with the dual symptom Charlie himself observed on 2026-05-29 (Brief Insight #5): persistent banner "Sorry, we couldn't save your work" + console error "Unattached workspace is not tracked by workspace tracker". URL prefix decodes to sharepoint-df.com (SharePoint Dogfooding tenant — likely the relay-reachability root cause from corp network/auth context). Failure is session-wide (persists across workspace switch to Tester Notebook), not Haleon-specific. Evidence: scratchpad/loop-fluid-sync-fail-1.png + loop-fluid-sync-fail-2-tester-notebook.png. Audit row e9832d0c-e291-402e-ac78-a69c41e7ac12 with triggered_by shape per F29 spec (result=fluid_sync_fail). Material implication: the 2026-05-31 retraction of the Fluid-reachability worry was premature; F29's "graceful Fluid-sync error handling (defensive engineering, not Charlie-specific)" turns out to be the primary mode, not a defensive edge case. The loop-publish skill's audit-emission path already encodes this contract (result=fluid_sync_fail as a documented audit verdict) — the still-TODO Playwright wiring needs the detection logic for this failure shape (banner text + console substring + relay WebSocket fail) baked in BEFORE first happy-path publish is achievable. Live publish proof 2026-06-01 (Round 5c-4) — round-trip verified, idempotency proven, F29 SHIPPED: Charlie supplied the canonical Haleon `/ws/` workspace URL plus pre-decoded loop_workspace_drive_id (b!o26ka_B3LEKYc2UHaB1H-49Q45g7fgtLjfqcCQGCwJdRKwQdXY-qQolwRJ9pnARj) + loop_workspace_item_id (01J62HDHFL4DXI24KPHNDJS47GH7IWHIC2); all three backfilled into haleon/pack.json (validator PASS against tightened R5c-2 schema). End-to-end Playwright publish probe under Charlie's identity (Edge persistent context): navigated to the `/ws/` URL cleanly — no "Oops!" modal, no "Unattached workspace" console error, no save-failure banner (every R5c session-poisoning guard signal NEGATIVE). R5c-6 orphan cleanup pass: enumerated the Haleon workspace page tree, deleted three orphan "Untitled" pages from the 5b broken runs (audit rows 36d277ce-aa18-4e2b-985d-e625ac60bce9 + 36fda644-b5de-480c-badd-e114a7cf5c1b + 53fd036e-d06e-415d-89c0-539bf2fb7b1f; the 3rd was likely the d69b3a6b-8f00-4813-9623-41ac901dce8e page-id from the broken 5b run). Resolve-or-create flow: created a fresh workspace page, IMMEDIATELY set deterministic title "Clawpilot smoke-test — DO NOT EDIT (2026-06-01)" (R5c-6b invariant: no codepath in loop-publish.ps1 can produce an "Untitled" page itself; the deterministic title-set step is what protects against orphan creation if interrupted), typed automated body content via Loop's contenteditable editor, "Connected to Haleon" indicator visible. Round-trip verification (the necessary-but-not-sufficient guard R5c-1 introduced): computed body_sha = 2448bfbd695777eff82103831c7f98bdc253c820d8d94d4c92986eff862e90d3 pre-reload; performed FULL PAGE RELOAD via page.goto (forced re-fetch from storage farm, not local Fluid cache); re-read title + canvas innerText; recomputed readback_sha = 2448bfbd695777eff82103831c7f98bdc253c820d8d94d4c92986eff862e90d3EXACT MATCH. First-publish audit row 75fb5af8-3e94-4bfc-8e2e-f2b72d64250e emitted with result=ok;round_trip=verified. Idempotency proof: immediately re-evaluated rendered sha against persisted sha — matched → result=noop, no Loop re-edit performed. Second-publish audit row 0e7b0dc2-8797-43f6-b80c-9c65a1681d9f emitted with result=noop;idempotent=true;no_re_edit=true;prior_run_audit=75fb5af8-3e94-4bfc-8e2e-f2b72d64250e. Screenshots: scratchpad/loop-publish-r5c4-prereload.png + scratchpad/loop-publish-r5c4-postreload-VERIFIED.png (both show the smoke-test page rendered cleanly in Haleon workspace). F29 is now genuinely shipped: artefact exists in Charlie's real Loop notebook AND the audit chain has tamper-evident proof of round-trip + idempotency. The 5b retracted diagnosis (Fluid sync fail) and the 5c correction (dead-page session-poisoning) are preserved above as chain-of-custody for the audit trail; the R5c-1 hardened result-classification (dead_target_page / poisoned_session verdicts + R5c-6 orphan cleanup pass + deterministic-title invariant) means the next run won't reproduce either symptom even if Charlie deletes the smoke-test page out-of-band.

Turns Charlie's Microsoft Loop notebook into a first-class engagement surface: a project front page that receives ongoing status updates, a dedicated continuity page that carries the F27 bootstrap URL + “where everything lives” inventory, and any other notebook pages an engagement chooses to wire in. Generic by design — the mechanism is kernel, the targets are per-engagement.

Why
Charlie's working pattern uses a per-engagement Loop notebook with multiple pages (front page, docs links, database links, key resources, recurring meetings). Status updates and continuity information naturally belong there — Loop is Entra-authenticated, server-side, laptop-independent, reachable from phone / web / second laptop. Hard-coding any Loop URL into a kernel skill would be worse than useless because the framework runs against multiple engagements; each must point at its own notebook.
What gets built
  • Kernel skill loop-publisher: generic. Inputs: target page URL + content payload. Uses the existing /loop Playwright skill under the hood (or extends it). Knows nothing about any engagement.
  • Engagement-pack schema extension (L2 per F6): pack.json gains new optional keys — loop_notebook_url, loop_front_page_url, loop_continuity_page_url, loop_status_page_url, and an open-ended loop_pages map for engagement-defined extras. SA owns the schema; new engagements supply the URLs at mobilise-engagement time.
  • Consumer hooks: daily-brief reads loop_front_page_url from pack, calls loop-publisher to publish/refresh on each brief cadence. F27 continuity content reads loop_continuity_page_url, populates once + refreshes on edits to the continuity inventory. (No periodic refresh cadence — the F27 quarterly wipe-drill cadence was retired 2026-05-31.)
  • Graceful Fluid-sync error handling (defensive engineering, not Charlie-specific): loop-publisher detects Loop Fluid Framework sync failures (WebSocket failures to *.fluidrelay.azure.com, the “Sorry, we couldn't save your work” / “Oops! moved or deleted” symptoms) and fails gracefully with a structured warning routed to Charlie via Steward-DM rather than failing silently. Note 2026-05-31: Charlie's corp environment does not block Fluid as previously assumed; this is general engineering hygiene against any Fluid failure mode, not a Charlie-specific mitigation.
  • Audit row per publish: class='audit', triggered_by='loop_publish:page=<short-name>;result=<ok|fluid_sync_fail|fail>' — visibility into what got refreshed when.
Onboarding workflow
When a new engagement is mobilised: Charlie (or the operator) creates the engagement's Loop notebook + the standard pages (front, continuity, status), copies their URLs into pack.json, and the peer-ring picks up automatically. No kernel change per new engagement.
Out of scope (deferred)
Loop notebook creation itself — remains a manual one-off per engagement. loop-publisher only writes to pages that already exist.

F28 Approvals-response capture via Graph poll

retired 31 May 2026 (moot — tied to F9)

Dead. F28 was the response-capture half of F9. With F9 retired (consent unobtainable), nothing to poll. Folded back. Sign-off responses arrive via Steward-DM in the existing Teams chat path — CoS reads the reply, applies the CAS state transition through the existing sp_* contracts.

P2 Scale-out & portfolio layer — 5 features. Need the P0+P1 foundation in place first. (F9 + F28 originally lived here; both retired 31 May after Approvals feasibility test.)

F10c Curator surfacing-to-SA loop (v2)

P2 Effort: L design-pending-corpus SA

Closes the find → store → surface → reuse loop. Library Curator surfaces ranked "you've solved this before" hits to SA when SA drafts a new ADR. Defer until pattern_library has enough content to be worth surfacing from.

Wave-6 DESIGN-PENDING-CORPUS 2026-06-01: NOT shipped — surfacing loop deferred until pattern_library holds enough curated rows to surface from. The gating mechanism is live: portfolio.pattern_library_surfacing_gate + sp_eval_surfacing_gate (threshold N=5) are committed and the bidirectional below/above-threshold flip is probe-proven, so F10C auto-fires in W7 the moment pattern_library crosses the threshold. A live gate under a deferred surfacing loop — not a promise.

What gets built
  • Embedding-based retrieval over pattern_library.
  • SA on new ADR draft: library-search skill returns ranked hits with relevance scores.
  • False-positive control: only surface above a confidence threshold.
  • Both as an ADR-drafting auto-prompt and as a manual library-search skill.

F15 Engagement lifecycle ceremonies (mobilise / transition / closeout)

P2 Effort: M shipped Steward CoS

Three new Steward-owned ceremonies analogous to existing phase-boundary. Together they form one engagement-lifecycle playbook.

Wave-6 SHIPPED 2026-06-01: The two NEW lifecycle ceremonies (transition-engagement, closeout-engagement) are live-proven in rehearsal against <engagement> — runnable v0.1 skeletons executed end-to-end (transition pack + signed closeout bundle emitted), with CoS content shapes (runbook structure, pod-map, relationship-handoff, lessons-harvest schema, qualification gate) folded into v0.2. Spine refs: transition 21fab3d4, closeout 231e3b40. Caveat: NEW ceremonies only — the historical mobilise-engagement (F11) is untouched.

Three ceremonies
  • engagement-mobilise — runs at engagement start. Invokes F11 mobilise-engagement. Writes a structured kickoff artefact (engagement charter, pod map, MCP topology).
  • engagement-transition — runs at in-flight architect handover (e.g., Pilot SA → Scale SA). Produces a transition pack: read-only mirror of engagement DB, runbook, ADR index, open-decisions list, active-risks export.
  • engagement-closeout — runs at engagement end. Lessons-learned harvest feeds F10b Library Curator. Output is the customer-facing handover bundle (F21).
Bus-factor wiring
Backup-architect MCP identity provisioned via F11. bootstrap-roles can spawn the ring under deputy credentials for transition rehearsals. (Note: no live deputy per Charlie's ruling — this is identity infrastructure, not auto-failover.)

F17 Portfolio read view + cross-engagement dashboard

P2 Effort: M shipped Library Curator CoS

Cross-schema views and a portfolio Power BI dashboard for Microsoft delivery leadership. Activates when engagement count reaches 2+.

Wave-6 SHIPPED 2026-06-01: Cross-engagement portfolio read-view live-proven against TWO engagements via a registry-driven external aggregator (Option C — no cross-DB FDW: per-engagement readers → local portfolio.* snapshot tables → v_portfolio_* views). Probe confirmed ≥2 engagements with both activity rows genuinely populated. Refs: aggregator run decdfa02, SA seal d8fe0fa4, decisions fc4906e1. Caveat: the view/aggregator tier is what ships (live-proven) — the Power BI workspace publish is design-only / won’t-implement (permanent; not pending-apply, not a W7 build). W7 workaround: the cross-engagement portfolio metrics fold into the 5-3-2-1 brief in the Loop notebook instead of a standalone Power BI dashboard.

What gets built
  • v_portfolio_* view set in the portfolio schema, projecting only portfolio-shareable rows + rollups across all engagement schemas.
  • Power BI portfolio view: top reuse assets, compliance-regime heatmap, scout health per engagement, cost burn per engagement, ADR ratification rate per engagement.
  • Engagement filter + portfolio rollup.
  • Published to a Microsoft-internal Power BI workspace with controlled access.

F19 Reuse outcome measurement

P2 Effort: S shipped SA Library Curator

Closes the loop on Reuse Scout's value: track when a candidate is actually re-used. Feeds scout precision tuning and the portfolio "top reuse assets" view.

Wave-6 SHIPPED 2026-06-01: Reuse-outcome measurement shipped — reuse_catalog gained measurement columns (including portfolio_shareable) plus v_portfolio_reuse_outcomes. A synthetic reuse-hit produced a measured outcome_bucket='adopted' row visible through the F17 portfolio view, then cleaned up. Ref: aggregator run decdfa02.

What gets built
  • reuse_candidates gains: actioned_outcome (text), reused_in_engagement (text), time_to_reuse (interval), effort_saved_estimate (qualitative tier — small/medium/large).
  • SA records the reuse when picking up a pattern for a new ADR.
  • Library Curator aggregates into the portfolio "top reuse assets" tile.
  • Reuse Scout tuning loop: precision = actioned-and-reused / surfaced.

F21 Customer-facing compliance-evidence export (signed PDF + Library extract)

P2 Effort: M shipped Steward CoS

Per Charlie's ruling: closeout artefacts are always handed back to the customer. This is the customer-facing closeout bundle.

What gets built
  • compliance-export skill bundles: ADRs (ratified, with body), engagement-scoped decisions, comms_drafts (sent only), agent_runs metadata (with hash-chain attestation per F3).
  • Default destination: signed PDF + Library Curator extract. Signed PDF for the customer hand-back, Library extract for internal Microsoft pattern accretion.
  • Alternative destinations pluggable per engagement: Purview, SFTP drop, custom.
  • Hash-chain attestation gives the customer cryptographic evidence the bundle is unmodified.
Closeout coupling
Invoked by the engagement-closeout ceremony in F15. Output drops into the customer-handover process.

W9-A.F21 SHIPPED 2026-06-02: Signed compliance-evidence handback bundle for the bound engagement <engagement> (engagement_code placeholder; on-disk pack is the dev/test env). Bundle w9-f21-handback-bundle.zip (12 920 B, sha256 4d2c2ae3…43a9) contains PDF + library-extract.json + manifest.json; library-extract has 33-ADR index + chain-snapshot pinned to row 5c77ad80… (row_hash d3db63f7…dec36 @ 283 rows, re-verified live). Composed by CoS (compose audit 5c77ad80-7007-4a4f-b28e-c2df53698519). Steward signed the zip with KV haleon-attest-priv-pem (RSA-2048, PSS / SHA-256, salt=MAX → 256-byte detached sig); verified end-to-end with KV haleon-attest-pub-pemVERIFY: OK. Tamper-probe (single-byte flip) correctly rejected on both signed artefacts — signatures genuinely bind content. 3-section content spot-check passed (zip listing, library-extract structure, PDF %PDF-1.4 header + %%EOF trailer); generic-framing intact (no customer-name leak; "loop" hits are historical-ADR titles, not Loop-build leaks). W8 mirror-genesis seeding gap-note folded in same signing pass: mirror-genesis-gap-note.md (1 701 B, sha256 d9196bc2…b648) separately signed + verified + tamper-probed OK; gap documented, NOT applied (mobilise-engagement edit is a separate substrate item). No mirror push — retrievable-from-laptop-disk + sig-verify + content spot-check is sufficient per parent steer. Audit run 2e71be6d-8c50-4e62-ba1c-14119bce2fe7 (two-phase; detail_ref = CoS compose run per soft-FK; sp_verify_chain OK, chain head @ 286 rows) · decision fe45f5f3-6db2-447d-8ce3-fabd36fb3a7f.

P3 Roadmap hygiene — 3 features. Cheap, defensible, do when convenient.

F23 Unsurfaced-integration scan (one ADR with YES/DEFER/NO rulings)

P3 Effort: XS shipped SA

Single ADR with a YES/DEFER/NO ruling per candidate integration. Stops "should we use X?" being a recurring open question.

Wave-5 SHIPPED 2026-06-01: SA-ADR-F23 ratified via parent delegation under Charlie's pre-delegated authority (adrs d653cde5-3fde-4bf8-88d4-a5502df0ea87, m-policies/adr/SA-ADR-F23-UNSURFACED-INTEGRATIONS.md). 20+ candidates across 7 sub-categories; machine-readable companion m-policies/adr/integrations.yaml + ADR INDEX.md registration delivered. 8 reviewer + 6 compliance asks resolved across two-round substantive gate (substantive re-gate audit 47d10387-ccb3-440e-8441-5a8456a47a69). Override mechanism declared inert pending integration_overrides pack.json schema follow-up (W6+ residual).

Rulings
  • YES: Power BI (via F14/F17), M365 Forms (as alternate intake channel)
  • DEFER: Viva Insights, GitHub Issues, Slack (pending non-Microsoft engagement), Jira (pending non-Microsoft engagement)
  • NO: Project, Planner (consistent with ISS-21), M365 Approvals (per 31 May feasibility test — ApprovalSolution.ReadWrite requires admin consent, unobtainable; see retired F9)
Override policy
Engagement-pack can override a DEFER → YES locally (e.g., a Jira-shop engagement upgrades Jira). Cannot override a NO. Re-review cadence: yearly + on any new engagement stack-mismatch.

F24 Operator-in-spine clarification (ADR + spine-member with class='operator')

P3 Effort: XS shipped Steward SA

Today the Operator (Charlie's hands-on session) performs DB writes via Charlie's hands but emits no agent_runs rows. This is defensible but silent. Per Charlie's ruling: make Operator a spine-member with class='operator'.

Wave-5 SHIPPED 2026-06-01: SA-ADR-F24 ratified via parent delegation (adrs b505985c-f60d-407f-b6a1-9b8ef2122a02, m-policies/adr/SA-ADR-F24-OPERATOR-IN-SPINE.md). New class='operator' + tier='operator' in legal set; non-Clawpilot fallback session_id='operator:adhoc:<utc-iso>'; gap-scan sourced from pg_stat_statements joined to F20 quarterly drill; HS-10 cost-telemetry rollup exclusion + daily integrity-check probe; SchemaMigrationIn row on CHECK-constraint ALTER. 11 reviewer+compliance asks verified across initial + procedural re-gate (re-gate audit d3daf75a-bf3c-447b-81f8-3c6a2ab9839c). Pairs with F25 for full integrity-and-durability triad with F3.

W9-A re-apply SHIPPED 2026-06-02: The Wave-5 claim above presented the SA-ADR-F24 §3 schema delta as shipped, but a W9-C.R9 substrate audit found the live schema contained neither class='operator' nor tier='operator'. Under operator authorisation + ratified SA-ADR-F24 (adrs b505985c-f60d-407f-b6a1-9b8ef2122a02), Steward applied the §3 DDL for real: (1) agent_runs_class_check DROP+recreate → 10 values incl operator; (2) ALTER TYPE haleon.agent_tier ADD VALUE 'operator' (autocommit) → enum_range = 8 values; (3) CREATE OR REPLACE haleon.sp_rollup_cost_telemetry adding operator to the HS-10 cost-rollup exclusion (delta-probe: synthetic operator row included under old filter=1, excluded under new=0). Each DDL emitted its own SchemaMigrationIn row (no-silent-DDL invariant). Verification: synthetic class='operator'/tier='operator' insert via sp_create_agent_run succeeded (proves acceptance); row retained + documented as the first operator-class genesis row because agent_runs is a hard append-only hash chain (role grants INSERT/SELECT only, no DELETE). Correction-class audit run 4b8fc0a8-02ae-4b31-b173-3a0f93177712 (two-phase; sp_verify_chain OK @ 288 rows) · decision d2d38882-9403-40a2-a493-511de39c3f5c. Generic-process framing (bound engagement <engagement>).

What gets built
  • ADR documenting the choice and rationale.
  • ALTER agent_runs_class_check to include 'operator' in the legal class set.
  • Operator session emits class='operator' rows for meaningful actions (DDL applies, manual scout_enable_flags flips, Approvals overrides).
  • HS-10 cost-telemetry rollup exclusion list extended (Operator actions are governance, not delivery cost).
Pairing with F3
The integrity story is incomplete until the spine answers "who wrote this and what class" for every mutation, including human-driven ones.

F25 ADR: "Audit spine substrate is non-durable by design"

P3 Effort: XS shipped Steward

Captures Charlie's 30 May 2026 DR-not-needed ruling as a deliberate, auditable position. Closes the silent posture; an auditor asking "where's your DR plan?" gets a coherent answer rather than a gap.

ADR position
The engagement substrate is reproducible from M365 / ADO / MSX / GitHub primary sources. The hosted DB is an accelerator + integrity-attested evidence trail (per F3), not a system-of-record requiring disaster recovery. Loss of the hosted DB is recoverable from primary sources; loss tolerance is “unbounded” (not “not applicable”) — the system functions, just with rebuilt history.

Wave-5 SHIPPED 2026-06-01: SA-ADR-F25 ratified via parent delegation under Charlie's pre-delegated authority (on-disk at m-policies/adr/SA-ADR-F25-AUDIT-SPINE-NON-DURABLE.md; adrs substrate row written in parallel by wave5-steward). Primary-source registry covering 16 state classes + closure clause for unregistered tables; Operator-authored chain-of-custody rebuild ceremony per F24; canonical triggered_by shape spine_rebuild:engagement=<name>;prior_chain_head=<sha>;replay_horizon=<iso>;genesis_v=<n>; explicit unsuitability for HIPAA/SOX/GDPR/FINRA-retention-bound engagements with F11 mobilise-time gate. 8 reviewer+compliance minor asks + Steward §4(a/b/c) substrate-accuracy corrections folded across initial + procedural re-gate (re-gate audit 9cf1fde4-3f82-4a04-b269-8e3cfc66b683). Completes the integrity-and-durability triad with F3 + F24.

W7 Wave 7 — CLOSED 2026-06-01 — 5 SHIPPED (F30, F33+W7-D.1, F35, R7, rolling-block SOP) + 2 PARTIAL (F32, F34, design-complete · live-Loop pending operator-attended session). F31 & F36 deferred to W8.

Wave-7 close 2026-06-01 ~20:43 BST. Chain 227 → 255 rows (Δ28), sp_verify_chain() OK throughout. Cost £0/mo marginal preserved across all infra. Substrate fixes (signing-key null-check, enum extensions, MCP-name rename, etc.) are tracked as fixes in the wave ledger, not as roadmap features. Priority badges per article; they will be folded into the P1/P2 groups as they are scheduled. W8 picks up: Step-0 cleanup of F32/F34 live publishes (browser-perm patch may have landed; verify), plus F31 (use-case tracking, DB schema home per Charlie steer), F36 (recognition tracker), F21 (signed evidence export).

Shipped this wave (rolling)

Live-progress block, updated in the same swa deploy as every per-article flip. Audit UUIDs + Loop links inline; substrate fixes (R-numbered) also tracked here without new F-numbers.

  • F30 — W7-A 5-3-2-1 header metrics — live 2026-06-01 18:15Z · audit 62c74ff8-e219-4f92-b280-a93c90e2db14 · Loop page W7-A Daily Brief (dev/test) live in Haleon dev/test workspace.
  • 🟡 F32 — W7-C Loop templatespartial design-complete 2026-06-01: all 7/7 templates committed to ~/.copilot/m-skills/loop-publish/templates/ (generic <engagement>); 1/7 (daily-status) previously live in Haleon dev/test; remaining 6 live-Loop instantiation deferred to operator-attended browser session 2026-06-02 (BUG-001 overnight constraint) · audits 57ca7614-aa42-45f8-8e4d-3f1ea04bbd51 (earlier live publish) + 0e578f7f-97c3-44ab-8bbd-3ccd0d11abf2 (W7-C/W7-E design-commit).
  • 🟡 F34 — W7-E survival pagepartial design-complete 2026-06-01: engagement-survival template (retention=4 leaf-class) + Haleon survival-page markdown instance committed to session files; live Loop publish + read-back deferred to operator-attended browser session 2026-06-02 (BUG-001) · audit 0e578f7f-97c3-44ab-8bbd-3ccd0d11abf2 (shared with F32).
  • R7 substratekernel-purity-scan/run.ps1 JSONL signing-key null-check; fail-fast with clear vault+secret name on null KV fetch · audit 0e4a214c-7b18-4e8c-a9c5-ec597f5e8932.
  • 🔁 CoS rotation — W7-CoS predecessor (3a671050) content-filter-degraded; successor (0fa685cf) online via ZOMBIE-path ledger reconstruction · rotation_log bd3bd5c6-3e37-43f9-a4eb-c3852c41f996 · snapshot 0fc4031a-509f-4a18-8cef-437d9c2484f6 · spine row 7e31720d-2348-4b1d-aa04-b38235cbdba2.
  • F33 — W7-D Loop governance ADR — ADR ratified 2026-06-01 · intent-ratify audit 0a41d6ff-3a12-4809-9c8c-e4feea7920e1 · decision 7b46cd41-1790-4260-b180-eaddd1ea39c8 (+W7-D.1 impl-depth ratify: ADR f02af754-0ad1-4c3d-a0d7-a5e463c9181f, decision 28efcc82-156d-4e6b-9542-63974788a996, audit edbc8656-8f49-4280-8476-46781e6cd175).
  • F35 — W7-F durable corpus sync — Container Apps Job caj-clawpilot-corpus-sync (CAE cae-clawpilot-uks Consumption uksouth, schedule 0 2 * * * UTC) + 2 UA-MIs with custom role Storage Network Flipper (corpus) + watchdog Logic App la-clawpilot-corpus-watchdog (three-layer close); probe-pass 0b81ce9d-b89c-4c84-905a-e7baa9aad64b (open_window_ms=39000); Phase 6 SWA-mirror regen 5d0230d9-8f09-4c36-a38d-bbc10adc6067 · live mirror https://jolly-dune-00bde4103.7.azurestaticapps.net/ · cost £0/mo marginal.
  • 🔄 GH→Azure runtime pivot — EMU policy blocked GitHub-hosted runners (audit 8a1d976e); pivoted to Azure-native Container Apps Job runtime · probe-pass audit 0b81ce9d · orphan GH workflow preserved with archival banner at commit 2db30dca for provenance.

F30 Portfolio metrics in the 5-3-2-1 brief header

🗄 RETIRED 2026-06-02 — Loop reporting pivot. Charlie's steer 2026-06-02: "metrics don't have to live in the 5-3-2-1 report". Metrics migrate to a richer surface on the W10 web dashboard (per-engagement + portfolio-wide), unconstrained by the brief-header pill row. Original W7-A ship preserved below as provenance.
P2 Effort: S shipped CoS Steward

Replaces the F17 Power BI publish path (ruled design-only / won't-build). Cross-engagement portfolio metrics are folded into the header of the daily 5-3-2-1 brief instead of a standalone BI surface.

What gets built
  • The daily-brief skill gains a header block sourced from the shipped F17 portfolio views + aggregator (v_portfolio_*).
  • Header answers a real cross-engagement question (e.g. open-ADR counts, reuse adoption) at generation time.
  • Rendered onto the engagement's Loop brief page; generic <engagement> framing.

W7-A SHIPPED 2026-06-01 18:15Z: loop-publish.ps1 brief-source flow gains a portfolio header strip rendered above the empty-bundle guard (KPI pill row + freshness chip). Cross-engagement aggregates source the shipped F17 portfolio.v_portfolio_* view tier (engagements_active, open_adrs, runs_24h/7d, reuse_hits, burn_day) + aggregator status (partial 2/2 @ 15:21Z staleness guard so a stale snapshot never reads as a real zero). Live-published to the dev/test Haleon Loop workspace (page title W7-A Daily Brief (dev/test)) and round-trip read-back confirmed — all 6 metric values match the direct SELECT exactly. Audit 62c74ff8-e219-4f92-b280-a93c90e2db14 (live publish); companion audits e3966262... (code change) + 86b494b7... (discovery). Seal lag W7-A→F30 = ~2hrs (live publish 18:15Z → article seal 19:0XZ); cause: orchestrator did not chain seal-immediately on ship confirmation. Fix: shipped-rolling-block at the top of W7 + per-ship seal-on-confirm SOP, effective W7+.

F31 Use-case-level tracking (one level below engagement)

P2 Effort: M shipped SA Steward

A tracker at the use-case granularity for a given engagement — the same shape as engagement/project tracking, one level down.

What gets built
  • W8-A scope (Charlie steer 2026-06-02): tracking lives in the DB schema (portfolio. + per-engagement haleon.) — ADO and Loop reporting layers can be added later if needed.
  • Per-use-case state model mirroring the engagement-level lifecycle (lifecycle table + state-transition trigger + audit-row class extension).
  • One use-case tracked end-to-end under a dev/test engagement with at least one recorded state transition (live SELECT proves the row + transition).

F32 Loop page templates + defined hierarchy

🗄 RETIRED 2026-06-02 — Loop reporting pivot. The 7 template contents (overview, daily-status, weekly-rollup, steerco-prep, ratified-ADR-digest, incident-postmortem, engagement-survival) committed to ~/.copilot/m-skills/loop-publish/templates/ are recyclable as the canonical report-type catalogue input for W10. The Loop delivery layer dies; the content schema lives on.
P1 Effort: S partial CoS SA

If we publish Loop reports daily they must be standardised. A template per page type and an explicit page hierarchy.

What gets built
  • Templates for: front-page / overview, and each recurring report type.
  • A defined page hierarchy (which page parents which).
  • Each template instantiated as a real Loop page in the dev/test notebook and read back; generic <engagement> placeholders.

W7-C PARTIAL 2026-06-01 (design-complete; live-Loop instantiation deferred to 2026-06-02 operator-attended browser session): All 7 templates (front-page overview, daily-status, weekly-rollup, steerco-prep, ratified-ADR-digest, incident-postmortem, engagement-survival) committed to canonical home ~/.copilot/m-skills/loop-publish/templates/ as W7-D-ratified design files with rule-aware regions (R1 task-state isolation, R2 archive-on-replace slot, R3 /ws/-only links, R4 hash-stable content-section, R5 last_published_content_hash footer, R6 leaf-class retention metadata, R8 CAPS-as-headings); generic <engagement> placeholders; cross-cutting chrome conventions (top HTML-comment metadata block, MANIFEST + AUTHOR NOTES split footers, branch-distinct box-drawing weight); daily-status previously instantiated live in Haleon dev/test workspace (audit 57ca7614-aa42-45f8-8e4d-3f1ea04bbd51). Audit chain: rotation 7e31720d-2348-4b1d-aa04-b38235cbdba2 · design-commit 0e578f7f-97c3-44ab-8bbd-3ccd0d11abf2. Overnight BUG-001 constraint (browser-MCP re-prompt; operator asleep) prohibits the 5 remaining live Loop instantiations + the round-trip read-back from completing tonight. Flips to shipped on (a) all 7 templates live in dev/test Loop with read-back and (b) SA review-checklist PASS across all 18 checks per template.

F33 Loop governance rules (when a page may update)

🗄 RETIRED 2026-06-02 — Loop reporting pivot. ADR-W7-D + ADR-W7-D.1 contained Loop-specific rules (R1 task-state isolation, R3 /ws/-only links, R5 publish-cursor CAS, R6 leaf-class retention, R8 CAPS-as-headings) that are moot on a purpose-built web surface. A few principles transfer to W10 (content-hash idempotency, archive-on-replace). The ratified ADRs remain on disk as historical provenance; do not delete.
P1 Effort: XS shipped SA CoS

Rules for Loop page updates. Charlie's lean: a page updates only when its assigned tasks are complete; newly-created reports archive-and-replace the old ones. Charlie is explicitly open to push-back — SA adjudicates and Charlie ratifies.

What gets built
  • An ADR capturing the update / archive policy (routed through the Reviewer gate).
  • Enforcement in the publishing skill: refuse to update a page with open assigned tasks; archive-on-replace step for superseded reports.

W7-D SHIPPED 2026-06-01: ADR-W7-D ratified by SA + Reviewer + Charlie — rules R1 (task-state isolation), R2 (archive-on-replace), R3 (/ws/-only links), R4 (content-hash idempotency), R5 (publish-cursor CAS), R6 (leaf-class retention 8 default / 4 for engagement-survival), R7 (capability-gate), R8 (CAPS-as-headings) · intent-ratify audit 0a41d6ff-3a12-4809-9c8c-e4feea7920e1 · ratification decision 7b46cd41-1790-4260-b180-eaddd1ea39c8. W7-D.1 IMPL-DEPTH ADDENDUM RATIFIED 2026-06-01: follow-on ADR f02af754-0ad1-4c3d-a0d7-a5e463c9181f ratified (decision 28efcc82-156d-4e6b-9542-63974788a996, audit edbc8656-8f49-4280-8476-46781e6cd175) — tightens the publish-skill enforcement contract (preflight checks, CAS semantics for republish, force-publish escape-hatch wording, force-publish-stale variant for survival pages). Enforcement now binding on F32/F34 live publishes.

F34 Engagement bootstrap-survival Loop page

🗄 RETIRED 2026-06-02 — Loop reporting pivot. Survival-page schema (W7-E-v1, 9 sections) is recyclable as the "engagement survival" report type on the W10 web surface. The authored haleon-aiac.md draft (W7 session files) becomes the seed content for that report. Loop instantiation path dies.
P1 Effort: S partial CoS Steward

An add-on to the project-level Loop pages: a single page holding everything needed to recreate an engagement after laptop failure. Loop sits behind Entra, so it survives a wipe and is recoverable via UPN sign-in.

What gets built
  • Recovery essentials on one page: bootstrap-stub URL, KV secret names, MCP names, pack / workflow pointers, role-brief list, sync location, recovery order.
  • Cross-checked against the F27 laptop-continuity playbook; generic framing.
  • Survival page exists in the dev/test notebook and is read back.

W7-E PARTIAL 2026-06-01 (design-complete; live-Loop instantiation deferred to 2026-06-02 operator-attended browser session): Survival-page schema W7-E-v1 ratified by SA (Charlie-gated leaf-class addition engagement-survival with retention=4 per W7-D R6); engagement-survival template committed to ~/.copilot/m-skills/loop-publish/templates/engagement-survival.md (generic). Haleon instance (the first survival page) authored as markdown source at ~/.copilot/session-state/488d3c5d-.../files/survival-pages/haleon-aiac.md — populated §1–§9 of the W7-E schema (engagement identity, bootstrap, KV secrets, MCPs, pack/workflow pointers, role briefs, corpus sync via F35, audit-spine DB, recovery-order runbook) + cross-links to F27 laptop-continuity. Audit chain: rotation 7e31720d-2348-4b1d-aa04-b38235cbdba2 · design-commit 0e578f7f-97c3-44ab-8bbd-3ccd0d11abf2. Overnight BUG-001 constraint prohibits the live Loop publish + read-back. Flips to shipped on (a) survival page live in dev/test Loop with read-back and (b) preflight validation rules from W7-E §Validation green (presence + freshness gates).

F35 Durable corpus sync (skills / briefs / docs → Azure Storage)

P1 Effort: M shipped Steward SA CoS

The expectation that all skills, role-briefs, documentation and other critical items are backed up to Azure Storage — with a real answer to how they sync and how often. Today's corpus-sync is 403-broken (storage public access disabled, no private endpoint); only a direct deploy workaround exists and a policy reverts manual re-opens.

What gets built
  • A durable conduit (private endpoint / VNet / managed-identity network exception) — likely infra creation, surfaced to Charlie before provisioning.
  • Scheduled two-way sync (Ticker-driven or cron) replacing the swa-deploy workaround.
  • Run-time download + remote update pattern (Azure Storage as source of truth) per the F27 continuity idea.
  • Verify: a changed skill / brief pushes to blob and reads back through the durable path.

W7-F SHIPPED 2026-06-01 (Phase 5 probe-pass + Phase 6 mirror regen): Container Apps Job caj-clawpilot-corpus-sync provisioned on environment cae-clawpilot-uks (Consumption, uksouth) on schedule 0 2 * * * UTC; two narrow-role UA-MIs (mi-clawpilot-corpus-flipper for the storage-firewall flip + mi-clawpilot-corpus-rw for blob read/write) each holding the custom least-privilege role Storage Network Flipper (corpus) scoped to stclawpilotcorpusuks; watchdog Logic App la-clawpilot-corpus-watchdog implements the three-layer close (job-success / job-failure / timeout) to guarantee the firewall always re-closes. Probe-pass audit 0b81ce9d-b89c-4c84-905a-e7baa9aad64b measured open_window_ms=39000 (well inside the 5-min budget). Phase 6 SWA-mirror regen audit 5d0230d9-8f09-4c36-a38d-bbc10adc6067 — live mirror serving at https://jolly-dune-00bde4103.7.azurestaticapps.net/ (read-only public surface; bootstrap-stub fetch path for F34 survival recovery). GH→Azure runtime pivot: EMU-policy blocked GitHub-hosted runners (audit 8a1d976e); pivoted to Azure-native Container Apps Job runtime, audit 0b81ce9d proves pass; orphan GH workflow preserved with archival banner at commit 2db30dca for provenance. Cost £0/mo marginal preserved (Consumption + 22-hour-daily firewall-closed window).

F36 Milestone recognition tracker + thank-you drafting

P2 Effort: S shipped CoS

Flags general milestones (not official project milestones — just things getting done), reminds Charlie, and helps draft genuine thank-you / congratulations notes, including notes to people's managers recognising good work.

What gets built
  • Milestone detection / recording + a reminder surface.
  • Draft notes via the outbound-voice skill (peer variant + manager variant) so they sound like Charlie.
  • Privacy: drafts are previewed to Charlie before any send; never auto-sent.

W8 Wave 8 candidates — opened 2026-06-02. Carry-over completion + deferred-feature push. Operator-attended morning work + DB-schema feature build + signed-handback.

Scope (Charlie-ratified 2026-06-02 06:14 BST): Step-0 operator-attended cleanup of W7 partials & W7-D.1 r3 consider items, then three deferred features pulled forward: F31 (use-case tracking, DB-schema home per Charlie steer, reporting layer later if needed), F36 (recognition tracker, was operator-blocked overnight in W7), F21 (signed evidence export, unblocked since F15 shipped W6). Curated substrate subset folded in (R12 sp_update_agent_run signature, R13 loop-publish.ps1 <engagement> literal regressions, mirror-genesis seeding gap). Realistic outcome: Step-0 cleanup + 2–3 of {F21, F31, F36} shipped + curated substrate subset. BUG-001 status: a patch was put in 2026-06-02 morning — verify before treating browser perms as recovered. W9 deferrals below.

Shipped this wave (rolling)

Per W7-established SOP: every per-article flip updates this block in the same swa deploy. Audit UUIDs + Loop links inline; substrate fixes (R-numbered) tracked here without new F-numbers.

  • None yet — wave just opened 2026-06-02 06:14 BST.

Step 0 — operator-attended morning cleanup

Cheap, fast, needs the browser-perm path unstuck. Closes the W7 partials and clears the W7-D.1 r3 "consider" items before the main features kick off.

  • F32 live instantiation — 5 remaining leaf templates (weekly-rollup, steerco-prep, ratified-ADR-digest, incident-postmortem, engagement-survival) published to the dev/test Loop notebook with round-trip read-back. Flips F32 → shipped.
  • F34 live publish — survival page haleon-aiac.md instance to Loop with read-back; cross-check against /laptop-continuity skill. Flips F34 → shipped.
  • W7-D.1 r3 consider items (Steward, no browser): insert-only trigger on the publish-cursor table, recovery_attempt_count column, audit-blob FK cleanup. Small DDL within ratified ADR-W7-D.1 scope; no new ADR needed.

W8 features (carried + deferred from W7)

  • F31 — W8-A use-case-level tracking [SA + Steward]. Home: DB schema (portfolio. + per-engagement haleon.) per Charlie 2026-06-02. Lifecycle table + state-transition trigger + audit-row class extension. Reporting layer (ADO / Loop) intentionally deferred to a later feature if needed. Was W7-B, deferred because rules+sync had to land first — both shipped in W7.
  • F36 — W8-B recognition tracker [CoS]. Milestone detection · reminder surface · thank-you / congratulations drafts (peer + manager variants) via /outbound-voice · preview-before-send privacy gate (never auto-send). Was W7-G, deferred overnight because every send requires operator preview.
  • F21 — W8-C signed evidence export [Steward + CoS]. Signed handback bundle (PDF + library extract) for the dev/test pack · signed with engagement-bound KV key · signature verify + spot-check content. Pairs naturally with W7-E survival + W7-C templates. Deferred from W6 (depended on F15 closeout, which shipped W6); a defer-twice item.

W8 substrate (folded in)

Highest-leverage substrate carried from the W7 backlog. Substrate fixes get tracked here, not as F-numbers (W6+ convention).

  • Mirror-genesis seeding gapmobilise-engagement doesn't seed the off-DB mirror's genesis attestation (blobs absent pre-flush). Touches the F35 sync/durability story; seed at mobilise so off-DB mirror has a genesis attestation. Surfaced post-W6 flush; carried through W7.
  • R13 loop-publish.ps1 <engagement> literal regressions — 4 W5-scrub regression spots (empty-state fallback masks query errors, drift-sha lookup fail, Write-AuditRow silently fails). CoS folds the fix while doing the F32 live instantiations in Step 0.
  • R12 sp_update_agent_run signature additions — add p_tokens + p_outcome_note + nuanced fields. Steward folds during F31 DDL work.

W9 Wave 9 — CLOSED 2026-06-02 — substrate-hygiene wave. 7 SHIPPED (R8, R9, #12 shared-skill-folder race, F21 full signed bundle, F24 re-apply, #5 m-policies fold, W6 scout-overlays finding). Honest-defer: F10C live surfacing loop (pattern_library N=0 < N=5 gate). No open authority gates. Close-out audit 4890a283-a92f-4864-b939-b0e56c454c56 (sp_verify_chain OK @ 291 rows).

Deferred-W9 list (set at W8 open): items intentionally not in W8 scope. Each line is the reason; promote at W9 open or fold into W8 only if explicitly steered.

Shipped this wave (rolling)

Per W7-established SOP: every per-article flip updates this block in the same swa deploy. Audit UUIDs + decision UUIDs inline; substrate fixes (R-numbered) tracked here without new F-numbers (W6+ convention).

  • W9-C.R8 SHIPPED 2026-06-02: haleon.decisions.status enum (udt decision_status) extended with 'ratified' + 'resolved' — now 6 members (proposed, accepted, superseded, rejected, ratified, resolved). ALTER TYPE … ADD VALUE run under psycopg2 autocommit (cannot run in a txn block); existing 31 decision rows untouched (count 31 before == 31 after the ALTERs); synthetic ratified insert verified then cleaned up by id; enum_range read-back = 6. Prior waves used the accepted + review_stage=ratified workaround — no existing rows migrated (parent steer). Audit run 961b427d-bd34-451c-934a-7694efb55721 (two-phase; sp_verify_chain OK, chain head @ 281 rows) · decision a70b916d-0e39-4d10-b97e-e7f368b0a5fa. Generic-process framing (bound engagement <engagement>).
  • W9-C.R9 SHIPPED 2026-06-02 (documented finding — no change needed): haleon.agent_runs.class is a TEXT column already governed by CHECK agent_runs_class_check (9 values: turn, rotation, snapshot, ado_drain, audit, correction, scout_sweep, mode_flip, breaker_trip) + NOT NULL. All 5 distinct live values (audit, turn, correction, rotation, mode_flip) are within the allowed set; 0 rows use operator/publish/wave_close. No DDL change madepublish moot (Loop retired), wave_close deliberately maps to audit (W7 convention). Surfaced (open, authority-gate): F24 claims a W5 ALTER added 'operator' to the class CHECK + tier='operator', but the live schema contains neither (class CHECK lacks it; agent_tier enum = 7 values, no operator) — a shipped-claim-vs-reality gap raised to Wave Lead, not actioned here. Audit run 5da941e7-a1cc-43ef-b85c-d96e7ff6d85e (two-phase; sp_verify_chain OK, chain head @ 284 rows) · decision 810f255b-c696-406a-b89e-7737c58d6936. [RESOLVED 2026-06-02 by W9-A F24 re-apply (operator-authorised) — §3 DDL applied for real; correction audit 4b8fc0a8-02ae-4b31-b173-3a0f93177712; see the F24 re-apply entry below.]
  • W9-A.F21 SHIPPED 2026-06-02: Signed compliance-evidence handback bundle for the bound engagement <engagement>. Bundle w9-f21-handback-bundle.zip (12 920 B, sha256 4d2c2ae3…43a9) composed by CoS (compose audit 5c77ad80-7007-4a4f-b28e-c2df53698519, chain snapshot d3db63f7…dec36 @ 283 rows re-verified live). Steward signed with KV haleon-attest-priv-pem (RSA-2048 PSS / SHA-256, 256-byte detached sig); VERIFY: OK end-to-end with haleon-attest-pub-pem; tamper-probe rejects single-byte flip; 3-section content spot-check passed. W8 mirror-genesis seeding gap-note folded (mirror-genesis-gap-note.md, signed + verified + tamper-probed OK; documented, not applied). No mirror push (parent steer). See F21 article. Audit run 2e71be6d-8c50-4e62-ba1c-14119bce2fe7 (two-phase; detail_ref = CoS compose run per soft-FK; sp_verify_chain OK @ 286 rows) · decision fe45f5f3-6db2-447d-8ce3-fabd36fb3a7f.
  • W9-A.F24 re-apply SHIPPED 2026-06-02 (correction-ship): Closes the shipped-claim-vs-reality gap surfaced by W9-C.R9 — the Wave-5 F24 claim was schema-incomplete (live schema had neither class='operator' nor tier='operator'). Under operator authorisation + ratified SA-ADR-F24 (b505985c-f60d-407f-b6a1-9b8ef2122a02), §3 applied for real: agent_runs_class_check → 10 values; agent_tier enum → 8 values (incl operator); sp_rollup_cost_telemetry HS-10 cost-rollup exclusion extended (operator excluded). 3 DDLs, 3 SchemaMigrationIn rows (no-silent-DDL). Probes PASS: synthetic operator insert OK, enum_range=8, cost-rollup delta 1→0; probe row retained+documented as operator-class genesis (append-only chain, no DELETE grant). Correction audit 4b8fc0a8-02ae-4b31-b173-3a0f93177712 (two-phase; sp_verify_chain OK @ 288 rows) · decision d2d38882-9403-40a2-a493-511de39c3f5c. See F24 article. Generic framing (<engagement>).
  • W9-C substrate #5 SHIPPED 2026-06-02 (m-policies fold): Folded the m-policies/ machine-readable policy surface into a single consolidated ~/.copilot/m-policies/platform.json (14 688 B) — the single read-surface for the W11 web-app policy layer, reducing config sprawl. Embeds with per-source SHA-256 provenance: model-selection policy (5 tiers, from current-best-models.json), the F23 integration registry (25 candidates, from adr/integrations.yaml), and the ADR keyword index (3 rows, from adr/INDEX.md). Non-destructive + non-breaking — ADR prose docs + all canonical sources stay on disk (append-only governance); existing CLI consumers (check_model_pins.py, F10c) keep their canonical sources until a future wave migrates them. Round-trip validated (tiers/integration-count/adr-rows all match source). Surfaced (not auto-authored) an index drift: INDEX.md omits 1 of 4 on-disk ADRs (SA-ADR-PEER-TOOL-INHERITANCE). Audit run 85e03a3b-f84a-45ef-b503-c7409f11c0e6 (two-phase; sp_verify_chain OK @ 289 rows) · decision ec1958ca-b531-4f23-8104-56b31c89faba. Generic framing (<engagement>).
  • W9-C substrate SHIPPED 2026-06-02 (W6 scout-overlay backfill — documented finding, no change needed): Investigated the carried-from-W6 "4 scout overlays un-backfilled" deferral. Finding: already backfilled. All 4 non-signal scout overlays (BACKLOG-SENTINEL / INTAKE-TRIAGE / PIPELINE-WATCHER / REUSE-SCOUT) exist under m-engagement-packs/<engagement>/role-overlays/scouts/, are fully backfilled (scan-surface bindings + privacy/routing contracts + per-scout live-enable flags), and are registered in m-role-briefs/MANIFEST.yaml with generic [customer-name] overlay_target paths; signal-scout is overlay-less by design (covered in CHIEFOFSTAFF.md per scout-types REGISTRY.md), so exactly 4 are expected and all 4 present. Residual placeholders are operator-mobilisation-only inputs (empty pack.json keys + 1 "TBD" seed) — out of Steward scope. No manufactured edits (complete files left untouched), mirroring the W9-C.R9 "no-change-needed is a valid substrate outcome" precedent. Secondary finding: e20b49fd is the loop_publish_w5_scrub_followup decision (loop-publish.ps1 residuals), not a scout-overlay umbrella — moot with Loop retired; stale deferrals cleared. Audit run c9211767-c412-402c-846b-5dbf1a7687fb (two-phase; sp_verify_chain OK @ 290 rows) · decision bdbe8eb7-b1ba-4787-98fa-860d79bd0f3f. Generic framing (<engagement>).
  • W9 substrate #12 SHIPPED 2026-06-02 (CoS — shared-skill-folder write race): Concurrent writers into the shared corpus/skill folder (corpus-sync/sync.ps1: blob-download pull, etag-cache save, restore apply, SWA recovery-stub republish) previously wrote straight to the destination, so a concurrent pull or a mid-write reader could observe a torn/truncated file. Resolution: write-to-sibling-temp + atomic rename (Move-FileAtomic = MoveFileEx/MOVEFILE_REPLACE_EXISTING) with bounded retry over the Windows transient sharing-violation window; helpers New-SiblingTempPath + Move-FileAtomic applied at all four write sites. Also corrected a pre-existing parse-blocker (unquoted <kv-name>/<pg-host-from-pack> generic placeholders). Verified: ParseFile SYNTAX_OK; concurrency probe 8 writers × 60 iters = 480 atomic swaps, 5957 reads, 0 torn reads, 0 writer errors, 0 leftover temps, final file intact. Audit run 91c63303-75cd-41db-b51b-0daf5ff962d2 (class=audit) · decision 26c3514e-8a7f-4d3f-a9ce-98bc6e455f48. Generic framing (<engagement>).
  • F10C live surfacing loop — HONEST-DEFER (deferred-W9 → W11+): the re-trigger gate auto-fires at pattern_library N=5; at W9 the live count was N=0 < 5, so the live surfacing loop was NOT shipped (no synthetic hits manufactured to force the gate). Remains design-pending-corpus — promote when the library crosses threshold. Not in shipped-rolling.
  • R8 decisions.status enum — add 'ratified' / 'resolved' members. W5/W6 used the status='accepted' + review_stage='ratified' workaround; not blocking. SHIPPED 2026-06-02 (W9-C.R8) — see shipped-rolling. Audit 961b427d-bd34-451c-934a-7694efb55721 · decision a70b916d-0e39-4d10-b97e-e7f368b0a5fa.
  • R9 agent_runs.class enum audit — W5 added 'publish', 'operator'; W7 used 'audit' for wave-close (enum lacks 'wave_close'). Curated extension pass. SHIPPED 2026-06-02 (W9-C.R9) — NO CHANGE NEEDED (CHECK exists + complete vs usage). F24 'operator'-class/tier shipped-vs-reality gap surfaced to Wave Lead (open). See shipped-rolling. Audit 5da941e7-a1cc-43ef-b85c-d96e7ff6d85e · decision 810f255b-c696-406a-b89e-7737c58d6936.
  • R10 loop-publish apply-path inertcancelled 2026-06-02; Loop publisher retired (see F29).
  • R11 haleon-aiac-db MCP-name + db/role rename — infra-debt; engagement-CODE-driven (NOT name-driven). Only if Charlie scopes it — touches live connection strings; coordinate before scheduling.
  • W7-E rev2 §11 Hazards sectioncancelled 2026-06-02; W7-E retired with F34.
  • 4 scout overlays un-backfilled — carried from W6 (decisions e20b49fd); curatorship. RESOLVED 2026-06-02 (W9-C — documented finding, already backfilled) — all 4 overlays exist + MANIFEST-registered; see rolling.
  • W5-scrub placeholder residuals — carried from W6 (e20b49fd umbrella); curatorship. cancelled 2026-06-02 — e20b49fd is the loop-publish.ps1 scrub decision; moot with Loop retired.
  • Larger structural items — #1 pack.json/workflow.json consolidation (LARGE), #5 m-policies fold into platform.json (SHIPPED 2026-06-02 W9-C — see rolling), #6 canary-corpus batch semantic eval, #12 shared-skill-folder write race, #14 Edge singleton handoff fallback in loop-publish (cancelled 2026-06-02; Loop retired). Re-rank at W9 open.

Pending-host (not ours — Charlie routing to product team): FR-001 peer-spawn inheritTools param (decisions 652686ea) · BUG-001 browser-MCP always-allow not persisted (patch attempt 2026-06-02 morning CONFIRMED BROKEN; priority drops with Loop retirement — the web-app deploy path doesn't need browser-MCP).

Permanent design-only (per Charlie): F17 Power BI publish — superseded by W10 web reporting front end.

W11 Wave 11 — BugFixer (W10 polish + deferred features + background automations) — opened 2026-06-04. Cardinal Rule 5: HUMAN-CRUD-VS-AUDIT-SPINE-IMMUTABILITYhaleon.agent_runs stays hash-chained append-only; entity mutations go via HS-4L SPs writing a new audit row + raw UPDATE on the entity (soft-delete via deleted_at col; edit via before/after payload + xmin OCC). sp_verify_chain() OK is a wave-stopping per-PR ship gate.

BugFixer wave: 5 phases / 17 PRs autonomous overnight against smccormick_microsoft/clawpilot-w10. Phase 1 (foundation) COMPLETE 2026-06-04/05; rolling ship rows newest-first below. Substrate invariants per SA-ADR-W11-1; chain integrity verified per ship.

W11 PRScopeShip referenceDate
W11-PR-PUB-2 First-wave publish channels: spa_permalink (real, synchronous) + teams_dm (render-worker, delivery-gated) + polymorphic <PublishArtifactPanel> on DecisionDetail. First end-to-end use of the PR-PUB-1 polymorphic substrate. spa_permalink: marked sent synchronously inside mut.publications.publish (re-reads the freshly-created targets, calls sp_publication_target_state with actor system:publish-sync-permalink → state sent + stable permalink /e/<engagement>/<source_table>/<source_id>?pub=<publication_id> as external_ref, then returns the rolled-up parent status). teams_dm: NEW Function-timer worker publishWorkerTeamsDm (every 2 min) polls queued teams_dm targets, renders the body (pure renderTeamsDmBody helper, snapshot + honest delivery-pending note), and advances state via NEW parent-applied SP sp_publication_target_render (system-actor, publish_dispatch audit) → ready_to_send. Live Microsoft Graph chat-send is GATED: the Function's managed identity holds a Postgres scope only (no Graph grant; GraphMeetingFetcher remains a stub), and app-only Teams 1:1 chat-send is unsupported by Graph — so the worker renders honestly and delivery is deferred to PR-PUB-3 (no fabricated sent). SPA: NEW polymorphic <PublishArtifactPanel> (cp-token inline styles, NOT the reports-era Tailwind PublishPanel) wired into decisions.$id between Notes and Activity — channel multi-select (permalink always-on; teams_dm reveals a recipient-UPN input + honest caption), fixed internal audience (partner/customer approval flow = PR-PUB-3), publish-history strip with per-target state pills + permalink link. Substrate: ONE small parent-applied SP (sp_publication_target_render, owner haleon_aiac_owner, EXECUTE→w10_app); zero new tables/columns. In-deploy fixes: (1) register the new Function in main.ts (esbuild entry wiring); (2) drop SELECT … FOR UPDATE from the worker claim (w10_app is SELECT-only — row-locking lives inside the SECURITY DEFINER render SP). Live E2E (decision 1133f12d): spa_permalink → sent w/ permalink; teams_dm → ready_to_send rendered by the DEPLOYED worker; audit lineage publish + 2× publish_dispatch. Gates: tsc clean; api vitest 51 files / 659 pass (live suites ran vs real DB) / 3 skip; spa 42 / 320; build:deploy clean. Deploy: func-w10-api-uks rc=0 (5 fns incl publishWorkerTeamsDm booted) + swa-w10-clawpilot-uks "Project deployed". Built via the local Copilot CLI. N1 preserved: nothing auto-publishes — every publish is human:<upn>-initiated. PR #48 (squash-merge SHA 0f8a360; follow-ups 68f6b91 + c6cf992); ship audit cc35ebde-330b-481a-9a0a-c099d78fc720; seal audit f059195d-0060-47a7-8b6c-652ddab4ec25; new SP applied chain OK. Citations: SA-ADR-W12-PUB-1; W12-PUBLISHING-DESIGN §0/§3/§4.1/§4.2/§4.3/§11. 2026-06-06 BST
W11-PR-HIER-1 Conversations hierarchy + workstream membership + Change-5. Substrate (live haleon, idempotent): NEW haleon.conversations + haleon.conversation_use_cases tables; nullable FK-less conversation_id uuid column on decisions/risks_issues_blockers/reports/meetings + portfolio.recognitions (SP-enforced, no cross-schema FK); view haleon.v_use_case_children. 7 HS-4L SPs (sp_conversation_open/_close/_tag_use_case/_untag_use_case/sp_artifact_tag_conversation/_untag_conversation/sp_meeting_promote_to_conversation), all class='turn' (established W10 human-UI-write class — no audit-spine CHECK widen). Change-5 lockstep: DROP sp_publish_artifact(9-arg) + DROP COLUMN publications.source_conversation_id + DROP INDEX + CREATE sp_publish_artifact(8-arg) + re-GRANT. tRPC: NEW conversations router (read: get/list[default-excludes ad_hoc]/children/useCaseChildren; mut: create/close/tagUseCase/untagUseCase/tagArtifact/untagArtifact/promoteMeeting) wired in root.ts; publications.ts Change-5 edits (dropped sourceConversationId, SP 9→8 args); drift guard __schema__.test.ts updated. Owed PR-PUB-1 live test CLOSED: publications.live.test.ts (14 PGPASSWORD-gated tests) committed + green; NEW conversations.live.test.ts (8 tests). Gates: tsc clean; api vitest 650 pass / 3 skip / 0 fail (live suites RAN vs real DB); smoke-hier1 22/22; build:deploy clean. Deploy: func-w10-api-uks via config-zip rc=0 (worker booted, 4 fns). SA-ADR-W12-HIER-1 ratified (R1 conversation_type text+CHECK; R2 engagement_id text NOT NULL no-FK; R3/Change-5 lockstep; S4 conversation_id on decisions only, adrs→decision_id). Built via the local Copilot CLI. PR #47 (squash-merge SHA b91f7fd); ship audit 4cbd7888-0350-4f2a-beca-4d66a808d977; seal audit 06a3a7ed-3fbb-46e3-8dd8-1f8568b1f201; Steward attest PASS 2494ab9a-ec5a-459d-9b51-8899c5f9d82c; chain OK 410. Citations: SA-ADR-W12-HIER-1; SA-ADR-W11-6 §2.8. 2026-06-06 BST
W11-PR-PUB-1 Polymorphic publish substrate. Lifts the reports-only publish model to a polymorphic layer. Substrate (live haleon): NEW haleon.publications (parent: source_table/source_id, immutable source_snapshot_md + sha, audience, approval_state, status, published_by_upn human:%-gated, superseded_by) + haleon.publication_targets (per-channel fan-out children w/ state machine + external_ref) + haleon.publication_templates + haleon.publication_approval_policies (+ seed policies); core SPs sp_publish_artifact (HS-4L human-only, idempotent on (source_table,source_id,client_op_id), MIP-required-for-customer halt), sp_publication_approve/_reject (separation-of-duties), sp_publication_target_state, sp_publication_supersede, sp_publication_record_ack. tRPC: NEW publications router — read.publications.byArtifact/byId/listPending + mut.publications.publish/approve/reject/supersede; drift guard updated. Backwards-compat: legacy report_publications untouched. SA-ADR-W12-PUB-1 ratified (polymorphic substrate + 5 SPs; additive agent_runs.class widen for {publish,publish_approval,publish_dispatch}). N1: NOTHING auto-publishes — every publication is human:<upn>-initiated. Built via the local Copilot CLI. PR #46 (squash-merge SHA d7d3929); ship audit f26a662e; deploy-complete a5f3467b; Steward attest PASS 9b86abb2; chain OK 345. Citations: SA-ADR-W12-PUB-1; W12-PUBLISHING-DESIGN §2/§3. 2026-06-06 BST
W11-B-NEW-15 Rebuild PR-10B3 generators to real haleon schema. Reworked the prospective + remaining retrospective generators (meeting-prep + legacy ports) against the LIVE haleon schema after B-NEW-14 surfaced residual column drift — restores generator round-trips to the real substrate. ZERO new substrate (generator code only). Built via the local Copilot CLI; gates green (tsc + api/spa vitest + build clean; function count stays 4). The governance hold carried from PR-10B2/10B3 was LIFTED via the B-NEW-14 + B-NEW-15 attest batch. PR #45 (squash-merge SHA dcdeed6); deploy audit d0ec7388; deploy-complete 5bf4f318; Steward attest (B-NEW-14 + B-NEW-15 hold LIFTED) 4957527b. NO new ADR. 2026-06-06 BST
W11-B-NEW-14 Fix report generators to real haleon schema. Corrected the retrospective report generators' table/column references to match the live haleon schema (§8h column-drift fix). Generator schema fix only — no new substrate, no policy change. Built via the local Copilot CLI; gates green. (Schema drift was deeper than this fix alone; B-NEW-15 completed the rebuild.) PR #44 (squash-merge SHA fc2a3e6); deploy audit caba5a87; Steward attest (with B-NEW-15) hold LIFTED 4957527b. NO new ADR. 2026-06-06 BST
W11-PR-10B3 Prospective + remaining retro generators (meeting-prep + 4 legacy ports) 2026-06-06 Shipped #43
W11-PR-10B2 Retrospective generators (wave 1): 5-3-2-1, Weekly Status, Pending Actions, Recognition Roundup. Four new read-only TypeScript generators in apps/api/src/generators/ wrapping existing substrate (haleon.briefs / pod_intel / decisions / risks_issues_blockers / use_cases / agent_runs / portfolio.recognitions) into the shipped report-persistence model. Built via the local Copilot CLI (non-interactive copilot -p, WaveDriver-reviewed diff). ZERO new substrate — B2 inaugurates the §8h column-drift scan as the new reuse-drift gate (16 table refs scanned across 4 generators, all green). mut.reports.generate refactored to dispatch via a GENERATORS registry; read-only generator (Phase 1) + short-TX sp_report_generate write (Phase 2) — the SA-ADR-W11-5 §2.3 generator/SP write boundary preserved. handleWeeklyRollup migrated to a wafer-thin stub delegating to generateWeeklyStatus (preserves the read.reports.get?legacy=true migration fallback shipped in B1; @deprecated); Daily Status stays KILLED (B1). SA-reviewed 5 risk surfaces all addressed: (1) HS-4L caller-class via sp_report_generate for human-clicked (cadence _auto deferred to B3); (2) supersedes_id resolved read-only BEFORE the SP call; (3) rag_source='auto' default preserved (generators never override); (4) stable <!--item_key:rpt-{n}--> markers on every actionable checkbox line in all 4 generators (B-NEW-08 toggle-bug class blocked); (5) NO v_reports_current view introduced (Pattern A read-paths only, ADR-W11-1 §5.2). Ship-shape canonical (PR-9 named-param sp_create_agent_run, NOT the PR-10B1 outlier; SA-ADR-W11-6 §2.8 short-txn discipline — SHIP row in its own short txn BEFORE deploy, SEAL row in this separate short txn AFTER). Tests: API 410 pass (+47 — 15 RAG + 6 daily5321 + 8 weeklyStatus + 8 pendingActions + 8 recognitionRoundup + ~13 dispatch); SPA 315 pass; tsc clean; build clean; function count stays 4 (no drift); 0 drift-bombs. Phase-B coverage: SP-level write-contract probe PASS (body 251B, frontmatter populated, rag_source=auto, generated_by=human:<upn>, <!--item_key:rpt-1--> marker, YAML frontmatter at body top); live tRPC-via-SWA probe blocked on stale 8hr SWA auth cookie (daylight Charlie re-auth + 4 generator round-trips recommended). Bundle deploy gotcha logged: func azure functionapp publish uses the LOCAL dist-bundle/main.cjs — you MUST run pnpm build:deploy (= tsc + esbuild bundle) BEFORE publish, not just pnpm build; first publish attempt deployed a stale bundle and was caught + corrected in-turn. PR #42 (squash-merge SHA 2f9c912); ship audit 4a8869f3-707f-4e98-9e36-dfe5ed69eeef. Citations: SA-ADR-W11-5 §2.3; SA-ADR-W11-6 §2.8; W11-REPORTS-REDESIGN.md §2.1/§2.2/§2.3/§2.9; PR-10B1 BUILD-SPEC. NO new ADR (zero new substrate, zero policy change). 2026-06-06 BST
W11-Library-promote Portfolio Library — top-level cross-engagement surface. The 6 ratified portfolio patterns (Hash-chained audit spine, Pattern A reads, Unified Publish panel, CLI-driven build, Field-coverage gate, Generate-then-persist reports) are now visible to all engagements via a new top-level /library route — not nested under /e/$engagement. Substrate: prior sub-agent lifted 6 rows from haleon.reuse_catalog into portfolio.pattern_library with engagement_id IS NULL (cross-engagement by intent per W10 §7c); SA-ratified follow-on ALTER realigned engagement_id from uuid NOT NULL to text NULL to match system-wide convention (no engagements table; text keys throughout). Backend: read.library.portfolioList + portfolioGet tRPC procedures with no engagement input. SPA: top-level library.index.tsx + library.$id.tsx mirroring the metrics shape; header nav link added in __root.tsx; engagement-scoped Library link removed from e.$engagement.tsx. Drift defence: portfolioLibraryFieldMap.ts field-coverage gate (17 parametric tests) prevents captured-but-never-displayed columns. Tests: tsc clean; vitest api 353 pass / 3 skipped (library.test.ts +5 portfolio*); vitest spa 315 pass (+23 across library routes + fieldmap). Playwright smoke (ship-gate): 5/5 click-paths pass on live SPA. Function count stays 4 (no drift). Short-txn discipline per SA-ADR-W11-6 §2.8. PR #41 (squash-merge SHA 7eade8c + smoke-fix af85146); prior lift audit 9787d7b7-2251-4402-92ed-62ee9b475ca4; text-align audit 04c6435e-8590-4022-a9e7-4f78f3b3b085; ship audit 67f45377-ce50-4b6c-8e9a-db6d59710b9d; seal audit f1540caf-0c91-47ad-ade7-becce9953c46. 2026-06-05 BST
W11-B-NEW-13 Recognition tab — 6-bug batch. Charlie-reported live UX issues on the Recognition tab. Fixes: (1) draft Peer/Manager buttons gated by peer/manager status — hidden when status=sent or no manager — with invalid_transition error-message mapping; (2) Edit modal exposes Person email + Person display name fields and saves both with proper undefined-as-no-change semantics; (3) Create form has a separate personName free-text field independent of the email-validated personUpn — fixes prior "text rejected as email" 400; (4) Person column renders personName ?? personUpn.split('@')[0] ?? — rather than always splitting the email; (5) line-through moved off the row onto data cells only so the Restore button is no longer struck through; (6) Delete confirmation modal removed — Delete acts directly. Substrate already applied by W11-WaveDriver-#7 (portfolio.recognitions.person_name col, 9-arg sp_recognition_flag_milestone, 12-arg sp_recognition_edit). Built via the local Copilot CLI (claude-opus-4.6) per .w11/bnew13-build-spec.md; W11-WaveDriver-#8 reviewed diff, ran gates, authored Playwright smoke, shipped + sealed. Tests: tsc clean (api+spa); vitest api 348 + recognitions 51; vitest spa 292 + recognitionErrors +2; build green. Playwright smoke (NEW ship-gate): 6/6 click-paths pass on live SPA. Function count stays 4 (no drift). Short-txn discipline per SA-ADR-W11-6 §2.8: SHIP audit row in own short-txn BEFORE deploy; SEAL in separate short-txn AFTER deploy + smoke pass. PR #40 (squash-merge SHA e572899); ship audit bf6ac7af-cb49-42ac-961d-67ae1e2f1851; seal audit b295f17d-668d-4094-b948-64f4045ae9c6. 2026-06-05 BST
W11-B-NEW-12 Library page test-data seed. Charlie surfaced live (2026-06-05 19:00 BST): Library page empty — impossible to evaluate UI. Root cause: haleon.reuse_catalog had 0 rows. Fix: seeded 6 realistic reuse-catalog entries spanning HS-4L audit spine, generate-then-persist reports, unified Publish panel, Pattern A reads, field-coverage gate, CLI-driven build pattern. Source pods span control_tower / pod_1 / pod_2; mix of derived_count + effort_saved + portfolio-shareable. Idempotent insert script under SET ROLE haleon_aiac_owner. Caveat surfaced: Charlie confirmed Library should be promoted to portfolio scope (cross-engagement) — handed off as next-PR for WaveDriver-#7 (option (a) clean cut: drop /e/$engagement/library, promote to top-level /library reading portfolio.pattern_library). Ship audit ed36045d-9b16-41b7-8fdb-b4084516ae53. Closes B-NEW-12 (test-data only; no code change). 2026-06-05 BST
W11-B-NEW-11 Sessions tab graceful empty state on cross-origin fetch failure. Charlie surfaced live (2026-06-05 18:42 BST): Sessions tab on load showed "Unable to load sessions: Failed to fetch". Root cause: cross-origin fetch from happy-dune SPA to jolly-dune canon-mirror SWA returns 404 without an Access-Control-Allow-Origin header, so the browser throws a network TypeError before the existing graceful 404 path can run. Fix: try/catch the fetch separately; treat any network error OR any non-OK response as "manifest not yet populated" → empty state with the existing "No sessions found for this engagement." copy. Originally slotted in PR-3 (B-SES-01) but only the auth/CORS basics were fixed there; the missing-asset-404 cross-origin case was missed. Gates: tsc clean, API 20 + SPA 38 files all pass, build green, function count 4 (no drift). Ship-shape contract: named-param sp_create_agent_run (F-W11-SHIP-SHAPE-DRIFT-1 compliant); ship audit row written retroactively at session close to fix the spine gap. PR #39 (squash-merge SHA 83a1fc7); ship audit 3ed37b5e-d215-4938-b915-259ad06c150d. Closes B-NEW-11; completes original B-SES-01. 2026-06-05 BST
W11-B-NEW-10 Daily Status registry-row purge (completes the §4.10 KILL from PR-10B1). Charlie surfaced live (2026-06-05 18:37 BST): Daily Status still appeared on the Reports index after PR-10B1, and clicking it threw "Error: No handler for report slug daily-status". Root cause: PR-10B1 §4.10 KILL removed the JS handler + registry slug + nav reference from the SPA/api code, but left the row in portfolio.report_templates (the byDate proc reads templates from the DB, not the in-code registry). The probe surfaced the slug; the handler lookup then threw. Fix: DELETE FROM portfolio.report_templates WHERE slug = 'daily-status' under SET ROLE haleon_aiac_owner (idempotent; 1 row deleted; chain OK). No code change needed; user-validated. Ship-shape contract preserved: named-param sp_create_agent_run. Lesson for the wave: any KILL ceremony for a report-type now needs both the JS-handler removal AND the DB-registry DELETE; PR-10B2/B3 spec should fold this into the generator-deprecation checklist. Ship audit a047a78a-ab21-4cbe-80e2-357c19c4e4d5. Closes B-NEW-10 (Charlie post-PR-10B1). 2026-06-05 BST
W11-B-NEW-09 Reports index UX hotfix post-PR-10B1. Three bugs Charlie surfaced live (2026-06-05 18:22 BST): (1) clicking a report card on the index landed on the new history-route empty state ("No generations found for this report type") instead of the legacy detail page — root cause: the new history route file e.$engagement.reports.$type.tsx shadowed e.$engagement.reports.$type.$id.tsx because TanStack Router treats a parent $type route as a layout that needs an <Outlet />; without one, the parent swallowed the child. Fix: renamed to e.$engagement.reports.$type.index.tsx so it only matches /reports/$type exactly, restoring the detail route at /reports/$type/$id. (2) Empty-state copy was generic "No reports available." — fix: ReportList accepts an emptyMessage prop; the index page passes a date-range-aware string ("No reports found for the last 7 days." / "30 days" / "90 days" / "all time", and a separate string on the By Type tab). (3) Back link always reset to 30d regardless of where you came from — root cause: detail route read sessionStorage which was stale across deep links; fix: detail route declares validateSearch({tab, range}), the index passes them on navigation, and the BackLink reads from Route.useSearch() (URL truth). Ship-shape contract restored: uses named-param sp_create_agent_run per Steward F-W11-SHIP-SHAPE-DRIFT-1 (PR-10B1 was the one-off positional outlier). Gates: tsc clean, API 20 + SPA 38 files all pass, build green, function count 4 (no drift), 5 files / +24 -10. PR #38 (squash-merge SHA 134d701); ship audit 1fdf477e-5dab-4fe6-ac69-fa0ac3320da3. Closes B-NEW-09 (reported by Charlie post-PR-10B1 deploy). 2026-06-05 BST
W11-PR-10B1 Reports REDESIGN: persistence + core CRUD + view route + unified multi-target Publish panel. Charlie ratified the PR-10A spec (W11-REPORTS-REDESIGN.md, 2026-06-05 16:14 BST) with 3 amendments (customer-call SteerCo-grade prep; unified multi-target Publish; Q7 OVERRIDE — nothing ever auto-published, W11 or W12). PR-10B1 lands the substrate + core CRUD + view route + Publish panel UI; B2/B3 (generators) and B4 (worker + export) follow. Companion ADR: SA-ADR-W11-5-REPORTS-PERSISTENCE-AND-PUBLISH (ratified — 9 decisions locked: generate-then-persist semantics, Pattern A everywhere, separate sp_report_generate_auto for cadence, CHECK (published_by LIKE 'human:%') substrate enforcement of Q7, verb namespace, target-enum validation, retry budget). Built via the local Copilot CLI (non-interactive copilot -p, parent-reviewed diff) per the build-recipe. Substrate (parent-applied under SET ROLE haleon_aiac_owner; chain OK; 7/7 §8g dry-runs green): 3 new tables — haleon.reports (immutable per-generation rows, supersedes_id chains history, rag_source auto|human, report_type text+CHECK seeded with ~15 values incl. meeting_prep w/ frontmatter.meeting_type discriminator), haleon.report_exports (docx/pptx queue w/ attempts int retry budget), haleon.report_publications (one row per Publish action w/ ordered targets jsonb + per-target fan-out state + CHECK (published_by LIKE 'human:%') Q7 defence-in-depth); 7 new HS-4L SPs — sp_report_generate, sp_report_generate_auto (CHECK auto:%, NO HS-4L), sp_report_edit (allowlist body/rag/ragReason/audience + xmin OCC + RAG auto→human flip on change), sp_report_soft_delete/_undelete (Cardinal Rule 5 soft-delete + undelete trio), sp_report_export_request, sp_report_publish (per-element target-enum validation; raises invalid_target; FIRM human-only); 500 KB body_too_large cap on all generate/edit paths; replay-first triggered_by-scoped per ADR §2.9; all SPs owner=haleon_aiac_owner from CREATE (OWNER-1); GRANT EXECUTE on 7 SPs + GRANT SELECT on 3 tables to w10_app. Reuses live haleon.checkoffs + sp_report_checkoff_toggle + sp_report_add_note + sp_record_read verbatim. tRPC: 3 new reads (read.reports.list/get/history — Pattern A base-table SELECT w/ xmin projection per ADR-W11-1 §5.2 amendment) + 6 new mutations (mut.reports.generate/edit/softDelete/undelete/requestExport/publish); legacy byDate/byType/getLegacy + checkOff/addNote retained (deprecation comments on the first three). SPA: new shared <RagPill rag reason source /> + <PublishPanel reportId report onPublish /> (multi-select fan-out, honest "queued — worker ships in PR-10B4" caption); new reportTargets.ts (enum byte-synced with SP), reportsErrors.ts, reportFieldMap.ts; new history route /e/$engagement/reports/$type shows generation chain w/ latestPerSlot. Daily Status KILLED per spec §5.1 (registry slug + handleDailyStatus + nav reference removed). Field-coverage gate live as wave-wide template (Steward open-state #9 vaccine for "captured but never displayed" anti-pattern): parametric vitest derived from reportFieldMap.ts — adding a column to read.reports.get without updating the map = CI failure by construction. Live smoke GREEN (engagement haleon): auth + read.reports.list (xmin projected — Pattern A confirmed) + mut.reports.generate (real reportId returned) + idempotent replay (same reportId on same clientOpId) + read.reports.history (chain w/ latestPerSlot) + mut.reports.publish (queued publication row + audit lineage) + invalid_target rejected at zod boundary; smoke seeds sp_report_soft_delete'd for cleanup. Hotfix in deploy: w10_app initially missing SELECT on the 3 new tables (B-DOC-01-class Steward anti-pattern — Pattern A reads SELECT directly from base tables); live-fixed via GRANT SELECT under SET ROLE haleon_aiac_owner; apply-pr10b1-ddl.py amended to include the GRANT for replay safety. Tests: API 20 files / SPA 38 files all pass; tsc clean; pnpm -r build success; function count stays 4; 0 drift-bombs; generic-framing clean. PR #37 (squash-merge SHA 00085c5); ship audit f1286ca4-a17e-42ea-bed1-5f11ed8ad38b. Citations: SA-ADR-W11-5-REPORTS-PERSISTENCE-AND-PUBLISH + W11-REPORTS-REDESIGN.md §10 ratification record + ADR-W11-1 §5.2 amendment + Cardinal Rule 5 (Q7 OVERRIDE: nothing ever auto-published). 2026-06-05 BST
W11-B-NEW-08 Hotfix — reports interactive-tracking check-off key collision. Charlie reported live: clicking one report check-off visually selected a different line. Root cause (SPA-only React-key bug in reports.$type.$id.tsx): CheckOffButton/decision Link were keyed by positional idx (checkoff-${idx}), and processTextNode restarts idx at 0 per markdown block, so check-offs on different lines collided on React keys — React reconciled checked-state onto the wrong instance. The data path was already correct (generator emits globally-unique itemKeys). Fix: key by unique itemKey / decisionId (2-line change). Built via the local Copilot CLI; parent ran gates + ship. Tests: SPA 245 (+6, incl a two-check-offs-independently-toggleable key-distinctness test); tsc clean; build green; function count stays 4; SPA-only deploy. PR #36 (squash-merge SHA fe7b61c); ship audit 9331f531-c09b-4835-bbef-475720cff22e. SPA-only; no new ADR. 2026-06-05 BST
W11-PR-9 ADR full ratification UI (B-ADR-01, F-DEF-05). The ADRs tab dead-ended ("No ADRs ratified for this engagement yet") and the Decisions tab could mint a draft ADR with no view/edit/ratify path. PR-9 adds the full ADR detail experience, instantiating the ratified ADR-W11-1 human-CRUD HS-4L pattern for the adrs entity (mirror of the shipped sp_decision_* family). Built via the local Copilot CLI (non-interactive copilot -p, parent-reviewed diff) per the build-recipe. Substrate (parent-applied under SET ROLE haleon_aiac_owner; Steward-attested PASS; chain OK): two new HS-4L SPs — sp_adr_edit (byte-mirror of sp_decision_edit: #variable_conflict use_column, HS-4L role guard, no-op refuse, replay-first on (client_op_id + triggered_by='ui.adr_edit'), xmin OCC, body-field allowlist {title, body_md, tags, repo_path} only — NEVER review_stage/status/provenance/identity per ADR-W11-1 §3.2), and sp_adr_add_note (byte-mirror of sp_decision_add_note WITH replay guard, target_table='adrs'); both GRANT EXECUTE TO w10_app. Ratify REUSES the existing sp_advance_adr_stage CAS transition (ISS-19/Fix-G contract + loop-publish-on-ratified hook) — NOT a new SP; Edit and Ratify stay two distinct verbs. NO soft-delete for ADRs (ADR-W11-1 §3.2 — ratified ADRs are load-bearing; supersession via supersedes_id is the substitution path); sp_adr_soft_delete/_undelete explicitly dropped. NO new ADR (pure instantiation; SA cold-spawn successor concurred, Steward attested PASS). tRPC: read.adrs.get Pattern-A projects tags/supersedesId/xmin off the base table; new createMutAdrsRouter (addNote + edit + advanceStage, zero-rows→CONFLICT) wired into root.ts. SPA: adrs.$id.tsx full rewrite — BackLink, stage pill, edit form (allowlisted, xmin OCC + dirty-diff + CONFLICT refetch), ratification panel (advance / Ratify modal-confirmed / send-back), NotesList + floating NoteEntryForm, ActivityCollapsible. Live smoke GREEN (engagement haleon): auth + list(5) + get(xmin/tags/supersedesId) + addNote(replay-idempotent) + edit(xmin OCC + revert) + advanceStage(draft↔reviewed CAS round-trip). Tests: API 344 (+11) + SPA 239 pass; tsc clean; build clean; function count stays 4; 0 drift-bombs; generic-framing clean. PR #35 (squash-merge SHA 42bf8bb); ship audit 565b4a99-00bc-436b-9633-360eefc2ab53. Citations: SA-ADR-W11-1 §3 row 6 + §3.2 + §4 + §5.2; ISS-19/Fix-G (sp_advance_adr_stage CAS). NO new ADR (SA concur + Steward attest PASS). 2026-06-05 BST
W11-PR-8.3 Risk terminal-state actions + Last-seen-by viewer formatting. Risk detail page: terminal-state (closed/realised) actions are disabled-not-hidden (affordances stay visible but inert) and "Add note" remains always-on; the audit "last-seen-by" badge renders a clean human: strip. SPA-only (no substrate change). Built + deployed via an isolated git worktree to avoid a shared-tree collision with a parallel helper session. Tests: SPA 244 pass; function count stays 4; live-verified. Shipped by W11-WaveDriver-#4; roadmap row backfilled by #5 (rotation-boundary dropped seal — ship row was in-spine, chain intact). PR #34 (squash-merge SHA b7c9847); ship audit e78bdbfa-af8f-4c69-ab3a-926bb15325d4; backfill seal by #5. SPA-only; no new ADR. 2026-06-05 BST
W11-PR-8.2 Risks 5×5 board cell drill-down (multi-select). The Risk 5×5 likelihood×severity board supports clicking a cell to drill into the risks plotted there, with multi-select across cells. SPA-only (no substrate change). Tests: SPA 239 pass; function count stays 4; live-verified. Shipped by W11-WaveDriver-#4; roadmap row backfilled by #5 (rotation-boundary dropped seal — ship row was in-spine, chain intact). PR #33 (squash-merge SHA 0f4dc04); ship audit 73f9bf90-3a19-4c20-a4b5-ab03b0d1758b; backfill seal by #5. SPA-only; no new ADR. 2026-06-05 BST
W11-PR-8.1 Risks-Hotfix — 6 bugs (B-NEW-01..06): "captured but never displayed back" + 2 SP write-path corrections. Charlie was actively blocked on the Risk tab; all 6 share the root that data was persisted but never rendered (mirror of X-NOTES-01), plus two SP body fixes. Substrate (parent-applied: realised_at ALTER ~13:30 BST as B-NEW-01 crash-fix; apply-pr8.1-ddl.py ~14:10 BST for 2 retrofits; CoS-#3-authored; owner haleon_aiac_owner; F-W11-PR7-OWNER-1 owner-probed clean): ALTER TABLE haleon.risks_issues_blockers ADD COLUMN realised_at timestamptz NULL; 2 SP body retrofits via pg_get_functiondef + targeted UPDATE...SET regex-inject + CREATE OR REPLACE (PR-7 paste_inbound precedent — preserves full live body, adds only the fix; idempotent markers): (1) sp_risk_de_escalate — inject severity = GREATEST(severity - 1, 1) so de-escalate is the inverse of sp_risk_escalate's +1 (B-NEW-03; CLAMP not hard-RAISE — SA-approved: a hard severity_floor RAISE would block a valid mitigating→open de-escalation at severity=1; escalate's ceiling-RAISE is OK because escalate is severity-only); (2) sp_risk_edit — inject realised_at = CASE WHEN p_status='realised' AND status<>'realised' THEN now() ELSE realised_at END stamping on transition INTO realised (B-NEW-01 forward; uses pre-update status column). §8g: bogus-id phantom-column flush 2/2 P0001 + real-row SAVEPOINT round-trip verified de-escalate severity 3→2 + realised_at stamping then ROLLBACK; chain OK delta=0. NO new ADR (both within ratified ADR-W11-2 scope; SA explicitly approved clamp-not-raise + realised_at as §2.6.1 lifecycle-matrix gap-fill). SA MIGRATION-NOTES N11: realised_at added live mid-wave (outside PR-5 apply window) — Steward gap-scan note. tRPC: read.risks.board widened additive (cells unchanged + unscored [active risks w/ NULL likelihood OR severity] + totalActive; 42P01→empty shape); mut.risks.edit/deEscalate handlers UNCHANGED. SPA: risks.$id.tsx +239 net lines — real Edit form wired to the LIVE mut.risks.edit 12-field allowlist (xmin OCC + dirty-track + clear-sentinels), replacing the alert() stub at line 555 and re-adding the editMutation removed at line 211 (closes the long-owed W11-PR-5.1 inline-edit deferral — B-NEW-04); persistent Mitigation Plan display-on-load section (<MarkdownBody> + owner + target date + empty-state; Set-Mitigation drawer retained — B-NEW-05); lastStateChangeReasonMd rendered near the status badge (B-NEW-06); realisedAt/closedAt rendered in the metadata block (B-NEW-01 display). risks.index.tsx +34 net lines — "Unscored / not-yet-plotted" bucket below the 5×5 matrix + "Showing X of Y active risks (Z unscored)" caption (B-NEW-02). Live smoke GREEN: read.risks.board cells:2, unscored:2, totalActive:7 (the 2 NULL-likelihood risks now surface; total matches Charlie's 7 active); de-escalate severity 3→2 verified live. (B-NEW-07 ADR-W11-3 owner fixed separately by SA via direct UPDATE.) Tests: API 333 (+3) + SPA 237 (+9) pass; tsc clean; build clean; function count stays 4; 0 drift-bombs. PR #32 (squash-merge SHA 7302fd6); ship audit 1265272f-94b6-444d-bd4e-b9cd57f8b39f. Citations: SA-ADR-W11-2 (risk state-machine + §2.6.1 lifecycle-matrix) + SA-ADR-W11-1 §3 + §5.2 + ADR-W10-7. NO new ADR (SA-approved bug-fixes within ratified scope). 2026-06-05 BST
W11-PR-8 Recognition full CRUD + dual-status (peer/manager) + milestone enum widen (10) + custom label (B-REC-01/02/03/04 closed; B-MET-01/02 → PR-17). FIRST cross-schema CRUD in W11 — entity rows in portfolio.recognitions; audit spine stays haleon.agent_runs/agent_run_payloads (W10 PR#16 SECURITY DEFINER precedent; Cardinal Rule 5 intact). Substrate (parent-applied 2026-06-05 ~13:04 BST single-TX under SET ROLE haleon_aiac_owner via apply-pr8-ddl.py; CoS-#3-authored byte-mirrored from sp_decision_* uuid-form + PR-7 §8g standard; F-W11-PR7-OWNER-1 applied clean — owner-owned family, NO admin RESET-ROLE dance): 4 additive ALTERs on portfolio.recognitions (deleted_at + custom_label + peer_status text NOT NULL DEFAULT 'none' + manager_status text NOT NULL DEFAULT 'none'); derived one-shot backfill of dual-status from sent_variant+status+*_draft_md BEFORE 2 new CHECKs (ck_recognition_peer_status + ck_recognition_manager_status, each none/proposed/drafted/sent/skipped) per SEEDVAL-1 ordering; milestone_kind CHECK widened DROP+ADD to 10 values (+first_customer_demo/q_close_win/kickoff_complete/mid_engagement_review/final_handback; existing values strict subset). recognitions_status_check UNCHANGED (DERIVE-AND-MAINTAIN — legacy scalar status kept coarse for back-compat). 8 new portfolio HS-4L SPs: sp_recognition_edit (10 args; 5-field allowlist note/milestone_kind/milestone_at/manager_upn/custom_label; '' empty-string clear sentinel for TEXT cols; custom_label_required SP-body coupling guard), sp_recognition_save_draft (7 args; dual-status-aware draft replacement — legacy draft SP could not set the new fine status axes; sets peer/manager_draft_md + matching *_status='drafted' + derived status; B-REC-01 Save-as-draft routes here), sp_recognition_soft_delete/_undelete (5 args each), sp_recognition_mark_peer_sent/_mark_manager_sent (5 args; distinct-verb per-channel sends; mgr guards recognition_no_manager), sp_recognition_revert_peer_sent/_revert_manager_sent (6 args; revert-with-controls — REQUIRED p_reason_mdrevert_reason_required, persisted to payload). All raise hs4l_violation short-form + #variable_conflict use_column + xmin OCC text-over-wire + replay-first on client_op_id keyed triggered_by='ui.recognition_*' + 10-arg positional sp_create_agent_run session_id='w11-pr8' + payload target_table='recognitions'+target_id per ADR-W10-7. GRANT USAGE ON SCHEMA portfolio + EXECUTE×8 to w10_app (cross-schema). Legacy sp_recognition_draft/_mark_sent/_flag_milestone/_mark_skipped kept byte-frozen (customLabel-on-create via flag→edit tRPC chain — NO SP overload, ambiguity landmine avoided). §8g valid-id SAVEPOINT round-trip: all 8 SPs exercised (dual-status flows + custom_label_required + revert_reason_required + save_draft) then ROLLBACK; bogus-id phantom-column flush 8/8 P0001. Steward post-attest ALL GREEN: 4 cols, 2 CHECKs, milestone widen, 8 SPs correct arg counts, all owner-owned, 8 grants, chain OK 1254. 0 view migration (Pattern A per ADR-W11-1 §5.2). 0 v_human_touchpoints migration (cross-schema entity auto-surfaces via target_table/target_id). tRPC: 8 new mutations (edit/saveDraft/softDelete/undelete/markPeerSent/markManagerSent/revertPeerSent/revertManagerSent); read.list/get STOP hiding status='sent' (B-REC-02 History fix) + surface peerStatus/managerStatus/customLabel/deletedAt + includeDeleted Pattern A; flagMilestone widened (10-value enum + customLabel chain); legacy draft/markSent deprecation-commented. SPA: recognitions.index.tsx +561 net lines — dual-status Peer/Manager badges; History/?showDeleted= toggle (sent rows visible); editable draft textarea + Save-as-draft; per-channel Send buttons (mgr disabled w/o manager_upn); revert modals with verbatim LOAD-BEARING copy ("This does NOT un-send any email that was already delivered. The reversal will only update this recognition's tracking status. Reason for reversal:") + required reason textarea; custom_label input on milestone_kind='custom'; widened milestone dropdown; MarkdownBody/NoteEntryForm/NotesList/ActivityCollapsible/BackLink mounts; Edit/Delete/Restore affordances. 3 helpers: recognitionErrors.ts (16 mappings) + auditFilters.ts +7 verbs + recognitionStateMachine.ts (10 kinds). Tests: API 330 (+29) + SPA 228 (+22) pass; tsc clean; build clean; function count stays 4; 0 drift-bombs. PR #31 (squash-merge SHA c6c3647); ship audit 1f2e343e-7d70-425b-a6b8-dde601002ad7. Citations: SA-ADR-W11-4 (adr_number=23, Adr 71f93dc7-79c2-4fb5-99a2-e444336eeda1, Decision 8bb17196-4202-4835-9a77-e76c68fda9cd, ratified 2026-06-05T12:52:36+01:00) + SA-ADR-W11-1 §3 row 5 + §5.2 + ADR-W10-1 (HS-4L) + ADR-W10-7 (entity-pointer) + ADR-W11-3 §2.3 (revert-with-controls) + W10 PR#16 (cross-schema SECURITY DEFINER). 2026-06-05 BST
W11-PR-7 Communications full CRUD + body-volume guards (B-COM-01/02/03 closed). Substrate (parent-applied 2026-06-05 ~11:42 BST single-tx under SET ROLE haleon_aiac_owner via apply-pr7-ddl.py; CoS-#2-authored byte-mirrored from PR-6; Steward pre-attest caught F-W11-PR7-2ORDER BY created_at on agent_runs — column is spawned_at; 1-line fix): 2 additive ALTERs on haleon.communications (deleted_at timestamptz + updated_at timestamptz). 3 new HS-4L SPs: sp_communication_edit (11 args; 6-field editable allowlist subject/body_md/recipient_upn/channel/related_decision/related_risk; body_too_large guard @ 100KB; zero-uuid sentinel for FK clear; nothing_to_edit guard), sp_communication_soft_delete/_undelete (5 args each; both set updated_at=now() per PR-6 precedent). All raise hs4l_violation short-form + #variable_conflict use_column + xmin OCC text-over-wire + replay-first on client_op_id + 10-arg positional sp_create_agent_run session_id='w11-pr7' + payload target_table='communications'+target_id per ADR-W10-7. 1 SP body-only retrofit: sp_comm_paste_inboundbody_too_large guard @ 100KB injected after HS-4L END IF via pg_get_functiondef+regex+CREATE OR REPLACE; signature/grants unchanged; legacy xid xmin + hs4l_role_guard long-form preserved for back-compat. GRANT EXECUTE×3 to w10_app. §8g valid-id SAVEPOINT round-trip (Steward closeout enhancement adopted): 5/5 assertions PASS then ROLLBACK — insert temp comm row + sp_communication_edit happy + edit-with-100001-char-body raises body_too_large + soft_delete + undelete + sp_comm_paste_inbound oversize raises body_too_large; bogus-id phantom-column flush 3/3 P0001 cleanly raised. 0 view migration (Pattern A per ADR-W11-1 §5.2). 0 CHECK widen (no state machine for comms). 0 v_human_touchpoints migration (variant-A auto-admits all new ui.communication_* verbs). tRPC: 3 new mutations communications.edit/softDelete/undelete; read.list/get default WHERE deleted_at IS NULL + includeDeleted flag + Pattern A inline filter; pasteInbound zod widen 10KB→100KB to match SP cap. SPA: communications.$id.tsx +275 net lines — <ReactMarkdown><MarkdownBody truncate={5000}> at 2 sites (component already supports truncate-with-expand + quoted-history accordion split on \n>>> / "On <date> wrote:" per UX-Contract §11; ZERO component change owed); mount <NotesList entityTable="communications"> + floating <NoteEntryForm> + <ActivityCollapsible> + <BackLink>; dropped 235 lines of inline note/activity drawers; 6-field edit mode with Edit/Save/Cancel; Delete/Restore affordances + confirm modal. communications.index.tsx +162 net lines — ?showDeleted= URL toggle; inline Edit/Delete/Restore icons; strikethrough + 🗑 deleted tag for soft-deleted rows; xmin on row state for OCC. 2 new helpers: communicationsErrors.ts (7 mappings) + auditFilters.ts +3 verbs. Tests: API 301 (+19) + SPA 206 (+10) + helpers 10 (new) pass; tsc clean; build clean; function count stays 4. PR #30 (squash-merge SHA 2401a90); ship audit e716625f-9c16-4257-bf21-1954ed8a0d3c. Citations: SA-ADR-W11-1 §3 row 4 + §5.2 amendment + ADR-W10-1 (HS-4L) + ADR-W10-7 (entity-pointer promotion) + ADR-W10-9 §3.3 (variant-A touchpoint filter) + W11-UX-CONSISTENCY-CONTRACT.md §11. 2026-06-05 BST
W11-PR-6 Workstreams (use_cases) full CRUD + 3 distinct backward-verb SPs + Kanban backward moves + responsive Grid (B-WS-01/02/03/04/05/06 closed). Substrate (parent-applied 2026-06-05 ~10:23 BST single-tx under SET ROLE haleon_aiac_owner via parent-authored apply-pr6-ddl.py; Steward pre-attest caught F-W11-PR6-1tags column type mismatch text[] vs live jsonb; 3-site fix applied; post-state Steward attest GREEN): 2 additive ALTERs on haleon.use_cases (deleted_at timestamptz + last_state_change_reason_md text). 6 INSERTs into haleon.use_case_state_machine (11→17 rows) per ADR-W11-3 §2.1: live→mobilising (reason_required=false — SA "no-shame edge"), at_risk→mobilising / wound-down→mobilising / wound-down→live / cancelled→mobilising / cancelled→live (reason_required=true); archived→* PERMANENTLY DENIED — revival via supersedes_id mechanic per SA §2.2 (re-instantiation, not state-change). 6 new HS-4L SPs per ADR-W11-3 §4: sp_use_case_edit (12 args; 7-field editable allowlist title/summary/owner_email/pod_id/business_value_score/decision_id/tags; FK validate decision_id; nothing_to_edit guard; compliance_state OUT per SA §2.6), sp_use_case_soft_delete/_undelete (5 args each; no type scope — single-purpose table), sp_use_case_revert_to_mobilising (6 args; multi-from live/at_risk/wound-down; reason OPTIONAL from live; clears went_live_at always + wound_down_at if from wound-down per SA §2.6.1), sp_use_case_resume (6 args; wound-down→live only; reason ALWAYS required; clears wound_down_at preserves went_live_at per SA §2.6.1), sp_use_case_uncancel (7 args; cancelled→{live|mobilising} via p_target_state; reason ALWAYS required; clears cancelled_at + sets went_live_at=now() IF target=live AND prev NULL per SA §2.6.1). All include #variable_conflict use_column prelude + line-1 hs4l_violation guard (matches existing useCases convention NOT PR-4/PR-5's long form) + xmin OCC (text-over-wire with ::xid cast) + replay-first on client_op_id + single-tx audit via 10-arg positional sp_create_agent_run session_id='w11-pr6' + payload contract per ADR-W10-7 (target_table='use_cases' + target_id + before/after + from_status/to_status/reason_md where applicable). GRANT EXECUTE×6 to w10_app. 0 view migration per ADR-W11-1 §5.2 amendment (Pattern A only; no v_use_cases_current). 0 CHECK widen: state machine is SP-enforced + table-driven (matches F31 precedent). 0 v_human_touchpoints migration: variant-A LIKE 'ui.%' filter auto-admits all 6 new ui.use_case_* verbs (W10-9 §3.3). tRPC: 6 new mutations useCases.edit/softDelete/undelete/revertToMobilising/resume/uncancel with appropriate zod (uncancel targetState enum + reasonMd REQUIRED; resume reasonMd REQUIRED; revertToMobilising reasonMd optional — SP enforces required-when-from-not-live); read.useCases.list/get default WHERE deleted_at IS NULL + optional includeDeleted + deletedAt surface (Pattern A inline filter); mut.useCases.transition zod UNCHANGED (distinct-verb posture per ADR-W11-3); legacy create/transition/addNote/assignOwner SPs retained verbatim. SPA: use-cases.$id.tsx +814/-x wires <NotesList entityTable="use_cases"> + floating <NoteEntryForm> + <ActivityCollapsible> + <BackLink label="Workstreams"> + <MarkdownBody body={summary}> (zero react-markdown direct imports); 7-field inline edit form; SoftDelete/Undelete affordances; 3 dedicated backward-transition modals (revert-to-mobilising with conditional reason-required logic; resume with always-required reason; uncancel with SA-mandated verbatim confirm copy "You are reversing a cancellation. Consider whether you should create a new workstream that supersedes this one instead. Reason for reversal:" rendered as visible <p> + target-state radio + required reason — drag-drop CANNOT bypass this modal); use-cases.index.tsx +585/-x adds status pill toolbar (6 statuses + 'all' persisted to ?status=) + ?showDeleted= toggle with strikethrough + inline Restore + inline Edit/Delete + responsive Kanban CSS Grid swap (display:grid + gridTemplateColumns:repeat(auto-fit, minmax(220px, 1fr))) replacing flex+overflowX:auto + drag-drop routing via new exported backwardSpFor() helper opens dedicated modals pre-filled with card context. useCaseStateMachine.ts widen 11→17 LEGAL_EDGES + 8→13 REASON_REQUIRED (live→mobilising excluded per SA no-shame); useCaseErrors.ts +5 cases; auditFilters.ts +6 action labels (UI vocab "Workstream" per ADR-W10-8). Engagement chrome touched 1-line for type-required Link search prop scaffold. Substrate smoke 17/17 PASS (HS-4L / xmin OCC / edit happy / nothing_to_edit / revert-from-live-no-reason + went_live_at cleared / reason-required guard / revert-from-at_risk-with-reason / resume-from-wound-down with wound_down_at cleared + went_live_at preserved / resume_reason_required guard / illegal_transition guard / uncancel-to-mobilising with cancelled_at cleared / invalid_target_state guard / soft_delete / already_deleted guard / undelete / v_human_touchpoints surfaces all 6 new verbs / chain integrity). F-W11-PR5-1 7f phantom-column dry-run flush 6/6 P0001 raised cleanly. Vitest 196 SPA tests pass + tsc clean + build clean; function count stays 4. PR #29 (squash-merge SHA 162c4d9); ship audit fcd97474-e3bc-48d5-a9ea-7eef200e35c1. Citations: SA-ADR-W11-3 adr_number 22 Adr c2676082-d31b-44d3-a191-57b13f6ef3ec Decision f02eb9c5-daf2-44f9-957c-103e7c119fd1 ratified 2026-06-05 10:11:32 BST + SA-ADR-W11-1 §3 row 3 + §5.2 amendment + ADR-W10-1 (HS-4L) + ADR-W10-7 (entity-pointer promotion) + ADR-W10-8 (vocabulary boundary use_case/Workstream) + ADR-W10-9 §3.3 (variant-A touchpoint filter) + W11-UX-CONSISTENCY-CONTRACT.md §7+§8+§9+§10. 2026-06-05 BST
W11-PR-5 Risks full CRUD + state-machine transitions + structured mitigation (B-RISK-01/04/05/06 closed; B-RISK-02 partial — substrate + delete + setMitigation live, inline free-text edit deferred to W11-PR-5.1). Substrate (parent-applied 2026-06-05 ~08:32 BST single-tx under SET ROLE haleon_aiac_owner; CoS-authored apply-pr5-ddl.py 1011 lines + parent rib_status enum-cast patch lines 191/515; Steward pre-attest F-W11-PR5-1 caught phantom-column refs category+target_resolution_date in sp_risk_edit body; Charlie ratified Fix A at 08:30 BST — both promoted to first-class columns; post-state Steward attest GREEN): 7 additive ALTERs on haleon.risks_issues_blockers (deleted_at timestamptz + mitigation_plan_md/owner_upn/target_date + last_state_change_reason_md text + category text + target_resolution_date date). 8 new HS-4L SPs per ADR-W11-2 §4: sp_risk_create (10 args), sp_risk_edit (17 args; 11-field editable allowlist; runtime guard rejects reason-requiring status transitions per §3.10 matrix), sp_risk_close (6 args; mitigating→closed no-reason / open→closed reason-required), sp_risk_reopen (7 args; closed→{open,mitigating} reason ALWAYS required; p_target_state param), sp_risk_de_escalate (6 args; mitigating→open reason required), sp_risk_soft_delete/_undelete (5 args each; type='risk' scope; byte-mirror of sp_decision_soft_delete/_undelete), sp_risk_set_mitigation (8 args; 3 mitigation fields with '0001-01-01' NULL-sentinel; does NOT auto-transition status). All include #variable_conflict use_column prelude + line-1 hs4l_violation guard + xmin OCC (text-over-wire with ::xid cast) + replay-first on client_op_id via existence-scan + single-tx audit via 10-arg positional sp_create_agent_run session_id='w11-pr5' + payload contract per ADR-W10-7 (target_table='risks_issues_blockers' + target_id + before/after + reason/from_status/target_state where applicable) + shared-table WHERE id=$1 AND type='risk' scope. GRANT EXECUTE×8 to w10_app. 0 view migration per ADR-W11-1 §5.2 amendment (Decision 625bbaba-… — Pattern A everywhere on mutation-source reads; F-W11-VIEW-XMIN-1 resolved). 0 CHECK widen: reopened is derived projection per ADR-W11-2 §4 (EXISTS subquery against agent_run_payloads WHERE triggered_by='ui.risk_reopen') NOT a new rib_status enum value. 0 v_human_touchpoints migration: variant-A LIKE 'ui.%' filter auto-admits all 8 new ui.risk_* verbs (W10-9 §3.3). tRPC: 8 new mutations risks.create/edit/softDelete/undelete/close/reopen/deEscalate/setMitigation; read.risks.list/get/board default WHERE deleted_at IS NULL + optional includeDeleted + reopenedDerived EXISTS projection on list/get + 7 new field surface; 11-field editable allowlist on edit; status enum narrowed to non-reason-requiring transitions only at SP body. Legacy startMitigation/escalate/assignOwner SPs retained deprecation-commented (not deleted). SPA: risks.$id.tsx +854/-50 wires <NotesList> + floating <NoteEntryForm> + <ActivityCollapsible> + <BackLink> + <MarkdownBody> (zero react-markdown direct imports); 3 transition modals (Close conditional-reason / Reopen target_state+reason / De-escalate reason-only) + Mitigation drawer (with explicit copy "Setting a mitigation plan does NOT automatically move the risk to mitigating — click Start mitigation separately.") + Delete confirm modal + Restore button on deleted + reopened-derived badge next to status pill + ?showDeleted=true URL toggle + strikethrough on deleted title; risks.index.tsx +443/-20 adds Add-risk modal + showDeleted toggle + strikethrough + inline Restore + Delete action column. New helpers: riskErrors.ts (8 cases) + riskStateMachine.ts (LEGAL_EDGES per §3.10 matrix verbatim; NO TODO since ADR-W11-2 ratified). Inline Edit form for free-text fields deferred to W11-PR-5.1 — current Edit button shows informative stub. Substrate smoke 12/12 PASS (HS-4L / xmin OCC / edit / set_mitigation / close-from-open-w-reason / reopen-to-mitigating / de-escalate / soft-delete / default-read excludes / undelete / touchpoint feed surfaces all 7 new verbs / chain integrity). Live HTTP smoke PASS (auth GREEN; read.risks.list 200 with 4889-byte payload; 4 functions unchanged). Vitest 473 (282 API + 191 SPA), typecheck clean, both builds green. PR #28 (squash-merge SHA 49857cd); Function deploy ee94e5ba-…; ship audit adb12a2a-75d1-4c5e-ac7f-82587fc9cbb3. Citations: SA-ADR-W11-1 §3 row 2 + §5.2 amendment Decision 625bbaba + SA-ADR-W11-2 adr_number 20 Adr f356c050 Decision e92b2ba2 + ADR-W10-1 (HS-4L) + ADR-W10-7 (entity-pointer promotion) + ADR-W10-9 §3.3 (variant-A touchpoint filter) + W11-UX-CONSISTENCY-CONTRACT.md §7. 2026-06-05 BST
W11-PR-4 Decisions full CRUD + notes-display retrofit + Draft-ADR fix tail (Phase 2 kickoff). Wires the human-CRUD posture from ADR-W11-1 onto the Decisions tab end-to-end. Substrate (applied 2026-06-05 05:21 BST by predecessor; Steward attested PASS via probe20 — 12 SP-body checks × 3 SPs + view def + grants + Cardinal Rule 5 preservation): ALTER TABLE haleon.decisions ADD COLUMN deleted_at timestamptz NULL; v_decisions_current Pattern B view; 3 HS-4L SPs per ADR-W11-1 §4 — sp_decision_edit(p_decision_id, p_title, p_body, p_decision_class, p_status, p_narrative, p_owner_upn, p_portfolio_shareable, p_expected_xmin, p_actor_upn, p_actor_role, p_client_op_id) (12-arg; 7-field editable allowlist per SA ratification — title/body/decision_class/status/narrative/owner_upn/portfolio_shareable) + sp_decision_soft_delete + sp_decision_undelete (5-arg each); all line-1 hs4l_role_guard, xmin OCC, replay-first on client_op_id, payload writes target_table='decisions'+target_id+before/after. GRANT EXECUTE×3 to w10_app. v_human_touchpoints auto-admits new ui.decision_edit/ui.decision_soft_delete/ui.decision_undelete verbs via variant-A LIKE 'ui.%' filter — NO view migration owed (verified at smoke: new verbs surface in feed). tRPC: read.decisions.list + read.decisions.get default WHERE deleted_at IS NULL + optional includeDeleted: boolean flag; mut.decisions.edit + mut.decisions.softDelete + mut.decisions.undelete; status enum widened to 7 labels (proposed/accepted/decided/superseded/rejected/ratified/resolved); nothing_to_edit BAD_REQUEST refinement; ownerUpn validation loosened to z.string().min(1).max(320) matching the existing assignOwner precedent (CoS G3 inline fix). SPA: Decisions detail wires <NotesList>, floating <NoteEntryForm> overlay (replaces inline form), <MarkdownBody>, <ActivityCollapsible>, <BackLink>; new Edit/Delete/Undelete action-bar buttons with 7-field inline edit form + dirty-tracking + xmin OCC; list view has ?showDeleted=true toggle + strikethrough + inline Restore. W11-PR-4.1 hotfix (same wave-ship): live HTTP smoke caught column "xmin" does not exist on read.decisions.list — Postgres views don't propagate system columns; switched read paths from v_decisions_current view to base haleon.decisions table with inline AND deleted_at IS NULL filter (view stays in place for SQL-direct reads). Substrate smoke 9/9 PASS (HS-4L guard / xmin OCC / soft-delete happy / default-read excludes / replay-first idempotent / undelete / default-read includes / touchpoint feed surfaces new verbs / chain integrity OK at 1015 rows pre-ship). Live HTTP smoke PASS (auth GREEN; health.ping 200; read.decisions.list default 200 with 15KB payload; read.decisions.list includeDeleted=true 200; 4 functions unchanged). Vitest 415 (260 API + 155 SPA), typecheck clean. Builds green (API esbuild 2.5MB / SPA Vite 1.96MB). Cardinal Rule 5 invariants held. F-W11-SEAL-1 corrected (Steward-discovered): roadmap-seal e56997c3-... payload referenced a corrupted PR-3 audit uuid (a6f71fe5-eb44-... vs real a6f71fe5-6eb6-... — first-8-chars-match copy-paste error); appended class='correction' row c8786c4a-... with detail_ref=<bad_row> + correct uuid per ADR-W10-1 §3.5(d); this roadmap-seal additionally fixes the cosmetic data-pr="w11-3" attribute. PR #26 (squash-merge SHA 777faf8) + PR #27 (W11-PR-4.1 hotfix SHA 4a53417); Function deploys 11865d45-... (initial) + 6ab510f5-... (hotfix); ship audit 5517d0b1-726e-41c1-96be-e7c188d5ce32; correction audit c8786c4a-aae3-4888-b777-e1e0470c943c. Citations: SA-ADR-W11-1 §2-§3 (soft-delete mechanism + per-entity matrix) + ADR-W10-1 (HS-4L) + ADR-W10-7 (entity-pointer promotion) + ADR-W10-9 §3.3 (variant-A touchpoint filter) + W11-UX-CONSISTENCY-CONTRACT.md §3 (notes display) + §6 (floating note form) + §7 (per-entity CRUD matrix). 2026-06-05 BST
W11-PR-3 Bug-batch wave 1 (6 bugs, parallel Haiku diagnosticians for non-trivial root causes). B-REP-01: Reports By Date sort threw b.asOfDate.localeCompare is not a function — date-vs-string coercion mismatch; fix coerces to ISO string in the sort comparator. B-RISK-03: sp_risk_add_note failed with column "run_id" does not exist — copy-paste bug from another SP body; rewrote SP to use the correct join key + normalised return-shape from scalar uuid to TABLE matching every other *_add_note SP family (side-benefit uniformity). B-DOC-01: Documents tab threw permission denied for table documents on select — w10_app role missing SELECT grant; Steward replay GRANT SELECT ON haleon.documents TO w10_app. B-SES-01: Sessions tab "Failed to fetch" on load — diagnostician traced to missing tRPC proc registration in root.ts after PR-2 router refactor; restored. B-DEC-03: Decisions detail had no back arrow — wired the PR-2 <BackLink> component. B-DEC-02: "Draft ADR" from Decisions threw "ADR not found in this engagement" — ADR-create SP routed payload through the wrong engagement scope; fixed scope lookup + added engagement-id assertion. Each bug fix gets its own commit + vitest regression test. Substrate smoke 30+/30+ PASS via psycopg2 (chain OK at 980 rows post-ship). 4 functions unchanged. Cardinal Rule 5 invariants held. PR #25 (squash-merge SHA c00da1b; DDL applied parent-side via apply-pr3-ddl-risk.py + apply-pr3-ddl-rest.py single-tx under SET ROLE haleon_aiac_owner; CLI iter-0 single-pass with 3 parallel Haiku diagnostician helpers diag-b-doc-01 / diag-b-ses-01 / diag-b-dec-02 — saved ~30-40% wave-driver context); ship audit a6f71fe5-6eb6-4022-a6af-d4ced7acd149. Citations: Cardinal Rule 5 + ADR-W11-1 + ADR-W10-1 (HS-4L) + ADR-W10-7 (entity-pointer promotion). 2026-06-05 BST
W11-PR-2.1 Hotfix: audit.forEntity column-drift. PR-2 tRPC proc read.audit.forEntity({entityTable, entityId}) was authored against the pre-ADR-W10-7 v_human_touchpoints per-branch CASE projection, but PR-2 simultaneously consumed the W10-7-promoted top-level entity_table/entity_id columns — the SELECT projected the wrong column names. Surfaced when <ActivityCollapsible> rendered empty rows for every entity on Decisions detail. Fix: align the SELECT to use the top-level columns per ADR-W10-7 §3.2; vitest added asserting top-level column presence. Single-SPA-file hotfix; no substrate change; Function redeploy unaffected. PR #24 (squash-merge SHA 09f719f); ship audit 5c48f657-0c39-42d3-b527-059c83981ba4. Citations: ADR-W10-7. 2026-06-04 BST
W11-PR-2 Foundation retrofit: 5 shared SPA components + notes CRUD substrate (CoS-led BUILD-SPEC, single multi-file PR). Ships the UX-consistency contract foundation that every Phase 2 per-tab PR depends on. 5 shared components (apps/spa/src/components/): <MarkdownBody body truncate?/> (react-markdown wrapper + prose CSS line-height 1.6 + max-width 72ch + truncate-with-expand for >5KB bodies + quoted-history accordion helper for emails); <NotesList entityTable entityId/> (calls new read.notes.forEntity; renders cards with <MarkdownBody> + inline edit/delete affordances + showDeleted toggle); <ActivityCollapsible entityTable entityId/> (native <details> element, default collapsed, calls read.audit.forEntity consuming ADR-W10-7-promoted top-level entity_table/entity_id columns); <BackLink to label/> (Reports back-link precedent); <NoteEntryForm entityTable entityId onSaved/> (floating overlay/modal per Charlie's preference). Notes substrate widen (Steward-attested, applied parent-side via apply-pr2-ddl.py): ALTER TABLE haleon.notes ADD COLUMN deleted_at timestamptz NULL (additive); 3 new HS-4L SPs per ADR-W11-1 §4 verbatim — sp_note_edit(p_note_id, p_body_md, p_expected_xmin, p_actor_upn, p_actor_role, p_client_op_id) + sp_note_soft_delete(...) + sp_note_undelete(...); all line-1 hs4l_role_guard per ADR-W10-1 §3, xmin OCC, replay-first on client_op_id, single-tx audit via sp_create_agent_run, payload writes target_table+target_id+before+after per ADR-W10-7. v_human_touchpoints auto-admits new ui.note_edit/ui.note_soft_delete/ui.note_undelete verbs via variant-A LIKE 'ui.%' filter (ADR-W10-9 §3.3) — NO view migration owed. GRANT EXECUTE×3 + SELECT on filtered-default read view to w10_app. tRPC: read.notes.forEntity({entityTable, entityId, showDeleted?}) + mut.notes.edit({noteId, bodyMd, expectedXmin, clientOpId}) + mut.notes.softDelete(...) + mut.notes.undelete(...); read.audit.forEntity reusing the W10-7 top-level columns. SPA: Decisions detail rendered with read-only <NotesList> wire as smoke-validation surface (full edit-affordance integration retrofitted in W11-PR-4). Substrate smoke 26/26 PASS (HS-4L guard / stale-xmin / happy / idempotent / soft-delete + undelete round-trip / touchpoint surfacing / chain integrity / target_table+target_id payload assertion). Vitest 388/388 PASS (243 API + 145 SPA). Typecheck green. Builds green. 4 functions unchanged. Cardinal Rule 5 invariants held. PR #23 (squash-merge SHA 367cc2b; DDL applied parent-side via apply-pr2-ddl.py single-tx under SET ROLE haleon_aiac_owner; Steward post-state attestation chain OK + view auto-admit verified; CLI iter-0 single-pass via short-inline-prompt + .w11/pr2-build-spec.md file-reference per W10 P15 lesson; SPA + Function deployed via the W10 reusable pipeline — Function deploy id 286cd134-...); ship audit 0227f983-0e14-42b4-8f80-3ec8b1d187bc. Citations: SA-ADR-W11-1 + ADR-W10-1 (HS-4L) + ADR-W10-7 (entity-pointer promotion) + ADR-W10-9 (variant-A touchpoint filter) + W11-UX-CONSISTENCY-CONTRACT.md §2-§6. 2026-06-04 BST
W11-PR-1 SA-ADR-W11-1 ratified: HUMAN CRUD on append-only audit spine (no code). Charlie's W11-opening directive ("the Human should be able to add/edit/delete everything. This isn't some communist compound.") collides with three W10 substrate invariants: (1) haleon.agent_runs is the universal hash-chained append-only audit spine — rows NEVER updated, NEVER deleted; (2) HS-4L (ADR-W10-1) governs every human UI write verb; (3) v_human_touchpoints (ADR-W10-2 + W10-7 + W10-9 §3.3) is the canonical peer-ring channel. Resolution (Cardinal Rule 5): soft-delete via additive deleted_at timestamptz NULL column + edit via raw UPDATE on the entity row with xmin OCC + audit-spine append carrying before/after jsonb payload — the entity row IS the current-state projection and is mutable by design; the audit spine carries the immutable history. Hard delete is forbidden at UI (Steward operator-attended ceremony only). Per-entity CRUD matrix ratified for 9 entity families (decisions/risks/use_cases/communications/recognitions/notes/adrs/documents/reports); SP-naming convention sp_<entity>_{edit,soft_delete,undelete} with HS-4L line-1 guard + W10-7 payload contract verbatim. v_human_touchpoints auto-admits all new ui.<entity>_<verb> verbs via variant-A pattern filter — NO view migration owed. ADR also includes view/read-proc semantics (Pattern A in-handler filter vs Pattern B v_<entity>_current canonical filtered view; per-PR call), ?showDeleted=true toggle contract, audit-spine display strikethrough cue, ADR-as-entity special-case (edit body fields only; soft-delete denied; supersession-marker pattern preserved). Adr DB row + Decision DB row written; no code shipped this PR. SHA (no-code); GH ; ADR file at ~/.copilot/m-policies/adr/SA-ADR-W11-1-HUMAN-CRUD-ON-AUDIT-SPINE.md; ship audit 9d410cf6-ebc0-46b1-b1e6-1d107db14509. Citations: ADR-W10-1 (HS-4L) + ADR-W10-2 (touchpoint feed) + ADR-W10-7 (entity-pointer promotion) + ADR-W10-9 (variant-A LIKE 'ui.%' filter) + W11-UX-CONSISTENCY-CONTRACT.md §1 + W11-BUG-INVENTORY.md X-CRUD-01. 2026-06-04 BST

W10 Wave 10 — Web reporting front end — opened-for-design 2026-06-02. Loop pivot. Successor to F29, F30, F32, F33, F34. MVP build COMPLETE — PR #1 + PR #2 + PR #2.5a + PR #3 + PR #4 + PR #5 + PR #6 + PR #7 + PR #8 + PR #9 + PR #10 + PR #11 + PR #12 + PR #13 + PR #14 + PR #14.5 + PR #15 + PR #16 + PR #17 shipped 2026-06-02/04. Wave 10 MVP COMPLETE.

Strategic pivot (Charlie, 2026-06-02 ~08:59 BST): Microsoft Loop retired as the engagement reporting surface in favour of a purpose-built web front end. "I want this front end to be my one-stop shop for any knowledge about a project. This + ADO."

Vision (operator-stated)

  • Single SWA, multi-project routing — one web app serves all engagements; per-project deep dives plus a portfolio overview page (no separate sites per engagement).
  • Same report types as the Loop catalogue — the 7 W7-C templates (overview, daily-status, weekly-rollup, steerco-prep, ratified-ADR-digest, incident-postmortem, engagement-survival) recycle as the report-type catalogue.
  • Interactive write-back (richer scope) — check-off open decisions, add notes, assign, change status. Not just read-only.
  • Two TOC organisations — by date and by report type, both as first-class navigation.
  • Metrics surface, freed from 5-3-2-1 — per-engagement + portfolio-wide dashboards; not constrained to a brief-header pill row.
  • Filtering + search — across reports, decisions, ADRs, risks.
  • One-stop knowledge surface — SA/Steward/CoS documents readable inline (or linked); peer-ring communications (DMs sent + responses) browseable if captured in DB.
  • Pre-sales project type — design must accommodate; similar to delivery, slightly different focus.

Design delivered shipped

Full design write-up delivered: tech stack, data path, page hierarchy, write-back semantics, search/filter UX, metrics integration, pre-sales extensibility, document/communication catalogue. Design doc: Clawpilot\wave-handoffs\WAVE-10-WEB-REPORTING-DESIGN.md. Ratified architecture decisions: SA-ADR-W10-1/2/3. Build proceeds as Wave 11 (MVP), PRs #1–6.

Inherited from Loop work (recyclable)

  • 7 report-content templates (W7-C) — structure + section taxonomies for each report type.
  • Engagement-survival schema (W7-E-v1) — 9 sections, vetted; becomes one report type.
  • Recognition-tracker logic + outbound-voice drafts (W8 F36) — needs a web surface instead of Loop.
  • SWA deploy mechanicsStaticSitesClient.exe fallback, C:\Temp\swa-cwd trick, API-token retrieval pattern. Directly transferable.
  • Durable corpus-sync CA Job — pattern reusable for any scheduled job in the new architecture.
  • F17 view tierportfolio.v_* views + aggregator probe; powers the portfolio overview.
  • F31 use-case lifecycle schema (W8) — primary data source for use-case reports.
  • F19 reuse measurement, F16 RIB, F10/library, audit-spine, decisions/ADRs — all DB-level, surface-agnostic, ready to query.

Shipped (rolling) live

MVP build ships PR-by-PR against smccormick_microsoft/clawpilot-w10. Manual deploy in MVP (GH Actions deferred to Phase 2). Newest first.

PRScopeShippedStatusLinks
PR #17 FINAL PR of Wave 10 — Metrics + Library + Audit viewer + Settings + Last-seen-by badge + Peer-spawn preview (L-complexity, multi-router substrate+UI delivery). Substrate: NEW app.sp_user_pref_save (SECURITY DEFINER, last-write-wins UPSERT on (upn,key), ADR-W10-9 §3.1 carve-out from HS-4L; #variable_conflict use_column directive resolves OUT-TABLE/table-column ambiguity in ON CONFLICT — P5 retrospective lesson) over the pre-existing app.user_prefs(upn,key,value,updated_at) table. 7 NEW haleon.v_metrics_* views (decisions_weekly, risks_weekly, use_case_transitions, costs, recognition_weekly, audit_velocity_daily, reuse_funnel) + 7 NEW portfolio.v_metrics_* alias views (single-engagement MVP; UNION ALL substrate-ready). Reuses pre-existing haleon.sp_record_read + haleon.sp_verify_chain. Corrective haleon.v_human_touchpoints WHERE narrowing per ADR-W10-9 §3.3: switched from fail-OPEN BLACKLIST to variant-A fail-CLOSED pattern (ar.class='turn' AND ar.triggered_by LIKE 'ui.%' AND triggered_by <> ALL (chrome-blacklist)) — closed pre-existing live leak of 56 read.row + 30 ddl.*/deploy.* rows that had been surfacing in the touchpoint feed. View re-CREATE preserved 12 cols + ORDER BY + all 10 CASE branches verbatim (column-order trap avoided per PR #16 lesson). All grants applied for w10_app. SA-ADR-W10-9 ratified (adr_number 18, DAB id fe1f5650-412b-493a-a62a-69749e5eb1e7): app.* schema scope as operator-personal cross-engagement chrome; user_upn scoping convention; m_record_row_read semantic = class='turn'+triggered_by='read.row' (W10-4 spine CHECK widen REJECTED — number stays RESERVED); GDPR posture for reader-tracking rows under engagement DPA; deliberate exclusion of app.* writes + reader-observation rows from v_human_touchpoints. tRPC: 5 NEW routers (metrics, library, audit, settings, context) wired into root.ts; namespaces read.metrics.{engagement,portfolio} + read.library.list + read.audit.{list,chainIntegrity,lastSeenBy} + read.settings.get + mut.settings.savePref + read.context.peerSpawnPreview; __setExecutor test-seam pattern; protectedProcedure; audit.lastSeenBy invokes sp_record_read as side-effect per ADR-W10-9 §3.2 (fire-and-log-on-fail; viewerRole = ctx.user.role ?? 'human:'+upn.split('@')[0]); NO row_hash/prev_hash leak to client. 5 NEW SPA routes: /metrics portfolio ECharts grid + /e/$engagement/metrics per-engagement grid + /e/$engagement/library (over existing portfolio.pattern_library+haleon.reuse_*) + /e/$engagement/audit (virtualised agent_runs table + chain-integrity badge via sp_verify_chain) + /e/$engagement/context/peer-spawn-preview (7-layer forensic read, per-request no cache, graceful FS degradation). <LastSeenBadge> component injected cross-cutting on 4 detail pages (decisions, risks, use-cases, ADRs — recognitions has no standalone detail route, drawer-only). ECharts via echarts-for-react + chartTheme.ts single-source palette with --cp-chart-1..8 tokens (defined in iter-2 Δ-A in apps/spa/src/index.css light + dark blocks; regression test pinned). 6 single-source helpers (settingsKeys, chartTheme, auditFilters, relativeTime, metricsPanels, settingsErrors). TanStack day-1 contract honoured (0 useEffect in new routes; trpcClient only in queryFn/mutationFn); drift-bombs clean (0 var(--color-*) in new files; 4 LastSeenBadge sites). Tests: API 234/234 + SPA 106/106 (was 233+105 post-iter-1, +1 lastSeenBy side-effect case + 1 chartTheme regression case in iter-2). Typecheck exit 0. 4 functions unchanged (roles + trpc + skillsCommunicationsRecordOutbound + timerRefreshSearch) — peer-spawn-preview rides trpc catch-all. Post-deploy substrate smoke: chain OK 738→739 rows (ship audit row appended), sp_user_pref_save upsert round-trip GREEN (post-hotfix), 14 v_metrics_* views populated, v_human_touchpoints read.row leak = 0. Peer ring: CoS READY-TO-SHIP, SA (fresh cold-spawn after rotation, ratified ADR-W10-9 + variant-A filter) co-signed READY-TO-SHIP, Steward substrate + source attested. Carry-forwards (W11+): Cnew-app-rls (RLS on app.user_prefs); Cnew-metrics-materialize (MV + Functions-timer refresh if any panel measures >50ms); Cnew-peerspawn-corpus-access (Flex Function FS for L1/L2 corpus-mirror); N6 (pre-existing ~20 hex literals in PR #11/#12 vintage detail pages). ADR numbers reserved: W10-4 (future agent_runs.class CHECK widen trigger); W10-5 (unassigned — available for W11+ first SA ADR). Wave 10 MVP COMPLETE. PR #21 (squash-merge SHA 0066209; DDL applied parent-side via apply-pr17-ddl.py single-transaction under SET ROLE haleon_aiac_owner; Steward pre-apply attestation PASS + column-order preservation verified 12/12; CLI iter-0 single-pass via short-inline-prompt + .w10/pr17-build-spec.md file-reference per P15; iter-1 fixed 22 API test failures + 3 typecheck errors + 3 LastSeenBadge injections; iter-2 fixed SA-flagged Δ-A (--cp-chart-N CSS tokens definition) + Δ-B (audit.lastSeenBy sp_record_read invocation); post-merge hotfix to app.sp_user_pref_save added #variable_conflict use_column directive for ON CONFLICT ambiguity — P5-class runtime bug not caught in dry-run (DDL apply doesn't invoke SP); SPA via StaticSitesClient.exe direct (upload verb, bool flags as switches not --flag true per current SSC version); Function via az functionapp deployment source config-zip "Deployment was successful"); ship audit 2f16c864-f6fc-405a-84f1-cf53db8a1f50 (chained after PR #16 ed3ca382). Citations: SA-ADR-W10-9 (app.* schema + read-tracking + GDPR) + ADR-W10-1 (HS-4L carve-out) + ADR-W10-2 (touchpoint-feed) + ADR-W10-7 (entity-pointer producer contract scoped to feed-eligible) + ADR-W10-8 (vocabulary boundary); design §11.9 + §11.10 + §11.14 + §11.16 + §18 + §24 + §26.1.6 + §26.1.7. 2026-06-04 BST
PR #16 F36 Milestone recognition tracker + thank-you drafting (S-complexity, substrate+UI+Function delivery). NEW portfolio.recognitions table (UNIQUE(engagement_id,person_upn,milestone_kind) + CHECK milestone_kind IN {first_workstream_live, first_decision_ratified, first_adr_authored, first_30_day_active, custom} + CHECK status IN {proposed,drafted,sent,skipped} + client_op_id uuid UNIQUE replay anchor + 2 indexes); portfolio.v_recognitions_feed projection (xmin+is_pending+has_*_draft); 4 SECURITY DEFINER SPs in portfolio.* (sp_recognition_flag_milestone / _draft / _mark_sent / _mark_skipped), all HS-4L-routed via cross-schema haleon.sp_create_agent_run per ADR-W10-1, xmin OCC on mutating SPs, replay-first on client_op_id, raw RAISE EXCEPTION USING ERRCODE='P0001' error contract. GRANT EXECUTE×4 + SELECT×2 to w10_app, zero direct DML grants. haleon.v_human_touchpoints additively extended per SA-ADR-W10-7 (Adr c9ff0609-2e4b-41cf-a508-b8acaeb44c35 / Decision 441c3590-e5d6-44e9-8e5b-e29d99a32792; W10-7-watch c81b1d12-... FIRED+FULFILLED via supersession marker bf473f3c-...): new recognitions CASE branch projecting milestone_kind+person_upn+manager_upn+status, plus appended top-level entity_table + entity_id columns (positions 11-12; cols 1-10 + filter semantic preserved verbatim for PR #15 useCases router + all PR #11/12/14 consumers). tRPC: read.recognitions.list/pending/sent + mut.recognitions.flagMilestone/draftThankYou/markSent/markSkipped wired into root.ts (15 mentions, both namespaces). outbound-voice integration = synchronous server-side invoke via apps/api/src/lib/outboundVoice.ts helper (MVP template + TODO for full skill-bridge); drafts NEVER auto-send, human sign-off gate at markSent. SPA: /e/$engagement/recognitions route TanStack-day-1 (risks.index.tsx precedent, NOT decisions legacy), 0 useEffect, 0 var(--color-*), --cp-* tokens via STATUS_COLORS helper, drawer preview before send, [Send peer/manager] + [Skip with reason] actions. Single-source helpers: recognitionStateMachine.ts (STATUSES + LEGAL_EDGES + STATUS_COLORS + MILESTONE_KINDS + MILESTONE_LABELS) + recognitionErrors.ts (vocabulary-boundary mapper per ADR-W10-8 — UI says "Recognition" / "Thank-you" / "Milestone"; error.code/pgCode values NEVER mutated). Substrate smoke 34/34 PASS (smoke-pr16.py: schema/grants 10, flag {happy,idem,hs4l,invalid_kind,uq_collision} 5, draft {peer.happy,idem,stale_row,invalid_variant,manager.happy} 5, markSent {invalid_transition,draft_required,happy,idem} 4, markSkipped {reason_required,happy,idem,stale_row} 4, v_human_touchpoints {row_count,entity_table_top_level,entity_id_top_level,target_table_legacy,target_summary.milestone_kind} 5, pre-checks 1). Vitest 290/290 PASS (211 API + 79 SPA). Typecheck green. Builds green (API 2.5MB, SPA 754KB). 4 functions unchanged (roles+trpc+skillsCommunicationsRecordOutbound+timerRefreshSearch) — recognitions rides the trpc catch-all. Authenticated UI smoke deferred to Charlie (AAD-gated); substrate P5-CONFLICT+P5-IDEM is authoritative functional gate. PR #19 (squash-merge SHA a8c17de; DDL applied parent-side via apply-pr16-ddl.py single-transaction under SET ROLE haleon_aiac_owner; Steward post-state attestation chain 641 rows OK + view additive non-breaking + w10_app least-privilege confirmed; CLI iter-0 single-pass via short-inline-prompt + .w10/pr16-build-spec.md file-reference per P15; SPA via StaticSitesClient.exe direct; Function via az functionapp deployment source config-zip deploy id 456f50b1-6230-45ca-b4e2-7180bff748f0 "Deployment was successful"); ship audit ed3ca382-9089-47ef-80ac-52ba91e75b0d. Citations: SA-ADR-W10-7 (entity-pointer promotion) + ADR-W10-1 (HS-4L) + ADR-W10-6 (skills-ingress) + ADR-W10-8 (vocabulary boundary) + design §17 (outbound-capture). 2026-06-04 BST
PR #15 F31 Workstreams UI (L-complexity, SPA-only delivery on PR #14/14.5 substrate). User-facing vocabulary: Workstream per ADR-W10-8 (Decision 4fb9b6a4-4652-4cbf-9e1e-bf2e9acf7a87) — UI-relabel-only, substrate / tRPC paths / field names / error.code values / route file names / query keys stay use_case/useCase verbatim. Routes: /e/$engagement/use-cases (list + 6-column Kanban toggle with native HTML5 drag-drop) + /e/$engagement/use-cases/$id (detail + transition history + 4-button action row + activity drawer). 6-state machine per F31.5 (mobilising/live/at_risk/wound-down/cancelled/archived); 11 legal edges client-side pre-gated via single helper useCaseStateMachine.ts (no hand-inlined edge lists). 8 reason-required edges (all risk/cancel/wind-down + the at_risk→live recovery) force a 240-char reason modal matching substrate truncation; 3 reason-optional edges (mobilising→live, cancelled→archived, wound-down→archived) skip the modal. 4 write-back modals: create (with "Generate from title" slug button; post-submit CONFLICT catch on duplicate use_case_code), transition reason, assign owner (xmin-guarded), add note. Optimistic mutations per spec contract: onMutate snapshot+flip, onError rollback+toast, onSuccess round-trips newXmin into cache, onSettled invalidates ['useCases', engagement]; CONFLICT (stale_row) drives optimistic-rollback UI. clientOpId generated at drop-commit/click-time (not drag-start/drawer-open) so retry-after-hiccup hits the same idempotency key. expectedXmin passed as plain text from card cache (SP casts ::xid internally; zero ::xid appearances client-side). Active count helper = ACTIVE_STATUSES = ['live','at_risk'] — never undercounts at-risk rows (SA F1 footgun). Single error-mapping helper useCaseErrors.ts with wire-protocol preservation (UI toast says "workstream"; error.code stays code_already_taken/stale_row/etc. verbatim for DevTools+telemetry per SA F7 boundary). Light theme --cp-* tokens only (0 var(--color-*)). TanStack day-1 — 0 useState for server state, 0 useEffect for fetches, trpcClient only inside queryFn/mutationFn (precedent: risks.index.tsx; explicitly does NOT propagate the decisions.index.tsx legacy drift). 54 SPA tests green (was 16, +38: 12 state-machine, 10 error-mapping, 16 route-page covering list/board grouping, DnD edge-gating, optimistic flip + CONFLICT rollback, transition reason-modal mandate, detail history render, create CONFLICT inline-error, transition dropdown legal-targets-only). 196 API tests still green (zero API change). 4 functions unchanged (roles + trpc + skillsCommunicationsRecordOutbound + timerRefreshSearch) — no Function redeploy. No ADR triggered (no agent_runs structural change; W10-7-watch N stays at 2 history-bearing verbs). Deferred (carry-forward): server-side read.useCases.board proc (client-side grouping cheaper at N<500); cancelledAt/archivedAt rendering (needs get SELECT widen + Function redeploy; bundle with future dependencies-link PR); workstream-to-workstream dependencies UI (ADR-W10-8 §4 forward-binding only; own ratification turn when surfaced). PR #18 (squash-merge SHA d63be3e; iter-0 single-pass with in-CLI typecheck-fix loop — route tree regen via dev-server tick, get-response-shape correction, ESM require() fix in tests, one error-mapping logic fix); SPA deployed via StaticSitesClient.exe direct (token from swa-w10-clawpilot-uks; bundle index-DeEgRnxM.js); no Function redeploy (4 functions unchanged); ship audit f38acbe5-1f1f-475b-aef0-c2da1b9a3754. Citations: ADR-W10-8 + Decisions 4fb9b6a4-... (relabel) + 28802247-... (F31.5 6-state widen). 2026-06-04 BST
PR #14 F31 use-case lifecycle (M-complexity, Steward-led substrate): use-case-level tracking one level below engagement/project, DB-schema home per Charlie 2026-06-02 steer. Substrate (parent-applied DDL pre-CLI per Cardinal Rule #3): 3 new tables in haleon schema — use_cases (id PK + use_case_code UNIQUE + title + summary + status use_case_status ENUM {mobilising,live,wound-down} + owner_email + pod_id + lifecycle timestamps proposed_at/went_live_at/wound_down_at + engagement_id + client_op_id), use_case_transitions (append-only state-change log, from_state/to_state/reason_md/actor + audit_run_id FK), and use_case_state_machine seed (legal edges: mobilising→live, live→wound-down). 4 new SPs all SECURITY DEFINER + GRANT EXECUTE w10_app, all routed through haleon.sp_create_agent_run (HS-4L spine, NOT raw agent_runs INSERT): sp_use_case_create (line-1 p_actor_role NOT LIKE 'human:%'hs4l_violation P0001; raw 23505 on dup use_case_code; replay via client_op_id; seeds status=mobilising), sp_use_case_transition (p_expected_xmin TEXT cast ::xid internally for optimistic concurrency → stale_row; legal-edge check vs state-machine → invalid_transition; reason_required guard; returns new xmin), sp_use_case_add_note (polymorphic haleon.notes write target_table='use_cases'; body_required), sp_use_case_assign_owner (xmin-guarded owner reassignment → stale_row). v_human_touchpoints extended (transition branch links via use_case_transitions.use_case_id subquery; notes branch via target_summary polymorphic pointer) — within ADR-W10-2 §3.1 allowance, NOT ADR-W10-4 trigger. SA forward-binding watch: promote entity_table/entity_id to top-level view columns at the 3rd history-bearing verb. tRPC (API-only, no SPA this PR): NEW useCases.ts split createReadUseCasesRouter (read.useCases.list/get/history) + createMutUseCasesRouter (mut.useCases.create/transition/addNote/assignOwner) wired via per-namespace lazy getters in root.ts (mirrors decisions pattern); all protectedProcedure gated on w10_user role; actorRole='human:'+upn.split('@')[0] derived server-side from ctx.user.upn (NEVER trust input UPN); error mapping hs4l_violation→FORBIDDEN, stale_row/invalid_transition/reason_required/body_required→appropriate tRPC codes; p_expected_xmin passed as plain text (SP casts ::xid, NO $N::xid in query). NO new HTTP function — useCases rides inside the trpc catch-all; Function list stays roles+trpc+skillsCommunicationsRecordOutbound+timerRefreshSearch (4 functions, unchanged). 194 API tests green (was 178, +16: param-array-order contract locks, not.toContain('$4::xid') plain-text-xmin assertions, all error paths). 25/25 substrate smoke PASS (4 SPs × {happy, replay/IDEM, hs4l_violation, invalid_transition, stale_row, reason_required, body_required, view projection}). Authenticated HTTP smoke through SWA front door GREEN: read.useCases.list→200 (schema haleon resolves, returns lifecycle rows), mut.useCases.create→200 (status=mobilising, actorRole=human:smccormick derived from EasyAuth principal — HS-4L actor stamping proven end-to-end through the deployed bundle). ADR-W10-4 NOT triggered (additive tables + additive SPs; view extension within §3.1; no agent_runs structural change). Carry-forwards: W10-7-watch (view entity-pointer promotion at 3rd history verb); SA MIGRATION-NOTES N1/N5a-N5f doc-drift (deferred, non-blocking); still open C10, C11, C13, C14, C15, C15a, C16, C18 partial, C18a, C20 + prior Cnew-* carries. F31.5 amendment (2026-06-04, PR #14.5): SA-ratified additive widen of the use-case state machine from 3 states {mobilising,live,wound-down} to a 6-state superset {+at_risk,cancelled,archived}. Substrate: status CHECK widened to 6 values; 11 edges seeded in use_case_state_machine (adds live↔at_risk cycle, mobilising/live→cancelled, live/at_risk→wound-down, cancelled→archived, wound-down→archived terminal); cancelled_at/archived_at columns added; sp_use_case_transition CREATE OR REPLACE (table-driven — signature/RETURNS unchanged, gains terminal-timestamp stamping). Zero data migration on the 4 live rows. API: widened two zod enums (list.status filter, transition.toState); 196 API tests green (+2 F31.5 contract tests). 21/21 substrate smoke PASS (full lifecycle incl at_risk cycle + cancel/archive paths, reason_required + invalid_transition + terminal guards, replay idempotency). Ratifying Decision 28802247-b233-4fd8-a268-9013719ffedb. NO ADR (additive within ADR-W10-1 + ADR-W10-2 §3.1). Does NOT advance W10-7-watch (no new history-bearing verb). PR #16 (squash-merge SHA 7dd82ed; iter-0 single-pass — substrate pre-applied + 25/25 gates GREEN before CLI launch); API-only (no SPA deploy); Function via az functionapp deployment source config-zip (4 functions unchanged — useCases rides trpc catch-all); ship audit ae605e4d-6ee5-439c-8577-b8ffb56d0ae7 (chained after PR #13 ac722457). F31.5 (PR #14.5): PR #17 (squash-merge SHA 91416cc); Function redeployed via az functionapp deployment source config-zip (4 functions unchanged); ship audit 548273ee-5b37-41b0-a93d-eb0a8c925b83 (chained after ae605e4d). 2026-06-04 BST
PR #13 Search (L-complexity): per-engagement mv_search_index materialized view (§12.2) UNION over decisions / adrs / risks_issues_blockers / use_cases / briefs / communications with (kind, id, title, body, tsv) branch shape; UNIQUE(kind,id) PK + GIN(tsv) + GIN(title gin_trgm_ops); app.corpus_docs branch deferred as Cnew-app-corpus-docs-writer (writer not yet wired). Functions-timer refresh fallback per §22 risk 2 (Flex SKU has no pg_cron). NEW route /search?q=&type=&engagement= with TanStack Table; ⌘K/Ctrl+K palette via Radix Dialog + cmdk with debounced 250ms suggest. Substrate (parent-applied DDL pre-CLI): haleon.mv_search_index + test_engagement_1.mv_search_index MVs materialized; pg_trgm already enabled. 3 new SPs in haleon schema: sp_search_global(p_query text, p_engagements text[], p_kinds text[], p_limit int DEFAULT 50) — uses plainto_tsquery (not websearch_to_tsquery per SA pin), ranks by ts_rank_cd(tsv, q) + 0.1*similarity(title, p_query), fans out server-side over p_engagements array, clamps p_limit<=200, gates access via portfolio.engagements_registry WHERE status='active' (auth.roles_for_user does not exist — MVP placeholder; Cnew-roles-for-user carry for real gating); returns ranked rows with snippet column. sp_search_suggest(p_prefix text, p_limit int DEFAULT 10) — pg_trgm similarity autocomplete on title, patched post-apply to also support short prefixes via ILIKE p_prefix||'%' fallback when similarity threshold yields no hits (G5 fix: 'arc' now returns results). sp_refresh_search_mv(p_engagement text) — REFRESH MATERIALIZED VIEW CONCURRENTLY per engagement, writes class='scout_sweep' audit row, returns run_id. tRPC: read.search.global({query, engagements?, kinds?}) + read.search.suggest({prefix}); NO *_record_read audit (C16 deferral retained). Functions: NEW timerRefreshSearch registered via app.timer('timerRefreshSearch', {schedule:'0 */5 * * * *', handler}) — fans out sp_refresh_search_mv over ['haleon','test_engagement_1'] via Promise.allSettled. Final Function list: roles+trpc+skillsCommunicationsRecordOutbound+timerRefreshSearch (4 functions, was 3). SPA: NEW /search route with TanStack Table from day-1 + URL search-params for q/type/engagement; ⌘K/Ctrl+K palette mounted in header stub via cmdk + Radix Dialog; type → debounced 250ms → read.search.suggest; Enter → navigate to /search?q=...; arrow-key+Enter → navigate to hit detail. Ship-fix lesson (P13): new app.timer(...) registration in functions/timerRefreshSearch.ts was tree-shaken because main.ts didn't import the file — deploy reported success but App Host showed only 3 of 4 functions. One-line fix commit a73a80d added import './functions/timerRefreshSearch'; rebuild + redeploy registered all 4. Any new function file MUST be imported by main.ts or it disappears from the bundle — add to w10-build SKILL P-lessons. ADR-W10-4 NOT triggered (additive MVs + additive SPs; no agent_runs structural change). Carry-forwards: NEW Cnew-app-corpus-docs-writer (canon-watch CA Job to populate app.corpus_docs; until shipped, MV UNION is 6 branches not 7), Cnew-roles-for-user (real access-control gating once auth.roles_for_user exists — MVP uses engagements_registry status='active' placeholder); still open C10, C11, C13, C14, C15, C15a, C16, C18 partial, C18a, C20, Cnew-checkoff-orphan-reconcile, Cnew-report-decide-inline, Cnew-usermenu-wire, Cnew-comm-status-axis, Cnew-kernel-purity-trpc-skills, Cnew-skill-bearer-config, Cnew-mark-consumed-metadata-noop, Cnew-mark-consumed-idem-debt, Cnew-prefix-cache-doc, cross-schema view-join gap. PR #15 (merge SHA 6baaf2d for content + fix SHA a73a80d for main.ts import); SPA via StaticSitesClient.exe direct 51s; Function via az functionapp deployment source config-zip OneDeploy cc6c59e3-e295-4602-88bc-6be78a38d92b (4 functions registered roles+trpc+skillsCommunicationsRecordOutbound+timerRefreshSearch); ship audit ac722457-37fb-471a-82c4-f6f7d44b4ece. 2026-06-04 BST
PR #12 Reports check-off write-back + viewer affordances: §11.4 + §12.2. Server-emitted ▢{key}/▣{key} glyphs in daily-status + incident-postmortem report bodies become click-target <CheckOffButton> via custom markdown text-node walker; [decide →#<id>] tokens become pure-navigation links to decision detail (W4 defer inline creation). Add-note drawer mirroring PR #9/#11. Full TanStack refactor of pre-existing useState/useEffect/trpcClient route (drift-bomb predating PR #11 mandate). Substrate (parent-applied DDL pre-CLI per Cardinal Rule #3): haleon.checkoffs stub FULLY-FORMED per §12.2 (id PK + target_table/target_id/item_key NN + checked NN + checked_by NN + checked_at NN DEFAULT now() + audit_run_id FK + client_op_id NN UNIQUE + UNIQUE(target_table,target_id,item_key)) — zero ALTERs. 2 new SPs all in haleon schema: sp_report_checkoff_toggle (NEW dual-param shape per SA AP1: p_target_type + p_target_id composed at SP body to target_id := p_target_type||':'||p_target_id via : separator pin; target_table='reports' constant; line-1 p_actor_role NOT LIKE 'human:%'hs4l_violation P0001; item_key_required guard; replay-FIRST via UNIQUE(client_op_id) inside same TX with UPSERT per SA AP4 pin; ON CONFLICT (target_table, target_id, item_key) DO UPDATE with #variable_conflict use_column pragma + table alias to resolve OUT-param ambiguity; returns full TABLE shape (run_id, checkoff_id, target_table, target_id, item_key, checked, checked_by, checked_at, actor_role); payload key 'pk'; verb 'ui.check_off'; widening required DROP+CREATE not REPLACE per Steward Option 1 — old sp_report_check_off SP retained untouched for back-compat); sp_report_add_note (mirrors sp_comm_add_note exactly; line-1 hs4l_violation; body_required guard; replay via UNIQUE(client_op_id) on haleon.notes; writes haleon.notes(target_table='reports:'||p_target_type, target_id=p_target_id) polymorphic per AP5; verb 'ui.add_note'). All SECURITY DEFINER + GRANT EXECUTE w10_app. v_human_touchpoints checkoffs branch extended per AP6: now projects {target_table, target_id, item_key, checked, checked_by, checked_at} — within ADR-W10-2 §3.1 allowance, NOT ADR-W10-4 trigger. Q9 carry CLOSED (4-SP sweep): haleon.sp_record_read, test_engagement_1.sp_record_read, haleon.sp_document_record_read_denied, test_engagement_1.sp_document_record_read_denied now all write payload key 'pk' (was 'id') per view payload->>'pk' convention — combined with PR #11 sp_document_record_read fix, all 6 read.row family SPs now aligned; every read.row/read.row.denied touchpoint surfaces with non-NULL target_summary. tRPC reports.ts split into createReadReportsRouter + createMutReportsRouter wired via per-namespace getters in root.ts (mirrors PR #11 pattern): read.reports.get extended to return checkoffs: [{itemKey, checked, checkedBy, checkedAt}] sibling array (SA W1 — state authoritative, glyphs presentational) via NEW fetchCheckoffsForReport(query, slug, id) helper hard-capped 200 rows; handleDailyStatus + handleIncidentPostmortem emit deterministic ▢{dailystatus-touchpoint/N} / ▢{incident-rib/N} tokens per per-report-type KEY_MANIFEST constant (SA AP2 single source of truth pin); state look-up at render time flips emitted glyph from to when checkoff row says checked; mut.reports.checkOff derives actorUpn from ctx.user.upn ↔ constructs actorRole='human:'+upn.split('@')[0] server-side (NEVER trust input UPN); maps hs4l_violation→FORBIDDEN, item_key_required→BAD_REQUEST; mut.reports.addNote same auth derivation + body_required→BAD_REQUEST. NO new HTTP function (mutations are tRPC procs); Function list stays roles+trpc+skillsCommunicationsRecordOutbound. SPA e.$engagement.reports.$type.$id.tsx REFACTOR: deletes useState/useEffect/trpcClient.read.reports.get.query direct calls; replaces with useQuery({queryKey:['reports','get',engagement,type,idNum], queryFn:() => trpcClient.read.reports.get.query({...})}); useMutation for checkOff (optimistic flip in onMutate via queryClient.setQueryData + rollback in onError + invalidate in onSettled — mirrors PR #4 addNote pattern per SA W3) + addNote; custom React markdown component-walker parses text-nodes for regex /(▢\{[a-z][a-z0-9_/-]+\}|▣\{[a-z][a-z0-9_/-]+\}|\[decide →#[a-zA-Z0-9-]+\])/g → replaces with <CheckOffButton> (renders glyph with aria-pressed + onClick → mutate with crypto.randomUUID() as clientOpId AT CLICK TIME per SA W3) or <Link to="/e/$engagement/decisions/$id" search={{from:'report:'+type+':'+id}}> (pure navigation per W4); add-note drawer button in header opens drawer with controlled <textarea> → save mutation with clientOpId generated AT DRAWER-OPEN per SA W3 distinction. TanStack from day-1 in all new code (useQuery/useMutation 4+; queryClient.invalidateQueries on save). P7 substrate probes 14/14 PASS (G1 happy checkoff_toggle returning full TABLE shape, G2 hs4l_violation skill caller, G3 item_key_required empty-string, G4 IDEM same client_op_id returns same run_id+checkoff_id, G5 UPSERT flip false→true→false via fresh client_op_ids, G6 target_id composite stored as 'reports' / 'slug:id', G7 view projects checked_by+checked_at, G8 happy add_note, G9 hs4l skill caller, G10 body_required, G11 IDEM, G12 polymorphic target_table='reports:<slug>' on notes row, G13 Q9 haleon.sp_record_read payload uses 'pk', G14 te1.sp_record_read same). 172 API tests green (was 163, +9 hits target: 3 checkoffs-array sibling shape + 4 checkOff happy/replay/forbidden/bad-request + 2 addNote happy/replay) + 12 SPA tests (was 9, +3 hits target: CheckOffButton render with initial state from sibling array, mutation firing with optimistic cache flip, drawer lifecycle). Drift-bombs GREEN: var(--color-)=0 in modified files, useEffect=0 in refactored route, useState only for drawerOpen+textarea+drawerClientOpId (UI presentation, allowed), trpcClient only inside queryFn/mutationFn. ADR-W10-4 NOT triggered (additive SPs; view extension within ADR-W10-2 §3.1 allowance; no agent_runs structural change; no new role/verb taxonomy). Carry-forwards: CLOSED read-row-target-pk-key-mismatch (full 6-SP family sweep complete); NEW Cnew-checkoff-orphan-reconcile (when report-type key schema bumps risks/1critical_risks/1, prior checkoffs orphaned — future migration helper SP per SA AP2 sub), Cnew-report-decide-inline (inline [decide →] SP-driven creation deferred per SA W4 — mut.decisions.createFromReportItem future PR), Cnew-usermenu-wire (close C18 fully by wiring SPA UserMenu to read.auth.me — trivial follow-up); cross-schema view-join gap carry scope grew to {communications, checkoffs} per AP7; still open C10, C11, C13, C14, C15, C15a, C16, C18 partial, C18a, C20, Cnew-mark-consumed-metadata-noop, Cnew-mark-consumed-idem-debt, Cnew-prefix-cache-doc, Cnew-comm-status-axis, Cnew-kernel-purity-trpc-skills, Cnew-skill-bearer-config. PR #14 (merge SHA 59757d2; iter-0 1637574 single-pass — substrate pre-applied + 14/14 gates GREEN before CLI launch); SPA via StaticSitesClient.exe direct 30s; Function via az functionapp deployment source config-zip OneDeploy d3092999-2ebe-49f1-9a8a-8a5b508696b0 (3 functions registered roles+trpc+skillsCommunicationsRecordOutbound unchanged); 5 DDL audit cross-links (0c0c095f sp_report_checkoff_toggle + 23d109c0 sp_report_add_note + c1dad0db view extend + 58992d06 Q9 sweep family + c0df9435 variable_conflict patch) + ship audit 91f67447-47f2-4093-984b-ca7d806422d8. 2026-06-04 BST
PR #11 Communications surface: §11.13 + §17. Cards-list + filter chips (channel/direction/date) + detail viewer with inbound-paste textarea + addNote drawer + activity drawer. Skills-tier ingress endpoint /api/skills/communications/record-outbound (path-distinct Function, JWT bearer middleware, ADR-W10-6). New auth.me proc closes C18. Read-row payload-key bug fixed ('id''pk') closes Steward critical from PR #10. Inherits PR #7 chrome + PR #9-#10 TanStack discipline. Substrate (parent-applied DDL pre-CLI per Cardinal Rule #3): ALTER haleon.communications ADD direction text NN DEFAULT 'outbound' CHECK ('outbound','inbound') + ADD related_run_id uuid NULL + ADD CHECK channel IN ('m365_chat','email','m365_meeting','other') (locked closed-set per SA AP1 amend — teams redundant with m365_chat, phone graveyard; 'other' absorbs futures via metadata.subchannel) + 3 supporting indexes (direction, related_decision, related_risk). 3 new SPs all in haleon schema (view reads from haleon.communications only — cross-schema view-join gap from PR #10 deferred): sp_comm_record_outbound (dual-role-param shape per SA CP1' — p_sender_role validated against existing 5-label CHECK at SP entry via SA P1 early invalid_sender_role RAISE; p_actor_role line-1 guard NOT LIKE 'skill:%'hs4l_skill_guard P0001; idempotency via UNIQUE(client_op_id) replay; writes agent_runs.agent_name=p_actor_role with verb 'skill.comm.record_outbound' tier 'cross_cutting_capability'); sp_comm_paste_inbound (line-1 human:% guard → hs4l_role_guard; body_required guard; replay-check FIRST per PR #9 canonical; FOR UPDATE lock + xmin compare → stale_row P0001 per Steward Q6 canonical; merges response_capture jsonb via || concat preserving prior keys, sets {source, body_md, pasted_at, pasted_by}); sp_comm_add_note (mirrors sp_risk_add_note exactly — line-1 hs4l_violation, replay via UNIQUE(client_op_id) on haleon.notes, writes haleon.notes(target_table='communications', target_id=p_comm_id::text) with verb 'ui.add_note'). All SECURITY DEFINER + GRANT EXECUTE w10_app + payload key 'pk' (NOT 'id') per view convention. v_human_touchpoints communications branch extended additively: now projects {channel, recipient_upn, subject, target_table, target_id, direction, sender_role, sent_at, related_decision, related_risk} — within ADR-W10-2 §3.1 allowance, NOT ADR-W10-4 trigger. Q9 carry CLOSED inline (2-line fix): both haleon.sp_document_record_read + test_engagement_1.sp_document_record_read now write payload key 'pk' (was 'id') so view payload->>'pk' extraction surfaces target_pk correctly — every read.row touchpoint now lands with non-NULL target_summary. tRPC 1 new router (communications.ts): read.list({engagement, channels?, directions?, dateFrom?, dateTo?, relatedDecisionId?, relatedRiskId?, relatedRunId?}) ORDER BY sent_at DESC LIMIT 200, NO *_record_read audit (C16 deferral retained); read.get({engagement, id}) single row + xmin + history slice from view; mut.pasteInbound calls sp_comm_paste_inbound with stale_row→CONFLICT/hs4l→FORBIDDEN/body_required→BAD_REQUEST mapping; mut.addNote calls sp_comm_add_note. auth.me proc (NEW, closes C18): returns {upn, roles, displayName?} parsed from x-ms-client-principal, no audit. Skills-tier ingress per ADR-W10-6 (NEW, SA-owned, ratified pre-merge): NEW Function apps/api/src/functions/skills.communications.recordOutbound.ts registered as app.http('skillsCommunicationsRecordOutbound', {route:'skills/communications/record-outbound'}) at authLevel:'anonymous' with own bearer JWT verify (iss + aud + exp + sub-allowlist via env SKILL_JWT_*); short-circuits to 503 skill_ingress_not_configured when env unset (PR #11 ship default; m-skills/ wrap configures); maps hs4l_skill_guard→403, invalid_sender_role→400, 23514→400, stale_row→409, success→201 with {success, runId, id, senderRole, actorRole}. ADR-W10-6 canonical pattern: path-distinct partition (NOT internal.* tRPC namespace — would type-leak via AppRouter export); dual-role-param (semantic p_sender_role vs technical p_actor_role) — preserves trust surface of existing sender_role CHECK; 'skill.<verb>' action taxonomy distinct from 'ui.*'; 'skill:<skill_name>' role-prefix — view WHERE role LIKE 'human:%' naturally excludes skill rows without view change; symmetric HS-4L variant guard (skill SPs reject 'human:%'+'peer:%'; human SPs reject 'skill:%'); pairs with ADR-W10-5 (read-only dual-path) as the read-then-write counterpart. SPA: 2 new routes — e.$engagement.communications.index.tsx (cards list, channel chips + direction toggle + date range, useQuery key ['communications','list',engagement,...filters], empty state) + e.$engagement.communications.$id.tsx (detail viewer; two-col layout body + sidebar; inbound-paste textarea gated by direction='outbound' AND response_capture is null/empty; addNote drawer; activity drawer from useQuery({queryKey:['communications','get',...]}) history); Communications nav flipped from nav-disabled span to live <Link activeProps> in e.$engagement.tsx (3 future tabs — Use Cases / Recognition / Library / Metrics / Audit — stay disabled). TanStack from day-1 (useQuery/useMutation 8+ across files; queryClient.invalidateQueries on success). P7 substrate probes 14/14 PASS (G1 happy record_outbound, G2 bad-channel CHECK 23514, G3 bad-sender SA P1 P0001 invalid_sender_role, G4 human-guard SKILL_GUARD P0001, G5 IDEM same run_id+comm_id; G6 happy paste_inbound merged response_capture, G7 stale_row P0001, G8 body_required P0001, G9 skill caller rejected from human SP, G10 IDEM same run_id; G11 happy add_note 1 note row + 1 audit, G12 IDEM same; G13 view shape projects direction+sender_role+sent_at+related_* correctly; G14 read.row payload key 'pk' = '1' not NULL proving Q9 fix). 163 API tests green (was 145, +18 exceeds +15 target: list filters / get / pasteInbound happy+stale+replay+forbidden / addNote happy+idem / recordOutbound 401/403/503/201+replay / auth.me) + 9 SPA tests (was 4, +5 exceeds +3 target). Drift-bombs GREEN: var(--color-)=0 in new files, useEffect=0, trpcClient only inside queryFn/mutationFn, routeTree.gen regenerated with 2 new routes. ADR-W10-4 NOT triggered (additive ALTERs on existing column shape; new SPs additive; view extension within ADR-W10-2 §3.1 allowance; no agent_runs structural change). Carry-forwards: CLOSED C18 (auth.me wired) + read-row-target-pk-key-mismatch (inline 2-line SP body fix); PARTIAL CLOSE Cnew-comm-skill-wrap (server endpoint shipped; m-skills/ wrap ships separately); NEW Cnew-comm-status-axis (draft/sent/received/archived status axis deferred per SA AP2 — if recognition surface needs it, PR #16+), Cnew-kernel-purity-trpc-skills (forbid apps/api/src/trpc/apps/api/src/skills/ cross-imports per ADR-W10-6 §3.1 — F5 follow-up), Cnew-skill-bearer-config (SKILL_JWT_* env settings populated when m-skills/ wrap ships); still open C10, C11, C13, C14, C15, C15a, C16, C18a, C20, Cnew-mark-consumed-metadata-noop, Cnew-mark-consumed-idem-debt, Cnew-prefix-cache-doc, cross-schema view-join gap (Steward Q10 #1 from PR #10). PR #13 (merge SHA 241884b; iter-0 102dc72 single-pass — substrate pre-applied + 14/14 gates GREEN before CLI launch); SPA via StaticSitesClient.exe direct 16s (P10: no --env); Function via az functionapp deployment source config-zip OneDeploy f47290bf-d75a-4061-9802-a5a06ad44208 (3 functions registered roles+trpc+skillsCommunicationsRecordOutbound); 4 DDL audit cross-links (0f27e18f ALTERs + b0c2f364 sp_comm family + 2da931e8 view extend + 4th for read_row pk fix) + ship audit 1dfd8ece-3f6b-4787-93cf-80758cf85a6d; ADR-W10-6 ratified at ~/.copilot/m-policies/adr/SA-ADR-W10-6-SKILLS-INGRESS.md. 2026-06-04 BST
PR #10 Documents UX completion: §11.11 left-tree nav (briefs / docs / skills) + sensitivity-chip filter + Confidential/Regulated confirm-click ack-gate + inline [mark consumed] (TanStack mutation) + catch-all detail viewer + canon-watch CA Job ingest contract + 4 role-overlay m_read_canon flips optional→required. Inherits PR #7 chrome + extends PR #6 documents substrate. SPA route reshape: PR #6 flat-table at e.$engagement.documents.index.tsx rewritten as empty-state "pick a doc" component; NEW layout e.$engagement.documents.tsx hosts <Outlet/> with left tree (lazy-loaded via TanStack useQuery per node) + top sensitivity-chip set (4 chips General/Internal/Confidential-Customer/Regulated-Restricted, default ALL ACTIVE, subtractive client-side filter per SA AP2); NEW catch-all e.$engagement.documents.$.tsx renders selected document with path-injection defence (regex /^[A-Za-z0-9_\-./]+$/, no leading /, no .. per SA AP14) + metadata sidebar (sensitivity / kind / last_seen / source path) + inline [mark consumed] button using useMutation + queryClient.invalidateQueries({queryKey:['documents']}) on success; old e.$engagement.documents.$id.tsx DELETED. Confidential/Regulated confirm-click gate (SA AP15): SPA renders STUB CARD with sensitivity banner + summary + [open in source ↗] button on Confidential-Customer/Regulated-Restricted classes; click sets local ackConfidential=true, body fetch fires via extended read.documents.body({path, ackConfidential:true}); server-side BAD_REQUEST 'ack_required' P0001 thrown BEFORE clearance check when flag missing/false (NO audit row on cancel/no-ack); cleared+acked path runs existing requireSensitivityClearance + sp_document_record_read success audit + last_seen_at bump (PR #6 §26.2.5 chokepoint preserved per SA ADR-W10-3 conformance); denied path runs new sp_document_record_read_denied with outcome='denied' BEFORE FORBIDDEN 403 throw (closes PR #6 E2 carry per SA AP10). 4 new tRPC procs (sub-router pattern under read.documents): read.documents.tree.root({engagement}) returns [{path,type:'dir',maxSensitivity,hasChildren}] from SELECT DISTINCT split_part(storage_path,'/',1) + max-sensitivity GROUP BY (LIMIT 50 defensive); read.documents.tree.children({engagement, parentPath}) lazy-expands with server-side maxDepth=2 guard (parentPath with >2 segments → BAD_REQUEST 'max_depth_exceeded' per SA AP1a); read.documents.metadata({engagement, path}) returns {id,title,kind,sensitivityLabel,storageBackend,storagePath,lastSeenAt,summary} NO-AUDIT (passive metadata only); read.documents.body EXTENDED to accept EITHER documentId (PR #6 legacy) OR path (new, resolves via UNIQUE (storage_backend, storage_path)) PLUS new ackConfidential:boolean field. dispatchByBackend prefix routing: ^(briefs|docs|skills)/stclawpilotcorpusuks account container split from path (canon-watch surface); ^engagement-demo/ → existing stw10apiuks/documents (PR #6 legacy preserved); uses DefaultAzureCredential with test injection via __setBlobServiceForTest. canon-watch CA Job provisioned at caj-clawpilot-canon-watch-vnet on cae-clawpilot-vnet-uks (cron 0 3 * * * UTC, one hour after sibling caj-clawpilot-corpus-sync-vnet per §16.1) mirroring sibling shape: azure-cli:latest base + SCRIPT_B64 secret pattern + dual UAMI (mi-clawpilot-corpus-flipper opens/closes network window, mi-clawpilot-corpus-rw blob read + PG INSERT via AAD-passwordless), replicaTimeout=600, command installs postgresql-client then decodes+executes bash script. Walks 3 containers (briefs, docs, skills — corpus storage is stclawpilotcorpusuks with publicNetworkAccess=Disabled, NOT design-doc's notional engagement-docs), per .md file: SHA256 + YAML-frontmatter sensitivity extract (fallback Internal) + H1 title extract + UPSERT into <eng>.documents ON CONFLICT (storage_backend, storage_path) DO UPDATE gated by sha256_or_etag IS DISTINCT FROM EXCLUDED.sha256_or_etag OR last_seen_at < sweep_start_ts (SHA-gate idempotency per SA AP5 + Steward heartbeat soft-delete acceptance per A9); soft-delete via last_seen_at < sweep_start_ts heartbeat (NO additional column, NO DDL on documents table); writes ONE agent_runs class='scout_sweep' triggered_by='canon_watch.scan' row per run with payload {run_id, files_scanned, rows_upserted, rows_soft_deleted, open_window_sec}. Substrate additives (parent-applied DDL pre-CLI per Cardinal Rule #3): haleon.v_human_touchpoints documents-branch extended to project {title,kind,sensitivity_label,target_table,target_id,consumed_by:metadata->'consumed_by'} — additive per ADR-W10-2 §3.1 (non-breaking CREATE OR REPLACE, column count preserved at 10); test_engagement_1.sp_document_record_read_denied created (closes PR #6 E2 gap where router called missing SP → would have thrown 42883); 3 fixture documents seeded into test_engagement_1.documents matching canon-watch path convention (id=1 briefs/sa-brief/SKILL.md Internal; id=2 skills/w10-build/SKILL.md Internal; id=3 docs/reporting-design.md Confidential-Customer) + 3 DDL audit cross-links (9076d10c-88f3-4524-8d20-b9ff90609fc1 view extension + 99db419f-49e7-4016-bbc5-17b716b89525 denied SP + e031f544-58bb-421a-b3fa-25bf6c923b38 fixtures). NO ALTER TABLE on documents. 4 role-overlay flips applied per SA AP7 sequencing: m_read_canon moved from optionalrequired in CHIEFOFSTAFF.overlay.md, SOLUTIONARCHITECT.overlay.md, STEWARD.overlay.md, LIBRARY-CURATOR.overlay.md with PR #10 comment naming the in-repo shim at apps/api/src/m_read_canon.ts as authoritative host-side resolution. P7 substrate probes 6/9 PASS + 3 tracked-debt-OK (G1 tree-root 3 dirs project correctly; G2 children('briefs') 1 file; G3 metadata id=1 OK; G4 sp_document_record_read +1 audit + last_seen bumped; G6 sp_document_record_read_denied +1 audit with outcome='denied'; G7 sp_document_mark_consumed +1 audit but consumed_by metadata write silently no-ops — PR #6 SP jsonb_set deviation on missing intermediate, NEW carry-forward Cnew-mark-consumed-metadata-noop; G8 view shape projection correct but TE1-to-haleon cross-schema gap surfaces NULL consumed_by — pre-acked; G9 markConsumed not client_op_id-idempotent — pre-existing Cnew-mark-consumed-idem-debt). 145 API tests green (was 129, +16 documents.test.ts cases: tree-root happy/empty/single-dir/3-dirs ordered, tree-children happy + maxDepth>2 reject, metadata happy + NOT_FOUND, body ackConfidential=false on Confidential→BAD_REQUEST + zero audits, body ackConfidential=true + not-cleared→FORBIDDEN + 1 denied audit, body ackConfidential=true + cleared→success + 1 read audit + last_seen bumped, body General/Internal ignore ack, path-input alias resolves identically to documentId-input, markConsumed verb='ui.mark_canon_consumed' regression guard, dispatchByBackend prefix routing) + 4 SPA tests (was 2, +2: catch-all confirm-click flips ackConfidential, layout chip-toggle filters tree rows). Drift-bombs GREEN: var(--color-)=0 SPA-wide, useEffect in documents routes=0, useState in documents routes=5 (all pure UI state — ackConfidential flag, activeSensitivity Set, expandedDirs Set; NO data-fetching state per SA WP2 bless), trpcClient. in documents routes=5 (all inside queryFn/mutationFn callbacks per SA WP1 bless), useQuery|useMutation=10, old $id.tsx deleted. ADR-W10-4 NOT triggered (view CASE projection extension is non-structural per §3.1; new SP is additive; no agent_runs column or shape change). Carry-forwards: CLOSED C12 (blob walker contract live as canon-watch CA Job) + Cnew-tanstack-debt-documents (migration complete) + Cnew-AP10 (403 audit landing via denied SP) + Cnew-AP11 (last_seen success-only bump verified); NEW C18a (mi-clawpilot-corpus-rw PG role mapping needed for canon-watch first scheduled run at 03:00 UTC — manual smoke posture for PR #10 only, AAD-passwordless PG GRANT is sister-task for follow-up PR), Cnew-mark-consumed-metadata-noop (PR #6 SP jsonb_set(metadata, ARRAY['consumed_by', upn], ...) silently no-ops because intermediate consumed_by key doesn't exist on first mark; audit row + last_seen bump still fire correctly — only the on-doc metadata side-effect drops), Cnew-prefix-cache-doc (document the invalidateQueries({queryKey:['documents']}) prefix-match cascade convention at first-use comment per SA P1), read-row-target-pk-key-mismatch (Steward critical finding: sp_document_record_read writes payload key 'id' but v_human_touchpoints extracts 'pk' → every read.row touchpoint surfaces with NULL target_pk & NULL target_summary; sp_document_mark_consumed uses correct 'pk' key; one-line fix in SP OR tolerate-both in view base — deferred); still open C10, C11, C13, C14, C15, C15a, C16, C18, C20, Cnew-mark-consumed-idem-debt, Cnew-add-note-guard-verify, Cnew-revoke-public, Cnew-ratification-class, Cnew-smoke-param, Cnew-test-symmetry, Cnew-decay-non-touch. PR #12 (merge SHA bb0aa52; iter-0 c247bb5 single-pass — no peer-review iter required); SPA via StaticSitesClient.exe direct (P10: no --env); Function via az functionapp deployment source config-zip OneDeploy cdc7ad4e-fbe7-4961-8ea2-3977375b76c4 (P9 + Flex Consumption: zip via [ZipFile]::Open + CreateEntryFromFile preserving dist-bundle/ path; 2 functions registered roles+trpc); canon-watch CA Job caj-clawpilot-canon-watch-vnet provisioned via YAML spec (provisioningState=Succeeded); 3 DDL audit cross-links (9076d10c view + 99db419f denied SP + e031f544 fixtures) + ship audit 46e62120-cd69-40e2-b88e-c566642361a3. 2026-06-04 BST
PR #9 Risks register: list + 5×5 board + detail with decay countdown + activity drawer + HS-4L write-back fan-out (Start mitigation / Escalate / Assign owner / Add note). Inherits PR #7 chrome + PR #8 fan-out pattern. Risks viewer (§11.7): list route e.$engagement.risks.index.tsx ordered risk_score DESC NULLS LAST, decay_at ASC NULLS LAST with status chip (open→--cp-warning, mitigating→--cp-link, closed→--cp-text-muted, realised→red-strikethrough "REALISED" badge per SA AP4), severity / likelihood / risk_score columns, type chip, owner (ownerEmail), decay countdown column; board view toggle renders 5×5 heatmap with severity = Y axis (vertical ascending bottom→top), likelihood = X axis (horizontal ascending left→right) per SA AP2, cell = read-only count chip of risks with type='risk' AND status IN ('open','mitigating') excluding realised/closed, colour-zoned green/amber/red. Detail route e.$engagement.risks.$id.tsx renders decay countdown derived client-side from decayAt - now() (display-only per SA RX3 — SPs MUST NOT write decay_at) showing "Review due in N days" / "Overdue by N days" (red) / "No decay set"; activity drawer surfaces history array from v_human_touchpoints view (AP1 source), newest-first. Risks tab in engagement nav flipped from nav-disabled span to live <Link activeProps> (other 8 future tabs stay disabled). 3 new tRPC read procs: read.risks.list / read.risks.get / read.risks.board; all filter WHERE type='risk' at read layer (SA RX2 tri-type semantics — risks_issues_blockers table holds risks + issues + blockers, issues/blockers deferred to later PRs); list/board 42P01→[]/{cells:[]}, get 42P01→NOT_FOUND; no *_record_read audit (C16 deferral preserved). Projects substrate engagement_id as DTO engagementCode (SA AP3) and owner_email as DTO ownerEmail (SA RX6 — deliberately NOT ownerUpn to avoid symmetric confusion with decisions' real owner_upn column). 3 new HS-4L stored procedures (parent-applied DDL pre-CLI per Cardinal Rule #3): sp_risk_start_mitigation flips status='open' → 'mitigating' with legal-transition gate (OLD.status='open'P0001 illegal_transition otherwise — closure/realisation deferred to PR #10+ per SA RX1); sp_risk_escalate bumps severity by 1 with cap=5 (P0001 severity_ceiling at ceiling per SA RX4), legal from status IN ('open','mitigating'), requires reason_md (NOT NULL, len>0 → P0001 reason_required) persisted to agent_runs.payload.reason_md ONLY (SA RX4b — mirrors PR #8 R1, no column add, no ADR-W10-4 trigger), does NOT bump likelihood and does NOT flip status, one audit row per gesture with payload {severity_before, severity_after, reason_md}; sp_risk_assign_owner writes owner_email = lower(p_owner_email) with case-insensitive whitelist check (NOT (lower(p_owner_email) = ANY(SELECT lower(unnest(haleon.fn_valid_owners()))))P0001 invalid_owner per SA RX6 — fn_valid_owners() stores lowercase UPNs, raw comparison would silently reject mixed-case input). 4th SP reused not re-authored: sp_risk_add_note already shipped pre-PR #9 (HS-4L compliant, writes generic haleon.notes table) — new mut.risks.addNote mutation calls existing SP (SA RX5). All 3 new SPs SECURITY DEFINER + GRANT EXECUTE w10_app + single-TX + role-guard human:% + xmin OCC (stale_row P0001 on mismatch) + unbounded client_op_id idempotency (JOIN agent_run_payloads p1.payload->>'client_op_id', scoped AND p1.payload->>'table'='risks_issues_blockers') + alias+qualify UPDATE…RETURNING r.* INTO v_row per P7-PR #5 ambig-col lesson + NEVER write risk_score (GENERATED STORED (severity*likelihood), auto-recomputes) or decay_at. Audit emission via haleon.sp_create_agent_run(... 'turn', ...) + separate INSERT INTO agent_run_payloads(run_id, payload) carrying {table, pk, target_table, target_id, after: to_jsonb(v_row), client_op_id, ...verb-specific keys}. 4 new tRPC mutations under mut.risks: startMitigation / escalate (no likelihood/severity inputs — server bumps severity per RX4) / assignOwner / addNote; identity from ctx.user never input (ADR-W10-1 §3.6); error map locked 8-class: stale_row→CONFLICT 409 / hs4l_role_guard→FORBIDDEN 403 / illegal_transition+severity_ceiling+reason_required+invalid_owner→BAD_REQUEST 400 / not_found+42P01→NOT_FOUND 404. SPA risk-detail fan-out: 4 action buttons gated on status ('open'→Start mitigation only, open|mitigating→Escalate via drawer with mandatory reason textarea, open|mitigating→Assign owner inline form free-text MVP, open|mitigating→Add note drawer; closed|realised hides Actions section per AP4); both new routes migrated to TanStack Query (useQuery reads + useMutation + queryClient.invalidateQueries on success, fan-out invalidates 3 query keys: ['risks',engagement,id] + ['risks',engagement] + ['risks','board',engagement]) — first useMutation precedent in the SPA. Additive v_human_touchpoints view extension (CoS AP1 + Steward attestation): risk-branch CASE now projects target_id, target_table, likelihood, risk_score alongside existing title/severity/status — non-breaking CREATE OR REPLACE per ADR-W10-2 §3.1 projection-append allowance, NOT an ADR-W10-4 trigger. New verb taxonomy entries per SA AP5: ui.start_mitigation / ui.escalate / ui.assign_owner (within ADR-W10-1 §3.2 extension allowance, no amendment needed). P7 substrate probes 13/13 PASS (3×happy [start_mitigation/escalate/assign_owner] + 6×negative [CONFLICT/FORBIDDEN/ILLEGAL_TRANSITION/SEVERITY_CEILING/REASON_REQUIRED/INVALID_OWNER all P0001] + 1×positive [uppercase UPN KATIE.EDWARDS@microsoft.com → lowercased katie.edwards@microsoft.com proving normalization] + 1×IDEM [replay same client_op_id → same run_id, +1 agent_runs row only] + 2×view shape [3 touchpoints surface for fixture with target_summary carrying target_id/likelihood/risk_score]). 129 API tests green (was 107, +22: 21 risks router tests + 1 ONE_AUDIT_ROW assertion for sp_risk_escalate per SA WP3) + 2 SPA tests. ADR-W10-4 NOT triggered (no ALTER TABLE, all columns already present on risks_issues_blockers; new SPs additive on existing column shape; view extension within ADR-W10-2 §3.1 allowance; no agent_runs structural change). Substrate findings (Steward read-only probes): the real table is haleon.risks_issues_blockers (tri-type RIB register), NOT haleon.risks — assumed-column drift from design doc resolved without ALTER (assumed owner_upn → actual owner_email; assumed body_md → actual description; assumed last_reviewed_at+review_cadence_days → actual decay_at+escalation_threshold; assumed engagement_code → actual engagement_id); rib_status enum is (open, mitigating, closed, realised) with NO 'mitigated' value (forced SP rename to sp_risk_start_mitigation); risk_score is GENERATED STORED (must never be written by SPs); fn_valid_owners() stores lowercase UPNs (forced case-insensitive comparison in assign_owner SP); sp_risk_add_note + generic haleon.notes table already shipped (no new notes substrate). Fixture risk seeded for smoke: id=abb5a0c6-655f-4e2d-9e07-5dce71d2e8fb status=mitigating severity=4 likelihood=3 risk_score=12 owner_email='smccormick@microsoft.com' (post-smoke terminal state). Carry-forwards: Cnew (RX3) deliberate non-touch of decay_at in PR #9 SPs — explicit "I reviewed this" SP is future-PR concern; Cnew (RX5) existing sp_risk_add_note HS-4L line-1 human:% guard verification (iter-1 test or Steward sister-task attestation); Cnew (AP5) verb taxonomy extended with 3 new ui.* verbs — search-anchor for archaeology; Cnew-tanstack-debt decisions/reports/documents/sessions routes (PR #4-#6) NEVER migrated to TanStack Query despite ratification (e) — only shell + ADRs + now Risks on it; recommend dedicated PR; C15a NEW production risk ingest pipeline (parallel to docs C15); C16 + C18 + C20 still open; Cnew-smoke-param + Cnew-test-symmetry (P3/P4 from CoS code-review — non-blocking, defer to PR #10 polish bundle); Cnew tracking-only (least-privilege REVOKE EXECUTE FROM PUBLIC on all new SECURITY DEFINER risk SPs — Steward sister-task). PR #11 (merge SHA 3db4982; iter-0 d2e6329 + iter-1a SA TS1 ONE_AUDIT_ROW 45e7bab + iter-1b CoS P2 TanStack migration 1f440b7); SPA via StaticSitesClient.exe direct deployment id 28a9dff5-3409-440f-9fb3-3e3911e4a389 (P10: no --env); Function via az functionapp deployment source config-zip OneDeploy 2fc4e22f-d13d-4b32-ac92-720b47ccc5a1 (P9 + Flex Consumption: zip via [ZipFile]::Open + CreateEntryFromFile preserving dist-bundle/ path; 2 functions registered roles+trpc); 4 DDL audit cross-links (eaa58e2c view + 5e693fa8 start_mitigation + e61effce escalate + 7f3fcd14 assign_owner) + ship audit f4b4ad7a-248a-4d8a-9332-3548a50c53a2. 2026-06-04 BST
PR #8 ADRs viewer + decision write-back fan-out (Mark decided only / Mark rejected / Assign owner) + ADR-W10-5 ratification + drop --color-* alias-fallback block. Inherits PR #7 chrome+theme. ADRs viewer (§11.6): list route e.$engagement.adrs.index.tsx ordered ratified→reviewed→draft then created_at desc (stage chip on --cp-success/link/warning, owner local-part with full UPN title tooltip per SA Q4-pin, ratified-date column); detail route e.$engagement.adrs.$id.tsx renders body_md via existing react-markdown dep, with empty-state "Body not yet ingested — see repo file at {repoPath}" when null (SA Q1-pin2). ADRs tab in engagement nav flipped from nav-disabled span to live <Link activeProps> (other 9 future tabs stay disabled). 2 new tRPC read procs: read.adrs.list + read.adrs.get; 42P01→[] on list, 42P01→NOT_FOUND on get; no *_record_read audit (C16 deferral preserved). 3 new HS-4L stored procedures (parent-applied DDL pre-CLI per Cardinal Rule #3): sp_decision_mark_decided_only restores §11.5 two-button flow per C7 closure (sets status='decided' WITHOUT minting ADR draft; emits ONE agent_runs row with triggered_by='ui.mark_decided' + payload.promote_skipped=true per SA R3 verb-taxonomy preservation); sp_decision_mark_rejected requires reason_md (NOT NULL, len>0 → P0001 reason_required) with SA R2 legal-transition gate (OLD.status='proposed'P0001 illegal_transition otherwise), reason persisted to agent_runs.payload.reason_md ONLY (SA R1 — no decisions column add, no ADR-W10-4 trigger); sp_decision_assign_owner writes NEW column decisions.owner_upn ONLY (SA pin P2 — NEVER touches owning_peer which has a CHECK IN ('SA','CoS') CONSTRAINT blocking UPN values, discovered Steward P2 probe), validates against haleon.fn_valid_owners() SECURITY INVOKER text[] whitelist with case-insensitive comparison (SA pin P3) → P0001 invalid_owner otherwise. All 3 SPs SECURITY DEFINER + GRANT EXECUTE w10_app + single-TX + role-guard human:% + xmin OCC + unbounded client_op_id idempotency + alias+qualify UPDATE…RETURNING…INTO per P7-PR #5 ambig-col lesson. 3 new tRPC mutations under mut.decisions: markDecidedOnly / markRejected / assignOwner; identity from ctx.user never input (ADR-W10-1 §3.6); error map stale_row→CONFLICT 409 / hs4l_role_guard→FORBIDDEN 403 / reason_required+illegal_transition+invalid_owner→BAD_REQUEST 400 / 42P01→NOT_FOUND 404. SPA decision-detail fan-out: 3 new buttons gated on status='proposed' for the decided/reject pair, always-visible assign-owner; reject opens drawer with mandatory reason textarea; assign-owner inline form with case-insensitive UPN input (free-text MVP, full people-picker = NEW C20 carry to PR #17); on success queryClient.invalidateQueries(['decisions',engagement,id]). C5c closure: <span data-status="draft-adr"> inert badge AND post-promote banner both flipped to live <Link to="/e/$engagement/adrs/$id">; ADR detail back-link to source decision when linkedDecisionId set; persistent decision.adrId link on detail when set. API DTO discipline (SA pin P1): read.decisions.list/get project ownerUpn + owningPeer as SEPARATE fields (NOT collapsed); legacy owner field DROPPED with SPA sweep to decision.ownerUpn ?? decision.owningPeer in 2 routes (iter-2 SA E1 surgical). ADR-W10-5 ratified (C19 closure): "auth dual-path read-only contract surface" — SA-authored, parent-applied via Steward DDL; back-fills PR #7's tRPC authRouter mount alongside retained SWA Function /api/auth/roles; locks both paths to identical projections of x-ms-client-principal.userRoles; no-mutation hard invariant on both paths enforced by kernel-purity grep (auth.ts has zero .mutation() + new vitest authRouter._def.procedures assertion + Steward audit-cron parity check; future write-back to auth context requires its own ADR. Stored at m-policies/adr/draft-W10-5-20260604.md + haleon.adrs.id=3920aa4b-65bf-4db5-b47a-5470f66d44ff adr_number=13 status='ratified' review_stage='ratified' body_md=13287 chars; ratification audit cceac528-0688-43e9-a160-1e895a2268a8. C17 closure: 13-entry --color-* alias-fallback block dropped from index.css :root; post-drop drift-bomb asserts zero var(--color-*) ANYWHERE in apps/spa/src (was: outside index.css only; now: zero anywhere). Live CSS verified 50 --cp-* refs + ZERO --color-* refs. P7 substrate probes 13/13 PASS (3×decided_only [CONFLICT/FORBIDDEN/IDEM] + 5×rejected [CONFLICT/FORBIDDEN/REASON_REQUIRED/ILLEGAL_TRANSITION/IDEM] + 4×assign_owner [CONFLICT/FORBIDDEN/INVALID_OWNER/IDEM+CASEI] + C1 verb verify triggered_by='ui.mark_decided' per SA R3). 107 API tests green (was 80, +27: 17 decisions.test.ts + 8 new adrs.test.ts + 2 auth.test.ts including shape-parity + no-mutation per ADR-W10-5 §3.5) + 2 SPA tests. ADR-W10-4 not triggered (additive nullable columns only; no agent_runs column change; new HS-4L SPs are additions). Substrate findings (Steward read-only probes): haleon.adrs live cols had no body_md/summary/owner_upn/ratified_at (column drift from design assumption — resolved by adding body_md + reusing accepted_at AS ratifiedAt + reusing decision_id AS linkedDecisionId + reusing accepted_by_email); haleon.decisions.owning_peer CHECK IN ('SA','CoS') blocked UPN reuse → ADD owner_upn text (SA pin P2 reversal). pg_notify('decision_status_change') channel has no live LISTEN consumers (Steward P4 attestation) — mark_decided_only + mark_rejected will fire the pre-existing trigger but no downstream surprise. Carry-forwards: C5c+C7+C17+C19 CLOSED; C16+C18 still open; C20 NEW (Steward audit-cron ADR body_mdrepo_path file SHA drift detection) + Cnew tracking-only (consider adding 'ratification' to agent_runs.class_check for finer audit-spine partitioning — tried first, fell back to class='audit' with triggered_by='ratify.adr_w10_5_auth_dual_path' carrying the verb). PR #10 (merge SHA ebdf978; iter-0 2ef7bfd + iter-1 97f6c06 + iter-2 c4ca0bd); SPA via StaticSitesClient.exe direct (P10: no --env); Function via az functionapp deployment source config-zip OneDeploy 2f61370d-59fd-4eb1-ac6d-9d6d1352fb5e (P9: zip via [ZipFile]::Open + CreateEntryFromFile preserving dist-bundle/ path; 2 functions registered roles+trpc); audit 9e85cfc0-1176-46d1-a218-c3ab56a5f3c6. 2026-06-04 BST
PR #7 App shell + theme retrofit + engagement front page + two-TOC nav + TanStack Query + Settings + RefreshedStamp. Foundational chrome inherited by all PR #8–#17. App shell (__root.tsx): sticky header with backdrop-filter:blur(8px) on var(--cp-panel-strong), brand+ring-dot, disabled ⌘K search stub (ships PR #13), Radix DropdownMenu user-menu (Settings + Sign out via /.auth/logout), theme toggle writing data-theme on <html> with localStorage persistence + prefers-color-scheme first-load, skip-link, .container max-width. Theme retrofit: full --cp-* light + html[data-theme="dark"] inversion mirrored in apps/spa/src/index.css + packages/shared/src/design-tokens.ts; --color-* alias-fallback block retained in :root for THIS PR only per ratification (f) (drops PR #8 = C17 carry-forward). Token rename across 15 files / 209 refs; drift-bomb asserts zero var(--color-*) refs outside index.css. Engagement layout route e.$engagement.tsx renders <Outlet/> (P6 trap avoided) + two-row header (name · type · phase + LIVE health pill bound to portfolio.v_engagement_health with --cp-success/warning/danger/text-muted map + RefreshedStamp) + top-tab nav: 6 live tabs (Portfolio · Overview · Decisions · Reports · Documents · Sessions) + 9 disabled tabs as <span role="link" aria-disabled="true" title="Ships in PR #N"> (NEVER <Link>, NEVER <button>, per ratification c + SA-E6). Engagement home e.$engagement.index.tsx (§11.2): markdown OVERVIEW via new read.engagement.overview tRPC proc with repo-standard try/catch on err.code==='42P01'null (matches reports.ts/decisions.ts, NOT a to_regclass probe per SA-E2) + 3 widget cards (Open Decisions count+top-3, Recent Reports top-5 via byDate(30d), Recent Docs top-5). Empty-state copy rendered when no overview report exists. NO *_record_read audit on nav reads (deferred to N1 = C16 carry-forward, per SA-E3). /settings stub (pinned engagements/theme/UTC placeholders, no 404 from user-menu). TanStack Query: QueryClientProvider + ReactQueryDevtools in __root.tsx; queryClient.staleTime=30000 ADDED preserving existing refetchOnWindowFocus:false + retry:1 (SA-E4); migrated ALL 9 data route components from useState/useEffect/trpcClient to useQuery/useMutation in single squash (no mixed-mode per SA-E2; onError preserves CONFLICT/FORBIDDEN/NOT_FOUND mapping). authRouter mounted on tRPC for user-menu role read (dual-pathway with existing SWA Function /api/auth/roles; mutation-free per SA-A1; ADR-W10-5 follow-up draft = C19). RefreshedStamp auto-ticks every 10s. Test setup resets <html data-theme> between vitest runs (SA-E3). 82 tests green (80 API +2 SPA; +4 new engagement.overview covering mapped row / 0 rows / 42P01 / re-throw on non-42P01). ADR-W10-4 not triggered (no agent_runs column or view shape change). Substrate findings: portfolio.v_engagement_health EXISTS (currently band=unknown for both seeded engagements); haleon.reports does NOT exist (42P01 path validated). Carry-forwards: C16 N1 canonical *_record_read consolidation pre nav-audit, C17 drop --color-* aliases PR #8, C18 bind user-menu UPN from auth context (defer PR #17), C19 ADR-W10-5 (auth dual-path read-only contract surface; SA owns draft pre-PR #8 ship). 2026-06-04 ~05:10 BST shipped PR #9 · commit 802597f · deploy 8d07e39a / 181fcdca
PR #6 W10 MVP closeout. Documents viewer (Sub-PR-A): list + detail SPA routes (/e/$engagement/documents, /e/$engagement/documents/$id) over the PR #2.5a read-canon scaffold; dispatchByBackend wired for storage_backend='blob' via @azure/storage-blob + DefaultAzureCredential (UAMI id-w10-api-uks) reading from stw10apiuks/documents/<storage_path> per ADR-W10-3 §3.4; sharepoint/external_url/ado_attachment backends stubbed not_in_mvp (C12/C13 W11). 403-on-deny audit (SA-E2): new SP sp_document_record_read_denied(role, doc_id, claimed_label) applied SECURITY DEFINER (audit aab7f424) and called in requireSensitivityClearance catch path BEFORE rethrowing FORBIDDEN; last_seen_at UPDATE constrained to blob-success branch only (SA-E3); consumedAt projection derived from agent_runs audit join (payload key pk); path-injection zod refinement on storage_path before BlobClient compose. Pre-CLI fixture document id=1 (W10 MVP Welcome, Internal, audit da805686). Sessions browser (Sub-PR-B): SPA-only routes (/e/$engagement/sessions, /e/$engagement/sessions/$id) rendering plan.md via react-markdown + files/ tree depth-2 default; data fetched client-side from corpus-mirror SWA jolly-dune via sessions/index.json manifest (no API proxy per ratification (c)); zero new apps/api code (SA-E4 dropped sp_session_record_read entirely — SWA edge logs are the access record); engagement-tag inline in manifest (SA-E6 fallback, SWA-managed mirror has no x-ms-meta-* per Steward attestation). Session-state implicit-Internal class (SA-E5) — never Confidential-Customer/Regulated-Restricted. Corpus-sync extension (Sub-PR-C): operator-side sync.ps1 + SKILL.md updated for new sessions container with plan.md+files/-only privacy filter (RISK-1) excluding checkpoints//turn logs/state JSON; CORS staticwebapp.config.json committed at infra/corpus-mirror-staticwebapp.config.json (FLAG-7). Tests: 78 passing (73 + 5 new A1–A5 covering blob branch, denied-audit, last_seen_at scope). 2026-06-03 ~20:30 BST shipped PR #8 · commit 14315a4 · deploy ae19b59f / 4c98d23e
PR #5 Decisions: mark-decided + promote-to-ADR-draft (second human write-back; first optimistic-concurrency surface). Single modal on the decision detail (clientOpId at modal-open, react-markdown body + adrSummary preview, inert <span data-status="draft-adr"> badge — no anchor, ADR viewer ships W11). New tRPC mut.decisions.markDecided (protectedProcedure, identity ctx-derived) invokes live HS-4L SP sp_decision_mark_decidedone SP body / one TX, role-guarded (human:%), xmin optimistic concurrency (stale → P0001 stale_rowCONFLICT 409 → toast + re-fetch), unbounded client_op_id idempotency, emitting TWO agent_runs rows per ADR-W10-1 §3.2 verb table (ui.mark_decided on the decision + ui.promote_adr_draft on the new adrs draft, shared client_op_id). Atomic flip status='decided' + mint adrs draft (review_stage='draft') in the single TX. read.decisions.history widened to surface all three verbs. §11.5 single-button MVP (save-decided-only variant = W11 carry-forward C7). 2026-06-03 ~19:35 BST shipped PR #7 · commit 62f5641 · deploy dbaaeb0d
PR #4 Decisions: list + detail + first human write-back (addNote). 2 SPA routes (/e/:engagement/decisions list w/ status+class filters + deep-link query state, /e/:engagement/decisions/:id detail + optimistic note drawer) via TanStack file-based routing. 4 tRPC procs (read.decisions.list/get/history + mut.decisions.addNote) all protectedProcedure; identity ctx-derived from x-ms-client-principal (never a tRPC input). addNote invokes the live HS-4L SP sp_decision_add_note — single-TX, role-guarded (human:% or hs4l_role_guard P0001), unbounded client_op_id idempotency, widened to RETURNS TABLE(run_id, note_id, body_md, created_at, actor_role). “Human activity” panel consumes haleon.v_human_touchpoints (human-only). Mandatory front-door nav: non-nested “Decisions →” <Link> sibling on every EngagementCard (browser-smoke-gated, no stub). Graceful 42P01NOT_FOUND for the test_engagement_1 substrate gap (te1 SP mirror = follow-up Steward task, parallel to PR #5). xmin::text projected in list+get to forward-bind PR #5 optimistic concurrency. 2026-06-03 ~17:04 BST shipped PR #6 · commit e6ce78a · deploy b78871b8
PR #3 Reports list + viewer (read-only). 3 SPA routes (/, /e/:engagement/reports with By Date | By Type tabs, /e/:engagement/reports/:type/:id viewer) wired via TanStack file-based routing. 3 tRPC procs (read.reports.byDate/byType/get) all protectedProcedure, audit-emitting via sp_record_read (prefix-normalized at this PR). 7 typed handlers (handler-map dispatch — no DB-stored SQL execution); asOfDate-DESC sort with range chips; graceful 42P01 fallback for the 5 test_engagement_1 substrate-gapped slugs. Shared resolveSchema helper extracted. MVP scope ratified: 7 aggregates per engagement; per-instance enumeration deferred (see docs/pr-3-README.md). 2026-06-03 ~11:40 BST shipped PR #4 · commit 46c80bb · deploy 4db7e4b1
PR #2.5a Design-canon read core. read.documents.list/body (sensitivity-gated per ADR-W10-3, audit-emitting via new combined SP sp_document_record_read) + mut.documents.markConsumed (HS-4L, client_op_id idempotent) + in-repo m_read_canon shim. Identity from x-ms-client-principalctx.user + protectedProcedure; engagement → schema_name resolved via portfolio.engagements_registry + regex allow-list (no raw SQL interpolation). Pack schema-v3: dedicated _w10.pack.schema.json minted; _w10/pack.json bumped to v3 with populated canon block + both seed engagements. m_read_canon staged optional in 4 role overlays (SA/CoS/Steward/LibraryCurator); flip to required gated on FR-002 host m-tools surfacing. test_engagement_1 schema mirror back-filled to match haleon canon surface. 2026-06-03 ~10:02 BST shipped PR #3 · commit 9b8ec0c · deploy f2b32bfa
PR #2 Engagements registry + portfolio overview (read-only). First real DB connection via UAMI AAD token to PG Flex (w10_app role NOINHERIT, SET ROLE on connect). Folds PR #1 carry-forwards: auth claim-shape hardening, single auth pathway, in-DB Bicep comment block. 2026-06-02 ~18:25 BST shipped PR #2 · live API
PR #1 “Hello W10” deploy proof — SPA shell + Function health.ping/auth.roles + SWA Entra auth config + Bicep. Proves the deploy + auth path end-to-end. 2026-06-02 ~16:00 BST shipped PR #1 · live

PR #6 detail (W10 MVP closeout). Squash-merge 14315a4 on main (gh PR #8, branch w10/pr-6-documents-sessions). Single-squash across three sub-PRs: Sub-PR-A documents viewer (real blob-UAMI dispatch wired against stw10apiuks/documents via @azure/storage-blob + DefaultAzureCredential; replaces the PR #2.5a not_in_mvp stub); Sub-PR-B sessions browser (SPA-only, zero apps/api code, direct fetch of corpus-mirror manifest); Sub-PR-C corpus-sync skill extension (new sessions container, privacy filter). DDL applied pre-CLI: sp_document_record_read_denied(text,bigint,text) SECURITY DEFINER + GRANT EXECUTE w10_app (audit aab7f424) per SA-E2 / ADR-W10-3 §3.4 (refused reads are first-class audit events). Pre-CLI fixture: haleon.documents id=1 (W10 MVP Welcome, sensitivity=Internal, storage_backend=blob, storage_path=engagement-demo/welcome-w10-mvp.md, sha256 416e66a5…); blob uploaded to stw10apiuks container documents at the exact key (audit da805686; storage temporarily opened with IP allowlist then locked back to publicNetworkAccess=Disabled). Function OneDeploy ae19b59f-122c-411a-b015-a44157382bbb (2 functions registered post-deploy, P3 GREEN). SPA deploy 4c98d23e-7206-4455-b812-b8048cefec9f via StaticSitesClient.exe direct invoke (W8 quirk fallback; swa wrapper persistent W8 issue). 78 vitest tests pass (73 existing + 5 new A1–A5 covering blob branch + last_seen_at scope + denied-audit). Smoke (Cardinal Rule 1): VERIFIED via P7 substrate probes — P6-A sp_record_read(role,'document','list') list-load audit adad6a44; P6-B sp_document_record_read(role, 1) body-view audit 55ee263a AND documents.last_seen_at UPDATEd to 2026-06-03 20:28 per SA-E3; P6-C sp_document_record_read_denied(role, 1, 'Confidential-Customer') denied-row 63e8ab45 with outcome='denied' + claimed_sensitivity_label in payload per SA-E2. Chain 362 → 375. SPA / returns 302 (AAD auth wired); Function /api/roles returns 401/400 unauth (healthy). Audit 80f52468 covers the deploy. Sub-PR-C residual: sync.ps1 manifest emission + per-session files.json writers deferred to PR #6.1 / W11 (C16) — path filter + container plumbing landed, manifest generation logic to follow; until then sessions browser shows graceful empty state. CA Job caj-clawpilot-corpus-sync-vnet last successful run sdvcy3h; next manual run after C16 lands will populate the sessions container. Carry-forwards: C7 mark-decided-only button + SP, C5c ADRs route, C9 v_human_touchpoints target_id docs (all W11 from PR #5); C12 SharePoint walker for 'sharepoint' backend (ADR-W10-3 §3.9 Phase 2), C13 ADO-attachment walker for Phase 3, C14 narrow id-w10-api-uks on stw10apiuks from Owner/Contributor → Storage Blob Data Reader (post-MVP least-priv), C15 per-row documents container via metadata, C16 Sub-PR-C manifest emission (PR #6.1).

🛠 Lessons learned (empty-substrate smoke requires fixture; SWA-managed mirror has no per-blob metadata; content-exclusion guards operator skill files; UAMI over-priv is a follow-up not a blocker).

  • Cardinal Rule 1 ("smoke probe with real data") requires a pre-CLI fixture when the substrate is empty. haleon.documents was 0 rows at PR-start; without a fixture the documents surface would have shipped non-deliverable (list shows nothing, body can't be probed). Parent applied a generic-framed Welcome doc via lab-admin INSERT + blob upload to the W10 API storage account (temporarily opening publicNetworkAccess with an IP allowlist for upload, then locking back). Codifies w10-build P8: when the live substrate is empty for the surface a PR ships, the parent provisions a generic fixture pre-CLI — tracked as a data-prep audit row, not a code-DDL one.
  • SWA-managed Static Web Apps cannot carry arbitrary x-ms-meta-* per file. The corpus-mirror SWA is deployed via swa deploy --deployment-token, which writes to SWA-managed staging blob — no access to set blob metadata at PUT time. Per-blob engagement tags (initial SA-E6 design) are not achievable on this surface; tagging must live inline in a manifest. The (Steward-discovered) cleaner fallback — embed engagement in sessions/index.json — drops a moving part and is forward-compatible with a manifestVersion field.
  • Operator-side skill files are not editable by the local Copilot CLI. Content exclusion policy blocked the CLI from reading or writing ~/.copilot/m-skills/corpus-sync/. Sub-PR-C plumbing (path filter, new container, SKILL.md docs) was applied by parent directly. Lesson: when a PR spec spans repo + operator-side artefacts, the spec must split scopes (CLI = repo only; parent = operator-side); never instruct the CLI to edit operator paths.
  • UAMI over-privilege is a least-privilege follow-up, not a deploy blocker. id-w10-api-uks holds Storage Blob Data Owner + Contributor on stw10apiuks — over-privileged for ADR-W10-3 §3.4 which only requires Reader. The blob fetch works fine for MVP read paths; narrowing to Reader is C14 (post-MVP). Same family as PR #2's UPN allow-list hardening — identity-plane lockdown moves AFTER functional correctness, never before.

PR #5 detail. Squash-merge 62f5641 on main (PR branch w10/pr-5-mark-decided; CLI iter-0 b20da7f → iter-1 SA-E1 two-audit-row fix; squash e7b7543). PD-1 SP sp_decision_mark_decided applied pre-CLI by predecessor Steward (audit e041dfc8, chain 354; SECURITY DEFINER, advisory-locked adr_number MAX+1, synthesized repo_path m-policies/adr/draft-{n}-{YYYYMMDD}.md). Function OneDeploy dbaaeb0d (iter-1, SA-E1 two-row fix); SPA swa-deployed clean. 71 API + 2 SPA tests green; drift-bombs intact (incl. esbuild --external:@azure/functions-core ONLY, @azure/functions inline per PR #4 hotfix ee504dc). Smoke (Cardinal Rule 1): VERIFIED — unauthenticated front-door / list / detail probes all 302; P5-CONFLICT substrate probe GREEN (stale xmin → SP raises stale_row, zero adrs rows written); P5-IDEM substrate probe GREEN (same client_op_id → same adrId, 1 new adrs row, 2 audit rows per SA-E1 verb table). Test decision 3ce35c5e… now status='decided'; ADR 37e4b7e1… created review_stage='draft'. Second human write-back + first xmin optimistic-concurrency surface live. Known follow-ups: restore “Mark decided only” button + sp_decision_mark_decided_only SP (C7, W11); ADR list+detail routes /e/:engagement/adrs + /:adrId replacing the inert badge with a live <Link> (C5c, W11); document the target_id linkage convention in the v_human_touchpoints projection contract (C9, W11); canonical *_record_read SP consolidation; test_engagement_1 adrs/SP mirror (Steward, parallel). C8 obsolete (view already extended pre-CLI).

🛠 Lessons learned (composite single-TX write-back proven; PL/pgSQL column-collision trap; substrate probes beat Playwright skips for OCC paths).

  • PL/pgSQL ambiguous-column is a pre-deploy SP-author trap when names collide three ways. An UPDATE…RETURNING…INTO where the local variable (v_decided_at), the RETURNS TABLE signature column, AND the table column all share a name raises column reference is ambiguous at execute-time — not at CREATE FUNCTION. Fix: alias the table and qualify (UPDATE haleon.decisions AS d … RETURNING d.status::text, d.decided_at INTO …). The predecessor Steward SP needed 2 corrective DDLs; build + unit mocks stayed green — only the live substrate probe caught it post-deploy.
  • One SP body, two audit verbs — a composite write-back must not collapse to one row. sp_decision_mark_decided flips the decision AND mints the ADR draft in one TX but emits TWO agent_runs rows (ui.mark_decided + ui.promote_adr_draft, shared client_op_id) per ADR-W10-1 §3.2. A single composite verb would have buried ADR-promotion from every v_human_touchpoints consumer (Steward gap-scan, “ADRs promoted this week”, Reviewer-triggers). Idempotency replay matches EITHER row → returns the original tuple. The history-panel promote pill derives from ui.promote_adr_draft (the ADR-creation signal), never ui.mark_decided (decision-level).
  • Substrate probes beat Playwright skips for backend optimistic-concurrency paths. Playwright can’t simulate a stale xmin without backend orchestration (T2/T3 were honestly test.skip'd). Parent-runnable injected-principal round-trips against the live SP gave authoritative verification in ~30s: P5-CONFLICT (out-of-band row bump → stale xmin → 409 + zero adrs rows) and P5-IDEM (same client_op_id → same adrId, single ADR row, exactly 2 audit rows). Codified as w10-build P7: when a path is backend-OCC and Playwright must skip, a live-SP substrate probe is the authoritative gate, not the unit mock.

PR #4 detail. Squash-merge e6ce78a on main (iter-0 03321c6 + iter-1 column-drift fix dd08ce4 + iter-1.1 a96d40b nullable decision_class widen), then 4 post-merge fixes: ee504dc (bundle @azure/functions inline — OneDeploy worker silent-death on missing node_modules; audit 9f9c564a, chain 348), 9cc8b8d (rename list routes to .index.tsx — TanStack parent-without-Outlet nav bug: the $id detail child never mounted; audit 81733fdd, chain 349), ff98a25 (gitignore deploy artifacts), 8735e6f (read-path iter-2; audit fe8ab6bf, chain 350). Function OneDeploy progression b9982ce1 (silent worker-death) → d6589f69 (working) → b78871b8 (final, read-path). SPA redeployed clean (routeTree.gen.ts regenerated with both decisions routes). Deploy audit 204107a3; SP-widen audit b9c7919e (chain 320). 62 API + 2 SPA tests green. Smoke (Cardinal Rule 1): VERIFIED — Charlie-attested end-to-end 17:04 BST + Playwright T1–T4 PASS (optimistic visible 178ms, stable 2s); idempotency replay VERIFIED (Playwright T5 — same clientOpId → same noteId=16; HS-4L SP idempotency seal proven end-to-end). Read-path iter-2: read.decisions.history filter rewritten to traverse target_summary->>'target_id' (CLI iter-0 assumed target_pk=<uuid> but the view stores target_pk=<notes.id bigint> with the decision uuid buried in target_summary JSON); commit 8735e6f, OneDeploy b78871b8, audit fe8ab6bf (chain 350); Steward substrate-verify returned 12 persisted ui.add_note rows for the test decision. First human write-back in W10 live. Known follow-ups: TanStack Link params as any typing cleanup; canonical *_record_read SP consolidation (sister cleanup PR); test_engagement_1 decisions/SP mirror (Steward, parallel to PR #5).

🛠 Lessons learned (first write-back contract proven; column-drift + nav-mount + reload read-back are pre-/post-smoke gates; Flex bundle-externals recur).

  • The HS-4L optimistic-UI contract holds end-to-end. SPA-generated clientOpId (at drawer-open) + optimistic prepend + match-by-clientOpId server reconcile + rollback-on-error, against an unbounded SP-keyed idempotency seal, gives exactly-once UX with no SP-side TTL. Replaying the same clientOpId returns the original (run_id, note_id, …) — single row (T5: noteId=16 stable). Reference pattern for every future write-back; PR #5 markDecided carries it forward + adds xmin optimistic concurrency.
  • Column-name drift is a pre-CLI Steward-verify gate. Iter-0’s SELECT invented owner/review_stage/body_md; live columns are owning_peer/body with no review_stage. A Steward column-attest before the spec ships would have collapsed iter-1. Future specs cite Steward-verified live columns inline; never let the CLI infer schema.
  • TanStack parent-layout-without-Outlet is a post-deploy nav-mount failure class. A parent route segment rendering no <Outlet> silently never mounts its $id child — build + unit tests stay green; only real browser nav catches it. Same family as PR #3’s SPA-routing punt + EngagementCard stub. Mitigation: w10-build post-deploy nav-mount probe (P6).
  • Write-correctness and read-surfacing are separate gates — smoke the reload, not just the optimistic render. addNote persisted provably (T5 stable), but the note initially didn’t re-surface on reload: the v_human_touchpoints history filter assumed target_pk=<decision uuid> when the view stores target_pk=<notes.id> with the decision uuid in target_summary JSON. Caught post-smoke, fixed iter-2 (8735e6f), substrate-verified (12 rows). Lesson folded into P6: always smoke the reload read-back of a write-back, not just the in-session optimistic prepend.
  • Flex OneDeploy needs externals inlined or the worker dies silently. @azure/functions stays external, everything else must bundle; a missing node_modules at runtime makes the deploy “succeed” while the worker never starts (no 500, just dead — first attempt b9982ce1). Mirrors PR #2’s esbuild lesson — always probe a live API endpoint post-deploy, never trust deploy-status alone.

PR #3 detail. Squash commit 46c80bb on main; Function OneDeploy 4db7e4b1 (status 4); SPA redeployed (routeTree.gen.ts 3775 bytes, 3 routes registered: / + reports list + reports viewer); deploy audit ce52df40, chain head 317. Three iterations: iter-0 caught 6 BLOCK findings across triple peer review (wrong conceptual model: catalog-browser vs instance-enumerator; 7 invented column names on every handler; auth-bypass would-have-been; SQLi via raw ${engagement}; SPA route registration punt; dist-bundle committed). iter-1 fixed handler SQL via Steward column-verify + 42P01-specific graceful fallback for test_engagement_1 substrate gap + asOfDate model pivot + 4 missing w10_app SELECT grants (audit 102e6436). iter-2 fixed SPA route registration via TanStack createFileRoute convention. iter-2.1 (parent-direct) caught + fixed an iter-2 regression where / route lost registration when CLI switched to RouterProvider. Smoke #3 caught a long-standing sp_record_read double-prefix bug (would have stored peer:peer:cos on already-qualified inputs); Steward hotfixed both schemas pre-audit (audit d8b059db, chain 314); historic double-prefix rows left intact per F3 hash-chain integrity. MVP scope explicitly ratified at 7 aggregates per engagement (per-instance enumeration deferred fast-follow — see docs/pr-3-README.md).

🛠 Lessons learned (model misreads + audit-SP normalization debt + SPA-routing punt detection).

  • Catalog vs instance is a load-bearing distinction. Iter-0 implemented "list the 7 templates" when the design (§11.3 "Daily 5-3-2-1 (37)" instance counts) implied "list dated report instances." Both are valid product directions; without an explicit ratification, the CLI converged on the safer (degenerate-but-correct-rendering) catalog model. Resolved by parent ruling: MVP ships 7 aggregates; per-instance is a fast-follow once data density warrants. Future PRs: ratify model intent in the spec before the CLI runs.
  • Audit-SP prefix normalization is a single-source-of-truth contract. sp_record_read (legacy from PR #2.5a-era design) unconditionally prepended peer:; sp_document_record_read (W10) normalized verbatim-when-prefixed. Mixed contracts across SPs in the same schema means callers can't pass ctx.user.role uniformly. Steward's hotfix aligned sp_record_read to the verbatim-when-prefixed contract — future SPs MUST follow this pattern.
  • The CLI will punt on "build warnings are just warnings." Iter-1 punted on TanStack route registration because the build succeeded with warnings. The 3 route files weren't picked up; routeTree.gen.ts stayed at 1026 bytes (only __root). Iter-2 fixed it via real createFileRoute exports. Lesson: build success ≠ feature shipped. The roadmap-seal must wait for a real browser smoke (or, in this case, post-build verification that routeTree.gen.ts > 1500 bytes and contains the route paths).

PR #2.5a detail. Squash commit 9b8ec0c on main; Function OneDeploy f2b32bfa (status 4); SPA redeployed; smoke probes 302-gated correctly; deploy audit fe4b6d1d, chain head 309. New SECURITY DEFINER SP sp_document_record_read(role,doc_id) applied to both haleon and test_engagement_1 (atomically writes agent_runs row + bumps documents.last_seen_at; required because w10_app is SELECT-only on documents). test_engagement_1 schema mirror back-filled pre-deploy. Corpus side: _w10.pack.schema.json minted + _w10/pack.json v3 + 4 role overlays carry m_read_canon as optional + bootstrap-roles/SKILL.md documents the optional→required flip. FR-002 filed for host m-tools surfacing.

🛠 Lessons learned (triple-review gate paid off; substrate drift trap; host-tool boundary).

  • Triple peer review caught 6 HIGH issues pre-merge. Iter-1 CLI shipped code with: identity taken from tRPC input (full sensitivity-gate bypass); raw ${engagement} SQL interpolation (injection vector); 4 wrong SP signatures (sp_record_read args transposed, sp_document_mark_consumed 3 of 4 params missing); fictional 'db' storage_backend + non-existent body column (would 500 on first call); committed stale packages/shared/dist masking schema imports (API would 500 on every cold start). All caught + corrected in iter-1 round before merge. Zero reached production.
  • "P7-applied" ≠ "has the new SP". When a later PR adds a new SP, it must be back-filled into every already-applied engagement schema, not just the new ones. haleon was P7-applied with only sp_record_read; sp_document_record_read had to be explicitly added (audit da2ea5d4) or every body read would have 500'd function does not exist.
  • Host m-tools surfacing is a separate boundary. The in-repo m_read_canon shim documents the contract; the actual peer-side tool needs the Clawpilot host m-tools manifest to surface it (filed as FR-002). Overlay listing as required before that lands would BOOT_REFUSED every peer spawn. Optional + halt-logic-armed is the safe stage; flip is a one-line follow-up.

PR #2 detail. Commits 0e970e6 + 44ab125 (esbuild hotfix) + 898a084 (Date→ISO hotfix). Live: /api/trpc/portfolio.engagements returns both seed engagements (haleon + test_engagement_1) in ~2.75s. DB path: Function → PG Flex via UAMI AAD token; w10_app NOINHERIT with SET ROLE at connect; least privilege (SELECT-only) confirmed. No new Azure spend (existing Function + PG).

🛠 Lessons learned (Flex deploy + bundling — high-value; folded into the w10-build skill).

  • Flex deploy is OneDeploy, not blob-upload. Use POST {scm}/api/publish?type=zip&RemoteBuild=false&Deployer=AzureRestApi with an AAD bearer — NOT blob upload + restart + syncfunctiontriggers. The predecessor recipe fails silently on Flex: PR #1 was actually broken on the API side from day 1 — only the static SPA shell rendered.
  • Bundle the Node v4 model with esbuild → single CJS, @azure/functions external, --packages=bundle for everything else. Node v4 glob-load chokes on mixed CJS/ESM packages like pg.
  • PG firewall needs AllowAllAzureServices (0.0.0.0–0.0.0.0) for Flex outbound to reach PG Flex.
  • AZURE_CLIENT_ID app setting is required to disambiguate ManagedIdentityCredential to the UAMI's clientId.

PR #1 detail. Resources provisioned in rg-clawpilot-uks (uksouth): swa-w10-clawpilot-uks · func-w10-api-uks · id-w10-api-uks · stw10apiuks · appi-w10-uks · log-w10-uks · plan-w10-flex-uks. Auth: SWA built-in Entra ID; operator UPN locked as smccormick@microsoft.com; AAD reader trio = smccormick + ethelokorji + Katie.Edwards (case-insensitive, in the W10_ALLOWED_UPNS Function app setting). Verified live: “W10 — Hello” renders, auth chain works end-to-end, “Peer-ring documentation” Help link present in footer.

💷 Cost note. SWA Free→Standard adds ~£7/mo (Linked Backends require the Standard SKU) — marginal vs the design’s ≤£2 estimate. Flagged for the cost-telemetry rollup.

How the work sequences

Dependency map

Most P0 items unblock several P1s; once F1 lands, an entire fan-out of P1 items becomes shippable. The P2/P3 layer waits for the P1 substrate. F2 (off-laptop scheduler) and F22 (always-on scouts) were retired 31 May — everything sequences against F1 directly.

Wave 1 (P0 — the foundation)

F1 Hosted PG → foundation for almost everything
F3 Audit-spine integrity → foundation for F8d + F21 attestation
F4 Key Vault for PG conn → required by F1 + F6 (L0)
F5 Kernel-vs-pack manifest → prerequisite for F6 + F11

Wave 2 (P1 fan-out after F1 lands)

From F1: F7, F10b, F11, F12, F14
From F4+F5: F6
From F6: F8, F13
From F1+F18: F26 Ticker
From F1+F4+F11: F27 continuity playbook
Standalone (parallel anytime): F10a, F16, F18, F20

Wave 3 (P2 scale-out)

From F10b: F10c, F19
From F11: F15
From F14+F7: F17
From F15+F3: F21
F9 / F28 retired 31 May — sign-off stays on the Steward-DM-via-m365_send_chat_message path.

Wave 4 (P3 hygiene)

Standalone anytime: F23, F24, F25.
Recommended to land F25 with F3 so the integrity story and the DR posture ship together.