DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path phases/phase-51-marketing-site-and-companion-email/phase-51-cr-marketing-site-and-companion-email-v0_1.md

DUNIN7-M4 — INFRASTRUCTURE CHANGE REQUEST

CR-2026-066 — Phase 51: Marketing Site, Companion-Driven Email, and Phase 45 E2E Tests (v0.1)

Version. v0.1 Date. 2026-05-09 Author. Claude (drafting) / Marvin Percival (approving). Target. /Users/dunin7/loomworks-engine and /Users/dunin7/loomworks on DUNIN7-M4 (MacMini M4); plus a new repo DUNIN7/loomworks-marketing to be created during Step 4. Baseline reference. Tag phase-50-companion-as-authority-and-public-form on engine c01124d (annotated tag object 67ba251); on Operator Layer e4c09e0 (annotated tag object fd8177a). Phase 50 final state per handoff §6: 2,095 tests passed / 26 skipped, Alembic head 0064, working tree clean (engine); 139 vitest passed, 29 test files, 11 prerendered routes, eslint/tsc/build clean (Operator Layer). Priority. Standard. Confidential. Internal DUNIN7. Supersedes. No prior Phase 51 CR — v0.1 is the first. CR number. [CC verifies at Step 0]CR-2026-066 is the expected value if Phase 50 was CR-2026-065. Confirm against engine repo docs/phase-crs/ directory listing; advance if taken. Companion to. loomworks-phase-51-scoping-note-v0_2.md (authoritative scope; absorbs Step 0 findings); loomworks-phase-51-cr-drafting-handoff-v0_1.md (drafting instructions for this CR); phase-51-step-0-findings-v0_1.md (engine repo docs/phase-impl-notes/; verified live-codebase state); phase-50-cr-companion-as-authority-and-public-form-v0_1.md (substrate Phase 51 extends); phase-48-cr-credit-completion-and-operator-sign-in-v0_1.md §9 (SMTP plumbing Phase 51 reuses); phase-46-cr-operator-layer-frontend-v0_1.md (new-repo-as-phase precedent for sub-arc 3); current-status-manifest-v0_36.md. Status. Pre-execution CR. Ready for Operator review and approval. Step 0 pre-flight runs against this version.


Contents


1. Executive summary

Phase 51 closes the form-initiated grant flow end-to-end across three sub-arcs:

Sub-arc 1 — Substrate (engine). SMTP path extension to form-initiated grants reuses Phase 48's email_service.send_email API as-is. A new voice template prompts/credit_voice/grant_email.md lands as the fifth sibling in the credit_voice family. A synchronous Companion-driven email composition step inserts inline in the seam.issue_grant(grant_kind='form_initiated', ...) post-grant-write path between row-write commit and send_email fire-and-forget call. Phase 45 dispatcher gains end-to-end integration tests with fixture-replayed LLM responses through the real dispatcher, covering the form-submission → grant-decision → approval → email sequence.

Sub-arc 2 — Operator Layer. Empty per V7 finding. <GrantDecisionApprovalCard>.tsx (138 lines, current Phase 50 substrate) accommodates the post-approval-automatic posture (P51-D2) without changes. The Operator Layer tag for Phase 51 lands on the Phase 50 tag commit — a marker, not a fresh commit. 0 vitest, 0 component changes.

Sub-arc 3 — Marketing Site (new repo). New repo DUNIN7/loomworks-marketing — Astro on Cloudflare Pages, static site hosting the credit-request form. Form posts four fields (email, display_name, use_case, jurisdiction) to the engine's POST /authority/grant-request endpoint. The jurisdiction field is added as an additive optional field on the existing endpoint per V5; the marketing-site domain is added to the engine's loomworks_cors_origins literal allowlist per V6 / P51-D14.

By the close of Phase 51, a stranger fills out a form on the marketing site → the Companion proposes → the Operator approves → the recipient receives a Companion-composed email with a working claim URL.

Tag at completion: phase-51-marketing-site-and-companion-email on all three repos.

Build-time estimate: 3.5–4.5 hours total. Substrate sub-arc 1 = 1.5–2 hours; Operator Layer sub-arc 2 = 0 hours; Marketing site sub-arc 3 = 2–2.5 hours.


2. In-scope and out-of-scope

2.1 In-scope

Per scoping note v0.2 §1 + §5, fully transposed:

2.2 Out-of-scope (per scoping v0.2 §8 / handoff §6)


3. Prerequisites

3.1 Baseline at Phase 51 Step 0

If the baseline diverges (test count off by more than ±2 routine noise, Alembic head different, working tree dirty, tag missing), CC stops at the start of Step 0 and reports before running any pre-flight items.

3.2 CR-drafting-time verifications already completed

The scoping note v0.2 absorbed eight verifications run by CC against the live codebase at Step 0 (recorded in phase-51-step-0-findings-v0_1.md). All resolved as HOLDS or PARTIALLY-HOLDS-with-scope-optionality. Consolidated state per Step 0 brief §5: State 1 — All HOLDS or naming-only divergences.

| # | Verification | Verdict | Where absorbed | |---|--------------|---------|---------------| | V1 | Phase 48 SMTP wiring shape | HOLDS | scoping v0.2 §3 V1; §5.1.1 (reuse as-is) | | V2 | Seam post-write hook surface | HOLDS (inline-composition design) | scoping v0.2 §3 V2; §5.1.3 | | V3 | Voice template loader for grant_email.md | HOLDS (naming-only divergence — two parallel template systems) | scoping v0.2 §3 V3; §5.1.2 | | V4 | Phase 45 dispatcher testability with fixture-replayed LLM | HOLDS | scoping v0.2 §3 V4; §5.1.4 | | V5 | Endpoint body shape and jurisdiction-field tolerance | HOLDS for option (a) | scoping v0.2 §3 V5; §5.3.4; settles P51-D7 | | V6 | CORS env-var posture | PARTIALLY HOLDS — comma-separated literals only | scoping v0.2 §3 V6; settles P51-D14 | | V7 | <GrantDecisionApprovalCard> Approve response shape | HOLDS, sub-arc 2 empty | scoping v0.2 §3 V7; §5.2 (empty) | | V8 | Impl notes §7 items 12 and 13 disposition | HOLDS as Phase 51 absorbed (item 6 absorbs; 12/13 correctly out-of-scope) | scoping v0.2 §3 V8; §8 carry-forward unchanged |

3.3 Pre-flight Step 0 (CR-time)

