You are a Senior Code Reviewer with expertise in Kubernetes and infrastructure-as-code. Review the init Job just committed.

## What Was Implemented

Task 2 of 6 for `churn-renewal-forecast` Kustomize deployment. Created a one-shot Kubernetes Job for `bunx prisma migrate deploy` with ttlSecondsAfterFinished, backoffLimit, envFrom references to ConfigMap and Secret.

## Requirements / Plan

- job.yaml: batch/v1 Job, name churn-renewal-forecast-migrate, namespace churn-renewal-forecast, labels (name + component: migrate), ttlSecondsAfterFinished: 300, backoffLimit: 3, restartPolicy: OnFailure, serviceAccountName: churn-renewal-forecast, image registry.ic-ops.net/churn-renewal-forecast:latest, command ["bunx", "prisma", "migrate", "deploy"], envFrom configMap + secret
- kustomization.yaml: resources: [job.yaml]

## Git Range to Review

**Base:** d38ec0affc66f147bcd43dc10223f2df6f96f5eb
**Head:** 8ae4a6ef409140f1a0d3248dba181e9a57171875

```bash
git diff --stat d38ec0affc66f147bcd43dc10223f2df6f96f5eb..8ae4a6ef409140f1a0d3248dba181e9a57171875
git diff d38ec0affc66f147bcd43dc10223f2df6f96f5eb..8ae4a6ef409140f1a0d3248dba181e9a57171875
```

Work from: `/Users/steve.briggs/projects/churn-renewal-forecast`

## Output Format

### Strengths
### Issues
#### Critical (Must Fix)
#### Important (Should Fix)
#### Minor (Nice to Have)
### Assessment
**Ready to merge?** [Yes | No | With fixes]
**Reasoning:** [1-2 sentences]


- obsidian-load: Load context from Obsidian notes in the `Claude/` directory into the current conversation.
- obsidian-save: Save current conversation context to an Obsidian note under the `Claude/` directory.
- mind:stats: Show memory statistics and storage information
- mind:search: Search memories for specific content or patterns
- mind:ask: Ask questions about memories and get context-aware answers
- mind:recent: Show recent memories and activity timeline
- mind:memory: Claude Mind - Search and manage Claude's persistent memory stored in a single portable .mv2 file
- mind:mind: Claude Mind - Search and manage Claude's persistent memory stored in a single portable .mv2 file
- superpowers:test-driven-development: Use when implementing any feature or bugfix, before writing implementation code
- superpowers:using-git-worktrees: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback
- superpowers:receiving-code-review: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
- superpowers:using-superpowers: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
- superpowers:writing-plans: Use when you have a spec or requirements for a multi-step task, before touching code
- superpowers:subagent-driven-development: Use when executing implementation plans with independent tasks in the current session
- superpowers:requesting-code-review: Use when completing tasks, implementing major features, or before merging to verify work meets requirements
- superpowers:verification-before-completion: Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
- superpowers:executing-plans: Use when you have a written implementation plan to execute in a separate session with review checkpoints
- superpowers:finishing-a-development-branch: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
- superpowers:dispatching-parallel-agents: Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies
- superpowers:writing-skills: Use when creating new skills, editing existing skills, or verifying skills work before deployment
- superpowers:brainstorming: You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation.
- superpowers:systematic-debugging: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled remote agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching. Also handles migrating existing Claude API code between Claude model versions (4.5 → 4.6, 4.6 → 4.7, retired-model replacements).
TRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.
SKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a pull request
- security-review: Complete a security review of the pending changes on the current branch


I'll review the changes in the specified git range to assess the Kubernetes Job implementation.


 deploy/kubernetes/base/init/job.yaml           | 28 ++++++++++++++++++++++++++
 deploy/kubernetes/base/init/kustomization.yaml |  5 +++++
 2 files changed, 33 insertions(+)


Now let me view the actual changes:


diff --git a/deploy/kubernetes/base/init/job.yaml b/deploy/kubernetes/base/init/job.yaml
new file mode 100644
index 00000000..78c0026a
--- /dev/null
+++ b/deploy/kubernetes/base/init/job.yaml
@@ -0,0 +1,28 @@
+apiVersion: batch/v1
+kind: Job
+metadata:
+  name: churn-renewal-forecast-migrate
+  namespace: churn-renewal-forecast
+  labels:
+    app.kubernetes.io/name: churn-renewal-forecast
+    app.kubernetes.io/component: migrate
+spec:
+  ttlSecondsAfterFinished: 300
+  backoffLimit: 3
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/name: churn-renewal-forecast
+        app.kubernetes.io/component: migrate
+    spec:
+      serviceAccountName: churn-renewal-forecast
+      restartPolicy: OnFailure
+      containers:
+        - name: migrate
+          image: registry.ic-ops.net/churn-renewal-forecast:latest
+          command: ["bunx", "prisma", "migrate", "deploy"]
+          envFrom:
+            - configMapRef:
+                name: churn-renewal-forecast-config
+            - secretRef:
+                name: churn-renewal-forecast
diff --git a/deploy/kubernetes/base/init/kustomization.yaml b/deploy/kubernetes/base/init/kustomization.yaml
new file mode 100644
index 00000000..3e6455ec
--- /dev/null
+++ b/deploy/kubernetes/base/init/kustomization.yaml
@@ -0,0 +1,5 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+  - job.yaml


Let me check the broader context of the Kubernetes structure and verify there are no existing init jobs:


Bash

find /Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes -type f -name "*.yaml" | head -20


Let me check if there's a kustomization.yaml at the base level that should include the init job:


Error: EISDIR: illegal operation on a directory, read '/Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes/base'


drwxr-xr-x steve.briggs staff  96 B Mon May 18 17:02:22 2026 ..
drwxr-xr-x steve.briggs staff 224 B Mon May 18 17:59:09 2026 bootstrap
drwxr-xr-x steve.briggs staff 128 B Mon May 18 18:00:50 2026 .
drwxr-xr-x steve.briggs staff 128 B Mon May 18 18:01:00 2026 init


find /Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes -maxdepth 2 -name "kustomization.yaml"


(Bash completed with no output)


Let me check if there's a parent kustomization that should include the init job:


find /Users/steve.briggs/projects/churn-renewal-forecast/deploy -name "kustomization.yaml"


Let me check the bootstrap kustomization to understand the structure:


1	apiVersion: kustomize.config.k8s.io/v1beta1
2	kind: Kustomization
3
4	resources:
5	  - namespace.yaml
6	  - serviceaccount.yaml
7	  - secret.yaml
8
9	configMapGenerator:
10	  - name: churn-renewal-forecast-config
11	    envs:
12	      - app-config.env
13	    options:
14	      disableNameSuffixHash: true
15


Let me check the bootstrap serviceaccount to understand the RBAC context:


1	apiVersion: v1
2	kind: ServiceAccount
3	metadata:
4	  name: churn-renewal-forecast
5	  namespace: churn-renewal-forecast
6	  labels:
7	    app.kubernetes.io/name: churn-renewal-forecast
8


Let me check the namespace to understand the overall structure:


1	apiVersion: v1
2	kind: Namespace
3	metadata:
4	  name: churn-renewal-forecast
5	  labels:
6	    app.kubernetes.io/name: churn-renewal-forecast
7


Now let me check if there's a parent-level orchestration kustomization that should include both bootstrap and init:


find /Users/steve.briggs/projects/churn-renewal-forecast/deploy -type f -name "*.yaml" -o -name "*.yml" | sort


Let me check the full directory structure to see if there's an integration point:


tree -L 3 /Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes 2>/dev/null || find /Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes -type d


Let me verify the image reference exists and check the broader project context:


grep -r "registry.ic-ops.net/churn-renewal-forecast" /Users/steve.briggs/projects/churn-renewal-forecast/deploy 2>/dev/null || echo "No matches found"


/Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes/base/init/job.yaml:          image: registry.ic-ops.net/churn-renewal-forecast:latest


Let me check if the ConfigMap and Secret names match what's defined in bootstrap:


1	apiVersion: external-secrets.io/v1beta1
2	kind: ExternalSecret
3	metadata:
4	  name: churn-renewal-forecast
5	  namespace: churn-renewal-forecast
6	  labels:
7	    app.kubernetes.io/name: churn-renewal-forecast
8	spec:
9	  refreshInterval: 1h
10	  secretStoreRef:
11	    name: vault
12	    kind: ClusterSecretStore
13	  target:
14	    name: churn-renewal-forecast
15	    creationPolicy: Owner
16	  data:
17	    - secretKey: DATABASE_URL
18	      remoteRef:
19	        key: /internal/churn-renewal-forecast
20	        property: DATABASE_URL
21	    - secretKey: CLERK_SECRET_KEY
22	      remoteRef:
23	        key: /internal/churn-renewal-forecast
24	        property: CLERK_SECRET_KEY
25	    - secretKey: FRONT_API_TOKEN
26	      remoteRef:
27	        key: /internal/churn-renewal-forecast
28	        property: FRONT_API_TOKEN
29	    - secretKey: FRONT_API_TOKEN_2
30	      remoteRef:
31	        key: /internal/churn-renewal-forecast
32	        property: FRONT_API_TOKEN_2
33	    - secretKey: PRODUCTBOARD_API_KEY
34	      remoteRef:
35	        key: /internal/churn-renewal-forecast
36	        property: PRODUCTBOARD_API_KEY
37	    - secretKey: SLACK_BOT_TOKEN
38	      remoteRef:
39	        key: /internal/churn-renewal-forecast
40	        property: SLACK_BOT_TOKEN
41


