Skip to content

Phase 3 -- Corporate Updates -- Specification

The contract for Phase 3 implementation. Each task lists its scope, file targets, AWS impact, and a STOP point where the implementer pauses for review before proceeding.

This specification is derived from requirements.md and analysis.md. The verification regime is in verification.md. The cross-phase exports Phase 3 produces are catalogued in exports.md. The execution plan is in plan/task-plan.md.

Phase 3 implementation is TypeScript / CDK / infra tooling on the infrastructure repository, plus Markdown / Starlight on the documentation repository. The full inventory of agent-execution skills (which Claude Code skills apply where, in what order) is orchestration metadata for the AI executor, not specification content; it lives in plan/task-plan.md § 1 under “Agent execution notes.” A human implementer or reviewer reading this spec does not need to know which skills apply.

Per the project-level CLAUDE.md, every change is classified as None / Synth-only / Resource-touching. Each task below carries its impact tag. Tasks at Resource-touching require a cdk diff summary surfaced in the PR description and explicit user confirmation before deploy (Phase B).

The deploy targets the production Root account (Admin-Alpha1 profile, platformRoot AWS account); the Corporate Email stack and the Free Kanban Tool stack both deploy here for the foreseeable future per architecture-overview § 6.4.

Wherever an API equivalent exists, the operator path uses the API via corporate-cli.ts (Postmark Account API, gh CLI, 1Password SDK, AWS CDK / aws CLI). The Postmark Console click-through is reserved for steps that have no API equivalent (sandbox-to-live approval is the only such step in Phase 3).

The two new Corporate stacks have pinned CloudFormation stack names: CorporateMailDns and FreeKanbanToolMailDns. These literal strings are passed as the CDK id argument to each stack’s constructor. Once a stack is deployed, the name must not change — changing the id argument forces CloudFormation to delete and recreate the stack, destroying every resource it owns (the arda.ardamails.com zone, the NS-delegation Custom Resource, the IAM role, etc.). An inline source-code comment immediately above each constructor call documents the constraint, mirroring the Phase 2 "RootConfiguration" pattern.

The CDK class names match the CFN stack names: class CorporateMailDns extends cdk.Stack and class FreeKanbanToolMailDns extends cdk.Stack, in src/main/cdk/stacks/corporate/corporate-mail-dns.ts and src/main/cdk/stacks/corporate/free-kanban-tool-mail-dns.ts respectively.

Every task that introduces or modifies executable code (constructs, stacks, app, instance config, CLI, drift driver, helpers, lambdas) ships with parallel tests in the same PR, and the tests must pass before the STOP gate clears. The “tests” criterion includes:

  • Jest (or the repo’s chosen runner) unit tests with adequate coverage for new public surface.
  • Integration / synth-only smoke tests where applicable (CDK Template-matcher + npx cdk synth against fixture context).
  • Injected-dependency tests for code with external boundaries (Postmark client, 1Password SDK, filesystem, dig).
  • The repo’s lint and type-check gates (npm run lint, tsc --noEmit) green.
  • The repo’s CI matrix exercises the new code path.

A task whose scope is “add construct X” without adding X.test.ts (or an equivalent) does not clear its STOP gate. The same rule applies to any change to existing code that broadens or changes a public surface — the change ships with the test that locks the new shape.

Every task that produces durable documentation (planning artifacts, design / decision-log entries, the operator runbook, current-system/ pages, byproducts) is reviewed by a specialized documentation reviewer before the STOP gate clears. The review is in addition to the implementer’s own self-review and the human reviewer’s PR-level review:

  • The reviewer is a technical-writer (or quality-reviewer with a docs-specific brief) launched as a sub-agent.
  • The reviewer’s brief: assess structure, clarity, internal consistency, link integrity, frontmatter conformance, en_US locale (artifact, behavior, analyze), and adherence to the document-writing and path-conventions skills.
  • Findings are returned in a structured report. The implementer addresses each finding before the STOP gate clears (or accepts and records the rationale inline if a finding is rejected).

This rule applies to:

  • Tasks D1, D3, D4 (Phase A contract documentation).
  • Tasks D2, D5, D6 (Phase D system documentation, byproducts, CHANGELOG amendment) — the operator runbook at process/sre/runbooks/postmark-domain-verification.md is included.
  • Any other task whose scope adds a .md / .mdx file under documentation/src/content/docs/.

Per requirements.md § Out of scope of Phase 3:

  • Per-partition mail sub-zones (Phase 4).
  • Per-partition Postmark account-token + encryption-key Secrets Manager entries (Phase 4).
  • Backend ShopAccess/Email module in operations (Phase 5b).
  • Apex SPF / DMARC at ardamails.com (out of project scope).
  • Tightening AllowCreatingNSRecordsRole.allowedParentHostedZoneIds (out of project scope).
  • Migration of PostmarkServer thin-wrapper internals to a Lambda-backed Custom Resource (deferred).
  • Helm-release equivalent of the IaC patterns (deferred).

The tasks divide into four execution phases. The split follows the principle that documentation describing what to build comes first (it is the contract); documentation describing what was built comes last (it benefits from learnings, discoveries, and any deploy-time corrections to the contract).

Numbering scheme. Task IDs are content-typed, not execution-ordered: D* for documentation, I* for infrastructure, O* for operator. Numbers are not sequential within a phase (e.g., Phase D contains tasks D2, D5, D6 — D2 is in Phase D because it is system documentation, D5 because it is byproducts, etc.). The companion plan/task-plan.md prefixes every task with T- (e.g., T-D1, T-I3, T-O0) for clarity in cross-document references; T-D1 and bare D1 denote the same task. Execution order is in § 3.

  • Execution Phase A — Contract documentation (Tasks D1, D3, D4): the planning artifacts, the phases.md patch, the docs-side CHANGELOG. Authored before implementation begins. The implementer reads these to know what to build.
  • Execution Phase B — Infrastructure code (Tasks I1-I10): CDK constructs, stacks, app, instance config, CLI, drift workflow. Authored against the Phase A contract.
  • Execution Phase C — Operator deploy + verification (Tasks O0-O4): pre-deploy state assertion, mailbox prerequisite, Phase A run + Phase B deploy, post-deploy verification, drift-workflow first run.
  • Execution Phase D — System documentation + byproducts (Tasks D2, D5, D6): the current-system/oam/ and current-system/runtime/ pages describing what was actually built; the long-lived operator runbook at process/sre/runbooks/postmark-domain-verification.md; the implementation byproducts (changelog, learnings, suggestions, alternatives, skipped, specification-post under 3-corporate-updates/implementation/). Authored after the deploy so any discoveries (e.g., a Postmark API quirk like the IMPORT-detour discovered in Phase 2) are reflected.

