export const meta = { name: 'phase-4-cdk-common', description: 'Phase 4 of the procurement-ingest refactor (docs/refactor-evaluation.md): collapse the 379 identical CDK lines into cdk/common.py as PLAIN FUNCTIONS taking (scope, id, ...) — called with the SAME Stack scope and the SAME construct ids the stacks use today, so every logical ID is byte-stable (Construct-subclass wrapping is forbidden: it would reparent the tree and attempt REPLACEMENT of the RETAIN-protected purchase-orders/WorkOrders tables and named buckets = data loss). Extracts add_ddb_alarms, add_sender_auth_rejected_alarm, add_standard_lambda_alarms, make_bedrock_invoke_statement (account/region-derived ARN, not hardcoded 328440206208), make_email_bucket, make_processor_dlq, make_fallback_rate_alarm — preserving every per-function alarm variance byte-for-byte. Same PR: account on both cdk.Environment, constructs== exact pin, stale-comment fix, CfnOutputs for five function ARNs + consumed table names. ZERO cdk diff on both stacks is the acceptance test. The make_bedrock_invoke_statement IAM PolicyStatement move triggers mandatory GPT-4.1 cross-family review even though semantics are identical. Committed locally, never pushed.', phases: [ { title: 'Setup', detail: 'verify Phases 0+1+2+3 on base, branch feature/phase-4-cdk-common', model: 'haiku' }, { title: 'Recon', detail: '4 mappers: the 379 duplicated CDK lines + per-function alarm variance, the two inline Bedrock PolicyStatements, the fallback-rate + rejected alarm math (post-Phase-1), app.py/pins/outputs surface' }, { title: 'Spec', detail: 'serial fable spec: pin common.py contents + every function signature, per-stack call-site rewrites, alarm-variance table, Bedrock IAM equivalence, fallback-rate call params, app/pins/CfnOutputs, zero-diff judging rules' }, { title: 'Implement', detail: 'opus: cdk/common.py; opus: both stacks (rewire + CfnOutputs); sonnet: app.py + requirements pin + README — disjoint files', model: 'opus' }, { title: 'Verify', detail: 'mechanical gates + zero-cdk-diff verifier (the load-bearing gate) + 3 fable lenses (logical-ID safety, alarm variance, IAM equivalence)' }, { title: 'Fix', detail: 'opus fixer, full re-verify, max 3 rounds', model: 'opus' }, { title: 'Package', detail: 'single commit via -F (no push); runs cross_review.py inline on the IAM diff', model: 'sonnet' }, ], } // ---------------------------------------------------------------- constants const REPO = '/Users/adammoussa/Documents/repositories/seahaven/procurement-ingest' const BRANCH = 'feature/phase-4-cdk-common' let _args = args if (typeof _args === 'string') { try { _args = JSON.parse(_args) } catch (e) { _args = null } } const BASE = (_args && _args.base) || 'main' const CONSTRAINTS = ` PINNED BEHAVIORAL CONSTRAINTS (docs/refactor-evaluation.md Phase 4 — violating any is a build failure): 1. EXTRACT AS PLAIN FUNCTIONS taking (scope, id, ...), called with the SAME scope (the Stack instance) and the SAME construct ids the stacks use today -> 100% logical-ID-safe. NEVER wrap in Construct subclasses: a subclass inserts a tree node, changes EVERY child logical ID, and would attempt REPLACEMENT of the RETAIN-protected purchase-orders / WorkOrders tables and the named buckets = DATA LOSS. This is THE load-bearing rule of the phase — every other check exists to defend it. 2. New module cdk/common.py. Extract exactly: - _DDB_ALARM_OPERATIONS + add_ddb_alarms - add_sender_auth_rejected_alarm - add_standard_lambda_alarms(scope, id_prefix, fn, name_prefix, topic, *, duration_statistic, errors=True, dlq=None, descriptions=...) - make_bedrock_invoke_statement (DERIVE the inference-profile ARN + the per-region foundation-model ARNs from Stack.of(scope).account / Stack.of(scope).region — NOT hardcoded 328440206208) - make_email_bucket - make_processor_dlq - make_fallback_rate_alarm(namespace, rejected_included, period, threshold, floor, evaluation_periods, datapoints_to_alarm) reproducing the expression strings / FILL / labels BYTE-FOR-BYTE. 3. PER-FUNCTION ALARM VARIANCE — preserve EXACTLY, do NOT homogenize: PO email-processor duration p99 vs wo-email-processor p95; po-web-ui throttles+duration only; site_extractor no-DLQ; wo web_ui has ZERO alarms (do NOT let the shared helper silently add any); every bespoke alarm DESCRIPTION string is passed through verbatim. 4. FALLBACK-RATE RECONCILIATION (post-Phase-1): PO's template-fallback-rate EXCLUDES the rejected series (byte-identical to today, a pre-call double-count would result otherwise) -> call make_fallback_rate_alarm with rejected_included=False; WO's INCLUDES rejected -> rejected_included=True. The two REJECTED alarms are a DIFFERENT shape and are NOT make_fallback_rate_alarm: PO's ai-fallback-rejected is a 6h count-floor IF(FILL(rej,0)>=1,...) alarm (added in Phase 1); WO's is the 5-min / 2-of-6 sparse idiom. Keep those as DISTINCT call sites (or a separate dedicated helper) — do NOT force them through make_fallback_rate_alarm. NO element-wise MAX anywhere (post-#102 rule). 5. Do NOT import stack-specific services (kms / ssm / event_sources) into common.py — only the constructs the shared helpers actually need. 6. SAME PR, net-new & logical-ID-safe additions: - add account='328440206208' to BOTH cdk.Environment calls - pin constructs== to the exact installed version (not a floor >=) - fix the stale "2.259.0" version comments - add CfnOutputs for the FIVE function ARNs + the consumed table names. These are additive; CfnOutputs and account are ID-safe. Verify none of them perturbs an existing logical ID. 7. ZERO lambdas/ diff: git diff ${BASE}...HEAD -- lambdas/ must be EMPTY. This phase is CDK-ONLY. tests/ may gain a cdk-diff / synth-only test for the acceptance gate, but NO other tests/ change and NO lambdas/ change. 8. ZERO cdk diff on BOTH stacks is the acceptance test: npx cdk diff po-ingest and npx cdk diff workorder-ingest must show ZERO resource changes (no logical-ID, alarm, IAM, table, bucket, env, or metadata delta beyond CDK-tooling noise). The CfnOutputs are the ONLY net-new resources allowed to appear, and only as additions. 9. The wo artifact id is 'workorder-ingest' (the construct id / 2nd positional arg), NOT 'WorkorderIngestStack' (that is stack_name). ALWAYS drive synth/diff by the artifact id: npx cdk synth workorder-ingest, npx cdk diff workorder-ingest. Using the stack_name selector fails. 10. NO cdk deploy, NO invoke, NO AWS mutation. Read-only AWS only if needed (e.g. confirming deployed alarm names) — the diff gate is a pure local synth-vs-synth comparison. ` const PREAMBLE = ` You are one of several agents building refactor Phase 4 in the git repo at ${REPO} on branch ${BRANCH} (already checked out — do NOT switch branches, do NOT create branches, do NOT commit, NEVER push, do NOT run cdk deploy or touch AWS resources beyond read-only calls). Authoritative spec: docs/refactor-evaluation.md, section "Phase 4". Work ONLY in the files you are told you own; other agents are concurrently editing other files in this same working tree. ${CONSTRAINTS} Your final message is consumed by an orchestrator script, not a human — return only the structured data requested. ` // ------------------------------------------------------------------ schemas const RECON = { type: 'object', required: ['summary', 'facts'], properties: { summary: { type: 'string' }, facts: { type: 'array', items: { type: 'string' } }, blockers: { type: 'array', items: { type: 'string' } }, }, } const SPEC = { type: 'object', required: ['commonModule', 'poStackEdits', 'woStackEdits', 'alarmVariance', 'bedrockStatement', 'fallbackRateCalls', 'appAndPins', 'diffRules', 'notes'], properties: { commonModule: { type: 'string', description: 'the full cdk/common.py: every function (add_ddb_alarms + _DDB_ALARM_OPERATIONS, add_sender_auth_rejected_alarm, add_standard_lambda_alarms, make_bedrock_invoke_statement, make_email_bucket, make_processor_dlq, make_fallback_rate_alarm) with its EXACT signature, and the exact imports it needs (no stack-specific kms/ssm/event_sources per constraint 5)' }, poStackEdits: { type: 'string', description: 'cdk/po_stack.py: every inline block replaced by a common.* call, file:line, with the exact scope + construct-id + kwargs each call passes so the emitted resource is byte-identical; plus the PO CfnOutput additions (function ARNs + consumed table names)' }, woStackEdits: { type: 'string', description: 'cdk/wo_stack.py: same — call-site rewrites file:line preserving construct ids, plus WO CfnOutput additions; explicitly note wo web_ui gets NO alarms (constraint 3)' }, alarmVariance: { type: 'string', description: 'the per-function alarm-variance table proving each helper call reproduces exactly what the inline code emits today: PO p99 vs wo-email-processor p95, po-web-ui throttles+duration only, site_extractor no-DLQ, wo web_ui ZERO alarms, every bespoke description string mapped verbatim' }, bedrockStatement: { type: 'string', description: 'make_bedrock_invoke_statement: the exact actions + resources, showing how the inference-profile ARN and per-region FM ARNs are derived from Stack.of(scope).account/.region, and PROVING the derived strings resolve in-account to the SAME ARNs the two inline PolicyStatements hardcode today' }, fallbackRateCalls: { type: 'string', description: 'the make_fallback_rate_alarm call params for PO (rejected_included=False) and WO (rejected_included=True) reproducing the expression/FILL/label strings byte-for-byte; PLUS how the two DISTINCT rejected alarms stay distinct call sites (PO 6h count-floor IF(FILL(rej,0)>=1,...); WO 5-min/2-of-6 sparse) — NOT folded into make_fallback_rate_alarm, NO element-wise MAX' }, appAndPins: { type: 'string', description: 'app.py account= additions to both cdk.Environment calls; the exact constructs== pin (installed version); the stale "2.259.0" comment fix locations + new text; confirmation none perturbs a logical ID' }, diffRules: { type: 'string', description: 'exactly how Verify proves zero cdk diff: synth BASE and HEAD into separate temp dirs, diff the two stacks templates, ANY resource/logical-ID/property delta = FAIL, the ONLY allowed additions are the net-new CfnOutputs' }, notes: { type: 'string' }, }, } const IMPL = { type: 'object', required: ['filesChanged', 'summary', 'checksRun'], properties: { filesChanged: { type: 'array', items: { type: 'string' } }, summary: { type: 'string' }, checksRun: { type: 'string' }, blockers: { type: 'array', items: { type: 'string' } }, }, } const CHECKS = { type: 'object', required: ['passed', 'details'], properties: { passed: { type: 'boolean' }, details: { type: 'string' }, scopeViolations: { type: 'array', items: { type: 'string' } }, }, } const DIFF = { type: 'object', required: ['passed', 'poDiffVerdict', 'woDiffVerdict', 'details'], properties: { passed: { type: 'boolean' }, poDiffVerdict: { type: 'string', description: 'po-ingest BASE-synth vs HEAD-synth: ZERO resource/logical-ID/property changes (CfnOutputs the only allowed net-new additions) — full template-diff evidence' }, woDiffVerdict: { type: 'string', description: 'workorder-ingest (artifact id, NOT WorkorderIngestStack): same zero-change evidence' }, details: { type: 'string' }, }, } const FINDINGS = { type: 'object', required: ['findings'], properties: { findings: { type: 'array', items: { type: 'object', required: ['title', 'severity', 'confirmed', 'evidence', 'fix'], properties: { title: { type: 'string' }, severity: { enum: ['critical', 'high', 'medium', 'low'] }, confirmed: { type: 'boolean' }, evidence: { type: 'string' }, fix: { type: 'string' }, }, }, }, }, } // ------------------------------------------------------------------- setup phase('Setup') const setup = await agent(` In ${REPO}: 1. SEQUENCING GATE — Phases 0, 1, 2 AND 3 must all be on ${BASE} (Phase 4 dedups BOTH cdk stacks, which Phases 1 (po_stack alarms), 2 (bundling roots) and 3 (shared cp) all edited — building against a pre-Phase-3 stack file guarantees a conflict and a wrong diff baseline). git fetch origin, then pick the base ref: origin/${BASE} if that remote ref exists, otherwise the local branch ${BASE} (a stacked local-only base is expected and fine). Verify on the base ref: (a) Phase 3: lambdas/shared/ exists (git ls-tree -- lambdas/shared | head); (b) Phase 2: BOTH cdk/po_stack.py and cdk/wo_stack.py contain Code.from_asset("../lambdas") for the email processors (git show :cdk/po_stack.py | grep -n '\\.\\./lambdas', same for wo_stack.py); (c) Phase 1: the po-email-processor-ai-fallback-rejected alarm / EmailProcessorAiFallbackRejectedAlarm construct exists in po_stack.py (git show :cdk/po_stack.py | grep -n 'ai-fallback-rejected\\|AiFallbackRejected'). If Phase 3 is not on ${BASE}, STOP with a blocker (Phase 4 needs the post-Phase-3 stack files as its zero-diff baseline). If any other phase is missing, STOP with a blocker naming the unmet phase and do nothing else. 2. Verify clean working tree (untracked .coverage / .claude/ / the local lambdas/po/email_processor/package/ dir are fine; any OTHER dirt = blocker, never stash or discard). 3. git checkout ${BASE}; then git pull --ff-only ONLY if the branch has an upstream (a local-only base skips the pull — not a blocker); then git checkout -b ${BRANCH} 4. gh pr list --state open --json number,title,headRefName (overlap check). Return facts: HEAD sha, per-phase gate evidence, open PRs, blockers. `, { label: 'setup:branch', model: 'haiku', schema: RECON }) if (!setup || (setup.blockers && setup.blockers.length)) { return { aborted: 'setup blockers', blockers: setup ? setup.blockers : ['setup agent died'], facts: setup ? setup.facts : [] } } log(`Branch ${BRANCH} ready off ${BASE}. ${setup.summary}`) // ------------------------------------------------------------------- recon phase('Recon') const recon = await parallel([ () => agent(`${PREAMBLE} Read-only recon of the ~379 duplicated CDK lines and the PER-FUNCTION ALARM VARIANCE that MUST survive the dedup (constraint 3). In cdk/po_stack.py and cdk/wo_stack.py, quote verbatim with file:line: 1. _DDB_ALARM_OPERATIONS + add_ddb_alarms (both copies — are they byte-identical? diff them). 2. add_sender_auth_rejected_alarm (both copies). 3. Every add_standard_lambda_alarms-shaped block: for EACH function (po email-processor, wo email-processor, po web_ui, wo web_ui, po site_extractor) list the exact alarm set, the duration statistic (prove PO email p99 vs wo email p95), whether throttles/errors/dlq alarms are present, and QUOTE every bespoke alarm description string. CRITICALLY: confirm wo web_ui has ZERO alarms today. 4. make_email_bucket / make_processor_dlq shaped blocks (both copies), and which functions get a DLQ (site_extractor has none). 5. The exact construct ids (2nd positional arg to every alarm / bucket / dlq / statement construct) — these are the logical-ID roots that must be passed UNCHANGED into the shared helpers. 30-40 precise facts. Any block that is NOT actually identical between stacks (genuine drift) is a blocker to report, not to silently reconcile.`, { label: 'recon:duplicated-cdk', model: 'sonnet', phase: 'Recon', schema: RECON }), () => agent(`${PREAMBLE} Read-only recon of the two inline Bedrock IAM PolicyStatements (the make_bedrock_invoke_statement source — constraint 2, the cross-family-review surface). In both stacks quote verbatim with file:line: the full iam.PolicyStatement (effect, actions, resources, conditions). For EACH resource ARN record whether the account (328440206208) and region are hardcoded or referenced, and enumerate every inference-profile ARN and every per-region foundation-model ARN. Determine the EXACT list of regions / model ids baked into the resources so the derived form (Stack.of(scope).account / .region) can be proven to resolve to the identical strings in-account. Note which principal/role each statement is attached to and how (add_to_role_policy vs inline policy). 15-25 facts.`, { label: 'recon:bedrock-iam', model: 'sonnet', phase: 'Recon', schema: RECON }), () => agent(`${PREAMBLE} Read-only recon of the fallback-rate + rejected alarm math AS IT STANDS POST-PHASE-1 (constraint 4). In both stacks quote verbatim with file:line: 1. PO's template-fallback-rate alarm: the full metric-math expression string(s), every FILL(), every label, the period/threshold/ evaluation_periods/datapoints_to_alarm — and CONFIRM it EXCLUDES the rejected series today (rejected_included=False). 2. WO's template-fallback-rate alarm: same, and CONFIRM it INCLUDES the rejected series (rejected_included=True). 3. PO's ai-fallback-rejected alarm (added in Phase 1): confirm it is the 6h count-floor IF(FILL(rej,0)>=1,...) shape — quote the expression. 4. WO's rejected alarm: confirm it is the 5-min / 2-of-6 sparse idiom — quote it. These two are DIFFERENT shapes and must stay distinct call sites, NOT folded into make_fallback_rate_alarm. 5. Confirm NO element-wise MAX exists anywhere in either expression (post-#102 rule). Return the exact parameter values each make_fallback_rate_alarm call must carry so Verify can prove byte-identity. 15-25 facts.`, { label: 'recon:fallback-alarms', model: 'sonnet', phase: 'Recon', schema: RECON }), () => agent(`${PREAMBLE} Read-only recon of the app.py / pins / outputs surface (constraint 6): 1. cdk/app.py: quote both cdk.Environment(...) calls verbatim with line numbers (region-only today; account must be added). 2. The constructs dependency pin: quote cdk/requirements.txt line(s) and find the stale "2.259.0" version comment(s) wherever they live (app.py, stacks, requirements — grep the whole cdk/ tree) with file:line and the actual installed constructs / aws-cdk-lib versions. 3. The five Lambda function construct variables in the stacks whose ARNs need CfnOutputs (po email-processor, po web_ui, po site_extractor, wo email-processor, wo web_ui) and the table constructs whose names are consumed (purchase-orders, WorkOrders, and any comments table) — quote the construct handles + ids so the CfnOutputs reference them correctly. 4. Any existing CfnOutput in either stack (WO reportedly has zero). 5. Confirm the wo artifact id is 'workorder-ingest' (the 2nd positional arg to the Stack constructor in app.py), distinct from stack_name='WorkorderIngestStack' (constraint 9). 15-25 facts.`, { label: 'recon:app-pins-outputs', model: 'haiku', phase: 'Recon', schema: RECON }), ]) const reconOk = recon.filter(Boolean) const pack = reconOk.map(r => `## ${r.summary}\n${r.facts.join('\n')}`).join('\n\n') const reconBlockers = reconOk.flatMap(r => r.blockers || []) .filter(b => b && !/^\s*(none|n\/a)\b/i.test(b)) log(`Recon complete: ${reconOk.length}/4 mappers, ${reconBlockers.length} advisory notes`) // Recon "blockers" for this phase are advisory design-notes / intended // constraint-2 & 6 work items (derive the Bedrock ARN from Stack.of(scope), // add account= to the environments, exact-pin constructs, fix the stale // aws-cdk-lib version comment, decide the one common sender-auth docstring), // NOT stop conditions — main-loop verified. The real gate is the ZERO cdk diff // acceptance test in Verify. Fold the notes into the spec context instead of // aborting so the spec agent must address each. const reconAdvisories = reconBlockers.length ? `\n\nRECON ADVISORIES (recon flagged these; they are the intended constraint-2/6 work + a docstring choice, NOT drift that blocks dedup — resolve each per the constraints; the zero-cdk-diff test is the real acceptance gate):\n- ${reconBlockers.join('\n- ')}` : '' // -------------------------------------------------------------------- spec phase('Spec') const spec = await agent(`${PREAMBLE} You are the SPEC agent — the single authority that pins every contested decision BEFORE parallel implementation (parallel leaves cannot see each other's choices). Using the recon pack below plus your own reads of the actual files, produce the binding implementation spec: - commonModule: the full cdk/common.py — every function per constraint 2 with its EXACT signature (add_standard_lambda_alarms keyword-only params exactly as the doc pins them), and ONLY the imports the helpers need (constraint 5 — no kms/ssm/event_sources). Every function is a PLAIN function taking (scope, id, ...) — NO Construct subclass anywhere (constraint 1). - poStackEdits / woStackEdits: for each stack, the exact call-site rewrites (file:line) mapping each inline block to a common.* call, passing the SAME scope (the Stack) and the SAME construct id it uses today so every logical ID is byte-stable; plus the CfnOutput additions (five function ARNs split across the two stacks + consumed table names). State explicitly that wo web_ui receives NO alarm call (constraint 3). - alarmVariance: the per-function variance table (constraint 3) proving each helper call reproduces today's emitted alarms EXACTLY — PO p99 vs wo-email p95, po-web-ui throttles+duration only, site_extractor no-DLQ, wo web_ui zero alarms, every bespoke description string mapped verbatim. - bedrockStatement: make_bedrock_invoke_statement's actions + resources and the derivation of the inference-profile / per-region FM ARNs from Stack.of(scope).account/.region, with a proof table showing each derived string equals the inline hardcoded ARN in-account (this is the cross-family-review surface — be exhaustive). - fallbackRateCalls: the make_fallback_rate_alarm call params for PO (rejected_included=False) and WO (rejected_included=True) reproducing the expression/FILL/label strings byte-for-byte, PLUS the plan for keeping the two DISTINCT rejected alarms as separate call sites (PO 6h count-floor, WO 5-min/2-of-6) — NOT folded, NO element-wise MAX (constraint 4). - appAndPins: app.py account= on both Environment calls; the exact constructs== pin; the stale "2.259.0" comment fix (file:line + new text); confirmation each is logical-ID-neutral. - diffRules: exactly how Verify proves ZERO cdk diff — synth ${BASE} and HEAD each into a separate temp dir, diff both stacks' templates, ANY resource/logical-ID/property delta = FAIL, the ONLY allowed net-new is the CfnOutputs; drive synth/diff by artifact id (po-ingest / workorder-ingest, constraint 9). Recon pack:\n${pack}${reconAdvisories}`, { label: 'spec:pin-common', phase: 'Spec', schema: SPEC }) if (!spec) return { aborted: 'spec agent died — rerun workflow', reconBlockers } const specBlock = `BINDING SPEC (from the spec agent — implement EXACTLY this):\n${JSON.stringify(spec, null, 2)}` log('Spec pinned: common.py contents, per-stack rewrites, alarm variance, Bedrock IAM, fallback-rate calls, app/pins/outputs') // --------------------------------------------------------------- implement phase('Implement') const impl = await parallel([ () => agent(`${PREAMBLE} YOU OWN: cdk/common.py ONLY (create it). Do not touch po_stack.py, wo_stack.py, app.py, requirements.txt, README, tests, or anything under lambdas/. Task: author cdk/common.py per spec.commonModule EXACTLY — every function (add_ddb_alarms + _DDB_ALARM_OPERATIONS, add_sender_auth_rejected_alarm, add_standard_lambda_alarms with the pinned keyword-only signature, make_bedrock_invoke_statement deriving ARNs from Stack.of(scope).account/ .region, make_email_bucket, make_processor_dlq, make_fallback_rate_alarm) as a PLAIN function taking (scope, id, ...) — NEVER a Construct subclass (constraint 1). Import ONLY what the helpers need — no kms/ssm/ event_sources (constraint 5). Match the fallback-rate expression/FILL/label strings byte-for-byte (constraint 4). Do NOT put the two rejected alarms in make_fallback_rate_alarm. Run before returning: ruff check cdk/common.py && ruff format cdk/common.py --check, plus a py_compile import smoke (python3 -c "import common" from cdk/ with the CDK venv). Full synth is the stacks agent's job — but if you can import common cleanly, report it. ${specBlock}`, { label: 'impl:common-module', model: 'opus', phase: 'Implement', schema: IMPL }), () => agent(`${PREAMBLE} YOU OWN: cdk/po_stack.py and cdk/wo_stack.py ONLY. Do not touch cdk/common.py (another agent authors it), app.py, requirements.txt, README, or lambdas/. Task: apply spec.poStackEdits + spec.woStackEdits — replace each inline block with the matching common.* call, passing the SAME scope (the Stack instance, NOT a new Construct) and the SAME construct id used today so every logical ID is byte-stable (constraint 1). Preserve every per-function alarm variance (constraint 3): PO email p99 / wo email p95, po-web-ui throttles+ duration only, site_extractor no-DLQ, wo web_ui gets NO alarm call, bespoke descriptions passed verbatim. Wire the fallback-rate calls per spec.fallbackRateCalls (PO rejected_included=False, WO True) and keep the two rejected alarms as distinct call sites (constraint 4). Add the CfnOutputs per spec (function ARNs + consumed table names) — additive, ID-safe. Import from common (bare 'import common' / 'from common import ...' — cdk/ is on sys.path via app.py's imports). Touch nothing else (tables, KMS, SSM, event sources, the RETAIN policies stay byte-identical). Run before returning: ruff check cdk && cd cdk && npx cdk synth po-ingest -q -o /tmp/phase4-synth && npx cdk synth workorder-ingest -q -o /tmp/phase4-synth (artifact-id selectors, NOT stack_name — constraint 9). If cdk/common.py has not landed yet the synth fails on the missing import — poll by re-running up to ~10 min before reporting a blocker. Confirm both stacks synth and note that the ONLY template delta vs ${BASE} is the net-new CfnOutputs (spot-check a couple of alarm logical IDs are unchanged). ${specBlock}`, { label: 'impl:cdk-stacks', model: 'opus', phase: 'Implement', schema: IMPL }), () => agent(`${PREAMBLE} YOU OWN: cdk/app.py, cdk/requirements.txt and README.md ONLY. Do not touch common.py, the stacks, tests, or lambdas/. Task A: apply spec.appAndPins — add account='328440206208' to BOTH cdk.Environment calls in app.py, pin constructs== to the exact installed version in cdk/requirements.txt, and fix the stale "2.259.0" comment(s) at the file:line spec.appAndPins gives (only those in files you own — if a stale comment lives in a stack file, note it for the stacks agent, do NOT edit their file). Every edit here must be logical-ID-neutral (account on Environment does not change resource logical IDs; verify in your summary). Task B: README — document cdk/common.py in the CDK/architecture section (the shared plain-function helpers, the logical-ID-safety rule, the account/region-derived Bedrock ARN, the new CfnOutputs), and note the account is now explicit on both stacks. Match existing README style. If the README documents the stacks' resource inventory, keep it accurate. Run before returning: ruff check cdk (app.py) and a py_compile of app.py; you cannot run the full synth without the stacks agent's edits — if you want to smoke-test, poll cd cdk && npx cdk synth po-ingest -q up to ~10 min, but a clean app.py parse is sufficient for your scope. ${specBlock}`, { label: 'impl:app-pins-readme', model: 'sonnet', phase: 'Implement', schema: IMPL }), ]) const implOk = impl.filter(Boolean) const implBlockers = implOk.flatMap(r => r.blockers || []) log(`Implement complete: ${implOk.length}/3 agents, blockers: ${implBlockers.length}`) // ---------------------------------------------------- verify + fix loop const EXPECTED_SCOPE = [ 'cdk/common.py', 'cdk/po_stack.py', 'cdk/wo_stack.py', 'cdk/app.py', 'cdk/requirements.txt', 'README.md', 'tests/', ] const mechanicalPrompt = `${PREAMBLE} Independent re-verification — trust nothing self-reported. Run ALL gates, quoting failures verbatim: 1. pytest -q --no-cov (repo root — all suites green; a synth-only cdk-diff test may now exist under tests/) 2. ruff check . && ruff format --check . 3. cd cdk && npx cdk synth po-ingest -q && npx cdk synth workorder-ingest -q (artifact-id selectors, NOT stack_name — constraint 9) 4. ZERO-CODE invariant (constraint 7): git diff ${BASE}...HEAD -- lambdas/ must output NOTHING. 5. NO CONSTRUCT SUBCLASS (constraint 1): grep cdk/common.py for 'class .*Construct' / 'class .*(Construct)' — there must be NONE; every extracted symbol is a plain 'def'. Report any subclass as a hard FAIL. 6. cdk/common.py imports NO kms/ssm/event_sources (constraint 5) — grep and confirm. 7. Both cdk.Environment calls in app.py carry account='328440206208'; constructs== is an exact pin (not >=); no "2.259.0" stale comment remains (grep the cdk/ tree). 8. CfnOutputs: five function ARNs + the consumed table names are present across the two stacks (grep CfnOutput). 9. git status --porcelain scope check: every modified/added path under ${EXPECTED_SCOPE.join(', ')} (untracked .coverage/.claude/package/ tolerated). No lambdas/ or non-cdk-diff tests/ change. passed=true only if all green. YOU MAY NOT edit files.` const diffPrompt = `${PREAMBLE} You are the ZERO-CDK-DIFF verifier — the load-bearing gate of this phase (the doc names it the acceptance test). Everything is local synth-vs-synth. 1. From a clean worktree state, synth the BASE templates: check out (via git worktree add or git stash-free 'git show'-based synth — prefer 'git worktree add /tmp/phase4-base ${BASE}' so HEAD is untouched), then in that BASE tree cd cdk && npx cdk synth po-ingest -q -o /tmp/phase4-diff-base && npx cdk synth workorder-ingest -q -o /tmp/phase4-diff-base. 2. Synth the HEAD templates: in the working tree cd cdk && npx cdk synth po-ingest -q -o /tmp/phase4-diff-head && npx cdk synth workorder-ingest -q -o /tmp/phase4-diff-head. (Both by ARTIFACT ID, constraint 9.) 3. Diff the two CloudFormation templates per stack (/tmp/phase4-diff-base/.template.json vs /tmp/phase4-diff-head/.template.json). Normalize only CDK-tooling noise (the CDKMetadata Analytics string, asset-hash-derived S3Key values that were ALSO equal on BASE). Then judge: - ZERO logical-ID changes (no renamed/removed/added Resources except the net-new CfnOutputs). - ZERO alarm property deltas (thresholds, statistics p99/p95, expression strings, FILL, labels, evaluation_periods, datapoints_to_alarm). - ZERO IAM deltas: the Bedrock PolicyStatement actions/resources must be byte-identical after the account/region derivation resolves in-account. - ZERO DynamoDB table / bucket / DLQ / env / runtime deltas. - The ONLY allowed net-new is the Outputs block (the five function ARNs + consumed table names). ANY delta outside that allowance = FAIL. Paste the actual per-stack template diff (or 'identical' with the normalized-noise list). 4. Cross-check with the live tool: cd cdk && npx cdk diff po-ingest ; npx cdk diff workorder-ingest against the deployed state must also show only the CfnOutput additions (network permitting; if AWS creds are read-only-absent, the BASE-vs-HEAD template diff in steps 1-3 is authoritative). 5. Clean up any /tmp worktrees you created (git worktree remove). passed=true only if BOTH stacks show zero resource change beyond the CfnOutput additions.` const lenses = [ { key: 'logical-id-safety', prompt: `${PREAMBLE} ADVERSARIAL REVIEW — logical-ID-safety lens (the load-bearing rule, constraint 1). Try to prove a construct-id or tree-shape change slipped in. (1) Read cdk/common.py: is EVERY extracted symbol a plain 'def' taking (scope, id, ...)? Any 'class X(Construct)' / Construct subclass / nested Construct is an automatic CRITICAL — a subclass reparents the tree and would attempt REPLACEMENT of the RETAIN-protected purchase-orders / WorkOrders tables and the named buckets. (2) For every rewritten call site in both stacks, prove the scope passed is the Stack instance (self), NOT a new intermediate construct, and the construct id string is IDENTICAL to the BASE inline id (git diff ${BASE} the id strings). (3) Synth BASE and HEAD and diff the full Resources logical-ID SET — it must be identical (only Outputs added). Any renamed/removed logical ID = confirmed CRITICAL. (4) Confirm the RETAIN removal policies on the tables and the bucket names/policies are byte-identical post-refactor. confirmed=true only with a concrete logical-ID delta or a subclass-smell file:line.` }, { key: 'alarm-variance', prompt: `${PREAMBLE} ADVERSARIAL REVIEW — alarm-variance lens (constraint 3). The shared helpers must NOT homogenize the deliberate per-function differences. Read cdk/common.py + every helper call site + the synthesized templates' alarms. Prove each of these survived EXACTLY: (1) PO email-processor duration alarm uses p99, wo-email-processor uses p95 — quote both from the synthesized templates. (2) po-web-ui has ONLY throttles+duration alarms (no errors/dlq beyond what it had). (3) site_extractor has NO DLQ alarm. (4) wo web_ui has ZERO alarms — prove the shared helper did NOT silently add any (search the wo template for any alarm whose dimensions point at the wo web_ui function). (5) Every bespoke alarm description string is byte-identical to BASE (diff the AlarmDescription fields). (6) Fallback-rate: PO excludes the rejected series (rejected_included=False), WO includes it; the two rejected alarms kept their distinct shapes (PO 6h count-floor, WO 5-min/2-of-6); NO element-wise MAX anywhere. confirmed=true only with a template-level diff showing a homogenized or dropped alarm.` }, { key: 'iam-equivalence', prompt: `${PREAMBLE} ADVERSARIAL REVIEW — IAM-equivalence lens (the cross-family-review surface). make_bedrock_invoke_statement moved the PolicyStatement construction and swapped hardcoded 328440206208 for Stack.of(scope).account/.region. Prove the produced IAM is IDENTICAL. (1) Synth both stacks and extract the Bedrock PolicyStatement from each template; diff actions and resources against ${BASE}'s synthesized statements — the resolved ARN strings (account + region substituted) must be byte-identical in-account. (2) Confirm the resources still enumerate the SAME inference-profile ARN and the SAME per-region foundation-model ARNs (no region dropped/added, no wildcard broadening). (3) Confirm effect/conditions/principal attachment unchanged and the statement is attached to the SAME role. (4) Flag any broadening (e.g. a Ref/Sub that resolves to a wildcard, or Stack.region producing a different region than the hardcoded one) as confirmed HIGH. confirmed=true only with the two synthesized statements diffed.` }, ] let round = 0 let checks = null let diff = null let confirmed = [] while (round < 3) { phase('Verify') const results = await parallel([ () => agent(mechanicalPrompt, { label: `verify:mechanical-r${round}`, model: 'sonnet', phase: 'Verify', schema: CHECKS }), () => agent(diffPrompt, { label: `verify:cdk-diff-r${round}`, model: 'opus', phase: 'Verify', schema: DIFF }), ...lenses.map(l => () => agent(l.prompt, { label: `verify:${l.key}-r${round}`, phase: 'Verify', schema: FINDINGS })), ]) checks = results[0] diff = results[1] confirmed = results.slice(2).filter(Boolean) .flatMap(r => r.findings || []) .filter(f => f.confirmed && f.severity !== 'low') const green = checks && checks.passed && diff && diff.passed log(`Verify round ${round}: mechanical ${checks && checks.passed ? 'GREEN' : 'RED'}, cdk-diff ${diff && diff.passed ? 'GREEN' : 'RED'}, confirmed findings: ${confirmed.length}`) if (green && confirmed.length === 0) break round += 1 if (round >= 3) break phase('Fix') await agent(`${PREAMBLE} You are the fix agent — you may edit files under: ${EXPECTED_SCOPE.join(', ')}. Fix EVERY item below minimally; the binding spec and 10 pinned constraints still hold (a finding that conflicts with a constraint is reported, not "fixed" — the constraint wins, esp. constraint 1's plain-function / no-Construct-subclass rule, constraint 3's alarm variance, and constraint 4's distinct rejected alarms / no element-wise MAX). Re-run the specific failing gate per fix (the zero-cdk-diff check is authoritative — a fix that introduces ANY logical-ID or property delta is worse than the finding). MECHANICAL:\n${checks ? checks.details : '(agent died — rerun all gates)'} CDK-DIFF:\n${diff ? diff.details : '(agent died — rerun all)'} CONFIRMED FINDINGS:\n${JSON.stringify(confirmed, null, 2)} ${specBlock}`, { label: `fix:round-${round}`, model: 'opus', phase: 'Fix', schema: IMPL }) } const verifyClean = checks && checks.passed && diff && diff.passed && confirmed.length === 0 if (!verifyClean) { return { status: 'NEEDS ATTENTION — verify not clean after 3 rounds; branch left uncommitted', branch: BRANCH, mechanical: checks, cdkDiff: diff, unresolvedFindings: confirmed, implBlockers, reconBlockers, spec, } } // ----------------------------------------------------------------- package phase('Package') const commit = await agent(`${PREAMBLE.replace('do NOT commit, ', '')} YOU are the commit agent: 1. MANDATORY GPT-4.1 CROSS-FAMILY REVIEW (the doc mandates it for the make_bedrock_invoke_statement IAM PolicyStatement move, even though semantics are identical). Capture the Bedrock-statement diff first: git diff ${BASE}...HEAD -- cdk/common.py cdk/po_stack.py cdk/wo_stack.py (isolate the make_bedrock_invoke_statement + its two call sites), then RUN inline: python3 ~/Documents/repositories/seahaven/security-review/cross_review.py "Review this CDK IAM PolicyStatement move for breaking changes: the two inline Bedrock invoke PolicyStatements (hardcoded account 328440206208) were consolidated into make_bedrock_invoke_statement in cdk/common.py, deriving the inference-profile + per-region foundation-model ARNs from Stack.of(scope).account/.region. Confirm the produced actions/resources are byte-identical in-account and no privilege broadening. " Record the verdict verbatim in your summary. If cross_review.py is unavailable, DO NOT block the local commit but flag the review as OUTSTANDING in your summary (it must be run before merge). 2. Read ~/Documents/repositories/seahaven/engineering-handbook/commit-messages.md and follow it exactly. 3. git add only paths under: ${EXPECTED_SCOPE.join(', ')} and .claude/workflows/phase-4-cdk-common.js. NOT .coverage, NOT package/, NOT cdk.out. Verify the staged set with git status. 4. ONE commit; write the message to /tmp/phase4-commit-msg.txt and use git commit -F /tmp/phase4-commit-msg.txt (backticks in -m get eaten by zsh). Suggested subject: "feat: extract cdk/common.py — dedup 379 CDK lines as logical-ID-safe plain helpers (refactor phase 4)" Body: the plain-function (scope, id, ...) approach and WHY no Construct subclass (RETAIN-table replacement), the account/region-derived Bedrock ARN, the zero-cdk-diff acceptance evidence for both stacks, the account=/constructs pin/stale-comment/CfnOutput net-new additions, and the cross_review.py verdict one-liner. NO AI attribution / Co-Authored-By lines. 5. Do NOT push. Return commit sha + shortstat + the cross_review.py verdict in summary.`, { label: 'package:commit', model: 'sonnet', phase: 'Package', schema: IMPL }) return { status: 'BUILT — committed locally, NOT pushed', branch: BRANCH, base: BASE, commit: commit ? commit.summary : 'commit agent died — commit manually', spec: { commonModule: spec.commonModule, bedrockStatement: spec.bedrockStatement, fallbackRateCalls: spec.fallbackRateCalls, appAndPins: spec.appAndPins, notes: spec.notes }, diffEvidence: diff ? { po: diff.poDiffVerdict, wo: diff.woDiffVerdict } : null, implementation: implOk.map(r => r.summary), filesChanged: implOk.flatMap(r => r.filesChanged), verifyRounds: round + 1, blockers: implBlockers.concat(reconBlockers), outstandingGates: [ 'MANDATORY cross-family GPT-4.1 review (cross_review.py) on the make_bedrock_invoke_statement IAM PolicyStatement move — the Package agent runs it inline and records the verdict; confirm that verdict is clean (or re-run) before merge. If cross_review.py was unavailable at commit time, this review is OUTSTANDING — do not merge without it.', '/sh-security-review NOT required (pure CDK refactor — no untrusted-input/auth-logic change; the pre-push scanners still run as the unattended backstop)', 'push + PR + gh pr checks green', 'deploy-then-merge with cdk diff zero-change on BOTH stacks as the live acceptance signal: deploy from branch, confirm cdk diff po-ingest / cdk diff workorder-ingest show only the CfnOutput additions, both stacks reach UPDATE_COMPLETE, all alarms still OK, THEN merge (a no-op resource redeploy is itself the verification signal). Drive synth/diff by artifact id workorder-ingest, NOT stack_name WorkorderIngestStack (constraint 9).', 'update the Confluence "AWS Architecture Map" if the new CfnOutputs / account-explicit envs change the documented resource inventory', ], }