Task Plan: Email Integration Phase 5a
Author: Miguel Pinilla
Date: 2026-05-28
Status: Historical plan — Phase 5a completed 2026-05-29. See phases.md § 5a and goal.md for the as-shipped release map. The task list below is preserved as the planning-time snapshot; the planned release versions (9.2.0 → 9.5.0 / 10.0.0) shifted in flight to the actual 10.1.0 → 11.2.0 sequence because of unrelated intervening common-module releases.
User Request
Section titled “User Request”Plan and execute Phase 5a — five additive helpers in common-module (AppError.Application, IncompatibleState reclassification sweep, sanitizeHeader, TokenCipher + Hmac, idempotency helpers) and the roadmap promotion that precedes them. All five PRs reference Linear PDEV-201; transitions are manual (no PDEV-201 token in any PR body). Sequencing per DQ-R1-028: the four Added-only minor PRs land in any order; the Changed major PR (the sweep) lands last.
Decomposition
Section titled “Decomposition”Task List
Section titled “Task List”| # | Task | Persona | Repository | Depends On | Planned Release | Status |
|---|---|---|---|---|---|---|
| 0 | Roadmap promotion (this directory + decision-log entries) | (this plan) | documentation | — | n/a (docs PR) | Pending |
| 1 | Implement AppError.Application introduction | back-end-engineer | common-module | #0 merged | Added; 9.2.0 | Pending |
| 3 | Implement sanitizeHeader + lib/api/headers/ package | back-end-engineer | common-module | #0 merged | Added; 9.3.0 | Pending |
| 4 | Implement TokenCipher + Hmac + adjacent refactor | back-end-engineer | common-module | #0 merged | Added; 9.4.0 | Pending |
| 5 | Implement idempotency helpers (lib/runtime/idempotency/) | back-end-engineer | common-module | #0 merged | Added; 9.5.0 | Pending |
| 2 | Internal.IncompatibleState reclassification sweep | back-end-engineer | common-module | #1 merged + (#3, #4, #5 merged in any order) | Changed; 10.0.0 | Pending |
Task numbers follow the design topic ordering (PR #1 first by intent, PR #2 last by sequencing). Each implementation task is its own PR opened off origin/main in common-module. The sweep (#2) is sequenced last so consumers absorb one combined gradle.properties lift to 10.0.0.
Worktree Strategy
Section titled “Worktree Strategy”- Documentation promotion (task #0) — single worktree at
phase-5/documentation-5a-promotion/, branched offjmpicnic/email-integration-phase-4(PR #100’s branch); stacks on the open Phase 4 documentation PR. Branch name:jmpicnic/email-integration-phase-5a-promotion. Single PR againstArda-cards/documentation. - Common-module implementation tasks (#1, #2, #3, #4, #5) — one focused worktree per task at
phase-5/common-module-<task-slug>/. Each worktree’s branch is namedjmpicnic/email-integration-phase-5a-<task-slug>and bases offorigin/main(NOT off the documentation branch — code and docs ship from separate repos with their own version trees). Task #2 (sweep) waits until task #1 has merged tomain; it can be opened earlier but should not be reviewed before #1’s PR is reviewable. - Existing
phase-5/common-module/— continues to exist as the longer-running integration / scratch worktree on branchjmpicnic/email-integration-phase-5. It is NOT a PR branch; do not push it to origin until / unless we decide to use it for cross-Phase-5 work. The five implementation PRs use the focused per-task worktrees described above.
| Worktree directory | Branch | Repository | Task |
|---|---|---|---|
phase-5/documentation-5a-promotion/ | jmpicnic/email-integration-phase-5a-promotion | documentation | #0 |
phase-5/common-module-app-error-application/ | jmpicnic/email-integration-phase-5a-app-error-application | common-module | #1 |
phase-5/common-module-incompatible-state-sweep/ | jmpicnic/email-integration-phase-5a-incompatible-state-sweep | common-module | #2 |
phase-5/common-module-sanitize-header/ | jmpicnic/email-integration-phase-5a-sanitize-header | common-module | #3 |
phase-5/common-module-token-cipher/ | jmpicnic/email-integration-phase-5a-token-cipher | common-module | #4 |
phase-5/common-module-idempotency-helpers/ | jmpicnic/email-integration-phase-5a-idempotency-helpers | common-module | #5 |
Agent absolute-path rule: agents working in any of these worktrees use absolute paths for all tool calls (Read, Edit, Write, Glob, Grep) and prefix Bash commands with git -C <absolute-worktree-path> or equivalent. Never use relative paths or cd.
Worktree creation pattern:
# from the workspace root, for each implementation task:git -C /Users/jmp/code/arda/common-module worktree add \ /Users/jmp/code/arda/projects/email-integration-worktrees/phase-5/common-module-<task-slug> \ -b jmpicnic/email-integration-phase-5a-<task-slug> \ origin/mainParallelization
Section titled “Parallelization”- Sequential at the boundary: Task #0 (roadmap promotion) merges first; the five implementation PRs cite the promoted pages.
- Parallel within implementation phase: Tasks #1, #3, #4, #5 are independent and can be implemented and reviewed concurrently. Each PR opens off
origin/main; merges happen in whichever order reviews finish. - Final sequencing: Task #2 (sweep) waits on #1 (which introduces
AppError.Application). It can be implemented concurrently with #3/#4/#5 but must not merge until #1 has merged. The sweep’s branch should rebase againstorigin/mainafter each of #1/#3/#4/#5 merges so the final review compares against the right baseline.
Idle Agent Strategy
Section titled “Idle Agent Strategy”Single-engineer (Miguel) plan. No multi-agent parallelism in this phase; per-task worktrees exist for branch isolation rather than concurrent execution. If a back-end-engineer agent is spawned per task, the agent works one task at a time; idle time between tasks is acceptable (the implementation work itself is the value, not throughput optimisation).
Personas Required
Section titled “Personas Required”| Persona | Tasks Assigned | Worktree | Spawn Order |
|---|---|---|---|
| (this plan author) | #0 | phase-5/documentation-5a-promotion/ | First |
back-end-engineer | #1 | phase-5/common-module-app-error-application/ | After #0 merges |
back-end-engineer | #3 | phase-5/common-module-sanitize-header/ | After #0 merges (parallel with #1) |
back-end-engineer | #4 | phase-5/common-module-token-cipher/ | After #0 merges (parallel with #1) |
back-end-engineer | #5 | phase-5/common-module-idempotency-helpers/ | After #0 merges (parallel with #1) |
back-end-engineer | #2 | phase-5/common-module-incompatible-state-sweep/ | After #1 merges; merges last |
pr-steward (skill) | All PRs | per-PR | Per PR push (check monitoring + review triage) |
Per-task Acceptance Criteria
Section titled “Per-task Acceptance Criteria”Task #0 — Roadmap promotion (documentation)
Section titled “Task #0 — Roadmap promotion (documentation)”Branch: jmpicnic/email-integration-phase-5a-promotion off jmpicnic/email-integration-phase-4.
PR base: jmpicnic/email-integration-phase-4 (stacked on the open Phase 4 docs PR #100).
Acceptance criteria:
5a-component-library-updates/goal.mdexists and matches the goal template shape.5a-component-library-updates/design/index.mdexists with the four-helper canonical design.5a-component-library-updates/design/idempotency-design.mdexists with the JsonElement-boundary design.5a-component-library-updates/task-plan.mdexists (this file).decision-log.mdcontains five new entriesDQ-R1-027throughDQ-R1-031plus aRound R1-Phase5aheader; the Decision Table at the top and the Summary table at the bottom include the new entries.- Workbook references to
DQ-R1-027 (suggested numbering)are updated to the real numbersDQ-R1-027..031acrossnotebooks/email-integration/5a/(decision record, design docs, README, work-board). make pr-checkspasses locally (lint, preview build, link check, smoke tests).- PR body includes a
## CHANGELOGsection under categoryAdded. NoPDEV-201token in the body.
Deliverable: one PR against Arda-cards/documentation, base = jmpicnic/email-integration-phase-4.
Task #1 — AppError.Application introduction (common-module)
Section titled “Task #1 — AppError.Application introduction (common-module)”Branch: jmpicnic/email-integration-phase-5a-app-error-application off origin/main.
Acceptance criteria:
lib/src/main/kotlin/cards/arda/common/lib/lang/errors/AppError.ktextended withsealed class Applicationand three subtypes (PreconditionFailed,PolicyRejected,ConflictingState);reportable()returns empty list at the branch root.lib/src/test/kotlin/cards/arda/common/lib/lang/errors/AppErrorReportableTest.ktextended with tests covering all three subtypes’ constructors andreportable()contract.lib/src/main/kotlin/cards/arda/common/lib/api/rest/types/HttpResponses.kt(or wherever the L4 error-mapping lives) maps the three subtypes to documented HTTP statuses; tests in the existingHttpAppErrorResponsesTest.ktcover the mapping.CHANGELOG.md— one new entry[9.2.0]; categoryAdded; entry describes the new branch and its consumer-relevant impact.- All existing tests pass.
- PR body includes a
## CHANGELOGsection if applicable to this repo’s model (workspace memory:common-moduleis direct-edit — entry goes inCHANGELOG.md, not the PR body). NoPDEV-201token in the body.
Deliverable: one PR against Arda-cards/common-module, base = main.
Task #2 — Internal.IncompatibleState reclassification sweep (common-module)
Section titled “Task #2 — Internal.IncompatibleState reclassification sweep (common-module)”Branch: jmpicnic/email-integration-phase-5a-incompatible-state-sweep off origin/main (rebased onto main after each of #1/#3/#4/#5 merges).
Acceptance criteria:
- A discovery file at
scratch/incompatible-state-inventory.md(worktree-local, not committed) enumerates everyIncompatibleState(...)construction site inlib/src/mainwith a one-line per-site classification rationale (bucket A / B / C per design § 2.2). - Every site in bucket B is reclassified to
Application.ConflictingState; every site in bucket C is reclassified toInvocation.GeneralValidation; bucket A sites are untouched. - Existing test suites pass after the sweep. Tests that asserted
is Internal.IncompatibleStatefor migrated sites change to assert the new bucket type. - The PR description carries the per-bucket inventory (lifted from the scratch file) so reviewers see the classification at a glance.
CHANGELOG.md— one new entry[10.0.0]; categoryChanged; entry describes the reclassification and lists the buckets.- All existing tests pass.
Deliverable: one PR against Arda-cards/common-module, base = main. Major bump.
Task #3 — sanitizeHeader (common-module)
Section titled “Task #3 — sanitizeHeader (common-module)”Branch: jmpicnic/email-integration-phase-5a-sanitize-header off origin/main.
Acceptance criteria:
- New package
lib/src/main/kotlin/cards/arda/common/lib/api/headers/withSanitizeHeader.ktexposingfun sanitizeHeader(name: String, value: String): Result<String?>. - Test file
lib/src/test/kotlin/cards/arda/common/lib/api/headers/SanitizeHeaderTest.ktcovers the cleaning rules per design § 3.4. - A composition example test demonstrates the
HeadersAllowList+sanitizeHeaderpipeline shape. CHANGELOG.md— one new entry[9.3.0]; categoryAdded.- All existing tests pass.
Deliverable: one PR against Arda-cards/common-module, base = main.
Task #4 — TokenCipher + Hmac (common-module)
Section titled “Task #4 — TokenCipher + Hmac (common-module)”Branch: jmpicnic/email-integration-phase-5a-token-cipher off origin/main.
Acceptance criteria:
- New package
lib/src/main/kotlin/cards/arda/common/lib/crypto/withTokenCipher.kt,Hmac.kt,EnvelopeAlgorithm.kt(internal),EnvelopeAlgorithmA1.kt(internal). TokenCipherfactory shape:companion operator fun invoke(info, materials, currentVersionId): Result<TokenCipher>per design § 4.4.- Auth-tag failure on
decryptreturnsResult.failure(AppError.Internal.IncompatibleState); unknownversionIdondecryptreturnsResult.failure(AppError.Transient.FailoverFailed(...))per design § 4.7. Hmac.sha256(key)exposed; the two JDK-Maccall sites atOpaqueId.kt:67andS3AssetService.kt:143-144migrated to use it (internal refactor; no external API change at those sites).- Test files under
lib/src/test/kotlin/cards/arda/common/lib/crypto/cover the matrix in design § 4.9. CHANGELOG.md— one new entry[9.4.0]; categoryAddedonly (the call-site migrations are internal refactor; no external API change).- Existing
OpaqueIdTestandS3AssetServicetests pass. - All other existing tests pass.
Deliverable: one PR against Arda-cards/common-module, base = main.
Task #5 — Idempotency helpers (common-module)
Section titled “Task #5 — Idempotency helpers (common-module)”Branch: jmpicnic/email-integration-phase-5a-idempotency-helpers off origin/main.
Acceptance criteria:
- New package
lib/src/main/kotlin/cards/arda/common/lib/runtime/idempotency/with the structure in idempotency-design § 1:core/,service/,api/. RawIdempotencyStoreandIdempotencyStore<Req, Res>interfaces exposed; the typed wrapper produced byinline fun typedAs(json: Json = JsonConfig.standardJson): IdempotencyStore<Req, Res>per design § 2.8.MismatchcarriesrecordedRequest: JsonElement(native) /Result<Req>(typed).- Decode failures in the typed wrapper return
Result.failure(AppError.Internal.IncompatibleState)per design § 5. IdempotencyKeyMinterexposed for Intent (b) use.- ContainerizedPostgres integration tests at
lib/src/test/kotlin/cards/arda/common/lib/runtime/idempotency/cover the matrix in design § 7. - The
idempotency_recordFlyway migration is NOT shipped by this PR. It ships in Phase 5b’s consumer adoption per DQ-R1-031. The PR’s tests create the table via the existingContainerizedPostgres.fromTestConfigflow with a test-only migration underlib/src/test/resources/. CHANGELOG.md— one new entry[9.5.0]; categoryAdded.- All existing tests pass.
- Knowledge-base extraction follow-ups flagged in the PR description (
canonical-json-hashing.md,reified-inline-extension-factory.md) per design § 8.
Deliverable: one PR against Arda-cards/common-module, base = main.
Open Questions and Decisions
Section titled “Open Questions and Decisions”Resolved at planning time
Section titled “Resolved at planning time”All structural decisions for Phase 5a are resolved in DQ-R1-027..031 and were settled in the workbook discussion (notebooks/email-integration/5a/) before this plan was drafted. The plan executes against the locked design.
Operational
Section titled “Operational”- Linear ticket transitions: PDEV-201 transitions are manual. No
PDEV-201token appears in any PR body (workspace memory:feedback_clock_timezone/ Linear-GitHub-integration auto-transitions on body mentions). Cross-references go in Linear-side comments on PDEV-201, not in PR bodies. - Branch cleanup: after each PR merges, delete the local branch (
git branch -d <branch>) and remove the worktree (git -C <repo> worktree remove <path>). Workspace memoryfeedback_delete_superseded_branchesextends to merged branches: delete the origin-side branch too once nothing else bases on it. - CHANGELOG model:
common-moduleis direct-edit (workspacechangelog.mdrule). Each implementation PR editsCHANGELOG.mddirectly. The PR body does NOT need a## CHANGELOGsection; the CHANGELOG.md edit is the contract. (Contrast with the documentation repo, which is PR-body-changelogs.) - Per-PR pre-push gate: run
./gradlew check(or workspace-equivalentmake/npmtarget if one is defined forcommon-module) before every push. Catches lint, type, and test failures locally before CI.
Out-of-scope
Section titled “Out-of-scope”- Phase 5b consumer adoption — separate plan; lifts
common-moduleto 10.0.0, applies the email-module-scopedIncompatibleStatesweep, ships theidempotency_recordFlyway migration. - HKDF exposure as a standalone helper — deferred (workbook DT-003).
- Documentation promotion of Phase 5a to
completed/— happens at project close, after Phase 5b also lands.
References
Section titled “References”goal.md— Phase 5a goal and success criteria.design/index.md— canonical Phase 5a design.design/idempotency-design.md— idempotency carved-out design.../decision-log.md—DQ-R1-027..031for Phase 5a; DQ-201..208 + DQ-012 + DQ-R1-019 for inherited constraints.design/index.md§ 6.4 restates the four constraints this design honours implicitly.- Workspace
kotlin-codingstandards.
Copyright: © Arda Systems 2025-2026, All rights reserved