If a Phase D task surfaces a contradiction with the Phase A contract, the contract is amended (per the implementation-task skill’s specification-post.md byproduct convention) — the spec is not retroactively rewritten to hide the divergence.

The infrastructure tasks are the most numerous; they are deployed in dependency order within Phase B.

Task D1: Phase 3 planning artifacts on the documentation side

Section titled “Task D1: Phase 3 planning artifacts on the documentation side”

Goal: publish the Phase 3 planning artifacts so reviewers can read the contract before the IaC tasks land.

Scope:

  1. Author requirements.md, specification.md (this file), verification.md, exports.md, and plan/task-plan.md under roadmap/in-progress/email-integration/3-corporate-updates/. (analysis.md and the operator-domain-verification-checklist stub are already in place from Pass 1.)
  2. Patch phases.md — the Phase 3 section’s deliverables table is extended with a row for the Postmark Sender Signature creation by Phase A (per DQ-R1-009) and a row for the corporate-drift.yml workflow.
  3. Patch decision-log.md — the Round R1-Phase3 section already exists from Pass 1; confirm it is published and linked from the decision table at the top.

File targets:

  • New: 3-corporate-updates/{requirements,specification,verification,exports}.md, 3-corporate-updates/plan/task-plan.md.
  • Edit: phases.md (Phase 3 deliverables table), decision-log.md (already complete).

AWS impact: None.

STOP — review after D1: make pr-checks passes. The five Pass-2 documents are linked from phases.md and from each other.


Task D3: Patch phases.md Phase 3 deliverables table

Section titled “Task D3: Patch phases.md Phase 3 deliverables table”

Goal: keep the project-level phase plan aligned with the Phase 3 contract.

Scope:

  1. In phases.md’s Phase 3 deliverables table, add a row for the Postmark Sender Signature registration (per DQ-R1-009) — previously the deliverables table only mentioned DNS records.
  2. Add a row for the corporate-drift.yml workflow.
  3. Confirm the Phase 3 entry-criteria / recovery subsections still match the spec (they were authored in a prior commit).

File targets:

  • Edit: phases.md (Phase 3 deliverables table).

AWS impact: None.

STOP — review after D3: make pr-checks passes; reviewer confirms the patch reflects the spec’s deliverable list.


Goal: every PR to a protected branch carries a CHANGELOG.md entry; this task folds the Pass-2 contract artifacts into the existing [0.31.0] entry.

Scope:

  1. Extend the existing [0.31.0] entry with bullets for the Pass-2 artifacts (requirements.md, this specification.md, verification.md, exports.md, plan/task-plan.md).
  2. The current-system/ pages bullet (D2) is added later when D2 lands in Phase D.

File targets:

  • Edit: documentation/CHANGELOG.md.

AWS impact: None.

STOP — review after D4: make clq passes.


Task I1: Rename route-53-hosted-zone.tsdns-zone.ts and migrate callers

Section titled “Task I1: Rename route-53-hosted-zone.ts → dns-zone.ts and migrate callers”

Goal: generalize the hosted-zone construct so it serves any registrable domain.

Scope:

  1. git mv the file src/main/cdk/constructs/xgress/route-53-hosted-zone.ts to dns-zone.ts.

  2. Inside, rename the class Route53HostedZone to DnsZone. Drop the arda.cards default for overrideDomainName; the prop now accepts any zone name explicitly.

  3. Update validateProps() to require the zoneName (no default).

  4. Migrate every caller to DnsZone and the new path. Per DQ-R1-011, all callers migrate in this PR. The caller list at the time this spec was written (verified via grep against Arda-cards/infrastructure HEAD on phase-2-infra):

    • src/main/cdk/stacks/infrastructure/ingress-stack.ts — 5 instantiations: lines 194 (ioHostedZone), 203 (appHostedZone), 212 (authHostedZone), 221 (assetsHostedZone), 458 (per-partition / config-driven hosted zone). Plus the import at line 1.
    • src/main/cdk/constructs/xgress/route-53-hosted-zone.ts — the construct itself (renamed in this task).
    • knowledge-base/route53-and-dns.md — one prose mention of the construct name; update if still relevant after the rename.

    Re-run grep -rn "Route53HostedZone\|route-53-hosted-zone" against the actual HEAD at implementation time — if the count differs, reconcile with the caller-list above and update Task I1 in specification-post.md. After migration, the same grep returns zero hits.

  5. Update the construct’s tests (route-53-hosted-zone.test.tsdns-zone.test.ts) to exercise the new shape.

File targets:

  • Move + edit: src/main/cdk/constructs/xgress/route-53-hosted-zone.tsdns-zone.ts; corresponding test.
  • Edit: every caller of Route53HostedZone (expected: a small number; identify exhaustively via grep).

AWS impact: Synth-only — the synthesized template per existing caller is unchanged once the rename is applied (zone names already explicit in the existing callers’ configs; the migration only removes the arda.cards default).

STOP — review after I1: npm run build and npm test pass on the infrastructure repo. cdk synth for every existing app produces identical templates to the pre-rename baseline (verified by template diff against a checkpoint).


Task I2: Add dns-email-records.ts xgress construct

Section titled “Task I2: Add dns-email-records.ts xgress construct”