Before Step 1 begins, CC archives this CR at docs/phase-crs/phase-51-cr-marketing-site-and-companion-email-v0_1.md in the engine repo and runs the following pre-flight reads. These are scoped to confirm specific live strings, signatures, and patterns that the CR specifies but did not read directly during drafting. The eight Step 0 verifications absorbed in v0.2 are not re-run; these are CR-time deltas only.

  1. CR registry. Confirm CR-2026-066 is the next available CR number; advance if taken.
  1. Phase 48 email_service.send_email exact import path and signature. Read src/loomworks/email/service.py (or wherever the live module lives per V1 evidence). Confirm import statement form (from loomworks.email.service import send_email) and complete signature including parameter names, types, defaults, and return type.
  1. seam.issue_grant exact signature and grant_kind='form_initiated' branch shape. Read the seam module (likely src/loomworks/credit/seam.py or comparable per V2 evidence). Confirm complete signature including all parameters and the existing branch-handling code for grant_kind='form_initiated' and grant_kind='operator_curated'. Identify the exact insertion site for §9 inline composition step.
  1. prompts/credit_voice/grant_proposal.md exact path and template loader function signature. Per V3 confirmation; CR drafter could not specify exact loader function name. Read the loader (likely src/loomworks/credit/companion_intelligence/voice_composition.py or comparable) and confirm the load function name, signature, variable-substitution mechanism, and family-member registration pattern (if any). The naming-only divergence flagged by V3 means a parallel template system exists elsewhere; confirm grant_email.md lands in the credit_voice family loader, not the parallel system.
  1. <GrantDecisionApprovalCard>.tsx exact path and Approve handler call shape. Per V7 confirmation; this is verification-of-absence rather than verification-of-modification. Read the file, confirm 138 lines, confirm the Approve handler's call into the seam, confirm the response-handling code path. The CR specifies no changes to this file; verification confirms the existing surface continues to support the post-approval-automatic posture (P51-D2) and that no surprise modification is implied.
  1. POST /authority/grant-request pydantic model exact field set, types, defaults, and extra config. Per V5 confirmation. Read the route handler and pydantic body model. Confirm field set (email, display_name, use_case, audience optional, source optional per Phase 50 CR §10.3), types, position in field order. Confirm extra config posture (V5 confirmed accepts additive optional fields cleanly). Confirm Memory event payload propagation — that downstream append_event for grant_request_received reads from the pydantic model and passes through new optional fields.
  1. loomworks_cors_origins env var exact name and config field declaration site. Per V6 confirmation. Read the settings/config module and the FastAPI app construction site. Confirm exact lowercase config field name. Confirm comma-separated parsing logic. Confirm dev/test posture when env var unset (Phase 50 §10.2 specified existing permissive behavior preserved).
  1. Phase 45 dispatcher register_action_dispatcher exact signature and existing fixture-replay infrastructure. Per V4 confirmation. Read the Phase 45 dispatch module. Confirm complete signatures of register_action_dispatcher, process_approval, process_decline. Confirm the LLM client interface (substitution boundary location). Identify any existing fixture / replay / cassette pattern (vcr.py, responses, custom replay). Confirm the dispatcher's invocation entry point for grant_request_received events.
  1. Phase 50 grant-decision dispatcher's existing test surface. Read the existing test file location and existing mocking posture (which functions are patched, what they return). §10 builds tests stylistically consistent with this surface.
  1. The grant_email.md template variable surface. Read grant_proposal.md's variable surface. Confirm the variable set ({recipient_display_name}, {asset_id}, {amount}, etc.) and the loader's substitution mechanism. §7 specifies the grant_email.md variable set against this evidence.
  1. seam.issue_grant task-lifecycle pattern for Phase 48's POST /admin/grants path. Per V1 / V2 confirmation. Read the existing grant_kind='operator_curated' branch's post-write pattern (fire-and-forget background task scheduling pattern). §9 inline composition + §8 SMTP wiring follow the same pattern.
  1. Engine repo docs/phase-crs/ directory listing. Confirm CR number ordering for CR-2026-066. Already in pre-flight item 1; this is the operational confirmation step.

If any of items 1–12 reveal architectural divergence (not just naming or exact-string), CC halts and surfaces — does not draft and hope. Naming-only divergences absorb in-flight per the standard discipline.

3.4 CR archival

Archive this CR at:


/Users/dunin7/loomworks-engine/docs/phase-crs/phase-51-cr-marketing-site-and-companion-email-v0_1.md

before Step 1 begins. Per Phase 47/48/49/50 standard pattern.


4. Construction decisions this CR closes

Sixteen decisions, settled in scoping note v0.2 §4. CC executes against them; does not re-decide. Two decisions remain [Operator confirms] at CR drafting time (P51-D5 stack and P51-D15 domain); both settle at sub-arc 3 Step 4 kickoff at the latest.

P51-D1. Marketing site repo: separate, named DUNIN7/loomworks-marketing. Phase 46 precedent for new-repo-as-phase. Co-located in loomworks (Operator Layer) rejected — different stack, different deployment target.

P51-D2. Companion-driven email composition: post-approval automatic, NOT a second approval gate. The Operator's approval of the grant decision via <GrantDecisionApprovalCard> consents both to the grant and to the email body. Confirmed empty Operator Layer changes per V7.

P51-D3. SMTP-wired form-initiated grants: replace manual claim-URL copy as default; preserve send_email: bool override per Phase 48 §9.7 admin-grants pattern.

P51-D4. Phase 45 e2e integration testing: fixture-replayed LLM through real dispatcher. Real-LLM verification stays manual / spike, not test suite. V4 confirms substitution boundary is clean.

P51-D5. Marketing site stack: Astro on Cloudflare Pages. [Operator confirms] Hugo alternative if preferred. CR drafts to Astro by default; Operator confirms at sub-arc 3 Step 4 kickoff at the latest.

P51-D6. Marketing site content authoring: Operator-driven, with Companion advice. Voice tuning iterations parallel to Phase 50's grant_proposal.md cycle. Operator iteration after the Phase 51 tag is ongoing work, not a Phase 51 amendment.

P51-D7. Jurisdiction field: additive optional jurisdiction: str | None = None on POST /authority/grant-request request body. Settles V5 option (a) over option (b). The form presents three options (US / EU / Other); engine records the value as Memory event payload context.

P51-D8. Captcha: deferred. Conditional on abuse signals.

P51-D9. Threshold-driven Companion autonomy: deferred. Gated on Operator content authoring being mature.

P51-D10. Modify-the-proposal action on <GrantDecisionApprovalCard>: deferred. Opportunistic.

P51-D11. Build-time estimate: 3.5–4.5 hours total. Substrate sub-arc 1 = 1.5–2 hours; Operator Layer sub-arc 2 = 0 hours per V7; Marketing site sub-arc 3 = 2–2.5 hours.

P51-D12. Voice content for grant_email.md recorded in Phase 51 implementation notes. Voice content is novel work; impl notes capture as-shipped baseline; Operator iteration after the tag is ongoing work, not a Phase 51 amendment.

P51-D13. Reserved buffer slots: 3 slots, "unconsumed if no amendment arises." Mirrors Phase 50 reserved-slot pattern (which Phase 50 left fully unconsumed).

P51-D14. Marketing-site CORS allowlist posture: production-domain-only literal allowlist for alpha. Wildcard / regex pattern support deferred to Phase 52+. Cloudflare Pages preview URLs not allowlisted via wildcard; preview deployments tested via direct API calls during development.

P51-D15. Marketing site domain: [Operator confirms]. Three viable forms — production loomworks.com, staging subdomain, Cloudflare Pages preview URL. Settles at CR drafting time or sub-arc 3 Step 4 kickoff at the latest.

P51-D16. Tag name: phase-51-marketing-site-and-companion-email on engine, Operator Layer, and marketing repos. Operator Layer tag lands on the Phase 50 tag commit (a marker, not a fresh commit) since sub-arc 2 is empty per V7.


5. Migration

None expected. V1, V2, V3, V5, V6, V7 all confirm reuse of existing surfaces or simple additive changes. No new database tables. No schema modifications. No Alembic migration. Alembic head remains 0064 at Phase 51 close.

If CR drafter / CC discovers a need during implementation (e.g., the additive jurisdiction field per V5 turns out to require a Memory-event-payload schema update tracked through Phase 38 declare-and-register), halt and surface per §17 halt conditions. The expected case is no migration.


6. Backend modules and changes (sub-arc 1)

Sub-arc 1 touches the engine repo at the following surfaces. Section references below specify each in detail.

