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.
prompts/credit_voice/grant_email.mdPhase 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.
Per scoping note v0.2 §1 + §5, fully transposed:
email_service.send_email.prompts/credit_voice/grant_email.md (fifth sibling in credit_voice family).jurisdiction: str | None = None field on POST /authority/grant-request request body and Memory event payload propagation.loomworks_cors_origins allowlist update (one production-domain literal added).DUNIN7/loomworks-marketing. Astro project. Cloudflare Pages deployment configuration.<GrantDecisionApprovalCard> (P51-D10 deferred, opportunistic).loomworks_cors_origins (Phase 52+ candidate; V6 scope-optional).conversation_turns reconciler coverage (substrate-gap-dependent; future Phase 42 amendment).grant_email.md (P51-D12; iteration after tag is ongoing work).what-dunin7-is-building-v0_21.md (separate work; runs parallel).DUNIN7/loomworks-engine): 2,095 tests passed, 26 skipped, Alembic head 0064, working tree clean on main. Tag phase-50-companion-as-authority-and-public-form at engine c01124d (annotated tag object 67ba251).DUNIN7/loomworks): 139 vitest passed, 29 test files, 11 prerendered routes, eslint/tsc/build clean. Tag phase-50-companion-as-authority-and-public-form at frontend e4c09e0 (annotated tag object fd8177a).DUNIN7/loomworks-ui): unchanged from Phase 48 baseline; not in Phase 51 scope.DUNIN7/loomworks-marketing): does not yet exist. Sub-arc 3 Step 4 creates it.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.
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 |
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.
CR-2026-066 is the next available CR number; advance if taken.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.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.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.<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.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.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).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.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.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.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.
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.
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.
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.
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).
prompts/credit_voice/grant_email.md
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.
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. -->
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:
{recipient_display_name} — from form submission's display_name field, propagated through proposal and grant.{asset_id} — from grant row (e.g., loomworks_credit_sonnet).{amount} — from grant row.{claim_url} — from grant issuance.{expires_at} — from grant row's expiration timestamp.{use_case_summary} — optional; from form submission's use_case field, possibly summarized by the Companion during proposal authoring.
Exact variable names: [CC verifies at Step 0 pre-flight item 10] against grant_proposal.md family conventions.
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.
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.
Reuse Phase 48's email_service.send_email API. No changes to the email module itself per V1.
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.
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).
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.
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.
This is the substantive substrate section. Per scoping v0.2 §5.1.3 and V2 inline-composition design.
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:
<GrantDecisionApprovalCard> consents to both the grant and the email body. The composition runs after that approval; there is no second approval.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).
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:
send_email:send_email(to=..., template_name='grant_email', context={...}) — the email service loads grant_email.md itself and renders against context.context={'rendered_body': ...} to a thin email_body_passthrough.md template that just renders the rendered_body.
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.
_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).
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.
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.
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.
Concrete tests, one per scenario:
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.declined.send_email=false; grant writes; composition does not run; email does not send. Asserts grant exists and email-send was not called.claim_url value. Asserts URL substring presence.jurisdiction='EU'; Memory event payload includes jurisdiction; LLM proposal context includes jurisdiction. Asserts P51-D7 / V5 propagation.audience field. Phase 50 endpoint carries audience: str | None = None; submission omits it; grant flow completes. Asserts existing optional-field handling unchanged.[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.
[CC verifies at Step 0 pre-flight item 9] exact location. Likely candidates:
tests/integration/test_phase_45_grant_decision_e2e.py (new file)tests/integration/test_phase_50_grant_decision.py (or comparable from Phase 50)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.
Add an optional jurisdiction: str | None = None field to the POST /authority/grant-request request body. Per P51-D7 / V5.
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).
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.
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.
Add the marketing-site production domain to the loomworks_cors_origins env var literal allowlist. Per P51-D14 / V6.
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]
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:
loomworks.com (production): add https://loomworks.com.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.
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.
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.
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.]
Per P51-D5 Astro recommendation ([Operator confirms] Hugo alternative). Astro provides:
.astro files).@astrojs/tailwind.@astrojs/cloudflare.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.
{
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
}
}
# 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)
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.
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):
/request-credits page that hosts the form).The voice register: warm, brief, domain-fluent, no AI disclosure language. Operator iterates.
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:
email — required, email format, HTML5 <input type="email">.display_name — required, 1–128 chars.use_case — required, 1–2000 chars, <textarea>.jurisdiction — required, <select> with three options: US, EU, Other.
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.
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.
Per P51-D5, deploy target is Cloudflare Pages. Integration:
DUNIN7/loomworks-marketing as a Pages project. Build command: npm run build. Build output directory: dist/. Production branch: main.PUBLIC_ENGINE_API_URL set to production API URL per P51-D15.loomworks.com). Cloudflare Pages provides automatic TLS.
[Operator confirms at Step 5] whether to set up Cloudflare Pages via dashboard (recommended for first-time setup) or via Wrangler CLI (scriptable, reproducible).
Per scoping v0.2 §5.3.7. Marketing site does not get vitest-style automated tests. Smoke tests at sub-arc 3 close:
grant_request_received Memory event lands on engine staging instance with all four fields including jurisdiction.These are manual checks at Step 5 close + Checkpoint B, not automated tests in the marketing repo's CI.
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]
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.
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:
email_service.send_email exact import path and signature (pre-flight item 2).seam.issue_grant exact signature and grant_kind='form_initiated' branch (pre-flight item 3).POST /admin/grants (pre-flight item 11).
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.
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:
prompts/credit_voice/grant_proposal.md for AI-invisibility carve-out comment shape and template variable conventions (pre-flight items 4, 10).src/loomworks/credit/companion_intelligence/voice_composition.py or equivalent (pre-flight item 4).
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:
grant_email.md, not grant_proposal.md).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.
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).
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.
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:
src/styles/tokens.css or comparable).[CC verifies at Step 4] markers.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.
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:
POST /authority/grant-request pydantic model and current field set (pre-flight item 6).loomworks_cors_origins env var configuration site (pre-flight item 7).e4c09e0).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.
(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.
(reserved — buffer)
(reserved — buffer)
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:
DUNIN7/loomworks-engine at the post-Step-5 engine commit.DUNIN7/loomworks at e4c09e0 (Phase 50 commit; marker tag per §13).DUNIN7/loomworks-marketing at the post-Step-5 marketing commit.Annotated tags. Pushed to origin on all three repos.
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 |
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.
email_service.send_email signature has shifted; seam's grant_kind='form_initiated' branch has shifted in shape.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).
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).
Items intentionally not built in Phase 51, recorded for Phase 52 scoping. Mirrors Phase 50 v0.1 §20's explicit list shape.
loomworks_cors_origins. Per V6 PARTIALLY HOLDS finding. Triggered by Cloudflare Pages preview-URL form testing becoming load-bearing. Small substrate addition.<GrantDecisionApprovalCard> (P51-D10 deferred). Opportunistic.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.These land in implementation notes during construction and feed the next manifest update / methodology consolidation pass.
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