Goal: introduce a generic construct that publishes a sending sub-domain’s DKIM TXT record and Return-Path CNAME record.

Scope:

  1. Author src/main/cdk/constructs/xgress/dns-email-records.ts. Public shape:

    export interface Configuration {
    hostedZone: r53.IHostedZone;
    subdomain: string; // e.g., "freekanban"
    dkimSelector: string; // from PostmarkSendingDomain.Built
    dkimKey: string; // public DKIM key text
    returnPathTarget: string; // e.g., "pm.mtasv.net"
    ttlSeconds?: number; // default 300
    }
    export interface Built {
    dkimRecord: r53.TxtRecord;
    returnPathRecord: r53.CnameRecord;
    }
  2. Implement the construct: emits one r53.TxtRecord at <selector>._domainkey.<subdomain>.<zoneName> and one r53.CnameRecord at pm-bounces.<subdomain>.<zoneName>returnPathTarget. TTL 300 seconds default.

  3. No environment-variable bridge; no file-artifact channel. Inputs come from props only.

  4. validateProps() requires non-empty selector, key, target.

  5. Author dns-email-records.test.ts — CDK Template assertions for the two records’ Names, Types, and TTLs.

File targets:

  • New: src/main/cdk/constructs/xgress/dns-email-records.ts, dns-email-records.test.ts.

AWS impact: Synth-only.

STOP — review after I2: tests pass. Construct synthesizes against a fixture hosted zone.


Task I3: Add Postmark thin-wrapper constructs

Section titled “Task I3: Add Postmark thin-wrapper constructs”

Goal: introduce the IaC-side analogue of L1 protocol proxies for the Postmark Account API.