| File / module | Change | Section | |---------------|--------|---------| | src/loomworks/email/service.py (or per V1 evidence) | No changes — reused as-is | §8 | | src/loomworks/credit/seam.py (or per V2 evidence) | Inline composition step inserted in grant_kind='form_initiated' branch | §9 | | prompts/credit_voice/grant_email.md | New file (fifth sibling in family) | §7 | | src/loomworks/credit/companion_intelligence/voice_composition.py (or per V3 evidence) | Possibly minor — confirm whether grant_email.md requires loader registration or loads via existing pattern | §7, §9 | | Phase 45 dispatcher tests (e.g., tests/integration/test_phase_45_dispatch.py or [CC verifies at Step 0]) | New tests added (~8–12) | §10 | | POST /authority/grant-request route handler (per V5 evidence) | Additive optional jurisdiction: str | None = None field | §11 | | Memory event payload propagation for grant_request_received | New jurisdiction field carried through | §11 | | Settings/config module (per V6 evidence) | No code changes — env var value extended at deploy time | §12 |

No frontend Operator Layer changes per V7 (§13).


7. Backend voice template — prompts/credit_voice/grant_email.md

7.1 Path

prompts/credit_voice/grant_email.md — sibling to Phase 50's grant_proposal.md and Phase 49's three credit_voice templates. Fifth template in the AI-invisibility carve-out family.

7.2 AI-invisibility carve-out comment

Top of file. Mirrors the comment shape Phase 49 / Phase 50 templates use. [CC verifies at Step 0 pre-flight item 4] exact shape — the family convention is consistent across the four existing templates.

Approximate form (CC substitutes exact shape from existing templates):


<!-- AI-invisibility carve-out: this template is rendered as Companion-voice
prose to a recipient. Do not name the LLM, do not refer to "AI" generation,
do not surface model identifiers. The Companion is the speaker. -->

7.3 Template variables

Per V3 / pre-flight item 10, CC reads grant_proposal.md's variable surface and confirms the variable set for grant_email.md. Expected variable set:

Exact variable names: [CC verifies at Step 0 pre-flight item 10] against grant_proposal.md family conventions.

7.4 Voice content (first-draft baseline)

Per P51-D12, voice content is novel work; first-draft baseline records as-shipped at Step 5 in Phase 51 implementation notes; Operator iteration after the tag is ongoing work.

First-draft sketch (CC adapts at Step 5 for as-shipped voice):


Hi {recipient_display_name},

Your request came through and I've set up your access. Here's what's ready:

- {amount} {asset_id} credits.
- Claim them here: {claim_url}
- This link is good through {expires_at}.

The use case you described — {use_case_summary} — was helpful for sizing this. Let me know how the work goes; I'm curious what you build.

— The Companion

The voice register: warm, brief, first-person Companion voice, no AI disclosure, no model-name surfaces. Conveys the recipient's request was reviewed and the credit is theirs to claim. Operator iterates the as-shipped baseline post-tag.

7.5 Loader integration

Per V3 confirmation, grant_email.md loads via the same loader as grant_proposal.md. [CC verifies at Step 0 pre-flight item 4] whether registration is automatic (file-system based) or explicit (registered list). If explicit, register grant_email.md in the same place grant_proposal.md is registered. Naming-only divergence flagged by V3 (parallel template system elsewhere) doesn't affect this template — grant_email.md lands in the credit_voice family loader.


8. SMTP wiring to form-initiated grants

8.1 What

Reuse Phase 48's email_service.send_email API. No changes to the email module itself per V1.

8.2 Phase 48 reuse posture

Per Phase 48 CR §9, the email service exposes:


from loomworks.email.service import send_email  # [CC verifies path at Step 0]

send_email(
    to: str,
    template_name: str,
    context: dict,
    send_email: bool = True,  # override; default True
) -> None  # [CC verifies signature at Step 0 pre-flight item 2]

The send_email: bool = True parameter is the override per P51-D3; Operator can set to False for testing or out-of-band delivery via API control or the Approve action's request body.

8.3 Call site

Phase 51 wires send_email into the seam's grant_kind='form_initiated' branch post-grant-write — see §9 for the inline composition + send sequence. The template_name argument is 'grant_email' (or 'credit_voice/grant_email.md', depending on the loader convention per V3 / pre-flight item 4).

8.4 Fire-and-forget posture

Phase 48 designed send_email as fire-and-forget per V1's task-lifecycle pattern confirmation. Phase 51's seam call inherits this — the seam schedules the send as a background task; the grant write commits regardless of send outcome; send failure surfaces via existing logging and the existing email-failure observability surface. CR drafter does not add new monitoring infrastructure.

8.5 send_email override at Operator approval

The <GrantDecisionApprovalCard> Approve action's request body MAY (per P51-D3) carry a send_email: bool parameter. If false, the seam writes the grant but does not call email_service.send_email post-write. The claim URL is still returned in the approval response so the Operator can deliver out-of-band.

[CC verifies at Step 0 pre-flight item 5] whether the Approve handler currently accepts request body parameters; if not, the override surfaces as a query parameter or the Operator omits it (default = True, send the email). This is a small surface decision; CR specifies the default (True) and treats the override path as an additive enhancement that may or may not require frontend touch — V7 confirmed the Operator Layer surface accommodates the post-approval-automatic posture, so the override is exercisable via direct API call regardless.


9. Companion-driven inline email composition step in seam

This is the substantive substrate section. Per scoping v0.2 §5.1.3 and V2 inline-composition design.

9.1 Architectural shape

The composition step is synchronous inline code in the seam.issue_grant(grant_kind='form_initiated', ...) post-grant-write phase. It is NOT a Phase 45 dispatch. Distinguishing characteristics:

This is architecturally distinct from Phase 50's grant-decision dispatcher (which produces a held assertion). The methodology pattern is Operator-approved-decision + Companion-rendered-prose-output → external delivery, distinct from Phase 50's Companion-proposed-decision → Operator-approved → external action. The two patterns share proposer/committer DNA but apply at different surfaces. Methodology consolidation note (P51-D12 / scoping v0.2 §13) — surface this as the third sub-shape of the proposer/committer pattern at delivery-class scale: closed-loop (Phase 49), delivery-class decision (Phase 50), delivery component within resident engagement (Phase 51).

9.2 Insertion site in seam

Per V2 / pre-flight item 3, CC reads the existing grant_kind='form_initiated' branch in seam.issue_grant. The Phase 50 substrate has this branch handling row-write and (per Phase 50 alpha posture) returning the claim URL for Operator manual delivery. Phase 51 inserts inline composition + send between row-write commit and end-of-function.

Approximate insertion shape (CC adapts at Step 2 per live evidence):


# seam.issue_grant body (form_initiated branch)
async def issue_grant(grant_kind: GrantKind, ...) -> Grant:
    # ... Phase 50 substrate: branch resolution, validation, row write ...

    grant = await _write_grant_row(...)  # commits

    if grant_kind == 'form_initiated':
        # NEW IN PHASE 51: inline composition + send (P51-D2 / P51-D3)
        if send_email_override:  # default True
            try:
                email_body = await _compose_grant_email(  # §9.3
                    grant=grant,
                    proposal=proposal,
                    form_payload=form_payload,
                )
                await send_email(  # §8.2 (Phase 48 reuse)
                    to=grant.recipient_email,
                    template_name='grant_email_rendered',
                    context={'rendered_body': email_body},
                )
            except Exception as e:
                logger.error("grant email composition or send failed", ...)
                # Do not raise — grant write is already committed; send is
                # fire-and-forget at the task-lifecycle layer per Phase 48.

    return grant

