How I run AI agents on production software, and what that actually produces.
Agents fail in three predictable ways. They write before they understand the codebase, they report success they never verified, and they repeat a mistake next session because nothing recorded it. Shipline is a set of commands that puts a written plan, pasted evidence and a persistent lessons file in the way of each one. Plain markdown in the repo, no external service.
Read the plan, approve or send it back
Implementation, migrations, adapters
Types, tests, production build
Run the script by hand
What failed, and the rule that prevents it
Read the diff, permit the commit
Every retro has to answer one question: what rule, written down in advance, would have prevented this. The answer becomes a numbered guardrail that loads back into context before every future plan. That file is append-only, which is what turns a checklist into something that compounds.
I work as a software engineer with an agentic AI practice: from the command line I delegate backend, front end, migrations, tests, deployment configuration and documentation to sub-agents that run in parallel, while architecture, contracts and review stay with me.
Scoped by written plans, verified by type-checking, tests and a production build, and gated by human review on every merge.
What keeps it safe is the context file every session loads before touching code, and the rule that consequential decisions stay with a human. Both are written up with real diffs in the evidence below.
Agent-assisted delivery changes the economics in three places. Migrations and cleanups that were never worth scheduling become ordinary work. Tests, API notes and release notes are produced alongside the implementation instead of lagging it. And repetitive implementation stops being a reason to add headcount, so budget stays with product decisions, design and review.
That is only worth saying because of the guardrails: the 8 artifacts below are the failure modes, reverts and audits that came out of reviewing agent output on production code, which is the only reason the speed is safe to claim.
Requirements become module scope, API assumptions, edge cases and a completion contract first.
Sub-agents run in parallel from the CLI on scaffolding, refactors, API adapters and UI states.
Type-check, test suite and production build on every change. I read every diff before it merges.
Implementation brief, an API issue note when contracts disagree, and release evidence with rollback notes.
Actual diffs, commits and PR notes from delivery. Not descriptions of process. Open any of them.
This portfolio: personal site & digital garden (ariefrse.my) · 2026-07
A user report "clicking Notes shows a blank page" became a systematic debugging session: reproduce in a real browser, isolate to next/link swallowing cross-origin URLs, fix, verify the click end-to-end, and publish the failure mode as a garden note. Traceable to commit ad8fdd1.
The report: clicking the Notes item in the sidebar blanked the content area. No console error, no 404, and the nav item lit up as active. The UI looked like it worked. The debugging loop, run by an AI agent driving a real browser: 1. Reproduce on production: click intercepted, URL never changes. 2. Rule out the target: every page of the external notes site returned valid content when fetched directly. 3. Isolate: the sidebar rendered every nav item through next/link, including one cross-origin URL the App Router can't route to. The click dies silently inside the router. 4. Fix: external hrefs branch to a plain <a target="_blank">, with an isExternalHref() helper and an outbound-arrow marker. The mobile drawer also closes onClick, since no route change ever fires. 5. Verify: dev server + real click → the garden opens. Then the same click checked again on production after deploy. The fix is one commit (ad8fdd1); the failure mode became a public garden note the same day: the mistake now documents itself.
This portfolio: personal site (ariefrse.my) · 2026-07
The auto-deploy pipeline (CI-built images + Watchtower) froze silently twice: green builds, stale production. The durable fix bakes the commit SHA into the HTML and adds a CI job that polls production until it serves that exact commit, failing loudly if it never does. Commit 6276eff.
Push-to-live pipelines fail in a uniquely quiet way: the build is
green, the image is published, and production just… stays old. First
cause: Watchtower's bundled Docker client spoke API 1.25 against a
daemon requiring 1.40+, every poll errored before reaching the
registry.
The durable fix makes the loop prove itself on every push:
+ <meta name="build-commit" content={process.env.NEXT_PUBLIC_COMMIT_SHA} />
verify-deploy:
needs: build
steps:
- run: |
for i in $(seq 1 20); do
live=$(curl -sf https://ariefrse.my/ \
| grep -o 'build-commit" content="[^"]*"')
[ "$live" = "$GITHUB_SHA" ] && exit 0
sleep 30
done
exit 1 # production never picked up this commit
On its first run the job failed; correctly: Watchtower had frozen
again, and what used to be days of silently stale production became a
red X and an email within ten minutes of the push.Anchorworx Web Platform: maritime job platform (private client work) · 2026-07
Migrating a Next 14 production platform toward 16, the dangerous changes aren’t the build errors; they’re the ones that keep rendering: dynamic route params silently becoming Promises. AI agents swept every route for the invisible class before anything shipped.
The compile errors in a Next 14 → 15/16 migration fix themselves. The
build tells you. The bugs worth an agent sweep are the ones that don't
crash:
- Dynamic route params: in 14, params is a plain object; from 15 on
it's a Promise. Code reading params.id without await doesn't throw,
it reads a property off a Promise, gets undefined, and the page
quietly renders its empty/fallback state. Same for searchParams.
- fetch caching defaults flipped in 15: pages silently change between
cached and per-request behavior with zero code changes.
- cookies()/headers() went async, sync usage survives loose type
checking and only fails on the runtime paths that touch it.
The canonical fix, applied route by route:
- export default function JobPage({ params }: { params: { id: string } }) {
- const job = await getJob(params.id)
+ export default async function JobPage({ params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params
+ const job = await getJob(id)
The AI workflow: inventory before touching anything, an agent pass
listing every dynamic segment ([id], [slug]), every params/searchParams
read, and every fetch with an implicit caching assumption. Then the
mechanical rewrites in bulk, human-reviewed per diff. Then the check
that actually matters: drive each migrated route and confirm real data
renders: because on this class of bug, "it builds and the page loads"
is exactly what failure looks like.
Employer dashboards, job boards, and the candidate directory are dense
with dynamic routes: silent-undefined params would have shipped as
blank screens, not stack traces.hrmydeck: Malaysian HRMS (production pilot, built end-to-end with AI agents) · 2026-06
Ran an AI "gap-finder" audit across the API and admin-panel code paths before release. It found real money-touching bugs, and one of its fixes was reverted the same day because it violated the intended approval chain.
All of hrmydeck is built through AI agent sessions, implementation, fixes, deployment config, and docs, with human review gating every merge. The commit trail carries the AI co-author trailers to prove it. This entry is one of those sessions. Audit brief: compare every API code path against its Filament admin-panel equivalent and report behavioral gaps. Findings that shipped as fixes: - Payroll calculation parity: the API's processPayroll was missing claims reimbursement, unpaid-leave deductions, and custom deductions that the admin panel's generatePayslips applied, two code paths were computing different salaries for the same employee - API employee onboarding never sent the welcome invite the admin-panel flow sent, so API-created employees silently never got credentials - Claim routes exposed show/update/destroy endpoints that were never implemented: restricted apiResource to index+store only - Claim approver deadlock: no approval possible when no named approvers were configured in env The deadlock "fix" (fall back to any MANAGER-role user as approver) was technically sound and organizationally wrong, approvers must be explicitly configured, not inferred from a role. Human review caught it and it was reverted the same day: revert: remove MANAGER role fallback in claim approval CEO/COO approvers must be explicitly configured via CLAIM_APPROVER_* env vars. The MANAGER fallback is not the intended approval chain. The audit was worth running for the payroll bug alone. The revert is the point of human-in-the-loop: agents optimize for "no deadlock," humans know which deadlocks are load-bearing.
hrmydeck: Malaysian HRMS (production pilot, built end-to-end with AI agents) · 2026-06
A hardening pass before the pilot release found managers could mark other teams’ claims as paid, every employee could see the whole company’s leave calendar, and approver IDs were hardcoded.
A pre-release authorization sweep across claims, payroll, and the leave
calendar found scoping and hardcoding issues fixed before the pilot
went live:
- Move hardcoded claim approver employee IDs to env vars
(CLAIM_APPROVER_1_ID/2_ID) so the approval chain doesn't break when
staff change without a deploy
- Add employee scope check to ClaimsController::markPaid() to prevent
managers marking claims outside their team as paid
- Remove MANAGER role from canAccessPanel('admin'), managers use the
employee panel only
- Add explicit canAccess() to PayrollRunResource and LeaveBalanceResource
for consistent defense-in-depth across all admin resources
Follow-up caught in the same sweep:
- Leave calendar: employees could see all approved leave requests
regardless of role. Scoped the response to the employee's own leaves
for non-approver roles (EMPLOYEE, FINANCE); approver roles still see
the full calendar.hrmydeck: Malaysian HRMS (production pilot, built end-to-end with AI agents) · 2026-06
After a duplicate-ID seeder bug silently broke CI, the lesson went into the repo’s CLAUDE.md not a wiki so every future agent session loads it as context before touching the code.
The bug: MockDataSeeder plucked employee IDs without deduplication;
duplicate IDs broke seeding silently and CI went red with no obvious
cause.
The fix was one line. The durable fix was context engineering, a
standing section in CLAUDE.md that every agent session reads:
+ ## Known CI Footguns: Never Re-introduce
+ 1. Seeder dedup: always ->unique() on pluck('employeeId') in
+ MockDataSeeder: duplicate IDs will break seeding silently
+ 2. Leave calendar scoping: non-approver roles (EMPLOYEE, FINANCE)
+ must only see their own leaves; global scoping breaks this
+ 3. Staging Docker BuildKit cache is flaky, pre-existing, unrelated
+ to code changes
Agents forget between sessions; the repo doesn't. Every regression a
reviewer catches becomes a constraint the next agent session starts
with, instead of a mistake it gets to repeat.CrewSync: maritime crew management SaaS (personal product, deployed MVP) · 2026-06
Docs before code: an Obsidian vault mapping every module (safe manning, certificates, sign-on/off, sea service) became the agent context. The MVP foundation landed the next day: 196 files, ~24k lines, and it has been running deployed with a queue worker and scheduler since.
The planning vault came first: architecture, tech stack, and one note
per module (Auth & Authorization, Company & Multi-Tenancy, Crew
Management, Crew Matrix, Certificate Management, Crew Rotations, Safe
Manning Compliance, Sign-On/Sign-Off, Sea Service Records), so agent
sessions built against a written contract instead of improvising scope.
Commit trail:
Jun 22 Initial commit: Laravel backend scaffold
Jun 23 Build CrewSync MVP foundation
196 files changed, 23,716 insertions
What that foundation covers: multi-tenant company isolation, crew and
vessel records, certificate validity tracking with expiry alerts, SOLAS
safe-manning checks before assignment, sign-on/sign-off workflows,
cumulative sea service records, a Filament admin panel, and a React
crew self-service portal (rest hours, payslips, PSC score, sea service).
Deployment is part of the evidence: the stack runs as app + queue
worker + scheduler + Postgres + Redis containers, serving real usage,
not a repo that only ever ran on localhost.Anchorworx Web Platform: maritime job platform (private client work) · 2025-04
Realigned the crew-profile module to the backend’s api.json contract typed DTOs and a dedicated service layer and maintained a memory-bank of project context files so every AI session resumed with full state instead of re-deriving it.
From the refactor doc kept in the repo (PROFILE_REFACTOR.md): - Created src/utils/profileTypes.ts with types matching the API schema: CrewProfileName, CrewProfileContact, ProfileDetail, CrewProfileSeamanDocument (passport, seaman ID, seaman book), CrewProfileExperienceVessel, CrewCertificate, CrewProfileResume, plus request DTOs (CrewExperienceDTO, CrewCertificateDTO, …) - Created src/services/profileService.ts as the single API surface: getProfile, getProfileCompletionStatus, updateProfile, uploadProfilePicture, experience/certificate/education CRUD The contract (api.json) was the source of truth: types were derived from it, not from what the UI happened to send. That's what made changing backend requirements survivable on a 2-person frontend team. The workflow artifact worth copying: a memory-bank/ directory: projectbrief.md, productContext.md, systemPatterns.md, techContext.md, activeContext.md, progress.md, updated as work progressed, so each AI session started from the project's actual state and decisions instead of rediscovering them from scratch.
The concepts behind the practice, including MCP and multi-agent orchestration.
If the pipeline above is useful to someone building the same habit, send it on.