Scope:

  1. Create the directory src/main/cdk/platform/constructs/postmark/.
  2. Author server.ts (PostmarkServer thin-wrapper):
    • Configuration: accountToken: string (resolved at runtime from 1Password), serverName: string, colorOptional?: PostmarkServerColor, bounceHookUrl?: string, etc.
    • Built: public values onlyserverId: number plus any other DNS-publishable metadata. The Postmark Server API token is NOT exposed on Built in either the interim or the target Custom-Resource mode. The token is a Phase-A side-effect persisted to 1Password (op://Arda-CorporateOAM/Free-Kanban-Generator-Postmark-Server/credential) and is the only place Phase 3 stores it. Stacks that need the token at runtime resolve it from 1Password through their own auth path (out of scope of CDK).
    • In the interim mechanism, the construct reads serverId (and any other public values) from cdk.context.json — the source of truth populated by Phase A of the Corporate CLI.
  3. Author sending-domain.ts (PostmarkSendingDomain thin-wrapper):
    • Configuration: accountToken: string, domainName: string (e.g., "arda.ardamails.com"), returnPathDomainOptional?: string.
    • Built: dkimSelector: string, dkimKey: string, returnPathTarget: string.
    • Same interim-mechanism pattern: read public values from cdk.context.json.
  4. Both constructs follow the construct-line shape (validateProps(); Configuration and Built types). Neither is a full CDK Construct subclass for synth-time runtime behavior; they emit nothing into the CFN template directly but expose typed Built fields the stacks compose into other constructs (e.g., DnsEmailRecords).
  5. Tests at server.test.ts and sending-domain.test.ts. Mock the Postmark API client at the HTTP boundary; assert idempotency (list-then-create), Built shape, error handling for 4xx vs 5xx (per the rev1 design intent recorded in the cross-cutting design’s Postmark API surface notes).

File targets:

  • New: src/main/cdk/platform/constructs/postmark/{server,sending-domain}.ts and tests.

AWS impact: Synth-only.

STOP — review after I3: tests pass. The constructs synthesize cleanly against a fixture cdk.context.json.


Task I4: Reintroduce FREE_KANBAN_POSTMARK_ITEM; extend ari-configuration.ts; extend drift-check

Section titled “Task I4: Reintroduce FREE_KANBAN_POSTMARK_ITEM; extend ari-configuration.ts; extend drift-check”

Goal: re-add the typed 1Password reference removed by Phase 1’s DQ-R1-007 (with the new vault); add the reserved-words list extension; extend the drift-check tool’s surface.

Scope:

  1. In src/main/cdk/platform/one-password.ts, add:

    export const FREE_KANBAN_POSTMARK_ITEM: OnePasswordItem = {
    vault: "Arda-CorporateOAM",
    title: "Free-Kanban-Generator-Postmark-Server",
    primaryField: "credential",
    reference: "op://Arda-CorporateOAM/Free-Kanban-Generator-Postmark-Server/credential",
    };
  2. In src/main/cdk/platform/ari-configuration.ts, add a constant MAIL_RESERVED_SLUGS_AT_MAIL_ROOT (or similar; name TBD by implementer with rationale in the PR description) holding ["arda"] (and an extension point for future Corporate-instance-group zone names). Wire into any partition-zone slug-validation that should reject these names.

  3. In tools/drift-check.ts, extend the ALL_OP_ITEMS array with FREE_KANBAN_POSTMARK_ITEM. Important: the drift run from external-resources-drift.yml (Phase 1’s workflow) authenticates with OP_SERVICE_ACCOUNT_TOKEN, which is scoped to Arda-SystemsOAM only. Do not assert the new item’s content from the Phase 1 workflow. Instead, the Phase 1 drift run only asserts the three Phase-1-resolvable items; the new item’s existence assertion lives in the Corporate drift workflow (Task I8) which authenticates differently.

  4. Update tools/drift-check.test.ts — the test that asserts the count of items moves from 3 to 4 (item declarations); the test for OP_SERVICE_ACCOUNT_TOKEN-scoped resolution still asserts only the three Phase-1 items.

File targets:

  • Edit: src/main/cdk/platform/one-password.ts, src/main/cdk/platform/ari-configuration.ts, tools/drift-check.ts, tools/drift-check.test.ts.

AWS impact: Synth-only on one-password.ts and ari-configuration.ts. None on the drift-check changes.

STOP — review after I4: tests pass. tools/drift-check.ts --report (dry-run mode) returns four items.


Task I5: Add instances/Corporate/free-kanban-tool.ts

Section titled “Task I5: Add instances/Corporate/free-kanban-tool.ts”

Goal: declare the Free Kanban Tool’s Phase-3 configuration in the rev1 declarative pattern.

Scope:

  1. Create the directory src/main/cdk/instances/Corporate/.

  2. Author free-kanban-tool.ts (the asset-named instance config; not the stack file):

    import { POSTMARK_PROD_ACCOUNT } from "../../platform/postmark-service";
    import { FREE_KANBAN_POSTMARK_ITEM } from "../../platform/one-password";
    export const FREE_KANBAN_CONFIG = {
    postmarkAccount: POSTMARK_PROD_ACCOUNT,
    postmarkItem: FREE_KANBAN_POSTMARK_ITEM,
    sendingSubdomain: "freekanban",
    corporateZoneName: "arda.ardamails.com",
    postmarkPlanColor: PostmarkServerColor.GREEN, // or similar plan attribute
    } as const;
  3. Future Corporate consumers (HubSpot, marketing) add sibling files in this directory.

File targets:

  • New: src/main/cdk/instances/Corporate/free-kanban-tool.ts.

AWS impact: Synth-only.

STOP — review after I5: file imports resolve; npm run build passes.


Task I6: Add Corporate stacks (CorporateMailDns and FreeKanbanToolMailDns)

Section titled “Task I6: Add Corporate stacks (CorporateMailDns and FreeKanbanToolMailDns)”

Goal: stand up the two Corporate stacks per the architecture-overview composition.

Scope:

  1. Create src/main/cdk/stacks/corporate/.
  2. Author corporate-mail-dns.ts (CorporateMailDns stack):
    • Owns the arda.ardamails.com zone via DnsZone.
    • Owns the SPF TXT record at apex (v=spf1 include:spf.mtasv.net ~all).
    • Owns the DMARC TXT record at _dmarc.arda.ardamails.com (initial monitoring policy v=DMARC1; p=quarantine; sp=quarantine; rua=mailto:dmarc-reports@arda.cards; alignment defaults to relaxed — adkim / aspf tags omitted).
    • Instantiates WriteNSRecordsToUpstreamDns with subdomain: "arda", nameServers from the new zone’s hostedZoneNameServers, targetAccountId set to platformRoot’s account ID, createNsRoleArn referencing the Phase 2 export arda-allow-create-ns-record-role.
    • Exports arda-corporate-mail-zone (zone ID).
    • RemovalPolicy.RETAIN on the zone.
    • Inline comment above the constructor call in tools/cdk-corporate.ts: // CFN stack name MUST remain "CorporateMailDns" -- changing it would force CloudFormation to delete and recreate the stack.
  3. Author free-kanban-tool-mail-dns.ts (FreeKanbanToolMailDns stack):
    • Composes PostmarkServer (thin-wrapper, reads from cdk.context.json), PostmarkSendingDomain (thin-wrapper, reads from cdk.context.json), and DnsEmailRecords.
    • The PostmarkSendingDomain.Built.dkimSelector / dkimKey and PostmarkServer.Built.serverId plus returnPathTarget are passed into DnsEmailRecords.
    • The Corporate zone is consumed via the arda-corporate-mail-zone export from the CorporateMailDns stack (or via direct Fn.import_value(...) if the stack is in the same App, which it is).
    • Inline comment above the FreeKanbanToolMailDns constructor call in tools/cdk-corporate.ts: // CFN stack name MUST remain "FreeKanbanToolMailDns" -- changing it would force CloudFormation to delete and recreate the stack.
  4. Tests for each stack — CDK Template assertions for the resources declared (zone, NS-write CR, SPF / DMARC TXT records, Free Kanban DKIM TXT, Return-Path CNAME).

File targets:

  • New: src/main/cdk/stacks/corporate/corporate-mail-dns.ts, free-kanban-tool-mail-dns.ts, and tests.

AWS impact: Synth-only (the resources will be deployed in Task O2; this task only synthesizes).

STOP — review after I6: tests pass. cdk synth via tools/cdk-corporate.ts (added in I7) produces a valid template against a fixture cdk.context.json populated with sentinel values.


Task I7: Add Corporate App class and entry script

Section titled “Task I7: Add Corporate App class and entry script”

Goal: a reusable CorporateApp class plus a dedicated entry script that calls new cdk.App() and instantiates both Corporate stacks.

Scope:

  1. Create src/main/cdk/apps/Corporate/.

  2. Author index.ts — exports CorporateApp class (no new cdk.App() at module load; App class is reusable and side-effect-free per the pattern in apps/Al1x/).

  3. Author src/main/cdk/tools/cdk-corporate.ts — the entry script:

    const app = new cdk.App();
    // CFN stack name MUST remain "CorporateMailDns" -- changing it would
    // force CloudFormation to delete and recreate the stack.
    const corporateMailDns = new CorporateMailDns(app, "CorporateMailDns", { ... });
    // CFN stack name MUST remain "FreeKanbanToolMailDns" -- changing it
    // would force CloudFormation to delete and recreate the stack.
    new FreeKanbanToolMailDns(app, "FreeKanbanToolMailDns", { ... });

    (Note: the CDK id argument and the CFN stack name are the same literal string; using a single name reduces the chance of an inadvertent rename relative to Phase 2’s pattern of distinct values for “logical id” and “CFN name”.)

  4. Both constructor calls carry the inline CFN-stack-name preservation comments shown above (rule from § 1.4).

  5. Wire the new App into CI synth. tools/cdk-runner.js is partition-instance-driven (its srcRoot is src/main/cdk/instances), so it does not natively exercise non-partition apps. Phase 2 introduced tools/ci-root-check.js for the same reason (the Root app is also non-partition). Phase 3 mirrors that pattern:

    • Author tools/ci-corporate-check.js along the same lines as tools/ci-root-check.js. The check runs cdk synth --app 'npx ts-node ... tools/cdk-corporate.ts' against a fixture cdk.context.json and asserts the synthesized template includes the expected stacks and resources.
    • Add a CI workflow step (or extend the existing one) that invokes node tools/ci-corporate-check.js.
    • Out of scope for this task: a generalization of cdk-runner.js to admit non-partition apps — that is a follow-up tracked alongside PDEV-440 (the Phase-2-discovered test-coverage gap).

File targets:

  • New: src/main/cdk/apps/Corporate/index.ts.
  • New: src/main/cdk/tools/cdk-corporate.ts.
  • New: tools/ci-corporate-check.js.
  • Edit: .github/workflows/<existing-CI-workflow>.yml — add the ci-corporate-check step.

AWS impact: Synth-only.

STOP — review after I7: CI matrix run includes the Corporate App; cdk synth via cdk-corporate.ts passes against the fixture context.


Task I8: Add corporate-drift.yml workflow + driver

Section titled “Task I8: Add corporate-drift.yml workflow + driver”

Goal: scheduled drift detection for the Corporate instance group.

Scope:

  1. Author infrastructure/.github/workflows/corporate-drift.yml:
    • schedule: cron: "0 9 1 * *" (monthly).
    • workflow_dispatch: for manual trigger.
    • Permissions: contents: read, issues: write.
    • Steps: checkout, setup Node, run npm ci, run tools/corporate-drift.ts with OP_SERVICE_ACCOUNT_TOKEN available, on failure gh issue create with labels drift,phase-3,corporate.
  2. Author tools/corporate-drift.ts:
    • Enumerate the assets declared in instances/Corporate/.
    • For each asset:
      • Resolve the Postmark account token via the 1Password SDK (op://Arda-SystemsOAM/...); the Corporate item itself is not read here (out of OP_SERVICE_ACCOUNT_TOKEN scope).
      • Call the Postmark Account API to list servers; assert the asset’s server is present.
      • dig the asset’s DNS records; assert presence and shape.
      • For the 1Password item, only assert the declaration exists in platform/one-password.ts (verified by the existing drift-check pattern).
    • Output: structured JSON report.
  3. Tests for corporate-drift.ts follow the drift-check.test.ts pattern — injectable dependencies (PostmarkClient, httpGet, dig).

File targets:

  • New: infrastructure/.github/workflows/corporate-drift.yml.
  • New: infrastructure/tools/corporate-drift.ts and corporate-drift.test.ts.

AWS impact: None (CI YAML; tool only reads from external services).

STOP — review after I8: tests pass. A manual workflow_dispatch smoke run of the workflow succeeds against a populated state (postpone to after Task O2 deploy).


Goal: the two-phase orchestrator the operator invokes.

Scope:

  1. Author infrastructure/tools/corporate-cli.ts with two subcommands:
    • corporate-cli prepare <asset> — Phase A. Steps in order:
      • Resolve the Postmark account token from 1Password (Arda-SystemsOAM for the Postmark account itself).
      • Conflict-check (REQ-CLI-003): list existing Postmark Sender Signatures, list existing servers, list existing 1Password items in Arda-CorporateOAM. If the asset’s name collides with a pre-existing entity that is not the one this invocation is reconciling, exit with a structured failure message.
      • Create-or-reconcile the Sender Signature for the asset’s domain (arda.ardamails.com for Free Kanban). Capture DKIM selector + key + Return-Path target.
      • Invoke verifyDkim and verifyReturnPath against the Sender Signature. (May initially return unverified if the DNS records don’t exist yet — that’s fine; Phase B writes them, then Task O3 re-verifies.)
      • Create-or-reconcile the Postmark server for the asset. Capture the Server API token in a process-local secret-handling buffer.
      • Read DeliveryType from GET /servers/{id} on the just-created (or reconciled) server. Emit a structured log event: info for Live, warn for Sandbox or unknown. This is the signal for REQ-OPS-002: Postmark has no account-level sandbox endpoint (GET / redirects; GET /account returns 404), so DeliveryType on the individual server is the correct and only API-surface indicator of whether mail will be delivered. The operator reads this log output and acts on it before proceeding to Phase B if approval is needed.
      • Write the 1Password item Free-Kanban-Generator-Postmark-Server (in Arda-CorporateOAM) with the server-token as the credential field. Retries with exponential backoff. On permanent failure, exit with redacted summary; do not write cdk.context.json.
      • Write public values to cdk.context.json: postmark.<asset>.serverId, postmark.<asset>.dkimSelector, postmark.<asset>.dkimKey, postmark.<asset>.returnPathTarget.
    • corporate-cli deploy <asset> — Phase B. Steps:
      • Run cdk diff apps/Corporate/. Display the diff.
      • On confirmation (interactive prompt or --yes flag), run cdk deploy apps/Corporate/.
  2. Source comments at the top of each subcommand explicitly name the phase and the migration trigger (when Lambda-backed Custom Resources become a wider repo pattern, prepare is retired).
  3. Structured logging (per REQ-CLI-007): JSON-line per event; redaction of token-shaped fields.
  4. Tests at corporate-cli.test.ts — inject mocks for the Postmark client, the 1Password SDK, the filesystem; cover idempotency, conflict-check failure path, in-memory token-buffer with retries, redaction.

File targets:

  • New: infrastructure/tools/corporate-cli.ts and corporate-cli.test.ts.

AWS impact: None (the CLI runs externally and only invokes APIs / writes files; the cdk deploy invocation is what carries the Resource-touching impact, attributed to Task O2).

STOP — review after I9: tests pass. Dry-run of corporate-cli prepare free-kanban against a NonProd fixture succeeds.


Goal: every PR to a protected branch carries a CHANGELOG.md entry; this task adds the infrastructure-side entry alongside the I1-I9 work.

Scope:

  1. Add an entry to infrastructure/CHANGELOG.md under [2.30.0] (or the next available minor) with:
    • ### Added: the Postmark thin-wrappers, dns-email-records.ts, the Corporate stacks, the apps/Corporate/ app, the instances/Corporate/free-kanban-tool.ts config, the corporate-cli.ts tool, the corporate-drift.yml workflow + driver, the reintroduced FREE_KANBAN_POSTMARK_ITEM typed reference, the reserved-words extension.
    • ### Changed: the rename route-53-hosted-zone.tsdns-zone.ts with caller migration.

File targets:

  • Edit: infrastructure/CHANGELOG.md.

AWS impact: None.

STOP — review after I10: clq validation passes via the infrastructure repo’s CI gate.


Goal: confirm the live environment is in the state the spec assumes before any Phase C action that touches it. Catches drift, partial-deploys, and cross-phase merge-state surprises up front, not after a side-effect has been issued.

Scope:

  1. Phase 1 / Phase 2 / Phase 3 merge state (default: strict; overridable — see below):

    • gh pr view 67 -R Arda-cards/documentation --json state reports MERGED (or auto-merge armed and approval landed; see override).
    • gh pr view 69 -R Arda-cards/documentation --json state reports MERGED.
    • gh pr view 70 -R Arda-cards/documentation --json state reports MERGED.
    • gh pr view 446 -R Arda-cards/infrastructure --json state reports MERGED.
    • gh pr view 448 -R Arda-cards/infrastructure --json state reports MERGED.
    • The Phase 3 docs Wave-1 PR (this branch’s PR) is MERGED.
    • The Phase 3 infrastructure PR is MERGED — the deploy in Task O2 expects the code on main of Arda-cards/infrastructure.

    Override (operator discretion): if a human reviewer has explicitly approved a PR and auto-merge is armed but the merge has not yet landed (e.g., a CI re-run is in flight), the operator may proceed past T-O0 with a documented exception captured in operator notes for Phase D’s runbook. The override exists because reviewer-delay shouldn’t block a deploy that is otherwise green; it does not waive the requirement that PRs be approved before deploy. Operators do not override around an unapproved or red-CI PR.

  2. AWS state (platformRoot account, Admin-Alpha1 profile):

    • cdk diff for apps/Root/ against the deployed RootConfiguration stack reports zero differences (Phase 2 baseline preserved).
    • aws cloudformation describe-stacks --stack-name RootConfiguration --profile Admin-Alpha1 reports UPDATE_COMPLETE (or IMPORT_COMPLETE); Outputs includes arda-ardamails-zone and arda-allow-create-ns-record-role.
    • No AWS::Route53::HostedZone for arda.ardamails.com exists yet (the zone Phase 3 creates is not present from a previous attempt).
    • No NS record set named arda.ardamails.com. exists in the ardamails.com zone.
  3. Postmark state (PostmarkProd):

    • Account-token resolves via the operator’s 1Password DesktopAuth: op read 'op://Arda-SystemsOAM/Postmark-Prod/credential' returns a valid token.
    • No existing Sender Signature for arda.ardamails.com; no existing server matching the Free Kanban Tool’s configured name. (If either is found, the operator confirms whether to reconcile or to abort; idempotency is by design but a stale entity from a previous attempt deserves explicit acknowledgment.)
    • Note: sandbox-vs-live status cannot be probed at this pre-deploy stage because DeliveryType is a per-server property returned by GET /servers/{id} — it exists only after server creation. Phase A surfaces it immediately after creating the server; see Task I9. REQ-OPS-002 is answered during Phase A, not at pre-deploy time.
  4. 1Password state:

    • Arda-CorporateOAM vault is reachable from the operator workstation (DesktopAuth).
    • Free-Kanban-Generator-Postmark-Server item does not exist (Phase A creates it).
    • Phase 1 items in Arda-SystemsOAM resolve under OP_SERVICE_ACCOUNT_TOKEN-scoped CI auth (the existing external-resources-drift.yml workflow’s last successful run confirms this; if the workflow has not run since the relevant change, trigger it via workflow_dispatch).
  5. Operator-side prerequisites (recap):

    • Local infrastructure repo on jmpicnic/email-integration-phase-3 (in infrastructure); npm ci clean; npm test clean.
    • Phase 3 contract documentation merged to main in documentation (Wave 1).
    • DMARC reporting mailbox (dmarc-reports@arda.cards) NOT yet provisioned (that’s Task O1; pre-state assertion only confirms its absence is the expected starting state, not that it should already exist).

File targets: none (this is a state assertion, not a state mutation).

AWS impact: None (read-only assertions).

STOP — review after O0: every assertion above passes. If any fails, do not proceed; investigate (typically: a previous Phase 3 attempt left a partial state; cleanup is a separate operator decision). The Corporate CLI’s --dry-run mode (introduced in I9) is the recommended driver for the API-side assertions; the AWS-side assertions use aws CLI / cdk diff.


Task O1: Operator prerequisite — DMARC reporting mailbox

Section titled “Task O1: Operator prerequisite — DMARC reporting mailbox”

Goal: ensure the DMARC rua destination resolves before Phase B deploy.

Scope:

  1. Operator provisions dmarc-reports@arda.cards in Arda’s Google Workspace as a real, reachable mailbox or distribution list.
  2. Send a test email to the address; confirm receipt.

File targets: none (out-of-IaC operator action).

AWS impact: None.

STOP — review after O1: confirmation captured in the operator companion (Task O3).


Goal: run the orchestrator against PostmarkProd; deploy the Corporate stacks.

Scope:

  1. Run Phase A in PostmarkNonProd first as a smoke test (use a fixture asset name that does not collide with PostmarkProd). Validate the structured output and the resulting cdk.context.json (committed, but tagged so it does not pollute the prod context).
  2. Run Phase A in PostmarkProd: corporate-cli prepare free-kanban. The 1Password item is created in Arda-CorporateOAM; cdk.context.json is updated with the prod values.
  3. Commit the updated cdk.context.json. PR review surfaces the diff.
  4. Run Phase B: corporate-cli deploy free-kanban. The cdk diff is displayed; user confirmation is the gate. After confirmation, cdk deploy apps/Corporate/ runs:
    • Creates the arda.ardamails.com zone.
    • Writes SPF / DMARC records.
    • Writes the NS-delegation record for arda into ardamails.com via the WriteNSRecordsToUpstreamDns CR.
    • Writes the Free Kanban DKIM TXT and Return-Path CNAME records.

File targets: none (this is the deploy task; the file deltas were produced by I1-I9 and the cdk.context.json update).

AWS impact: Resource-touching. Real production resources land in platformRoot. Postmark state (Sender Signature, server, item) lands in PostmarkProd / Arda-CorporateOAM.

STOP — review after O2: cdk diff against the deployed Corporate stacks reports zero differences. dig confirms the records resolve.


Task O3: Post-deploy verification + operator notes capture

Section titled “Task O3: Post-deploy verification + operator notes capture”

Goal: verify end-to-end and capture the raw material the runbook (Phase D, Task D2) is authored from.

Scope:

  1. Run corporate-cli verify free-kanban (or invoke the verifyDkim / verifyReturnPath API calls directly): the Sender Signature is now marked verified. Capture timestamps + any operator-visible quirks.
  2. (If account is in sandbox) Submit the Postmark sandbox-to-live approval via the Postmark Console — the only manual UI step in Phase 3 (per REQ-OPS-002). Wait for approval; capture submit / approval timestamps.
  3. End-to-end smoke send: send a test email from the Free Kanban Tool to a non-owner address. Confirm Authentication-Results shows dkim=pass, spf=pass, dmarc=pass aligned with arda.ardamails.com (per REQ-OPS-004). Capture the header text.
  4. Stash the captured notes in operator scratch (a personal note, a Linear comment, or a working file outside source control). They feed Task D2 in Phase D, which authors the long-lived runbook.

The runbook is not authored in this task — it lives at process/sre/runbooks/... and is a Phase D deliverable so it benefits from the deploy experience and any discoveries made between O3 and the runbook write.

File targets: none in this task (the captured notes are operator scratch, not committed).

AWS impact: None on the IaC side; the smoke send produces real live mail.

STOP — review after O3: smoke send succeeded; operator notes captured for Phase D’s runbook authoring.


Task O4: First scheduled run of corporate-drift.yml

Section titled “Task O4: First scheduled run of corporate-drift.yml”

Goal: confirm the drift workflow exercises the deployed Corporate state successfully and opens an issue on injected drift.

Scope:

  1. Trigger corporate-drift.yml via workflow_dispatch (manual run) immediately after T-O3 to validate the workflow fires correctly against the deployed state.
  2. Wait for the next scheduled run (monthly) to confirm the cron-driven path works.
  3. Optionally inject a drift (e.g., temporarily delete a non-critical record in a test fixture or simulate via a test mode) to confirm the issue-on-failure path; back-out immediately.

File targets: none (this is a runtime confirmation; no file deltas).

AWS impact: None (the workflow only reads external state).

STOP — review after O4: workflow run reports success against the green deployed state; the drift-detection-failure path is confirmed by the optional injected-drift test (or deferred to first natural drift event).


Task D2: System documentation pages + operator runbook

Section titled “Task D2: System documentation pages + operator runbook”

Goal: publish the Phase 3 long-lived documentation under current-system/ (describing what was built) and the operator runbook under process/sre/runbooks/ (describing the durable procedure). Both authored after deploy so they reflect any deploy-time discoveries.

Scope:

  1. Add new pages under current-system/oam/:
    • A Postmark service overview for the Corporate consumer pattern (the operational view: how the parent Sender Signature works, how leaf sub-domains inherit DKIM, how the Phase A / Phase B split surfaces in operator workflows, what the actual verifyDkim / verifyReturnPath round-trip looks like in production).
    • A Free Kanban Tool service page (per-asset OAM: where the server lives, where the token lives, how to rotate it, the smoke-send playbook from Task O3’s experience).
    • A Corporate drift notes page (cadence, label conventions, escalation, real first-run results from Task O4).
  2. Add new pages under current-system/runtime/:
    • A Corporate Resource Group structure page (instance-group definition; AWS-account-vs-instance-group decoupling; reserved-name discipline at arda.ardamails.com; the actual deployed CFN stack names).
    • A Free Kanban Tool component placement page.
  3. Author the long-lived operator runbook at process/sre/runbooks/postmark-domain-verification.md (or a similarly scoped name finalized at write time — the procedure generalizes beyond Free Kanban Tool: any future Corporate consumer or partition-level Postmark Sender Signature verification reuses it). The runbook is sourced from the operator notes captured in Task O3. The Phase 1 stub at roadmap/in-progress/email-integration/3-corporate-updates/operator-domain-verification-checklist.md is superseded by the new runbook; D2 either rewrites the stub as a forward-pointer to the runbook or removes it (operator decision at authoring time).
  4. No new pages under current-system/functional/ or current-system/data-model/ (Phase 3 is all IaC).

File targets:

  • New: current-system/oam/postmark-service/corporate-consumers.md (or similarly named).
  • New: current-system/oam/corporate/free-kanban-tool.md, current-system/oam/corporate/drift-notes.md.
  • New: current-system/runtime/corporate-resource-group.md, current-system/runtime/free-kanban-tool.md.
  • New: process/sre/runbooks/postmark-domain-verification.md (the durable runbook).
  • Edit or remove: roadmap/in-progress/email-integration/3-corporate-updates/operator-domain-verification-checklist.md (superseded by the runbook).

AWS impact: None.

STOP — review after D2: pages render in make preview; internal links resolve; make test-links returns zero broken links; the new runbook is reachable from the Free Kanban Tool service page and from the Corporate consumers OAM page. If any system-doc text contradicts the contract in specification.md, capture the divergence in implementation/specification-post.md (Task D5) and amend the spec only after user direction — the contract is the historical record of what was prescribed.


Goal: produce the standard implementation byproducts under 3-corporate-updates/implementation/ per the implementation-task skill.

Scope:

  1. Author six files under 3-corporate-updates/implementation/:
    • changelog.md — task-by-task summary of what landed, deviations from the spec, and any deferred follow-ups.
    • learnings.md — non-obvious insights surfaced during implementation (the Phase-2 IMPORT-detour pattern is the model; if Phase 3 surfaces an analogous discovery, capture it here as L-N).
    • suggestions.md — cross-cutting improvements that fell out of scope but should be tracked (S-N).
    • alternatives.md — design choices considered and rejected during implementation (A-N).
    • skipped.md — spec items intentionally not implemented, with rationale (SK-N).
    • specification-post.md — spec deltas (D-N): each entry names a section of specification.md that was amended after deploy and the reason. Captures contract drift without rewriting history.
  2. Cross-link with the corresponding files in 1-external-resources/implementation/ and 2-root-updates/implementation/.

File targets:

  • New: 3-corporate-updates/implementation/{changelog,learnings,suggestions,alternatives,skipped,specification-post}.md.

AWS impact: None.

STOP — review after D5: byproducts published; make pr-checks clean.


Task D6: Documentation CHANGELOG amendment (system docs + byproducts)

Section titled “Task D6: Documentation CHANGELOG amendment (system docs + byproducts)”

Goal: capture the system documentation and byproducts in the documentation/CHANGELOG.md.

Scope:

  1. Open a follow-up docs PR (or extend an open one) carrying D2 + D5 + D6.
  2. Add a new top entry to documentation/CHANGELOG.md (next available minor; the contract entry [0.31.0] is closed once the contract docs PR merges):
    • ### Added: the current-system/oam/... and current-system/runtime/... pages from D2; the 3-corporate-updates/implementation/*.md byproducts from D5.

File targets:

  • Edit: documentation/CHANGELOG.md.

AWS impact: None.

STOP — review after D6: make clq passes.

Phase A (contract docs PR):
D1 -> D3 -> D4
Phase B (infra PR; can start as soon as Phase A is reviewable):
I1 -> I2 -> I3 ----> I6 -> I7 -> I9 -> I10
\ /
I4 -> I5 ------/
\
I8 (parallel; first run validates O2)
Phase C (operator deploy + verification, post-Phase-B-merge):
O0 -> O1 -> O2 -> O3 -> O4
(O0 is a read-only state assertion; nothing in Phase C may execute until O0's assertions pass.)
Phase D (system-docs follow-up PR, post-O3):
D2 -> D5 -> D6

Phase relationships:

  • Phase A is the contract. It must land first; Phase B implements against it; Phase C verifies it.
  • Phase A and Phase B can run in parallel after Phase A’s PR is reviewable — Phase B’s implementer treats Phase A’s open PR as the contract.
  • Phase A merges before Phase C (the deploy needs the contract published).
  • Phase D explicitly lands last — after the deploy in Phase C produces real evidence — so the current-system/ pages, the byproducts, and the operator companion all benefit from learnings, deploy-time discoveries, and any contract drift captured in specification-post.md.

Within Phase B:

  • I1 (rename) blocks I2 / I3 only because it changes import paths. Otherwise the construct work is independent.
  • I4 / I5 are independent and can land in either order.
  • I6 (stacks) requires I1, I2, I3, I5.
  • I7 (app) requires I6.
  • I8 (drift workflow) is parallel to the deploy tasks; the workflow’s first scheduled run validates the deploy.
  • I9 (CLI) requires I3 (it imports / interacts with the Postmark thin-wrapper internals’ shape).
  • I10 (infra CHANGELOG) is the last Phase-B task before opening the infra PR.

Within Phase C:

  • O0 (pre-deploy state assertion) is the first action; nothing else in Phase C runs until its assertions pass. Read-only; safe to re-run.
  • O1 (mailbox) is an out-of-IaC prerequisite that can run anytime between O0 and O2.
  • O2 (deploy) requires every I task complete, O0 cleared, and O1 confirmed.
  • O3 (post-deploy verification + operator notes) requires O2.
  • O4 (drift workflow first run) requires O2 deployed.

Within Phase D:

  • D2 (system docs) draws on what was actually deployed and verified in Phase C.
  • D5 (byproducts) draws on the implementation experience — what surprised, what was skipped, what alternatives surfaced.
  • D6 (CHANGELOG amendment) folds D2 + D5 into a new top entry on a follow-up docs PR.

Restated for clarity; identical to requirements.md § Out of scope of Phase 3.

The eight Phase 3 design questions were resolved during Pass 1 of this planning. Each is recorded in decision-log.md under Round R1-Phase3 (DQ-R1-009..016). Restated here so the implementer has a single index in the spec:

#Question (short form)Decision (short form)Reference
OQ-1Postmark verification target — parent or leaf?Parent (arda.ardamails.com); leaves inherit DKIM.DQ-R1-009
OQ-2NS-delegation write — through assume-role even when same-account?Yes; uniform pattern; preserves invariance under future Corporate-account migration.DQ-R1-010
OQ-3route-53-hosted-zone.ts migration shape?Rename in place; callers updated in same PR.DQ-R1-011
OQ-4Corporate drift-workflow filename and scope?corporate-drift.yml; instance-group-scoped; data-driven over instances/Corporate/.DQ-R1-012
OQ-5Phase A failure ordering for the server token?In-memory buffer + retries on the 1P write; fail loud; no auto-rollback via delete-server.DQ-R1-013
OQ-6cdk.context.json commit policy?Commit (public values only; standard CDK convention).DQ-R1-014
OQ-7DMARC reporting mailbox?dmarc-reports@arda.cards; operator provisions in Google Workspace before Phase B deploy.DQ-R1-015
OQ-8Reserved-name registry scope at arda.ardamails.com?Documentation-only; CLI enforces locally via Phase-A conflict-check.DQ-R1-016

If implementation surfaces new OQs, record them here with options + recommendation, then resolve to a DQ-R1-NNN entry continuing from DQ-R1-017.