Exact shape: [CC verifies at Step 0 pre-flight item 3]. Two design points worth surfacing:

  1. Error-recovery semantics if composition fails. The grant row is already committed before composition runs. If composition fails (LLM unreachable, template malformed, etc.), the grant is valid; the email is not sent; the failure logs. The Operator can retrieve the claim URL from the approval response (which surfaces the URL regardless per Phase 50 §13) and deliver out-of-band. This is the alpha posture — robust over polished. Phase 52+ may layer retry / re-compose-and-send / surface-failure-to-Operator behaviors.
  1. Template-via-context vs template-via-name. Two viable shapes for invoking send_email:

The CR-time recommendation is template-via-context because the Companion-driven composition produces dynamic prose that the static template substitution mechanism can't replicate. The Phase 48 templates (claim_email.txt) are static templates with variable substitution; grant_email.md is composed by the Companion against form context, which is qualitatively different. [CC verifies at Step 0 pre-flight item 4] whether the existing loader supports a passthrough-rendered-body shape. If it doesn't, Step 2 either adds a passthrough template or extends the loader — small additive change.

Halt threshold: if Step 2 surfaces that the loader / template system requires substantial restructuring to accommodate Companion-rendered-body shape, halt and surface per §17.

9.3 _compose_grant_email implementation

New private function in the seam module (or in a separate composition helper module — [CC verifies at Step 0] for module organization conventions):


async def _compose_grant_email(
    grant: Grant,
    proposal: GrantDecisionProposal,
    form_payload: dict,
) -> str:
    """
    Compose email body for a form-initiated grant via the Companion-voice
    pure-composition pattern. Loads grant_email.md template, composes against
    grant + proposal + form context. Returns rendered body text.

    NOT a Phase 45 dispatch. No held artifact. No approval gate.
    """
    template = load_credit_voice_template('grant_email')  # [CC verifies loader name at Step 0]
    context = {
        'recipient_display_name': form_payload.get('display_name'),
        'asset_id': grant.asset_id,
        'amount': grant.amount,
        'claim_url': grant.claim_url,
        'expires_at': grant.expires_at,
        'use_case_summary': proposal.use_case_summary or form_payload.get('use_case'),
        # [CC verifies exact variable names at Step 0 pre-flight item 10]
    }
    body = await companion_compose(template, context)  # [CC verifies LLM-call interface at Step 0 pre-flight item 8]
    return body

Exact shape: [CC verifies at Step 0] for module organization, function naming conventions, async-await pattern, error-handling pattern (raise vs return-None vs log-and-return-fallback), and the LLM-call interface (companion_compose is a placeholder; the actual interface is the same one Phase 49 / Phase 50 use for voice composition).

9.4 Memory event for the email send (DEFERRED)

A future Phase 52+ concern: should the seam emit a Memory event for the email send (e.g., grant_email_sent or grant_email_failed) so the Companion's CME Memory carries a record of the outbound communication? Phase 51 alpha does not — the existing email service's logging and the grant row's existence are sufficient for alpha. If Operator iteration produces a need to query "did the recipient get an email about grant X" through the Companion, Phase 52+ adds the Memory event. For Phase 51, the email send is observable via standard logging only.


10. Phase 45 end-to-end integration tests

10.1 What

End-to-end integration tests for the Phase 45 dispatcher path covering form-submission → Companion grant-decision dispatcher → held assertion → Operator approval → seam → grant write → Companion inline email composition → email send. Per P51-D4 and V4. Closes the substrate-tests-with-mocked-LLM gap surfaced in phase-50-implementation-notes-v0_2.md §7.

10.2 Test posture

Fixture-replayed LLM responses through the real register_action_dispatcher and delegation_required=True path. No real LLM calls in tests. No mocked dispatcher with real LLM. The substitution is at the LLM client interface level — fixtures provide the LLM responses; everything else (dispatcher, voice composition, seam, email service) runs as real code.

[CC verifies at Step 0 pre-flight item 8] the LLM substitution boundary location and any existing fixture / replay / cassette infrastructure. Recommended: build on existing infrastructure if present (vcr.py, responses, custom replay); add minimal scaffolding if not. The LLM client interface should be a single substitution point per V4 — confirm at pre-flight.

10.3 Test list (8–12 tests)

Concrete tests, one per scenario:

  1. Happy path: form-submission → grant issuance + email send. Fixture LLM proposes "approve, $X of loomworks_credit_sonnet"; Operator approves; seam writes grant; composition produces non-empty email body; email send fires (mocked at the SMTP layer). Asserts grant row exists, email body contains recipient display name and asset_id.
  1. LLM proposes decline. Fixture LLM proposes "decline, reason=..."; Operator declines; no grant written; no email sent. Asserts proposal state advances to declined.
  1. LLM proposes approve, Operator overrides to decline. Fixture LLM proposes approve; Operator declines via the surface; no grant written. Asserts override path works.
  1. send_email override = false. Approval includes send_email=false; grant writes; composition does not run; email does not send. Asserts grant exists and email-send was not called.
  1. Email body contains use_case_summary. Fixture LLM proposes approve with non-empty use_case_summary; composition produces body containing the summary. Asserts substring presence.
  1. Email body contains working claim URL. Fixture LLM proposes approve; composition produces body; body contains the grant's claim_url value. Asserts URL substring presence.
  1. Composition failure does not roll back grant. Fixture LLM proposes approve; composition step is mocked to raise; grant row exists; email send is not called; failure logs. Asserts robustness of P51-D2 separation between grant-decision approval and email rendering.
  1. Send failure does not roll back grant. Fixture LLM proposes approve; composition succeeds; SMTP send mocked to raise; grant row exists; failure logs. Asserts fire-and-forget posture per Phase 48.
  1. (Optional) jurisdiction field propagates from form to Memory event to proposal context. Form submission carries jurisdiction='EU'; Memory event payload includes jurisdiction; LLM proposal context includes jurisdiction. Asserts P51-D7 / V5 propagation.
  1. (Optional) Two consecutive form submissions produce two distinct grants. Idempotency / concurrency boundary check — the form-initiated path doesn't accidentally collapse two requests into one.
  1. (Optional) Form submission with missing optional audience field. Phase 50 endpoint carries audience: str | None = None; submission omits it; grant flow completes. Asserts existing optional-field handling unchanged.
  1. (Optional) Form submission with invalid jurisdiction value (e.g., 'XX'). Per P51-D7, jurisdiction is freeform string with three expected values (US/EU/Other). Validation either accepts (freeform) or rejects (enum). [CC verifies at Step 0 pre-flight item 6] the validation posture; test asserts behavior matches.

Tests 9–12 are optional padding within the 8–12 estimate; CC selects the final set during Step 3 implementation. Tests 1–8 are required.

10.4 Test file location

[CC verifies at Step 0 pre-flight item 9] exact location. Likely candidates:

Recommendation: new file for Phase 51's e2e tests, consistent with the file-per-scenario / file-per-phase test organization convention. CC confirms at Step 0 against existing test layout.


11. Endpoint additive jurisdiction field

11.1 What

Add an optional jurisdiction: str | None = None field to the POST /authority/grant-request request body. Per P51-D7 / V5.

11.2 Pydantic model addition

Per V5 confirmation, the existing pydantic model accepts additive optional fields cleanly. CR-time guidance:


class GrantRequestBody(BaseModel):  # [CC verifies model class name at Step 0 pre-flight item 6]
    email: EmailStr
    display_name: str = Field(..., min_length=1, max_length=128)
    use_case: str = Field(..., min_length=1, max_length=2000)
    audience: str | None = None
    source: str | None = None
    jurisdiction: str | None = None  # NEW IN PHASE 51 — P51-D7 / V5
    # [CC verifies field set, types, defaults, and exact position at Step 0]