Now let me check if there are any docs about the deployment structure:


find /Users/steve.briggs/projects/churn-renewal-forecast/docs -name "*deploy*" -o -name "*kubernetes*" 2>/dev/null | head -10


Let me check the spec to understand the requirements:


1	# Kustomize Deployment Design
2
3	**Date:** 2026-05-18
4	**Project:** churn-renewal-forecast
5	**Status:** Approved
6
7	## Overview
8
9	Add a Kustomize-based Kubernetes deployment under `deploy/kubernetes/`, modelled after the `saas-tenant-manager` pattern. The app has two runtime components (Next.js web server and pg-boss background worker) that share a single container image with different command overrides. A one-shot init Job handles Prisma migrations before pods come up.
10
11	## Directory Structure
12
13	```
14	deploy/kubernetes/
15	├── base/
16	│   ├── kustomization.yaml
17	│   ├── bootstrap/
18	│   │   ├── kustomization.yaml
19	│   │   ├── namespace.yaml
20	│   │   ├── serviceaccount.yaml
21	│   │   ├── secret.yaml           # ExternalSecret pulling from Vault
22	│   │   └── app-config.env        # ConfigMap values
23	│   ├── init/
24	│   │   ├── kustomization.yaml
25	│   │   └── job.yaml              # Prisma migrate deploy
26	│   ├── netpol/
27	│   │   ├── kustomization.yaml
28	│   │   ├── allow-dns.yaml
29	│   │   ├── allow-rds.yaml
30	│   │   └── allow-external.yaml
31	│   └── services/
32	│       ├── kustomization.yaml
33	│       ├── web-deployment.yaml
34	│       ├── worker-deployment.yaml
35	│       ├── service.yaml
36	│       └── ingress.yaml
37	└── deploy/
38	    └── eks-shared-infra-use1-2/
39	        ├── kustomization.yaml
40	        ├── .app.yaml
41	        ├── ingress-patch.yaml
42	        └── serviceaccount.yaml
43	```
44
45	## Components
46
47	### bootstrap/
48
49	**namespace.yaml** — Creates the `churn-renewal-forecast` namespace with label `app.kubernetes.io/name: churn-renewal-forecast`.
50
51	**serviceaccount.yaml** — ServiceAccount named `churn-renewal-forecast` in the namespace. No IRSA annotation needed at base level (app does not access AWS APIs directly).
52
53	**secret.yaml** — `ExternalSecret` (external-secrets.io v1beta1) that pulls from `ClusterSecretStore` named `vault` at path `/internal/churn-renewal-forecast`. Maps the following keys to a Secret named `churn-renewal-forecast`:
54
55	| Vault key | Secret env key |
56	|---|---|
57	| `DATABASE_URL` | `DATABASE_URL` |
58	| `CLERK_SECRET_KEY` | `CLERK_SECRET_KEY` |
59	| `FRONT_API_TOKEN` | `FRONT_API_TOKEN` |
60	| `FRONT_API_TOKEN_2` | `FRONT_API_TOKEN_2` |
61	| `PRODUCTBOARD_API_KEY` | `PRODUCTBOARD_API_KEY` |
62	| `SLACK_BOT_TOKEN` | `SLACK_BOT_TOKEN` |
63
64	Refresh interval: `1h`.
65
66	**app-config.env** — ConfigMap (generated by Kustomize `configMapGenerator`) with non-sensitive config:
67
68	| Key | Value |
69	|---|---|
70	| `APP_URL` | `https://churn-forecast.infrastructure.immuta.io` |
71	| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | `PLACEHOLDER` |
72	| `NEXT_PUBLIC_PRODUCTBOARD_WORKSPACE` | `PLACEHOLDER` |
73	| `SLACK_TEST_CHANNEL` | `""` |
74	| `PG_POOL_MAX` | `10` |
75	| `PG_POOL_IDLE_TIMEOUT_MS` | `30000` |
76	| `PG_POOL_CONNECTION_TIMEOUT_MS` | `2000` |
77	| `NEXT_PUBLIC_CLERK_SIGN_IN_URL` | `/sign-in` |
78	| `NEXT_PUBLIC_CLERK_SIGN_UP_URL` | `/sign-up` |
79	| `NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL` | `/` |
80	| `NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL` | `/` |
81
82	### init/
83
84	**job.yaml** — One-shot Kubernetes Job:
85	- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
86	- Command: `["bunx", "prisma", "migrate", "deploy"]`
87	- `restartPolicy: OnFailure`
88	- `backoffLimit: 3`
89	- `envFrom` the ConfigMap and Secret
90	- Uses the `churn-renewal-forecast` ServiceAccount
91	- `ttlSecondsAfterFinished: 300` to clean up completed jobs
92
93	### netpol/
94
95	Three `CiliumNetworkPolicy` resources, all selecting pods with `app.kubernetes.io/name: churn-renewal-forecast`:
96
97	**allow-dns.yaml** — Egress to kube-dns on UDP port 53.
98
99	**allow-rds.yaml** — Egress on TCP port 5432 (PostgreSQL/RDS).
100
101	**allow-external.yaml** — Egress on TCP port 443 to:
102	- `clerk.com`
103	- `api.frontapp.com`
104	- `api.productboard.com`
105	- `slack.com`
106
107	Using `toFQDNs` matchers.
108
109	### services/
110
111	**web-deployment.yaml** — Deployment:
112	- Name: `churn-renewal-forecast-web`
113	- Replicas: `1`
114	- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
115	- Command override: `["bun", "run", "start"]` (`next start`)
116	- Port: `3000`
117	- `envFrom` ConfigMap + Secret
118	- Resources: `requests: {memory: 256Mi, cpu: 100m}`, `limits: {memory: 512Mi, cpu: 500m}`
119	- Liveness probe: HTTP GET `/api/health/live` port 3000, `initialDelaySeconds: 15`, `periodSeconds: 20`
120	- Readiness probe: HTTP GET `/api/health/ready` port 3000, `initialDelaySeconds: 10`, `periodSeconds: 10`
121	- ServiceAccount: `churn-renewal-forecast`
122
123	**worker-deployment.yaml** — Deployment:
124	- Name: `churn-renewal-forecast-worker`
125	- Replicas: `1`
126	- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
127	- Command override: `["bunx", "tsx", "src/worker/index.ts"]`
128	- No HTTP port exposed
129	- `envFrom` ConfigMap + Secret
130	- Resources: same as web
131	- No HTTP probes (queue consumer; process liveness is sufficient)
132	- ServiceAccount: `churn-renewal-forecast`
133
134	**service.yaml** — ClusterIP Service targeting `churn-renewal-forecast-web` pods on port 3000.
135
136	**ingress.yaml** — Nginx Ingress:
137	- Host: `churn-forecast.infrastructure.immuta.io`
138	- Backend: the ClusterIP service on port 3000
139	- TLS: references a secret `churn-renewal-forecast-tls` (managed externally / cert-manager)
140
141	### Overlay: deploy/eks-shared-infra-use1-2/
142
143	**kustomization.yaml** — References `../../base` and applies patches.
144
145	**.app.yaml** — Flux app metadata file with app name, namespace, and target cluster.
146
147	**ingress-patch.yaml** — Strategic merge patch adding the private visibility annotation to the base Ingress (e.g., `nginx.ingress.kubernetes.io/scheme: internal` or equivalent annotation used by the cluster).
148
149	**serviceaccount.yaml** — Patch file. IRSA annotation is commented out since no AWS API access is needed. Can be uncommented if requirements change.
150
151	## Labels
152
153	All resources use `app.kubernetes.io/name: churn-renewal-forecast` consistently. Deployments also add `app.kubernetes.io/component: web` or `app.kubernetes.io/component: worker`.
154
155	## Image Strategy
156
157	Single image `registry.ic-ops.net/churn-renewal-forecast:latest` shared across web Deployment, worker Deployment, and init Job. The image tag should be pinned to a specific digest or semver tag in CI via a Kustomize image transform in the overlay.
158
159	## Deployment Ordering
160
161	Kustomize itself does not enforce that the init Job completes before the web/worker Deployments start. Ordering is handled via Flux `Kustomization` resources with `dependsOn` — the init kustomization must complete (health check: Job succeeded) before the services kustomization is applied.
162
163	The overlay's `.app.yaml` captures the Flux app metadata for this pattern.
164
165	## Open Items
166
167	- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `NEXT_PUBLIC_PRODUCTBOARD_WORKSPACE` values need to be filled in for the target environment.
168	- TLS secret name (`churn-renewal-forecast-tls`) assumes cert-manager or equivalent; adjust if cluster uses a different mechanism.
169	- Confirm whether `FRONT_RATE_LIMIT_RPM` should be in the ConfigMap (currently omitted since the spec didn't list it; add if needed).
170	- If the app ever needs AWS API access (S3, Secrets Manager), uncomment the IRSA annotation in the overlay serviceaccount.yaml and supply the role ARN.
171
172


Now let me review the actual init job implementation more carefully and check the image/environment context:


8ae4a6ef feat: add k8s init job for prisma migrations
d38ec0af fix: use empty values for NEXT_PUBLIC placeholders, scope disableNameSuffixHash per-generator
9a27b793 feat: add k8s bootstrap resources (namespace, sa, secret, configmap)
ec746498 Merge pull request #1213 from immuta/chore/archive-plan-sync-bug-front-sub-job-gate
9a8e1136 chore: Archive plan file for Front sub-job sync toggle gate


Perfect. Now let me synthesize my review:

---

## Strengths

1. **Correct API version and Kind** — Uses `batch/v1 Job`, which is the stable GA API for Kubernetes Jobs (not beta).

2. **Accurate resource naming** — Name `churn-renewal-forecast-migrate` and namespace `churn-renewal-forecast` match the design spec exactly.

3. **Proper label structure** — Uses standard Kubernetes labels (`app.kubernetes.io/name`, `app.kubernetes.io/component: migrate`) consistent with bootstrap resources and the spec.

4. **TTL cleanup configured** — `ttlSecondsAfterFinished: 300` ensures completed/failed jobs are cleaned up after 5 minutes, reducing namespace clutter.

5. **Sensible backoff strategy** — `backoffLimit: 3` with `restartPolicy: OnFailure` provides reasonable retry behavior for transient failures without runaway restarts.

6. **Correct command** — `["bunx", "prisma", "migrate", "deploy"]` is the right choice for running migrations in a container without installing Prisma globally.

7. **Environment injection** — `envFrom` correctly references both ConfigMap (`churn-renewal-forecast-config`) and Secret (`churn-renewal-forecast`), providing DATABASE_URL and other credentials needed for migrations.

8. **ServiceAccount properly set** — References the `churn-renewal-forecast` ServiceAccount created in bootstrap, allowing RBAC controls if needed.

9. **Clean kustomization.yaml** — The init Kustomization is minimal and focused—only declares the Job resource.

## Issues

#### Critical (Must Fix)

None identified. The implementation matches the design spec completely.

#### Important (Should Fix)

1. **Missing container name metadata** — The container is named `migrate`, which is good, but there's no `imagePullPolicy: IfNotPresent` or explicit `pullPolicy`. For a `:latest` tag in production, consider adding `imagePullPolicy: Always` to ensure the latest image is always pulled, avoiding stale image cache surprises. (Minor—implicit default is `IfNotPresent`, which is usually fine for `:latest`, but explicit is safer.)

#### Minor (Nice to Have)

1. **Image tag: `:latest` in base** — The design spec notes that "The image tag should be pinned to a specific digest or semver tag in CI via a Kustomize image transform in the overlay" (section "Image Strategy"). This base Job uses `:latest`, which is correct for the base layer, but the overlay will need an `images` transform to pin it. Not an issue in this change, but ensure the overlay overlay implements this.

2. **No resource requests/limits** — Unlike the web and worker Deployments (which specify `requests: {memory: 256Mi, cpu: 100m}` and `limits: {memory: 512Mi, cpu: 500m}`), the init Job has none. For a short-lived migration job, this is often acceptable, but adding requests ensures scheduler placement and prevents OOMkill surprises. Consider adding conservative limits (e.g., `memory: 512Mi`, `cpu: 1`) or matching the Deployment limits if migrations are heavy.

3. **No activeDeadlineSeconds** — The Job lacks a timeout. If migrations hang (e.g., deadlock or infinite loop), the Job hangs indefinitely. Consider adding `activeDeadlineSeconds: 600` (10 minutes) or similar to prevent stuck Jobs blocking deployment.

## Assessment

**Ready to merge?** Yes

**Reasoning:** The Job implementation is accurate to spec and follows Kubernetes best practices. The core functionality (command, env, RBAC, cleanup) is correct. Image pinning will be handled by the overlay per the design, and resource limits can be added in a follow-up if needed. No blockers.