The new field is the last in the field order (additive position; no reordering of existing fields).

11.3 Validation posture

Per P51-D7, jurisdiction is freeform string with three expected values (US / EU / Other) for Phase 51 alpha. No enum validation. The marketing-site form constrains to three options at the form layer; the engine accepts whatever the form posts. If a submission carries an unexpected value (e.g., 'XX' from a manual API call), it lands in Memory unchanged.

[CC verifies at Step 0 pre-flight item 6] whether existing audience and source fields use enum or freeform validation; jurisdiction follows the same pattern.

11.4 Memory event payload propagation

The grant_request_received Memory event payload (per Phase 50 substrate) carries the form fields. Phase 51 adds jurisdiction to the payload propagation. Per V5, the existing payload-construction code reads from the pydantic model; adding the field to the model propagates automatically through any .dict() or .model_dump() call without explicit propagation code change.

[CC verifies at Step 0 pre-flight item 6] the exact payload-construction call site to confirm the propagation is transparent.


12. CORS allowlist update for marketing-site domain

12.1 What

Add the marketing-site production domain to the loomworks_cors_origins env var literal allowlist. Per P51-D14 / V6.

12.2 No code changes

Per V6 confirmation, the env var supports comma-separated literal origins. Adding a domain is a deploy-time configuration change, not a code change.


# Existing posture (Phase 50)
export LOOMWORKS_CORS_ORIGINS="https://app.loomworks.com,https://staging.loomworks.com"

# Phase 51 addition (sub-arc 3 Step 5)
export LOOMWORKS_CORS_ORIGINS="https://app.loomworks.com,https://staging.loomworks.com,https://loomworks.com"
# [adjust for actual production / staging / preview domain per P51-D15]

12.3 Operator action

The Operator updates the env var on the running engine instance(s) at sub-arc 3 Step 5 — the same step that wires the form to post to the engine. The exact domain to add depends on P51-D15 settlement:

12.4 Wildcard/regex deferred

Per P51-D14, wildcard / regex pattern support in the allowlist mechanism is Phase 52+ work. If during sub-arc 3 testing it becomes load-bearing to allowlist Cloudflare Pages preview URLs (rather than just the production domain), halt per §17 halt conditions and surface for Phase 51 amendment scoping vs Phase 52 deferral decision.


13. Frontend changes (Operator Layer)

Empty per V7.

<GrantDecisionApprovalCard>.tsx (138 lines per V7 evidence — corrected from scoping v0.1's 139 reference) accommodates the post-approval-automatic posture (P51-D2) without changes. Email-send status surfaces through the existing notification stream rather than the Approve response shape. 0 vitest, 0 component changes.

The Operator Layer tag for Phase 51 (phase-51-marketing-site-and-companion-email) lands on the Phase 50 tag commit e4c09e0 — a marker, not a fresh commit. CC tags the existing commit at Step 5 / Checkpoint B.

This section exists for symmetry with Phase 50's §13 frontend section and to mark the absence explicitly; it is not a placeholder for unbuilt work.


14. Marketing site sub-arc — new repo, Astro, Cloudflare Pages

This section mirrors Phase 46's §6 + §7 + §14 (new-repo scaffolding for Operator Layer) adapted for static-site / Astro / Cloudflare Pages. Phase 46 used Next.js + Vercel-comparable; Phase 51 uses Astro + Cloudflare Pages. The structural pattern transfers; the specific tools differ.

14.1 Create the repo

Per P51-D1, new repo name DUNIN7/loomworks-marketing, public visibility recommended ([Operator confirms] if private preferred — marketing sites are typically public; the repo's deploy target and the form's POST behavior are independent of repo visibility).


mkdir /Users/dunin7/loomworks-marketing
cd /Users/dunin7/loomworks-marketing
npm create astro@latest .  # [CC adapts: --template, --typescript, --git flags per current Astro CLI]

[CC verifies: the npm create astro@latest version and flags. If the installed version differs, adjust flags accordingly. The goal is: Astro project, TypeScript, Tailwind CSS integration recommended for design-token reuse with Operator Layer.]

14.2 Tooling setup

Per P51-D5 Astro recommendation ([Operator confirms] Hugo alternative). Astro provides:

Install adapter and Tailwind:


npm install @astrojs/cloudflare @astrojs/tailwind

Update astro.config.mjs:


import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
import tailwind from '@astrojs/tailwind';

export default defineConfig({
  output: 'static',  // SSG
  adapter: cloudflare(),
  integrations: [tailwind()],
});

[CC verifies at Step 4] against current Astro / adapter conventions.

14.3 Package.json scripts


{
  "scripts": {
    "dev": "astro dev",
    "start": "astro dev",
    "build": "astro build",
    "preview": "astro preview",
    "astro": "astro"
  }
}

14.4 README


# Loomworks — Marketing Site

The public-facing marketing site for Loomworks. Hosts the credit-request form
that posts to the loomworks-engine `POST /authority/grant-request` endpoint.
Static site generated by Astro, deployed to Cloudflare Pages.

## Development

\`\`\`
npm install
npm run dev          # localhost:4321 (Astro default)
\`\`\`

The credit-request form posts to the engine. For local development, point
the form at a local engine instance; for production, the form posts to
the production engine API.

## Deployment

Cloudflare Pages reads from `main`. Push to `main` triggers a deploy.
Production domain: see deployment configuration.

## Companion repos

- `DUNIN7/loomworks-engine` — substrate (FastAPI/Python)
- `DUNIN7/loomworks` — Operator Layer (Next.js)
- `DUNIN7/loomworks-ui` — Workshop (Next.js, Builder's view)

14.5 Design token import from Operator Layer

Per scoping v0.2 §5.3.2, brand colors, typography, layout primitives reused via design-token export. No shared codebase.

The Operator Layer (DUNIN7/loomworks) maintains the design token surface in src/styles/tokens.css (or comparable [CC verifies at Step 4]). The marketing site imports the same tokens as a CSS file copy or via build-time fetch. Recommendation: copy at scaffolding time, document the source, refresh manually when tokens drift. Avoiding a shared NPM package or Git submodule for alpha keeps complexity low.

14.6 Landing page

src/pages/index.astro — single-page or multi-section landing content per scoping v0.2 §5.3.3. Operator-authored draft (Companion-advised; iterated post-tag per P51-D6).

Content sketch (Operator iterates at Step 4):

The voice register: warm, brief, domain-fluent, no AI disclosure language. Operator iterates.

14.7 Credit-request form

src/pages/request-credits.astro (or as section on landing per Operator preference at Step 4).

Form fields per scoping v0.2 §5.3.4:

Submit button posts JSON to the engine's POST /authority/grant-request endpoint. Production endpoint URL: https://api.loomworks.com/authority/grant-request (or per P51-D15 settlement). Client-side validation only (HTML5 validation + minimal JS for required-field checks); server-side validation handled by the Phase 50 endpoint per pydantic model.

Approximate form shape (CC adapts for Astro / Tailwind conventions at Step 5):


<form id="credit-request" onsubmit="submitForm(event)">
  <label>
    Email
    <input type="email" name="email" required />
  </label>
  <label>
    Your name
    <input type="text" name="display_name" required minlength="1" maxlength="128" />
  </label>
  <label>
    Use case
    <textarea name="use_case" required minlength="1" maxlength="2000"></textarea>
  </label>
  <label>
    Where are you based?
    <select name="jurisdiction" required>
      <option value="">Select…</option>
      <option value="US">US</option>
      <option value="EU">EU</option>
      <option value="Other">Other</option>
    </select>
  </label>
  <button type="submit">Request credits</button>
</form>

<script>
async function submitForm(e) {
  e.preventDefault();
  const data = Object.fromEntries(new FormData(e.target).entries());
  const res = await fetch('https://api.loomworks.com/authority/grant-request', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify(data),
  });
  if (res.status === 202) {
    document.getElementById('credit-request').replaceWith(/* success message */);
  } else {
    /* surface error */
  }
}
</script>

[CC verifies at Step 5] the production API URL per P51-D15 settlement. Recommend Astro environment variables (PUBLIC_ENGINE_API_URL) for the URL so dev/staging/production swap without code edits.

14.8 Privacy and terms pages

src/pages/privacy.astro and src/pages/terms.astro per scoping v0.2 §5.3.5. Brief content:

Operator authors at Step 4; iterated post-tag per P51-D6.

14.9 Cloudflare Pages deployment

Per P51-D5, deploy target is Cloudflare Pages. Integration:

  1. Connect repo to Cloudflare Pages. From the Cloudflare dashboard or via Wrangler CLI: connect DUNIN7/loomworks-marketing as a Pages project. Build command: npm run build. Build output directory: dist/. Production branch: main.
  1. Environment variables. PUBLIC_ENGINE_API_URL set to production API URL per P51-D15.
  1. Custom domain. Per P51-D15, attach the production domain (e.g., loomworks.com). Cloudflare Pages provides automatic TLS.
  1. Preview deploys. Cloudflare Pages auto-creates preview URLs per Git branch. Per P51-D14 / V6, these preview URLs are NOT in the engine's CORS allowlist; preview-URL form submissions return CORS errors. Preview deploys are useful for visual review of the static content but not for end-to-end form flow exercise.

[Operator confirms at Step 5] whether to set up Cloudflare Pages via dashboard (recommended for first-time setup) or via Wrangler CLI (scriptable, reproducible).

14.10 Smoke tests

Per scoping v0.2 §5.3.7. Marketing site does not get vitest-style automated tests. Smoke tests at sub-arc 3 close:

  1. Form round-trip. Submit the form on the staging deploy; confirm 202 response; confirm grant_request_received Memory event lands on engine staging instance with all four fields including jurisdiction.
  1. Pages serve. Privacy and terms pages return 200; render readable content.
  1. Mobile responsive. Form and landing render at 375px without breakage. Visual check by Operator.
  1. Tokens applied. Brand colors, typography, layout primitives match Operator Layer per design-token import.

These are manual checks at Step 5 close + Checkpoint B, not automated tests in the marketing repo's CI.

14.11 Git and GitHub


cd /Users/dunin7/loomworks-marketing
git init
git add .
git commit -m "Phase 51 step 4: marketing site repo scaffold"
gh repo create DUNIN7/loomworks-marketing --public --source=. --push
# [Operator confirms public vs private]
# [CC verifies: gh CLI is authenticated for the DUNIN7 org]

15. Build steps

Eight slots: 5 active + 3 reserved buffer per P51-D13. Two checkpoints (A and B). Inverted-pyramid ordering: smallest independent slice first; foundations next; dependents on top.

Step 1 — SMTP wiring to form-initiated grants (engine)

What. Wire email_service.send_email (Phase 48 reuse, no changes per V1) into the seam's grant_kind='form_initiated' post-grant-write path. This is the smallest independent slice — validates the wiring posture without touching the larger composition surface yet. Initial wiring uses a static placeholder template; Step 2 replaces with grant_email.md Companion composition.

Step 0 inspection. Before implementing, CC reads:

Build. Per §8 (SMTP wiring) and §9.2 inline insertion site sketch. First-pass implementation uses a static template (existing Phase 48 claim_email.txt or comparable) so the wiring is exercised without depending on Step 2's voice composition.

Tests. 5–8 tests per scoping v0.2 §6.2:

Acceptance gate. All Step 1 tests pass; Phase 48 admin-grants path tests pass unchanged; wiring is reachable via the form-initiated flow.

Halt threshold. email_service.send_email signature has shifted (architectural divergence beyond naming); seam's grant_kind='form_initiated' branch has shifted in shape; more than 30 tests touched.

Mode posture. Auto-mode-proceed.

Step 2 — grant_email.md voice template + Companion composition step (engine)

What. Create prompts/credit_voice/grant_email.md template per §7. Implement the inline _compose_grant_email helper per §9.3. Replace Step 1's static-template wiring with the Companion-composition path. Voice content as-shipped baseline records in implementation notes per P51-D12.

Step 0 inspection. Before implementing, CC reads:

Build. Per §7 (voice template) and §9 (composition step + insertion). The composition is async; the seam awaits it. Template-via-context posture per §9.2 design point 2 — [CC verifies at Step 0] whether the loader supports passthrough-rendered-body shape; if not, add minimal extension.

Tests. 8–12 tests per scoping v0.2 §6.2:

Acceptance gate. All Step 2 tests pass; voice template loads; composition + send sequence works end-to-end against fixture-mocked LLM.

Halt threshold. Loader requires substantial restructuring to accommodate Companion-rendered-body shape; LLM-call interface is not the substitution boundary V4 expected; more than 30 tests touched.

Mode posture. Auto-mode-proceed.


Checkpoint A — between Step 2 and Step 3

CC produces implementation notes draft v0.1 at /Users/dunin7/loomworks-engine/docs/phase-impl-notes/phase-51-implementation-notes-v0_1.md. Records build summary for Steps 1 and 2; any in-flight resolutions (naming-only divergences absorbed); the as-shipped first-draft grant_email.md voice content per P51-D12; any methodology findings.

Acceptance gate (Checkpoint A).


Step 3 — Phase 45 end-to-end integration tests (engine)

What. Establish end-to-end integration test coverage for the form-submission → grant-decision → approval → email sequence with fixture-replayed LLM through the real dispatcher. Per P51-D4 / V4 / §10.

Step 0 inspection. Before implementing, CC reads:

Build. Per §10. New test file (likely tests/integration/test_phase_45_grant_decision_e2e.py per pre-flight item 9). Eight required tests + up to four optional. Fixtures provide LLM responses; everything else runs as real code.

Tests. 8–12 tests per scoping v0.2 §6.2 / §10.3.

Acceptance gate. All Step 3 tests pass; dispatcher path runs end-to-end with fixture-replayed LLM; substrate test count baseline + ~21–32 new tests cumulative across Steps 1+2+3.

Halt threshold. LLM substitution boundary requires restructuring beyond Phase 51 scope; existing fixture infrastructure absent and adding scaffolding consumes >2 hours; test count drift > ±3 unexpectedly; more than 30 tests touched by a single change.

Mode posture. Auto-mode-proceed.

Step 4 — Marketing site repo scaffolding + design tokens + landing page (marketing repo)

What. Create DUNIN7/loomworks-marketing repo per §14. Initialize Astro project per P51-D5. Import design tokens from Operator Layer per §14.5. Author landing page draft per §14.6. Author privacy and terms pages per §14.8. Form scaffolded but not yet wired to engine endpoint (deferred to Step 5).

Step 0 inspection. Before implementing, CC reads:

Build. Per §14 sections 14.1 through 14.8 + 14.11 (repo creation, scaffolding, design tokens, landing, privacy, terms, git/GitHub). Form HTML structure exists but submit handler is a placeholder.

Tests. No vitest-style tests on marketing site per §14.10. Manual visual check at Operator review.

Acceptance gate. Repo exists on GitHub; npm run build succeeds; landing/privacy/terms pages render in npm run dev; design tokens applied; brand consistency with Operator Layer visible.

Halt threshold. P51-D5 stack settlement still pending (Astro vs Hugo); design token import surfaces tight coupling to Operator Layer build tooling that scoping didn't size; gh CLI not authenticated for DUNIN7 org.

Mode posture. Auto-mode-proceed if P51-D5 settled; otherwise halt at Step 4 entry and confirm with Operator.

Step 5 — Marketing site form + production wiring + Operator Layer tag (cross-repo)

What. Wire the marketing site form to post to the engine's POST /authority/grant-request endpoint. Add jurisdiction field to the engine's pydantic model per §11. Add marketing-site domain to engine's CORS allowlist per §12. Configure Cloudflare Pages deployment per §14.9. Run smoke tests per §14.10. Tag Operator Layer at the Phase 50 commit per §13.

Step 0 inspection. Before implementing, CC reads:

Build. Per §11 (pydantic model addition + payload propagation), §12 (CORS allowlist update — env var change at deploy time), §14.7 (form submit handler wiring), §14.9 (Cloudflare Pages config), §13 (Operator Layer tag at Phase 50 commit).

Tests. 0 substrate (pydantic model addition is covered by Step 3 e2e tests if test 9 selected; otherwise 1–2 small tests added here for the additive field). Marketing site smoke tests per §14.10 are manual.

Acceptance gate. Form posts to engine endpoint successfully; jurisdiction field lands in Memory event payload; Cloudflare Pages staging deploy serves; CORS allowlist accepts marketing-site domain; Operator Layer tag lands on Phase 50 commit.

Halt threshold. P51-D15 domain still pending and blocking deployment configuration; CORS posture under V6 surfaces wildcard need (substrate-friction-discipline-pattern threshold per §17); production-domain ownership / DNS not configured by Operator.

Mode posture. Auto-mode-proceed if P51-D15 settled; otherwise halt at Step 5 entry and confirm with Operator.

Step 6 — Reserved buffer

(reserved — buffer for amendments arising from steps 1–5)

Unconsumed if no amendment arises. Phase 50 left all three reserved slots unconsumed; Phase 51 expects the same posture given Step 0 absorbed eight verifications cleanly.

Step 7 — Reserved buffer

(reserved — buffer)

Step 8 — Reserved buffer

(reserved — buffer)


Checkpoint B — final, before tagging

CC produces implementation notes v0.2 (or higher if amendments consumed reserved slots) at /Users/dunin7/loomworks-engine/docs/phase-impl-notes/phase-51-implementation-notes-v0_2.md. Records build summary across all Steps; any in-flight resolutions; the as-shipped voice content; methodology findings to record (e.g., third sub-shape of proposer/committer pattern; first three-repo phase; Phase 46 / Phase 51 sub-arc 3 loose-coupling asymmetry observation).

Acceptance gate (Checkpoint B). Per §16.

Tag. phase-51-marketing-site-and-companion-email on:

Annotated tags. Pushed to origin on all three repos.


16. Acceptance gates

Sixteen items. One per active build step (5) + two checkpoint gates (A and B) + nine integration / cross-cutting gates spanning the slices. Mirrors the Phase 50 v0.1 17-item shape, scaled down by one because sub-arc 2 is empty per V7.

| # | Gate | Where verified | |---|------|---------------| | 1 | All Step 1 tests pass; Phase 48 admin-grants path tests unchanged; SMTP wiring reachable via form-initiated flow. | Step 1 | | 2 | All Step 2 tests pass; voice template loads; AI-invisibility carve-out comment present; Companion composition produces non-empty body. | Step 2 | | 3 | Checkpoint A: substrate test count baseline + ~13–20 new tests; Steps 1 + 2 implementation notes draft v0.1 records build summary, voice content, in-flight resolutions. | Checkpoint A | | 4 | All Step 3 tests pass; dispatcher path runs end-to-end with fixture-replayed LLM. | Step 3 | | 5 | All Step 4 outcomes pass: marketing repo exists, builds, renders landing/privacy/terms pages, design tokens applied. | Step 4 | | 6 | All Step 5 outcomes pass: form posts to engine, jurisdiction lands in Memory event, Cloudflare Pages deploy serves, CORS allowlist accepts domain, Operator Layer tag lands on Phase 50 commit. | Step 5 | | 7 | Checkpoint B: substrate +21–32 new tests; Operator Layer +0 per V7; marketing site repo exists with passing build; eslint/tsc/build clean (Operator Layer); working tree clean across three repos. | Checkpoint B | | 8 | Form-to-Memory round trip. Marketing-site form submission lands a grant_request_received Memory event on engine staging instance carrying the jurisdiction field per P51-D7 / V5. End-to-end smoke test. | Cross-cutting | | 9 | Grant-to-email round trip. <GrantDecisionApprovalCard> Approve fires seam.issue_grant(grant_kind='form_initiated'); the seam writes the grant; the Companion composes the email body inline using grant_email.md per V2/V3; the email sends via Phase 48 SMTP per V1; the recipient receives an email with a working claim URL. | Cross-cutting | | 10 | Companion-composed email body assertion. The email body sent contains the recipient's display name and asset_id (string-substring assertion). Verifies the inline composition step ran and didn't fall back to a static placeholder. | Step 2 / Step 3 | | 11 | AI-invisibility carve-out comment present. prompts/credit_voice/grant_email.md has the carve-out comment at top of file (fifth template in the carve-out family). | Step 2 | | 12 | Phase 45 e2e tests pass with fixture-replayed LLM through real dispatcher. Per P51-D4 / V4. | Step 3 | | 13 | No regression on Phase 50 surfaces. All Phase 50 tests pass unchanged; <GrantDecisionApprovalCard> Approve / Decline behavior unchanged in the existing direct-call test surface. | Cross-cutting | | 14 | Marketing site smoke test green. Form round-trips against Cloudflare Pages staging deploy; jurisdiction answer recorded in Memory event payload; privacy/terms pages serve. CORS allowlist includes marketing-site domain per P51-D14 / P51-D15. | Step 5 | | 15 | No regression on Phase 48 SMTP path. POST /admin/grants operator-curated path still sends claim_email.txt (not grant_email.md) — the two paths stay distinct. | Cross-cutting | | 16 | Working tree clean on main for all three repos before tag. eslint/tsc/build clean on Operator Layer (unchanged from Phase 50 baseline per V7). | Checkpoint B |


17. Halt conditions per build step

Per scoping v0.2 §9 and Phase 49/50 substrate-friction-discipline-pattern. Mid-build friction at any of these thresholds halts the build and surfaces for Operator-elective amendment scoping.

General (any step).

Step-specific.

In all cases, halt-and-surface is preferred to draft-and-hope. Reserved buffer slots 6–8 absorb amendment scoping if needed; if no amendment arises, slots stay unconsumed (Phase 50 precedent).


18. Test list

Total substrate: ~21–32 new tests across Steps 1–3. Operator Layer: 0 per V7. Marketing site: smoke-test only.

| Step | Substrate | Frontend (Operator Layer) | Marketing | |------|-----------|---------------------------|-----------| | Step 1 | 5–8 | — | — | | Step 2 | 8–12 | — | — | | Step 3 | 8–12 | — | — | | Step 4 | — | — | static-build (no tests) | | Step 5 | 0–2 | 0 | smoke-test (manual) | | Total | ~21–32 | 0 | smoke-test only |

Build time estimate: 3.5–4.5 hours total per P51-D11.

Compared to Phase 50's ~66–86 substrate + ~10–12 frontend, Phase 51 is smaller-substrate, larger-cross-repo. The work shifts from substrate to surface (new repo creation + static site authoring).


19. Carry-forward to Phase 52

Items intentionally not built in Phase 51, recorded for Phase 52 scoping. Mirrors Phase 50 v0.1 §20's explicit list shape.

19.1 Phase 52 primary build candidates

  1. Modify-the-email-body extension. If Operator iteration after Phase 51 produces a need to inspect / edit the email body before send (vs the post-approval-automatic posture P51-D2 ships), Phase 52 adds a small frontend extension analog to the modify-the-proposal action.
  1. Wildcard / regex pattern support in loomworks_cors_origins. Per V6 PARTIALLY HOLDS finding. Triggered by Cloudflare Pages preview-URL form testing becoming load-bearing. Small substrate addition.
  1. Marketing site jurisdiction routing logic at Authority side. Phase 51 records the jurisdiction value in Memory; Phase 52+ implements the Authority-side routing logic that consumes the value to direct recipients to the right production instance.

19.2 Phase 52 secondary or conditional

  1. Captcha for the public form (P51-D8 deferred). Surfaces if abuse signals warrant.
  1. Threshold-driven Companion autonomy (P51-D9 deferred). Gated on Operator content authoring being mature.
  1. Modify-the-proposal action on <GrantDecisionApprovalCard> (P51-D10 deferred). Opportunistic.
  1. Multi-Companion-instance cross-checking on grant decisions. Speculative future work.
  1. Real-LLM end-to-end spike test cadence formalization. If quarterly-or-on-demand integration verification cadence becomes useful.

19.3 Continuing parallel work

  1. Operator content authoring (continuation). Per P49-D12 / P50-D7 / P51-D6, the Operator authors voice content + landing copy + privacy/terms iterations in parallel with build phases.
  1. Voice tuning iterations. Per P51-D12. Voice quality emerges through Operator iteration on grant_email.md (and grant_proposal.md from Phase 50). Iteration after Phase 51 tag is ongoing Operator work, not a Phase 51 amendment. Phase 52+ may absorb tuning findings into a methodology consolidation pass.

19.4 Substrate-gap-dependent or deferred

  1. Phase 42 turns reconciler coverage (item 10 from Phase 50 carry-forward). Substrate-gap-dependent; future Phase 42 amendment work.
  1. Reactivation in-session chrome (item 11 from Phase 50 carry-forward). Conditional; revisit if cross-tab edge case becomes real.

19.5 Methodology consolidation

  1. Methodology v0.21 consolidation. Continuing parallel work, runs separately. Phase 51 contributes:

These land in implementation notes during construction and feed the next manifest update / methodology consolidation pass.


20. Kickoff prompt for CC

Paste-ready. Mirrors the Phase 50 kickoff shape. Run on DUNIN7-M4 in a fresh Claude Code session against the engine repo at /Users/dunin7/loomworks-engine.


> Read the Change Request document at the path I supply below. This is
> CR-2026-066 v0.1, the Phase 51 Change Request (first version; no prior
> Phase 51 CR exists). You are the executing agent named in the CR.
>
> CR path: ~/Downloads/phase-51-cr-marketing-site-and-companion-email-v0_1.md
> (confirm the latest approved version if more than one is present in
> Downloads).
>
> v0.1 drafts against:
>   - loomworks-phase-51-scoping-note-v0_2.md (authoritative scope; absorbs
>     Step 0 findings)
>   - phase-51-step-0-findings-v0_1.md (engine repo
>     docs/phase-impl-notes/; verified live-codebase state, absorbed into
>     scoping v0.2)
>   - loomworks-phase-51-cr-drafting-handoff-v0_1.md (drafting handoff)
>
> Code baseline:
>   - engine: tag phase-50-companion-as-authority-and-public-form at c01124d
>     (annotated tag object 67ba251). 2,095 tests passed, 26 skipped,
>     Alembic head 0064, working tree clean.
>   - Operator Layer (DUNIN7/loomworks): tag
>     phase-50-companion-as-authority-and-public-form at e4c09e0 (annotated
>     tag object fd8177a). 139 vitest passed, 29 test files, 11 prerendered
>     routes, eslint/tsc/build clean.
>   - Marketing repo (DUNIN7/loomworks-marketing): does not yet exist;
>     Step 4 creates it.
>
> Per CR §3.4: archive this CR to
> docs/phase-crs/phase-51-cr-marketing-site-and-companion-email-v0_1.md
> at Step 0 before Step 1 begins.
>
> Per CR §3.3: run pre-flight Step 0 (12 items). The eight scoping-time
> verifications absorbed into v0.2 (V1–V8) are not re-run; the pre-flight
> items here are CR-time deltas only. Naming-only divergences absorb
> in-flight per the standard discipline; architectural divergences halt
> and surface (see CR §17 halt conditions).
>
> Per CR §15: five active build steps + three reserved buffer slots
> (Steps 6–8 reserved-not-skipped per P51-D13). Two checkpoints — A
> after Step 2, B after Step 5 (final, before tagging). Standard
> auto-mode posture: Steps 1–2 accept auto-mode-proceed; Checkpoint A
> halts until Operator confirms; Steps 3–5 auto; Checkpoint B (final)
> halts for tagging.
>
> Per CR §4: sixteen construction decisions (P51-D1 through P51-D16)
> are settled. CC executes against them; does not re-decide. Two
> decisions remain [Operator confirms]: P51-D5 (Astro vs Hugo stack;
> Astro recommended; CC drafts to Astro) and P51-D15 (marketing site
> domain; settle at Step 4 or Step 5 entry).
>
> P51-D12 in particular: voice quality emerges through Operator
> iteration after wire-up; first-draft voice content shipped at Step 2
> is the as-shipped baseline; iteration after the Phase 51 tag is
> ongoing Operator work, not a Phase 51 amendment trigger.
>
> Per CR §17: halt-and-surface is preferred to draft-and-hope. Halt
> thresholds include: substrate-friction-discipline-pattern;
> Phase 45 dispatcher fixture-replay friction beyond V4 verdict;
> CORS posture under V6 surfaces wildcard need; P51-D5 / P51-D15
> still-pending blocking; >30 tests touched by a single change;
> any divergence from v0.2 scoping decisions.
>
> Implementation notes at Checkpoints A and B:
> docs/phase-impl-notes/phase-51-implementation-notes-v0_1.md (and
> v0_2.md if revised at Checkpoint B). Records build summary for each
> step, in-flight resolutions, methodology findings, and as-shipped
> voice content per P51-D12.
>
> Tag at completion: phase-51-marketing-site-and-companion-email
> (annotated, on engine + Operator Layer + marketing repos). Operator
> Layer tag lands on Phase 50 commit e4c09e0 per §13. Push tags after
> Checkpoint B.

After CC reports build summary at completion, a fresh scoping chat opens for Phase 52 with the carry-forward from §19 as relevant.

If a mid-build amendment surfaces (Finding 6 trajectory — Operator-elective amendment scoping), the discipline is established: build doesn't halt; Operator decides architecturally with options + halt-threshold review; sub-step lands before continuing. Phase 49's phase-49-step-4-amendment-scoping-v0_1.md is the canonical instance; Phase 50 left all three reserved slots unconsumed.


DUNIN7 — Done In Seven LLC — Miami, Florida Phase 51: Marketing Site, Companion-Driven Email, and Phase 45 E2E Tests — CR v0.1 — 2026-05-09