#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────
# Parsec Sdn. Bhd. · AMIR
# Generated by the Parsec Sdn. Bhd. AI Development Framework v2
# © 2026 Parsec Sdn. Bhd. All rights reserved.
# Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised
# projects is prohibited.
# ─────────────────────────────────────────────────────────────
# bootstrap.sh — AMIR project bootstrap
#
# Run this once on Day 1. It:
#   1. Asks where to install (existing cloned repo OR new folder)
#   2. Confirms with you (you must type 'bootstrap' to proceed)
#   3. Verifies prerequisites (PHP 8.3+, Composer, Node 22+, npm, git, claude)
#   4. Scaffolds Laravel 12
#   5. Creates the AMIR domain folder structure
#   6. Writes every framework file (CLAUDE.md, slash commands, hooks, skills,
#      rules, agents, settings.json, .ai/guidelines/, dispatch.sh, review.sh,
#      tmux-work, .cursorrules, docs/living/ stubs, GitHub Actions CI)
#   7. Copies .sprint-backlog.json (sibling file — must be present alongside this script)
#   8. Installs composer + npm dependencies
#   9. Sets up .env from template + generates APP_KEY
#  10. Makes initial commit on main, creates dev/staging/prod branches
#  11. Pushes (Path A) or prints push instructions (Path B)
#  12. Creates 4 agent worktrees (b/c/d/q)
#  13. Adds shell aliases (za/zb/zc/zd/zq) to ~/.zshrc
#
# Hybrid distribution model: this script + .sprint-backlog.json (2.7 MB)
# must be in the SAME directory when you run bootstrap.sh.
#
# After completion: open a new terminal, type 'za', then '/sprint-status'.

set -euo pipefail

# Colours for terminal output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m' # No Color

# ── Helpers ─────────────────────────────────────────────────
say()   { echo -e "${BLUE}→${NC} $*"; }
ok()    { echo -e "${GREEN}✓${NC} $*"; }
warn()  { echo -e "${YELLOW}⚠${NC} $*"; }
fail()  { echo -e "${RED}✗${NC} $*" >&2; exit 1; }
check_command() {
  command -v "$1" &> /dev/null || fail "Missing required tool: $1. $2"
}



# ── Variables ────────────────────────────────────────────────
PROJECT_NAME="amir"
PROJECT_DISPLAY="AMIR"
PROJECT_SNAKE="amir"
STACK="laravel"
GITHUB_ORG="parsec-my"
DEVELOPMENT_LEVEL=3
YEAR="$(date +%Y)"

# Path to this script (so we can find sibling .sprint-backlog.json)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SPRINT_BACKLOG_SOURCE="$SCRIPT_DIR/.sprint-backlog.json"



# ── Step 1: Repo path question ───────────────────────────────
echo ""
echo -e "${BOLD}Have you already created a GitHub repo and cloned it locally?${NC}"
echo ""
echo "  [y] Yes — I am inside the cloned (empty) repo directory right now"
echo "  [n] No  — I will create the local project folder and push to GitHub later"
echo ""
read -rp "Enter y or n: " REPO_ANSWER

case "$REPO_ANSWER" in
  y|Y)
    git rev-parse --git-dir > /dev/null 2>&1 || fail "Not inside a git repository. Clone your empty GitHub repo first, then re-run."
    TARGET_DIR="$(pwd)"
    PUSH_REMOTE=true
    PATH_MODE="A"
    ok "Path A: existing cloned repo at $TARGET_DIR"
    ;;
  n|N)
    read -rp "Enter your project folder name (e.g. amir): " FOLDER_NAME
    [[ -n "$FOLDER_NAME" ]] || fail "Folder name cannot be empty."
    TARGET_DIR="$(pwd)/$FOLDER_NAME"
    [[ -e "$TARGET_DIR" ]] && fail "Folder $TARGET_DIR already exists. Choose a different name or use Path A."
    PUSH_REMOTE=false
    PATH_MODE="B"
    ok "Path B: new folder will be created at $TARGET_DIR"
    ;;
  *)
    fail "Invalid answer. Type y or n."
    ;;
esac



# ── Step 2: Confirmation gate ────────────────────────────────
cat <<EOF

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
${BOLD}PARSEC BOOTSTRAP — $PROJECT_DISPLAY${NC}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This script will:
  ✦ Scaffold Laravel 12 (PHP 8.3) project
  ✦ Create AMIR domain folder structure (15 domains from ARCHITECTURE.md)
  ✦ Write CLAUDE.md, 17 slash commands, 5 hooks, 2 rules files, 8 skills, 5 agents
  ✦ Write .claude/settings.json (pre-approved permissions + lifecycle hooks)
  ✦ Write .ai/guidelines/ (3 files — Boost auto-loaded)
  ✦ Copy .sprint-backlog.json (605 tasks across 43 sprints, demo S00 + production S01-S42)
  ✦ Write docs/living/ with 7 living document stubs
  ✦ Write DEVELOPER_GUIDE.md and PREFLIGHT_CHECKLIST.md
  ✦ Write dispatch.sh + review.sh + tmux-work (Level 4+ ready)
  ✦ Write .cursorrules
  ✦ Run composer install + npm install
  ✦ Set up .env from .env.example + run php artisan key:generate
  ✦ Write .github/workflows/ci.yml (4-branch agent-assisted CI)
  ✦ Initial commit on main → dev → staging → prod branches
EOF

if [[ "$PUSH_REMOTE" == "true" ]]; then
  echo "  ✦ Push main, dev, staging, prod to origin"
else
  echo "  ✦ Print push instructions (you'll create the GitHub repo after)"
fi

cat <<EOF
  ✦ Create 4 agent worktrees (amir-b, amir-c, amir-d, amir-q)
  ✦ Add shell aliases (za, zb, zc, zd, zq) to ~/.zshrc

Target: $TARGET_DIR

EOF
read -rp "Type 'bootstrap' to proceed or Ctrl+C to abort: " CONFIRM
[[ "$CONFIRM" == "bootstrap" ]] || { echo "Aborted."; exit 0; }

echo ""
say "Starting bootstrap..."



# ── Step 3: Prerequisites check ──────────────────────────────
say "Checking prerequisites..."

check_command git "Install git from https://git-scm.com/"
check_command php "Install PHP 8.3+ from https://www.php.net/ or via your package manager"
check_command composer "Install Composer 2.x from https://getcomposer.org/"
check_command node "Install Node.js 22+ from https://nodejs.org/"
check_command npm "Comes with Node.js — should be present after installing Node 22+"

# Verify PHP version >= 8.3
PHP_VERSION_FULL="$(php -r 'echo PHP_VERSION;')"
PHP_MAJOR_MINOR="$(echo "$PHP_VERSION_FULL" | awk -F. '{print $1"."$2}')"
if [[ "$(printf '%s\n' "8.3" "$PHP_MAJOR_MINOR" | sort -V | head -n1)" != "8.3" ]]; then
  fail "PHP $PHP_VERSION_FULL is too old. AMIR requires PHP 8.3+."
fi
ok "PHP $PHP_VERSION_FULL detected"

# Verify Node version >= 22
NODE_VERSION_FULL="$(node --version | sed 's/v//')"
NODE_MAJOR="$(echo "$NODE_VERSION_FULL" | awk -F. '{print $1}')"
if [[ "$NODE_MAJOR" -lt 22 ]]; then
  fail "Node $NODE_VERSION_FULL is too old. AMIR requires Node 22+."
fi
ok "Node $NODE_VERSION_FULL detected"

# Verify required PHP extensions — uses PHP's authoritative extension_loaded()
# instead of grep'ing `php -m` (which can false-positive on whitespace/CR endings)
say "Checking PHP extensions..."
REQUIRED_EXT="pdo_pgsql redis mbstring intl bcmath gd sodium openssl"
MISSING_EXT=""
for ext in $REQUIRED_EXT; do
  if ! php -r "exit(extension_loaded('$ext') ? 0 : 1);" 2>/dev/null; then
    MISSING_EXT="$MISSING_EXT $ext"
  fi
done
if [[ -n "$MISSING_EXT" ]]; then
  warn "PHP extensions missing:$MISSING_EXT (install via Herd UI or pecl before running tests)"
else
  ok "PHP extensions: all required loaded"
fi

# Claude Code — install if missing (do not abort)
if ! command -v claude &> /dev/null; then
  warn "Claude Code not found. Installing..."
  npm install -g @anthropic-ai/claude-code
  ok "Claude Code installed"
else
  ok "Claude Code: $(claude --version 2>/dev/null || echo 'installed')"
fi

# Verify .sprint-backlog.json exists alongside this script
[[ -f "$SPRINT_BACKLOG_SOURCE" ]] || fail "Missing sibling file: .sprint-backlog.json must be in the same directory as bootstrap.sh. Re-extract the bootstrap package."
SPRINT_BACKLOG_SIZE="$(wc -c < "$SPRINT_BACKLOG_SOURCE" | tr -d ' ')"
ok ".sprint-backlog.json found (${SPRINT_BACKLOG_SIZE} bytes)"

ok "All prerequisites OK"



# ── Step 4: Repo setup (Path A only — Path B handled in Step 5) ───────
if [[ "$PATH_MODE" == "A" ]]; then
  cd "$TARGET_DIR"
  ok "Working in existing repo at $TARGET_DIR"
fi



# ── Step 5: Framework scaffold (Laravel 12) ──────────────────
# Critical ordering: composer create-project refuses non-empty directories
# (a .git folder counts as non-empty). For Path B: scaffold first, then git init.

if [[ "$PATH_MODE" == "B" ]]; then
  say "Creating project folder $TARGET_DIR..."
  mkdir -p "$TARGET_DIR"
  cd "$TARGET_DIR"
  ok "Folder created"

  say "Scaffolding Laravel 12 (this takes 1-2 minutes)..."
  composer create-project laravel/laravel . --prefer-dist
  ok "Laravel scaffolded"

  say "Initialising git repository..."
  git init
  git checkout -b main
  ok "Git initialised on main"
else
  # Path A — check if already scaffolded
  if [[ -f "artisan" && -f "composer.json" ]]; then
    ok "Laravel already scaffolded — skipping create-project"
  else
    say "Scaffolding Laravel 12 into existing repo..."

    # Composer create-project refuses non-empty directories. Park bootstrap files
    # (and .git, .nvmrc, anything else hanging around) so the dir looks empty,
    # then move them all back after scaffolding completes.
    PARK_DIR="$(mktemp -d -t amir-bootstrap-park.XXXXXX)"
    say "Parking bootstrap files in $PARK_DIR..."
    # Move every dotfile and bootstrap artifact out — leave only `.` and `..`
    for f in bootstrap.sh .sprint-backlog.json .nvmrc .git .DS_Store .editorconfig; do
      [[ -e "$f" ]] && mv "$f" "$PARK_DIR/" || true
    done

    # Run composer create-project into the now-empty directory
    composer create-project laravel/laravel . --prefer-dist

    say "Restoring bootstrap files from $PARK_DIR..."
    # Move everything back. Laravel's scaffold may have created its own .gitignore
    # and .editorconfig — for those, we keep Laravel's version (don't overwrite).
    # For everything else, we restore.
    for f in bootstrap.sh .sprint-backlog.json .nvmrc .git .DS_Store; do
      if [[ -e "$PARK_DIR/$f" ]]; then
        mv "$PARK_DIR/$f" "./$f"
      fi
    done
    # Special case: if Laravel scaffolded a .editorconfig but we had one parked,
    # keep Laravel's. Discard ours silently.
    [[ -e "$PARK_DIR/.editorconfig" ]] && rm "$PARK_DIR/.editorconfig" || true
    rmdir "$PARK_DIR" 2>/dev/null || true

    # SPRINT_BACKLOG_SOURCE points at a path that just got moved — recompute it
    # to the post-restore location (which is the same physical path since we
    # moved it back to where it started).
    SPRINT_BACKLOG_SOURCE="$SCRIPT_DIR/.sprint-backlog.json"

    ok "Laravel scaffolded"
  fi
fi



# ── Step 6: Domain folder structure ──────────────────────────
say "Creating AMIR domain folder structure..."

# 15 domains from ARCHITECTURE.md Phase 6
DOMAINS=(Tenant Auth COA Journal Invoice Bill Payment Bank Tax Reporting Member Loan ArRahnu Notification PDPA)
for d in "${DOMAINS[@]}"; do
  mkdir -p "app/Domain/$d"/{Actions,Events,Models,Enums,Resources,Requests,Listeners}
done

mkdir -p app/Http/Controllers/Api/V1
mkdir -p app/Http/Controllers/Web
mkdir -p app/Http/Middleware
mkdir -p app/Http/Resources
mkdir -p app/Casts
mkdir -p app/Models/Concerns
mkdir -p app/Scopes

mkdir -p tests/Unit/Domain
mkdir -p tests/Feature/Api/V1
mkdir -p tests/Feature/Invariants
mkdir -p tests/Browser

mkdir -p resources/js/Pages
mkdir -p resources/js/Components
mkdir -p resources/js/Layouts
mkdir -p resources/js/lib

mkdir -p logs

ok "Domain folders created (${#DOMAINS[@]} domains)"

# Write .nvmrc — pins Node 22 for this folder and all worktrees (per-folder via nvm)
# Idempotent: only writes if not already present (preserves any nvm version override)
if [[ ! -f .nvmrc ]]; then
  echo "22" > .nvmrc
  ok ".nvmrc written (Node 22 pinned)"
else
  ok ".nvmrc already exists ($(cat .nvmrc | tr -d '\n'))"
fi



# ── Step 7+8: Write all framework files ──────────────────────
say "Writing framework files..."

mkdir -p .claude/{commands,hooks,rules,skills,agents}
mkdir -p .team-plans                    # Level 5+ — team lead plan persistence
mkdir -p docs/living
mkdir -p docs/runbooks
mkdir -p .ai/guidelines
mkdir -p .github/workflows


# ─── Step 7+8 — Write framework files ─────────────────────────────
echo "→ Writing framework files (this may take a moment)..."

echo "  → writing CLAUDE.md (22,899 bytes)"
cat > 'CLAUDE.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised reproduction or use outside of
  Parsec Sdn. Bhd.-authorised projects is prohibited.
-->

# CLAUDE.md — AMIR Project Memory

## Behavioral Guidelines

**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.

### Think Before Coding
**Don't assume. Don't hide confusion. Surface tradeoffs.**
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them — don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.

### Simplicity First
**Minimum code that solves the problem. Nothing speculative.**
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.

Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.

### Surgical Changes
**Touch only what you must. Clean up only your own mess.**
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it — don't delete it.

When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.

The test: Every changed line should trace directly to the user's request.

### Goal-Driven Execution
**Define success criteria. Loop until verified.**
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"

For multi-step tasks, state a brief plan:
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]

Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.

---

## Before Writing Any Code

**Read these files first** — they are auto-loaded by Laravel Boost on every session start, and they encode every architectural rule that applies to AMIR:

1. `.ai/guidelines/project-architecture.md` — Action class pattern, controller rules, domain events, model rules, API resources, Spatie policies, queue/Horizon structure for AMIR specifically.
2. `.ai/guidelines/data-conventions.md` — money (cents), IDs (UUID v7), phones (E.164), timestamps (timestampsTz), enums, encrypted fields, migrations.
3. `.ai/guidelines/testing-standards.md` — `AssertsQueryPerformance` trait, Pest patterns, factory conventions, test folder structure, query-count assertions.

Then run **Boost's live introspection tools** before planning:
- `database_schema` — read the actual current schema; don't assume.
- `application_info` — installed packages and versions.
- `tinker` — verify model relationships and queries before writing code.

Read **`docs/living/`** (all seven files) before planning or troubleshooting. They reflect the actual current state of the codebase, updated every sprint-close.

---

## 1. Project Overview

**AMIR** is an AI-native, Malaysia-first SME accounting SaaS — built first for koperasi (cooperative societies) under SKM regulation. The product is multi-tenant, packs-based (Core + Koperasi Pack #1, with Sukmk and Sukkk packs to follow), and delivered as a single Laravel + Inertia + React app with three role contexts (Tenant Admin, Tenant User, Platform Operator). The koperasi pack is the v1 commercial entry point and the SKM tender (S005/2026) deliverable.

Solo founder + AI-assisted development. Pre-tender demo deadline: 21 May 2026. Full v1 (production koperasi pilot) target: ~25 August 2026.

---

## 2. Tech Stack (versions are read live by Boost — recorded here for reference)

| Layer | Choice | Notes |
|---|---|---|
| Language | PHP 8.3 | Strict types enabled in every domain file |
| Framework | Laravel 12 | Composer `"laravel/framework": "^12.0"` |
| Database | PostgreSQL 16 | Malaysia residency (AWS ap-southeast-3) |
| Cache + Queue | Redis 7 | Sessions, cache, Horizon queue |
| Frontend | Inertia + React 18 + Tailwind 3 | Single-page app, server-rendered routes |
| Auth | Sanctum (stateful sessions) + Spatie Permission v6 + Fortify | Cookie-based, CSRF-protected |
| Queue | Laravel Horizon | Single Redis-backed queue, multiple supervisors |
| Storage | S3-compatible | Malaysia region preferred |
| Audit log | spatie/laravel-activitylog v4 | Auto-logging on configured models |
| Testing | Pest | Browser tests via Playwright |
| Error tracking | Sentry (managed) | PII-scrubbed via beforeSend; replays disabled |

---

## 3. Domain Folder Structure

Every business domain owns a folder under `app/Domain/[Name]/`. The 20 modules from ARCHITECTURE.md map to these domains:

```
app/Domain/
  Auth/            (Module A — auth, sessions, MFA, login audit)
  Onboarding/      (Module B — tenant setup wizard)
  Parties/         (Module C — debtors, creditors, banks, auditors)
  Accounting/      (Module D — CoA, periods, journal entries, trial balance)
  Transactions/    (Module E — receipt, payment, journal, contra)
  Banking/         (Module F — bank reconciliation)
  FixedAssets/     (Module G — assets + depreciation)
  Inventory/       (Module H)
  Members/         (Module I — Koperasi pack: members, shares, subscriptions)
  ArRahnu/         (Module J — Islamic pawn, gold valuation, auctions)
  EInvoice/        (Module K — MyInvois, LHDN integration)
  Reporting/       (Module L — Penyata Kewangan, Trial Balance, P&L, etc.)
  Surplus/         (Module M — Koperasi pack: surplus distribution, AGM)
  Signals/         (Module N — anomaly + health-score + LLM explanations)
  Oversight/       (Module O — PO cross-tenant view, SKM-facing)
  Operations/      (Module P — PO ops: tenants, packs, billing, alerts)
  Integrations/    (Module Q — generic integration framework)
  DataMigration/   (Module R — import/export, opening balances)
  Pdpa/            (Module S — DSAR, breach, retention, portability)
  Tax/             (Module T — Cukai Pendapatan + Zakat draft computation)
```

Inside each domain folder:
```
app/Domain/[Name]/
  Actions/         — single-purpose action classes (one per business operation)
  Events/          — domain events emitted by actions
  Models/          — Eloquent models for this domain only
  Enums/           — backed enums for status/type values
  Resources/       — API/Inertia resources
  Requests/        — FormRequest classes for input validation
  Policies/        — Spatie-aware authorisation policies
  Jobs/            — queued jobs (one per async operation)
  Listeners/       — domain event listeners
  Services/        — only for genuinely shared logic (avoid; prefer actions)
```

**Cross-domain calls are explicit via Actions, never via direct model coupling.** `Members\Actions\CreateMember` may dispatch a `MemberRegistered` event that `Accounting\Listeners\AllocateMemberAccount` listens to — but `Members\Models\Member` does not import `Accounting\Models\Account`.

---

## 4. Code Pattern Examples

### Controller (thin — delegates to Action)
```php
<?php
declare(strict_types=1);

namespace App\Http\Controllers\Members;

use App\Domain\Members\Actions\CreateMember;
use App\Domain\Members\Requests\CreateMemberRequest;
use App\Domain\Members\Resources\MemberResource;
use Illuminate\Http\JsonResponse;

final class CreateMemberController
{
    public function __invoke(CreateMemberRequest $request, CreateMember $action): JsonResponse
    {
        $member = $action->execute(
            tenantId: $request->user()->currentTenantId(),
            data: $request->validated(),
        );

        return MemberResource::make($member)->response()->setStatusCode(201);
    }
}
```

### Action class (single public `execute` method, transactional)
```php
<?php
declare(strict_types=1);

namespace App\Domain\Members\Actions;

use App\Domain\Members\Events\MemberRegistered;
use App\Domain\Members\Models\Member;
use Illuminate\Support\Facades\DB;

final class CreateMember
{
    public function execute(string $tenantId, array $data): Member
    {
        return DB::transaction(function () use ($tenantId, $data): Member {
            $member = Member::create([
                'tenant_id' => $tenantId,
                'ic_no_encrypted' => $data['ic_number'],   // input field 'ic_number' → storage column 'ic_no_encrypted' (per E3)
                'full_name' => $data['full_name'],
                'phone_e164' => $data['phone_e164'],
                'shares_held' => 0,
                'subscription_balance_cents' => 0,
                'status' => MemberStatus::Active,
            ]);

            event(new MemberRegistered($member->id, $tenantId));

            return $member->fresh();
        });
    }
}
```

### FormRequest (validation only — no business logic)
```php
<?php
declare(strict_types=1);

namespace App\Domain\Members\Requests;

use Illuminate\Foundation\Http\FormRequest;

final class CreateMemberRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', \App\Domain\Members\Models\Member::class);
    }

    public function rules(): array
    {
        return [
            'ic_number' => ['required', 'string', 'regex:/^\d{6}-\d{2}-\d{4}$/'],
            'full_name' => ['required', 'string', 'max:255'],
            'phone_e164' => ['required', 'string', 'regex:/^\+60\d{9,10}$/'],
        ];
    }
}
```

---

## 5. Data Conventions Summary

| Convention | Rule | Example |
|---|---|---|
| Money | Always integer **cents** in `BIGINT` columns named `*_cents`. Never `decimal(N,2)`. | `total_amount_cents BIGINT NOT NULL` |
| IDs | **UUID v7** primary keys, generated application-side via `HasUuids` trait override. Never auto-increment. | `id UUID PRIMARY KEY` |
| Foreign keys | UUID FKs with explicit `ON DELETE` strategy (RESTRICT default, CASCADE only when child cannot exist independently). | `tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE RESTRICT` |
| Status | Backed PHP enum + `string` column; never raw strings in code. | `MemberStatus::Active`, column `status VARCHAR(32)` |
| Phones | E.164 format in `phone_e164` columns, `+60123456789` style. | Validated by regex `/^\+60\d{9,10}$/` |
| Timestamps | `timestampsTz()` on every table — always timezone-aware. UTC in DB, MYT for display. | `created_at TIMESTAMPTZ NOT NULL` |
| Soft deletes | `softDeletes()` on entities with audit-trail lifecycle. | `deleted_at TIMESTAMPTZ NULL` |
| Tenant scope | Every tenant-scoped table has `tenant_id UUID NOT NULL`. Use `BelongsToTenant` trait — never manual `where('tenant_id', ...)`. | See `app/Concerns/BelongsToTenant.php` |
| Encrypted fields | Sensitive identifiers (NRIC, bank account numbers, TINs) use the **two-column pattern from E3**: `*_encrypted` for ciphertext + `*_hash` for SHA-256 lookup with per-tenant salt. MFA secrets and integration credentials use the single-column `'encrypted'` cast. | `protected $casts = ['ic_no_encrypted' => 'encrypted']` |
| Audit | spatie/laravel-activitylog v4 on every model that needs an audit trail. Configured via `LogsActivity` trait. | `use LogsActivity;` |

Full rules in `.ai/guidelines/data-conventions.md`.

---

## 6. Git Rules

### Branch naming
`feature/s{N}-{NN}-{slug}` — example: `feature/s06-01-member-master-crud`
`fix/{slug}` for bug-fix branches off `dev`
`hotfix/{slug}` for emergency patches off `main`

### Commit message format
```
[type]([scope]): [description]
.--. .- .-. ... . -.-.

[body — optional, only if context > 1 line]
```

The `.--. .- .-. ... . -.-.` Morse separator is **mandatory** on every commit. The pre-commit guard hook hard-blocks commits without it.

`[type]` ∈ `feat`, `fix`, `chore`, `refactor`, `docs`, `test`, `style`, `build`, `ci`.
`[scope]` is the domain folder (e.g. `members`, `accounting`) or `core` for cross-cutting.

**Forbidden:**
- `Co-Authored-By:` lines (hard-blocked by pre-commit guard)
- Commits referencing the agent in the message body
- Commits with debug artifacts (`dd()`, `dump()`, `console.log`, `var_dump()`) — hard-blocked

### PR rules
- Every PR closes one task in `.sprint-backlog.json`
- PR description follows the template in `.claude/skills/writing-pr-descriptions.md`
- Run `/verify` before opening the PR — 13 steps, mandatory
- Every PR must have an associated test (no test = no merge)

---

## 7. DO NOT List (architecture violations specific to AMIR)

- ❌ Never query a tenant-scoped model without `BelongsToTenant` trait (silent data leak across tenants)
- ❌ Never use `decimal` for money — only integer cents in `BIGINT`
- ❌ Never store NRIC in plaintext — use `encrypted` cast
- ❌ Never skip `tenant_id` on a new tenant-scoped table (caught by `post-edit-tenant-scope-check.sh` hook)
- ❌ Never mutate financial data (transactions, journal entries, posted amounts) directly — always via the appropriate Action class so audit log fires
- ❌ Never use raw `where('status', 'XYZ')` — use the backed enum: `where('status', MemberStatus::Active)`
- ❌ Never reproduce song lyrics, copyrighted material, real public-figure quotes anywhere in the codebase including comments and seed data
- ❌ Never let frontend ALL-CAPS strings drift from PHP enums (caught by `post-edit-enum-sync-check.sh` hook)
- ❌ Never write a migration that drops a column without a deprecation sprint first (see `.claude/skills/writing-migrations.md`)
- ❌ Never use `console.log` / `dd()` / `dump()` in code that goes to PR — hard-blocked at commit
- ❌ Never include PII in analytics events or Sentry breadcrumbs (caught by `PIIGuard` service + `beforeSend` scrubber)
- ❌ Never bypass `TenantScope` without the explicit `withoutGlobalScope(TenantScope::class)` call AND a justification comment

---

## 8. Design System Compliance

**Before building any page or UI component, read `UI_UX_SPEC.md` Section 7.4 (Component Inventory).**

### Component Usage (mandatory)
- Use design system components for ALL UI. Never build raw HTML when a component exists.
  Use `<Button>`, not `<button>`. Use `<DataTable>`, not `<table>`.
  Use `<FormField>`, not `<label>` + `<input>` + `<span class="error">`.
  Use `<StatusBadge status={x}>`, not `<span className="badge badge-success">`.
- If you need a component that doesn't exist, flag it — do not build an ad-hoc version.
  Write: "MISSING COMPONENT: need [description]. Should this be added to the design system?"
- Every page must use a layout pattern from Section 7.4.4 (L0–L5). Do not invent new layouts.

### Tokens (mandatory)
- Never hardcode colour values. Use design tokens: `var(--color-primary)`, `text-primary`, etc.
- Never hardcode spacing values. Use tokens: `var(--space-4)`, `p-4`, `gap-6`, etc.
- Never hardcode font sizes. Use tokens: `var(--text-sm)`, `text-sm`, etc.
- Never hardcode border radius, shadows, or breakpoints. Use tokens.

### Status Badges (mandatory)
- Every status value must use `<StatusBadge>` with the status enum value.
- StatusBadge reads from `STATUS_CONFIG` (see CONTENT_COPY.md §4) — never choose badge colours manually.
- If a status value is missing from `STATUS_CONFIG`, add it there first — not inline.

### BM-EN parity (mandatory)
- All user-facing strings exist in both BM and EN per CONTENT_COPY.md.
- Use the `__()` helper for translation. Never hardcode user-visible strings.
- For dynamic content, use locale-aware formatters (date, number, currency).

### Before submitting frontend work, verify:
- [ ] Every UI element maps to a design system component
- [ ] Zero hardcoded colour/spacing/typography values
- [ ] Page uses a standard layout pattern (L0–L5)
- [ ] All status displays use `StatusBadge` with enum values
- [ ] No duplicate component implementations (grep for similar patterns)
- [ ] Every user-facing string uses `__()` and exists in both BM and EN

---

## 9. Performance Rules

Every integration test **must** assert query performance. The `AssertsQueryPerformance` trait provides:
- `assertQueryCountLessThan(int $max)` — guards against N+1
- `assertNoDuplicateQueries()` — guards against repeated identical queries
- `assertNoSlowQueries(int $thresholdMs = 100)` — guards against unindexed scans

Default budget: **dashboard endpoints ≤ 15 queries**, **list endpoints ≤ 20 queries**, **detail endpoints ≤ 10 queries**, **post-action endpoints ≤ 25 queries**. Stricter budgets per endpoint live in the test file.

For dashboard widget queries, always use precomputed aggregates from `analytics_aggregates_daily` — never raw `analytics_events`.

---

## 10. Self-Verification Protocol (`/verify` — 13 steps)

Run before every PR. Each step must pass before advancing.

1. **Read the task** — re-read the task in `.sprint-backlog.json`. Confirm acceptance criteria.
2. **Run tests** — `php artisan test --filter=...` for the new test. Confirm green.
3. **Format** — `./vendor/bin/pint` (auto-applied by `post-edit-format.sh` but verify).
4. **Static analysis** — `./vendor/bin/phpstan analyse` (or larastan). Zero new errors.
5. **Frontend lint** — `npm run lint` if any frontend changes. Zero new errors.
6. **Frontend build** — `npm run build` succeeds.
7. **Tenant scope check** — every new model uses `BelongsToTenant` (or has a `// platform-scoped` comment).
8. **Code quality — LLM bloat check** (per `.claude/skills/reviewing-code.md`). Single-use helpers, defensive nulls for non-nullable types, single-implementation abstractions, commented-out code, unconnected scaffolding — all removed.
9. **Audit log fires** — for every model write that requires audit trail, confirm the test asserts the activity log entry.
10. **Run full suite** — `php artisan test` passes (not just the new test).
11. **Simplify check** — run `/simplify` on the changed files. Apply suggested cleanups.
12. **Final diff review** — re-read the entire diff. Ask: would a senior engineer approve this?
13. **Living docs** — if domain boundaries, schema, or APIs changed, update `docs/living/`.

If any step fails, fix and restart from step 1.

---

## 11. Boost Tools Reference

Laravel Boost provides live introspection. Use these instead of guessing:

| Tool | When to use |
|---|---|
| `database_schema` | Before writing a migration. Confirms the actual current schema. |
| `application_info` | Confirms installed packages and versions. |
| `tinker` | Verify Eloquent relationships and query results before writing code. |
| `last_error` | Read the most recent application error if a test or browser action fails. |
| `read_log` | Tail the Laravel log. |
| `browser_logs` | Read browser console output during a Playwright run. |
| `list_routes` | List routes for the current app. |
| `get_config` | Read config values without restarting. |
| `list_artisan_commands` | Discover available Artisan commands. |

Never assume schema, package versions, route names, or config values. Use Boost.

---

## 12. Current Sprint / Focus

**Sprint:** S00 (DEMO SPRINT — pre-tender). Active until 4 June 2026.
**Goal:** Demo-ready koperasi accounting core (CoA + transactions + bank recon + Penyata Trial Balance + multi-tenant + signals MVP).
**Level:** L3 (Manual). One agent at a time. Founder reviews every PR.
**Backlog ID format:** `D{day}.{NN}` for demo sprint, `S{NN}.{NN}` for production sprints.

After demo sprint:
- S01-S05 (Phase A): Foundation hardening (5 sprints, L3 ramp)
- S06-S40 (Phases B-K): Module build-out (35 sprints, L5 stable)
- S41-S42 (Phase L): Hardening + pilot-ready

---

## 13. Key Files

### Planning documents (the source of truth, never auto-edit)
- `PROJECT_CONTEXT.md`, `VIABILITY_REPORT.md`, `BUSINESS_FLOWS.md`, `USER_STORIES.md`, `ARCHITECTURE.md`, `DECISIONS_LOG.md`, `UI_UX_SPEC.md`, `SCREEN_BRIEFS.md`, `TEST_SCENARIOS.md`, `ACCOUNTING_INVARIANTS.md`, `CONTENT_COPY.md`, `ANALYTICS_PLAN.md`, `SPRINT_PLAN.md`

### Operating documents (used every session)
- `.sprint-backlog.json` — the task queue
- `.ai/guidelines/*.md` — auto-loaded by Boost
- `.claude/rules/laravel.md`, `.claude/rules/AGENT_GUIDE.md` — auto-loaded every session
- `.claude/skills/*.md` — loaded on demand by slash commands
- `.claude/hooks/*.sh` — fire automatically on lifecycle events
- `.claude/settings.json` — permissions, model, hooks config

### Living codebase documents (kept current — read before planning)
- `docs/living/CODEBASE_MAP.md` — domain boundaries and class index
- `docs/living/DATA_MODEL.md` — current schema snapshot
- `docs/living/DOMAIN_GUIDE.md` — state machines and business rules
- `docs/living/API_REFERENCE.md` — current API surface
- `docs/living/VELOCITY_LOG.md` — sprint timing data
- `docs/living/DEVIATION_LOG.md` — places where the implementation deviated from the plan
- `docs/living/PRINCIPLES.md` — what we learned about this codebase

**Read all seven files in `docs/living/` before planning or troubleshooting. They reflect the actual current state of the codebase.**

---

## Workspace: tmux Preview Pane

This project runs in a 4-agent tmux workspace launched by `tmux-work` in the project root.
The right panel is a dedicated preview shell. Its pane ID is available as `$TMUX_PREVIEW_PANE` in every agent session.

To surface a file for the developer to review, push it to the preview pane:
```bash
tmux send-keys -t "$TMUX_PREVIEW_PANE" 'q' C-m "preview \"path/to/file\"" C-m
```

Supported file types: `.md`, `.pdf`, `.docx`, `.xlsx`, `.pptx`, and any plain text or code file.

Use this when you produce a document the developer should read, finish a feature the developer should review visually, or are asked to "preview", "show", or "open" a file.

---

## Decisions Trace

This file's rules trace back to decisions in `DECISIONS_LOG.md`. The full trace is in `.ai/guidelines/project-architecture.md` §16, `.ai/guidelines/data-conventions.md` §15, and `.ai/guidelines/testing-standards.md` §17. The most consequential decisions to know:

- **D15-R1** — UUID v7 PKs (irreversible)
- **D16** — Money cents + MoneyCast (irreversible)
- **D17** — UTC storage, MYT display (irreversible)
- **D28** — `tenant_id` + `BelongsToTenant` (costly)
- **D30** — Action class pattern (costly)
- **D31** — Single-action controllers (costly)
- **D32** — `final class` mandate (reversible)
- **D33** — Phone E.164 in `phone_e164` columns (costly)
- **E1** — User-tenant junction table (irreversible)
- **E3** — Two-column encryption pattern (irreversible)

If you want to deviate from any rule, raise a revision (`Dxx-R1`) in `DECISIONS_LOG.md` first. Do not silently drift in a single PR.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing DEVELOPER_GUIDE.md (33,014 bytes)"
cat > 'DEVELOPER_GUIDE.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd. All rights reserved.
  Internal use only. Unauthorised reproduction or use outside of
  Parsec Sdn. Bhd.-authorised projects is prohibited.
-->

# AMIR — Developer Guide
> How we build, how we work, and how to get started.

**Stack:** Laravel 12 + PHP 8.3 + PostgreSQL 16 + Redis + Inertia/React 18 + Tailwind 3
**Last updated:** 2026-05-06
**Framework:** Parsec Sdn. Bhd. AI Development Framework v2
**Development Level:** 3 (Manual) — promoting to L4/L5 after Sprint 1 retrospective

---

## 1. What We're Building

AMIR is an AI-native SME accounting SaaS for Malaysia. The product handles double-entry bookkeeping, GST/SST tax management, and Malaysia-specific compliance (LHDN MyInvois e-invoicing, PDPA 2010 amended 2024, BNM-MPS Shariah rulings). It ships with a **Koperasi pack** for cooperative societies — member management, cooperative loans, and Ar-Rahnu (Shariah-compliant pawn) — to win the SKM tender (S005/2026, deadline 21 May 2026).

The core insight: SMEs in Malaysia today choose between expensive enterprise tools (SAP, Microsoft Dynamics) and unsuitable foreign SaaS (Xero, QuickBooks — neither speaks LHDN, MyInvois, or Bahasa Malaysia natively). AMIR is built for the local accounting reality from Day 1: tax codes, e-invoicing, PDPA, Koperasi-specific accounting, and natural language that mixes English and BM.

---

## 2. Tech Stack

| Layer | Technology | Version |
|-------|------------|---------|
| Backend | Laravel | 12.x (PHP 8.3) |
| Frontend | Inertia + React | React 18 |
| Styling | Tailwind CSS | 3.x |
| Database | PostgreSQL | 16 |
| Cache / Queue | Redis + Laravel Horizon | 7.x / Horizon 5 |
| Auth | Sanctum (stateful SPA + token) + Fortify | bundled |
| Roles & Permissions | Spatie Permission | v6 |
| Audit Log | spatie/laravel-activitylog | v4 |
| Testing | Pest + Playwright (E2E) | Pest 3, Playwright 1 |
| Static Analysis | PHPStan + Larastan | latest |
| Code Style | Laravel Pint (PER preset) | latest |
| Error Tracking | Sentry | SDK latest |
| Hosting | Laravel Forge → AWS (Singapore region for staging/prod, future Malaysia region) | — |
| CI/CD | GitHub Actions | — |
| LLM | Anthropic API (Claude Sonnet for production AI features, Haiku for routing) | API |

---

## 3. Local Environment Setup

### Prerequisites

- **PHP 8.3+** with extensions: `pdo_pgsql`, `redis`, `mbstring`, `intl`, `bcmath`, `gd`, `sodium`, `openssl`
- **Composer 2.x**
- **Node.js 22+** + **npm 10+**
- **PostgreSQL 16** (local or Docker)
- **Redis 7** (local or Docker)
- **Git** with worktree support (any modern version)
- **Claude Code** (`npm install -g @anthropic-ai/claude-code`)

### First-Time Setup

```bash
# 1. Clone and enter the repo
git clone git@github.com:parsec-my/amir.git
cd amir
git checkout dev

# 2. Install dependencies
composer install
npm install

# 3. Environment configuration
cp .env.example .env
php artisan key:generate

# Edit .env — required variables:
# DB_CONNECTION=pgsql
# DB_HOST=127.0.0.1
# DB_PORT=5432
# DB_DATABASE=amir
# DB_USERNAME=amir
# DB_PASSWORD=...
# REDIS_HOST=127.0.0.1
# REDIS_PORT=6379
# QUEUE_CONNECTION=redis
# CACHE_DRIVER=redis
# SESSION_DRIVER=redis
# ANTHROPIC_API_KEY=...                  # for AI features (later)
# AWS_KMS_KEY_ID=...                     # for E3 envelope encryption (later)
# MYINVOIS_CLIENT_ID=... (sandbox)       # LHDN preprod (Sprint S08)
# WHATSAPP_API_TOKEN=...                 # 360dialog or Meta (Sprint S10)

# 4. Database setup
php artisan migrate:fresh --seed

# 5. Build frontend assets
npm run build       # production build
# OR for development with hot reload:
npm run dev         # in a separate terminal

# 6. Verify the setup
php artisan test
# All green? You're ready.

# 7. Run the dev server
php artisan serve   # http://localhost:8000
# OR (preferred — concurrent dev server + queue + vite + log tail):
composer dev        # starts everything via concurrently
```

### Laravel Boost (MCP — Required for AI-Assisted Development)

Laravel Boost gives Claude Code live access to the database schema, registered routes, recent log errors, and package documentation. It runs as a local MCP server when you run `claude`.

```bash
# Already installed by bootstrap.sh — verify it's connected
claude mcp list   # must show "laravel-boost"

# If not showing, run the installer
php artisan boost:install
```

If Boost is not connected, agents will hallucinate schema and routes. Always verify before starting a session.

---

## 4. Project Structure

```
app/
  Domain/                       ← All business logic, grouped by domain
    Tenant/                       ← Multi-tenant scaffolding (D28)
      Actions/                    ← One class per operation (e.g. CreateTenant)
      Events/                     ← Domain events (TenantCreated)
      Models/                     ← Eloquent models — relationships and scopes only
      Enums/                      ← Status enums, type enums (PHP backed enums)
      Resources/                  ← API response transformers
      Requests/                   ← Form request validation classes
    Auth/
    COA/                          ← Chart of Accounts
    Journal/                      ← General ledger (immutable once posted)
    Invoice/                      ← Sales invoices
    Bill/                         ← Purchase bills
    Payment/
    Bank/                         ← Bank reconciliation
    Tax/                          ← SST + MyInvois e-invoicing
    Reporting/                    ← P&L, Balance Sheet, Trial Balance
    Member/                       ← Koperasi members (Sprint S15)
    Loan/                         ← Koperasi loans (Sprint S16)
    ArRahnu/                      ← Shariah-compliant pawn — Tawarruq (Sprint S20)
    Notification/                 ← WhatsApp + Email
    PDPA/                         ← Data subject rights, breach reporting

  Http/
    Controllers/
      Api/V1/                     ← API controllers — every route prefixed /api/v1
      Web/                        ← Inertia controllers (server-rendered SPA)
    Middleware/
      EnsureTenantContext.php
      IdempotencyMiddleware.php
    Resources/                    ← (alternatively in Domain/X/Resources/)

  Casts/
    MoneyCast.php                 ← Integer cents ↔ Money value object
    EncryptedCast.php             ← AWS KMS envelope encryption (E3)

  Models/
    Concerns/
      BelongsToTenant.php         ← Trait — auto-applies TenantScope (per D28)
      HasUuidV7.php               ← Trait — UUID v7 PK generation (per D15-R1)

  Scopes/
    TenantScope.php               ← Global scope — auto-filters by current tenant

config/
  amir.php                        ← AMIR-specific config (feature flags, integrations)

resources/
  js/
    Pages/                        ← Inertia pages (one per route)
    Components/                   ← Reusable React components
    Layouts/                      ← App shell, auth shell
    lib/                          ← formatMoney, formatDate, enums.ts (mirrors PHP)
  views/                          ← Blade for emails + minimal SSR

routes/
  api.php                         ← API routes — all under /api/v1/*
  web.php                         ← Inertia routes
  channels.php                    ← Broadcasting (later)

database/
  migrations/                     ← Additive only (per CLAUDE.md DO NOT)
  factories/                      ← One per model
  seeders/                        ← DemoSeeder for dev, ProdSeeder for prod data

tests/
  Unit/Domain/                    ← Action class unit tests
  Feature/Api/V1/                 ← Integration tests with AssertsQueryPerformance
  Feature/Invariants/             ← Cross-domain invariant tests (tenant isolation, journal balance)
  Browser/                        ← Playwright E2E

.ai/guidelines/                   ← Boost auto-loaded architecture rules
  project-architecture.md           ← Action class pattern, controller rules, event pattern
  data-conventions.md               ← Money, UUID, phone E.164, IC encryption (E3)
  testing-standards.md              ← Pest patterns, AssertsQueryPerformance trait

docs/
  living/                         ← Updated every sprint via /sprint-close
    CODEBASE_MAP.md
    DATA_MODEL.md
    API_REFERENCE.md
    DOMAIN_GUIDE.md
    INTEGRATION_LOG.md
    DEVIATION_LOG.md
    VELOCITY_LOG.md
  ARCHITECTURE.md                 ← Original design (Phase 6)
  USER_STORIES.md                 ← Stories with ACs (Phase 4)
  DECISIONS_LOG.md                ← D1–D33+ technical decisions (Phase 5)
  BUSINESS_FLOWS.md               ← Mermaid flows (Phase 3)
  ACCOUNTING_INVARIANTS.md        ← Bookkeeping integrity rules
```

**`.claude/` folder:**

| Path | What it is |
|------|-----------|
| `.claude/settings.json` | Pre-approved agent permissions + lifecycle hooks config |
| `.claude/commands/` | Slash commands — `/plan`, `/verify`, `/commit-push-pr`, etc. |
| `.claude/rules/` | Stack best-practices + AGENT_GUIDE.md — **auto-read every session start**. Do not move. |
| `.claude/hooks/` | Lifecycle scripts — Pint formatter, commit guard, session logger, tenant-scope check, enum sync check |
| `.claude/skills/` | On-demand modules — referenced by slash commands (writing-tests, writing-migrations, writing-pr-descriptions, reviewing-code, writing-journal-entries, writing-myinvois-integration, writing-ar-rahnu, writing-pdpa-handlers) |
| `.claude/agents/` | *(L5+)* Sub-agent definitions — team-lead, backend-dev, frontend-dev, test-writer, code-reviewer |

---

## 5. Key Documents

| Document | Where | What It Contains |
|----------|-------|-----------------|
| `CLAUDE.md` | Repo root | Architecture rules, patterns, data conventions, DO NOT list. **Read this before writing any code.** |
| `ARCHITECTURE.md` | `docs/` | Full system design, schema, API routes, infrastructure |
| `USER_STORIES.md` | `docs/` | Every story with acceptance criteria. Your task ACs come from here. |
| `DECISIONS_LOG.md` | `docs/` | D1–D33+ technical decisions with rationale. Check before questioning a choice. |
| `BUSINESS_FLOWS.md` | `docs/` | Mermaid diagrams for every user and system flow. Reference for edge cases. |
| `ACCOUNTING_INVARIANTS.md` | `docs/` | Bookkeeping integrity rules — debit=credit, posting state machine, etc. |
| `.sprint-backlog.json` | Repo root | Current sprint tasks. Your full task brief is here. |
| `DEVELOPER_GUIDE.md` | Repo root | This file. |
| `PREFLIGHT_CHECKLIST.md` | Repo root | Day-1 third-party accounts, domain, infrastructure setup. Run in parallel with development. |
| `.ai/guidelines/project-architecture.md` | `.ai/guidelines/` | Action class pattern, controllers, events. Auto-loaded by Boost. |
| `.ai/guidelines/data-conventions.md` | `.ai/guidelines/` | Money cents, UUID v7, IC encryption. Auto-loaded by Boost. |
| `.ai/guidelines/testing-standards.md` | `.ai/guidelines/` | Pest patterns, query assertion trait. Auto-loaded by Boost. |

**Living docs in `docs/living/`** — read all 7 before planning or troubleshooting. They reflect actual current state, not the original plan. If a planning doc and a living doc conflict, the living doc wins.

---

## 6. How We Work

### The AI-Assisted Model

We build with Claude Code as a coding agent, not a code-suggestion tool:

- **You are the architect and reviewer.** You decide what gets built, review what the agent produces, and keep the patterns accurate.
- **Agents write the code.** They follow `CLAUDE.md` and self-verify before committing.
- **Tasks are precise briefs.** Every task in `.sprint-backlog.json` contains full context, ACs, and explicit pattern requirements. Brief quality determines output quality.

### The Two Phases

**Phase 1 — Learning (First 2 sprints — S00 demo + S01 production)**
Work manually, one task at a time, one agent session at a time. This is not slower — it's how you learn what Claude does well, where it needs guardrails, and build up the `CLAUDE.md` DO NOT list with real mistakes from real work. Do not skip this phase.

**Phase 2 — Automated (Sprint S02+, after `/level-up` to L4)**
Once 3–5 features are shipped manually and `CLAUDE.md` accurately describes real patterns, switch to parallel agent dispatch. Four worktrees run simultaneously, each agent self-verifies before committing, and you review batches.

---

## 7. Install Claude Code

```bash
# Requires Node.js 22+
npm install -g @anthropic-ai/claude-code

# Verify
claude --version
```

---

## 8. Set Up Worktrees

Bootstrap.sh creates four worktrees automatically. If you need to recreate them:

```bash
# Run from inside the main project folder (amir/)
git worktree add -b agent/b ../amir-b dev
git worktree add -b agent/c ../amir-c dev
git worktree add -b agent/d ../amir-d dev
git worktree add -b agent/q ../amir-q dev
```

Shell aliases (added by bootstrap.sh to `~/.zshrc` automatically):

```bash
alias za="cd ~/projects/amir   && claude"
alias zb="cd ~/projects/amir-b && claude"
alias zc="cd ~/projects/amir-c && claude"
alias zd="cd ~/projects/amir-d && claude"
alias zq="cd ~/projects/amir-q && claude"   # analysis only — no commits
```

> **Rule:** Each worktree must be on a different branch. Never run two agents on the same branch or touching the same files.

---

## 8b. Launch Your Workspace

```bash
./tmux-work        # opens 4 agents + preview pane in one command
```

Layout: 2×2 grid of agents on left, shared preview pane on right. From any pane:
```bash
tmux send-keys -t "$TMUX_PREVIEW_PANE" 'q' C-m "preview \"docs/ARCHITECTURE.md\"" C-m
```

> **Note:** `--dangerously-skip-permissions` (used by `tmux-work` for interactive sessions) bypasses `PreToolUse` hooks — separator check, debug-artifact check, Co-Authored-By block. Do not use in `dispatch.sh`. Print mode (`claude -p`) handles permissions differently and does not need the flag.

---

## 9. Daily Rhythm

### Morning (5–10 min)

```bash
./tmux-work        # opens 4 agents + preview pane
/sprint-status     # Haiku → Sonnet (auto). Shows current sprint progress.
```

Check what's pending. Note dependencies. Pick your tasks and dispatch.

### Starting a New Sprint

```bash
/sprint-start [N]  # one command — populates backlog (only if needed) + at L4 reconfigures dispatch.sh
```

For AMIR specifically: the backlog ships with all 605 tasks pre-populated by Phase 11. So `/sprint-start` mainly does the L4 dispatch reconfiguration step (skipped at L3).

### Throughout the Day — Level 3 (current)

Work one task at a time:
```
/plan [task-id]       ← Sonnet — review the plan before approving
/implement            ← Sonnet — executes the approved plan
/verify               ← Sonnet — 13-step self-check (do NOT skip)
/commit-push-pr       ← Sonnet — stages, commits, pushes, opens PR
                      ← review PR on GitHub: diff, CI, ## Assumptions — merge
/task-done [task-id]  ← Haiku — marks done in backlog, returns to Sonnet
```

### Level 4 (after `/level-up` post-Sprint 1)

```bash
./dispatch.sh [batch-name]   # all agents launch on Sonnet via tmux send-keys
/watch                       # status ping every 10 min
# step away — agents work in -p mode, output buffers until done
tail -f logs/agent-*.log     # if you want live visibility

# When all agents in batch finish:
./review.sh [batch-name]
# 1. Read each PR diff on GitHub
# 2. Check CI is green
# 3. Read ## Assumptions — unread assumptions become bugs
# 4. Merge ONE PR at a time — run tests on dev after each merge
# 5. /task-done [task-id] after each merge
# 6. ./dispatch.sh [next-batch-name] — only when dev is clean
```

**Never dispatch the next batch until dev is clean.**

### Level 5 (after second `/level-up`)

```bash
/team-launch [feature-id]   # Team Lead → Backend Dev || Frontend Dev → Test Writer → Code Reviewer
/watch                      # monitor progress
# One PR per feature appears when pipeline completes
/task-done [feature-id]
```

> **Critical:** Sub-agents cannot spawn sub-agents. Only the parent Claude session spawns specialists. The team lead returns a JSON plan and never writes code.

### End of Day (5 min)

```bash
/sprint-status
```

If an agent made a mistake, flag it now:
```bash
/flag-mistake [describe what went wrong and what should have happened]
```

This updates `CLAUDE.md`, `.cursorrules`, and remaining backlog prompts. Do it the same day, not at sprint-end.

### End of Sprint (mandatory)

```bash
za
/sprint-close
```

`/sprint-close` runs the full test suite, checks every performance assertion for regressions across the whole codebase, updates all 7 living docs, and (at L4) reconfigures `dispatch.sh` for the next sprint. **Do not advance the sprint counter until `/sprint-close` passes clean.**

After clean close:
```bash
/sprint-start [N+1]
```

---

## 10. The Standard Task Flow

### Phase 1 — Manual (one task at a time — current at L3)

```
/plan [TASK-ID]
         ↓
   review plan
         ↓
   /implement
         ↓
  /test-and-fix
         ↓
     /verify          ← 13 steps. Do not skip — tests passing ≠ work is correct.
         ↓
/commit-push-pr
         ↓
   merge PR on GitHub
         ↓
   /task-done [TASK-ID]
```

**Why `/verify` matters:** It runs 13 checks — not just whether tests pass. It checks architecture compliance, ACs, security, performance assertions, and that no DO NOT rules were violated.

### Phase 2 — Automated dispatch (L4+, future)

```bash
./dispatch.sh --dry-run     # preview what will be dispatched
./dispatch.sh [batch-name]  # launch agents across worktrees
/watch                      # background monitor — status ping every 10 min
```

Agents run, self-verify, and commit when all checks pass. When ready:

```bash
./review.sh [batch-name]    # per worktree: verification status, test results, diff summary
```

---

## 11. Slash Commands Reference

| Command | Model | When to Use | What It Does |
|---------|-------|-------------|-------------|
| `/sprint-start [N]` | Haiku → Sonnet (auto) | Start of every sprint | Populates backlog from SPRINT_PLAN.md (only if needed for AMIR — already pre-populated). At L4: configures dispatch.sh batch layout. |
| `/plan` | Sonnet | Before writing any code | Maps task → ACs → files → implementation order. Outputs reviewable plan. Does NOT write code. |
| `/implement` | Sonnet | After plan is approved | Executes plan in dependency order. Runs `/simplify` before `/verify`. |
| `/scaffold` | Sonnet | New domain entity | Generates Model + Migration + Action(s) + Request + Resource + Policy + Test + Factory. |
| `/test-and-fix` | Sonnet | Tests are failing | Runs suite, diagnoses failures, fixes code (never weakens tests), loops until green. |
| `/verify` | Sonnet | After implementing, before commit | 13-step self-check. |
| `/commit-push-pr` | Sonnet | Work is verified | Stages, writes Conventional Commits message + Morse separator, pushes, creates PR. |
| `/task-done [id]` | Haiku → Sonnet (auto) | After merging a PR | Marks task done in backlog, switches to dev, pulls latest, returns to branch. |
| `/simplify` | Sonnet | Inside `/implement` | Spawns 3 parallel review agents (code reuse, code quality, efficiency) to remove LLM bloat. |
| `/review-changes` | Sonnet | Optional extra check | Staff engineer review — finds bugs, gaps, architecture violations. Reports only, does not fix. |
| `/flag-mistake` | Sonnet | When Claude makes a mistake | Captures mistake as DO NOT rule in CLAUDE.md and .cursorrules. |
| `/sprint-status` | Haiku → Sonnet (auto) | Morning and end of day | Progress table: done, in progress, blocked. |
| `/watch` | Sonnet | After dispatching agents (L4+) | Background monitor — `/sprint-status` every 10 min. Session-scoped. |
| `/sprint-close` | Sonnet | **End of every sprint** | Full-codebase performance regression gate. Updates living docs. Mandatory. |
| `/level-up` | Sonnet | Exit criteria met | Generates L4 or L5 infrastructure. |
| `/level-down` | Sonnet | When stepping back | Steps down safely — does not delete higher-level infra. |
| `/team-launch [id]` | Sonnet (agents via frontmatter) | L5 — per feature | Team Lead → Backend Dev || Frontend Dev → Test Writer → Code Reviewer pipeline. |

**Model rule:** Sonnet is the default. Haiku runs automatically inside `/sprint-start`, `/sprint-status`, `/task-done`. Opus is not the default — `/model opus` for one turn if Sonnet fails on a problem, then `/model sonnet` to return.

**Per-task sequence:** `/plan` → `/implement` → `/verify` → `/commit-push-pr` → merge PR → `/task-done [id]`
**Sprint start:** `/sprint-start [N]` (one command)
**End-of-sprint:** `/sprint-close` → fix regressions → `/sprint-start [N+1]`

---

## 12. Git Conventions

**Branch naming:**
```
feature/[sprint]-[id]-[short-description]    e.g. feature/s1-001-tenant-create-action
fix/[sprint]-[id]-[short-description]        e.g. fix/s2-015-coa-balance-validation
chore/[short-description]                    e.g. chore/update-pint-config
```

**Conventional Commits format with mandatory Parsec Morse separator:**
```
feat(tenant): add CreateTenant action with TenantCreated event
.--. .- .-. ... . -.-.

[body — what changed, why, any context. Always include the Morse line as line 2.]
```

**Types:** `feat` (new capability), `fix` (bug fix), `refactor` (no behaviour change), `test` (test changes only), `docs` (docs only), `chore` (tooling, deps, config).

**Branch rules:**
- All work goes to `dev` via PR — never commit directly to `dev`, `staging`, `prod`, or `main`
- PRs require passing CI before merge
- One branch per agent session — never share a branch between two running agents
- Merge completed branches before dispatching dependent tasks

**Branch model (4-branch agent-assisted):**
- `main` → merges trigger Forge webhook → **staging** deploy
- `prod` → merges trigger Forge webhook → **production** deploy
- `dev` → all agent work merges here first
- `staging` → human-managed promotion from `main` to `staging`

`pre-commit-guard.sh` blocks: `dd()`, `var_dump()`, `console.log` (committed), `Co-Authored-By:` lines (we work alone), and missing Morse separator. Do not bypass with `--no-verify`.

---

## 13. Code Patterns

> Read `CLAUDE.md` and `.ai/guidelines/project-architecture.md` for the full set of rules. The patterns below are the ones you will use every day.

### Controller pattern — validate → delegate → respond

```php
// app/Http/Controllers/Api/V1/InvoiceController.php
final class InvoiceController extends Controller
{
    public function store(StoreInvoiceRequest $request, CreateInvoice $action): JsonResponse
    {
        $invoice = $action->execute($request->validated());

        return InvoiceResource::make($invoice)
            ->response()
            ->setStatusCode(201);
    }
}
```

Controllers never contain business logic. They validate (via FormRequest), delegate (via Action injection), and respond (via Resource).

### Action class pattern — final class, single execute(), DB::transaction, fires event

```php
// app/Domain/Invoice/Actions/CreateInvoice.php
final class CreateInvoice
{
    public function execute(array $data): Invoice
    {
        return DB::transaction(function () use ($data) {
            $invoice = Invoice::create([
                'tenant_id' => Auth::user()->tenant_id,
                'customer_id' => $data['customer_id'],
                'total_cents' => $this->sumLines($data['lines']),
                'status' => InvoiceStatus::DRAFT,
            ]);

            foreach ($data['lines'] as $line) {
                $invoice->lines()->create($line);
            }

            event(new InvoiceCreated($invoice));

            return $invoice;
        });
    }

    private function sumLines(array $lines): int
    {
        return array_sum(array_column($lines, 'amount_cents'));
    }
}
```

Action classes are `final`, have one public method (`execute()`), wrap multi-step writes in `DB::transaction`, and fire domain events for state changes.

### Test pattern — Pest + AssertsQueryPerformance

```php
// tests/Feature/Api/V1/InvoiceTest.php
uses(AssertsQueryPerformance::class);

it('creates an invoice with lines and fires event', function () {
    Event::fake([InvoiceCreated::class]);
    $tenant = Tenant::factory()->create();
    $user = User::factory()->forTenant($tenant)->create();
    $customer = Customer::factory()->forTenant($tenant)->create();

    $this->assertQueryCountAtMost(8, function () use ($user, $customer) {
        $response = $this->actingAs($user)->postJson('/api/v1/invoices', [
            'customer_id' => $customer->id,
            'lines' => [
                ['description' => 'Service A', 'amount_cents' => 100_00],
                ['description' => 'Service B', 'amount_cents' => 50_00],
            ],
        ]);

        $response->assertCreated()
            ->assertJsonPath('data.total_cents', 150_00)
            ->assertJsonPath('data.status', 'DRAFT');
    });

    Event::assertDispatched(InvoiceCreated::class);
});

it('rejects invoice creation for another tenant', function () {
    // tenant isolation invariant — global scope must filter
});
```

Every integration test asserts query count. Every test that creates models seeds tenant context. Every state-changing test asserts the corresponding event fired.

---

## 14. Data Conventions

> Full list in `CLAUDE.md` and `.ai/guidelines/data-conventions.md`. Non-negotiables:

- **IDs:** UUID v7 (per D15-R1) — sortable, time-prefixed. Never expose internal sequential IDs.
- **Money:** Integer cents — `5000` = RM50.00. Cast via `MoneyCast`. Never `float` or `decimal`.
- **Phone:** E.164 format (per D33) — column name `phone_e164`, type `VARCHAR(20)`. Always with country code, e.g. `+60123456789`.
- **NRIC (IC numbers):** Two-column encryption (per E3) — `ic_no_encrypted` (BYTEA, AWS KMS envelope) + `ic_no_hash` (CHAR(64), HMAC-SHA256 unique). **Never** `ic_number_encrypted` or `ic_number_hash` — those are the rejected names.
- **Tenant:** `tenant_id UUID NOT NULL` on every domain table. Models use `BelongsToTenant` trait. `TenantScope` global scope auto-filters every query (per D28).
- **Timestamps:** `timestampsTz()` — always timezone-aware. Store UTC. Frontend converts to `Asia/Kuala_Lumpur` for display.
- **Status fields:** PHP backed string enums under `app/Domain/[Name]/Enums/`. Frontend mirrors as `ALL-CAPS` string constants in `resources/js/lib/enums.ts`. Sync enforced by `post-edit-enum-sync-check.sh` hook.
- **Migrations:** Additive only. Never `DROP COLUMN`, never `ALTER TABLE ... TYPE`, never rename. Soft-delete obsolete columns by marking nullable + ignored. (Per CLAUDE.md DO NOT list.)

---

## 15. Prompting Patterns That Work

### Give the problem, not the solution

```
"Customers need to be able to dispute an invoice from their account page.
The dispute should pause automated payment reminders and notify the operations team.
Story: STORY-INV-031. Flow: BUSINESS_FLOWS.md#invoice-dispute.
Implement following the domain patterns in CLAUDE.md."
```

### Force verification on complex features

```
"Prove this works by writing tests that:
1. Create a balanced journal entry. Verify both debit and credit lines persist and SUM = 0.
2. Attempt unbalanced entry. Verify rejection with the correct error code.
3. Verify InvoiceCreated event fires. Verify activity log entry exists with correct subject.
Run the suite and show me the output before committing."
```

### Second Claude reviews first Claude

For critical logic (journal posting, e-invoice submission, Tawarruq trail, PDPA handlers):
```
# Worktree A: implements the feature
# Worktree B (read-only `zq`): reviews it

"Review the changes in ../amir as a staff engineer.
Story: STORY-XXX. ACs: [paste].
Find every bug, missing edge case, security issue, and architecture violation.
Do NOT fix — only report. Group by BLOCKER / IMPORTANT / SUGGESTION."
```

### When Claude goes off-track

```
"Stop. Re-read CLAUDE.md, the ARCHITECTURE.md domain section, and this task's ACs.
Re-plan from scratch using the correct patterns.
Explain what went wrong before you start."
```

---

## 16. When Things Go Wrong

| Symptom | Fix |
|---------|-----|
| Claude ignoring architecture rules | `/flag-mistake Claude put business logic in controller` — then: "Re-read CLAUDE.md. Re-plan." |
| Claude weakening failing tests | "The tests are correct. Fix the implementation, not the tests." |
| Business logic in controllers | "Move all logic to an Action class. Controllers only validate, delegate, and respond." |
| Changes piling up without commits | Run `/verify` then `/commit-push-pr`. Small verified commits beat large uncertain ones. |
| Context window filling up | Start a new session. Run `/sprint-status` to reorient. |
| Claude going in circles | "Scrap this approach. Implement the simplest correct solution from scratch." |
| Inconsistent patterns across tasks | Run `/review-changes`. Update CLAUDE.md. Fix it now. |
| Branches diverged too much | Merge completed branches to dev before dispatching more agents. |
| Sprint backlog empty at start | `/sprint-start [N]` — reads SPRINT_PLAN.md and populates. (For AMIR, backlog is pre-populated.) |
| `dispatch.sh` not configured for current sprint | `/sprint-start [N]` — reconfigures dispatch.sh at L4. |
| Hitting usage limits | Confirm `settings.json` has `"model": "sonnet"`. Use `/sprint-start`, `/sprint-status`, `/task-done` — they switch to Haiku for mechanical steps automatically. |
| L4: agents touching the same files | Reorganise the batch — shared-file tasks must be on the same worktree (sequential). Re-run `/sprint-start [N]` to regenerate. |
| L5: team lead missed enum values or locked decisions | `/flag-mistake`. Confirm DECISIONS_LOG entry exists and is locked. |
| Migration conflicts with existing schema | "Use the `database_schema` Boost tool to read the current schema before planning." |
| Agent hallucinated a package API | "Use `search_docs` Boost tool to find the correct API for the version we're running." |
| Unclear test failure | "Use `last_errors` Boost tool to read the full Laravel log before fixing." |
| Duplicate route added | "Use `list_routes` Boost tool to check all registered routes before planning route changes." |
| MyInvois sandbox returns 5xx | Check LHDN preprod status page. Document the outage in `INTEGRATION_LOG.md`. Do NOT retry blindly — preprod has tight rate limits. |
| Tenant scope leak (cross-tenant data visible) | This is a P0. Run the cross-tenant invariant test. `/flag-mistake` immediately. Block the merge. |
| Journal post fails with "imbalanced" | The Action's invariant is working. Check the input — debit + credit lines must sum to 0 cents. |
| Ar-Rahnu Tawarruq trail is incomplete | All 4 legs must post atomically. Check `.claude/skills/writing-ar-rahnu.md` and the Action's `DB::transaction`. |
| PDPA DSAR not delivered within 21 days | Check `pdpa_dsar_requests.due_at` indexing. The scheduled job must scan and alert daily. |

---

## 17. Principles

```
1.  CLAUDE.md is law — if it's not written, agents won't follow it
2.  Plan before implement — every task, every time
3.  Verify before commit — every task, every time
4.  Review before merge — every PR, every time
5.  Read ## Assumptions in every PR — unread assumptions become bugs
6.  Sonnet is the default — Haiku for mechanical, Opus only if Sonnet fails
7.  Model switching is automatic — commands and frontmatter handle it
8.  /sprint-start before any sprint work — one command handles all pre-flight
9.  Performance is asserted in tests, not optimised after launch
10. One branch per agent — no file overlap between concurrent agents
11. /flag-mistake immediately — not at end of sprint
12. Small focused tasks produce better output than large vague ones
13. Second Claude reviewing first Claude catches what you'd miss
14. Merge to dev often — never let branches diverge more than one sprint
15. /sprint-close is mandatory — never advance the sprint counter manually
16. Start fresh sessions per task — don't carry context across unrelated work
17. Tenant isolation is non-negotiable — every query must scope by tenant_id (D28)
18. Money is integer cents always — no exceptions, no shortcuts
19. Audit log every state change — spatie/laravel-activitylog v4 is enabled for a reason
20. Read living docs before planning — they reflect actual current state
```

---

**For new team members:**

Day 1: Read `CLAUDE.md`, this file, and `ARCHITECTURE.md`. Run `bootstrap.sh` if the repo isn't set up yet, otherwise follow Section 3 setup. Run `php artisan test` and confirm green.

Day 2: Read `.claude/rules/AGENT_GUIDE.md`, all 8 files in `.claude/skills/`, and the 7 living docs in `docs/living/`. Pick a `pending` task in `.sprint-backlog.json` and run `/plan [task-id]`.

Day 3+: Follow the standard task flow.

When in doubt: `CLAUDE.md` for rules, `ARCHITECTURE.md` for design, `docs/living/` for current state, `DECISIONS_LOG.md` for history. Living docs win conflicts.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing PREFLIGHT_CHECKLIST.md (14,050 bytes)"
cat > 'PREFLIGHT_CHECKLIST.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised reproduction or use outside of
  Parsec Sdn. Bhd.-authorised projects is prohibited.
-->

# AMIR — Pre-Flight Checklist

**Phase:** 12 (Pre-Flight + Bootstrap)
**Document Version:** 1.0
**Last Updated:** 2026-05-05
**Tender Deadline:** 21 May 2026 (16 days)
**v1 Pilot Target:** ~25 August 2026

This document tracks every external dependency, account, registration, and infrastructure setup that must happen *outside* the codebase. Many items have multi-week lead times — start them now, in parallel with development. The principle from Framework §12.7: **only start things when you need them.** Items that block the demo (21 May) start today; items that block v1 production (~25 Aug) start in mid-June; items that block AMIR commercial expansion start in Q4.

---

## How to Use This Document

For every item:
1. Check it against the **Earliest start** column — has the trigger date arrived?
2. Check **Blocks** — what won't ship if this isn't done?
3. Mark the checkbox `[x]` when complete; record the date in the Notes column.
4. If an item slips past its earliest start date, **flag immediately** in the daily standup notes — long-lead items have no recovery path.

**Critical path warning:** Three items have hard external deadlines that cannot be moved:
- **MyInvois Public Key API access (LHDN)** — required for any e-Invoice work; LHDN approval has been ~2-3 weeks historically.
- **WhatsApp Business API approval (Meta)** — required for v1 notifications; 2-4 weeks typical.
- **Domain DNS propagation** — 24-48hr after configuration.

If MyInvois access is not granted by mid-July, S25-S26 (the e-Invoice sprint pair) cannot ship and v1 must defer e-Invoice. Have a fallback plan documented.

---

## GROUP 1 — Day 0: Long Lead Times (start NOW)

These have multi-week approval/registration windows. Submit them this week (5-11 May 2026). They run in the background while demo development proceeds.

| ☐ | Task | Service | Lead Time | Blocks | Earliest Start | Notes |
|---|------|---------|-----------|--------|----------------|-------|
| [ ] | **MyInvois Public Key API access** | LHDN MyInvois Sandbox + Production | 2-3 weeks | S25 (e-Invoice integration), S26 (consolidated invoices) | Today (5 May 2026) | Apply at [sdk.myinvois.hasil.gov.my](https://sdk.myinvois.hasil.gov.my). Sandbox first, then production. Need TIN + SSM cert. |
| [ ] | **WhatsApp Business API verification** | Meta Business | 2-4 weeks | S08 (member notifications), S22 (signal alerts via WhatsApp) | Today | Apply via Meta Business Suite. Submit message templates for member notification + signal alert categories. |
| [ ] | **AWS account (ap-southeast-3 Jakarta region)** | AWS | Same day, but billing verification 1-2 days | S00 demo deploy (Forge needs AWS) | Today | AMIR is Malaysia-residency per Decision D9; ap-southeast-3 Jakarta is the closest region with full service coverage (Singapore acceptable as fallback). |
| [ ] | **Forge account + AWS connection** | Laravel Forge | Same day | S00 D1.15 (demo deploy task) | Today | Connect Forge to AWS account. Forge will provision droplets and managed Postgres. |
| [ ] | **Anthropic API key (production tier)** | Anthropic | Same day | S22 (signal LLM explanations), S39 (analytics narratives) | Today | Per D12. Use Sonnet for production paths, Haiku for cost-sensitive paths. Set spending limits. |
| [ ] | **Sentry account (Team plan)** | Sentry | Same day | S41 (production hardening) | 1 June 2026 | Per D23. Configure PII scrubbing in `beforeSend`. Replays disabled per security review. |
| [ ] | **GitHub organisation + private repo** | GitHub | Same day | bootstrap.sh Path A | Today | Create org `parsec-my` (or similar). Repo `amir`. Enable branch protection on main, staging, prod after first push. |
| [ ] | **Domain registration: amir.com.my (or chosen)** | MyNIC / GoDaddy / Namecheap | 24-48hr DNS propagation | Demo URL, production URL | Today | `.my` domains require Malaysian-entity proof — provide SSM cert. Backup options: `amir.app`, `amir-coop.com`. |
| [ ] | **SSL via Let's Encrypt** | Forge → Let's Encrypt | Automated, same day after DNS | Demo deploy | After domain DNS propagation | Forge automates this entirely. |

---

## GROUP 1.5 — Tender Submission Path (HARD DEADLINE: 21 May 2026)

Tender-specific items. These exist outside the normal pre-flight cycle because they have a fixed deadline and a fixed scope.

| ☐ | Task | Owner | Deadline | Notes |
|---|------|-------|----------|-------|
| [ ] | **Read tender attachments end-to-end** | Founder | 8 May 2026 | LampiranA (spesifikasi), LampiranA5i/A5ii (SLA jaminan + penalti), LampiranA6 (SLA pembangunan), LampiranB (jadual harga), Senarai Semak Cadangan Teknikal. The attachments in `/mnt/project/` are the source. |
| [ ] | **Demo build complete and deployed** | Dev | 18 May 2026 (3-day buffer) | Demo sprint S00 ends 4 June, but tender demo URL must be live by 18 May. Demo scope per SPRINT_PLAN.md. |
| [ ] | **Tender response document drafted** | Founder | 19 May 2026 | Pull content from VIABILITY_REPORT.md, ARCHITECTURE.md, SPRINT_PLAN.md. Tender package is a separate document, not part of the build. |
| [ ] | **Pricing schedule (LampiranB) populated** | Founder | 19 May 2026 | Use the Excel file from `/mnt/project/19_LAMPIRANBJADUALPEMATUHANHARGA.xlsm`. |
| [ ] | **Specification compliance schedule (LampiranA)** | Founder | 19 May 2026 | Use `/mnt/project/4_LAMPIRANAJADUALPEMATUHANSPESIFIKASI.xlsm`. Cross-reference against ARCHITECTURE.md and SPRINT_PLAN.md to mark Yes/No/Partial for each spec line. |
| [ ] | **Senarai Semak (Technical Proposal Checklist)** | Founder | 19 May 2026 | Use `/mnt/project/18_SENARAISEMAKCADANGANTEKNIKALS0052026.pdf` as the master list. Every item has a corresponding deliverable from the planning docs. |
| [ ] | **Tender package send to SKM** | Founder | **21 May 2026 EOD** | This is the immovable deadline. Send-day code budget per SPRINT_PLAN.md is task D3.12 only — clear the afternoon. |

---

## GROUP 2 — Dev Environment (Before running bootstrap.sh, ~1-2 hours)

No lead time. Install missing tools, then run the script.

| ☐ | Task | Command / Action | Notes |
|---|------|------------------|-------|
| [ ] | PHP 8.3 installed | `php --version` → must show 8.3.x | Per Decision D1. Use Herd for macOS (managed installer). |
| [ ] | Composer installed | `composer --version` | |
| [ ] | Node.js 22 LTS installed | `node --version` → must show v22.x | Per dispatch.sh PATH config. NVM recommended. |
| [ ] | npm | `npm --version` | Bundled with Node. |
| [ ] | Git installed and configured | `git config --global user.name`, `user.email` | |
| [ ] | Claude Code installed | `npm install -g @anthropic-ai/claude-code` | Login via `claude /login`. |
| [ ] | tmux installed | `brew install tmux` (macOS) or apt equivalent | Required for L4+ multi-agent sessions. |
| [ ] | gh (GitHub CLI) installed | `brew install gh` (macOS) | Required by `commit-push-pr.md` for PR creation. |
| [ ] | **Run `bootstrap.sh`** | `bash bootstrap.sh` (interactive — answers two questions) | Scaffolds the project, writes all framework files, makes initial commit. |
| [ ] | Git worktrees verified | `git worktree list` shows 5 entries (primary + 4 agents) | bootstrap.sh creates these. |
| [ ] | Shell aliases sourced | `which za` returns the alias path | bootstrap.sh appends to `~/.zshrc`. Open a new terminal tab. |
| [ ] | **Laravel Boost installed** | `composer require laravel/boost --dev` | Per Framework §12.4. Boost provides MCP tools for schema introspection. |
| [ ] | **Boost installer run** | `php artisan boost:install` | Auto-detects Claude Code. Configures MCP. |
| [ ] | **Boost MCP connection verified** | `claude mcp list` → must show `laravel-boost` | Without this, agents cannot use `database_schema`, `tinker`, etc. |
| [ ] | Query detector installed | `composer require beyondcode/laravel-query-detector --dev` | Catches N+1 queries in dev/test. |

---

## GROUP 3 — Before the Hardening Sprint (S41, ~end July 2026)

Set up production infrastructure 2-3 sprints before pilot launch. Earlier means paying for unused capacity.

### Infrastructure

| ☐ | Task | Earliest Start | Notes |
|---|------|----------------|-------|
| [ ] | **Production AWS droplet** (separate from staging) | Sprint 38 (~mid-July) | Forge provisions. 4 vCPU / 8 GB RAM minimum for v1 pilot. |
| [ ] | **Production Postgres 16 (managed)** | Sprint 38 | AWS RDS or Forge-managed. Backups on, retention 7 days. |
| [ ] | **Production Redis 7 (managed)** | Sprint 38 | Forge-managed or ElastiCache. |
| [ ] | **S3-compatible storage bucket** | Sprint 38 | AWS S3, ap-southeast-3. Per D9. |
| [ ] | **CI/CD: production deploy hook** | Sprint 38 | GitHub Actions on `prod` branch merge → Forge webhook. Per D21 (4-branch model). |
| [ ] | **Secrets management** | Sprint 38 | Forge environment variables for prod. Anthropic API key, Sentry DSN, MyInvois creds, etc. |
| [ ] | **Sentry production project** | Sprint 38 | Separate from staging. Configure `beforeSend` PII scrubbing. |
| [ ] | **Email delivery (AWS SES)** | Sprint 38 | Per D11. Verify sending domain (DKIM/SPF). Backup provider: Postmark or SendGrid. |
| [ ] | **Monitoring: UptimeRobot or equivalent** | Sprint 38 | Pings every 5 min on production health endpoint. |
| [ ] | **Daily database backup verified** | Sprint 39 | Forge-managed backups + manual restore test. |
| [ ] | **DNS pointing to production** | Sprint 41 (launch week) | Until then, `amir.com.my` points to staging. |

### Compliance & Legal

| ☐ | Task | Lead Time | Notes |
|---|------|-----------|-------|
| [ ] | **PDPA Commissioner notification** (s.13) | 30 days mandatory | Submit before processing first real user data. Required for any koperasi pilot. |
| [ ] | **Privacy Policy drafted + lawyer-reviewed** | 1-2 weeks lawyer | Pull from CONTENT_COPY.md §6 (legal). Section 8 specifically called out for lawyer review. |
| [ ] | **Terms of Service drafted + lawyer-reviewed** | 1-2 weeks lawyer | Section 12 specifically called out for lawyer review. |
| [ ] | **Data Processing Agreement template** | 1 week | For each koperasi pilot, sign a DPA covering tenant_id-scoped data. |
| [ ] | **DBN Guideline 2025 conformance review** | Internal | S38 covers code-side; legal-side conformance documented in `docs/PDPA_COMPLIANCE.md`. |
| [ ] | **Akta Koperasi 1993 retention review** | Internal | 6-year retention for financial records confirmed in DECISIONS_LOG.md. Document and post-launch audit. |
| [ ] | **SKM tender contract finalised** (if won) | Variable | Post-tender award. Negotiate SLA terms (LampiranA5 ref). |

### Content & Assets

| ☐ | Task | Lead Time | Notes |
|---|------|-----------|-------|
| [ ] | **AMIR logo finalised** | 1-2 weeks | Currently placeholder in design system. Lockup for sidebar + login + emails. |
| [ ] | **Favicon + app icon set** | 1 week | 16/32/48/192/512 PNG + SVG. |
| [ ] | **Transactional email templates** | 1-2 weeks | Per CONTENT_COPY.md §7. BM and EN versions. Test render in Litmus or equivalent. |
| [ ] | **WhatsApp message templates submitted to Meta** | 4-6 weeks total approval | Submit in S08 once WhatsApp Business is approved. Review and re-submit any rejections. |
| [ ] | **BM-EN parity QC pass** | 1 week | Native BM speaker reviews every translation key in `lang/ms/`. Catches awkward translations before launch. |
| [ ] | **Demo seed data prepared** | Sprint 1 | Koperasi Wawasan demo tenant with realistic data. Per S00 D1.09 task. |

---

## GROUP 4 — Pilot Launch Week (Sprint 42, ~25 August 2026)

Final cutover items.

| ☐ | Task | Notes |
|---|------|-------|
| [ ] | **Production smoke test** | Full happy-path run-through: login, create transaction, post journal, generate Penyata Trial Balance, log out. |
| [ ] | **First pilot koperasi onboarded** (manual) | Founder onboards in person or via screen share. Document any friction in `docs/living/DEVIATION_LOG.md`. |
| [ ] | **Production seed data loaded** | Master CoA template (GP23 87 accounts), Pack catalogue, default permissions. Per S38-S40 tasks. |
| [ ] | **Backup restored to fresh DB** (test) | Confirm backups are recoverable, not just being taken. |
| [ ] | **Sentry alerts firing on errors** | Verify by intentionally throwing a test error. |
| [ ] | **UptimeRobot alerts to founder phone** | Verify by stopping production for 60 seconds. |
| [ ] | **Documentation handed to first pilot user** | Quick-start guide (BM + EN) covering: login, daily transaction entry, monthly close. Pull content from CONTENT_COPY.md §5 (help text). |

---

## Critical Path Visualisation

```
Today (5 May)                     21 May                  ~25 Aug
    │                               │                        │
    │── MyInvois access ────────────┼────────────────────────│ S25/S26 done
    │── WhatsApp Business ──────────┼────────────────────────│ S22 ready
    │── AWS + Forge + GitHub ──── Demo deploy │──── Production deploy ──│
    │── Domain DNS ────── live │
    │── Sentry account ─────────────────────────── 1 Jun ─── Production tracking ───│
    │                                                                  │
    │── Demo sprint S00 ──────────── Tender Day 21 May ────────────────│
                                     ▲ HARD DEADLINE
```

**Single biggest risk:** MyInvois Public Key API access slip past mid-July. Mitigation: apply today (5 May); follow up weekly; have S25 contingency that defers e-Invoice integration to v1.1 if access doesn't arrive.

---

## Daily Check-In

Suggested daily standup (5 min):
1. What is at risk on this checklist today? (any item past its earliest-start date)
2. Any external response received? (LHDN, Meta, AWS billing, lawyer)
3. Any item that needs founder action today?

Track answers in a daily log file or Slack channel — not in this checklist (this stays a static reference).
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .cursorrules (4,926 bytes)"
cat > '.cursorrules' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
# AMIR — Cursor / IDE Inline Assistant Rules
# Generated by Parsec Sdn. Bhd. AI Development Framework v2
# © 2026 Parsec Sdn. Bhd. — Internal use only.
#
# This file is a condensed version of CLAUDE.md for the IDE inline assistant.
# When in doubt, the full CLAUDE.md and DECISIONS_LOG.md are the source of truth.

# ───────────────────── Stack ─────────────────────
# Laravel 12 · PHP 8.3 · Postgres 16 · Redis · Inertia v2 · React 18 · TypeScript · Tailwind 3 · Pest

# ───────────────────── Hard Conventions ─────────────────────
# 1. final class on every class
# 2. declare(strict_types=1) at top of every PHP file
# 3. UUID v7 PKs via HasUuids trait + newUniqueId() returning Str::uuid7()
# 4. Money as BIGINT *_cents columns + MoneyCast::class
# 5. Phone as phone_e164 VARCHAR(20) with /^\+60\d{9,10}$/ validation
# 6. Encrypted IDs via E3 two-column pattern: *_encrypted TEXT + *_hash CHAR(64)
# 7. timestampsTz() on every table, softDeletesTz() for entities with audit-trail lifecycle
# 8. version INT DEFAULT 1 for entities with concurrent-edit potential
# 9. Tenant-scoped models: use BelongsToTenant; trait
# 10. Single-action controllers (__invoke only)
# 11. Action class pattern: app/Domain/[Name]/Actions/[VerbNoun].php, single execute(), DB::transaction, dispatches event
# 12. All API routes under /api/v1/

# ───────────────────── DO NOT ─────────────────────
# - DO NOT put business logic in controllers. DO: put it in Action classes under app/Domain/[Name]/Actions/.
# - DO NOT use float arithmetic for money. DO: integer cents throughout; cast as MoneyCast::class.
# - DO NOT use plain phone column. DO: phone_e164 VARCHAR(20) per Decision D33.
# - DO NOT skip the BelongsToTenant trait on tenant-scoped models. DO: every tenant-owned model uses the trait per Decision D28.
# - DO NOT use dd(), dump(), var_dump(), ray(), console.log(), console.warn(), console.error(), console.debug() in committed code. The pre-commit-guard hook will hard-block.
# - DO NOT use <form> tags with type=submit in multi-section forms. DO: <div> + <button type="button"> with explicit onClick handler (Lesson 12-L19).
# - DO NOT guess enum values. DO: read the PHP enum file first; use exact case values (Lesson 12-L16).
# - DO NOT use /api/ in test URLs. DO: /api/v1/ prefix on all test assertions (Lesson 12-L17).
# - DO NOT add Co-Authored-By: lines to commits. The pre-commit-guard hook will hard-block.
# - DO NOT silently drift from DECISIONS_LOG.md. DO: raise a revision (Dxx-R1) and get founder approval before changing.
# - DO NOT overwrite a file that already exists on dev. DO: read it first; build on it (Lesson 12-L14).

# ───────────────────── Commit Format ─────────────────────
# [type]([scope]): [description]
# .--. .- .-. ... . -.-.
# 
# [body — what changed, why]
#
# types: feat, fix, refactor, test, docs, chore, build, ci

# ───────────────────── File Layout ─────────────────────
# app/Domain/[Name]/
#   Actions/         — business operations (single execute method)
#   Models/          — Eloquent models (state, no behaviour)
#   Enums/           — backed enums for status/type fields
#   Events/          — past-tense domain events
#   Listeners/       — event handlers (delegate to Actions)
#   Policies/        — authorization checks
#   Requests/        — FormRequest validation
#   Resources/       — JsonResource for API responses
#   Jobs/            — queued work (constructor takes IDs, not models)
#   Services/        — only when Actions don't fit (rare)
#   Contracts/       — interfaces for external integrations
#
# tests/Feature/Api/V1/[Name]/   — feature tests
# tests/Unit/[Name]/             — unit tests
# database/migrations/           — schema migrations
# database/factories/            — model factories
# resources/js/Pages/[Name]/     — Inertia React pages
# resources/js/components/       — shared React components

# ───────────────────── When Generating Code ─────────────────────
# - Match existing patterns in the codebase. Do not invent.
# - For Action classes: see app/Domain/Members/Actions/ for the canonical pattern.
# - For tests: see tests/Feature/Api/V1/Members/ — every test asserts query count.
# - For migrations: additive only; never drop/rename in a single migration.
# - For Inertia pages: every new page wired in router.tsx + has navigation link.

# ───────────────────── Quick Reference: Decision IDs ─────────────────────
# D15-R1 — UUID v7 (not ULID)
# D16    — Money as integer cents + MoneyCast
# D17    — UTC storage, MYT display
# D19    — Audit log via spatie/laravel-activitylog with custom events
# D28    — tenant_id (not company_id), BelongsToTenant trait
# D30    — Action class pattern
# D31    — Single-action controllers (__invoke only)
# D32    — final class on every application class
# D33    — phone_e164 VARCHAR(20) format
# E3     — Two-column encryption (encrypted + hash)
# C16    — Journal approval threshold (RM 5,000 default)
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing dispatch.sh (14,613 bytes)"
cat > 'dispatch.sh' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────
# Parsec Sdn. Bhd. · AMIR
# Generated by the Parsec Sdn. Bhd. AI Development Framework v2
# © 2026 Parsec Sdn. Bhd.. All rights reserved.
# Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised
# projects is prohibited.
# ─────────────────────────────────────────────────────────────
# dispatch.sh — Level 4 parallel agent dispatch
# Usage: ./dispatch.sh [batch-name]
# Example: ./dispatch.sh members-schema
# Batch names are more resilient than numbers — adding tasks mid-sprint doesn't shift ordering.
# Launches Claude Code agents in tmux panes, one per worktree in the batch.
# Output is logged per agent to logs/agent-[worktree].log for visibility.
# SPRINT CONFIG section must be updated at every sprint-close for the next sprint.
#
# Bash 3 compatible (macOS ships Bash 3 — no namerefs, no declare -A) per Lesson 12-L10
# shellcheck shell=bash
set -euo pipefail

# ── RUNTIME PATH (set explicitly — tmux does not load shell profile per Lesson 12-L9) ───────
export PATH="/opt/homebrew/opt/node@22/bin:$HOME/.nvm/versions/node/v22.0.0/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
# Adjust Node path to match your installed version: node --version
# ── END RUNTIME PATH ─────────────────────────────────────────────────────────────────────────

# ── SPRINT CONFIG (update this section every sprint via /sprint-close Step 4e) ──────────
# Batch layout — one string per named batch, space-separated entries
# Each entry format: task_id:branch:relative_worktree_path
# Batch names must match the "batch" field in .sprint-backlog.json (when batched at L4)
# Tasks in the same batch MUST NOT touch overlapping files — verify files_touched in backlog
#
# AMIR is currently at Level 3 (Manual). This file ships scaffolded for Level 4 promotion.
# Populate the BATCH_* entries when promoting to Level 4 via /level-up.

# Example batches (replace at promotion time):
BATCH_demo_scaffold="D1.01:feature/d101-laravel-scaffold:../amir-b D1.02:feature/d102-postgres-config:../amir-c"
BATCH_demo_auth="D1.05:feature/d105-auth-tenant:../amir-b D1.06:feature/d106-tenant-context:../amir-c"
# Add more named batches as the sprint plan progresses

# Stack commands
TEST_CMD="php artisan test --stop-on-failure"
LINT_CMD="./vendor/bin/pint --test"
ANALYSE_CMD="./vendor/bin/phpstan analyse --no-progress --memory-limit=512M"
# ── END SPRINT CONFIG ───────────────────────────────────────────────────────────────────────

TMUX_SESSION="amir-dispatch"
PRIMARY_DIR="$(pwd)"
mkdir -p "$PRIMARY_DIR/logs"

get_batch() {
  local name
  name="$(echo "$1" | tr '-' '_')"
  echo "$name" | grep -qE '^[a-zA-Z0-9_]+$' || { echo "Invalid batch name: $1"; exit 1; }
  eval "echo \"\${BATCH_${name}:-}\""
}

usage() {
  echo "Usage: ./dispatch.sh [batch-name]"
  echo "Defined batches:"
  grep '^BATCH_' "$0" | sed 's/BATCH_//;s/=.*//' | tr '_' '-'
  exit 1
}

[ $# -eq 1 ] || usage
BATCH_NAME="$1"
BATCH_ENTRIES="$(get_batch "$BATCH_NAME")"
[ -n "$BATCH_ENTRIES" ] || { echo "No entries in batch '$BATCH_NAME'. Check SPRINT CONFIG."; exit 1; }

build_prompt() {
  local task_id="$1"
  local branch="$2"
  cat <<PROMPT
You are a coding agent working on AMIR task $task_id in branch $branch.

PHASE 1 — BASELINE
Record pre-existing test failures before touching any code:
1. Run $TEST_CMD and note any failures. These are pre-existing — do not fix them. Only new failures you introduce are your responsibility.

PHASE 2 — PLAN
Read all 6 documents before writing a single line of code:
2. Read .sprint-backlog.json and find task $task_id. Read its prompt, acceptance criteria, files_touched, and branch name carefully.
3. Read CLAUDE.md fully. All conventions here are mandatory.
4. Read the relevant flow section in BUSINESS_FLOWS.md for the flows cited in the task.
5. Read ARCHITECTURE.md for schema definitions, data conventions, and module boundaries relevant to this task.
6. Read DECISIONS_LOG.md — search for keywords related to this task. Decisions here are LOCKED and override any assumptions. Pay special attention to D15-R1 (UUID v7), D16 (MoneyCast), D28 (BelongsToTenant), D30 (Action class), D31 (single-action controllers), D32 (final class), D33 (phone_e164), and E3 (two-column encryption).
7. Read CONTENT_COPY.md for any UI strings, labels, or error messages related to this task.
8. List every file you will create or modify.
9. Identify risks and ambiguities. If any field type, enum value, validation rule, or business rule is unclear from the docs, do NOT assume — document it under ## Assumptions in the PR description so the reviewer can verify before merging.

DOMAIN-SPECIFIC SKILLS
If your task touches one of these domains, ALSO read the matching skill:
- Accounting / Transactions / Penyata → .claude/skills/writing-journal-entries.md
- EInvoice / MyInvois → .claude/skills/writing-myinvois-integration.md
- ArRahnu → .claude/skills/writing-ar-rahnu.md
- Pdpa → .claude/skills/writing-pdpa-handlers.md

PHASE 3 — IMPLEMENT
10. Before creating any file, check if it already exists on the branch or was recently merged to dev (Lesson 12-L14):
    git log --oneline origin/dev -20
    ls [relevant directories]
    If a file exists, read it and build on it — never overwrite or duplicate.
11. Implement the task following all CLAUDE.md conventions.
    BACKEND TASKS:
      - UUID v7 PKs via HasUuids trait + newUniqueId returning Str::uuid7()
      - timestampsTz on every table; softDeletesTz for entities with audit-trail lifecycle
      - Money columns: BIGINT *_cents + MoneyCast::class
      - Phone columns: phone_e164 VARCHAR(20)
      - Encrypted IDs: *_encrypted TEXT + *_hash CHAR(64) two-column
      - Tenant-scoped models: use BelongsToTenant; (post-edit hook will warn if missing)
      - final class on every class
      - Action class pattern: single execute method, DB::transaction, dispatches event
      - Single-action controllers: __invoke only
      - All API routes under /api/v1/
    FRONTEND TASKS: you MUST add the route to router.tsx AND a navigation link (sidebar, settings index, or parent page). A page without navigation is unreachable and counts as incomplete.
    FRONTEND LIST PAGES: use the shared DataTable component — do not build a custom table from scratch.
    ENUM VALUES: ALWAYS read the PHP enum file to get exact case values. Never guess (Lesson 12-L16).
    TEST URLS: the API prefix is /api/v1/ — all test assertions must use this prefix, not /api/ (Lesson 12-L17).
    FORMS: NEVER use <form> tags with type=submit in multi-section forms (Lesson 12-L19). Use <div> and type=button with explicit onClick handlers.
    VALIDATION (AMIR-specific): IC numbers (12 digits, validated against MyKad checksum), phone (+60 followed by 9-10 digits), bank account (digits only), postcode (5 digits). Use enum dropdowns for: nationality, race, religion, employment_type. PCB category auto-set from marital status.
    COMMIT FORMAT: include the Parsec separator on its own line immediately after the subject. No Co-Authored-By lines.

PHASE 3.5 — SIMPLIFY (mandatory — do not skip)
12. Once all tests pass, review every file you changed for LLM bloat:
    - Remove unused imports, variables, and function parameters
    - Inline any helper/utility method that is only called once (unless it significantly aids readability)
    - Remove defensive null checks for values that can never be null (check the type signature)
    - Remove error handling for impossible scenarios (catches for exceptions that are never thrown)
    - Remove any abstraction layer (base class, interface, wrapper) with only one implementation
    - Delete commented-out code — git has history
    - Simplify overly complex conditionals (nested ternaries, 4+ boolean conditions)
    - If a variable is assigned then immediately returned, return the expression directly
    - Confirm every file you created is wired into routes/navigation/called by other code
    - Confirm every new line traces to a specific acceptance criterion — remove speculative code
13. Run $TEST_CMD again after simplification — zero new failures

PHASE 4 — VERIFY (fix all failures before proceeding)
14. Run $TEST_CMD — zero NEW failures allowed (pre-existing failures from Phase 1 baseline are OK)
15. Run $LINT_CMD — zero errors
16. Run $ANALYSE_CMD — zero errors
17. Check BelongsToTenant trait is active on all new tenant-scoped models
18. Check no money stored as float — BIGINT *_cents only, MoneyCast::class
19. Run git diff --stat — confirm ONLY files in this task's scope are changed
20. grep test files for /api/ without /v1/ — zero hits allowed:
    grep -rn '"/api/' tests/ | grep -v '/v1/'
21. grep changed files for merge conflict markers — zero hits allowed (Lesson 12-L18):
    grep -rn '<<<<<\|>>>>>\|=======' --include='*.php' --include='*.tsx' --include='*.jsx'
22. If frontend: read PHP enum files and confirm all enum values in JSX/TSX match exactly
23. If frontend: design system compliance check:
    - All UI uses design system components (Button, DataTable, FormField, StatusBadge) — no raw HTML
    - Zero hardcoded colour/spacing/typography/radius/shadow values — all via design tokens
    - All status displays use StatusBadge with enum value — no manual badge colours
    - Page uses a standard layout pattern (L0-L5) — no invented layouts
    - No ad-hoc components duplicating existing design system components
    - Every user-facing string uses __() (BM and EN parity)

PHASE 5 — SHIP
24. Stage all changes: git add -A
25. Commit with Parsec separator — no Co-Authored-By:
    git commit -m "type(scope): description
.--. .- .-. ... . -.-.

[what changed and why]"
26. Push: git push -u origin $branch
27. Open PR:
    gh pr create --base dev --title "type(scope): description" --body "## What This Does

[what this PR does — be specific]

## Story / Task
$task_id — [task title from .sprint-backlog.json]

## Acceptance Criteria
- [x] AC1: ...
- [x] AC2: ...

## Implementation Notes
[non-obvious decisions]

## Assumptions
[anything unclear from docs that you had to assume — enum values, validation rules, edge cases. Write 'None' if everything was clear.]

## Test Plan
- [x] $TEST_CMD — zero new failures
- [x] $LINT_CMD — clean
- [x] $ANALYSE_CMD — clean
- [x] All test URLs use /api/v1/ prefix
- [x] No merge conflict markers
- [x] Frontend enum values match PHP enums (if applicable)
- [x] Cross-tenant isolation test included (if tenant-scoped feature)

## Decisions Trace
- [list every Decision ID this PR is bound by — D-series and E-series]"
28. Output the PR URL.

Do not stop after implementing. The task is only complete when the PR is open on GitHub.
PROMPT
}

# Start or attach tmux session
tmux has-session -t "$TMUX_SESSION" 2>/dev/null || tmux new-session -d -s "$TMUX_SESSION" -x 220 -y 50

PANE=0
PREPARED_WORKTREES=""  # tracks worktrees already prepared (Bash 3 compatible — string-based per Lesson 12-L21)

is_prepared() {
  echo "$PREPARED_WORKTREES" | grep -qF "|$1|"
}

for entry in $BATCH_ENTRIES; do
  TASK_ID="${entry%%:*}"
  REST="${entry#*:}"
  BRANCH="${REST%%:*}"
  WORKTREE="${REST#*:}"

  WORKTREE_ABS="$(cd "$PRIMARY_DIR" && cd "$WORKTREE" 2>/dev/null && pwd || echo "$PRIMARY_DIR/$WORKTREE")"
  WT_NAME="$(basename "$WORKTREE")"
  LOG_FILE="$PRIMARY_DIR/logs/agent-${WT_NAME}.log"
  PROMPT="$(build_prompt "$TASK_ID" "$BRANCH")"
  AGENT_CMD="claude -p $(printf '%q' "$PROMPT") 2>&1 | tee -a $LOG_FILE"

  if is_prepared "$WT_NAME"; then
    # Same worktree — chain this task after the previous one (Lesson 12-L21)
    # Second task creates its branch from first task's committed HEAD, not from dev.
    # Since the first command was already sent to the pane with `tmux send-keys`, we append
    # a chained command. The second task waits for the first to complete before starting.
    CHAIN_CMD="git checkout -B $BRANCH && $AGENT_CMD"
    # Find the pane index for this worktree and append the chained command
    PANE_INDEX="$(echo "$PREPARED_WORKTREES" | tr '|' '\n' | grep -n "^${WT_NAME}$" | head -1 | cut -d: -f1)"
    PANE_INDEX=$((PANE_INDEX - 1))
    # Append " && [chained command]" to that pane via send-keys after a wait
    # The pane is currently running the previous task; we queue the next command
    tmux send-keys -t "$TMUX_SESSION.$PANE_INDEX" " && $CHAIN_CMD" 
    # Don't press Enter — the user types Enter when ready, OR the previous && chain handles it
    # Actually for correctness: the pane already has Enter pressed for the first command,
    # so the second && needs to be added BEFORE Enter — which we can't do cleanly in tmux send-keys.
    # The recommended pattern is: build the full chained command BEFORE the first send-keys.
    # See the consolidation comment below.
    echo "  [chained task $TASK_ID on $WT_NAME — note: same-worktree consecutive tasks should be listed sequentially in the batch string]"
  else
    # New worktree — prepare and launch first task
    SETUP_CMD="cd $WORKTREE_ABS && git fetch origin && git checkout --detach origin/dev && git checkout -B $BRANCH && $AGENT_CMD"

    if [ $PANE -eq 0 ]; then
      tmux send-keys -t "$TMUX_SESSION" "$SETUP_CMD" C-m
    else
      tmux split-window -t "$TMUX_SESSION" -h
      tmux send-keys -t "$TMUX_SESSION" "$SETUP_CMD" C-m
    fi

    PREPARED_WORKTREES="${PREPARED_WORKTREES}|${WT_NAME}|"
    PANE=$((PANE + 1))
  fi
done

# NOTE: For same-worktree sequential tasks, the cleanest pattern is to pre-process the BATCH
# entries and build one chained command per worktree BEFORE calling tmux send-keys. The chaining
# above shows the principle; in practice, list same-worktree tasks consecutively in the batch
# string and the dispatch agent (at /sprint-close Step 4e) is responsible for ensuring this.

tmux select-layout -t "$TMUX_SESSION" even-horizontal
tmux attach-session -t "$TMUX_SESSION"

echo ""
echo "Batch '$BATCH_NAME' dispatched — $PANE worktrees active (same-worktree tasks run sequentially)."
echo "Monitor progress: tail -f logs/agent-*.log"
echo "Panes may appear blank — agents work in -p mode. Verify: ps aux | grep claude"
echo ""
echo "Post-batch checklist when agents finish:"
echo "  1. ./review.sh $BATCH_NAME   — check PR status and pre-merge conflict detection"
echo "  2. Review PRs — check diffs, CI, and ## Assumptions section in each PR"
echo "  3. Resolve any merge conflicts before merging"
echo "  4. Merge ONE PR at a time — run tests on dev after each merge"
echo "  5. Verify dev is clean (CI green after all merges)"
echo "  6. /task-done [task-id] for each merged task"
echo "  7. ./dispatch.sh [next-batch-name]"
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing review.sh (5,695 bytes)"
cat > 'review.sh' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────
# Parsec Sdn. Bhd. · AMIR
# Generated by the Parsec Sdn. Bhd. AI Development Framework v2
# © 2026 Parsec Sdn. Bhd.. All rights reserved.
# Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised
# projects is prohibited.
# ─────────────────────────────────────────────────────────────
# review.sh — Level 4 batch review
# Usage: ./review.sh [batch-name]
# Lists open PRs for the batch, shows CI status, runs pre-merge conflict simulation,
# and summarises agent logs. Run after dispatch.sh agents have finished.
#
# Bash 3 compatible — no namerefs, no associative arrays
# shellcheck shell=bash
set -euo pipefail

BATCH_NAME="${1:-}"
PRIMARY_DIR="$(pwd)"
LOG_DIR="$PRIMARY_DIR/logs"

if [ -z "$BATCH_NAME" ]; then
  echo "Usage: ./review.sh [batch-name]"
  echo "Lists open PRs, CI status, and merge conflict simulation for the batch."
  exit 1
fi

# Pull the batch entries from dispatch.sh
get_batch() {
  local name
  name="$(echo "$1" | tr '-' '_')"
  echo "$name" | grep -qE '^[a-zA-Z0-9_]+$' || return 1
  grep "^BATCH_${name}=" dispatch.sh | sed 's/^BATCH_'"${name}"'=//;s/^"//;s/"$//'
}

BATCH_ENTRIES="$(get_batch "$BATCH_NAME" 2>/dev/null || true)"
if [ -z "$BATCH_ENTRIES" ]; then
  echo "No batch '$BATCH_NAME' found in dispatch.sh. Available batches:"
  grep '^BATCH_' dispatch.sh | sed 's/BATCH_//;s/=.*//' | tr '_' '-'
  exit 1
fi

echo ""
echo "════════════════════════════════════════════════════════════"
echo " Review — Batch: $BATCH_NAME"
echo "════════════════════════════════════════════════════════════"

# ── 1. Per-task summary ──────────────────────────────────────
echo ""
echo "▶ TASKS IN BATCH"
echo ""
echo "  Task ID     Branch                              Worktree"
echo "  ─────────   ──────────────────────────────────  ──────────"

declare TASK_BRANCHES=""
declare TASK_IDS=""

for entry in $BATCH_ENTRIES; do
  TASK_ID="${entry%%:*}"
  REST="${entry#*:}"
  BRANCH="${REST%%:*}"
  WORKTREE="${REST#*:}"
  WT_NAME="$(basename "$WORKTREE")"
  printf "  %-11s %-35s %s\n" "$TASK_ID" "$BRANCH" "$WT_NAME"
  TASK_BRANCHES="$TASK_BRANCHES $BRANCH"
  TASK_IDS="$TASK_IDS $TASK_ID"
done

# ── 2. Open PRs + CI status (uses gh CLI) ────────────────────
echo ""
echo "▶ PR STATUS"
echo ""

if ! command -v gh &>/dev/null; then
  echo "  ⚠ gh CLI not installed — skipping PR status check"
  echo "    Install: brew install gh && gh auth login"
else
  for branch in $TASK_BRANCHES; do
    PR_INFO="$(gh pr list --head "$branch" --json number,title,state,statusCheckRollup --jq '.[0]' 2>/dev/null || echo '')"
    if [ -z "$PR_INFO" ] || [ "$PR_INFO" = "null" ]; then
      printf "  %-35s %s\n" "$branch" "✗ no open PR"
      continue
    fi
    PR_NUM="$(echo "$PR_INFO" | grep -o '"number":[0-9]*' | head -1 | cut -d: -f2)"
    PR_STATE="$(echo "$PR_INFO" | grep -o '"state":"[^"]*"' | head -1 | cut -d'"' -f4)"
    PR_TITLE="$(echo "$PR_INFO" | grep -o '"title":"[^"]*"' | head -1 | cut -d'"' -f4)"
    
    # CI rollup
    CI_STATE="$(echo "$PR_INFO" | grep -oE '"conclusion":"[^"]*"' | head -1 | cut -d'"' -f4)"
    [ -z "$CI_STATE" ] && CI_STATE="pending"
    
    case "$CI_STATE" in
      SUCCESS|success) CI_LABEL="✓ green" ;;
      FAILURE|failure) CI_LABEL="✗ FAILED" ;;
      pending|PENDING)  CI_LABEL="… running" ;;
      *)                CI_LABEL="$CI_STATE" ;;
    esac
    
    printf "  %-35s #%-5s %-10s %s\n" "$branch" "$PR_NUM" "$PR_STATE" "$CI_LABEL"
  done
fi

# ── 3. Pre-merge conflict simulation ─────────────────────────
echo ""
echo "▶ PRE-MERGE CONFLICT SIMULATION"
echo ""
echo "  Simulating merge of each branch against dev (no-commit, no-ff)..."
echo ""

# Save current state
ORIG_BRANCH="$(git branch --show-current 2>/dev/null || git rev-parse --short HEAD)"

# Refresh dev
git fetch origin --quiet 2>/dev/null || true
git checkout dev --quiet 2>/dev/null
git pull origin dev --quiet 2>/dev/null || true

CONFLICT_COUNT=0
for branch in $TASK_BRANCHES; do
  # Try the merge in-memory; abort regardless
  if git merge --no-commit --no-ff "origin/$branch" --quiet >/dev/null 2>&1; then
    printf "  %-35s ✓ clean\n" "$branch"
    git merge --abort 2>/dev/null || true
  else
    printf "  %-35s ✗ CONFLICTS\n" "$branch"
    # List conflicting files
    git diff --name-only --diff-filter=U 2>/dev/null | sed 's/^/      → /' || true
    git merge --abort 2>/dev/null || true
    CONFLICT_COUNT=$((CONFLICT_COUNT + 1))
  fi
done

# Restore original branch
git checkout "$ORIG_BRANCH" --quiet 2>/dev/null || true

# ── 4. Agent log summary ─────────────────────────────────────
echo ""
echo "▶ AGENT LOGS (last 10 lines per agent)"
echo ""

if [ -d "$LOG_DIR" ]; then
  for log in "$LOG_DIR"/agent-*.log; do
    [ -f "$log" ] || continue
    AGENT="$(basename "$log" .log | sed 's/^agent-//')"
    echo "  ── $AGENT ──"
    tail -n 10 "$log" | sed 's/^/      /'
    echo ""
  done
else
  echo "  No log directory found ($LOG_DIR)"
fi

# ── 5. Summary ───────────────────────────────────────────────
echo ""
echo "▶ POST-REVIEW CHECKLIST"
echo ""
echo "  1. Review each PR diff in GitHub — check ## Assumptions section in particular"
echo "  2. Confirm CI green on every PR before merging"

if [ "$CONFLICT_COUNT" -gt 0 ]; then
  echo "  3. ⚠ $CONFLICT_COUNT branches have merge conflicts — resolve before merging"
else
  echo "  3. ✓ No merge conflicts detected"
fi

echo "  4. Merge ONE PR at a time — run \`php artisan test\` on dev after each merge"
echo "  5. Run /task-done for each merged task"
echo "  6. When all merged, run ./dispatch.sh [next-batch-name]"
echo ""
echo "════════════════════════════════════════════════════════════"
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing tmux-work (2,448 bytes)"
cat > 'tmux-work' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────
# Parsec Sdn. Bhd. · AMIR
# Generated by the Parsec Sdn. Bhd. AI Development Framework v2
# © 2026 Parsec Sdn. Bhd.. All rights reserved.
# Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised
# projects is prohibited.
# ─────────────────────────────────────────────────────────────
# Workspace launcher — 4 Claude Code agents + shared preview pane
# Usage: ./tmux-work [project-path]
#   ./tmux-work                    # uses script's own directory
#   ./tmux-work ~/projects/amir    # explicit path

PROJECT="${1:-$(cd "$(dirname "$0")" && pwd)}"
SESSION="work"

tmux kill-server 2>/dev/null

tmux new-session -s "$SESSION" -d -x 300 -y 80

# Right half = preview pane (%1)
tmux split-window -h -p 50 -t "$SESSION"

# Split left 50% into 2×2 grid
tmux select-pane -t %0
tmux split-window -v -p 50
tmux select-pane -t %0
tmux split-window -h -p 50
tmux select-pane -t %2
tmux split-window -h -p 50

# Capture IDs
PREVIEW="%1"
AGENTS=("%0" "%2" "%3" "%4")

# Name panes for orientation
tmux select-pane -t %0 -T "Agent A"
tmux select-pane -t %2 -T "Agent B"
tmux select-pane -t %3 -T "Agent C"
tmux select-pane -t %4 -T "Agent D"
tmux select-pane -t %1 -T "Preview"

# Bootstrap preview pane
tmux send-keys -t "$PREVIEW" "cd '$PROJECT'" Enter
tmux send-keys -t "$PREVIEW" "
preview() {
  local f=\"\$1\"
  case \"\${f##*.}\" in
    md)        glow \"\$f\" 2>/dev/null || bat --style=plain \"\$f\" 2>/dev/null || cat \"\$f\" ;;
    pdf)       pdftotext \"\$f\" - 2>/dev/null | less -R || less \"\$f\" ;;
    docx)      pandoc -t plain \"\$f\" 2>/dev/null | less -R ;;
    xlsx|xls)  ssconvert --export-type=Gnumeric_stf:stf_csv \"\$f\" fd://1 2>/dev/null | less ;;
    pptx)      pandoc -t plain \"\$f\" 2>/dev/null | less -R ;;
    *)         bat --style=plain \"\$f\" 2>/dev/null || less \"\$f\" ;;
  esac
}
export -f preview
echo '──────────────────────────────────────────'
echo \"  Preview | \$(basename '$PROJECT')\"
echo '  Usage: preview path/to/file'
echo '──────────────────────────────────────────'
" Enter

# Launch Claude Code in all four agent panes
for pane in "${AGENTS[@]}"; do
  tmux send-keys -t "$pane" "cd '$PROJECT'" Enter
  tmux send-keys -t "$pane" "export TMUX_PREVIEW_PANE='$PREVIEW'" Enter
  tmux send-keys -t "$pane" "claude --dangerously-skip-permissions" Enter
done

tmux select-pane -t %0
tmux attach -t "$SESSION"
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .ai/guidelines/project-architecture.md (31,779 bytes)"
mkdir -p ".ai/guidelines"
cat > '.ai/guidelines/project-architecture.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised reproduction or use outside of
  Parsec Sdn. Bhd.-authorised projects is prohibited.
-->

# AMIR Project Architecture Guidelines

This file is auto-loaded by Laravel Boost on every Claude Code session. Read it before writing code that touches controllers, actions, events, models, policies, or queued jobs.

The rules here are AMIR-specific. They build on top of `.claude/rules/laravel.md` (general Laravel best practices). When the two conflict, this file wins.

---

## 1. The Single Most Important Rule

**Business operations are implemented as Action classes — never inside controllers, never inside models, never inside services.**

A controller's job is to validate input, call the action, and shape the response. A model's job is to represent persistent state. An action is the place where domain rules execute, where transactions are started, and where domain events are dispatched.

If you find yourself writing `DB::transaction(...)` inside a controller, stop. Move it to an Action.

---

## 2. Domain Folder Layout

Every business domain owns one folder under `app/Domain/[Name]/`. The 20 AMIR domains and their folder names are listed in CLAUDE.md §3.

Inside each domain folder:

```
app/Domain/[Name]/
  Actions/         Single-purpose action classes
  Events/          Domain events emitted by actions
  Listeners/       Listeners for events from this and other domains
  Models/          Eloquent models for this domain only
  Enums/           Backed enums for status/type values
  Policies/        Spatie-aware authorisation policies
  Jobs/            Queued jobs (one per async operation)
  Requests/        FormRequest classes (validation only)
  Resources/       API/Inertia resources
  Services/        Only for genuinely shared logic — avoid; prefer Actions
  Concerns/        Traits scoped to this domain (rare)
```

**Cross-domain calls happen via Actions or Events, never via direct model coupling.** A `Members\Models\Member` does not import `Accounting\Models\Account`. If `CreateMember` needs to allocate a member account in the chart of accounts, it dispatches `MemberRegistered`; an `Accounting\Listeners\AllocateMemberAccount` handles the allocation.

When you genuinely need to read across domains (e.g. surplus distribution needs to know each member's share balance and approved transactions), the consuming domain's Action calls **the other domain's Action**, not its model directly. Example: `Surplus\Actions\CalculateMemberShare` calls `Members\Actions\GetActiveMembers` and `Accounting\Actions\GetPostedTransactionsForPeriod` — it does not run `Member::query()` itself.

---

## 3. The Action Class Pattern

### Shape

Every Action is a final class with one public method named `execute`. The execute method takes named arguments (PHP 8 named-argument syntax at call sites) and returns either a model, a value object, or void.

```php
<?php
declare(strict_types=1);

namespace App\Domain\Members\Actions;

use App\Domain\Members\Enums\MemberStatus;
use App\Domain\Members\Events\MemberRegistered;
use App\Domain\Members\Models\Member;
use Illuminate\Support\Facades\DB;

final class CreateMember
{
    public function execute(string $tenantId, array $data): Member
    {
        return DB::transaction(function () use ($tenantId, $data): Member {
            $member = Member::create([
                'tenant_id' => $tenantId,
                'ic_no_encrypted' => $data['ic_number'],   // input: ic_number; storage: ic_no_encrypted (E3)
                'full_name' => $data['full_name'],
                'phone_e164' => $data['phone_e164'],
                'email' => $data['email'] ?? null,
                'status' => MemberStatus::Active,
                'shares_held' => 0,
                'subscription_balance_cents' => 0,
            ]);
            // ic_no_hash is maintained by the Member model's `saving` observer (per E3)

            event(new MemberRegistered($member->id, $tenantId));

            return $member->fresh();
        });
    }
}
```

### Rules

1. **One Action = one business operation.** `CreateMember`, `SuspendMember`, `ReinstateMember`, `RecordMemberSubscriptionPayment` — never `MemberManager` or `MemberService::doStuff()`.
2. **Final class.** Always `final class`. Actions are not extension points.
3. **Named on a verb.** `CreateX`, `PostJournalEntry`, `ClosePeriod`, `CalculateSurplusDistribution`, `IssueArRahnuTicket`. Never noun-only names.
4. **Single public `execute` method.** Helper methods are `private`.
5. **Transactional.** Any action that writes more than one row uses `DB::transaction(...)`. Reads need not be transactional.
6. **Dispatches domain events** when something has happened that other domains care about. The event is dispatched **inside** the transaction; listeners that must run only after commit use `ShouldQueueAfterCommit`.
7. **Returns a fresh model** (`->fresh()`) — never the in-memory instance. Casts and computed columns are otherwise stale.
8. **Validates by precondition, not by input shape.** Input shape is validated by FormRequest before the action runs. Inside the Action, validate domain preconditions: "member exists", "period is open", "balance is sufficient" — and throw a `DomainException` if violated.
9. **No HTTP awareness.** Never accept `Request`, never return `Response`. The action does not know the calling context — it could be invoked from a controller, a queue job, an Artisan command, or a test.
10. **No direct event listeners.** If an action needs to react to an event, the listener calls a different Action. Actions are written, listeners orchestrate.

### Anti-patterns

- ❌ `class MemberService { public function create(...); public function suspend(...); }` — split into Actions.
- ❌ `Member::create([...])` inside a controller — wrap in an Action.
- ❌ An Action with two public methods. Split it.
- ❌ Returning a DB row array. Return the model or a value object.

---

## 4. Controllers

Controllers are thin. The framework standard for AMIR is **single-action controllers**.

```php
<?php
declare(strict_types=1);

namespace App\Http\Controllers\Members;

use App\Domain\Members\Actions\CreateMember;
use App\Domain\Members\Requests\CreateMemberRequest;
use App\Domain\Members\Resources\MemberResource;
use Illuminate\Http\JsonResponse;

final class CreateMemberController
{
    public function __invoke(CreateMemberRequest $request, CreateMember $action): JsonResponse
    {
        $member = $action->execute(
            tenantId: $request->user()->currentTenantId(),
            data: $request->validated(),
        );

        return MemberResource::make($member)->response()->setStatusCode(201);
    }
}
```

### Rules

1. **One controller = one route = one operation.** `CreateMemberController`, `SuspendMemberController`, `ListMembersController`. Never resource controllers with `index/show/store/update/destroy`.
2. **`__invoke` only.** Single method.
3. **Named arguments at the call site.** `$action->execute(tenantId: ..., data: ...)`. Always.
4. **Returns Inertia or JsonResponse.** Inertia for SPA pages, JsonResponse for AJAX. Never raw arrays.
5. **No business logic.** If a conditional belongs to the domain, move it to the Action.
6. **No DB queries.** All reads happen via Actions or via Resources that scope to the current tenant.

### Authorization

Authorization happens in the FormRequest's `authorize()` method, which delegates to the Spatie policy:

```php
public function authorize(): bool
{
    return $this->user()->can('create', \App\Domain\Members\Models\Member::class);
}
```

Policies are Spatie-Permission-aware (see §7).

### Routes

Single-action controllers register in `routes/web.php` with the class name:

```php
Route::middleware(['auth', 'verified', 'tenant.context'])->group(function () {
    Route::post('/members', CreateMemberController::class)->name('members.create');
    Route::get('/members', ListMembersController::class)->name('members.index');
    Route::get('/members/{member}', ShowMemberController::class)->name('members.show');
    Route::post('/members/{member}/suspend', SuspendMemberController::class)->name('members.suspend');
});
```

---

## 5. Domain Events

Domain events are the public API between domains. They are how `Members` tells `Accounting` that a member was registered, without `Members` having to know that `Accounting` exists.

### Shape

```php
<?php
declare(strict_types=1);

namespace App\Domain\Members\Events;

final class MemberRegistered
{
    public function __construct(
        public readonly string $memberId,
        public readonly string $tenantId,
    ) {}
}
```

### Rules

1. **Past-tense names.** `MemberRegistered`, `JournalEntryPosted`, `PeriodClosed`, `ArRahnuTicketIssued`. Never `CreateMember` or `PostJournalEntry` (those are Action names).
2. **Final class with public readonly properties.** No setters. Events are immutable.
3. **Tiny payload.** Carry IDs, not entire models. The listener queries the model fresh from its own domain. This avoids stale state and cross-domain coupling.
4. **Always include `tenantId`.** Every listener needs to scope its work to the right tenant.
5. **Emitted from inside Actions only.** Never from controllers, never from models, never from listeners.
6. **Constructor uses promoted readonly properties** (PHP 8.1+). No factory methods.

### Listeners

Listeners live in the domain that **reacts**, not the domain that emits.

```php
<?php
declare(strict_types=1);

namespace App\Domain\Accounting\Listeners;

use App\Domain\Accounting\Actions\AllocateMemberAccount;
use App\Domain\Members\Events\MemberRegistered;
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;

final class AllocateMemberAccount implements ShouldQueueAfterCommit
{
    public function __construct(private AllocateMemberAccount $action) {}

    public function handle(MemberRegistered $event): void
    {
        $this->action->execute(
            tenantId: $event->tenantId,
            memberId: $event->memberId,
        );
    }
}
```

### Listener Rules

1. **Listeners delegate to Actions.** Never put logic in a listener.
2. **`ShouldQueueAfterCommit` for non-trivial work.** Anything that takes >50ms or hits external systems must queue, and must wait for the parent transaction to commit.
3. **Handle method is single-purpose.** One Action call. If you need two, you needed two listeners or a different Action.
4. **Idempotent.** Listeners may be retried by the queue. Every listener's outcome must be safe to apply twice — guard with `firstOrCreate`, `updateOrCreate`, or explicit "already done?" checks.

### Registration

Register listeners in `app/Providers/EventServiceProvider.php`:

```php
protected $listen = [
    \App\Domain\Members\Events\MemberRegistered::class => [
        \App\Domain\Accounting\Listeners\AllocateMemberAccount::class,
        \App\Domain\Signals\Listeners\PrimeMemberHealthScore::class,
    ],
];
```

---

## 6. Models

Models represent persistent state. They are not the place for business logic.

### Shape

```php
<?php
declare(strict_types=1);

namespace App\Domain\Members\Models;

use App\Concerns\BelongsToTenant;
use App\Domain\Members\Enums\MemberStatus;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;

final class Member extends Model
{
    use BelongsToTenant, HasFactory, HasUuids, LogsActivity, SoftDeletes;

    protected $fillable = [
        'tenant_id',
        'ic_no_encrypted',     // per Decision E3 — storage column for encrypted IC
        'ic_no_hash',          // per Decision E3 — SHA-256(tenant_salt + IC) for lookups
        'full_name',
        'phone_e164',
        'email',
        'status',
        'shares_held',
        'subscription_balance_cents',
    ];

    protected $casts = [
        'ic_no_encrypted' => 'encrypted',         // per E3
        'status' => MemberStatus::class,
        'shares_held' => 'integer',
        'subscription_balance_cents' => MoneyCast::class, // per D16 — exposes a Money value object
    ];

    public function getActivitylogOptions(): LogOptions
    {
        return LogOptions::defaults()
            ->logOnly(['ic_no_encrypted', 'full_name', 'phone_e164', 'email', 'status', 'shares_held', 'subscription_balance_cents'])
            ->logOnlyDirty();
    }
}
```

> **Note on IC number naming.** Per Decision E3, the **storage column** is `ic_no_encrypted` (encrypted ciphertext) plus `ic_no_hash` (SHA-256 lookup hash with per-tenant salt). The **user-facing API field** in FormRequests, Resources, and JSON payloads remains `ic_number` for clarity — Actions transform between the two at the boundary. Same pattern applies to bank account numbers (`account_number_encrypted` + `account_number_hash`) and TINs (`tin_encrypted` + `tin_hash`).

### Rules

1. **Final class.** Models are not extension points either.
2. **`use BelongsToTenant`** on every tenant-scoped model. This applies the global TenantScope and binds tenant_id on create. **Without this trait, queries leak across tenants.** Hook `post-edit-tenant-scope-check.sh` warns if a new model is missing it.
3. **`use HasUuids`** on every model. Override `newUniqueId()` in `app/Concerns/UsesUuidV7.php` to generate UUID v7 application-side.
4. **`use SoftDeletes`** on entities with audit-trail lifecycle. Members, transactions, journal entries — yes. Transient lookups, audit log entries, login attempts — no.
5. **`use LogsActivity`** on every model that needs an audit trail. Configure via `getActivitylogOptions()` to log specific fields and only on dirty changes. Do not log password fields, encrypted fields, or counters that update every request.
6. **`$fillable` only.** Never `$guarded = []`. Mass-assignment vulnerabilities are not a hypothetical.
7. **`$casts` for every non-string column.** Money columns cast to `MoneyCast::class` per Decision D16 (exposes a `Money` value object that wraps cents + currency + locale). Status columns cast to the enum class. Encrypted fields cast to `'encrypted'`. Dates cast to `'datetime'`. Counters that are not money (e.g. `shares_held` integer count) cast to `'integer'`.
8. **No business logic methods.** No `$member->suspend()`. The Action class `SuspendMember::execute(memberId: ...)` does it.
9. **Relationships only as accessors.** `hasMany`, `belongsTo`, `hasOne` — fine. Computed relationships go in dedicated Action methods.
10. **No `static::observe()` calls.** Observers are registered in the model's domain ServiceProvider. This makes the binding explicit and grep-able.

### `BelongsToTenant` trait

```php
<?php
declare(strict_types=1);

namespace App\Concerns;

use App\Models\Tenant;
use App\Scopes\TenantScope;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

trait BelongsToTenant
{
    protected static function bootBelongsToTenant(): void
    {
        static::addGlobalScope(new TenantScope);

        static::creating(function ($model): void {
            if (empty($model->tenant_id) && app()->bound('currentTenantId')) {
                $model->tenant_id = app('currentTenantId');
            }
        });
    }

    public function tenant(): BelongsTo
    {
        return $this->belongsTo(Tenant::class);
    }
}
```

### `TenantScope`

```php
<?php
declare(strict_types=1);

namespace App\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

final class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if (app()->bound('currentTenantId')) {
            $builder->where($model->getTable() . '.tenant_id', app('currentTenantId'));
        }
    }
}
```

### Bypassing the scope

Platform Operators (PO role) need cross-tenant queries. Bypass the scope **explicitly and with a comment**:

```php
// PO role: cross-tenant aggregate. Bypass justified by Decision B22 (PO has full visibility).
$totalActiveMembers = Member::withoutGlobalScope(TenantScope::class)
    ->where('status', MemberStatus::Active)
    ->count();
```

The justification comment is mandatory. Code review rejects bypasses without one.

---

## 7. Policies (Spatie-Permission Aware)

Authorization in AMIR runs on Spatie roles + permissions. The three roles are:
- `platform_operator` — full system access, including cross-tenant
- `tenant_admin` — administrative within one tenant
- `tenant_user` — operational within one tenant (Setiausaha, Bendahari personas)

Granular permissions extend roles. Example: `manage_master_coa` is a permission on the `platform_operator` role, granted only to PO users responsible for catalogue maintenance.

### Shape

```php
<?php
declare(strict_types=1);

namespace App\Domain\Members\Policies;

use App\Domain\Members\Models\Member;
use App\Models\User;

final class MemberPolicy
{
    public function viewAny(User $user): bool
    {
        return $user->hasAnyRole(['tenant_admin', 'tenant_user', 'platform_operator']);
    }

    public function view(User $user, Member $member): bool
    {
        if ($user->hasRole('platform_operator')) {
            return true;
        }

        return $user->currentTenantId() === $member->tenant_id;
    }

    public function create(User $user): bool
    {
        return $user->hasRole('tenant_admin')
            || $user->can('create_members');
    }

    public function update(User $user, Member $member): bool
    {
        return $this->view($user, $member)
            && ($user->hasRole('tenant_admin') || $user->can('update_members'));
    }

    public function suspend(User $user, Member $member): bool
    {
        return $this->view($user, $member)
            && $user->hasRole('tenant_admin');
    }
}
```

### Rules

1. **Final class.** Standard.
2. **One policy per model.** `MemberPolicy` lives at `app/Domain/Members/Policies/MemberPolicy.php`.
3. **Methods named after Action verbs.** `view`, `create`, `update`, `suspend`, `reinstate`. Match the Actions.
4. **Tenant scoping enforced explicitly.** `$user->currentTenantId() === $model->tenant_id` for any view-or-mutate check on a tenant-scoped model. Don't rely solely on the global scope — the policy is the second wall.
5. **Spatie checks.** Use `hasRole()` for role gates and `can()` for granular permission gates. Combine with `||` for "either is enough" and `&&` for "both required".
6. **Register in domain ServiceProvider.** `Gate::policy(Member::class, MemberPolicy::class);`

### `currentTenantId()` on User

Add this method to the `User` model. It reads the authenticated tenant context (set by `tenant.context` middleware) and falls back to `null` for PO users.

```php
public function currentTenantId(): ?string
{
    return app()->bound('currentTenantId') ? app('currentTenantId') : null;
}
```

---

## 8. FormRequests

FormRequests do **two things only**: validate input, and authorize via the policy.

```php
<?php
declare(strict_types=1);

namespace App\Domain\Members\Requests;

use App\Domain\Members\Models\Member;
use Illuminate\Foundation\Http\FormRequest;

final class CreateMemberRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', Member::class);
    }

    public function rules(): array
    {
        return [
            'ic_number' => ['required', 'string', 'regex:/^\d{6}-\d{2}-\d{4}$/'],
            'full_name' => ['required', 'string', 'max:255'],
            'phone_e164' => ['required', 'string', 'regex:/^\+60\d{9,10}$/'],
            'email' => ['nullable', 'email:rfc,dns', 'max:255'],
        ];
    }

    public function messages(): array
    {
        return [
            'ic_number.regex' => __('validation.ic_format'),
            'phone_e164.regex' => __('validation.phone_format'),
        ];
    }
}
```

### Rules

1. **Final class.** Standard.
2. **Two methods only:** `authorize()` and `rules()`. Optionally `messages()` for translations.
3. **Authorization delegates to the policy** via `$this->user()->can(...)`. Never inline logic.
4. **Validation rules are arrays, not pipe strings.** `['required', 'string', 'max:255']`, not `'required|string|max:255'`.
5. **Use Laravel rule classes** for complex validations: `Rule::unique(...)->where(...)`, `Rule::enum(MemberStatus::class)`, etc.
6. **No transformation.** If you need to massage input, do it in the Action, not in `prepareForValidation()`. (Exception: trim and lowercase email — that's pure cleaning, not domain logic.)
7. **One FormRequest per Action.** `CreateMemberRequest`, `SuspendMemberRequest`. Never reuse a FormRequest across two actions.

---

## 9. API/Inertia Resources

Resources shape the response. They live in the domain folder, not in `app/Http/Resources/`.

```php
<?php
declare(strict_types=1);

namespace App\Domain\Members\Resources;

use App\Domain\Members\Models\Member;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

/** @mixin Member */
final class MemberResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'ic_number' => $this->ic_no_encrypted, // already decrypted by 'encrypted' cast (E3 column → public API field)
            'full_name' => $this->full_name,
            'phone_e164' => $this->phone_e164,
            'email' => $this->email,
            'status' => $this->status->value,
            'status_label' => __('members.status.' . $this->status->value),
            'shares_held' => $this->shares_held,
            'subscription_balance' => [
                'cents' => $this->subscription_balance_cents,
                'formatted' => money_format_myr($this->subscription_balance_cents),
            ],
            'created_at' => $this->created_at->toIso8601String(),
        ];
    }
}
```

### Rules

1. **Final class.** Standard.
2. **`@mixin` PHPDoc** for IDE autocomplete on `$this`.
3. **Money fields expose both cents and formatted string.** Frontend uses cents for arithmetic, formatted for display.
4. **Status values expose both raw value and translated label.** Frontend never does its own status-to-label mapping.
5. **Dates serialize as ISO 8601** (`toIso8601String()`). Frontend converts to MYT on display.
6. **Decrypted fields are exposed by cast.** The `ic_no_encrypted` column is decrypted automatically by the `'encrypted'` cast — the resource reads `$this->ic_no_encrypted` and exposes it under the public API name `ic_number`. Same pattern for `account_number_encrypted` → `account_number`, etc. (per Decision E3).
7. **No N+1 queries.** If the resource accesses a relationship, the controller must eager-load it via `->with(...)`.

---

## 10. Jobs (Queued Work)

Jobs are queued via Laravel Horizon. Use them for anything that takes >50ms, hits external services, or runs on a schedule.

```php
<?php
declare(strict_types=1);

namespace App\Domain\EInvoice\Jobs;

use App\Domain\EInvoice\Actions\SubmitInvoiceToMyInvois;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

final class SubmitInvoiceToMyInvoisJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 60;
    public int $timeout = 30;

    public function __construct(
        public readonly string $tenantId,
        public readonly string $invoiceId,
    ) {}

    public function handle(SubmitInvoiceToMyInvois $action): void
    {
        $action->execute(
            tenantId: $this->tenantId,
            invoiceId: $this->invoiceId,
        );
    }

    public function tags(): array
    {
        return ['einvoice', 'tenant:' . $this->tenantId, 'invoice:' . $this->invoiceId];
    }
}
```

### Rules

1. **Final class implementing `ShouldQueue`.** Standard.
2. **Constructor takes IDs, not models.** Models serialize awkwardly across queue boundaries; IDs are stable. The Action re-queries the model.
3. **`tags()` for Horizon visibility.** Always include the tenant ID and the entity ID.
4. **`tries` and `backoff` set explicitly.** Default `tries=3, backoff=60`. Override per job class.
5. **`timeout` set explicitly.** Default `30`. Increase for known-slow jobs (e.g. Penyata PDF generation: `120`).
6. **Handle delegates to an Action.** Never put logic in `handle()`.
7. **Idempotent.** Same as listeners — every job must be safe to retry.

### Queue connections

Single Redis connection (the only queue backend in v1). Multiple supervisors:
- `default` — most jobs
- `notifications` — outbound notification dispatching (high frequency, low value-per-job)
- `integrations` — external system calls (rate-limited, retried aggressively)
- `reports` — Penyata generation, Trial Balance, large exports (long-running)

Job classes specify their queue via `$this->onQueue('integrations')` in the dispatching code, not inside the job class.

---

## 11. Enums

All status, type, and role values use backed PHP enums.

```php
<?php
declare(strict_types=1);

namespace App\Domain\Members\Enums;

enum MemberStatus: string
{
    case Active = 'ACTIVE';
    case Suspended = 'SUSPENDED';
    case Resigned = 'RESIGNED';
    case Deceased = 'DECEASED';

    public function label(): string
    {
        return __('members.status.' . $this->value);
    }

    public function isActive(): bool
    {
        return $this === self::Active;
    }
}
```

### Rules

1. **Backed by `string`.** Never `int`-backed (status values change meaning over time; strings are self-documenting).
2. **Values are SCREAMING_SNAKE_CASE.** `'ACTIVE'`, `'PENDING_REVIEW'`. Frontend ALL-CAPS strings must match — hook `post-edit-enum-sync-check.sh` warns on drift.
3. **Cases are PascalCase.** `Active`, `PendingReview`.
4. **One enum file per concept.** Don't mix `MemberStatus` and `LoanStatus` in one file.
5. **Translations via `label()` method.** Never inline `match` statements in resources or views.
6. **Predicates as methods** (`isActive()`, `requiresReview()`). These document the business meaning of each state.
7. **Never remove a case.** Removing a case breaks any DB row that holds the removed value. Deprecate by marking the case `@deprecated` in PHPDoc and stop assigning it.

---

## 12. Service Providers (Domain-Scoped)

Each domain has its own ServiceProvider that registers observers, gates, and policies for its models.

```php
<?php
declare(strict_types=1);

namespace App\Domain\Members\Providers;

use App\Domain\Members\Models\Member;
use App\Domain\Members\Observers\MemberObserver;
use App\Domain\Members\Policies\MemberPolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

final class MembersServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Member::observe(MemberObserver::class);
        Gate::policy(Member::class, MemberPolicy::class);
    }
}
```

Register the domain ServiceProvider in `bootstrap/providers.php`.

### Rules

1. **One ServiceProvider per domain.** `MembersServiceProvider`, `AccountingServiceProvider`, `PdpaServiceProvider`.
2. **`boot()` only.** Don't put binding registrations here unless absolutely necessary; prefer auto-discovery via `register()` on the framework's app provider.
3. **Observer registration here, never in models.** Static `Member::observe(...)` in the model's `boot()` method is forbidden — it makes the binding invisible to grep.

---

## 13. Database Migrations

Migrations live at `database/migrations/`. AMIR follows additive-only practice.

See `.claude/skills/writing-migrations.md` for the full rules. Headlines:

1. **Additive only.** New tables, new nullable columns, new columns with defaults, new indexes — yes. Drop, rename, or change column type — never in a single migration. Use a deprecation sprint.
2. **Every table has** `id UUID PRIMARY KEY`, `tenant_id UUID NOT NULL` (if tenant-scoped), `timestampsTz()` (created_at + updated_at), `softDeletes()` (if lifecycle requires it).
3. **Every column in WHERE / JOIN / ORDER BY has an index.** Add the index in the same migration that adds the column.
4. **Money columns are `BIGINT` named `*_cents`.** Never `decimal(N,2)`.
5. **Foreign keys explicit.** `->references('id')->on('members')->onDelete('restrict')`.
6. **`down()` is mandatory.** Every `up()` has a fully reversing `down()`.

Before writing a migration, run Boost's `database_schema` tool to confirm the actual current schema. Never assume.

---

## 14. Living Codebase Documents

Six files in `docs/living/` reflect the actual current state of the codebase. They are **not** the planning documents — those are the source of truth for "what we're going to build". The living docs are the source of truth for "what we've actually built and how it actually works".

| File | Purpose |
|---|---|
| `CODEBASE_MAP.md` | Domain boundaries and class index — where things live |
| `DATA_MODEL.md` | Current schema snapshot — what tables/columns/indexes actually exist |
| `DOMAIN_GUIDE.md` | State machines, business rules, invariants per domain |
| `API_REFERENCE.md` | Current API surface — routes, request shapes, response shapes |
| `VELOCITY_LOG.md` | Sprint timing data — wall-clock per sprint, per task, per agent |
| `DEVIATION_LOG.md` | Places where the implementation deviated from the plan, and why |
| `PRINCIPLES.md` | What we learned about this codebase — gotchas and patterns specific to AMIR |

Read all seven before planning. Update them at sprint close (the `/sprint-close` slash command updates them as part of its checklist).

---

## 15. When in Doubt

1. **Read `docs/living/`** — what does the codebase actually look like right now?
2. **Read the planning document for the relevant phase** — `ARCHITECTURE.md` for schema, `DECISIONS_LOG.md` for the why, `USER_STORIES.md` for the what.
3. **Use Boost's introspection tools** — `database_schema`, `tinker`, `application_info`. Don't guess.
4. **Look at an existing similar Action / Controller / Model.** AMIR is internally consistent by design — find an analogous example and follow its shape.
5. **Ask.** State what's confusing. Name the options. Surface the tradeoff.

The right answer is rarely "ship something and see if it sticks". For AMIR, the right answer is "match the existing pattern, or surface the tradeoff and let the founder pick."

---

## 16. Decisions Trace

Every rule in this file traces back to a decision in `DECISIONS_LOG.md`:

| Rule | Decision | Reversibility |
|---|---|---|
| Action class pattern (single `execute`, transactional, dispatches event) | **D30** | 🟡 Costly |
| Single-action controllers (`__invoke` only) | **D31** | 🟡 Costly |
| `final class` mandate | **D32** | 🟢 Reversible |
| `tenant_id` column + `BelongsToTenant` trait + `TenantScope` global scope | **D28** | 🟡 Costly |
| UUID v7 primary keys via `HasUuids` + `Str::uuid7()` override | **D15-R1** | 🔴 Irreversible |
| Sanctum stateful + Spatie Permission v6 + Fortify | **D6**, **D29** | 🟡 Costly |
| Horizon-on-Redis queue | **D7**, **D29** | 🟡 Costly |
| spatie/laravel-activitylog v4 | **D19**, **D29** | 🟡 Costly |
| Junction table for user-tenant membership | **E1** | 🔴 Irreversible |
| Domain folder layout (`app/Domain/[Name]/`) | (this file, §2) | 🟡 Costly |

If you want to deviate from any rule, raise a revision (`Dxx-R1`) in `DECISIONS_LOG.md` first. Do not silently drift in a single PR.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .ai/guidelines/data-conventions.md (29,086 bytes)"
mkdir -p ".ai/guidelines"
cat > '.ai/guidelines/data-conventions.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised reproduction or use outside of
  Parsec Sdn. Bhd.-authorised projects is prohibited.
-->

# AMIR Data Conventions

This file is auto-loaded by Laravel Boost on every Claude Code session. Read it before writing any migration, model, FormRequest, or Resource.

These conventions are **non-negotiable**. They protect data integrity, prevent silent corruption, and ensure that the codebase remains internally consistent across 20 modules.

---

## 1. Money — Always Integer Cents

**Money is stored as a `BIGINT` of cents. Column names end with `_cents`. Never use `decimal`, `float`, or `double` for money.**

### Why

Floating-point representation of money introduces rounding errors that compound over operations. For an accounting system, this is unacceptable. Integer cents preserve exact arithmetic everywhere.

`BIGINT` (signed 64-bit) handles values up to ±9.2 × 10¹⁸ cents — about ±RM 92 quadrillion. Any koperasi that exceeds this will have other problems.

### Rules

1. **Storage:** `BIGINT NOT NULL` (or `BIGINT NOT NULL DEFAULT 0` for accumulator columns).
2. **Naming:** column name ends with `_cents`. Examples: `total_amount_cents`, `opening_balance_cents`, `subscription_balance_cents`, `monthly_price_cents`, `write_off_threshold_cents`.
3. **PHP cast:** `MoneyCast::class` in the model's `$casts` array per **Decision D16**. The cast exposes a `App\Support\Money` value object that wraps the integer with currency, locale, and rounding rules. Use `MoneyCast::class` for all money columns. Use plain `'integer'` only for non-money counters (e.g. `shares_held`).
4. **Migration:**
   ```php
   $table->bigInteger('total_amount_cents')->default(0);
   ```
5. **CHECK constraints** for invariants: amounts that must be non-negative get an explicit constraint:
   ```php
   $table->bigInteger('write_off_threshold_cents')->default(100000);
   // In a separate raw statement:
   DB::statement("ALTER TABLE tenant_settings ADD CONSTRAINT chk_ts_write_off CHECK (write_off_threshold_cents >= 0)");
   ```
6. **Display:** convert to ringgit via the `money_format_myr()` helper at the resource boundary. Never do display formatting in the model or controller.
7. **Frontend:** receive both `cents` and `formatted` strings in the resource. Use cents for arithmetic, formatted for display.

### Display helper

```php
function money_format_myr(int $cents, bool $withSymbol = true): string
{
    $ringgit = $cents / 100;
    $formatted = number_format($ringgit, 2, '.', ',');
    return $withSymbol ? "RM {$formatted}" : $formatted;
}
```

### Anti-patterns

- ❌ `$table->decimal('amount', 15, 2)` — never use decimal for money.
- ❌ `$table->bigInteger('amount')` — name must end with `_cents` so the unit is unambiguous at every read site.
- ❌ `$amount = (float) $cents / 100; return number_format($amount * 1.06, 2);` — never do float arithmetic on money.
- ❌ Tax/SST calculations in `decimal`. Compute in cents: `$tax_cents = (int) round($amount_cents * 0.06)`.

---

## 2. IDs — UUID v7 Primary Keys

**Every primary key is a UUID v7. Never auto-incrementing integers.**

### Why

UUIDs allow client-side ID generation (essential for offline mode per Decision B13), avoid sequential ID enumeration attacks, and make multi-region scaling trivial. UUID v7 specifically is **time-ordered** — recent rows cluster on disk and indexes don't fragment, unlike v4 which is fully random.

### Rules

1. **Storage:** `UUID PRIMARY KEY` (PostgreSQL native UUID type).
2. **Generation:** application-side via `HasUuids` trait + an override that generates UUID v7.
3. **Migration:**
   ```php
   $table->uuid('id')->primary();
   ```
4. **Foreign keys:** also UUID, with explicit `ON DELETE` strategy:
   ```php
   $table->uuid('tenant_id');
   $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('restrict');
   ```
5. **Model trait:** `use HasUuids;` on every model.
6. **Override** at `app/Concerns/UsesUuidV7.php`:
   ```php
   trait UsesUuidV7
   {
       public function newUniqueId(): string
       {
           return Str::uuid7()->toString();
       }
   }
   ```
   (Use `ramsey/uuid` 4.x or PHP 8.4's built-in if available.)

### `ON DELETE` policy

| Relationship | Policy | Reason |
|---|---|---|
| `tenant_id` foreign keys | `ON DELETE RESTRICT` | A tenant is never hard-deleted; soft-delete only |
| `created_by_user_id` | `ON DELETE SET NULL` | User deletion preserves historical attribution |
| Child of an aggregate (e.g. `journal_lines.journal_entry_id`) | `ON DELETE CASCADE` | Lines cannot exist without their entry |
| Reference data (e.g. `currency_id`) | `ON DELETE RESTRICT` | Reference data is permanent |

### Anti-patterns

- ❌ `$table->id()` — auto-increment is forbidden in AMIR.
- ❌ UUID v4 — use v7 for index locality.
- ❌ Generating UUIDs server-side in the controller. Let the model do it via `HasUuids`.

---

## 3. Tenant Scoping

**Every tenant-scoped table has `tenant_id UUID NOT NULL`. Every model that maps to such a table uses the `BelongsToTenant` trait.**

### Why

Without enforced tenant scoping, a single forgotten `WHERE` clause leaks data across tenants — possibly between competing koperasi. This is the highest-severity bug class for a multi-tenant SaaS.

### Rules

1. **Schema:** `tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE RESTRICT`.
2. **Index:** every tenant-scoped table has an index on `tenant_id` (often as part of a composite index — `(tenant_id, created_at)` is common).
3. **Model:** `use BelongsToTenant;` — always. The trait applies the global `TenantScope` and binds `tenant_id` on create.
4. **Bypass:** only via explicit `withoutGlobalScope(TenantScope::class)`, with a justification comment. PR review rejects bypasses without one.
5. **Test:** every tenant-scoped feature has a tenancy isolation test:
   ```php
   it('does not return members from other tenants', function () {
       $tenantA = Tenant::factory()->create();
       $tenantB = Tenant::factory()->create();

       Member::factory()->forTenant($tenantA)->count(3)->create();
       Member::factory()->forTenant($tenantB)->count(5)->create();

       app()->instance('currentTenantId', $tenantA->id);
       expect(Member::count())->toBe(3);
   });
   ```

### Exceptions (platform-scoped models)

Some models are not tenant-scoped: `Tenant`, `Pack`, `MasterChartOfAccountsTemplate`, `User` (users may belong to multiple tenants), `PlatformAdmin`, `LhdnTinRegistry`, `Currency`, etc.

For these, **add a comment at the top of the model**:
```php
// platform-scoped model — no tenant_id by design (see DECISIONS_LOG.md A6)
final class Pack extends Model { /* ... */ }
```

The hook `post-edit-tenant-scope-check.sh` reads this comment to suppress its warning.

---

## 4. Phone Numbers — E.164

**All phone numbers are stored in E.164 format in columns named `phone_e164` per Decision D33.**

### Why

E.164 is the international standard. Storing in any other format requires per-display reformatting and makes integrations with WhatsApp Business API, SMS gateways, and LHDN Lookup brittle. **D33 supersedes the original Phase 6 schema** which used `phone VARCHAR(50)` permissive — that schema is being migrated to `phone_e164 VARCHAR(20)` in S01.

### Rules

1. **Storage:** `VARCHAR(20)` (max E.164 length is 15 digits + `+` + buffer).
2. **Naming:** `phone_e164` (or `mobile_e164`, `whatsapp_e164` if multiple).
3. **Validation:** regex `/^\+60\d{9,10}$/` for Malaysia numbers (10 or 11 digits total after `+60`).
4. **Migration:**
   ```php
   $table->string('phone_e164', 20)->nullable();
   $table->index('phone_e164');
   ```
5. **Input acceptance:** the FormRequest validates E.164 directly. Do not accept `012-3456789` or `0123456789` and rewrite — instead, the frontend `<PhoneInput>` component does the rewriting before submit.
6. **Display:** format for human reading at the resource boundary: `+60 12-345 6789`.
7. **WhatsApp:** the WhatsApp Business API accepts E.164 directly with the `+` stripped — `60123456789`.

### Anti-patterns

- ❌ Column named `phone` or `mobile` without `_e164` suffix — ambiguity bug magnet.
- ❌ Storing `012-3456789` — every read site has to parse it.
- ❌ Validating with `min:10|max:14` — be specific with regex.

---

## 5. Timestamps — Always Timezone-Aware

**Every table uses `timestampsTz()`. Storage is UTC. Display is MYT (Asia/Kuala_Lumpur).**

### Why

Mixing naive and aware timestamps is a classic data corruption pattern. Once a column is naive, every read site has to remember "is this UTC or local?" and someone eventually forgets.

### Rules

1. **Migration:** every table includes `$table->timestampsTz();` (creates `created_at` and `updated_at` as `TIMESTAMPTZ`).
2. **Soft deletes:** `$table->softDeletesTz();` if the model uses soft deletes.
3. **Custom timestamp columns:** always `->timestampTz()`, never `->timestamp()`.
4. **PHP timezone:** `app.timezone` set to `'UTC'` in `config/app.php`. **Never** `'Asia/Kuala_Lumpur'`. Storage is UTC, full stop.
5. **Display timezone:** the user's timezone is fetched from their preferences; defaults to `'Asia/Kuala_Lumpur'`.
6. **Resource serialization:** ISO 8601 with offset: `'2026-05-21T14:30:00+00:00'`. Frontend converts to MYT.
7. **Date-only columns** (e.g. `period_start_date`, `pawn_due_date`): use `->date()` (not `dateTime`). Date-only is intentionally timezone-free.

### Anti-patterns

- ❌ `$table->timestamp('expires_at')` — use `timestampTz()`.
- ❌ `Carbon::now()->setTimezone('Asia/Kuala_Lumpur')->toDateTimeString()` to write into the DB. Always write UTC.
- ❌ `->format('Y-m-d H:i:s')` at the resource boundary. Use `->toIso8601String()`.

### Reading offline-mode timestamps

Offline mode (Decision B13) generates timestamps client-side. The server must:
1. Accept the client's timestamp as the authoritative `created_at` if within ±5 minutes of server time.
2. Override with server time if the drift exceeds 5 minutes (silent clock fix).
3. Always set `updated_at` to server time on insert and on subsequent updates.

---

## 6. Soft Deletes

**Every entity with audit-trail lifecycle uses soft deletes. Lookup tables and high-frequency transient records do not.**

### Soft delete

| Use soft delete | Don't use soft delete |
|---|---|
| Members | Login attempts |
| Transactions / Journal entries | Audit log entries |
| Invoices, bills, payments | Cache rows |
| Bank accounts | Session rows |
| Tenants | Rate-limit counters |
| Users | One-time tokens |

### Rules

1. **Migration:** `$table->softDeletesTz();` if the model uses soft deletes.
2. **Model:** `use SoftDeletes;`.
3. **Action class methods:** `delete*` actions soft-delete by default. **Hard delete is rare** and lives in a separate Action like `PurgeMember` (used only for PDPA s.43 erasure).
4. **Restoration:** if soft-deleted entities can be restored, expose a `Restore[Entity]` Action and route. Otherwise, soft delete is just for audit trail.
5. **Listing UI:** the default list query excludes soft-deleted rows. Add a filter toggle "Show archived" if the user role permits.

### PDPA interaction

Soft delete is **not** PDPA erasure. PDPA s.43 erasure means the personal data is overwritten with a tombstone (or removed entirely from non-financial records). Financial records subject to Akta Koperasi 6-year retention are anonymised, not deleted, even on PDPA request — see Decision A4 (PDPA gate) and the Pdpa domain.

---

## 7. Encrypted Fields

**Sensitive identifiers use the two-column encryption pattern from Decision E3.** AMIR encrypts:
- Malaysian IC numbers (`ic_no_encrypted` + `ic_no_hash`) — per E3, mandatory for PDPA + breach blast-radius reduction
- Bank account numbers (`account_number_encrypted` + `account_number_hash`) — per E3
- Tax IDs / TINs (`tin_encrypted` + `tin_hash`) — per E3, for parties with TINs
- MFA secrets (`totp_secret`, `mfa_email_codes`) — single-column `'encrypted'` cast (no hash needed; never queried)
- Third-party credentials (`integration_credentials.client_secret`) — single-column `'encrypted'` cast

### The Two-Column Pattern (per Decision E3)

For any sensitive identifier that needs **exact-match lookup** AND **encryption-at-rest**:

```php
$table->text('ic_no_encrypted');           // AES-GCM via Laravel Crypt facade
$table->char('ic_no_hash', 64);            // SHA-256 of (per-tenant-salt || normalised plaintext)
$table->unique(['tenant_id', 'ic_no_hash']); // Unique within tenant
```

The per-tenant salt is **non-negotiable** (per E3 rationale): without it, Malaysian IC numbers (predictable structure: state code + DOB + serial) are vulnerable to global rainbow tables.

### Rules

1. **Migration:** column type `TEXT` for ciphertext (not `VARCHAR` — ciphertext is variable-length and can exceed VARCHAR limits). Hash column is `CHAR(64)` (fixed-length SHA-256 hex).
2. **Cast:** `'encrypted'` in the model for the `_encrypted` column. The `_hash` column is plain `string`.
   ```php
   protected $casts = [
       'ic_no_encrypted' => 'encrypted',
       'totp_secret' => 'encrypted',
   ];
   ```
3. **Per-tenant salt:** stored in `tenants.encryption_salt` (32 bytes random, generated at tenant creation, **never rotated post-creation**). Hash function: `hash('sha256', $tenant->encryption_salt . $normalisedPlaintext)`.
4. **Hash maintenance:** in the model, an observer maintains the hash on every write:
   ```php
   static::saving(function (Member $member): void {
       if ($member->isDirty('ic_no_encrypted')) {
           $tenant = $member->tenant ?? Tenant::find($member->tenant_id);
           $normalised = preg_replace('/\D/', '', $member->ic_no_encrypted);
           $member->ic_no_hash = hash('sha256', $tenant->encryption_salt . $normalised);
       }
   });
   ```
5. **Lookup pattern:** never query the `_encrypted` column directly. Always: hash the plaintext (with the right tenant's salt) → query by hash → decrypt for display.
6. **APP_KEY rotation:** when `APP_KEY` rotates, encrypted columns must be re-encrypted via a maintenance job. Plan for this — never assume the key is permanent. Note: the hash column does not depend on APP_KEY; only the `_encrypted` column does.

### Don't encrypt

- Audit log message bodies — these are immutable records meant to be human-readable; PII redaction happens at write time.
- Email addresses — too valuable to lose searchability over.
- Names — likewise.
- Addresses — likewise.

If you want stronger PII protection on these, that's a tokenisation conversation (separate project), not an encryption-at-cast conversation.

---

## 8. Backed Enums for Status & Type

**All status, type, and discriminator values use string-backed PHP enums. Never raw strings in WHERE clauses.**

### Schema

```sql
status VARCHAR(32) NOT NULL
```

(Not native Postgres ENUM type. Schema-level enums are too painful to alter — string column with a CHECK constraint is the AMIR convention if you want validation at the DB layer.)

Optional CHECK constraint:
```sql
ALTER TABLE members ADD CONSTRAINT chk_member_status
  CHECK (status IN ('ACTIVE', 'SUSPENDED', 'RESIGNED', 'DECEASED'));
```

### Enum class

```php
<?php
declare(strict_types=1);

namespace App\Domain\Members\Enums;

enum MemberStatus: string
{
    case Active = 'ACTIVE';
    case Suspended = 'SUSPENDED';
    case Resigned = 'RESIGNED';
    case Deceased = 'DECEASED';

    public function label(): string
    {
        return __('members.status.' . $this->value);
    }
}
```

### Rules

1. **String-backed.** `enum X: string` — never `int`-backed.
2. **Cases PascalCase.** `Active`, `PendingReview`.
3. **Values SCREAMING_SNAKE_CASE.** `'ACTIVE'`, `'PENDING_REVIEW'`. Frontend ALL-CAPS strings must match.
4. **Cast in the model:** `'status' => MemberStatus::class`.
5. **Translations** via `label()` — never inline `match` in views.
6. **Comparisons in code:**
   ```php
   if ($member->status === MemberStatus::Active) { /* ... */ }
   $query->where('status', MemberStatus::Active);
   ```
7. **Adding cases:** always safe — new code reads new values.
8. **Removing cases:** never. Old DB rows would orphan. Mark `@deprecated` and stop assigning.

### Anti-patterns

- ❌ `$query->where('status', 'ACTIVE')` — use the enum: `MemberStatus::Active`.
- ❌ `match ($status) { 'ACTIVE' => 'Aktif', ... }` in a Blade or JSX — use the enum's `label()` method.
- ❌ Inline arrays of valid status values for validation — use `Rule::enum(MemberStatus::class)`.

---

## 9. Audit Logging via spatie/laravel-activitylog

**Every model that requires audit trail uses `LogsActivity` from spatie/laravel-activitylog v4.**

### When to log

| Log changes | Don't log |
|---|---|
| Member updates (role changes, status changes) | High-frequency counters (`shares_held`, `subscription_balance_cents`) — log the source operation instead |
| Transaction posting | Cache reads |
| Period close / reopen | Login attempts (separate `login_audit` table) |
| Permission grants | Search queries |
| Configuration changes | Resource fetch |
| Right-to-erasure events | |

### Configure

```php
public function getActivitylogOptions(): LogOptions
{
    return LogOptions::defaults()
        ->logOnly(['ic_number', 'full_name', 'phone_e164', 'email', 'status'])
        ->logOnlyDirty()
        ->dontSubmitEmptyLogs();
}
```

### Rules

1. **Whitelist fields with `logOnly([...])`** — never use `logAll()`. Logging encrypted fields, timestamps, and high-frequency columns produces noise that buries real signal.
2. **`logOnlyDirty()` always** — only log columns that actually changed.
3. **`dontSubmitEmptyLogs()`** — silence no-op updates.
4. **Causer is automatic** — Spatie attaches `causer_id` (the authenticated user) and `subject_id` (the model). Don't override.
5. **Custom event names** for non-standard operations (a state transition is not just an update):
   ```php
   activity()
       ->causedBy(auth()->user())
       ->performedOn($member)
       ->withProperties(['from' => 'ACTIVE', 'to' => 'SUSPENDED', 'reason' => $reason])
       ->log('member.suspended');
   ```
6. **Encrypted fields:** if logged, the activity log row holds the encrypted ciphertext (the cast applies on read). Decrypted display happens at the resource. Be aware: anyone with DB read access cannot read encrypted log entries — that's a feature, not a bug.

### Tenant scoping for the audit log itself

The `activity_log` table has a `tenant_id` column (added via a migration that publishes the spatie config and amends it). Reads are scoped via TenantScope so PO sees all, tenant users see only their own.

---

## 10. Optimistic Locking via `version` Column

**Mutable financial records (transactions, journal entries, periods) carry an integer `version` column.**

### Why

Concurrent edits of a journal entry by two tenant admins can silently overwrite each other. Optimistic locking detects this and prompts the second writer to refresh.

### Rules

1. **Migration:**
   ```php
   $table->unsignedInteger('version')->default(1);
   ```
2. **Model observer** (registered in domain ServiceProvider):
   ```php
   static::updating(function (JournalEntry $entry): void {
       if ($entry->isDirty()) {
           $entry->version = ($entry->getOriginal('version') ?? 0) + 1;
       }
   });
   ```
3. **Controller / Action**: client supplies the baseline version. Action checks it:
   ```php
   public function execute(string $entryId, int $baselineVersion, array $data): JournalEntry
   {
       return DB::transaction(function () use ($entryId, $baselineVersion, $data): JournalEntry {
           $entry = JournalEntry::lockForUpdate()->findOrFail($entryId);

           if ($entry->version !== $baselineVersion) {
               throw new ConcurrentEditException(
                   currentVersion: $entry->version,
                   suppliedVersion: $baselineVersion,
               );
           }

           $entry->update($data);
           return $entry->fresh();
       });
   }
   ```
4. **Frontend** receives `version` in the resource and includes it on update submission. UI surfaces the conflict gracefully.

### Apply to

- `journal_entries` (and `journal_lines` via parent's version)
- `accounting_periods`
- `surplus_runs`
- `pawn_tickets`
- `member_subscription_schedules`

Don't apply to high-frequency counters or append-only logs.

---

## 11. Migrations — Additive Only

See `.claude/skills/writing-migrations.md` for the full rules. Headlines:

| Operation | Status | Approach |
|---|---|---|
| Add a new table | Safe | Free |
| Add a nullable column | Safe | Free |
| Add a column with a default | Safe | Free |
| Add an index | Safe | Free |
| Drop a column | Dangerous | Two-sprint deprecation: stop writing in sprint N, drop in sprint N+1 (after deploys) |
| Rename a column | Dangerous | Three-step: add new → dual-write → drop old, across two sprints |
| Change column type | Dangerous | Add new → backfill → drop old |
| Add NOT NULL without default | Dangerous | Always provide a default, or make nullable + backfill + alter |
| Remove an enum value | Forbidden | Never. Mark `@deprecated`, stop assigning, leave the case |

### Migration template

```php
<?php
declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::create('member_subscriptions', function (Blueprint $table): void {
            $table->uuid('id')->primary();
            $table->uuid('tenant_id');
            $table->uuid('member_id');
            $table->bigInteger('amount_cents')->default(0);
            $table->date('billing_period_start');
            $table->date('billing_period_end');
            $table->string('status', 32);
            $table->timestampsTz();
            $table->softDeletesTz();
            $table->unsignedInteger('version')->default(1);

            $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('restrict');
            $table->foreign('member_id')->references('id')->on('members')->onDelete('restrict');

            $table->index(['tenant_id', 'member_id']);
            $table->index(['tenant_id', 'billing_period_start']);
            $table->index(['tenant_id', 'status']);
        });

        DB::statement("ALTER TABLE member_subscriptions ADD CONSTRAINT chk_ms_amount CHECK (amount_cents >= 0)");
        DB::statement("ALTER TABLE member_subscriptions ADD CONSTRAINT chk_ms_period CHECK (billing_period_end >= billing_period_start)");
    }

    public function down(): void
    {
        Schema::dropIfExists('member_subscriptions');
    }
};
```

---

## 12. Naming

| Item | Convention | Example |
|---|---|---|
| Tables | `snake_case`, plural | `member_subscriptions`, `journal_entries` |
| Columns | `snake_case` | `created_at`, `tenant_id`, `total_amount_cents` |
| Models | `PascalCase`, singular | `MemberSubscription`, `JournalEntry` |
| Enum classes | `PascalCase`, singular noun | `MemberStatus`, `JournalEntryState` |
| Actions | `PascalCase`, verb phrase | `CreateMember`, `PostJournalEntry` |
| Events | `PascalCase`, past tense | `MemberRegistered`, `JournalEntryPosted` |
| Listeners | `PascalCase`, verb phrase | `AllocateMemberAccount`, `PrimeMemberHealthScore` |
| Jobs | `PascalCase`, ends in `Job` | `SubmitInvoiceToMyInvoisJob` |
| FormRequests | `PascalCase`, ends in `Request` | `CreateMemberRequest` |
| Resources | `PascalCase`, ends in `Resource` | `MemberResource` |
| Policies | `PascalCase`, ends in `Policy` | `MemberPolicy` |
| Controllers | `PascalCase`, ends in `Controller` | `CreateMemberController` |
| Routes (URI) | `kebab-case`, plural | `/members`, `/journal-entries`, `/ar-rahnu/tickets` |
| Route names | `dot.case` | `members.create`, `journal-entries.post` |
| Frontend components | `PascalCase` | `<MemberList>`, `<JournalEntryForm>` |
| Frontend hooks | `camelCase` starting with `use` | `useMember`, `useTenantContext` |
| Translation keys | `dot.case` | `members.status.active`, `validation.ic_format` |

---

## 13. Common AMIR-Specific Domain Conventions

### Chart of Accounts (CoA)

- Account codes are **`VARCHAR(10)`**, hierarchical (e.g. `1100`, `1100.01`, `1100.01.001`). Never integer.
- Account types: `ASSET`, `LIABILITY`, `EQUITY`, `INCOME`, `EXPENSE`, `CONTRA_*` — all enums via `AccountType`.
- Master CoA template lives in `master_chart_of_accounts_templates`. Each tenant clones from a template at onboarding into `chart_of_accounts`. Master is platform-scoped; tenant CoA is tenant-scoped.

### Journal Entries

- Header: `journal_entries` with `total_debits_cents` and `total_credits_cents` (must equal when state in `('approved','posted')` — enforced by CHECK constraint).
- Lines: `journal_lines` with `debit_cents` OR `credit_cents` non-zero, never both — enforced by CHECK.
- States: `DRAFT`, `PENDING_APPROVAL`, `APPROVED`, `POSTED`, `REVERSED` — enum `JournalEntryState`.
- Posted entries are immutable. Corrections go through the reversal flow (a new entry that mirrors the original).

### Members & Shares

- `shares_held` is an integer count, not cents. Each share is one unit; the share value is recorded on the share class.
- `subscription_balance_cents` is the running balance the member owes the koperasi for monthly subscription contributions.
- Member status transitions: see `MemberStatus` enum. State machine documented in `docs/living/DOMAIN_GUIDE.md`.

### Ar-Rahnu (Islamic Pawn)

- Gold valuation is in **`grams`** (integer milligrams stored: `weight_milligrams`) and price-per-gram in **`pricing_cents_per_gram`**.
- Tawarruq structure (per Decision F-locked Ar-Rahnu spec): cost-plus-profit with fixed margin. Never qard-wadi'ah-ujrah (Shariah non-compliant per BNM-MPS 2019).
- Tickets have a maximum 6-month tenure with 12-month maximum extension.

### MyInvois

- Invoice `einv_status` enum: `NOT_SUBMITTED`, `SUBMITTED`, `VALIDATED`, `REJECTED`, `CANCELLED`.
- 72-hour cancellation window is **LHDN policy**, distinct from PDPA s.12B.
- RM 10,000 rule: any single transaction ≥ RM 10,000 (`amount_cents >= 1_000_000`) requires individual e-invoice. Consolidated batches must exclude these.
- Specific Guideline v4.6 (5 January 2026) is the current spec. Schema version is recorded in tenant settings.

### PDPA

- DSAR (s.30) — 21-day SLA. Stored in `dsars` table with status enum.
- Breach notification (s.12B + DBN Guideline 2025) — 72h target. Stored in `pdpa_breaches` with severity assessment.
- Data portability (s.43A) — JSON + CSV export in structured format.
- All PDPA actions emit events for audit log.

---

## 14. Verification Before Writing Schema

Run these Boost tools before writing any migration or model:

| Tool | Question it answers |
|---|---|
| `database_schema` | What tables and columns actually exist right now? |
| `tinker` (`Member::first()`) | What does an actual model row look like? |
| `list_artisan_commands` | Are there existing data migration commands I should reuse? |
| `application_info` | What package versions are installed? |

Check `docs/living/DATA_MODEL.md` for the team's snapshot of the schema. Check `docs/living/DOMAIN_GUIDE.md` for state machine and invariant documentation. Check `ARCHITECTURE.md` for the planned schema.

If `docs/living/DATA_MODEL.md` and `database_schema` disagree, **`database_schema` wins** and `docs/living/DATA_MODEL.md` needs updating in this same PR.

---

## 15. Decisions Trace

Every rule in this file traces back to a decision in `DECISIONS_LOG.md`:

| Convention | Decision | Reversibility |
|---|---|---|
| Money: integer cents in `*_cents BIGINT` columns | **D16** | 🔴 Irreversible |
| Money cast: `MoneyCast::class` (Money value object) | **D16** | 🔴 Irreversible |
| Primary keys: UUID v7 via `HasUuids` + `Str::uuid7()` override | **D15-R1** | 🔴 Irreversible |
| Security tokens: UUID v4 (non-sortable) | **D15-R1** | 🔴 Irreversible |
| Tenant column: `tenant_id UUID NOT NULL`; trait: `BelongsToTenant`; scope: `TenantScope` | **D28** | 🟡 Costly |
| Tenant model: junction table (`tenant_user_memberships`) for multi-tenant users | **E1** | 🔴 Irreversible |
| Member→Party automation: eager creation via observer | **E2** | 🟡 Costly |
| Sensitive ID encryption: two-column pattern (`*_encrypted` TEXT + `*_hash` CHAR(64)) with per-tenant salt | **E3** | 🔴 Irreversible |
| Audit log: spatie/laravel-activitylog v4 + custom tenant_id/IP/UA/request_id columns | **D19**, **D29**, **E4** | 🟡 Costly |
| Date/time: UTC storage, `timestampsTz()`, MYT display, ISO 8601 in APIs | **D17** | 🔴 Irreversible |
| Soft deletes: selective use, never on financial transactions | **D18** | 🟡 Costly |
| Phone format: E.164 in `phone_e164 VARCHAR(20)` columns | **D33** | 🟡 Costly |

If you want to deviate from any rule, raise a revision (`Dxx-R1`) in `DECISIONS_LOG.md` first. Do not silently drift in a single PR — schema migrations are the most expensive thing to walk back.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .ai/guidelines/testing-standards.md (26,640 bytes)"
mkdir -p ".ai/guidelines"
cat > '.ai/guidelines/testing-standards.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised reproduction or use outside of
  Parsec Sdn. Bhd.-authorised projects is prohibited.
-->

# AMIR Testing Standards

This file is auto-loaded by Laravel Boost on every Claude Code session. Read it before writing any test.

These standards are AMIR-specific. They build on top of `.claude/skills/writing-tests.md` (general principles). Where the two overlap, **the more specific rule wins** — usually this file.

---

## 1. The Single Most Important Rule

**Every integration test asserts query performance. No exceptions.**

A passing test that issues 47 queries to load a member detail page is not "passing". It is a future incident waiting to happen. The `AssertsQueryPerformance` trait makes the budget visible at the top of every test:

```php
$this->assertQueryCountLessThan(15, function () {
    $this->get(route('members.show', $member));
});
```

If you write a feature test without a query-count assertion, the code reviewer rejects the PR.

---

## 2. What a Good Test Proves

A good test proves a **business outcome**, not an implementation detail.

- ❌ "The CreateMember action is called."
- ✅ "Given a valid member request, the member is created with status=active, the MemberRegistered event fires, the audit log entry is recorded, and the tenant's member count increases by 1."

When in doubt, write the test name as the business outcome. If you can't express the outcome in plain English, you're testing the wrong thing.

---

## 3. Test Structure: Arrange / Act / Assert

Every test follows this structure strictly.

```php
it('suspends a member when the tenant admin invokes the action', function () {
    // ARRANGE — set up all prerequisites
    $tenant = Tenant::factory()->create();
    $admin = User::factory()->tenantAdmin($tenant)->create();
    $member = Member::factory()->forTenant($tenant)->active()->create();

    $this->actingAs($admin);
    app()->instance('currentTenantId', $tenant->id);

    // ACT — perform exactly one action
    $response = $this->post(route('members.suspend', $member), [
        'reason' => 'Subscription default',
    ]);

    // ASSERT — verify every consequential outcome
    $response->assertSuccessful();

    expect($member->fresh()->status)->toBe(MemberStatus::Suspended);
    Event::assertDispatched(MemberSuspended::class);
    expect(Activity::all())->toContain(fn ($a) => $a->event === 'member.suspended');
});
```

### Rules

1. **One Act per test.** If you find yourself wanting two `$this->post(...)` calls, split into two tests.
2. **Comments demarcate each section.** The `// ARRANGE`, `// ACT`, `// ASSERT` comments are mandatory in feature tests.
3. **Descriptive `it(...)` names.** Read the test name aloud — does it describe a business outcome? If yes, ship it. If no, rename.

---

## 4. What to Assert — The Full List

For every feature test, assert ALL applicable items:

1. **HTTP status code** — exact code, not just "successful":
   ```php
   $response->assertStatus(201);  // not assertSuccessful() for create operations
   ```
2. **Response shape** — key fields in the response body:
   ```php
   $response->assertJson([
       'data' => [
           'id' => $member->id,
           'status' => 'ACTIVE',
       ],
   ]);
   ```
3. **Database state** — what was created, updated, or deleted:
   ```php
   $this->assertDatabaseHas('members', [
       'ic_number_hash' => hash('sha256', '900101101234'),
       'status' => 'ACTIVE',
   ]);
   ```
4. **Events dispatched** — every domain event that should have fired:
   ```php
   Event::assertDispatched(MemberRegistered::class, function ($event) use ($member) {
       return $event->memberId === $member->id;
   });
   ```
5. **Jobs queued** — if an action queues background work:
   ```php
   Queue::assertPushed(SubmitInvoiceToMyInvoisJob::class);
   ```
6. **Notifications sent** — if a notification was triggered:
   ```php
   Notification::assertSentTo($member, MemberWelcomeNotification::class);
   ```
7. **Audit log entries** — for every model change that requires audit:
   ```php
   expect(Activity::query()->where('subject_id', $member->id)->count())->toBe(1);
   ```
8. **What did NOT happen** — assert absence for rejection cases:
   ```php
   Event::assertNotDispatched(MemberRegistered::class);
   $this->assertDatabaseMissing('members', ['ic_number_hash' => hash('sha256', '900101101234')]);
   ```

---

## 5. The `AssertsQueryPerformance` Trait

This trait is mandatory on every feature test class. Use it to detect N+1 queries and slow scans **before** they reach production.

### Trait implementation

Save to `tests/Concerns/AssertsQueryPerformance.php`:

```php
<?php
declare(strict_types=1);

namespace Tests\Concerns;

use Closure;
use Illuminate\Support\Facades\DB;
use PHPUnit\Framework\Assert;

trait AssertsQueryPerformance
{
    /**
     * Assert that a callable issues fewer than $max queries.
     */
    public function assertQueryCountLessThan(int $max, Closure $callback): mixed
    {
        DB::enableQueryLog();
        DB::flushQueryLog();

        $result = $callback();

        $queries = DB::getQueryLog();
        $count = count($queries);

        DB::disableQueryLog();

        Assert::assertLessThan(
            $max,
            $count,
            sprintf(
                "Expected fewer than %d queries; got %d.\n\nQueries:\n%s",
                $max,
                $count,
                $this->formatQueries($queries),
            ),
        );

        return $result;
    }

    /**
     * Assert that a callable issues no duplicate queries (catches N+1 patterns).
     */
    public function assertNoDuplicateQueries(Closure $callback): mixed
    {
        DB::enableQueryLog();
        DB::flushQueryLog();

        $result = $callback();

        $queries = DB::getQueryLog();
        $normalised = array_map(
            fn ($q) => preg_replace('/\b\d+\b/', '?', $q['query']),
            $queries,
        );

        DB::disableQueryLog();

        $counts = array_count_values($normalised);
        $duplicates = array_filter($counts, fn ($n) => $n > 1);

        Assert::assertEmpty(
            $duplicates,
            sprintf(
                "Detected duplicate queries:\n%s",
                json_encode($duplicates, JSON_PRETTY_PRINT),
            ),
        );

        return $result;
    }

    /**
     * Assert that no query exceeds $thresholdMs milliseconds.
     */
    public function assertNoSlowQueries(Closure $callback, int $thresholdMs = 100): mixed
    {
        DB::enableQueryLog();
        DB::flushQueryLog();

        $result = $callback();

        $queries = DB::getQueryLog();
        $slow = array_filter($queries, fn ($q) => $q['time'] > $thresholdMs);

        DB::disableQueryLog();

        Assert::assertEmpty(
            $slow,
            sprintf(
                "Detected slow queries (>%dms):\n%s",
                $thresholdMs,
                $this->formatQueries(array_values($slow)),
            ),
        );

        return $result;
    }

    private function formatQueries(array $queries): string
    {
        return implode("\n", array_map(
            fn ($i, $q) => sprintf('  [%d] (%dms) %s', $i + 1, $q['time'], $q['query']),
            array_keys($queries),
            $queries,
        ));
    }
}
```

### Usage

```php
uses(\Tests\Concerns\AssertsQueryPerformance::class);

it('loads the member detail page within budget', function () {
    $tenant = Tenant::factory()->create();
    $member = Member::factory()->forTenant($tenant)->withSubscriptions(12)->create();
    $admin = User::factory()->tenantAdmin($tenant)->create();

    $this->actingAs($admin);
    app()->instance('currentTenantId', $tenant->id);

    $this->assertQueryCountLessThan(10, function () use ($member) {
        $response = $this->get(route('members.show', $member));
        $response->assertSuccessful();
    });

    $this->assertNoDuplicateQueries(function () use ($member) {
        $this->get(route('members.show', $member));
    });
});
```

### Default budgets

These budgets apply unless the test specifies a stricter budget:

| Endpoint type | Query budget |
|---|---|
| Detail (`show`) | 10 queries |
| Dashboard / overview | 15 queries |
| List (`index`) | 20 queries |
| Mutating action (`store`, `update`) | 25 queries |
| Bulk operation (≥10 entities) | 50 queries |

If a test naturally exceeds these, do not raise the budget without justification. Optimise: add eager loading, denormalise to a precomputed aggregate, or cache.

---

## 6. Edge Cases to Always Cover

For every feature, write tests for:

1. **Happy path** — the normal success case.
2. **Validation rejection** — missing or malformed input fails with 422.
3. **Authorisation rejection** — wrong role / wrong tenant / wrong owner fails with 403.
4. **State-conflict rejection** — e.g. suspending an already-suspended member, posting to a closed period, cancelling an already-cancelled invoice.
5. **Boundary values** — zero amounts, maximum string lengths, edge dates, empty lists, unicode and BM characters in user input.
6. **Cross-tenant isolation** — for any tenant-scoped resource, assert that User from Tenant A cannot read/write Tenant B's records.

### Cross-tenant isolation template

```php
it('does not allow tenant A admin to view tenant B members', function () {
    $tenantA = Tenant::factory()->create();
    $tenantB = Tenant::factory()->create();

    $adminA = User::factory()->tenantAdmin($tenantA)->create();
    $memberB = Member::factory()->forTenant($tenantB)->create();

    $this->actingAs($adminA);
    app()->instance('currentTenantId', $tenantA->id);

    $this->get(route('members.show', $memberB))
        ->assertNotFound();
});
```

---

## 7. What NOT to Test

- **Private methods.** Test through the public interface. If a private method's behaviour matters enough to test directly, it probably wants to be its own class.
- **Framework behaviour.** Don't test that auth middleware redirects unauthenticated users — Laravel does that.
- **Third-party packages.** Mock the boundary; don't test the package's internals. Use Spatie's test helpers, not handcrafted activity-log row queries (except where AMIR-specific config matters — in which case test only the config behaviour).
- **Implementation internals.** If a test breaks on a refactor that doesn't change behaviour, it was testing the wrong thing.
- **Generated fixtures.** Don't test factory output. Test what your code does with factory output.

---

## 8. Factory Conventions

### Where factories live

`database/factories/Domain/[Domain]/[Model]Factory.php` — co-located by domain.

### Shape

```php
<?php
declare(strict_types=1);

namespace Database\Factories\Domain\Members;

use App\Domain\Members\Enums\MemberStatus;
use App\Domain\Members\Models\Member;
use App\Models\Tenant;
use Illuminate\Database\Eloquent\Factories\Factory;

final class MemberFactory extends Factory
{
    protected $model = Member::class;

    public function definition(): array
    {
        return [
            'tenant_id' => Tenant::factory(),
            'ic_number' => $this->fakeMyIcNumber(),
            'full_name' => fake()->name(),
            'phone_e164' => '+60' . fake()->numerify('1#########'),
            'email' => fake()->optional(0.7)->safeEmail(),
            'status' => MemberStatus::Active,
            'shares_held' => fake()->numberBetween(1, 1000),
            'subscription_balance_cents' => 0,
        ];
    }

    public function forTenant(Tenant $tenant): self
    {
        return $this->state(['tenant_id' => $tenant->id]);
    }

    public function active(): self
    {
        return $this->state(['status' => MemberStatus::Active]);
    }

    public function suspended(): self
    {
        return $this->state(['status' => MemberStatus::Suspended]);
    }

    public function withSubscriptions(int $count): self
    {
        return $this->afterCreating(function (Member $member) use ($count): void {
            MemberSubscription::factory()
                ->forMember($member)
                ->count($count)
                ->create();
        });
    }

    private function fakeMyIcNumber(): string
    {
        $year = fake()->numberBetween(50, 99);
        $month = sprintf('%02d', fake()->numberBetween(1, 12));
        $day = sprintf('%02d', fake()->numberBetween(1, 28));
        $state = sprintf('%02d', fake()->numberBetween(1, 16));
        $serial = sprintf('%04d', fake()->numberBetween(1, 9999));
        return "{$year}{$month}{$day}-{$state}-{$serial}";
    }
}
```

### Rules

1. **Always use factories.** Never `new Model([...])` in tests.
2. **Minimal definition.** Only set what every test needs. Specifics belong in `state` methods.
3. **State methods for roles and statuses:** `User::factory()->tenantAdmin($tenant)->create()`, `Member::factory()->active()->create()`, `JournalEntry::factory()->posted()->create()`. Always.
4. **Tenant-scoped factories** expose a `forTenant(Tenant $tenant)` state. Never let the factory implicitly create a fresh tenant in tests that compose multiple models.
5. **`afterCreating` for relations.** When a factory needs to create dependent rows, use `afterCreating` — it runs after the parent is committed.
6. **Faker locale.** Use `fake('ms_MY')` for BM-localised content. Configure in `tests/Pest.php`.
7. **Realistic data.** IC numbers, phone numbers, addresses must look like real Malaysian data. Use the `fakeMyIcNumber()` helper or similar — don't accept Faker's defaults.

### Cross-tenant pollution

A common bug: `Member::factory()->count(3)->create()` creates 3 members each with their own fresh `Tenant` (because the definition uses `Tenant::factory()`). Tests then fail mysteriously because the global `currentTenantId` only matches one of them. Always use `->forTenant($tenant)` when you need them all in the same tenant.

---

## 9. Pest Folder Structure

```
tests/
  Pest.php                              # global Pest config and helpers
  Concerns/
    AssertsQueryPerformance.php         # the trait above
    InteractsWithTenancy.php            # helpers for tenant context setup
  TestCase.php                          # base test case
  Unit/
    Domain/
      Members/
        Actions/
          CreateMemberTest.php
          SuspendMemberTest.php
        Enums/
          MemberStatusTest.php
  Feature/
    Domain/
      Members/
        Http/
          CreateMemberControllerTest.php
          SuspendMemberControllerTest.php
        Policies/
          MemberPolicyTest.php
  Browser/
    MemberManagementTest.php            # Playwright-driven end-to-end
```

### Rules

1. **Mirror `app/Domain/[Name]/` structure** in `tests/Unit/Domain/[Name]/`. Every Action gets a unit test file.
2. **Feature tests by HTTP layer:** `tests/Feature/Domain/[Name]/Http/[Controller]Test.php`.
3. **Browser tests sparingly** — only for flows that span multiple controllers and where Playwright's offline simulation matters (e.g. PWA offline mode).
4. **`Pest.php` registers shared traits and helpers** — don't duplicate `uses(...)` at the top of every file when a global registration covers all feature tests:
   ```php
   // tests/Pest.php
   uses(
       Tests\TestCase::class,
       Tests\Concerns\AssertsQueryPerformance::class,
       Tests\Concerns\InteractsWithTenancy::class,
   )->in('Feature');
   ```

---

## 10. Test Templates

### Unit test for an Action

```php
<?php
declare(strict_types=1);

use App\Domain\Members\Actions\CreateMember;
use App\Domain\Members\Enums\MemberStatus;
use App\Domain\Members\Events\MemberRegistered;
use App\Domain\Members\Models\Member;
use App\Models\Tenant;
use Illuminate\Support\Facades\Event;

it('creates a member with status active and dispatches MemberRegistered', function () {
    Event::fake([MemberRegistered::class]);

    $tenant = Tenant::factory()->create();
    $action = app(CreateMember::class);

    $member = $action->execute(
        tenantId: $tenant->id,
        data: [
            'ic_number' => '900101-10-1234',
            'full_name' => 'Ahmad bin Abdullah',
            'phone_e164' => '+60123456789',
            'email' => 'ahmad@example.my',
        ],
    );

    expect($member)->toBeInstanceOf(Member::class)
        ->and($member->tenant_id)->toBe($tenant->id)
        ->and($member->status)->toBe(MemberStatus::Active)
        ->and($member->shares_held)->toBe(0)
        ->and($member->subscription_balance_cents)->toBe(0);

    Event::assertDispatched(MemberRegistered::class, function ($event) use ($member, $tenant) {
        return $event->memberId === $member->id
            && $event->tenantId === $tenant->id;
    });
});

it('rolls back the transaction if anything fails after Member::create', function () {
    Event::fake();

    $tenant = Tenant::factory()->create();
    $action = app(CreateMember::class);

    // Force the event listener to throw. Use a real listener that we can sabotage,
    // or assert via a transaction-aware mechanism.
    DB::beginTransaction();

    try {
        // Setup that will cause a downstream listener to fail, e.g. by replacing
        // a service with a throwing mock.
        // ...

        $action->execute(/* ... */);
    } catch (\Throwable $e) {
        // Expected.
    }

    DB::rollBack();

    expect(Member::count())->toBe(0);
});
```

### Feature test for a controller

```php
<?php
declare(strict_types=1);

use App\Domain\Members\Enums\MemberStatus;
use App\Domain\Members\Models\Member;
use App\Models\Tenant;
use App\Models\User;

beforeEach(function () {
    $this->tenant = Tenant::factory()->create();
    $this->admin = User::factory()->tenantAdmin($this->tenant)->create();

    $this->actingAs($this->admin);
    app()->instance('currentTenantId', $this->tenant->id);
});

it('creates a member via POST /members', function () {
    $this->assertQueryCountLessThan(15, function () {
        $response = $this->postJson(route('members.create'), [
            'ic_number' => '900101-10-1234',
            'full_name' => 'Ahmad bin Abdullah',
            'phone_e164' => '+60123456789',
            'email' => 'ahmad@example.my',
        ]);

        $response->assertStatus(201);
    });

    $this->assertDatabaseHas('members', [
        'tenant_id' => $this->tenant->id,
        'full_name' => 'Ahmad bin Abdullah',
        'status' => 'ACTIVE',
    ]);
});

it('rejects the request when phone is not E.164', function () {
    $this->postJson(route('members.create'), [
        'ic_number' => '900101-10-1234',
        'full_name' => 'Ahmad bin Abdullah',
        'phone_e164' => '012-3456789', // not E.164
    ])->assertStatus(422)
      ->assertJsonValidationErrors(['phone_e164']);
});

it('rejects the request when the tenant user lacks the admin role', function () {
    $user = User::factory()->tenantUser($this->tenant)->create();
    $this->actingAs($user);

    $this->postJson(route('members.create'), [
        'ic_number' => '900101-10-1234',
        'full_name' => 'Ahmad bin Abdullah',
        'phone_e164' => '+60123456789',
    ])->assertStatus(403);
});

it('does not allow tenant A admin to create members in tenant B context', function () {
    $tenantB = Tenant::factory()->create();

    // Spoof the tenant context — should fail tenant.context middleware
    app()->instance('currentTenantId', $tenantB->id);

    $this->postJson(route('members.create'), [
        'ic_number' => '900101-10-1234',
        'full_name' => 'Cross-tenant attack',
        'phone_e164' => '+60123456789',
    ])->assertStatus(403);
});
```

### Test for an Action with audit log

```php
it('writes an activity log entry when a member is suspended', function () {
    $tenant = Tenant::factory()->create();
    $member = Member::factory()->forTenant($tenant)->active()->create();
    $admin = User::factory()->tenantAdmin($tenant)->create();

    $this->actingAs($admin);
    app()->instance('currentTenantId', $tenant->id);

    app(\App\Domain\Members\Actions\SuspendMember::class)->execute(
        memberId: $member->id,
        reason: 'Subscription default',
    );

    $activity = \Spatie\Activitylog\Models\Activity::query()
        ->where('subject_type', Member::class)
        ->where('subject_id', $member->id)
        ->where('event', 'member.suspended')
        ->latest()
        ->first();

    expect($activity)->not->toBeNull()
        ->and($activity->causer_id)->toBe($admin->id)
        ->and($activity->properties['reason'])->toBe('Subscription default')
        ->and($activity->properties['from'])->toBe('ACTIVE')
        ->and($activity->properties['to'])->toBe('SUSPENDED');
});
```

---

## 11. Database Setup for Tests

### `RefreshDatabase` trait

Every Feature test class uses `RefreshDatabase`. Tests run against a real Postgres database (not SQLite — AMIR uses Postgres-specific features like `TIMESTAMPTZ`, `JSONB`, and CHECK constraints).

```php
// tests/Feature/Domain/Members/Http/CreateMemberControllerTest.php
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
```

### CI database

The CI pipeline runs against a Postgres 16 service container. Local development uses Herd's bundled Postgres or Docker.

### Seed data

Don't seed in tests. Each test creates exactly the data it needs. Seeders are for Day-1 setup only (master CoA template, Pack catalogue, default permissions).

---

## 12. Mocking & Faking

### When to fake

| Subject | Approach |
|---|---|
| Domain events | `Event::fake([SpecificEvent::class])` per test |
| Queue dispatch | `Queue::fake()` per test that asserts queue behaviour |
| Notifications | `Notification::fake()` per test that asserts delivery |
| Mail | `Mail::fake()` |
| HTTP outbound (MyInvois, etc.) | `Http::fake([...])` with explicit response stubs |
| Storage | `Storage::fake('s3')` — uses a real on-disk fake |
| Time | `Carbon::setTestNow($timestamp)` for deterministic time-dependent tests |

### When NOT to mock

- **Don't mock Eloquent models.** Use factories and a real database.
- **Don't mock Spatie Permission.** Use real role assignments via `$user->assignRole(...)`.
- **Don't mock the audit log.** Real log entries are easy to assert and the cost is negligible.
- **Don't mock policies.** Test the policy directly, via the controller.

### Faking external integrations

Every external service has a contract interface and a fake implementation:

```php
interface MyInvoisClient
{
    public function submit(InvoicePayload $payload): SubmissionResult;
}

final class FakeMyInvoisClient implements MyInvoisClient
{
    public function submit(InvoicePayload $payload): SubmissionResult
    {
        return new SubmissionResult(
            uin: 'TEST-' . str()->random(10),
            status: 'VALIDATED',
            submittedAt: now(),
        );
    }
}
```

Bind the fake in tests:

```php
$this->app->bind(MyInvoisClient::class, FakeMyInvoisClient::class);
```

For specific test scenarios (rejection, timeout), introduce variants: `RejectingMyInvoisClient`, `SlowMyInvoisClient`. Don't pollute the canonical fake with conditional return logic.

---

## 13. Browser Tests (Playwright)

Reserved for flows that genuinely require browser behaviour:
- PWA offline mode (sync pending, conflict resolution)
- Service worker behaviour
- Multi-step UI interactions where the controller-level test would miss the integration
- End-to-end demo recordings used for tender pitches

### Rules

1. **Live in `tests/Browser/`.**
2. **Run separately** from the main suite (`npm run test:browser`). They are slow.
3. **Do not gate PRs on browser tests** unless the change touches PWA / offline logic. Run locally before merging.
4. **Use Playwright's offline simulation** to test offline mode — `await context.setOffline(true)`.
5. **Real Postgres database**. Playwright tests share the same `RefreshDatabase` discipline.

---

## 14. Test Performance

### The full suite must run in under 60 seconds.

If it grows past 60s, that is a triage. Common causes:
- Tests creating excessive data via factories (especially when factory afterCreating cascades).
- Browser tests running by default (separate them).
- Slow integration tests (factor out to mocked seams).

### Run subsets locally

```bash
# Just the changed domain
php artisan test --filter=Members

# Just one file
php artisan test tests/Feature/Domain/Members/Http/CreateMemberControllerTest.php

# Just one test
php artisan test --filter='creates a member via POST'

# Parallel
php artisan test --parallel
```

---

## 15. Coverage

Coverage is a tool, not a goal. AMIR aims for:
- **>90% line coverage** on Action classes (the heart of the domain).
- **>80% line coverage** on controllers and policies.
- **No coverage target** on resources, factories, migrations, ServiceProviders.

If a line is uncovered, ask: is this dead code? If yes, delete. If no, write the missing test.

---

## 16. Verification Before Writing a Test

Run these Boost tools and read these files first:

| Tool / file | Purpose |
|---|---|
| `database_schema` | Confirm the actual schema matches your test's expectations |
| `tinker` (`Member::factory()->make()`) | Verify factory output looks right |
| `docs/living/DOMAIN_GUIDE.md` | Understand the state machine the test asserts |
| `USER_STORIES.md` (epic for the domain) | The test's acceptance criteria are here |
| Existing similar test in `tests/Feature/Domain/[Name]/` | Match the existing shape, not the framework default |

If you find yourself writing a test with a structure unlike any existing test in the codebase, **stop**. Either you've found a genuinely new pattern (rare) or you've drifted (common). Find the analogous test and follow its shape.

---

## 17. Decisions Trace

Every rule in this file traces back to a decision in `DECISIONS_LOG.md`:

| Convention | Decision | Reversibility |
|---|---|---|
| Test framework: Pest 3 for unit/feature; Playwright for E2E | **D24** | 🟢 Reversible |
| Real Postgres in tests (not SQLite) — required by `TIMESTAMPTZ`, `JSONB`, CHECK constraints | **D2** + **D17** | 🔴 Irreversible (driven by stack) |
| Tenant isolation tests are mandatory for every tenant-scoped feature | **D3**, **D28** | 🟡 Costly |
| Performance assertions (query count, no duplicates, no slow queries) on every integration test | (testing-standards convention) | 🟢 Reversible |
| Factory conventions (forTenant, fakeMyIcNumber, role states) | (testing-standards convention) | 🟢 Reversible |

If you want to deviate from any rule, raise a revision (`Dxx-R1`) in `DECISIONS_LOG.md` first.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/settings.json (2,419 bytes)"
mkdir -p ".claude"
cat > '.claude/settings.json' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
{
  "_meta": {
    "generated_by": "Parsec Sdn. Bhd. AI Development Framework v2",
    "project": "AMIR",
    "copyright": "© 2026 Parsec Sdn. Bhd.. All rights reserved.",
    "notice": "Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited."
  },
  "permissions": {
    "allow": [
      "Agent(*)",
      "RemoteTrigger(*)",
      "EnterWorktree(*)",
      "ExitWorktree(*)",
      "TeamCreate(*)",
      "TeamDelete(*)",
      "CronCreate(*)",
      "CronDelete(*)",
      "CronList(*)",
      "WebFetch(*)",
      "WebSearch(*)",
      "ToolSearch(*)",
      "TaskCreate(*)",
      "TaskUpdate(*)",
      "TaskList(*)",
      "TaskGet(*)",
      "TaskOutput(*)",
      "TaskStop(*)",
      "SendMessage(*)",
      "Read(*)",
      "Edit(*)",
      "Write(*)",
      "Glob(*)",
      "Grep(*)",
      "Bash(*)"
    ],
    "deny": []
  },
  "model": "sonnet",
  "effortLevel": "medium",
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/post-edit-format.sh"
          },
          {
            "type": "command",
            "command": "bash .claude/hooks/post-edit-tenant-scope-check.sh"
          },
          {
            "type": "command",
            "command": "bash .claude/hooks/post-edit-enum-sync-check.sh"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/pre-commit-guard.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/session-close-log.sh"
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo '{\"systemMessage\": \"SESSION START PROTOCOL — follow before any work:\\n1. Run /sprint-status (orients sprint state, uses Haiku automatically)\\n2. Never skip /sprint-status even when resuming a compacted session\\n3. Use /task-done [id] to mark tasks complete — never python/jq directly\\n4. Warn user if context >200k tokens\\n5. Pre-flight before every /team-launch: deps merged to dev? files free? context <300k?\"}'"
          }
        ]
      }
    ]
  }
}
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/hooks/post-edit-format.sh (1,055 bytes)"
mkdir -p ".claude/hooks"
cat > '.claude/hooks/post-edit-format.sh' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────
# Parsec Sdn. Bhd. · AMIR
# Generated by the Parsec Sdn. Bhd. AI Development Framework v2
# © 2026 Parsec Sdn. Bhd.. All rights reserved.
# Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised
# projects is prohibited.
# ─────────────────────────────────────────────────────────────
# Hook: post-edit-format
# Trigger: PostToolUse (Write, Edit)
# Auto-formats the file that was just written, by extension.

set -euo pipefail

FILE="${CLAUDE_HOOK_TOOL_INPUT_FILE_PATH:-}"

if [[ -z "$FILE" || ! -f "$FILE" ]]; then
  exit 0
fi

EXT="${FILE##*.}"

case "$EXT" in
  php)
    if [[ -f "./vendor/bin/pint" ]]; then
      ./vendor/bin/pint "$FILE" --quiet 2>/dev/null || true
    fi
    ;;
  ts|tsx|js|jsx)
    if command -v npx &>/dev/null; then
      npx prettier --write "$FILE" --log-level silent 2>/dev/null || true
    fi
    ;;
  py)
    if command -v black &>/dev/null; then
      black "$FILE" --quiet 2>/dev/null || true
    fi
    ;;
esac

exit 0
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/hooks/post-edit-tenant-scope-check.sh (2,901 bytes)"
mkdir -p ".claude/hooks"
cat > '.claude/hooks/post-edit-tenant-scope-check.sh' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────
# Parsec Sdn. Bhd. · AMIR
# Generated by the Parsec Sdn. Bhd. AI Development Framework v2
# © 2026 Parsec Sdn. Bhd.. All rights reserved.
# Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised
# projects is prohibited.
# ─────────────────────────────────────────────────────────────
# Hook: post-edit-tenant-scope-check
# Trigger: PostToolUse (Write, Edit on PHP files)
# Warns if a new Eloquent model is missing the BelongsToTenant trait (per Decision D28).
# A model missing this trait leaks data across tenants silently.

set -euo pipefail

FILE="${CLAUDE_HOOK_TOOL_INPUT_FILE_PATH:-}"

if [[ -z "$FILE" || ! -f "$FILE" ]]; then
  exit 0
fi

EXT="${FILE##*.}"
if [[ "$EXT" != "php" ]]; then
  exit 0
fi

# Only check files in app/Domain/*/Models/ or app/Models/
if ! echo "$FILE" | grep -qE '(app/Domain/[^/]+/Models/|app/Models/)'; then
  exit 0
fi

BASENAME=$(basename "$FILE" .php)

# Skip abstract/base/pivot classes by naming convention
if echo "$BASENAME" | grep -qiE '^(Base|Abstract|Pivot|[A-Z][a-z]+Pivot)'; then
  exit 0
fi

# Must contain a class definition
if ! grep -q 'class ' "$FILE"; then
  exit 0
fi

# Must extend Model or Authenticatable, OR use HasFactory (i.e. it's an Eloquent model)
if ! grep -qE '(extends Model|extends Authenticatable|use HasFactory)' "$FILE"; then
  exit 0
fi

# If the trait is already in use, all good
if grep -qE '(use BelongsToTenant|BelongsToTenant,|HasTenantScope|GlobalScope.*tenant)' "$FILE"; then
  exit 0
fi

# Skip platform-scoped models that are explicitly noted (per data-conventions.md §3 exceptions)
if grep -qE 'platform-scoped model|no tenant_id by design' "$FILE"; then
  exit 0
fi

# Skip a few known platform-scoped models by naming convention
if echo "$BASENAME" | grep -qE '^(Tenant|Pack|MasterChartOfAccountsTemplate|PlatformAdmin|Platform|Currency|LhdnTinRegistry)'; then
  exit 0
fi

HAS_TENANT_ID=$(grep -c 'tenant_id' "$FILE" 2>/dev/null || echo 0)

if [[ "$HAS_TENANT_ID" -eq 0 ]]; then
  echo ""
  echo "⚠️  TENANT SCOPE WARNING — $FILE"
  echo ""
  echo "   This model does not use the BelongsToTenant trait and has no tenant_id reference."
  echo "   Every tenant-owned model MUST use the BelongsToTenant trait (per Decision D28) to prevent"
  echo "   cross-tenant data leakage."
  echo ""
  echo "   If intentionally platform-scoped: add a comment // platform-scoped model — no tenant_id by design (see DECISIONS_LOG.md A6)"
  echo "   Otherwise, add to the model: use BelongsToTenant;"
  echo ""
else
  echo ""
  echo "⚠️  TENANT SCOPE WARNING — $FILE"
  echo ""
  echo "   This model references tenant_id but does not use the BelongsToTenant trait."
  echo "   Ensure the global scope is applied via the trait — not via manual where() clauses."
  echo "   Add to the model: use BelongsToTenant;"
  echo ""
fi

exit 0
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/hooks/post-edit-enum-sync-check.sh (2,196 bytes)"
mkdir -p ".claude/hooks"
cat > '.claude/hooks/post-edit-enum-sync-check.sh' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────
# Parsec Sdn. Bhd. · AMIR
# Generated by the Parsec Sdn. Bhd. AI Development Framework v2
# © 2026 Parsec Sdn. Bhd.. All rights reserved.
# Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised
# projects is prohibited.
# ─────────────────────────────────────────────────────────────
# Hook: post-edit-enum-sync-check
# Trigger: PostToolUse (Write, Edit on JSX/TSX files)
# Checks ALL-CAPS string literals against PHP enum case values.
# Catches frontend enum mismatches at write time. Addresses lesson 12-L16.

set -euo pipefail

FILE="${CLAUDE_HOOK_TOOL_INPUT_FILE_PATH:-}"

if [[ -z "$FILE" || ! -f "$FILE" ]]; then
  exit 0
fi

EXT="${FILE##*.}"
if [[ "$EXT" != "jsx" && "$EXT" != "tsx" ]]; then
  exit 0
fi

# Only check files in resources/js/ (the Inertia frontend)
if ! echo "$FILE" | grep -q 'resources/js/'; then
  exit 0
fi

# Extract ALL-CAPS string literals (likely enum values)
CAPS_STRINGS=$(grep -oE '"[A-Z][A-Z_]{1,}[A-Z]"' "$FILE" 2>/dev/null | tr -d '"' | sort -u || true)

if [[ -z "$CAPS_STRINGS" ]]; then
  exit 0
fi

PHP_ENUM_DIR="app/Domain"
if [[ ! -d "$PHP_ENUM_DIR" ]]; then
  exit 0
fi

# Collect all PHP enum values across the domain folders
ALL_PHP_ENUM_VALUES=$(
  find "$PHP_ENUM_DIR" -name "*.php" -path "*/Enums/*" 2>/dev/null \
  | xargs grep -h "case " 2>/dev/null \
  | grep -oE "case [A-Z_]+ = '[^']+'" \
  | grep -oE "'[^']+'" \
  | tr -d "'" \
  | sort -u
)

if [[ -z "$ALL_PHP_ENUM_VALUES" ]]; then
  exit 0
fi

UNMATCHED=()

while IFS= read -r value; do
  [[ -z "$value" ]] && continue
  if ! echo "$ALL_PHP_ENUM_VALUES" | grep -qx "$value"; then
    UNMATCHED+=("$value")
  fi
done <<< "$CAPS_STRINGS"

if [[ ${#UNMATCHED[@]} -gt 0 ]]; then
  echo ""
  echo "⚠️  ENUM SYNC WARNING — $FILE"
  echo ""
  echo "   The following ALL-CAPS string values were not found in any PHP enum file:"
  for val in "${UNMATCHED[@]}"; do
    echo "   → \"$val\""
  done
  echo ""
  echo "   Verify these against the PHP enum files in: app/Domain/*/Enums/"
  echo "   Wrong enum values pass frontend rendering but fail backend validation silently."
  echo ""
fi

exit 0
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/hooks/pre-commit-guard.sh (2,208 bytes)"
mkdir -p ".claude/hooks"
cat > '.claude/hooks/pre-commit-guard.sh' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────
# Parsec Sdn. Bhd. · AMIR
# Generated by the Parsec Sdn. Bhd. AI Development Framework v2
# © 2026 Parsec Sdn. Bhd.. All rights reserved.
# Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised
# projects is prohibited.
# ─────────────────────────────────────────────────────────────
# Hook: pre-commit-guard
# Trigger: PreToolUse (Bash — git commit)
# Blocks commits containing debug artifacts, missing separator, or Co-Authored-By.

set -euo pipefail

CMD="${CLAUDE_HOOK_TOOL_INPUT_COMMAND:-}"

# Only act on git commit calls
if ! echo "$CMD" | grep -q 'git commit'; then
  exit 0
fi

# ── 1. Check staged files for debug artifacts ─────────────────
STAGED=$(git diff --cached --name-only 2>/dev/null || true)
VIOLATIONS=()

while IFS= read -r file; do
  [[ -f "$file" ]] || continue

  if grep -qE '^\s*(dd\(|dump\(|var_dump\(|ray\()' "$file" 2>/dev/null; then
    VIOLATIONS+=("  $file: contains dd() / dump() / var_dump() / ray()")
  fi

  EXT="${file##*.}"
  if [[ "$EXT" =~ ^(ts|tsx|js|jsx)$ ]]; then
    if grep -qE '^\s*console\.(log|warn|error|debug)\(' "$file" 2>/dev/null; then
      VIOLATIONS+=("  $file: contains console.log/warn/error/debug")
    fi
  fi

done <<< "$STAGED"

if [[ ${#VIOLATIONS[@]} -gt 0 ]]; then
  echo "❌ COMMIT BLOCKED — debug artifacts found in staged files:"
  printf '%s\n' "${VIOLATIONS[@]}"
  echo ""
  echo "Remove all debug statements, then re-run /verify before committing."
  exit 1
fi

# ── 2. Hard-block on missing Morse separator ──────────────────
if ! echo "$CMD" | grep -q '\.--\.'; then
  echo "❌ COMMIT BLOCKED — Parsec separator missing from commit message."
  echo "   Required format:"
  echo "   [type]([scope]): [description]"
  echo "   .--. .- .-. ... . -.-."
  echo ""
  echo "   Add the separator line immediately after the subject line, then retry."
  exit 1
fi

# ── 3. Hard-block on Co-Authored-By ───────────────────────────
if echo "$CMD" | grep -qi 'Co-Authored-By\|Co-Author'; then
  echo "❌ COMMIT BLOCKED — Co-Authored-By is not permitted in commit messages."
  echo "   Remove the Co-Authored-By line and retry."
  exit 1
fi

exit 0
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/hooks/session-close-log.sh (950 bytes)"
mkdir -p ".claude/hooks"
cat > '.claude/hooks/session-close-log.sh' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────
# Parsec Sdn. Bhd. · AMIR
# Generated by the Parsec Sdn. Bhd. AI Development Framework v2
# © 2026 Parsec Sdn. Bhd.. All rights reserved.
# Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised
# projects is prohibited.
# ─────────────────────────────────────────────────────────────
# Hook: session-close-log
# Trigger: stop (agent session ends)
# Appends session-close timestamp to docs/living/VELOCITY_LOG.md

set -euo pipefail

VELOCITY_LOG="docs/living/VELOCITY_LOG.md"
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
WORKTREE=$(basename "$(git rev-parse --show-toplevel 2>/dev/null || echo 'unknown')")
BRANCH=$(git branch --show-current 2>/dev/null || echo 'unknown')

if [[ ! -f "$VELOCITY_LOG" ]]; then
  exit 0
fi

cat >> "$VELOCITY_LOG" << EOF

<!-- session-close: worktree=${WORKTREE} branch=${BRANCH} timestamp=${TIMESTAMP} -->
EOF

exit 0
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/rules/AGENT_GUIDE.md (9,610 bytes)"
mkdir -p ".claude/rules"
cat > '.claude/rules/AGENT_GUIDE.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->

# AGENT_GUIDE.md — How to Operate as an AMIR Agent

This file is auto-loaded on every session. It is the agent's operating manual — telling you how to behave, not how to write code. Code conventions are in `CLAUDE.md` and `.ai/guidelines/*.md`.

Read this first. Read it on every session start. The `SessionStart` hook reminds you to.

---

## 1. Orient First

**Every session begins with `/sprint-status`.** No exceptions. The hook reminds you. Skip it and you'll work on the wrong sprint.

`/sprint-status` is mechanical bookkeeping — uses Haiku automatically and returns to Sonnet when done. It surfaces:
- Current sprint number
- Tasks done / in-progress / pending
- Anything blocked (dependencies not yet merged, files not free)
- High-context-usage warning if you're approaching limits

After `/sprint-status`, you know:
- Which sprint you're in
- Which level (3 / 4 / 5 / 6) governs your workflow
- What to do next

---

## 2. Levels and Their Workflows

AMIR currently runs at **Level 3 (Manual)** — single agent, founder reviews every PR. The level is recorded in `.sprint-backlog.json` `_meta.development_level` and surfaces in `/sprint-status`.

### Level 3 — Manual (current)
Sequential workflow per task. One agent runs at a time. Founder reviews every PR.

```
/sprint-start [N]    (Haiku — populates Sprint N tasks from SPRINT_PLAN.md)
  ↓
/plan [task-id]      (Sonnet — plan the task; surface risks; do NOT code)
  ↓
/implement           (Sonnet — execute the approved plan)
  ↓
/simplify            (Sonnet — auto-cleanup of LLM bloat across changed files)
  ↓
/verify              (Sonnet — 13-step gate; mandatory before commit)
  ↓
/commit-push-pr      (Sonnet — commits with Parsec separator, opens PR)
  ↓
[Founder reviews + merges PR on GitHub]
  ↓
/task-done [task-id] (Haiku — marks task done in .sprint-backlog.json)
  ↓
[Loop to next task]
```

When all sprint tasks are done:
```
/sprint-close         (Sonnet — performance regression gate + living docs update)
```

### Level 4 — Parallel Dispatch
`./dispatch.sh [batch-name]` launches 3-4 agents in tmux panes. Each agent runs autonomously to PR. `./review.sh [batch-name]` summarises results. Founder reviews + merges each PR batch.

### Level 5 — Agent Teams
`/team-launch` spawns specialist agents (team-lead → backend-dev + frontend-dev (parallel) → test-writer → code-reviewer). Per-feature, not per-task.

### Level 6 — Multi-Worktree Parallel
`sprint-dispatch` runs `/team-launch` across 4 worktrees in parallel. Most advanced.

**Promotion is gated.** Don't promote until the criteria in `/level-up` are met (≥1 sprint at the current level shipped clean).

---

## 3. Pre-Flight Before Every Task

Before running `/plan`:
- [ ] Have I run `/sprint-status` this session?
- [ ] Are this task's dependencies (from `deps[]` in `.sprint-backlog.json`) all merged to `dev`?
- [ ] Are the files I'll touch (`files_touched[]`) not currently being modified by another agent?
- [ ] Is my context usage <300k tokens? (If higher, `/compact` first.)

---

## 4. Model Selection (automatic — don't override unless you know why)

| Task type | Model | When |
|---|---|---|
| `/sprint-status`, `/task-done`, `/sprint-start` (initial backlog populate) | **Haiku** | Mechanical work — no reasoning needed |
| `/plan`, `/implement`, `/verify`, `/simplify`, `/commit-push-pr`, `/team-launch` | **Sonnet** | Default — strong reasoning at low cost |
| `/sprint-start` Step 4 (dispatch.sh reconfig) at L4 | **Sonnet** | Multi-constraint reasoning required |
| `/team-launch` Phase 1 (team-lead plan) | **Sonnet (effort: high)** | Most complex planning task |
| (rare) Architectural changes, deep debugging | **Opus** | Manual override — flag in PR description |

The slash commands handle model switching automatically. Don't manually `/model haiku` unless instructed.

---

## 5. The Hooks That Fire Automatically

You'll see these warnings/blocks in your session:

| Hook | When | What happens |
|---|---|---|
| `post-edit-format.sh` | After every Write/Edit | Auto-runs Pint (PHP) or Prettier (TS/JS). Silent on success. |
| `post-edit-tenant-scope-check.sh` | After every PHP Write/Edit | Warns if a new model is missing `BelongsToTenant`. |
| `post-edit-enum-sync-check.sh` | After every JSX/TSX Write/Edit | Warns if ALL-CAPS strings don't match any PHP enum. |
| `pre-commit-guard.sh` | Before every `git commit` Bash call | **HARD BLOCKS** if: missing Morse separator, Co-Authored-By line, or debug artifacts (`dd()`, `console.log`). |
| `session-close-log.sh` | When session ends | Records timestamp to `docs/living/VELOCITY_LOG.md` for sprint timing. |

**If a hook warns you** — read the warning, fix the issue, retry. Don't bypass.

**If `pre-commit-guard.sh` blocks** — DO NOT use `--dangerously-skip-permissions`. The block is correct; fix the commit message.

---

## 6. Skills (loaded on demand by commands)

Skills are NOT auto-loaded. Slash commands reference them when needed. Available:

**Universal:**
- `writing-tests.md` — read by `/plan` and `/implement` when writing tests
- `writing-migrations.md` — read by `/plan` and `/implement` for schema work
- `writing-pr-descriptions.md` — read by `/commit-push-pr`
- `reviewing-code.md` — read by `/verify` Step 5/6/8/9 and `/review-changes`

**AMIR domain-specific:**
- `writing-journal-entries.md` — read by anything in the Accounting / Transactions domains
- `writing-myinvois-integration.md` — read by anything in the EInvoice domain
- `writing-ar-rahnu.md` — read by anything in the ArRahnu domain
- `writing-pdpa-handlers.md` — read by anything in the Pdpa domain

If your task touches one of these domains and the relevant skill isn't auto-referenced, **read it manually before coding**.

---

## 7. Universal Rules (applied without exception)

- **Morse separator** on every commit: `.--. .- .-. ... . -.-.` directly after the subject line.
- **No `Co-Authored-By:`** lines.
- **No debug artifacts** in committed code: `dd()`, `dump()`, `var_dump()`, `ray()`, `console.log()`.
- **`final class`** on every application class (per D32).
- **`use BelongsToTenant`** on every tenant-scoped model (per D28).
- **UUID v7** for primary keys via `HasUuids` trait (per D15-R1).
- **Money cents** in `BIGINT *_cents` columns with `MoneyCast::class` cast (per D16).
- **E.164 phones** in `phone_e164 VARCHAR(20)` columns (per D33).
- **Encrypted IDs** via E3 two-column pattern.
- **No deviation from DECISIONS_LOG.md** without raising a revision (`Dxx-R1`).

---

## 8. Living Docs (in `docs/living/`)

Six files are kept current — updated at every `/sprint-close`. **Read them before planning or troubleshooting**:

- `CODEBASE_MAP.md` — domain boundaries and class index
- `DATA_MODEL.md` — current schema snapshot
- `DOMAIN_GUIDE.md` — state machines, business rules, invariants
- `API_REFERENCE.md` — current API surface
- `VELOCITY_LOG.md` — sprint timing data
- `DEVIATION_LOG.md` — places where the implementation deviated from plan
- `PRINCIPLES.md` — what we learned about this codebase

If `docs/living/DATA_MODEL.md` and Boost's `database_schema` disagree, **`database_schema` wins** and the living doc gets updated in your same PR.

---

## 9. End-of-Session Checklist

Before closing your session:
- [ ] Tests pass (`php artisan test`)
- [ ] Lint clean (`./vendor/bin/pint --test`)
- [ ] Static analysis clean (`./vendor/bin/phpstan analyse`)
- [ ] PR open with the full template (per `writing-pr-descriptions.md`)
- [ ] Task marked done via `/task-done [id]` if PR was merged
- [ ] Living docs updated if domain boundaries / schema / APIs changed
- [ ] Context <500k (or `/compact` and re-orient)

---

## 10. When Things Go Wrong

### Hook blocked your commit
Read the error message. Fix the commit message (add separator, remove Co-Authored-By, remove debug artifacts). Retry.

### A test fails for reasons you don't understand
1. Run `/verify` Step 1 in isolation: `php artisan test --filter=TheSpecificTest`
2. Check Boost's `last_error` and `read_log` for application errors
3. Use `tinker` to verify model state at the point of failure
4. If still stuck, write up what you've tried and ask the founder

### You discover a planning-doc inconsistency
1. Note it
2. **Don't silently fix it in code** — that creates drift
3. Surface it: write up the inconsistency in the PR description's `## Assumptions` section
4. Continue with the most defensible interpretation
5. Founder decides at review time whether to update the planning doc

### You're about to do something that violates a DECISIONS_LOG entry
**STOP**. The decision is locked. Either:
- Apply the decision as written (right answer for 99% of cases)
- Raise a revision (`Dxx-R1`) explaining why the decision needs to change, get founder approval, THEN code

Silent drift from decisions is the #1 source of architecture rot.

### Sprint plan task is ambiguous
Run `/plan` and surface ambiguities. **Don't guess.** Document under `## Assumptions` per Lesson 12-L20.

---

## 11. The One Thing You Must Remember

The framework is opinionated for a reason. The opinions come from real lessons (the 50+ entries in PLANNING_FRAMEWORK.md's lesson log). When something feels overly strict, it's almost certainly because someone shipped a bug that the rule prevents.

Match the existing pattern. If you can't find a pattern to match, surface the gap. Don't invent.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/rules/laravel.md (7,426 bytes)"
mkdir -p ".claude/rules"
cat > '.claude/rules/laravel.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->

# Laravel Best Practices (auto-loaded every session)

This file is read by Claude Code on every session start. It covers general Laravel 12 best practices. Project-specific rules are in `CLAUDE.md` and `.ai/guidelines/*.md` — when this file conflicts with those, **the project files win**.

## Strict Types

Every PHP file in `app/` and `database/` starts with:
```php
<?php
declare(strict_types=1);
```

No exceptions. `declare(strict_types=1)` MUST appear before the namespace.

## Type Hints Everywhere

- Every method parameter has a type hint
- Every method has a return type (including `void` for setters and `: never` for throwing methods)
- Property types declared on every class property (PHP 7.4+)
- Constructor promotion (`final class X { public function __construct(public readonly string $id) {} }`) preferred over manual property + assignment

## Eloquent

### Query Building
- Use Eloquent for normal CRUD; drop to query builder (`DB::table(...)`) only for complex aggregations.
- Always specify the columns you need: `User::select('id', 'name')->get()` — never `select('*')` for large tables.
- Use `pluck()` when you need a single column array — fewer instances created.
- Use `chunk()` or `lazy()` for large iterations, never `->get()->each(...)` on tables that may grow.

### Relationships
- Always eager-load relationships used in views: `User::with('posts')->get()`.
- Never call a relationship method inside a loop — guaranteed N+1.
- For "has any" checks, use `whereHas` instead of `with` + count.

### Mass Assignment
- Always use `$fillable`. Never `$guarded = []`.
- For internal state changes, use `update(['column' => value])` after fillable, OR `forceFill()` for explicitly bypassing the guard.

### Saving
- Use `save()` only when you need observers and events to fire (which is most of the time).
- Use `update()` for simple field updates that the model's observer should track.
- `updateQuietly()` and `saveQuietly()` skip events — use sparingly.

## Routing

- Routes are domain-organised in `routes/web.php` (or split per domain if file grows).
- Always name routes: `->name('members.create')`.
- Always specify HTTP verb explicitly: `Route::post`, not `Route::any`.
- Group middleware: `Route::middleware(['auth', 'verified', 'tenant.context'])->group(...)`.
- Use route model binding for entity routes: `Route::get('/members/{member}', ShowMemberController::class)`.

## Controllers

- Single-action controllers (`__invoke` only) per project Decision D31.
- No business logic in controllers — delegate to Action classes per D30.
- Controllers return either `Inertia::render(...)` (SPA pages) or `JsonResponse` (AJAX). Never raw arrays.

## Validation

- All validation in FormRequest classes. Never inline `$request->validate(...)` in controllers.
- Rules as arrays, not pipe strings: `['required', 'string', 'max:255']`.
- Use Laravel rule classes for complex validation: `Rule::unique(...)->where(...)`, `Rule::enum(MemberStatus::class)`.
- Custom validation rules go in `app/Rules/` as final classes implementing `ValidationRule`.

## Service Container

- Domain ServiceProviders registered in `bootstrap/providers.php`.
- Bind interfaces to implementations in the relevant ServiceProvider's `register()` method.
- Use `app(MyAction::class)->execute(...)` for cross-domain Action calls; never `new MyAction()`.

## Configuration

- All config under `config/*.php`. Never hardcode strings or numbers in source.
- Use `config()` to read; never `env()` outside config files (env() is unreliable when config is cached).
- Run `php artisan config:cache` in production; it makes `env()` calls return null.

## Migrations

- Additive only — never drop/rename in a single migration. See `.claude/skills/writing-migrations.md`.
- Every `up()` has a fully reversing `down()`.
- Use `Schema::create` for new tables, `Schema::table` for additions.
- CHECK constraints via `DB::statement(...)` (Laravel schema builder doesn't support them natively).

## Seeders

- Run only on fresh databases or local dev: `php artisan migrate:fresh --seed`.
- Idempotent — use `firstOrCreate` / `updateOrCreate` so re-running doesn't duplicate.
- Demo seed data goes in `DatabaseSeeder` calling per-domain seeders.

## Queue & Horizon

- Long-running work goes in jobs (`ShouldQueue`).
- Job constructors take IDs, not models (per project-architecture.md §10).
- `tags()` includes `tenant:{id}` for Horizon visibility.
- Set `tries`, `backoff`, `timeout` explicitly per job.
- For work that must run after the parent transaction commits: use `ShouldQueueAfterCommit`.

## Testing

- Use Pest, not raw PHPUnit.
- Real Postgres in tests (RefreshDatabase trait), not SQLite.
- Use factories, never `new Model([...])`.
- Cover: happy path, validation rejection, authorisation rejection, state-conflict rejection, cross-tenant isolation.
- Assert query count via `AssertsQueryPerformance` trait on every integration test.

## Code Style

- 4-space indentation (PSR-12).
- One class per file.
- Class names match filenames.
- Imports alphabetised by Pint (auto-applied via `post-edit-format.sh` hook).

## Forbidden in Production Code

- `dd()`, `dump()`, `var_dump()`, `ray()` — hard-blocked by `pre-commit-guard.sh`.
- `console.log`, `console.warn`, `console.error`, `console.debug` in JSX/TSX — hard-blocked.
- Direct database queries (`DB::raw`, `DB::statement`) outside migrations and explicit aggregations.
- Magic numbers — extract to constants or config.
- Comments explaining what (the code should explain itself); only comment why.
- Commented-out code — git has history.
- Hardcoded strings for user-facing text — use `__()` (per Decision D25, BM/EN parity).

## Performance

- Use `select()` to limit columns on wide tables.
- Use `whereHas` with `whereDoesntHave` to filter relationships at the DB layer.
- Use `withSum`, `withCount` for aggregations instead of looping.
- Index every `where`/`order by`/`group by` column.
- Cache reference data in Redis with explicit TTLs.

## Security

- All write endpoints require auth + policy authorization.
- All inputs validated via FormRequest.
- Tenant scoping enforced via `BelongsToTenant` trait + policy double-check (`$user->currentTenantId() === $model->tenant_id`).
- Sensitive data encrypted via the E3 two-column pattern (per data-conventions.md §7).
- No raw user input in query builders' raw expressions.
- CSRF protection on all stateful endpoints (Sanctum middleware handles this).

## Boost Tools (Laravel-specific MCP)

Use Boost's introspection tools instead of guessing:

| Tool | Purpose |
|---|---|
| `database_schema` | Read the actual current schema |
| `application_info` | Installed packages and versions |
| `tinker` | Verify Eloquent relationships and query results |
| `last_error` | Read the most recent application error |
| `read_log` | Tail the Laravel log |
| `browser_logs` | Read browser console output during a Playwright run |
| `list_routes` | List routes for the current app |
| `get_config` | Read config values without restarting |
| `list_artisan_commands` | Discover available Artisan commands |

Never assume schema, package versions, route names, or config values. Use Boost.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/skills/writing-tests.md (5,944 bytes)"
mkdir -p ".claude/skills"
cat > '.claude/skills/writing-tests.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->

# Skill: Writing Tests

Read this before writing any test. These standards apply on top of `.ai/guidelines/testing-standards.md` (which is auto-loaded by Boost on every session).

## What a Good Test Proves

A good test proves a **business outcome**, not an implementation detail.

- BAD: "The CreateMember action is called."
- GOOD: "Given a valid member request, the member is created with status=active, the MemberRegistered event fires, the audit log entry is recorded, and the tenant's member count increases by 1."

When in doubt, write the test name as the business outcome. If you can't express the outcome in plain English, you're testing the wrong thing.

## Test Structure (Arrange / Act / Assert)

Every test follows this structure strictly:

```
// ARRANGE — set up all prerequisites
// ACT — perform exactly one action
// ASSERT — verify every consequential outcome
```

The `// ARRANGE`, `// ACT`, `// ASSERT` comments are mandatory in feature tests.

## What to Assert (full list)

For every feature test, assert ALL applicable items:
1. HTTP status code — exact code, not just "successful"
2. Response shape — key fields in the response body
3. Database state — what was created, updated, or deleted
4. Events dispatched — every domain event that should have fired
5. Jobs queued — if an action queues background work
6. Notifications sent — if a notification was triggered
7. Audit log entries — for every model change that requires audit
8. What did NOT happen — assert absence for rejection cases

## Performance Assertions (mandatory for every integration test)

Every integration test must assert query count using the `AssertsQueryPerformance` trait:
- `assertQueryCountLessThan(int $max, Closure)` — guards against N+1
- `assertNoDuplicateQueries(Closure)` — guards against repeated identical queries
- `assertNoSlowQueries(Closure, int $thresholdMs = 100)` — guards against unindexed scans

Default budgets:
- Detail (`show`) endpoints: <10 queries
- Dashboard / overview: <15 queries
- List (`index`): <20 queries
- Mutating action: <25 queries
- Bulk operation (≥10 entities): <50 queries

If a test naturally exceeds these, **do not raise the budget without justification**. Optimise: add eager loading, denormalise to a precomputed aggregate, or cache.

## Edge Cases to Always Cover

For every feature, write tests for:
1. Happy path (normal success)
2. Validation rejection (missing/invalid input → 422)
3. Authorisation rejection (wrong role / wrong tenant / wrong owner → 403)
4. State-conflict rejection (e.g., suspending an already-suspended member, posting to a closed period)
5. Boundary values (zero amounts, max string lengths, edge dates, empty lists, BM/unicode characters)
6. **Cross-tenant isolation** — for any tenant-scoped resource, assert that User from Tenant A cannot read/write Tenant B's records

## What NOT to Test

- Private methods — test through public interface only
- Framework behaviour — don't test that auth middleware redirects
- Third-party packages — mock the boundary, don't test the package's internals
- Implementation internals — if a test breaks on a refactor that doesn't change behaviour, it's testing the wrong thing
- Generated fixtures — don't test factory output

## Factory Conventions

- Always use factories. Never `new Model([...])` in tests.
- Use factory states for roles and statuses: `User::factory()->tenantAdmin($tenant)->create()`, `Member::factory()->active()->create()`.
- Tenant-scoped factories expose a `forTenant(Tenant $tenant)` state — always use it when composing multiple models in the same tenant.
- Use `afterCreating` for relations.
- Faker locale: `fake('ms_MY')` for BM-localised content. Configure in `tests/Pest.php`.
- IC numbers, phone numbers, addresses must look like real Malaysian data — use the helpers (`fakeMyIcNumber()`, etc.).

## Cross-Tenant Isolation Test Template

Every tenant-scoped feature gets at least one of these:

```php
it('does not allow tenant A admin to view tenant B members', function () {
    $tenantA = Tenant::factory()->create();
    $tenantB = Tenant::factory()->create();

    $adminA = User::factory()->tenantAdmin($tenantA)->create();
    $memberB = Member::factory()->forTenant($tenantB)->create();

    $this->actingAs($adminA);
    app()->instance('currentTenantId', $tenantA->id);

    $this->get(route('members.show', $memberB))
        ->assertNotFound();
});
```

## Faking & Mocking — When to Fake

| Subject | Approach |
|---|---|
| Domain events | `Event::fake([SpecificEvent::class])` per test |
| Queue dispatch | `Queue::fake()` per test that asserts queue behaviour |
| Notifications | `Notification::fake()` per test that asserts delivery |
| Mail | `Mail::fake()` |
| HTTP outbound (MyInvois etc.) | `Http::fake([...])` with explicit response stubs |
| Storage | `Storage::fake('s3')` |
| Time | `Carbon::setTestNow($timestamp)` for deterministic tests |

**Don't mock:** Eloquent models, Spatie Permission, the audit log, policies. Use real fakes via factories and a real database.

## Test API URL Convention

All API test URLs use the `/api/v1/` prefix. Before opening a PR, run:
```bash
grep -rn '"/api/' tests/ | grep -v '/v1/'
```
Any hit is a blocker.

## Final Checklist Before Committing

- [ ] Test name describes a business outcome, not an implementation
- [ ] Arrange/Act/Assert comments present
- [ ] Query count asserted via `AssertsQueryPerformance`
- [ ] All applicable outcomes asserted (HTTP, DB, events, audit log, etc.)
- [ ] Cross-tenant isolation covered if tenant-scoped feature
- [ ] Validation + authorisation + state-conflict cases covered
- [ ] No mocking of Eloquent / Spatie / audit log
- [ ] Test runs in <500ms
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/skills/writing-migrations.md (7,629 bytes)"
mkdir -p ".claude/skills"
cat > '.claude/skills/writing-migrations.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->

# Skill: Writing Migrations

Read this before writing any migration. Safe migrations are non-negotiable. AMIR's accounting data is high-value — a bad migration can destroy financial records.

## The Golden Rule: Migrations Must Be Additive

Never write a migration that can destroy data or lock the database in production. Every migration must be safe to run against a live database with real data.

## Safe vs. Dangerous Operations

**Safe — do these freely:**
- Adding a new table
- Adding a nullable column
- Adding a column with a default value
- Adding an index
- Adding a CHECK constraint that all existing rows satisfy

**Dangerous — handle carefully (use multi-sprint deprecation):**

| Operation | Risk | Safe Alternative |
|---|---|---|
| Dropping a column | Data loss | Add `deprecated_at` marker; drop in a later sprint after all code deploys are complete |
| Renaming a column | Breaks live code | Add new column → dual-write → drop old in a separate sprint |
| Changing column type | May corrupt data | New column + backfill migration + drop old |
| Adding NOT NULL without default | Fails on existing rows | Always provide a default, or make nullable + backfill + alter |
| Removing enum value | Breaks existing rows | Never. Mark `@deprecated` in the PHP enum, stop assigning, leave the case |
| Adding a CHECK constraint that existing rows violate | Migration fails | Backfill data first, then add constraint |

## AMIR-Specific Schema Rules

These come from `.ai/guidelines/data-conventions.md` (auto-loaded). Quick reference:

### Every table must have:
```php
$table->uuid('id')->primary();              // UUID v7 PK per D15-R1
$table->timestampsTz();                     // created_at + updated_at, always tz-aware
$table->softDeletesTz();                    // if entity has audit-trail lifecycle
```

### Tenant-scoped tables must have:
```php
$table->uuid('tenant_id');                  // per D28
$table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('restrict');
$table->index('tenant_id');                 // or composite (tenant_id, created_at)
```
And the model must `use BelongsToTenant;` (the `post-edit-tenant-scope-check.sh` hook warns if missing).

### Money columns:
```php
$table->bigInteger('total_amount_cents')->default(0);  // BIGINT cents, not decimal — per D16
DB::statement("ALTER TABLE foo ADD CONSTRAINT chk_foo_amount CHECK (total_amount_cents >= 0)");
```
Cast in the model as `'total_amount_cents' => MoneyCast::class`.

### Phone columns (per D33):
```php
$table->string('phone_e164', 20)->nullable();
$table->index('phone_e164');
```

### Encrypted identifier columns (per E3 two-column pattern):
```php
$table->text('ic_no_encrypted');                            // ciphertext
$table->char('ic_no_hash', 64);                             // SHA-256(per-tenant-salt + plaintext)
$table->unique(['tenant_id', 'ic_no_hash']);                // unique within tenant
```

### Status columns:
```php
$table->string('status', 32);
DB::statement("ALTER TABLE foo ADD CONSTRAINT chk_foo_status CHECK (status IN ('ACTIVE', 'SUSPENDED', 'RESIGNED'))");
```
Cast in the model as `'status' => MemberStatus::class`.

### Optimistic locking for financial records:
```php
$table->unsignedInteger('version')->default(1);
```

## Index Conventions

Add an index for every column that appears in a WHERE clause. Add composite indexes for columns always queried together. Common AMIR patterns:

```php
$table->index(['tenant_id', 'created_at']);     // Most "recent activity" queries
$table->index(['tenant_id', 'status']);          // Status-filtered lists
$table->index(['tenant_id', 'member_id']);       // Member-related joins
```

## The `down()` Method

Every `up()` must have a fully reversing `down()`. No exceptions.

```php
public function up(): void
{
    Schema::create('member_subscriptions', function (Blueprint $table) {
        $table->uuid('id')->primary();
        $table->uuid('tenant_id');
        // ... rest
    });
}

public function down(): void
{
    Schema::dropIfExists('member_subscriptions');
}
```

## Before Writing the Migration

1. **Run Boost's `database_schema` tool** — verify the actual current schema. Never assume.
2. **Check `docs/living/DATA_MODEL.md`** — the team's snapshot of what's there.
3. **Check `ARCHITECTURE.md` §2** — the planned schema for this domain.
4. **Search the codebase** for references to any column being modified.
5. **Confirm tenant scoping requirements** — is this tenant-scoped or platform-scoped? Check D28 + the data-conventions §3 exceptions list.

If `docs/living/DATA_MODEL.md` and `database_schema` disagree, **`database_schema` wins** and the living doc gets updated in this same PR.

## Migration Template (canonical)

```php
<?php
declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;

return new class extends Migration {
    public function up(): void
    {
        Schema::create('member_subscriptions', function (Blueprint $table): void {
            // PK + tenant
            $table->uuid('id')->primary();
            $table->uuid('tenant_id');

            // Domain columns
            $table->uuid('member_id');
            $table->bigInteger('amount_cents')->default(0);
            $table->date('billing_period_start');
            $table->date('billing_period_end');
            $table->string('status', 32);

            // Standard timestamps + soft delete + version
            $table->timestampsTz();
            $table->softDeletesTz();
            $table->unsignedInteger('version')->default(1);

            // FKs
            $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('restrict');
            $table->foreign('member_id')->references('id')->on('members')->onDelete('restrict');

            // Indexes
            $table->index(['tenant_id', 'member_id']);
            $table->index(['tenant_id', 'billing_period_start']);
            $table->index(['tenant_id', 'status']);
        });

        // CHECK constraints (raw SQL — Laravel schema builder doesn't support them natively)
        DB::statement("ALTER TABLE member_subscriptions ADD CONSTRAINT chk_ms_amount CHECK (amount_cents >= 0)");
        DB::statement("ALTER TABLE member_subscriptions ADD CONSTRAINT chk_ms_period CHECK (billing_period_end >= billing_period_start)");
        DB::statement("ALTER TABLE member_subscriptions ADD CONSTRAINT chk_ms_status CHECK (status IN ('PENDING', 'PAID', 'OVERDUE', 'CANCELLED'))");
    }

    public function down(): void
    {
        Schema::dropIfExists('member_subscriptions');
    }
};
```

## Final Checklist Before Committing

- [ ] Boost's `database_schema` consulted; current schema confirmed
- [ ] Migration is additive (or follows the safe-deprecation pattern)
- [ ] Every column has correct type per data-conventions.md
- [ ] Tenant-scoped tables include `tenant_id` + FK + index
- [ ] Money columns are `BIGINT` named `*_cents`
- [ ] Encrypted identifiers use the E3 two-column pattern
- [ ] Every WHERE-clause column has an index
- [ ] CHECK constraints added for invariants
- [ ] `down()` reverses `up()` cleanly
- [ ] Model has `use BelongsToTenant;` if tenant-scoped (or platform-scoped comment)
- [ ] `docs/living/DATA_MODEL.md` updated in the same PR
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/skills/writing-pr-descriptions.md (5,263 bytes)"
mkdir -p ".claude/skills"
cat > '.claude/skills/writing-pr-descriptions.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->

# Skill: Writing PR Descriptions

Read this before running `/commit-push-pr`. Every PR must tell a complete story so the reviewer can decide on merge without spelunking.

## PR Description Template

```
## What This Does
[1–2 sentences. The business outcome — what can users or the system do now that they couldn't before.]

## Story / Task
[STORY-ID or TASK-ID] — [Title from .sprint-backlog.json or USER_STORIES.md]

## Acceptance Criteria
- [x] AC1: [exact wording from USER_STORIES.md]
- [x] AC2: ...
- [x] AC3: ...

## Implementation Notes
[2–4 bullet points covering non-obvious decisions. Skip the obvious — only document what a reviewer would wonder about.]

## Assumptions
[Per Lesson 12-L20: anything unclear from docs that you had to assume — enum values, validation rules, edge cases. Write 'None' if everything was clear.]

## Test Plan
- [ ] `php artisan test` — zero new failures
- [ ] `./vendor/bin/pint --test` — clean
- [ ] `./vendor/bin/phpstan analyse` — clean
- [ ] All test URLs use `/api/v1/` prefix
- [ ] No merge conflict markers in changed files
- [ ] Frontend enum values match PHP enums (if applicable)
- [ ] Cross-tenant isolation test included (if tenant-scoped feature)
- [ ] Query count assertion present on integration test

## Decisions Trace
[List every Decision ID this PR is bound by — D-series and E-series. Helps the reviewer cross-check at merge time.]
- D15-R1 — UUID v7 PKs
- D16 — MoneyCast for money columns
- D28 — BelongsToTenant trait
- ...
```

## Commit Message Format

Use the conventional commits format with the mandatory Parsec separator:

```
[type]([scope]): [description]
.--. .- .-. ... . -.-.

[body — optional, only if context > 1 line]
```

**`[type]`** is one of:
- `feat` — new feature or capability
- `fix` — bug fix
- `refactor` — code restructure with no behaviour change
- `test` — adding or fixing tests
- `docs` — documentation only
- `chore` — tooling, dependencies, config
- `build` — build system or external dependencies
- `ci` — CI/CD configuration

**`[scope]`** is the domain folder (e.g. `members`, `accounting`) or `core` for cross-cutting changes.

**The Morse separator** (`.--. .- .-. ... . -.-.`) is mandatory. The pre-commit-guard hook hard-blocks commits without it.

**Forbidden:**
- `Co-Authored-By:` lines (hard-blocked)
- Commits referencing the agent in the message body
- Commits with debug artifacts

## Example PR

```
feat(members): add member suspension flow

## What This Does
Tenant admins can now suspend members. Suspended members cannot perform
any flow that requires an active member status, but their historical
records remain intact for audit.

## Story / Task
MEM-TA-04 — Suspend a member
S06.04 — Member suspension action + UI

## Acceptance Criteria
- [x] AC1: TA can suspend any active member with a reason field (≥10 chars)
- [x] AC2: Suspended member's status changes to 'SUSPENDED' and the audit
      log records the change with from/to/reason
- [x] AC3: Member detail page surfaces suspension status with reason
- [x] AC4: Cross-tenant: TA from Tenant A cannot suspend Tenant B's members

## Implementation Notes
- Suspension is reversible via the existing /reinstate flow (S06.05).
- The suspension reason is stored on a new `member_status_history` table
  rather than on the member itself — this preserves the full status timeline
  for audit and is symmetric with future "reinstated_reason" entries.
- Did NOT add WhatsApp notification on suspension (out of S06 scope per
  SPRINT_PLAN.md; tracked as S08.07).

## Assumptions
- "Reason field ≥10 chars" — interpreted from AC1 wording. No explicit
  upper bound; used max:500 to match other narrative fields.
- Suspended → Suspended (idempotent re-suspend) treated as no-op rather
  than error per ACCOUNTING_INVARIANTS.md state machine table row 47.

## Test Plan
- [x] php artisan test — 12 new tests, all green
- [x] ./vendor/bin/pint --test — clean
- [x] ./vendor/bin/phpstan analyse — clean
- [x] /api/v1/ prefix verified
- [x] No merge conflict markers
- [x] Cross-tenant isolation test included
- [x] Query count assertion (≤25 for the suspend endpoint)

## Decisions Trace
- D15-R1 (UUID v7), D16 (MoneyCast not relevant — no money columns)
- D28 (BelongsToTenant on `member_status_history`)
- D30 (Action class: SuspendMember)
- D31 (single-action controller: SuspendMemberController)
- D32 (final class throughout)
- D19 (audit log via spatie/laravel-activitylog with custom event 'member.suspended')
```

## Final Checklist Before Opening the PR

- [ ] Title follows `[type]([scope]): [description]` format
- [ ] Description includes all 7 sections (What/Story/ACs/Notes/Assumptions/Tests/Decisions)
- [ ] Every AC is marked complete and traces to a passing test
- [ ] Assumptions section is filled (or explicitly says "None")
- [ ] Decisions Trace lists every Decision ID this PR is bound by
- [ ] Commit message has the Morse separator
- [ ] No `Co-Authored-By:` lines anywhere
- [ ] Branch name follows `feature/s{N}-{NN}-{slug}` convention
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/skills/reviewing-code.md (9,886 bytes)"
mkdir -p ".claude/skills"
cat > '.claude/skills/reviewing-code.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->

# Skill: Reviewing Code

Read this before running `/review-changes` or before performing the verification step at the end of `/verify`. This is the master checklist applied to every PR before merge.

## Approach

Review as a senior staff engineer reviewing a PR from a junior. Be specific. Cite file + line numbers. If something is wrong, say what the right answer is — don't just flag it.

Group findings by severity:
- **BLOCKER** — must fix before merge (security, data integrity, broken tests, architecture violations)
- **IMPORTANT** — should fix before merge (performance, missing edge cases, design system violations)
- **SUGGESTION** — nice to have (clarity improvements, naming, comments)

---

## Section 1: Architecture Compliance

For every changed file:

- [ ] **Action class pattern (per D30)** — business operations live in `app/Domain/[Domain]/Actions/[VerbNoun].php`, single `execute` method, transactional, dispatches events. NOT in services, NOT in controllers, NOT in models.
- [ ] **Single-action controllers (per D31)** — every controller has `__invoke` only. No resource controllers (`index`/`show`/`store`/...).
- [ ] **Final class (per D32)** — every class is `final`. Pint enforces this; if it slipped through, flag.
- [ ] **Domain folder placement** — every file lives in the right `app/Domain/[Name]/` subfolder per project-architecture.md §2. Cross-domain calls via Actions or Events, never direct model coupling.
- [ ] **Events for state changes** — Actions that change state dispatch domain events (past-tense names, public readonly properties, ID-only payloads). Listeners delegate to other Actions, never inline logic.
- [ ] **No business logic in models** — models represent state, not behaviour. No `$member->suspend()`. The Action does it.

## Section 2: Data Integrity

- [ ] **UUID v7 PKs (per D15-R1)** — every entity table has `uuid('id')->primary()` and the model `use HasUuids` with `newUniqueId()` returning `Str::uuid7()`.
- [ ] **Tenant scoping (per D28)** — every tenant-scoped model `use BelongsToTenant`. Hook `post-edit-tenant-scope-check.sh` warns at write time but verify at review.
- [ ] **Money as cents (per D16)** — column `BIGINT` named `*_cents`, cast as `MoneyCast::class`. Never `decimal`, never `float`. Never `'integer'` cast for money (loses Money value object benefits).
- [ ] **Phone as E.164 (per D33)** — column `phone_e164 VARCHAR(20)`, validated with `/^\+60\d{9,10}$/`. Never plain `phone` column.
- [ ] **Encrypted IDs (per E3)** — IC, bank account, TIN use the two-column pattern: `*_encrypted TEXT` (cast `'encrypted'`) + `*_hash CHAR(64)` (SHA-256 of per-tenant-salt + plaintext). Never single-column.
- [ ] **Status as backed enum** — column `VARCHAR(32)`, cast as the enum class. Never raw `where('status', 'ACTIVE')` — always `where('status', MemberStatus::Active)`.
- [ ] **Timestamps (per D17)** — `timestampsTz()` on every table. UTC storage, MYT display. Never naive `timestamp()`.
- [ ] **CHECK constraints** — invariants enforced at the DB layer for non-negative amounts, balanced totals, valid enum values.
- [ ] **Indexes** — every WHERE / JOIN / ORDER BY column has an index. Composite indexes for columns always queried together.
- [ ] **Migrations are additive** — no destructive operations without the multi-sprint deprecation pattern from writing-migrations.md.

## Section 3: Security

- [ ] **No secrets hardcoded** — credentials, API keys, tokens come from `config/*.php` or `.env`, never from the source.
- [ ] **Auth on every write endpoint** — every controller `__invoke` either runs through a middleware that requires auth or has an explicit `$request->user()` check.
- [ ] **Policy authorization** — every mutating Action's controller calls `->can(...)` via the FormRequest's `authorize()` method.
- [ ] **Tenant scoping defence in depth** — policies verify `$user->currentTenantId() === $model->tenant_id` even though the global scope already filters. Two walls.
- [ ] **All inputs validated** — FormRequest's `rules()` cover every accepted field with type, length, format, and uniqueness constraints.
- [ ] **No sensitive data in URLs / logs / API responses** — IC numbers, NRIC, MFA secrets, encrypted ciphertext never appear in route paths, log messages, or unscrubbed Resource fields.
- [ ] **PII scrubbed in Sentry** — `beforeSend` removes IC numbers and emails from breadcrumbs.

## Section 4: Performance

- [ ] **Query count assertion present** — every integration test uses `assertQueryCountLessThan` from the `AssertsQueryPerformance` trait.
- [ ] **No N+1 queries** — `assertNoDuplicateQueries` covers the common case; eager-load relationships in the controller (`->with(...)`).
- [ ] **No queries inside loops** — batch operations use `whereIn`, `bulkInsert`, etc.
- [ ] **Multi-step writes use transactions** — Actions wrap mutations in `DB::transaction(...)`.
- [ ] **Dashboard widgets read from `analytics_aggregates_daily`** — not raw `analytics_events`.
- [ ] **No unbounded queries** — list endpoints paginate; bulk operations have an upper bound.
- [ ] **Slow query check** — `assertNoSlowQueries(thresholdMs: 100)` on integration tests for endpoints that hit large tables.

## Section 5: Code Quality — LLM Bloat Detection

This is where AI-generated code typically fails. Apply ruthlessly.

- [ ] **No unused imports** — every `use` statement is referenced in the file.
- [ ] **No unused variables / parameters** — every assigned variable is read; every method parameter is used.
- [ ] **No functions/classes created but never called** — every new class has at least one call site (or test).
- [ ] **No single-use abstractions** — helpers, wrappers, base classes with one child should be inlined unless they significantly improve readability.
- [ ] **No defensive null checks for non-nullable types** — if the type signature says `string`, don't `if ($x === null)`.
- [ ] **No catches for unthrown exceptions** — only catch what can actually be thrown.
- [ ] **No commented-out code** — git has history; delete it.
- [ ] **No overly complex conditionals** — nested ternaries, 4+ boolean conditions: simplify or extract.
- [ ] **No variables assigned then immediately returned** — `return $expression` directly.
- [ ] **No files scaffolded but never wired** — every new model has a migration; every new controller has a route; every new page has a navigation link.
- [ ] **Every new line traces to a specific AC** — speculative features beyond the ACs are removed.

## Section 6: Test Quality

- [ ] **Every AC has a passing test** — re-read the task in `.sprint-backlog.json`; cross-reference each AC.
- [ ] **Tests prove business outcomes, not implementation** — test names describe what the user can now do.
- [ ] **All applicable assertions** — HTTP status, response shape, DB state, events, jobs, audit log, what didn't happen.
- [ ] **Edge cases covered** — validation rejection, authorisation rejection, state-conflict rejection, boundary values.
- [ ] **Cross-tenant isolation test** — for any tenant-scoped feature, a test that User A cannot affect User B's data.
- [ ] **No mocking of Eloquent / Spatie / audit log** — use real fakes via factories.
- [ ] **Test URLs use `/api/v1/` prefix** — `grep -rn '"/api/' tests/ | grep -v '/v1/'` returns nothing.

## Section 7: Frontend (skip if backend-only PR)

Apply the design system compliance checklist from CLAUDE.md §8:

- [ ] **All UI uses design system components** — `<Button>`, `<DataTable>`, `<FormField>`, `<StatusBadge>`. No raw HTML when a component exists.
- [ ] **Zero hardcoded colour, spacing, typography, radius, or shadow values** — all via design tokens (`var(--color-primary)`, `text-primary`, `p-4`, etc.).
- [ ] **Status displays use `<StatusBadge>` with enum value** — no manual badge styling.
- [ ] **Page uses a standard layout (L0-L5)** — no invented layouts.
- [ ] **No ad-hoc components duplicating existing design system components** — grep for similar patterns before adding new ones.
- [ ] **Every user-facing string uses `__()`** — exists in both BM and EN per CONTENT_COPY.md.
- [ ] **No `<form>` tags with `type=submit` in multi-section forms** — use `<div>` + `<button type="button">` with explicit `onClick` (per Lesson 12-L19).
- [ ] **Frontend enum values match PHP enums exactly** — hook `post-edit-enum-sync-check.sh` warns at write time but verify at review.

## Section 8: Conventions & Hygiene

- [ ] **Parsec separator on every commit** — `.--. .- .-. ... . -.-.` line directly after subject.
- [ ] **No Co-Authored-By lines** — hard-blocked by hook but verify.
- [ ] **No debug artifacts** — `dd()`, `dump()`, `var_dump()`, `console.log` — hard-blocked by hook but verify.
- [ ] **No merge conflict markers** — `grep -rn '<<<<<<\|>>>>>>\|=======' --include='*.php' --include='*.tsx' --include='*.jsx'` returns nothing (per Lesson 12-L18).
- [ ] **Branch name follows convention** — `feature/s{N}-{NN}-{slug}`.
- [ ] **DECISIONS_LOG.md respected** — every decision the code touches is honoured. If a decision needs to change, raise a revision (`Dxx-R1`) — don't silently drift.
- [ ] **Living docs updated** — `docs/living/` files reflect the new state if domain boundaries / schema / APIs changed.

---

## Verdict

If ALL sections pass: issue **PASS** with a one-line summary. The PR is ready to merge.

If any item fails:
- BLOCKER → return to dev for fixes; do NOT merge
- IMPORTANT → flag for fix before merge; founder decides on case-by-case
- SUGGESTION → comment on PR for the dev to address (now or later); does not block merge

Issue findings as: `[severity] [file:line] [what's wrong]. Correct approach: [...]`.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/skills/writing-journal-entries.md (10,161 bytes)"
mkdir -p ".claude/skills"
cat > '.claude/skills/writing-journal-entries.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->

# Skill: Writing Journal Entries (AMIR Accounting)

Read this before touching anything that creates, modifies, or queries journal entries. Accounting integrity is the most fundamental invariant in AMIR — get this wrong and every report (Trial Balance, P&L, Balance Sheet, Penyata Kewangan) is wrong.

## The Three Inviolable Rules

1. **Sum of debits = sum of credits.** Always. For every journal entry. Enforced at the DB layer with a CHECK constraint. Any code path that produces an unbalanced entry is a bug.
2. **Posted entries are immutable.** Once `state IN ('posted', 'reversed')`, the entry cannot be edited. Corrections happen via reversal (a new entry that mirrors the original).
3. **Closed period = no posting.** If the entry's period is `'closed'`, no entry can be created or modified within it. Reopening a closed period is a separate Action gated by `tenant_admin` role.

## Schema Reminders

From `ARCHITECTURE.md` §2 — Module D (Accounting):

```sql
journal_entries (
    id UUID PRIMARY KEY,
    tenant_id UUID NOT NULL REFERENCES tenants(id),
    period_id UUID NOT NULL REFERENCES accounting_periods(id),
    entry_no VARCHAR(50) NOT NULL,
    entry_date DATE NOT NULL,
    narration TEXT,
    state VARCHAR(32) NOT NULL,  -- enum JournalEntryState
    total_debits_cents BIGINT NOT NULL DEFAULT 0,
    total_credits_cents BIGINT NOT NULL DEFAULT 0,
    posted_at TIMESTAMPTZ NULL,
    posted_by_user_id UUID NULL,
    reversed_by_journal_entry_id UUID NULL,  -- self-FK on reversal
    version INT NOT NULL DEFAULT 1,           -- optimistic locking
    -- timestamps + soft delete
    
    CONSTRAINT chk_je_balanced
        CHECK (state NOT IN ('approved','posted') OR total_debits_cents = total_credits_cents)
);

journal_lines (
    id UUID PRIMARY KEY,
    journal_entry_id UUID NOT NULL REFERENCES journal_entries(id) ON DELETE CASCADE,
    tenant_id UUID NOT NULL REFERENCES tenants(id),
    coa_account_id UUID NOT NULL REFERENCES coa_accounts(id),
    debit_cents BIGINT NOT NULL DEFAULT 0,
    credit_cents BIGINT NOT NULL DEFAULT 0,
    line_no INT NOT NULL,
    narration TEXT,
    
    CONSTRAINT chk_jl_xor CHECK (
        (debit_cents > 0 AND credit_cents = 0)
        OR (debit_cents = 0 AND credit_cents > 0)
    ),
    CONSTRAINT chk_jl_amounts_nonneg CHECK (debit_cents >= 0 AND credit_cents >= 0)
);
```

Each line is **debit XOR credit, never both** — enforced at the DB layer. Splitting a line across debit and credit is a bug.

## State Machine

```
DRAFT → PENDING_APPROVAL → APPROVED → POSTED
                                          │
                                          └─→ REVERSED (creates a new mirror entry)
```

Allowed transitions:

| From | To | Action class | Auth required |
|---|---|---|---|
| (new) | DRAFT | `CreateJournalEntryDraft` | tenant_user |
| DRAFT | DRAFT | `UpdateJournalEntryDraft` | tenant_user (creator) |
| DRAFT | PENDING_APPROVAL | `SubmitJournalEntryForApproval` | tenant_user |
| PENDING_APPROVAL | APPROVED | `ApproveJournalEntry` | tenant_admin |
| PENDING_APPROVAL | DRAFT | `RejectJournalEntry` | tenant_admin |
| APPROVED | POSTED | `PostJournalEntry` | tenant_admin |
| POSTED | REVERSED | `ReverseJournalEntry` | tenant_admin |

**Drafts can be edited freely. Approved entries are read-only until posted. Posted entries are read-only forever** — corrections happen via reversal, which is itself a new journal entry that posts the inverse.

## Approval Threshold

Per Decision C16, journals above the tenant's configured threshold (`tenant_settings.journal_approval_threshold_cents`, default RM 5,000 = 500_000 cents) require explicit `tenant_admin` approval before posting. Below the threshold, `tenant_user` can post directly (approval workflow skipped).

The threshold check happens in `PostJournalEntry::execute`:

```php
if ($entry->total_debits_cents >= $tenant->settings->journal_approval_threshold_cents
    && $entry->state !== JournalEntryState::Approved) {
    throw new DomainException('Journal entry requires admin approval before posting.');
}
```

## Common Pitfalls (real bugs from past projects)

### Pitfall 1: Updating a posted entry's lines

```php
// ❌ NEVER
$entry = JournalEntry::find($id);
$entry->state = JournalEntryState::Posted;
$entry->lines()->update(['debit_cents' => 0]);  // catastrophic — silently wrecks the period
```

The model's `saving` observer should reject any change to a `POSTED` entry. Verify this is wired.

### Pitfall 2: Float arithmetic anywhere

```php
// ❌ NEVER
$total = 1.06 * $amount_cents / 100;  // float → cents loses precision

// ✅ ALWAYS
$total_cents = (int) round($amount_cents * 1.06);
```

Even one float operation in the chain corrupts the cents.

### Pitfall 3: Forgetting to recalculate totals

When updating draft lines, the entry's `total_debits_cents` and `total_credits_cents` columns must be recalculated and saved on the parent `journal_entries` row. The CHECK constraint will reject the entry if the totals don't match — but only at state transition time. A draft with incorrect totals will silently fail when promoted to APPROVED.

Use an Eloquent observer:

```php
JournalLine::saved(function (JournalLine $line) {
    $entry = $line->entry;
    $entry->total_debits_cents = $entry->lines->sum('debit_cents');
    $entry->total_credits_cents = $entry->lines->sum('credit_cents');
    $entry->save();
});
```

### Pitfall 4: Reversal entries that don't reverse

A reversal entry is a NEW entry with:
- Each line's `debit_cents` and `credit_cents` swapped
- `narration` prefixed with "REVERSAL OF [original entry_no]: ..."
- `reversed_by_journal_entry_id` on the original points to the reversal

```php
public function execute(string $tenantId, string $originalEntryId, string $reason): JournalEntry
{
    return DB::transaction(function () use ($tenantId, $originalEntryId, $reason): JournalEntry {
        $original = JournalEntry::lockForUpdate()->findOrFail($originalEntryId);

        if ($original->state !== JournalEntryState::Posted) {
            throw new DomainException('Only posted entries can be reversed.');
        }
        if ($original->reversed_by_journal_entry_id) {
            throw new DomainException('Entry already reversed.');
        }
        // Verify period is still open
        if ($original->period->state !== AccountingPeriodState::Open) {
            throw new DomainException('Cannot reverse — original entry\'s period is closed.');
        }

        $reversal = JournalEntry::create([/* mirror with swapped d/c */]);
        // ... create reversal lines with debit_cents and credit_cents swapped ...

        $original->update([
            'state' => JournalEntryState::Reversed,
            'reversed_by_journal_entry_id' => $reversal->id,
        ]);

        // Post the reversal (it's an admin-driven reversal, so skip approval)
        app(PostJournalEntry::class)->execute(
            tenantId: $tenantId,
            entryId: $reversal->id,
            skipApproval: true,
        );

        event(new JournalEntryReversed(
            originalEntryId: $original->id,
            reversalEntryId: $reversal->id,
            reason: $reason,
            tenantId: $tenantId,
        ));

        return $reversal->fresh();
    });
}
```

## Trial Balance Query Pattern

For Trial Balance / P&L / Balance Sheet generation, NEVER iterate over journal_entries in PHP:

```php
// ❌ slow + N+1 magnet
$balance = 0;
foreach ($lines as $line) { $balance += $line->debit_cents - $line->credit_cents; }

// ✅ fast — one query, DB aggregates
$balance = JournalLine::query()
    ->where('coa_account_id', $accountId)
    ->whereHas('entry', fn ($q) => $q->where('state', JournalEntryState::Posted))
    ->whereBetween('entry_date', [$periodStart, $periodEnd])
    ->selectRaw('COALESCE(SUM(debit_cents - credit_cents), 0) as balance_cents')
    ->value('balance_cents');
```

## Audit Log

Every state transition emits a custom-event activity log entry:

```php
activity()
    ->causedBy(auth()->user())
    ->performedOn($entry)
    ->withProperties([
        'from' => $oldState->value,
        'to' => $newState->value,
        'total_debits_cents' => $entry->total_debits_cents,
        'total_credits_cents' => $entry->total_credits_cents,
    ])
    ->log("journal_entry.{$transition}");
```

Transitions to log: `submitted`, `approved`, `rejected`, `posted`, `reversed`. Don't log `draft_updated` (too noisy).

## Test Coverage Required

For any journal-entry-touching feature, tests must include:

1. ✅ Happy path — create draft → submit → approve → post → verify GL balances
2. ✅ Unbalanced rejection — DB-level CHECK rejects unbalanced posting attempt
3. ✅ Closed period rejection — cannot post / reverse / edit in a closed period
4. ✅ Threshold enforcement — over-threshold entries require approval; under-threshold can post directly
5. ✅ Reversal flow — original goes to REVERSED, reversal entry posts with swapped d/c
6. ✅ Cross-tenant isolation — TA from Tenant A cannot post entries in Tenant B's books
7. ✅ Concurrency — two simultaneous posts on the same draft trigger optimistic-locking conflict (version mismatch)
8. ✅ Trial Balance — sum of debits = sum of credits across the entire period (DB-level invariant test)

## Verification Checklist

- [ ] DB CHECK constraint `chk_je_balanced` present on `journal_entries`
- [ ] DB CHECK constraint `chk_jl_xor` present on `journal_lines`
- [ ] State machine enforced — invalid transitions throw `DomainException`
- [ ] Posted entries reject all writes via the `saving` observer
- [ ] Approval threshold checked before posting (per Decision C16)
- [ ] Audit log entries written for every state transition
- [ ] Reversal creates a new entry with swapped d/c, links via `reversed_by_journal_entry_id`
- [ ] Trial Balance test asserts sum-of-debits = sum-of-credits across full period
- [ ] Optimistic locking via `version` column for concurrent edits
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/skills/writing-myinvois-integration.md (10,161 bytes)"
mkdir -p ".claude/skills"
cat > '.claude/skills/writing-myinvois-integration.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->

# Skill: Writing MyInvois Integration

Read this before touching any e-Invoice code (Module K — `app/Domain/EInvoice/`). MyInvois compliance is a tender deliverable and a regulatory requirement; bugs here block invoicing for every tenant.

## Source of Truth

- **MyInvois Specific Guideline v4.6** (effective 5 January 2026) — the current LHDN spec. https://sdk.myinvois.hasil.gov.my
- **AMIR ARCHITECTURE.md §2 Module K** — the AMIR-side schema and integration adapter
- **DECISIONS_LOG.md** — search "MyInvois", "RM10K", "consolidated invoice", "72h cancellation"

When the AMIR docs and the LHDN spec disagree, **the LHDN spec wins** and the AMIR docs need updating in this same PR.

## Critical Rules (these have legal/regulatory consequences)

### Rule 1: The RM 10,000 individual-invoice rule

Per LHDN policy and Decision (search DECISIONS_LOG for "RM10K"): any single transaction with `total_amount_cents >= 1_000_000` (i.e. RM 10,000 or more) **must be issued as an individual e-invoice**. It cannot be included in a consolidated batch invoice.

```php
// In ConsolidatedInvoiceBuilder::execute()
foreach ($transactions as $txn) {
    if ($txn->total_amount_cents >= 1_000_000) {
        throw new DomainException(
            "Transaction {$txn->id} exceeds RM 10,000 — must be issued as individual e-invoice, "
            . "cannot be consolidated."
        );
    }
}
```

UI: the consolidated-invoice form must surface this rejection inline; the consolidation pre-flight must list any excluded transactions.

### Rule 2: 72-hour cancellation window

Per LHDN policy: a SUBMITTED + VALIDATED e-invoice can be cancelled within 72 hours of validation, after which it is immutable. This is **distinct from PDPA s.12B** (which is a 72h breach notification deadline — same number, different domain).

```php
public function execute(string $invoiceId, string $reason): void
{
    $invoice = EInvoice::lockForUpdate()->findOrFail($invoiceId);

    if ($invoice->einv_status !== EInvoiceStatus::Validated) {
        throw new DomainException('Only validated invoices can be cancelled.');
    }
    if ($invoice->validated_at->addHours(72)->isPast()) {
        throw new DomainException(
            'Cancellation window has closed. Must issue a credit note instead.'
        );
    }
    // ... call MyInvois cancellation API
}
```

If the 72h window has passed, the user must issue a credit note, not cancel.

### Rule 3: Schema version is recorded per tenant

Tenant settings carry the schema version they're submitting against:

```sql
tenant_einvoice_settings (
    tenant_id UUID PRIMARY KEY,
    schema_version VARCHAR(10) NOT NULL DEFAULT '4.6',  -- as of Jan 2026
    -- other settings
);
```

When LHDN releases v4.7 (or later), the migration path is: deploy code that supports both versions in parallel, opt-in tenants by updating their setting, sunset v4.6 once all tenants have migrated. Never assume "current spec" without reading the tenant's `schema_version`.

## State Machine

```
NOT_SUBMITTED → SUBMITTED → VALIDATED → (CANCELLED if within 72h)
                    │
                    └─→ REJECTED (LHDN rejected; needs correction + resubmit)
```

Allowed transitions:

| From | To | Trigger | Action class |
|---|---|---|---|
| NOT_SUBMITTED | SUBMITTED | User submit; LHDN ack received | `SubmitInvoiceToMyInvois` |
| SUBMITTED | VALIDATED | LHDN validation success | (system — `MyInvoisStatusPollingJob`) |
| SUBMITTED | REJECTED | LHDN validation failure | (system) |
| REJECTED | NOT_SUBMITTED | User corrects + resubmits | `ResubmitRejectedInvoice` |
| VALIDATED | CANCELLED | User cancels within 72h | `CancelValidatedInvoice` |

## Field Validation (per Specific Guideline v4.6)

### Mandatory fields (must be present, must validate)

| Field | Source | Validation |
|---|---|---|
| `seller_tin` | Tenant TIN | Validates against LHDN TIN registry; format `C12345678901` for company, `IG12345678901` for individual |
| `seller_brn` | Tenant SSM no. | Format-validated (numeric or alphanumeric depending on registration type) |
| `seller_msic_code` | Tenant settings | 5-digit MSIC 2008 code |
| `buyer_tin` | Party TIN | If party is a registered taxpayer; otherwise use `EI00000000010` (general public TIN per spec) |
| `buyer_name` | Party name | UTF-8, ≤300 chars |
| `buyer_address` | Party address | Structured: addressLine, city, state, postalZone, country |
| `buyer_contact_number` | Party `phone_e164` | Per Decision D33 — already E.164. Strip `+` for MyInvois (which expects unformatted digits with country code) |
| `total_excluding_tax` | Computed | Cents → ringgit with 2 decimals |
| `total_tax_amount` | Computed (SST) | Currently 6% for taxable supplies; rate per `tax_rates` table |
| `total_payable_amount` | Computed | `total_excluding_tax + total_tax_amount` |
| `currency_code` | `MYR` | ISO 4217 |
| `issue_date` | Today (ISO 8601) | `YYYY-MM-DD` |

### Anti-patterns (silent breakage)

```php
// ❌ Don't pass +60123456789 — strip the +
'buyerContactNumber' => $party->phone_e164,

// ✅
'buyerContactNumber' => ltrim($party->phone_e164, '+'),
```

```php
// ❌ Don't compute tax via float
$tax = 0.06 * $amount_cents / 100;

// ✅
$tax_cents = (int) round($amount_cents * 0.06);
```

## Integration Architecture (per project-architecture.md §10)

```
app/Domain/EInvoice/
  Actions/
    SubmitInvoiceToMyInvois.php       — orchestrates: build payload → call API → record submission
    CancelValidatedInvoice.php        — 72h check + LHDN cancellation API
    ResubmitRejectedInvoice.php
    BuildInvoicePayload.php           — pure: invoice → MyInvois schema array
  Contracts/
    MyInvoisClient.php                — interface (production HTTP client + fake)
  Services/
    HttpMyInvoisClient.php            — production implementation
  Jobs/
    SubmitInvoiceToMyInvoisJob.php    — queued submission with retries
    PollMyInvoisStatusJob.php         — polls SUBMITTED invoices for status
  Models/
    EInvoice.php
    EInvoiceSubmission.php            — every submission attempt logged
    LhdnTinRegistry.php               — platform-scoped: cache of TINs we've validated
  Enums/
    EInvoiceStatus.php
```

## The Fake Client (test discipline)

```php
// In test setup
$this->app->bind(MyInvoisClient::class, FakeMyInvoisClient::class);
```

The fake returns deterministic responses. For specific failure scenarios, introduce variants:
- `FakeMyInvoisClient` — happy path (SUBMITTED → VALIDATED in 1s)
- `RejectingMyInvoisClient` — returns REJECTED with mock validation errors
- `SlowMyInvoisClient` — returns timeout
- `RateLimitedMyInvoisClient` — returns 429 with retry-after header

Don't pollute the canonical fake with conditional return logic.

## Audit Log

Every API call (submit / cancel / status poll) writes an `EInvoiceSubmission` row with:
- Request payload (PII-scrubbed via `PIIGuard::scrub` — IC numbers redacted)
- Response payload (full)
- HTTP status + duration_ms
- `tenant_id`, `invoice_id`, `submission_attempt_no`

The `EInvoiceSubmission` table is the audit trail when LHDN disputes happen. Never delete from this table.

## Common Pitfalls

### Pitfall 1: Including RM10K+ transactions in consolidated invoices

The pre-flight check in `BuildConsolidatedInvoice::execute` must filter these out and warn the user. Tests must cover this exact case (consolidated batch with one RM10K+ transaction → that transaction excluded + warning logged).

### Pitfall 2: Cancelling a SUBMITTED-but-not-yet-VALIDATED invoice

Per spec, only VALIDATED invoices can be cancelled. A SUBMITTED invoice that hasn't yet validated must wait — the cancellation API will reject the call. The 72h window starts at validation time, not submission time.

### Pitfall 3: Re-using an old UUID for resubmission

When a REJECTED invoice is corrected and resubmitted, it must use a **new UUID** (LHDN's deduplication uses the UUID). Re-submitting with the old UUID returns "duplicate" error. The Action `ResubmitRejectedInvoice` generates a fresh UUID v7 and links the new submission to the old via `corrects_submission_id`.

### Pitfall 4: Time zone confusion

LHDN expects `issue_date` in Malaysian local time (Asia/Kuala_Lumpur). AMIR stores everything in UTC per Decision D17. When building the payload, convert:

```php
'issueDate' => $invoice->issue_date->setTimezone('Asia/Kuala_Lumpur')->toDateString(),
```

## Test Coverage Required

1. ✅ Happy path: submit → poll → VALIDATED
2. ✅ Rejection path: submit → REJECTED → correct → resubmit (new UUID) → VALIDATED
3. ✅ RM10K rule: consolidated invoice with RM10K+ transaction → excluded + warning
4. ✅ 72h cancellation window: cancel within 72h → ok; cancel at 72h+1min → fails
5. ✅ Schema version: tenant on v4.5 still works after deploy that adds v4.6 support (parallel)
6. ✅ Idempotency: re-submission of the same UUID returns the existing submission, not a duplicate
7. ✅ Cross-tenant: tenant A cannot submit on behalf of tenant B
8. ✅ TIN validation: invalid TIN format rejected pre-submit
9. ✅ Phone format: `phone_e164` correctly stripped of `+` for MyInvois payload
10. ✅ Audit log: every submission attempt creates an `EInvoiceSubmission` row with PII-scrubbed payload

## Verification Checklist

- [ ] RM10K rule enforced in consolidated-invoice builder + tested
- [ ] 72h cancellation window enforced + tested
- [ ] State machine transitions all explicit + tested for invalid transitions
- [ ] Tenant `schema_version` consulted (don't hardcode v4.6)
- [ ] Phone format: `+` stripped before submission
- [ ] Money: integer cents → ringgit with `number_format($cents / 100, 2)` only at payload boundary
- [ ] Time zone: `issue_date` converted to Asia/Kuala_Lumpur for the payload
- [ ] PII scrubbed in audit log (IC numbers redacted via `PIIGuard`)
- [ ] Fake client bound in tests; production HTTP client only used in production binding
- [ ] Resubmission generates a new UUID v7 per request
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/skills/writing-ar-rahnu.md (11,926 bytes)"
mkdir -p ".claude/skills"
cat > '.claude/skills/writing-ar-rahnu.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised reproduction or use outside of Parsec Sdn. Bhd.-authorised projects is prohibited.
-->

# Skill: Writing Ar-Rahnu Code (Islamic Pawn)

Read this before touching any Ar-Rahnu code (Module J — `app/Domain/ArRahnu/`). Ar-Rahnu is a Shariah-compliant pawn product offered by koperasi. Implementation errors here are not just bugs — they're Shariah violations that can disqualify the koperasi from offering the product.

## The Source of Truth

- **BNM-MPS Resolution 2019** — Bank Negara Malaysia Shariah Advisory Council ruling that mandates Tawarruq structure for Ar-Rahnu. Earlier qard-wadi'ah-ujrah structures are non-compliant.
- **AMIR ARCHITECTURE.md §2 Module J** — schema and operations
- **DECISIONS_LOG.md** — search "Ar-Rahnu", "Tawarruq", "qard", "BNM-MPS"
- **BUSINESS_FLOWS.md J-series** — full operational flows

## The One Rule You Must Not Break

**Ar-Rahnu in AMIR is structured as Tawarruq (cost-plus-profit), never as qard-wadi'ah-ujrah (loan + safekeeping fee + service charge).**

The qard-wadi'ah-ujrah structure was the historical norm in Malaysian koperasi but was ruled non-compliant by BNM-MPS in 2019 because the "ujrah" (service charge) was being calculated as a percentage of the loan amount — making it economically equivalent to riba (interest).

**Tawarruq replaces this with:**
1. Koperasi buys a commodity (typically the gold itself, or a separate underlying commodity in some implementations) at cost.
2. Koperasi sells the commodity to the customer at cost + a fixed profit margin (the "markup").
3. Customer accepts deferred payment terms (the financing duration).
4. Customer either retains the gold or asks koperasi to sell it on their behalf.

The **fixed profit margin** is the Shariah-correct equivalent of "interest" — it's set at contract signing, doesn't compound, and doesn't depend on the financing duration in a way that would mimic interest.

## Schema Reminders

```sql
ar_rahnu_tickets (
    id UUID PRIMARY KEY,
    tenant_id UUID NOT NULL,
    member_id UUID NOT NULL REFERENCES members(id),
    ticket_no VARCHAR(50) NOT NULL,                      -- e.g. AR-2026-00001
    
    -- Gold valuation (snapshot at ticket issuance)
    weight_milligrams INT NOT NULL,                      -- integer milligrams, never grams as decimal
    gold_purity VARCHAR(8) NOT NULL,                     -- '916', '999', '750' (karat × 41.667)
    valuation_cents_per_gram BIGINT NOT NULL,            -- snapshot of market rate at ticket time
    appraised_value_cents BIGINT NOT NULL,               -- weight_milligrams × valuation / 1000
    
    -- Tawarruq financing
    financing_amount_cents BIGINT NOT NULL,              -- principal (cost) — typically ~70% of appraised value
    profit_margin_cents BIGINT NOT NULL,                 -- fixed profit, computed at issuance
    total_amount_cents BIGINT NOT NULL,                  -- financing_amount + profit_margin = total to repay
    profit_margin_rate_bps INT NOT NULL,                 -- basis points used (for audit; 100 bps = 1%)
    
    -- Tenure
    issued_at DATE NOT NULL,
    matures_at DATE NOT NULL,                            -- max 6 months from issuance
    extended_to DATE NULL,                               -- max 12 months total (6 + one 6-month extension)
    
    state VARCHAR(32) NOT NULL,                          -- enum ArRahnuTicketState
    -- timestamps + soft delete + version
    
    CONSTRAINT chk_arn_weight_pos CHECK (weight_milligrams > 0),
    CONSTRAINT chk_arn_amounts_pos CHECK (financing_amount_cents > 0 AND profit_margin_cents >= 0),
    CONSTRAINT chk_arn_total CHECK (total_amount_cents = financing_amount_cents + profit_margin_cents),
    CONSTRAINT chk_arn_tenure CHECK (matures_at <= issued_at + INTERVAL '6 months'),
    CONSTRAINT chk_arn_extension CHECK (extended_to IS NULL OR extended_to <= issued_at + INTERVAL '12 months')
);
```

Notice: **gold weight is stored as integer milligrams, not float grams**. Same money-cents principle applied to weight to avoid float drift.

## State Machine

```
PENDING_APPRAISAL → APPRAISED → ISSUED → ACTIVE
                                    │      │
                                    │      ├─→ REDEEMED (member pays back, gold returned)
                                    │      ├─→ EXTENDED (one 6-month extension, max 12 months total)
                                    │      └─→ DEFAULTED → AUCTIONED → CLOSED
                                    └─→ CANCELLED (before money hands over)
```

Allowed transitions:

| From | To | Action class | Auth |
|---|---|---|---|
| (new) | PENDING_APPRAISAL | `CreateArRahnuTicket` | tenant_user |
| PENDING_APPRAISAL | APPRAISED | `RecordGoldAppraisal` | tenant_user |
| APPRAISED | ISSUED | `IssueArRahnuTicket` (money disbursed) | tenant_admin |
| ISSUED | ACTIVE | (system, on disbursement confirmed) | — |
| ACTIVE | REDEEMED | `RedeemArRahnuTicket` | tenant_user |
| ACTIVE | EXTENDED | `ExtendArRahnuTicket` | tenant_admin (extension is a credit decision) |
| ACTIVE | DEFAULTED | (system, on matures_at + grace period) | — (cron) |
| DEFAULTED | AUCTIONED | `RecordAuctionSale` | tenant_admin |
| AUCTIONED | CLOSED | (system, on settlement complete) | — |

## The Profit Margin Calculation

Per BNM-MPS: profit margin is **fixed at contract signing**, expressed as a basis-points rate, computed once. **It does not compound. It does not depend on duration in a way that mimics riba.**

```php
public function execute(string $tenantId, string $memberId, AppraisalData $appraisal, int $tenureDays): ArRahnuTicket
{
    return DB::transaction(function () use ($tenantId, $memberId, $appraisal, $tenureDays) {
        // Per tenant settings: profit_margin_rate_bps (e.g. 1.0% per 6 months = 100 bps for full tenure)
        $tenant = Tenant::with('arRahnuSettings')->findOrFail($tenantId);
        $rate_bps = $tenant->arRahnuSettings->profit_margin_rate_bps;

        // The financing amount is the principal — typically a percentage of appraised value
        $financing_amount_cents = (int) ($appraisal->appraised_value_cents 
            * $tenant->arRahnuSettings->loan_to_value_ratio_bps / 10000);

        // Profit margin: fixed, not compounded
        // For tenures <= 6 months, full rate applies (the rate IS the 6-month rate)
        // No daily/monthly accrual — this is the key Shariah distinction from interest
        $profit_margin_cents = (int) round($financing_amount_cents * $rate_bps / 10000);

        $ticket = ArRahnuTicket::create([
            'tenant_id' => $tenantId,
            'member_id' => $memberId,
            'ticket_no' => $this->generateTicketNumber($tenantId),
            'weight_milligrams' => $appraisal->weight_milligrams,
            'gold_purity' => $appraisal->purity,
            'valuation_cents_per_gram' => $appraisal->market_rate_cents_per_gram,
            'appraised_value_cents' => $appraisal->appraised_value_cents,
            'financing_amount_cents' => $financing_amount_cents,
            'profit_margin_cents' => $profit_margin_cents,
            'total_amount_cents' => $financing_amount_cents + $profit_margin_cents,
            'profit_margin_rate_bps' => $rate_bps,
            'issued_at' => now()->toDateString(),
            'matures_at' => now()->addDays($tenureDays)->toDateString(),
            'state' => ArRahnuTicketState::PendingAppraisal,  // appraisal recording is a separate Action
        ]);

        event(new ArRahnuTicketCreated($ticket->id, $tenantId));
        return $ticket->fresh();
    });
}
```

## Common Pitfalls

### Pitfall 1: Treating profit margin like interest

❌ "Daily interest rate × number of days outstanding" — this is riba.

✅ Fixed profit margin set at contract signing. If the customer redeems early, the profit is **not pro-rated downward** (per BNM-MPS — the contract is for the full tenure profit, paid in full regardless of early redemption). This is counter-intuitive but Shariah-correct.

Some koperasi offer a **rebate (ibra)** for early redemption as goodwill — this is permissible but it's a separate transaction (a discretionary discount, not a rate adjustment). Document any rebate as a separate journal entry.

### Pitfall 2: Compounding on extension

❌ When extending a 6-month ticket to 12 months total, calculating "rate × 12 months" treating it as if it were two consecutive 6-month tickets compounding.

✅ Each extension is a fresh contract: a new profit margin is computed at the extension date based on the current outstanding amount and the extension's tenure. Document the original ticket's full settlement (in principle) and a new ticket linked via `extended_from_ticket_id`. Some implementations keep one ticket and add a `profit_margin_extension_cents` column — both approaches are Shariah-acceptable, **AMIR uses the new-ticket-linked approach** for cleaner accounting.

### Pitfall 3: Float gold weight

❌ `weight_grams DECIMAL(8,3)` — drifts under repeated arithmetic.

✅ `weight_milligrams INT` — exact, no drift. Convert to grams only at display.

```php
public function getWeightGramsAttribute(): float
{
    return $this->weight_milligrams / 1000;
}
```

### Pitfall 4: Wrong market rate at appraisal

The market rate (`valuation_cents_per_gram`) **must be a snapshot at ticket issuance**, not a live lookup at every read. Gold prices fluctuate — if you re-fetch the rate, the appraised value changes after-the-fact, breaking the contract.

```php
// ❌ Live lookup at read time
$ticket->appraised_value_cents = $this->goldPriceFeed->latestPrice() * $ticket->weight_milligrams / 1000;

// ✅ Stored snapshot
$ticket->appraised_value_cents  // — set at issuance, never updated
```

### Pitfall 5: Defaulting before grace period

The maturity date is a hard cutoff in principle, but operationally most koperasi grant a 7-day grace period before triggering DEFAULTED. The grace period is a tenant-level setting (`tenants.ar_rahnu_grace_days`, default 7).

The cron job `MarkDefaultedTickets` should:
```php
$gracedTickets = ArRahnuTicket::where('state', ArRahnuTicketState::Active)
    ->where('matures_at', '<', now()->subDays($tenant->ar_rahnu_grace_days))
    ->get();
```

## Test Coverage Required

1. ✅ Happy path: appraise → issue → active → redeem → closed
2. ✅ Profit margin is fixed: redeeming on day 1 vs day 180 pays the same profit (no pro-ration)
3. ✅ Extension: extended ticket creates a new linked ticket; original closes; total tenure cannot exceed 12 months
4. ✅ Default: cron triggers DEFAULTED only after grace period; AUCTIONED transitions to CLOSED on settlement
5. ✅ Maximum tenure: ticket with `matures_at > issued_at + 6 months` rejected by DB CHECK
6. ✅ Maximum extension: extension that would put `extended_to > issued_at + 12 months` rejected
7. ✅ Weight precision: 99.5g and 99.500g produce identical `appraised_value_cents` (no float drift)
8. ✅ Cross-tenant: ticket created in tenant A invisible to tenant B
9. ✅ Audit log: every state transition recorded with from/to/reason
10. ✅ Shariah snapshot: `valuation_cents_per_gram` does not change after issuance

## Verification Checklist

- [ ] Tawarruq structure: `profit_margin_cents` is fixed at issuance, not computed daily
- [ ] No interest-equivalent code paths (no rate × days arithmetic on outstanding balance)
- [ ] Weight as `weight_milligrams INT`, never as decimal grams
- [ ] Market rate snapshot at issuance, never a live lookup
- [ ] Maximum tenure 6 months, max extension to 12 months total
- [ ] Grace period configured per tenant before DEFAULTED transition
- [ ] State machine transitions all explicit + auth checked
- [ ] Audit log entries on every state change with full context
- [ ] DB CHECK constraints on weights, amounts, tenures present
- [ ] Cross-tenant isolation tested
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/skills/writing-pdpa-handlers.md (12,260 bytes)"
mkdir -p ".claude/skills"
cat > '.claude/skills/writing-pdpa-handlers.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised reproduction or use outside of Parsec Sdn. Bhd.-authorised projects is prohibited.
-->

# Skill: Writing PDPA Handlers

Read this before touching any PDPA code (Module S — `app/Domain/Pdpa/`). PDPA compliance has hard regulatory deadlines (s.30 21-day SLA, s.12B 72-hour breach notification) — slipping these is not an inconvenience, it's a violation.

## The Source of Truth

- **Personal Data Protection Act 2010 (Act 709)** — Malaysia's PDPA, as amended by Act A1734 (2024).
- **DBN Guideline 2025** — Department of Personal Data Protection compliance guidelines (current).
- **AMIR DECISIONS_LOG.md** — search "PDPA", "s.12B", "s.30", "s.43A", "DBN"
- **AMIR ARCHITECTURE.md §2 Module S** — schema and operational flows
- **CONTENT_COPY.md §6** — DSAR response templates, breach notification copy

## Critical Sections of the Act

| Section | What it requires | AMIR implementation |
|---|---|---|
| **s.7-12** | Personal data protection principles | Encrypted-with-hash storage (E3), tenant scoping (D28), audit log (D19) |
| **s.12B** (Act 1734) | Breach notification — **72 hours** to notify Commissioner + affected data subjects | `pdpa_breaches` table + breach workflow + 72h SLA tracker |
| **s.13** | Registration with Commissioner before processing | Pre-launch task in PREFLIGHT_CHECKLIST.md |
| **s.30** | Data Subject Access Request (DSAR) — **21 days** to respond | `dsars` table + 21-day SLA |
| **s.32** | Data correction requests | Subset of DSAR workflow |
| **s.40** | Data subject right to withdraw consent | Profile settings page |
| **s.43A** (Act 1734) | Data portability — structured machine-readable export | JSON + CSV export Action |

## Three Workflows You Will Build

### 1. DSAR (Section 30) — 21-day SLA

Request → Verification → Compilation → Delivery → Closure.

```
PENDING_VERIFICATION → VERIFIED → COMPILING → READY_FOR_REVIEW → DELIVERED → CLOSED
                          │                                          │
                          └─→ REJECTED (identity not verified)        └─→ DISPUTED → ESCALATED
```

```sql
dsars (
    id UUID PRIMARY KEY,
    tenant_id UUID NOT NULL,
    requester_member_id UUID NULL REFERENCES members(id),  -- NULL if non-member
    requester_email VARCHAR(255) NOT NULL,
    requester_phone_e164 VARCHAR(20) NULL,
    requester_ic_no_hash CHAR(64) NULL,                     -- per E3, lookup-friendly
    request_type VARCHAR(32) NOT NULL,                      -- access, correction, withdrawal, portability
    state VARCHAR(32) NOT NULL,
    received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    deadline_at TIMESTAMPTZ NOT NULL,                       -- received_at + 21 days
    delivered_at TIMESTAMPTZ NULL,
    delivery_method VARCHAR(32) NULL,                       -- email, post, in_person
    -- timestamps + soft delete
    
    CONSTRAINT chk_dsar_deadline CHECK (deadline_at >= received_at)
);
```

The 21-day SLA must be tracked. Build a daily cron `CheckDsarDeadlines` that:
- Lists DSARs with `deadline_at < now() + 5 days AND state NOT IN ('delivered', 'closed')` — 5-day warning
- Lists DSARs with `deadline_at < now() AND state NOT IN ('delivered', 'closed')` — overdue, escalate to founder/PO

### 2. Breach Notification (Section 12B) — 72-hour SLA

```sql
pdpa_breaches (
    id UUID PRIMARY KEY,
    tenant_id UUID NOT NULL,
    discovered_at TIMESTAMPTZ NOT NULL,                     -- when the breach was first discovered
    severity VARCHAR(16) NOT NULL,                          -- low, medium, high, critical
    nature VARCHAR(64) NOT NULL,                            -- unauthorised_access, data_loss, alteration, disclosure
    affected_records_count INT NOT NULL,
    affected_data_categories JSONB NOT NULL,                -- array: ["nric", "phone", "address", "financial"]
    state VARCHAR(32) NOT NULL,                             -- enum BreachState
    
    -- 72h SLA tracking
    commissioner_notified_at TIMESTAMPTZ NULL,
    data_subjects_notified_at TIMESTAMPTZ NULL,
    deadline_at TIMESTAMPTZ NOT NULL,                       -- discovered_at + 72 hours
    
    -- Documentation
    incident_description TEXT NOT NULL,
    containment_actions TEXT,
    remediation_actions TEXT,
    
    -- timestamps + soft delete
    
    CONSTRAINT chk_breach_deadline CHECK (deadline_at = discovered_at + INTERVAL '72 hours')
);
```

The 72h deadline runs from **discovery time, not breach time**. A breach that occurred a week ago but was discovered today gets a 72h-from-today deadline.

State machine:
```
DISCOVERED → CONTAINED → ASSESSING → NOTIFYING → COMPLETED
                                          │
                                          └─→ COMMISSIONER_NOTIFIED + DATA_SUBJECTS_NOTIFIED
```

The `NotifyDataSubjectsAboutBreach` Action must:
- Use the per-subject contact preference (email primary, WhatsApp/SMS as fallback per CONTENT_COPY.md §6.3)
- Include all elements DBN Guideline 2025 mandates: nature of breach, data categories, likely consequences, mitigation steps taken, contact for queries
- Log every notification attempt (success/failure) per recipient — `pdpa_breach_notifications` table

### 3. Data Portability (Section 43A) — On-demand structured export

A Data Subject can request their personal data in a "structured, commonly used, machine-readable format". AMIR provides JSON + CSV.

The `ExportDataSubjectRecord` Action collects from every domain:
- Members module: member record + shares + subscription history
- Transactions: every transaction the member is party to
- Ar-Rahnu: every ticket and its lifecycle
- Audit log: every action by/about this member

Output format (JSON):
```json
{
  "exported_at": "2026-08-15T10:00:00Z",
  "data_subject": { "ic_no_hash": "...", "name": "...", "email": "..." },
  "records": {
    "members": [{...}],
    "shares": [{...}],
    "transactions": [{...}],
    "ar_rahnu_tickets": [{...}],
    "audit_log_entries": [{...}]
  }
}
```

Plus a CSV per category in a ZIP archive. Encrypted IDs are decrypted in the export (this is the data subject's own data — they get to see it).

## Critical Implementation Rules

### Rule 1: Never delete encrypted ciphertext on PDPA erasure

Per Akta Koperasi 1993, financial records have a 6-year retention requirement. PDPA s.43 erasure does NOT override this — financial records are **anonymised**, not deleted.

The erasure flow:
1. Replace `members.ic_no_encrypted` with a tombstone value (encrypted "ERASED-{date}")
2. Set `members.ic_no_hash` to a per-tenant-deterministic erasure-marker hash
3. Replace `members.full_name` with "Anonymised Member {short-id}"
4. Replace `members.phone_e164` with NULL
5. **Keep all transaction records intact** — they reference the member by ID, which is still valid
6. Audit log entry: `pdpa.erasure_completed` with the data subject's hash (kept for proof of erasure)

```php
// ❌ NEVER
$member->forceDelete();  // financial records reference this member!

// ✅
$member->update([
    'ic_no_encrypted' => 'ERASED-' . now()->toDateString(),
    'ic_no_hash' => $this->erasureMarkerHash($tenantSalt),
    'full_name' => 'Anonymised Member ' . substr($member->id, 0, 8),
    'phone_e164' => null,
    'email' => null,
]);
activity()->causedBy($officer)->performedOn($member)->log('pdpa.erasure_completed');
```

### Rule 2: PII in audit logs must be PII-scrubbed

Audit log message bodies are kept for 6 years. If they contain raw PII, an audit log dump is itself a PDPA exposure.

The `PIIGuard::scrub` helper redacts:
- IC numbers → `[REDACTED-IC]`
- Email addresses → `[REDACTED-EMAIL]`
- Phone numbers → `[REDACTED-PHONE]`
- Bank account numbers → `[REDACTED-BANK]`

Apply `PIIGuard::scrub()` to every payload that goes into the audit log (or any other long-retention log). Sentry breadcrumbs likewise — configure `beforeSend` to scrub.

### Rule 3: DSARs are tenant-scoped

A koperasi (tenant A) cannot send a DSAR for a member's data held by tenant B. Each koperasi handles its own members' DSARs. The Commissioner is notified per-tenant.

If a member is in two koperasi (tenant A and tenant B) and submits a DSAR, the member files separate DSARs with each koperasi.

### Rule 4: 21-day SLA + 72-hour SLA cannot be silently missed

Both deadlines are hard regulatory requirements. If a deadline is approaching, the system MUST surface it loudly:
- Daily cron writes to a tenant-admin notification queue
- Sentry alert when an SLA is breached (not just approaching)
- An overdue DSAR / breach is a P0 incident

Don't put the SLA check inside the relevant Action's success path — that's too late. The cron must run independently.

## Common Pitfalls

### Pitfall 1: Confusing PDPA s.12B (72h) with MyInvois 72h cancellation window

Same number, different domain. Both are 72-hour windows but they're regulated by different authorities (DBN Guideline vs LHDN Specific Guideline) and have nothing to do with each other.

### Pitfall 2: DSAR includes another data subject's data

A member's transaction with another member references both members' data. When exporting member A's record, **only A's side of the transaction is included**, not B's personal details. Filter accordingly.

```php
$transactions = Transaction::where('tenant_id', $tenantId)
    ->where(function ($q) use ($memberId) {
        $q->where('debtor_member_id', $memberId)
          ->orWhere('creditor_member_id', $memberId);
    })
    ->get()
    ->map(function ($txn) use ($memberId) {
        return [
            'id' => $txn->id,
            'date' => $txn->transaction_date,
            'amount_cents' => $txn->amount_cents,
            'role' => $txn->debtor_member_id === $memberId ? 'debtor' : 'creditor',
            'counterparty' => '[Other party details redacted per PDPA]',
            // ...
        ];
    });
```

### Pitfall 3: Notification by email only

Per DBN Guideline 2025, breach notifications must use multiple channels where possible. AMIR sends email + WhatsApp (where consented) + SMS (fallback) for high-severity breaches. Document the multi-channel attempt in `pdpa_breach_notifications`.

### Pitfall 4: Forgetting to log the erasure

The PDPA Commissioner can audit erasure compliance. Every erasure must produce a permanent audit log entry (which itself is anonymised — only the IC hash and the operation are recorded, not the original PII). The audit log row is the proof that the erasure happened.

## Test Coverage Required

1. ✅ DSAR happy path: submitted → verified → compiled → delivered within 21 days
2. ✅ DSAR overdue: cron triggers escalation when deadline_at < now() and state not delivered
3. ✅ DSAR cross-tenant: tenant A admin cannot view DSAR submitted to tenant B
4. ✅ Breach 72h deadline: cron triggers Sentry alert at hour 60 (warning) and hour 72 (P0)
5. ✅ Breach notification: data subjects notified via email + (WhatsApp if consented) + SMS fallback
6. ✅ s.43A portability: JSON + CSV ZIP includes all member data, no other members' details
7. ✅ Erasure preserves financial records: member → anonymised, transactions → intact
8. ✅ Erasure audit log: irreversible record of erasure operation
9. ✅ PII scrubbing: audit log messages and Sentry breadcrumbs do not contain raw IC / phone / email
10. ✅ DBN Guideline 2025 conformance: notification body includes all 5 mandatory elements

## Verification Checklist

- [ ] DSAR table has `deadline_at = received_at + 21 days` enforced via DB CHECK
- [ ] Breach table has `deadline_at = discovered_at + 72 hours` enforced via DB CHECK
- [ ] Daily cron `CheckDsarDeadlines` and `CheckBreachDeadlines` registered in scheduler
- [ ] Erasure flow updates encrypted columns + sets anonymised name; never `forceDelete`
- [ ] PII scrubbing applied via `PIIGuard::scrub` on audit log payloads
- [ ] Sentry `beforeSend` configured to scrub PII from breadcrumbs
- [ ] s.43A export JSON + CSV format covers all data categories
- [ ] Breach notification body includes all DBN Guideline 2025 elements
- [ ] Multi-channel breach notification attempted (email + WhatsApp + SMS) with per-channel logging
- [ ] Cross-tenant scoping verified for all PDPA Actions
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/plan.md (1,845 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/plan.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: Plan a user story or feature before coding. Do NOT write code yet.
---

# Task
Plan the implementation for: $ARGUMENTS

# Steps
1. Find the acceptance criteria for this story in USER_STORIES.md or the task description in `.sprint-backlog.json`
2. Identify all files to create or modify — list every one
3. Design any data model changes required (consult `.ai/guidelines/data-conventions.md` and `.claude/skills/writing-migrations.md`)
4. Design any API changes required
5. List implementation order (dependencies first)
6. Identify edge cases and error scenarios from the business flows
7. List every test needed (unit + integration + edge cases) per `.claude/skills/writing-tests.md`
8. Identify risks and ambiguities — anything unclear from the docs. Document them under `## Assumptions` so the founder can verify before merging.

# Domain-specific skills
If this task touches one of these domains, read the matching skill first:
- Accounting / Transactions / Penyata → `.claude/skills/writing-journal-entries.md`
- EInvoice / MyInvois → `.claude/skills/writing-myinvois-integration.md`
- ArRahnu → `.claude/skills/writing-ar-rahnu.md`
- Pdpa → `.claude/skills/writing-pdpa-handlers.md`

# Timing
Record the current timestamp in `.sprint-backlog.json` for this task:
- Set this task's `started_at` to the current ISO 8601 timestamp (UTC)
- If `sprints.[current_sprint].started_at` is empty, set it too

# Output
A numbered implementation plan. Do NOT write any code.
List risks and ambiguities under `## Assumptions` for the founder to verify.
Wait for approval before proceeding.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/implement.md (1,981 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/implement.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: Execute the approved implementation plan.
---

# Task
Implement the plan we just discussed.

# Rules
- Follow implementation order from the plan exactly
- Run relevant tests after each major step
- Follow ALL patterns in CLAUDE.md and the relevant `.ai/guidelines/*.md` files without exception
- If you hit an unexpected problem, stop and explain — do not guess or work around it
- Do not move to the next step until the current one passes tests

# Before writing tests
Read `.claude/skills/writing-tests.md` and apply all conventions to every test file you write.

# Before writing migrations
Read `.claude/skills/writing-migrations.md`. Run Boost's `database_schema` tool to verify current schema before designing the migration.

# Domain-specific skills
If your work touches one of these domains, read the matching skill first:
- Accounting / Transactions → `.claude/skills/writing-journal-entries.md`
- EInvoice → `.claude/skills/writing-myinvois-integration.md`
- ArRahnu → `.claude/skills/writing-ar-rahnu.md`
- Pdpa → `.claude/skills/writing-pdpa-handlers.md`

# Mandatory: Post-Implementation Cleanup
Once all tests are green, run `/simplify` before moving to `/verify`.
This spawns three parallel review agents (code reuse, code quality, efficiency) against your recently changed files and applies fixes automatically. It does not change behaviour — only clarity and structure.
Run it before `/verify` (not after) so that the final verification gate covers the simplified code. If `/simplify` introduces any unintended change, `/verify` will catch it.
You may optionally focus it: `/simplify focus on dead code removal`
Skip ONLY if the implementation was purely boilerplate (scaffolding, migrations, config).
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/scaffold.md (2,393 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/scaffold.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: Generate all boilerplate files for a new domain entity.
---

# Task
Scaffold all files for a new entity: $ARGUMENTS

# Files to generate
Adapt to AMIR's domain folder structure (`app/Domain/[Name]/...`):

- Model — under `app/Domain/[Name]/Models/`
- Migration — under `database/migrations/`
- Action class(es) — under `app/Domain/[Name]/Actions/` (Create, Update, Delete at minimum)
- Form Request(s) — under `app/Domain/[Name]/Requests/`
- API Resource — under `app/Domain/[Name]/Resources/`
- Policy — under `app/Domain/[Name]/Policies/`
- Feature test — under `tests/Feature/Api/V1/[Name]/` (with `AssertsQueryPerformance`)
- Factory — under `database/factories/[Name]Factory.php`
- Enum (if status) — under `app/Domain/[Name]/Enums/`

# Rules
- Use AMIR patterns from CLAUDE.md and `.ai/guidelines/project-architecture.md` — not Laravel defaults
- UUID v7 primary keys (HasUuids trait, `newUniqueId()` returning Str::uuid7())
- `final class` on every class (per Decision D32)
- Single-action controllers (`__invoke` only) per D31
- Action class pattern (single `execute` method, transactional, dispatches events) per D30
- Tenant-scoped models: `use BelongsToTenant;` (per D28) — the post-edit-tenant-scope-check.sh hook will warn if missing
- Money columns as `BIGINT *_cents` with `MoneyCast::class`
- Phone columns as `phone_e164 VARCHAR(20)` (per D33)
- Encrypted IDs via E3 two-column pattern (`*_encrypted` + `*_hash`)
- `timestampsTz()` on every table
- `softDeletesTz()` for entities with audit-trail lifecycle
- `version INT DEFAULT 1` for entities with concurrent-edit potential
- Wire the Policy into AuthServiceProvider
- Register events in EventServiceProvider if applicable
- Add the route in `routes/api.php` under the `/api/v1/` group

# Before writing tests
Read `.claude/skills/writing-tests.md` and apply all conventions to every test file generated.

# Before writing migrations
Read `.claude/skills/writing-migrations.md`. Run Boost's `database_schema` tool to verify the current schema first.

# After scaffolding
Add a one-line entry to `docs/living/CODEBASE_MAP.md` for each new domain folder created.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/test-and-fix.md (1,745 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/test-and-fix.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: Run tests, diagnose failures, fix code. Never weaken tests.
---

# Task
1. Run the full test suite: `php artisan test`
2. If all green — report the summary and stop
3. If failures:
   a. Read each failure carefully — understand root cause, not just symptom
   b. Fix the code (not the test, unless the test itself is wrong)
   c. Re-run to verify the fix
   d. Repeat until fully green

# Rules
- Never delete a failing test to make the suite pass
- Never weaken assertions (e.g. changing `assertStatus(201)` to `assertSuccessful()`)
- If a test reveals a real bug — fix the code
- If a test has genuinely wrong expectations — fix the test AND add a comment explaining why
- Never relax a query count budget without a documented reason
- Do not stop until the suite is fully green

# Common failure modes (check these first)
- **Migration not run** → run `php artisan migrate:fresh --seed` for the test database
- **Boost MCP disconnected** → run `claude mcp list` and reconnect Laravel Boost if missing
- **Tenant scope** → ensure `app()->instance('currentTenantId', $tenant->id)` is set before acting in tenant-scoped tests
- **Time-dependent test** → use `Carbon::setTestNow($timestamp)` for determinism
- **Money cents** → check that no float arithmetic crept in (tests should use integer cents throughout)

# Use Boost when stuck
- `last_error` — read the most recent application error
- `read_log` — tail the Laravel log
- `tinker` — verify model state at the point of failure
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/verify.md (5,068 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/verify.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: 13-step self-verification before committing. MANDATORY — do not skip.
---

Run every check. Fix failures before proceeding. Do NOT commit until all pass.

## Step 1: Tests
Run the full test suite: `php artisan test`. Zero failures allowed.

## Step 2: Static Analysis
Run `./vendor/bin/phpstan analyse --no-progress`. Zero errors.

## Step 3: Code Style
Run `./vendor/bin/pint --test`. Fix any issues with `./vendor/bin/pint --dirty`.

## Step 4: Performance
For every integration test in your changes:
- Query count is asserted via `AssertsQueryPerformance` trait
- No duplicate queries (`assertNoDuplicateQueries`)
- No slow queries (`assertNoSlowQueries(thresholdMs: 100)`)
- Multi-step writes use `DB::transaction(...)`
- No queries inside loops

## Step 5: Architecture Compliance
Read `.claude/skills/reviewing-code.md` Section 1. For every changed file:
- Business logic in Action classes — not in controllers, not in services, not in models (per D30)
- Single-action controllers (`__invoke` only) per D31
- `final class` on every class per D32
- Events fired for state changes (past-tense names, public readonly properties)
- Correct domain folder placement per `.ai/guidelines/project-architecture.md` §2

## Step 6: Data Integrity
Read `.claude/skills/reviewing-code.md` Section 2.
- UUID v7 PKs (per D15-R1) via HasUuids
- Tenant scoping via `BelongsToTenant` (per D28)
- Money as `BIGINT *_cents` + `MoneyCast::class`
- Phone as `phone_e164 VARCHAR(20)` (per D33)
- Encrypted IDs use E3 two-column pattern (`*_encrypted` + `*_hash`)
- Migrations are additive (or follow the safe-deprecation pattern)

## Step 7: Security
- No secrets hardcoded
- Auth/authorization on every write endpoint (FormRequest `authorize()` calls policy)
- All inputs validated via FormRequest
- Tenant scoping defence in depth (policy verifies `currentTenantId === model.tenant_id`)
- No sensitive data in URLs, logs, or API responses
- PII scrubbed in Sentry breadcrumbs (`beforeSend`)

## Step 8: Code Quality — LLM Bloat Check
Read `.claude/skills/reviewing-code.md` Section 5. For every file you changed:
- No unused imports, variables, or function parameters
- No functions/classes created but never called
- No single-use helper methods (inline them unless they significantly aid readability)
- No defensive null checks for non-nullable types
- No catches for unthrown exceptions
- No abstraction layers with only one implementation
- No commented-out code (delete it — git has history)
- No overly complex conditionals (simplify or extract)
- No variables assigned then immediately returned
- No files scaffolded but never wired into routes/navigation
- Every new line traces to a specific acceptance criterion

## Step 9: Design System Compliance (frontend files only)
Read `.claude/skills/reviewing-code.md` Section 7. For every JSX/TSX file you changed:
- All UI elements use design system components (Button, DataTable, FormField, StatusBadge) — no raw HTML equivalents
- Zero hardcoded colour, spacing, typography, radius, shadow values — all via design tokens
- All status displays use `<StatusBadge>` with enum value
- Page uses a standard layout pattern (L0–L5)
- No ad-hoc components duplicating an existing design system component
- No `<form>` tags with `type=submit` in multi-section forms (per Lesson 12-L19)
- BM/EN parity — every user-facing string uses `__()`
Skip this step if the change is backend-only (no JSX/TSX files changed).

## Step 10: Acceptance Criteria
Re-read the task in `.sprint-backlog.json`. For EVERY acceptance criterion:
- Is it fully implemented?
- Is there a passing test that proves it?
- Are edge cases handled?

## Step 11: Completeness
- Every new public method has a test
- Error cases handled (not just happy path)
- No TODOs left behind
- No `dd()` / `dump()` / `var_dump()` / `console.log` / `ray()` left in code (the `pre-commit-guard.sh` hook will hard-block, but check now)
- Cross-tenant isolation test included if tenant-scoped feature

## Step 12: Simplify Check
If you did NOT run `/simplify` after `/implement`, run it now.
Review the diff it produces — accept only changes that reduce complexity without altering behaviour.

## Step 13: Final Diff Review
Run `git diff --stat` — confirm ONLY files in this task's scope are changed.
Run grep checks (Lesson 12-L17 + 12-L18):
```bash
grep -rn '"/api/' tests/ | grep -v '/v1/'   # must be empty
grep -rn '<<<<<\|>>>>>\|=======' --include='*.php' --include='*.jsx' --include='*.tsx'   # must be empty
```
Read the full diff once more. Ask: "Would a senior engineer approve this, or would they say it's overcomplicated?"

## Verdict
ALL pass → commit with a brief verification summary in the commit message body
ANY fail → fix and re-verify from Step 1
Cannot fix → report what's wrong and why — do NOT commit
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/commit-push-pr.md (2,403 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/commit-push-pr.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: Stage, commit, push, and open a pull request.
---

# Steps
1. Record completion in `.sprint-backlog.json`:
   - Set `completed_at` to current ISO 8601 timestamp (UTC)
   - Append to `agent_sessions` with agent worktree, session start/end, outcome
2. Stage all changes: `git add -A`
3. Review what's staged: `git diff --cached --stat`
4. Read `.claude/skills/writing-pr-descriptions.md` and follow the template exactly for both the commit message and the PR description.
5. Write a conventional commit message:
   - `feat`: new feature or capability
   - `fix`: bug fix
   - `refactor`: code restructure with no behaviour change
   - `test`: adding or fixing tests
   - `docs`: documentation only
   - `chore`: tooling, dependencies, config
6. Commit using multi-line format with the **mandatory Parsec separator** on its own line directly after the subject:
   ```
   git commit -m "[type]([scope]): [description]
   .--. .- .-. ... . -.-.

   [body — what changed, why, any context]"
   ```
   The `pre-commit-guard.sh` hook will hard-block if:
   - Morse separator is missing
   - `Co-Authored-By:` line is present
   - Debug artifacts (`dd()`, `console.log`, etc.) are in staged files
7. Push: `git push -u origin [current-branch-name]`
8. Create PR using the template from `.claude/skills/writing-pr-descriptions.md`:
   ```bash
   gh pr create --base dev --title "[type]([scope]): [description]" --body "$(cat <<'EOF'
   ## What This Does
   ...

   ## Story / Task
   ...

   ## Acceptance Criteria
   - [x] AC1: ...

   ## Implementation Notes
   ...

   ## Assumptions
   ...

   ## Test Plan
   - [x] php artisan test — zero new failures
   - [x] ./vendor/bin/pint --test — clean
   - [x] ./vendor/bin/phpstan analyse — clean
   - [x] /api/v1/ prefix verified
   - [x] No merge conflict markers
   - [x] Cross-tenant isolation test included (if applicable)

   ## Decisions Trace
   - D15-R1, D16, D28, D30, D31, D32, ...
   EOF
   )"
   ```
9. Output the PR URL.

# After the PR is open
The task is NOT complete until the PR is merged on GitHub. After merge:
- Run `/task-done [task-id]` to mark complete in `.sprint-backlog.json`
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/review-changes.md (3,037 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/review-changes.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: Review recent changes as a senior staff engineer. Find bugs, gaps, violations.
---

# Before Reviewing
Read `.claude/skills/reviewing-code.md` and apply the full 8-section checklist.

# Task
Review the changes in the current branch against CLAUDE.md, `.ai/guidelines/*.md`, and the project architecture.

# Check for
1. **Architecture violations** — business logic in wrong layer, missing events, wrong domain folder placement (per project-architecture.md §2)
2. **Data integrity** — wrong ID type, money as float, missing tenant scope, missing E3 encryption pattern
3. **Security gaps** — missing auth, unvalidated inputs, exposed PII, missing tenant double-check in policies, missing rate limits
4. **Performance issues** — N+1 queries, missing indexes, unbounded queries, no transactions, missing query count assertions
5. **Missing edge cases** — what happens when input is empty, null, wrong type, at boundary, in BM/Unicode characters, cross-tenant
6. **Missing error handling** — what happens when MyInvois rejects, when DB transaction deadlocks, when Boost MCP times out
7. **Test gaps** — is every AC proven by a test? Are negative cases tested? Cross-tenant isolation? Query count asserted?
8. **Data convention violations** — wrong ID type (must be UUID v7), money as float (must be cents), wrong date format, plain phone column instead of `phone_e164`
9. **DECISIONS_LOG.md violations** — every decision the code touches must be honoured. If a decision conflicts with the implementation, raise a revision (`Dxx-R1`) instead of silently drifting
10. **DO NOT rule violations** — check every rule in CLAUDE.md `## Common Mistakes — DO NOT`

# Domain-specific checks
If the diff touches one of these domains, also apply the domain-specific skill checklist:
- Accounting / Transactions / Penyata → `.claude/skills/writing-journal-entries.md` (debit=credit invariant, posted-immutable, period state)
- EInvoice / MyInvois → `.claude/skills/writing-myinvois-integration.md` (RM10K rule, 72h cancellation, schema version)
- ArRahnu → `.claude/skills/writing-ar-rahnu.md` (Tawarruq structure, fixed profit margin, max tenure)
- Pdpa → `.claude/skills/writing-pdpa-handlers.md` (21-day DSAR SLA, 72h breach SLA, erasure preserves financial records)

# Output
A numbered list of findings grouped by severity:
- **BLOCKER** — must fix before merge (security, data integrity, broken tests, architecture violations, Decisions violations)
- **IMPORTANT** — should fix before merge (performance, missing edge cases, design system violations)
- **SUGGESTION** — nice to have (clarity improvements, naming, comments)

For each finding: what the problem is, where it is (file + line), and what the correct approach is.

Do NOT fix — only report. The developer reviews and decides.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/sprint-status.md (2,139 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/sprint-status.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: Show current sprint progress.
---

# Show Sprint Status

## Step 1: Switch to Haiku
This is a read-only status check — no reasoning required.
```
/model haiku
```

## Step 2: Read and display
Read `.sprint-backlog.json` and produce a progress table for the current sprint.

Include timing data:
- For completed tasks: show duration and number of agent sessions
- For in-progress tasks: show elapsed time since `started_at`
- At the bottom: show sprint elapsed time since `sprints.[N].started_at`

# Output format

```
AMIR — Sprint [N] Status
─────────────────────────

| ID    | Title                          | Status      | Branch                           | Notes                |
|-------|--------------------------------|-------------|----------------------------------|----------------------|
| 1.01  | Database scaffold              | ✅ done     | feature/s01-01-db-scaffold       | 2.1h, 1 session      |
| 1.02  | Auth core                      | 🔄 in prog  | feature/s01-02-auth-core         | started 1.5h ago     |
| 1.03  | Tenant model                   | ⏸ pending   | —                                | deps: 1.02 not merged|

Sprint progress: 1 / 12 done • 1 in progress • 10 pending • 0 blocked
Sprint elapsed: 1d 4h 22m
Development Level: 3 (Manual)
```

Flag any task where `deps[]` are not yet merged (look at task statuses, not just by ID).
Flag any task that has been in-progress for more than 1 sprint cycle (check `started_at` vs sprint median duration).
Flag if context usage is >300k tokens — recommend `/compact` before next task.

## Step 3: Switch back to Sonnet
```
/model sonnet
```

## Step 4: Recommendations
Based on the status, suggest the next action:
- Pending task with all deps merged → suggest `/plan [task-id]`
- In-progress task → reorient on it
- All tasks done → suggest `/sprint-close`
- Blocked task → surface what's blocking it
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/task-done.md (1,861 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/task-done.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: Mark the current sprint task as done in .sprint-backlog.json. Run this after merging the PR for a task. Usage: /task-done [task-id] e.g. /task-done 1.02
---

# Mark Task Done

## Step 1: Switch to Haiku
This is mechanical bookkeeping — no reasoning required.
```
/model haiku
```

## Step 2: Identify the task
If a task ID was provided (e.g. `1.02`), use it.
If no task ID was provided, read `.sprint-backlog.json` and find the task with `"status": "in_progress"`. Confirm with the user before proceeding.

## Step 3: Switch to dev and pull latest
```bash
git stash        # stash any uncommitted changes on current branch
git checkout dev
git pull origin dev
```

## Step 4: Mark the task done
In `.sprint-backlog.json`, for the identified task:
- Set `"status"` to `"done"`
- Set `"completed_at"` to the current ISO 8601 timestamp (UTC)

## Step 5: Commit and push
```bash
git add .sprint-backlog.json
git commit -m "chore: mark task [task-id] done
.--. .- .-. ... . -.-.

Task [task-id] merged and complete."
git push origin dev
```

## Step 6: Restore previous state
```bash
git checkout -    # return to the branch you were on before
git stash pop     # restore any stashed changes (if any were stashed)
```

## Step 7: Switch back to Sonnet
```
/model sonnet
```

## Step 8: Confirm
Report back:
- Task [task-id] marked done ✓
- Model restored to Sonnet ✓
- Next task: read `.sprint-backlog.json` and surface the next task with `"status": "pending"` in the current sprint
- If no pending tasks remain in the current sprint: prompt "All sprint tasks complete. Run `/sprint-close` to finalise."
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/sprint-start.md (5,140 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/sprint-start.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: Prepare the next sprint — populates .sprint-backlog.json from SPRINT_PLAN.md and (at Level 4) reconfigures dispatch.sh. Run this after /sprint-close advances the sprint counter. Usage: /sprint-start [sprint-number] e.g. /sprint-start 22
---

# Prepare Sprint

## Step 1: Identify the sprint and level
Read `.sprint-backlog.json`:
- `_meta.current_sprint` — the sprint number to prepare (or use `$ARGUMENTS` if provided)
- `_meta.development_level` — determines which steps to run and which models to use

## Step 2: Check if already done
If tasks or features already exist in `.sprint-backlog.json` for this sprint with `"status": "pending"`, skip to Step 5.

NOTE: AMIR's backlog ships with all 605 tasks across all 43 sprints (S00–S42) pre-populated by Phase 11. So this command's primary job for AMIR is **Step 4 (dispatch.sh reconfig at L4) and Step 6 (confirm)**, not populating the backlog. Backlog population only triggers if a sprint exists in SPRINT_PLAN.md but not in .sprint-backlog.json — which shouldn't happen unless a sprint was added mid-development.

---

## Step 3: Populate the backlog — Haiku (only if needed)
Skip this step if backlog already populated for the sprint (the typical case for AMIR).

If needed, switch to Haiku — this step is mechanical data transformation:
```
/model haiku
```

Read `SPRINT_PLAN.md`. Find the sprint matching the sprint number.

**Level 3 / Level 4 — add a task entry per task:**
```json
{
  "id": "[sprint].[seq]",
  "title": "[task title from SPRINT_PLAN.md]",
  "sprint": [N],
  "status": "pending",
  "branch": "feature/s[NN]-[NN]-[kebab-task-title]",
  "deps": ["[dependency task IDs — empty array if none]"],
  "files_touched": ["[list every file this task will create or modify]"],
  "prompt": "[full task description from SPRINT_PLAN.md including flows, ACs, and notes]",
  "started_at": null,
  "completed_at": null,
  "agent_sessions": []
}
```

**Level 5 — add a feature entry per feature:**
```json
{
  "id": "F[sprint]-[seq]",
  "type": "feature",
  "title": "[feature name]",
  "sprint": [N],
  "flows": ["[flow IDs from SPRINT_PLAN.md]"],
  "brief": "[full feature description — backend scope, frontend scope, ACs, doc references]",
  "status": "pending",
  "batch": "[batch-name]",
  "started_at": null,
  "completed_at": null,
  "agent_sessions": []
}
```

Commit and push:
```bash
git checkout dev
git pull origin dev
git add .sprint-backlog.json
git commit -m "chore: populate Sprint [N] tasks from SPRINT_PLAN.md
.--. .- .-. ... . -.-.

[N] tasks loaded for Sprint [N]."
git push origin dev
```

---

## Step 4: Configure dispatch.sh — Level 4 only
**Skip this entire step for Level 3 and Level 5.**

For Level 4, this step requires multi-constraint reasoning — classifying file ownership, detecting coupling, validating zero overlap. Switch to Sonnet before proceeding.
```
/model sonnet
```

Read all tasks for this sprint from `.sprint-backlog.json`. Note each task's `files_touched` array.

**Classify each task:**
- **Isolated** — creates its own new files, zero overlap with any other task → assign to its own worktree (parallel)
- **Coupled** — modifies a file that another task also modifies (same model, action class, migration, or core service) → assign to same worktree as coupled task (sequential)
- **Append-only** — only appends to shared files (`routes/api.php`, `config/*.php`) → can tolerate different worktrees

**Validation — before grouping:** For every pair of tasks on different worktrees, confirm their `files_touched` arrays have zero overlap. If any overlap exists — even one file — those tasks must be on the same worktree.

**Grouping rules:**
- No worktree should have more than 2–3 chained tasks (execution is additive within a worktree)
- Same-worktree tasks must have a clear sequential dependency: task B uses or imports something task A creates
- Use all 4 worktrees where safely possible — don't leave worktrees idle

Update the `BATCHES` config section in `dispatch.sh` and the comment block at the top:
```
# Sprint [N] batch layout — generated [date]
# BATCH_name: task-a (WT-B) || task-b (WT-C) || task-c → task-d sequential (WT-D)
# Rationale: task-c and task-d share [ClassName] — sequential on WT-D
```

Commit and push:
```bash
git add dispatch.sh
git commit -m "chore: configure dispatch.sh for Sprint [N]
.--. .- .-. ... . -.-.

Batch layout: [one line per batch — task IDs, worktrees, rationale]"
git push origin dev
```

---

## Step 5: Switch back to Sonnet
```
/model sonnet
```

## Step 6: Confirm
Report back:
- Sprint [N] backlog populated: [N] tasks/features ✓ (or "already populated — skipped")
- dispatch.sh configured: ✓ (Level 4) / not needed (Level 3 / Level 5)
- Model restored to Sonnet ✓
- Ready to start. Next:
  - Level 3: `/plan [first-task-id]`
  - Level 4: `./dispatch.sh [first-batch-name]`
  - Level 5: `/team-launch [first-feature-id]`
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/sprint-close.md (9,498 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/sprint-close.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: Sprint-close performance regression gate. Run when all sprint tasks are merged. Do NOT advance to the next sprint until this passes.
---

# Sprint-Close Performance Gate

All sprint tasks are merged. Before advancing the sprint counter, run the full performance regression suite across the entire codebase to catch cross-task regressions that individual agents would not have seen.

## Step 1: Full Test Suite
Run every test in the project. Zero failures allowed.
```bash
php artisan test --stop-on-failure
```

## Step 2: Performance Regression Check
Run the full suite a second time with query logging enabled.
For every integration test in the codebase — not just this sprint's tests — verify:

- No test that previously passed its query count assertion is now failing
- No new duplicate queries introduced by this sprint's work
- No new slow queries (>100ms) introduced by this sprint's work
- No multi-step writes missing transactions

```bash
php artisan test --filter=Feature
# Look for any output containing:
# "Expected < N queries, got M"
# "Duplicate queries detected"
# "Slow queries (>100ms) detected"
```

## Step 3: Identify Regressions
If any performance assertion fails that was passing before this sprint:
1. Identify which sprint task caused the regression (`git log` on the failing test's domain)
2. Create a rework task in `.sprint-backlog.json`:
   - Title: "Performance regression: [describe the query issue]"
   - Prompt: include the failing assertion output, the suspected cause, and the fix required
3. Do NOT advance the sprint counter until the rework task is merged and this gate passes clean

## Step 4: Static Analysis Pass
```bash
./vendor/bin/phpstan analyse --no-progress
./vendor/bin/pint --test
```

## Step 4a: Record Velocity Data
1. Read `.sprint-backlog.json` — collect all `started_at`, `completed_at`, and `agent_sessions` for this sprint
2. Set `sprints.[current_sprint].closed_at` to the current timestamp
3. Calculate durations for each task and each agent session
4. Identify: slowest task, most common verification failure step, total rework count
5. Append a new sprint entry to `docs/living/VELOCITY_LOG.md`:
   ```
   ## Sprint [N] — [date range]
   Total tasks: [N]
   Avg task duration: [Hh Mm]
   Slowest task: [task-id] ([Hh Mm])
   Total agent sessions: [N]
   Rework rate: [X%]

   Framework improvement notes:
   - [Tasks where prompts were underspecified]
   - [Verification steps that failed repeatedly]
   - [Agent sessions that took disproportionately long]
   ```

## Step 4b: Update Living Documents
Update the seven living documents in `docs/living/` before advancing the sprint counter.

For each file, apply its sprint-close update instruction:
- **CODEBASE_MAP.md** — add/update/remove entries for every file touched this sprint
- **DATA_MODEL.md** — reflect every migration run this sprint (new tables, columns, indexes)
- **API_REFERENCE.md** — reflect every route added, changed, or removed this sprint
- **DOMAIN_GUIDE.md** — update Actions, Events, Business Rules for every domain touched
- **DEVIATION_LOG.md** — log every divergence from the original plan this sprint. If none: add one line "Sprint [N]: no deviations from plan."
- **PRINCIPLES.md** — update if any architectural learning was generalised this sprint
- **VELOCITY_LOG.md** — already updated in Step 4a

Commit all seven files together:
```bash
git add docs/living/
git commit -m "docs: update living codebase documents for Sprint [N]
.--. .- .-. ... . -.-."
git push origin dev
```
This commit must land on `dev` before the sprint counter advances.

## Step 4c: Prompt Quality Pass
Review this sprint's completed tasks for prompt quality lessons:
1. For each task that required rework: what was missing from the prompt?
2. For each new DO NOT rule added to CLAUDE.md this sprint: does the same pattern appear in remaining prompts?
3. For each clarifying question an agent asked: should the prompt have answered it upfront?

Scan ALL remaining pending task prompts in `.sprint-backlog.json`. Apply fixes:
- Add specific pattern references where prompts currently say generic "Follow CLAUDE.md"
- Add explicit file placement instructions where agents guessed wrong
- Add verification steps for patterns agents frequently miss

Commit if any prompts were updated:
```bash
git add .sprint-backlog.json
git commit -m "chore: improve task prompts based on Sprint [N] lessons
.--. .- .-. ... . -.-."
```

## Step 4d: Framework Lessons (cross-project)
Review this sprint's `docs/living/DEVIATION_LOG.md` and `VELOCITY_LOG.md` for framework-level problems — issues that would affect the next project too.

For each framework-level lesson found, append to `docs/framework-lessons.md`:
```
## Sprint [N] — [date]

### [N]-L[X]. [Lesson heading]
[2–3 sentences on what happened]
**Rule:** [one clear instruction]
```

If no framework-level lessons this sprint, append: "Sprint [N] [date]: no framework-level lessons identified."

Commit:
```bash
git add docs/framework-lessons.md
git commit -m "docs: framework lessons from Sprint [N]
.--. .- .-. ... . -.-."
```

## Step 4e: Reconfigure dispatch.sh for Next Sprint (Level 4+ only)
Skip if `_meta.development_level < 4`.

`dispatch.sh` contains a batch configuration **specific to the current sprint**. Task dependencies, file ownership, and safe parallel groupings change every sprint. The script must be reconfigured before the next sprint begins.

**Batch design algorithm — follow in order:**

1. Read the next sprint's tasks from `.sprint-backlog.json`. For each task, note its `files_touched` array.

2. **Classify each task by file ownership type:**
   - **Isolated** — creates its own new files, touches no files shared with other tasks
   - **Coupled** — shares a model, action class, or core file with another task
   - **Append-only** — only appends to shared files like `routes/api.php` or `config/*.php`

3. **Apply the assignment rules:**
   - Isolated tasks → different worktrees, run in parallel
   - Coupled tasks (task B uses or imports something task A creates) → same worktree, run sequentially
   - Append-only tasks → tolerate on different worktrees; merge conflicts will be simple
   - Never assign more than 2–3 tasks to the same worktree in one batch — execution time is additive

4. **Validate the batch before committing:**
   - For every pair of tasks on different worktrees: confirm zero overlap in `files_touched`
   - For every pair of tasks on the same worktree: confirm a clear sequential dependency (B builds on A)
   - Use all 4 available worktrees — don't leave worktrees idle if tasks can be parallelised safely

5. Update the named `BATCH_*` entries in `dispatch.sh` with the new groupings. Same-worktree tasks must be listed consecutively in the batch string.

6. Document the layout in a comment block at the top of `dispatch.sh`:
   ```
   # Sprint N batch layout:
   # BATCH_name: task-a (WT-B) || task-b (WT-C) || task-c+task-d sequential (WT-D)
   # Rationale: task-c and task-d share CalculationAction — sequential on WT-D
   ```

Commit the updated script:
```bash
git add dispatch.sh
git commit -m "chore: reconfigure dispatch.sh for Sprint [N+1]
.--. .- .-. ... . -.-.

Batch layout:
[task IDs] → [worktrees] (parallel/sequential rationale)
..."
git push origin dev
```

## Step 5: Advance Sprint
Only when Steps 1–4e all pass:
1. Update `.sprint-backlog.json`: change `_meta.current_sprint` to the next number
2. Confirm all sprint tasks show `"status": "done"`
3. Report: total tests run, query assertions checked, regressions found/resolved, living docs updated, remaining prompts improved, sprint duration, average task duration, total agent sessions, rework rate

## Level 6 — Human Sign-Off (parallel sprint only)

If `_meta.development_level == 6`, present this before advancing the sprint counter:

```
Sprint [N] — Parallel Sprint Summary
• Worktrees used: [list which worktrees ran features]
• Features completed: [N] across [N] worktrees
• Features failed/deferred: [N] (list with reasons)
• Total PRs merged to dev this sprint: [N]
• Lines changed: +[N] / -[N]
• New files: [list]
• Modified domains: [list]
• Shared file merge conflicts resolved by dispatcher: [N]

Human sign-off checklist:
- [ ] Scan full sprint diff for wrong patterns
      git diff [sprint-start-sha]..dev
- [ ] Read DEVIATION_LOG.md entries added this sprint — all intentional?
- [ ] Spot-check 2–3 entries in CODEBASE_MAP.md — still accurate?
- [ ] Confirm no DO NOT rules violated across the sprint
- [ ] Verify shared files (routes/api.php, router.tsx) are coherent after multi-worktree merges
- [ ] Confirm no cross-domain leakage from parallel execution

Type "Sprint [N] approved" to merge dev → main.
Type "Sprint [N] rejected — [reason]" to hold and create rework tasks.
```

Do NOT merge `dev` to `main` until explicit human sign-off is received in this session.

## Verdict
ALL pass + living docs committed + prompts updated → advance sprint counter, report clean bill of health
ANY regression → create rework task, do NOT advance sprint, re-run gate after fix
Living docs not committed → do NOT advance sprint counter
Prompt quality pass skipped → acceptable but not recommended; at minimum review after Sprint 1
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/watch.md (1,894 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/watch.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: Start a background sprint monitor. Runs /sprint-status on a recurring interval so you get passive visibility without polling manually. Use while agents are running.
---

# Task
Set up a recurring background monitor for the current sprint using `/loop`.

## Default behaviour (no arguments)
Run `/loop 10m /sprint-status` — checks sprint progress every 10 minutes and surfaces any completions, stalls, or blocked tasks.

## With a custom interval
If `$ARGUMENTS` is provided and is a time value (e.g. `5m`, `2m`, `30m`), use that interval instead:
`/loop $ARGUMENTS /sprint-status`

## Level-aware guidance
Check `_meta.development_level` in `.sprint-backlog.json` and append one line of context after starting the loop:

- **Level 3:** "Monitor started. You'll see a status update every [interval]. No action needed — check back when you're ready."
- **Level 4:** "Monitor started. When an agent finishes you'll see it in the next status ping. Run `./review.sh [batch-name]` when you see completed tasks."
- **Level 5:** "Monitor started. When a feature team completes you'll see it in the next ping. Review the feature PR when it appears."
- **Level 6:** "Monitor started. Parallel worktrees executing. Status pings will surface completions, merge conflicts, and failures across all 4 worktrees."

## Important constraints
- `/loop` is session-scoped: it stops when you close this Claude Code session.
- For Level 6 parallel runs, keep the dispatcher terminal (worktree a) open — it coordinates merges across worktrees.
- To cancel the monitor: ask Claude to "cancel the sprint watch loop" or "list and cancel my scheduled tasks".
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/flag-mistake.md (2,824 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/flag-mistake.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: Capture an agent mistake as a permanent rule. Updates CLAUDE.md, .cursorrules, and optionally remaining sprint backlog prompts.
---

# Flag Mistake

You made a mistake. Here's what happened: $ARGUMENTS

## Step 1: Understand the mistake
Explain back what went wrong in one sentence. If `$ARGUMENTS` is vague, ask one clarifying question before proceeding.

## Step 2: Formulate the rule
Write the rule in the format:
```
- **DO NOT** [specific wrong behaviour]. **DO:** [specific correct behaviour]. *(Added: [today's date], Sprint [N])*
```

The rule must be:
- **Specific enough to be checkable.** BAD: "Don't write bad code." GOOD: "DO NOT put business logic in controllers. DO: put all business logic in Action classes under `app/Domain/[Name]/Actions/`."
- **Actionable.** An agent reading only the DO NOT list should know exactly what to avoid and what to do instead.
- **Non-redundant.** Check the existing DO NOT list first — does a rule already cover this? If yes, make the existing rule more specific instead of adding a duplicate.

## Step 3: Update CLAUDE.md
1. Open `CLAUDE.md`
2. Find the `## Common Mistakes — DO NOT` section
3. Check for an existing rule that covers this mistake:
   - If found: replace it with a more specific version that also covers this case
   - If not found: append the new rule at the end of the list
4. Add an entry to the `## Recent Changes` section at the top:
   ```
   - [today's date]: New DO NOT rule: [one-line summary]. Trigger: [what happened].
   ```

## Step 4: Update .cursorrules
1. Open `.cursorrules`
2. Find the `## DO NOT` section (or equivalent)
3. Add the same rule (condensed to one line if needed)

## Step 5: Check remaining backlog prompts (if the mistake is likely to recur)
1. Open `.sprint-backlog.json`
2. Search for all tasks with `"status": "pending"` that touch the same domain or file type
3. For each matching task, add a one-line warning to the prompt:
   ```
   ⚠️ DO NOT [short version of mistake] — see CLAUDE.md DO NOT list.
   ```
4. If no pending tasks are likely to trigger this mistake, skip this step.

## Step 6: Commit
```bash
git add CLAUDE.md .cursorrules
# Only if .sprint-backlog.json was changed:
git add .sprint-backlog.json
git commit -m "docs: add DO NOT rule — [one-line summary of mistake]
.--. .- .-. ... . -.-."
```

## Step 7: Confirm
Output:
- **Rule added:** [the full DO NOT rule]
- **CLAUDE.md updated:** ✅
- **.cursorrules updated:** ✅
- **Backlog prompts updated:** [N tasks updated / 0 — no matching pending tasks]
- **This mistake will not happen again.**
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/team-launch.md (4,871 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/team-launch.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised reproduction or use outside of Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: Launch agent team for a feature. Usage: /team-launch (reads next pending feature from .sprint-backlog.json). Level 5+ only.
---

# Team Launch (Parent-Orchestrated)

Sub-agents cannot spawn sub-agents. The parent (you, Claude Code) orchestrates all specialist agents.

## Step 1: Identify the feature
Read `.sprint-backlog.json` and find the next task with `"type": "feature"` and `"status": "pending"` in the current sprint.
If no feature brief found, tell the user and exit.

## Step 2: Mark as in_progress
Set the feature's status to `"in_progress"` in `.sprint-backlog.json`.

## Step 3: Create feature branch
```bash
git stash 2>/dev/null
git checkout dev && git pull origin dev
git checkout -b feature/{feature-id}
git stash pop 2>/dev/null
```

## Step 4: Launch team lead (PLAN ONLY)
First, check if a saved plan exists:
```bash
if [ -f ".team-plans/{feature-id}.json" ]; then
  cat .team-plans/{feature-id}.json
else
  # No saved plan — launch team lead agent
fi
```
If no saved plan: launch the `team-lead` agent with the feature brief. The team lead reads all docs and returns a decomposition plan (JSON with `backend_prompt`, `frontend_prompt`, `test_prompt`, `file_ownership`). It does NOT write code.

**Immediately after receiving the plan, persist it:**
```bash
mkdir -p .team-plans
# Write the team lead's JSON output to .team-plans/{feature-id}.json
```
This ensures the plan survives `/compact` and session restarts.

## Step 5: Launch specialists IN PARALLEL
Using the plan from step 4:
1. Launch `backend-dev` agent (foreground) with `backend_prompt`
2. Launch `frontend-dev` agent (background, run_in_background: true) with `frontend_prompt` — ONLY if there is frontend work
3. Wait for both to complete

## Step 6: Launch test-writer
After backend + frontend are done:
1. Launch `test-writer` agent with `test_prompt`
2. Wait for completion

## Step 7: Simplify
Run `/simplify` on all changed files (`git diff dev --name-only`).
This catches LLM bloat — unused imports, single-use abstractions, defensive code for impossible states, speculative features beyond ACs. Review the diff it produces and accept only changes that reduce complexity without altering behaviour. Run tests after to confirm nothing broke.

## Step 8: Integrate & verify
1. Run: `php artisan test --compact && ./vendor/bin/pint --dirty && ./vendor/bin/phpstan analyse --no-progress --memory-limit=512M`
2. Fix any failures yourself (integration issues from parallel work)
3. Handle any shared files (`routes/api.php` conflicts, `resources/js/router.tsx`, etc.)

## Step 9: Launch code-reviewer
1. Launch `code-reviewer` agent with the full diff (`git diff dev...HEAD`)
2. On FAIL: fix issues and re-review
3. On PASS: proceed

## Step 10: Ship
1. Commit any integration fixes with Parsec separator
2. Push to `origin feature/{feature-id}`
3. Open PR against `dev`

## Step 11: Merge + bookkeeping + compact
1. Merge the PR:
   ```bash
   gh pr merge {pr-number} --merge --delete-branch
   ```
2. Mark the feature done in `.sprint-backlog.json` (set `status: done`, `completed_at: now UTC`), commit and push:
   ```bash
   git add .sprint-backlog.json
   git commit -m "chore: mark {feature-id} done

   .--. .- .-. ... . -.-."
   git push origin dev
   ```
3. Clean up the persisted plan:
   ```bash
   rm -f .team-plans/{feature-id}.json
   ```
4. Compact context:
   ```
   /compact
   ```

## Step 12: Next feature
After compact, loop back to Step 1 and pick the next `"status": "pending"` feature.
If no pending features remain, report sprint complete and prompt: "All features complete. Run `/sprint-close` now."

## If $ARGUMENTS is empty or "status" — show sprint status
Read `.sprint-backlog.json`. Show all features with their status.
If all features are `done`: prompt "All features complete. Run `/sprint-close` now."
Do NOT advance the sprint counter manually — always use `/sprint-close`.

## If $ARGUMENTS is "close" — run sprint-close
Run `/sprint-close` immediately. Do not skip it.

## Key rules
- Backend and frontend run IN PARALLEL (different file ownership per `file_ownership` from team-lead's plan)
- Test-writer runs AFTER both complete (needs the code to exist)
- Code-reviewer runs LAST
- You (parent) handle integration — shared files, merge conflicts, verify suite
- Each specialist commits their own work
- ALWAYS run Step 11 before starting the next feature — never skip merge + compact
- Team-lead plans are persisted to `.team-plans/` — check before re-launching team-lead
- After compact, the SessionStart hook forces `/sprint-status` — this re-orients the agent
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/level-up.md (4,968 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/level-up.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: Promote the project to the next development level (3→4, 4→5, 5→6). Generates the additional infrastructure files required for the new level. Run only after the prerequisites for the target level are met.
---

# Level Up

## Step 1: Identify current level + target
Read `_meta.development_level` from `.sprint-backlog.json`. The target is `current + 1`.

| From | To | What unlocks |
|---|---|---|
| 3 (Manual) | 4 (Parallel Dispatch) | `dispatch.sh` + `review.sh` for parallel agent work |
| 4 (Parallel Dispatch) | 5 (Agent Teams) | `.claude/agents/` with team-lead, backend-dev, frontend-dev, test-writer, code-reviewer; `/team-launch` command |
| 5 (Agent Teams) | 6 (Multi-Worktree Parallel) | `/sprint-dispatch` command; full parallel-worktree execution |

## Step 2: Verify prerequisites
For the target level, confirm:

### To Level 4 (Parallel Dispatch)
- [ ] At least 2 sprints completed at Level 3 with no rework
- [ ] CLAUDE.md and `.ai/guidelines/` are stable (no major churn in the last 2 sprints)
- [ ] DEVIATION_LOG.md is short — patterns are well-established
- [ ] Founder is comfortable reviewing 3-4 PRs in one batch

### To Level 5 (Agent Teams)
- [ ] At least 1 sprint completed at Level 4 shipped clean
- [ ] DECISIONS_LOG.md has been refined — DECISIONS_LOG-driven development is reliable
- [ ] Domain folder structure is stable
- [ ] Architecture is mature enough that team-lead can produce clean decompositions

### To Level 6 (Multi-Worktree Parallel)
- [ ] At least 1 sprint completed at Level 5 shipped clean
- [ ] Cross-domain coupling is well-understood — feature parallelisation is safe
- [ ] CI pipeline is fast (<5 min)
- [ ] All worktrees can run `tmux-work` without conflicts

If prerequisites are not met, **abort** and tell the founder which prerequisite is missing.

## Step 3: Generate infrastructure (target-level-specific)

### Level 3 → 4: Generate dispatch.sh + review.sh
The Phase 12 bootstrap should have already written these. Verify:
```bash
ls -la dispatch.sh review.sh
```
If they're missing, copy them from the framework (the bootstrap.sh build always writes them — they exist for L4 use even when current level is 3).

Make them executable:
```bash
chmod +x dispatch.sh review.sh
```

### Level 4 → 5: Generate .claude/agents/ + /team-launch.md
The Phase 12 bootstrap should have already written these. Verify:
```bash
ls -la .claude/agents/
ls -la .claude/commands/team-launch.md
```
If missing, copy from the framework.

### Level 5 → 6: Generate /sprint-dispatch.md
This is the only file ONLY generated at L6 promotion. Build it now per Framework §11.6 (Level 5 → 6 generation block):
- Reads `.sprint-backlog.json` for pending features in the current sprint
- Plans assignment of features to worktrees b/c/d/q based on domain isolation
- Presents the dispatch plan and waits for founder approval
- Coordinates parallel `/team-launch` runs across worktrees
- Resolves merge conflicts on shared files at integration time

Save to `.claude/commands/sprint-dispatch.md`. (See Framework `## Level 5 → Level 6` section for the full body.)

## Step 4: Update .sprint-backlog.json
```json
"_meta": {
  "development_level": [target_level],
  ...
}
```

## Step 5: Update CLAUDE.md
Update the `## Current Level` section:

For Level 4:
```
Development Level: 4 (Parallel Dispatch)
Pipeline: dispatch.sh launches 3-4 agents in parallel → review.sh summarises → founder merges per batch
Human touchpoint: every PR batch
```

For Level 5:
```
Development Level: 5 (Agent Teams)
Pipeline: Team Lead (JSON plan) → Implementation (5a sequential or 5b parallel per feature) → Test Writer → Code Reviewer
Human touchpoint: one PR review per feature
Sub-agent constraint: only parent session can spawn agents — team lead plans only, never codes
```

For Level 6:
```
Development Level: 6 (Multi-Worktree Parallel)
Pipeline: /sprint-dispatch coordinates parallel /team-launch across 4 worktrees
Human touchpoint: per-sprint sign-off after dispatcher's parallel-merge integration
Constraint: dispatcher (worktree a) coordinates; never works on features itself
```

## Step 6: Commit
```bash
git add CLAUDE.md .sprint-backlog.json
# Plus any newly-generated files (dispatch.sh, review.sh, .claude/agents/, etc.)
git commit -m "chore: promote project from Level [N] to Level [N+1]
.--. .- .-. ... . -.-.

[summary of unlocked capabilities]"
git push origin dev
```

## Step 7: Confirm
Report:
- Promoted from Level [N] to Level [N+1] ✓
- Infrastructure files: [list of new files in place]
- CLAUDE.md updated ✓
- `.sprint-backlog.json` updated ✓
- Next: try the new workflow on the next available task/feature
- If anything goes wrong, run `/level-down` to revert to Level [N]
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/level-down.md (2,804 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/level-down.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: Demote the project to the previous development level. Use when a higher level is causing too much rework, conflicts, or quality issues. Demotion is reversible — promote again later via /level-up.
---

# Level Down

## Step 1: Identify current level + target
Read `_meta.development_level` from `.sprint-backlog.json`. The target is `current - 1`.

If current level is already 3, abort: "Already at minimum level (3 — Manual). Cannot demote further."

## Step 2: Confirm intent
Demotion is a serious step — it usually means the workflow has been failing. Confirm with the founder:
"Are you demoting because the current level is causing rework / merge conflicts / quality issues? (yes/no)"

If yes: continue and document the reason in DEVIATION_LOG.md.
If no: ask what's actually intended; possibly the founder wants to use a different command (e.g. `/sprint-status` or `/sprint-close`).

## Step 3: Capture the reason
The reason for demotion is a strong signal about what's going wrong. Append to `docs/living/DEVIATION_LOG.md`:
```
## Sprint [N] — Level Demotion: [N+1] → [N]

Reason: [founder's stated reason]

Symptoms:
- [list specific symptoms — high rework rate, merge conflicts, quality issues, missed ACs, etc.]

Decision: demote to Level [N] until [condition for re-promotion].
```

## Step 4: Update .sprint-backlog.json
```json
"_meta": {
  "development_level": [target_level],
  ...
}
```

## Step 5: Update CLAUDE.md
Update the `## Current Level` section to reflect the lower level (see `/level-up` for level-specific text).

## Step 6: Disable level-specific infrastructure (do NOT delete)
The infrastructure files (dispatch.sh, .claude/agents/, etc.) remain in place — they're just not used at the lower level. Don't delete them; they'll be needed if you re-promote.

For Level 6 → 5: stop using `/sprint-dispatch`; revert to per-feature `/team-launch`.
For Level 5 → 4: stop using `/team-launch`; revert to per-task `dispatch.sh`.
For Level 4 → 3: stop using `dispatch.sh`; revert to single-agent sequential workflow.

## Step 7: Commit
```bash
git add CLAUDE.md .sprint-backlog.json docs/living/DEVIATION_LOG.md
git commit -m "chore: demote project from Level [N+1] to Level [N]
.--. .- .-. ... . -.-.

Reason: [one-line reason]"
git push origin dev
```

## Step 8: Confirm
Report:
- Demoted from Level [N+1] to Level [N] ✓
- Reason recorded in DEVIATION_LOG.md ✓
- CLAUDE.md updated ✓
- `.sprint-backlog.json` updated ✓
- Continue with the lower-level workflow
- When the conditions for re-promotion are met, run `/level-up`
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/commands/simplify.md (3,848 bytes)"
mkdir -p ".claude/commands"
cat > '.claude/commands/simplify.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd.. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
-->
---
description: Post-implementation cleanup. Spawns three parallel review agents (code reuse, code quality, efficiency) against recently changed files and applies fixes. Does NOT change behaviour — only clarity and structure.
---

# Simplify

Run after `/implement` (and within `/team-launch` Step 7), before `/verify`. Removes LLM bloat. Does NOT change behaviour — only structure.

# Optional focus
If `$ARGUMENTS` is provided (e.g. "focus on dead code removal" or "focus on inlining single-use helpers"), interpret as a hint about which review angle is most relevant. The default is to apply all three angles.

# Step 1: Identify changed files
```bash
CHANGED_FILES=$(git diff --name-only origin/dev..HEAD)
```
If no changes since `dev`, exit with "No changes to simplify."

# Step 2: Run three review angles in parallel

## Angle 1 — Code Reuse
For each changed file:
- Is there an existing helper, Action, or component that already does this? Use it instead.
- Is there a copy-pasted block from elsewhere in the codebase that should be extracted?
- Is there a near-duplicate of an existing component (DataTable, FormField, etc.)? Replace with the canonical one.
- Run `grep` for similar function/class names across the codebase.

## Angle 2 — Code Quality (LLM Bloat)
For each changed file:
- Remove unused imports, variables, function parameters.
- Inline any helper/utility method called only once (unless it significantly aids readability).
- Remove defensive null checks for non-nullable types (check the type signature).
- Remove error handling for impossible scenarios (catches for unthrown exceptions).
- Remove abstraction layers (base class, interface, wrapper) with only one implementation.
- Delete commented-out code — git has history.
- Simplify overly complex conditionals (nested ternaries, 4+ boolean conditions).
- If a variable is assigned then immediately returned, return the expression directly.
- Confirm every file you created is wired into routes/navigation/called by other code.
- Confirm every new line traces to a specific acceptance criterion — remove speculative code.

## Angle 3 — Efficiency
For each changed file:
- N+1 queries — eager-load with `->with(...)`.
- Loops that could be aggregations — use `->sum()`, `->count()`, `withSum`, `withCount`.
- Repeated scans of the same collection — cache the intermediate result.
- Inefficient string operations in loops — use `array_map` + `implode`.
- Unnecessary serialisation/deserialisation — pass the object directly.

# Step 3: Apply fixes
Apply all three angles' findings as edits. Each edit must be:
- Behaviour-preserving (the test suite continues to pass)
- Documented in the commit message body if non-obvious

# Step 4: Run tests
```bash
php artisan test --stop-on-failure
```
Zero failures. If anything breaks, revert the offending edit and continue.

# Step 5: Report
```
Simplify report:
─────────────────
Files reviewed: [N]
Changes applied:
  Code reuse: [N] (e.g. inlined 2 single-use helpers, replaced custom table with DataTable)
  Code quality: [N] (e.g. removed 3 unused imports, simplified 1 conditional)
  Efficiency: [N] (e.g. fixed 1 N+1, replaced 1 loop with withSum)

Tests: ✓ green
Lint: ✓ clean

Diff staged for review. Inspect with: git diff
```

# Constraint
This command never:
- Changes a public method signature
- Renames a class/function/file
- Removes a test
- Changes the API surface
- Changes a database column

It only:
- Removes unused code
- Inlines single-use abstractions
- Replaces duplicate code with existing helpers
- Optimises queries
- Cleans up imports and conditionals
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/agents/team-lead.md (6,952 bytes)"
mkdir -p ".claude/agents"
cat > '.claude/agents/team-lead.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
---
description: Team lead planner for AMIR. Reads feature brief, decomposes into specialist prompts, returns JSON plan. Never writes code.
model: sonnet
effort: high
---

You are the team lead planner for an AMIR feature.

## Your role
Read the feature brief and all relevant docs. Return a JSON decomposition plan.
Do NOT write any code. The parent session orchestrates all specialist agents from your plan.

## Token optimization — CRITICAL
You are the ONLY agent that reads project docs. Specialists do not read docs — they rely entirely on what you embed in their prompts. Embed all relevant conventions, enum values, DECISIONS_LOG entries, and CONTENT_COPY strings verbatim in each specialist prompt. Do NOT tell specialists to "read CLAUDE.md" or "read DECISIONS_LOG.md" — point to specific files they need to modify only. This saves ~100k tokens per feature.

## Before planning
1. Read `CLAUDE.md` fully — all conventions mandatory
2. Read `.ai/guidelines/project-architecture.md`, `data-conventions.md`, `testing-standards.md`
3. Read the relevant flows in `BUSINESS_FLOWS.md` (search by flow ID: I1, J3, K2, etc.)
4. Read `ARCHITECTURE.md` for schema and module boundaries (search by module: Module D, Module J, etc.)
5. Read `DECISIONS_LOG.md` — search for keywords. All entries are LOCKED. Pay special attention to D15-R1 (UUID v7), D16 (MoneyCast), D28 (BelongsToTenant), D30 (Action class), D31 (single-action controllers), D32 (final class), D33 (phone_e164), and the Decision IDs the brief references
6. Read `CONTENT_COPY.md` for UI strings and labels (BM + EN)
7. Read `docs/living/CODEBASE_MAP.md` to verify domain boundaries and existing files
8. Read `docs/living/DATA_MODEL.md` for current schema
9. **If the feature touches one of these domains, also read the matching skill:**
   - Accounting / Transactions / Penyata → `.claude/skills/writing-journal-entries.md`
   - EInvoice / MyInvois → `.claude/skills/writing-myinvois-integration.md`
   - ArRahnu → `.claude/skills/writing-ar-rahnu.md`
   - Pdpa → `.claude/skills/writing-pdpa-handlers.md`
10. List all files to create or modify
11. Separate into backend, frontend, and test file sets — verify zero overlap between backend and frontend

## Return this JSON (nothing else, no preamble)

```json
{
  "implementation_mode": "sequential | parallel",
  "backend_prompt": "Conventions: [embed key rules inline — UUID v7 via HasUuids + Str::uuid7(); timestampsTz always; money as integer cents with MoneyCast::class; Action class pattern under app/Domain/[Name]/Actions/; final class on every class per D32; single-action controllers __invoke per D31; BelongsToTenant trait on tenant-scoped models per D28; phone_e164 VARCHAR(20) per D33]. DECISIONS_LOG: [embed relevant locked entries verbatim — D-IDs only when they apply]. Read these files: [specific list only — never docs]. Implement: [task details with exact enum values embedded; specific Action class names; specific routes; specific schema additions].",
  "frontend_prompt": "Conventions: [embed frontend rules inline — every page registered in router.tsx; navigation link required (sidebar or settings index); DataTable component for all list pages; FormField + Button + StatusBadge components — no raw HTML; design tokens only (no hardcoded colours/spacing); enum values: ExactCase=value; never <form>+type=submit in multi-section forms — use <div>+type=button+onClick]. CONTENT_COPY: [embed relevant strings verbatim — BM and EN entries]. Read these files: [specific list only]. Implement: [task details with specific page paths, specific form fields, specific status displays].",
  "test_prompt": "Conventions: [embed test rules inline — /api/v1/ prefix on all test URLs (Lesson 12-L17); factory dates dateTimeBetween('-2 years', '-3 months') for non-current ranges; AssertsQueryPerformance trait on every integration test with explicit budget; cross-tenant isolation test for tenant-scoped features; real Postgres via RefreshDatabase trait — never SQLite; PII-safe IC numbers via fakeMyIcNumber()]. ACs to cover: [list every AC with exact wording from USER_STORIES.md]. Read these files: [specific list]. Write tests.",
  "skip_code_review": false,
  "file_ownership": {
    "backend": ["app/Domain/[Name]/Actions/...", "app/Domain/[Name]/Models/...", "app/Domain/[Name]/Requests/...", "app/Domain/[Name]/Resources/...", "app/Domain/[Name]/Policies/...", "database/migrations/...", "routes/api.php", "app/Http/Controllers/Api/V1/[Name]/..."],
    "frontend": ["resources/js/Pages/[Name]/...", "resources/js/router.tsx", "resources/js/components/..."],
    "tests": ["tests/Feature/Api/V1/[Name]/...", "tests/Unit/[Name]/..."],
    "shared": ["files needing parent integration — typically routes/api.php, router.tsx, DatabaseSeeder.php"]
  },
  "assumptions": ["anything unclear from docs — enum values not specified, validation rules ambiguous, edge cases not covered in flows"],
  "risks": ["potential file conflicts between backend/frontend due to shared route file", "integration with existing [domain] code — verify [model name] hasn't already been built"]
}
```

Set `"skip_code_review": true` when:
- The feature follows established patterns very closely
- The verification step (`php artisan test && pint --test && phpstan`) passes clean
- No new domain boundaries are crossed

This saves ~50k tokens per feature. Default is `false` for unfamiliar territory.

## AMIR-Specific Conventions to Embed in Every Prompt

These are mandatory in every specialist prompt:

**Backend:**
- UUID v7 PKs via `HasUuids` trait + `newUniqueId(): string => Str::uuid7()->toString();`
- `timestampsTz()` on every table (per D17)
- Money columns: `BIGINT *_cents` + `MoneyCast::class` (per D16)
- Phone columns: `phone_e164 VARCHAR(20)` (per D33)
- Encrypted IDs: `*_encrypted TEXT` + `*_hash CHAR(64)` two-column (per E3)
- Tenant-scoped models: `use BelongsToTenant;` (per D28)
- Status as backed enum, cast in model
- `final class` on every class (per D32)
- Action class pattern: single `execute` method, `DB::transaction`, dispatches event (per D30)
- Single-action controllers: `__invoke` only (per D31)
- API prefix: `/api/v1/`

**Frontend:**
- Component-first: Button, DataTable, FormField, StatusBadge (no raw HTML when component exists)
- Design tokens only: no hardcoded colours/spacing/radius/shadow values
- Enum values match PHP enums exactly
- Every user-facing string uses `__()` (BM + EN parity)
- No `<form>+type=submit` in multi-section forms (Lesson 12-L19)
- Every page wired in router.tsx + has a navigation link

**Tests:**
- Use Pest, not raw PHPUnit
- Real Postgres via RefreshDatabase
- AssertsQueryPerformance trait on every integration test
- Cross-tenant isolation test for tenant-scoped features
- `/api/v1/` prefix on all test URLs (run `grep -rn '"/api/' tests/ | grep -v '/v1/'` — must be empty)
- Faker locale `ms_MY` configured in `tests/Pest.php`
- IC numbers via `fakeMyIcNumber()` helper
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/agents/backend-dev.md (1,763 bytes)"
mkdir -p ".claude/agents"
cat > '.claude/agents/backend-dev.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
---
description: Backend specialist for AMIR. Used in parallel (5b) mode. Receives complete self-contained prompt from parent.
model: sonnet
effort: medium
---

You are a backend specialist for AMIR (Laravel 12 + PHP 8.3 + Postgres 16 + Inertia/React).

You receive a complete self-contained prompt from the parent session. All context is embedded in your prompt — do not attempt to read project docs (`CLAUDE.md`, `DECISIONS_LOG.md`, etc.). The team lead has already extracted everything you need.

## Implement exactly what your prompt specifies.

## Hard constraints (always apply, regardless of prompt)

1. **Parsec separator on every commit**: `.--. .- .-. ... . -.-.` directly after the subject line.
2. **No `Co-Authored-By:`** in commit messages — `pre-commit-guard.sh` will hard-block.
3. **No debug artifacts**: `dd()`, `dump()`, `var_dump()`, `ray()` in committed code — hard-blocked by hook.
4. **`final class`** on every class.
5. **`use BelongsToTenant;`** on every tenant-scoped model.
6. **UUID v7 PKs** via `HasUuids` trait.
7. **Money as cents** in `BIGINT *_cents` columns with `MoneyCast::class`.
8. **All API routes under `/api/v1/`** — never bare `/api/`.

## Before creating any file
Check if it already exists on the branch or was recently merged to dev:
```bash
git log --oneline origin/dev -20
ls [relevant directories]
```
If a file exists, read it and build on it — never overwrite or duplicate. (Lesson 12-L14)

## Workflow
1. Implement what the prompt specifies.
2. Run `php artisan test` after each major step. Zero new failures allowed.
3. Run `./vendor/bin/pint --dirty` to format.
4. Commit with the Parsec separator.

## Do NOT open PRs
The parent handles integration and PR creation. You commit to the feature branch and stop.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/agents/frontend-dev.md (2,470 bytes)"
mkdir -p ".claude/agents"
cat > '.claude/agents/frontend-dev.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
---
description: Frontend specialist for AMIR. Used in parallel (5b) mode. Receives complete self-contained prompt from parent.
model: sonnet
effort: medium
---

You are a frontend specialist for AMIR (Inertia v2 + React 18 + TypeScript + Tailwind 3).

You receive a complete self-contained prompt from the parent session. All context is embedded in your prompt — do not attempt to read project docs (`CLAUDE.md`, `DECISIONS_LOG.md`, `CONTENT_COPY.md`). The team lead has already extracted everything you need.

## Implement exactly what your prompt specifies.

## Hard constraints (always apply, regardless of prompt)

1. **Parsec separator on every commit**: `.--. .- .-. ... . -.-.` directly after the subject line.
2. **No `Co-Authored-By:`** in commit messages — `pre-commit-guard.sh` will hard-block.
3. **No `console.log` / `console.warn` / `console.error` / `console.debug`** in committed code — hard-blocked by hook.
4. **Component-first**: use `<Button>`, `<DataTable>`, `<FormField>`, `<StatusBadge>` from the design system. Never raw HTML when a component exists.
5. **Design tokens only**: no hardcoded colour, spacing, typography, radius, or shadow values. Use `var(--color-primary)`, `text-primary`, `p-4`, etc.
6. **Enum values match PHP enums exactly** — the `post-edit-enum-sync-check.sh` hook will warn at write time. PERMANENT is not FULL_TIME, EARNING is not ALLOWANCE, etc.
7. **Every page wired in router.tsx + has a navigation link** — a page without navigation is unreachable and counts as incomplete.
8. **Every user-facing string uses `__()`** — BM and EN parity. The team lead embedded the relevant strings in your prompt; use them verbatim.
9. **No `<form>` + `type=submit` in multi-section forms** — use `<div>` + `<button type="button">` with explicit `onClick` (per Lesson 12-L19).

## Before creating any file
Check if it already exists on the branch or was recently merged to dev:
```bash
git log --oneline origin/dev -20
ls resources/js/Pages/ resources/js/components/
```
If a file exists, read it and build on it — never overwrite or duplicate. (Lesson 12-L14)

## Workflow
1. Implement what the prompt specifies.
2. Run `npm run build` (or `npm run dev` for verification) — must compile clean.
3. Run `npx prettier --write` on changed files (the format hook does this automatically).
4. Commit with the Parsec separator.

## Do NOT open PRs
The parent handles integration and PR creation. You commit to the feature branch and stop.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/agents/test-writer.md (3,095 bytes)"
mkdir -p ".claude/agents"
cat > '.claude/agents/test-writer.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
---
description: Test specialist for AMIR. Writes feature tests after implementation completes.
model: sonnet
effort: medium
---

You are a test specialist for AMIR.

You receive a complete self-contained prompt from the parent session. All context is embedded in your prompt — do not attempt to read project docs.

## First step (mandatory)
Run `php artisan test` once at the start. Record any pre-existing failures. Only NEW failures you introduce are your responsibility.

## Conventions from your prompt (the team lead has embedded these)

- **Test URLs use `/api/v1/` prefix** — grep for `"/api/` without `/v1/` before finishing:
  ```bash
  grep -rn '"/api/' tests/ | grep -v '/v1/'
  ```
  Any hit is a failure. Fix before committing.
- **Factory dates**: `dateTimeBetween('-2 years', '-3 months')` for non-current data ranges, to avoid proration triggers.
- **Every AC needs at least one test** — match acceptance criteria exactly.
- **Cross-tenant isolation** — for tenant-scoped features, write a test that User A cannot affect User B's data.
- **Query count assertion** — every integration test uses `AssertsQueryPerformance` trait (`assertQueryCountLessThan`, `assertNoDuplicateQueries`, `assertNoSlowQueries`).
- **Real Postgres via RefreshDatabase trait** — never SQLite. AMIR uses Postgres-only features.
- **Faker locale `ms_MY`** for BM-localised content.
- **PII-safe IC numbers via `fakeMyIcNumber()`** helper.
- **Test names describe business outcomes**, not implementation details.

## Test structure (mandatory)
Every feature test follows this:
```php
it('description of business outcome', function () {
    // ARRANGE — set up all prerequisites
    $tenant = Tenant::factory()->create();
    $admin = User::factory()->tenantAdmin($tenant)->create();
    
    // ACT — perform exactly one action
    $response = $this->actingAs($admin)->postJson('/api/v1/...', [...]);
    
    // ASSERT — verify every consequential outcome
    $response->assertStatus(201);
    expect(...)->toHaveCount(1);
    Event::assertDispatched(SomethingHappened::class);
    expect(activity()->latest())->toMatchSomething();
});
```

## What to assert (full list per `.claude/skills/writing-tests.md`)
1. HTTP status code — exact code
2. Response shape — key fields
3. Database state — what was created/updated/deleted
4. Events dispatched
5. Jobs queued (if applicable)
6. Notifications sent (if applicable)
7. Audit log entries
8. What did NOT happen (for rejection cases)

## Hard constraints
- **Parsec separator** on every commit.
- **No `Co-Authored-By:`**.
- **No `dd()` / `dump()` / `var_dump()` / `ray()`** — hard-blocked.
- **Do NOT mock** Eloquent models, Spatie Permission, the audit log, or policies. Use real fakes via factories.

## Workflow
1. Verify pre-existing test count (baseline).
2. Write tests covering every AC in your prompt.
3. Run `php artisan test --filter=<NewTestClass>` to verify each new test passes.
4. Run the full suite once: `php artisan test` — confirm no NEW failures.
5. Commit with the Parsec separator.

## Do NOT open PRs
The parent handles PR creation.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .claude/agents/code-reviewer.md (4,192 bytes)"
mkdir -p ".claude/agents"
cat > '.claude/agents/code-reviewer.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
---
description: Code reviewer for AMIR. Reviews integrated feature branch, issues PASS or FAIL against 28-point checklist.
model: sonnet
effort: high
---

You are a code reviewer for AMIR. Review the full diff against this checklist.

## 28-point review checklist

### Correctness & Conventions (15 points)
- [ ] All CLAUDE.md conventions followed
- [ ] All DECISIONS_LOG.md locked decisions respected (D15-R1, D16, D17, D19, D28, D30, D31, D32, D33, E3 — verify each that applies)
- [ ] No debug artifacts (`dd()`, `console.log`, `var_dump()`, `ray()`)
- [ ] Parsec separator on all commits
- [ ] No `Co-Authored-By:` lines
- [ ] Test URLs use `/api/v1/` prefix (run `grep -rn '"/api/' tests/ | grep -v '/v1/'` — must be empty)
- [ ] No merge conflict markers (run `grep -rn '<<<<<\|>>>>>\|=======' --include='*.php' --include='*.tsx' --include='*.jsx'` — must be empty)
- [ ] Frontend enum values match PHP enums exactly
- [ ] `BelongsToTenant` trait active on all tenant-scoped models
- [ ] Money stored as `BIGINT *_cents` integers, never floats
- [ ] Phone in `phone_e164 VARCHAR(20)` columns (per D33)
- [ ] Encrypted IDs use E3 two-column pattern (`*_encrypted` + `*_hash`)
- [ ] All new pages have router entry AND navigation link
- [ ] Shared `<DataTable>` used for all list pages — no custom tables
- [ ] No `<form>` tags with `type=submit` in multi-section forms

### Quality Gates (5 points)
- [ ] Tests pass: `php artisan test`
- [ ] Lint clean: `./vendor/bin/pint --test`
- [ ] Static analysis clean: `./vendor/bin/phpstan analyse`
- [ ] Frontend build clean: `npm run build`
- [ ] Cross-tenant isolation test present for any tenant-scoped feature

### Code Quality — LLM Bloat Detection (6 points)
- [ ] No unused imports, variables, or function parameters
- [ ] No functions/classes created but never called or instantiated
- [ ] No single-use abstractions (helpers, wrappers, base classes with one child)
- [ ] No defensive code for impossible states (null checks on non-nullable types, catches for unthrown exceptions)
- [ ] No commented-out code or speculative features beyond the acceptance criteria
- [ ] No files scaffolded but never wired into routes, navigation, or called by other code

### Design System Compliance (frontend files only) (2 points)
- [ ] All UI elements use design system components — `<Button>`, `<DataTable>`, `<FormField>`, `<StatusBadge>`. No raw HTML equivalents.
- [ ] Zero hardcoded colour, spacing, typography, radius, or shadow values — all via design tokens.

## AMIR domain-specific reviews

If the diff touches one of these domains, also apply the matching skill checklist:

- **Accounting / Transactions / Penyata** — `.claude/skills/writing-journal-entries.md` "Verification Checklist" section: debit=credit invariant, posted-immutable, period state machine, threshold C16
- **EInvoice / MyInvois** — `.claude/skills/writing-myinvois-integration.md` "Verification Checklist": RM10K rule, 72h cancellation window, schema version, phone format, time zone
- **ArRahnu** — `.claude/skills/writing-ar-rahnu.md` "Verification Checklist": Tawarruq structure (fixed profit margin, no compounding), max tenure (6 months + 6-month extension), weight in milligrams, market rate snapshot
- **Pdpa** — `.claude/skills/writing-pdpa-handlers.md` "Verification Checklist": 21-day DSAR SLA, 72h breach SLA, erasure preserves financial records, PII scrubbing in audit log

## Verdict

**PASS** — issue PASS with a one-line summary if all 28 points clear AND domain-specific checklist (if applicable) clears. The PR is ready to merge.

**FAIL** — issue FAIL with specific file:line references. Group by severity:
- **BLOCKER** — must fix before merge (security, data integrity, broken tests, conventions violations)
- **IMPORTANT** — should fix before merge (performance, missing edge cases, design system violations)
- **SUGGESTION** — does not block merge (clarity, naming, comments)

Format each finding as: `[severity] [file:line] [problem]. Correct: [fix].`

## Note
At Level 5+, auto-merge is handled by the `/team-launch` pipeline (Step 11).
At Level 6, each worktree runs `/team-launch` independently — the dispatcher coordinates merge ordering.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing docs/living/CODEBASE_MAP.md (3,625 bytes)"
mkdir -p "docs/living"
cat > 'docs/living/CODEBASE_MAP.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
  LIVING DOCUMENT — updated after every sprint via /sprint-close. Reflects actual current state of the codebase.
-->

# Codebase Map — AMIR
**Last updated:** Sprint 0 (bootstrap), 2026-05-06
**Status:** STUB — populate as code is written. Read this file before planning or troubleshooting anything.

## How to read this file
Each entry: `path/to/file.php` — what it does, what calls it, what it calls.
Group by domain/module. List in dependency order within each group (Models → Actions → Controllers → Tests).
A new Claude reading this must be able to find any piece of logic from the description alone.

## Sprint-close update instruction
Read before updating: read `docs/living/DEVIATION_LOG.md` first to see what changed this sprint at the architecture level. For every file created, modified, moved, or deleted in this sprint:
- **Created:** add a new entry under the correct domain/module section
- **Modified (purpose changed):** update the description; do not edit the entry if only line counts changed
- **Moved/renamed:** delete old entry, add new entry, note the move in DEVIATION_LOG.md if cross-domain
- **Deleted:** remove the entry; do not leave a tombstone

Every entry must be accurate — agents read this to find logic. Stale entries are worse than no entries.

---

## Domain: Tenant
*(populate as `app/Domain/Tenant/` is built — Sprint S1 creates initial scaffold)*

## Domain: Auth
*(populate as `app/Domain/Auth/` is built)*

## Domain: COA (Chart of Accounts)
*(populate as `app/Domain/COA/` is built)*

## Domain: Journal
*(populate as `app/Domain/Journal/` is built)*

## Domain: Invoice
*(populate as `app/Domain/Invoice/` is built)*

## Domain: Bill
*(populate as `app/Domain/Bill/` is built)*

## Domain: Payment
*(populate as `app/Domain/Payment/` is built)*

## Domain: Bank
*(populate as `app/Domain/Bank/` is built)*

## Domain: Tax (SST + e-Invoice)
*(populate as `app/Domain/Tax/` is built)*

## Domain: Reporting
*(populate as `app/Domain/Reporting/` is built)*

## Domain: Member (Koperasi)
*(populate as `app/Domain/Member/` is built)*

## Domain: Loan (Koperasi)
*(populate as `app/Domain/Loan/` is built)*

## Domain: ArRahnu (Koperasi pawn — Tawarruq)
*(populate as `app/Domain/ArRahnu/` is built)*

## Domain: Notification (WhatsApp + Email)
*(populate as `app/Domain/Notification/` is built)*

## Domain: PDPA
*(populate as `app/Domain/PDPA/` is built)*

## Http Layer
*(populate as `app/Http/Controllers/Api/V1/` controllers are built — all routes prefixed `/api/v1`)*

## Inertia Layer (Web)
*(populate as `app/Http/Controllers/Web/` and `resources/js/Pages/` are built)*

## Jobs & Queue Workers
*(populate as `app/Jobs/` jobs are built — Horizon-managed)*

## Console Commands
*(populate as `app/Console/Commands/` are built)*

## Config Files Modified
*(populate as config customisations are made beyond Laravel defaults)*

## Key Test Files
*(populate as `tests/Feature/` and `tests/Unit/` files are built)*

## Frontend (Inertia/React 18)
*(populate as `resources/js/` files are built — components, pages, layouts, hooks)*

---

**Note on initial state:** This stub was written by `bootstrap.sh` on Day 1. The first real entries will appear after Sprint S00 ships (Demo Sprint). Before Sprint S01, agents may read this and find it sparse — that is expected. Read `ARCHITECTURE.md` for designed structure, this file for actual structure.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing docs/living/DATA_MODEL.md (6,141 bytes)"
mkdir -p "docs/living"
cat > 'docs/living/DATA_MODEL.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
  LIVING DOCUMENT — updated after every sprint via /sprint-close. Reflects actual current schema.
-->

# Data Model — AMIR
**Last updated:** Sprint 0 (bootstrap), 2026-05-06
**Database:** PostgreSQL 16 (Laravel migrations source-of-truth)
**Status:** STUB — populate as migrations are run. Read this file before writing any migration or query.

> Divergences from `ARCHITECTURE.md` are noted inline with `[CHANGED]` or `[ADDED]` tags.
> When in conflict, this file is authoritative — `ARCHITECTURE.md` represents the original design,
> this file represents what was actually shipped.

## Sprint-close update instruction
For every migration run in this sprint:
- **New table:** add a complete table section (columns, indexes, relationships, key queries)
- **New column:** add the row to the existing table — note default and nullability
- **Changed column:** mark with `[CHANGED in Sprint N: was X, now Y]` — do not delete the original line
- **Removed column:** mark with `[REMOVED in Sprint N: reason]` — keep the line for history
- **New index:** add to Indexes section with the query it serves
- **New enum:** add to the Enums section at the bottom

Verify against actual schema via `php artisan db:show` and `\d table_name` in psql.

---

## Database-level conventions (apply to all tables)

- **Primary keys:** UUID v7 (per D15-R1) — column `id UUID NOT NULL DEFAULT uuid_generate_v7()`. Never expose internal numeric IDs.
- **Tenant scoping:** Every domain table has `tenant_id UUID NOT NULL` FK to `tenants.id` (per D28). Models use `BelongsToTenant` trait; queries auto-scoped by `TenantScope`. The only un-scoped tables are `tenants`, `users` (multi-tenant via pivot), `system_*` tables.
- **Money:** All money columns are `BIGINT NOT NULL` storing **cents** (per D14). Never use `DECIMAL` or `FLOAT`. Cast via `App\Casts\MoneyCast` in models. Currency is RM unless explicitly noted.
- **Phone:** All phone columns are named `phone_e164` `VARCHAR(20) NOT NULL` storing E.164 format (per D33). Never `phone VARCHAR(50)` — that's the rejected pre-D33 pattern.
- **NRIC (IC numbers):** Two-column encryption pattern (per E3): `ic_no_encrypted BYTEA NOT NULL` (envelope-encrypted via Vault) + `ic_no_hash CHAR(64) NOT NULL UNIQUE` (HMAC-SHA256 for lookup). Never `ic_number_encrypted` or `ic_number_hash` — those are the rejected names.
- **Timestamps:** `created_at TIMESTAMPTZ NOT NULL DEFAULT now()`, `updated_at TIMESTAMPTZ NOT NULL DEFAULT now()` on every table. Soft-delete tables add `deleted_at TIMESTAMPTZ NULL`.
- **Migrations:** Additive only. Never `DROP COLUMN`, never `ALTER ... TYPE`, never rename. Soft-delete obsolete columns by marking nullable + ignored.

---

## Table: tenants
*(scaffold target: Sprint S1 — Tenant domain. Populate this section when migration is committed.)*

## Table: users
*(scaffold target: Sprint S1)*

## Table: tenant_user
*(scaffold target: Sprint S1 — pivot for multi-tenant access per D28)*

## Table: roles
## Table: permissions
## Table: model_has_roles
## Table: role_has_permissions
*(Spatie Permission v6 tables — scaffold target: Sprint S1)*

## Table: chart_of_accounts
*(scaffold target: Sprint S2 — COA domain)*

## Table: journals
## Table: journal_lines
*(scaffold target: Sprint S3 — Journal domain. Note: `journal_lines.amount_cents` BIGINT, debits are positive, credits are negative; sum must equal zero per `App\Domain\Journal\Actions\PostJournal` invariant.)*

## Table: invoices
## Table: invoice_lines
*(scaffold target: Sprint S4)*

## Table: bills
## Table: bill_lines
*(scaffold target: Sprint S5)*

## Table: payments
## Table: payment_allocations
*(scaffold target: Sprint S6)*

## Table: bank_accounts
## Table: bank_transactions
*(scaffold target: Sprint S7)*

## Table: tax_rates
## Table: tax_codes
## Table: einvoice_documents
*(scaffold target: Sprint S8 — MyInvois integration. State machine: `draft → submitted → validated → cancelled` per writing-myinvois-integration skill)*

## Table: members
## Table: member_share_accounts
## Table: member_savings_accounts
*(scaffold target: Sprint S15 — Koperasi pack)*

## Table: loans
## Table: loan_repayments
*(scaffold target: Sprint S16 — Koperasi pack)*

## Table: ar_rahnu_pawns
## Table: ar_rahnu_tawarruq_legs
*(scaffold target: Sprint S20 — Koperasi pack. Note: 4-leg Tawarruq trail per BNM-MPS 2019 — see `.claude/skills/writing-ar-rahnu.md`)*

## Table: pdpa_dsar_requests
## Table: pdpa_breach_log
## Table: pdpa_consent_log
*(scaffold target: Sprint S25 — PDPA compliance domain. SLA: DSAR 21 days, breach notification 72h per s.12B)*

## Table: notifications
## Table: notification_templates
*(scaffold target: Sprint S10 — WhatsApp + Email notifications)*

## Table: audit_logs
*(scaffold target: Sprint S2 — spatie/laravel-activitylog v4 tables. Configured to record every model change, every login, every API call from `/api/v1/*`)*

## Table: jobs
## Table: failed_jobs
## Table: job_batches
*(Laravel/Horizon defaults — scaffold target: Sprint S1 alongside Horizon install)*

---

## Enums

| Enum Class | Values | Used In | Notes |
|------------|--------|---------|-------|
| *(populate as enums are created)* | | | |

**Enum convention:** PHP backed-enums under `app/Domain/[Name]/Enums/`, frontend mirror as ALL-CAPS string constants in `resources/js/lib/enums.ts`. Sync enforced by `.claude/hooks/post-edit-enum-sync-check.sh`.

---

## Critical query patterns

*(populate as queries are tuned)*

This section captures the queries that hit each table heavily, the index that serves them, and any patterns to avoid (full-table scans, missing indexes, N+1).

---

**Note on initial state:** This stub was written by `bootstrap.sh` on Day 1. The first real entries appear after the first migration ships in Sprint S00 or S01. Until then, treat `ARCHITECTURE.md` Section 6 as the source of truth for designed schema. Migrate that content here as it ships.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing docs/living/API_REFERENCE.md (8,027 bytes)"
mkdir -p "docs/living"
cat > 'docs/living/API_REFERENCE.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
  LIVING DOCUMENT — updated after every sprint via /sprint-close. Reflects actual registered routes.
-->

# API Reference — AMIR
**Last updated:** Sprint 0 (bootstrap), 2026-05-06
**Base URL (production):** `https://api.amir.my/api/v1`
**Base URL (staging):** `https://staging-api.amir.my/api/v1`
**Base URL (dev):** `http://localhost:8000/api/v1`
**Auth:** All routes require `Authorization: Bearer {token}` (Sanctum stateful) unless marked `[public]`. Tenant context resolved from token's bound tenant scope (per D28).
**Status:** STUB — populate as routes are registered.

> **Note on `/api/v1` prefix:** Every route is prefixed `/api/v1`. The `/api` prefix is automatic; the `v1` namespace is added in `routes/api.php` via `Route::prefix('v1')->group(...)`. Pre-commit guard checks for accidental v0/no-version routes.

## Sprint-close update instruction
For every route added, modified, or removed in this sprint:
- **Added:** add a complete entry (request, response, errors, rate limit, story ID)
- **Modified:** update the entry; mark with `[CHANGED in Sprint N]` if the contract changed
- **Removed:** mark with `[REMOVED in Sprint N: reason]` — keep for history

Verify against actual registered routes via `php artisan route:list --path=api/v1`. If any route exists in code but not here — add it. If any route exists here but not in code — remove or mark removed.

---

## Conventions

- **Auth header:** `Authorization: Bearer {sanctum_token}`
- **Tenant context:** resolved from token; no `X-Tenant-Id` header needed
- **Content-Type:** `application/json` for all writes; reads return `application/json`
- **IDs:** all IDs are UUIDs (v7) — 36-char string with hyphens, e.g. `0190a5b8-...`
- **Money:** all amounts are integer cents in JSON, never floats. Frontend formats as RM via `formatMoney()` helper.
- **Dates:** ISO 8601 UTC, e.g. `2026-05-06T08:30:00Z`. Frontend converts to Asia/Kuala_Lumpur for display.
- **Phone:** E.164 format (e.g. `+60123456789`) per D33
- **Pagination:** cursor-based on collection endpoints. Query: `?cursor={uuid}&limit={1-100}`. Response includes `meta.next_cursor`.
- **Errors:** problem+json shape — `{ "type": "...", "title": "...", "status": 422, "detail": "...", "errors": { "field": ["message"] } }`
- **Rate limits:** declared per-route below. Returned headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`.
- **Idempotency:** all writes accept `Idempotency-Key: {uuid}` header. Server caches response for 24h and replays on repeat.

---

## Domain: Auth (public + authenticated)
*(scaffold target: Sprint S1)*

### POST /auth/register `[public]`
*(populate when registration endpoint is built)*

### POST /auth/login `[public]`
*(populate when login endpoint is built)*

### POST /auth/logout
### GET  /auth/me
### POST /auth/refresh
*(populate when implemented)*

---

## Domain: Tenant
*(scaffold target: Sprint S1)*

### GET    /tenants/current
### PATCH  /tenants/current
### GET    /tenants/current/members
### POST   /tenants/current/members/invite
*(populate when implemented)*

---

## Domain: COA (Chart of Accounts)
*(scaffold target: Sprint S2)*

### GET    /coa
### POST   /coa
### GET    /coa/{id}
### PATCH  /coa/{id}
### DELETE /coa/{id}
*(populate when implemented)*

---

## Domain: Journal
*(scaffold target: Sprint S3)*

### POST /journals — create journal entry (immediately posted; balanced debits/credits required)
### GET  /journals — list with cursor pagination
### GET  /journals/{id} — show entry with all lines
*(populate when implemented. Note: journals are immutable once posted — no PATCH or DELETE. Reversal via `POST /journals/{id}/reverse` creates a contra entry per accounting integrity rules.)*

---

## Domain: Invoice
*(scaffold target: Sprint S4)*

### POST   /invoices — create draft
### GET    /invoices — list
### GET    /invoices/{id} — show
### PATCH  /invoices/{id} — edit (drafts only)
### POST   /invoices/{id}/issue — transition draft → issued, fires JournalPostingJob
### POST   /invoices/{id}/cancel — only if no payments allocated
### POST   /invoices/{id}/send — emails PDF to customer
*(populate when implemented)*

---

## Domain: Bill (AP)
*(scaffold target: Sprint S5)*

### POST   /bills
### GET    /bills
### GET    /bills/{id}
### PATCH  /bills/{id}
### POST   /bills/{id}/approve
### POST   /bills/{id}/cancel
*(populate when implemented)*

---

## Domain: Payment
*(scaffold target: Sprint S6)*

### POST   /payments
### GET    /payments
### GET    /payments/{id}
### POST   /payments/{id}/allocate — allocate payment to one or more invoices/bills
*(populate when implemented)*

---

## Domain: Bank
*(scaffold target: Sprint S7)*

### GET  /bank-accounts
### POST /bank-accounts
### GET  /bank-transactions
### POST /bank-transactions/import — CSV/MT940 upload
### POST /bank-transactions/{id}/match — match to invoice/bill/payment
*(populate when implemented)*

---

## Domain: Tax / e-Invoice (MyInvois)
*(scaffold target: Sprint S8)*

### POST /einvoice/submit/{invoice_id} — submit invoice to LHDN MyInvois
### GET  /einvoice/{id}/status — check submission status
### POST /einvoice/{id}/cancel — within 72h cancellation window per LHDN rule
*(populate when implemented. Note: state machine `draft → submitted → validated → cancelled` per `.claude/skills/writing-myinvois-integration.md`. RM10K rule: B2C invoices ≥ RM10,000 must be e-invoiced.)*

---

## Domain: Reporting
*(scaffold target: Sprint S9)*

### GET /reports/profit-loss
### GET /reports/balance-sheet
### GET /reports/trial-balance
### GET /reports/general-ledger
### GET /reports/aged-receivables
### GET /reports/aged-payables
*(populate when implemented)*

---

## Domain: Member (Koperasi)
*(scaffold target: Sprint S15)*

### POST /koperasi/members
### GET  /koperasi/members
### GET  /koperasi/members/{id}
### PATCH /koperasi/members/{id}
### POST /koperasi/members/{id}/contributions
*(populate when implemented)*

---

## Domain: Loan (Koperasi)
*(scaffold target: Sprint S16)*

### POST /koperasi/loans
### GET  /koperasi/loans
### POST /koperasi/loans/{id}/approve
### POST /koperasi/loans/{id}/disburse
### POST /koperasi/loans/{id}/repayments
*(populate when implemented)*

---

## Domain: Ar-Rahnu (Koperasi pawn — Tawarruq)
*(scaffold target: Sprint S20)*

### POST /koperasi/ar-rahnu/pawns
### GET  /koperasi/ar-rahnu/pawns/{id}
### POST /koperasi/ar-rahnu/pawns/{id}/redeem
### POST /koperasi/ar-rahnu/pawns/{id}/auction
*(populate when implemented. Note: 4-leg Tawarruq journal trail must post atomically — see `.claude/skills/writing-ar-rahnu.md`)*

---

## Domain: PDPA
*(scaffold target: Sprint S25)*

### POST /pdpa/dsar — submit data subject access request
### GET  /pdpa/dsar/{id}/status
### POST /pdpa/breach — internal breach reporting
### GET  /pdpa/consent-log
*(populate when implemented. SLA: DSAR 21 days, breach notification 72h per s.12B PDPA 2010 amended 2024)*

---

## Domain: Notifications
*(scaffold target: Sprint S10)*

### GET  /notifications
### POST /notifications/{id}/read
### GET  /notification-preferences
### PATCH /notification-preferences
*(populate when implemented. WhatsApp delivery via WhatsApp Business API; templates require Meta pre-approval.)*

---

## Webhooks (inbound)

### POST /webhooks/myinvois `[public — signed]`
### POST /webhooks/whatsapp `[public — signed]`
### POST /webhooks/stripe `[public — signed]` *(when Stripe added — post-tender)*

*(populate when implemented. All webhook signatures verified before processing.)*

---

**Note on initial state:** This stub was written by `bootstrap.sh` on Day 1. Routes will be added throughout Sprints S01–S25. Until each sprint ships, treat `ARCHITECTURE.md` Section 7 (API design) as the source of truth for designed contracts. Migrate to this file as routes ship.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing docs/living/DOMAIN_GUIDE.md (5,909 bytes)"
mkdir -p "docs/living"
cat > 'docs/living/DOMAIN_GUIDE.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
  LIVING DOCUMENT — updated after every sprint via /sprint-close. Reflects actual implemented business logic.
-->

# Domain Guide — AMIR
**Last updated:** Sprint 0 (bootstrap), 2026-05-06
**Status:** STUB — populate as domain logic is built. Read this file before planning anything that touches existing domains.

> **What goes here:** state machines, business rules, Actions, events, and edge cases — *as implemented*, not as designed. If `ARCHITECTURE.md` and this file conflict, this file wins for runtime behaviour; `ARCHITECTURE.md` represents the original plan.

## Sprint-close update instruction
For every domain touched in this sprint:
- **New Action:** add row to the domain's Actions table
- **Changed Action behaviour:** update the row; mark with `[CHANGED in Sprint N]`
- **New Event/Listener wiring:** update Events & Listeners table
- **Business rule discovered or enforced:** add to Business Rules section
- **Edge case handled in code:** add to Known Edge Cases — describe the scenario and how the code responds

If a new domain was created this sprint, add a complete section using the template below.

---

## Domain template
```
## Domain: [Name]
**Purpose:** [2–3 sentences — what problem it solves, who uses it]
**Owner table(s):** [table names]

### State Machine
[state] → [state]    trigger: [Action or event]    guard: [condition]
[state] → [state]    trigger: [Action or event]
[terminal state]     cannot transition further

### Actions
| Action | What it does | Guards/Validations | Events Fired | Called By |

### Events & Listeners
| Event | Fired When | Listeners | Listener Does |

### Business Rules (implemented)
- [Rule as plain sentence]

### Known Edge Cases
- [Edge case and how code handles it]
```

---

## Cross-domain invariants (always true)

These rules apply across every domain. Encoded in tests under `tests/Feature/Invariants/`.

1. **Tenant isolation:** No query may return rows from a tenant other than the authenticated user's bound tenant. Enforced by `TenantScope` global scope on every model with `BelongsToTenant` trait.
2. **Journal balance:** Every posted journal has `SUM(amount_cents) = 0` across all `journal_lines`. Debits positive, credits negative. Enforced in `App\Domain\Journal\Actions\PostJournal` via DB constraint and Action invariant.
3. **Money type:** No floats anywhere in financial flows. All money is integer cents. Enforced by `MoneyCast` on every money column. Casts throw on float input.
4. **Audit log:** Every state-changing Action calls `activity()->log()` via `spatie/laravel-activitylog v4`. Enforced by code review checklist + `audit_logs` row count assertion in feature tests.
5. **Idempotency on writes:** Every POST/PATCH/DELETE accepts `Idempotency-Key`. Cached responses replayed within 24h window. Enforced by `IdempotencyMiddleware` on `/api/v1/*` write routes.
6. **PDPA — IC numbers:** NRIC values stored only as `ic_no_encrypted` + `ic_no_hash` (per E3). Plain `ic_number` column anywhere is a violation.
7. **MyInvois — RM10K rule:** B2C invoices with `total_cents ≥ 1_000_000` (RM10,000) must be submitted to LHDN MyInvois. Enforced in `App\Domain\Tax\Actions\SubmitToMyInvois` precheck and feature test.

---

## Domain: Tenant
*(scaffold target: Sprint S1 — populate as built)*

## Domain: Auth
*(scaffold target: Sprint S1)*

## Domain: COA (Chart of Accounts)
*(scaffold target: Sprint S2)*

## Domain: Journal
*(scaffold target: Sprint S3 — most critical accounting domain. State machine: `draft → posted → reversed`. Posted journals are immutable per accounting integrity rule.)*

## Domain: Invoice
*(scaffold target: Sprint S4 — state machine: `draft → issued → partially_paid → paid → cancelled`. Issued invoices are immutable except via cancellation; cancellation requires no payment allocations.)*

## Domain: Bill
*(scaffold target: Sprint S5 — state machine: `draft → approved → partially_paid → paid → cancelled`)*

## Domain: Payment
*(scaffold target: Sprint S6 — state machine: `unallocated → partially_allocated → fully_allocated → reversed`)*

## Domain: Bank
*(scaffold target: Sprint S7 — bank reconciliation domain)*

## Domain: Tax (SST + MyInvois)
*(scaffold target: Sprint S8 — see `.claude/skills/writing-myinvois-integration.md` for invariants. State machine for e-invoice submission: `draft → submitted → validated → cancelled`. 72h cancellation window from LHDN validation timestamp.)*

## Domain: Reporting
*(scaffold target: Sprint S9 — read-only domain. All reports are computed from posted journals, never from invoices/bills/payments directly. This guarantees report consistency.)*

## Domain: Notification
*(scaffold target: Sprint S10 — WhatsApp + Email delivery. Templates pre-approved by Meta for WhatsApp Business API.)*

## Domain: Member (Koperasi)
*(scaffold target: Sprint S15)*

## Domain: Loan (Koperasi)
*(scaffold target: Sprint S16)*

## Domain: Ar-Rahnu (Koperasi pawn — Tawarruq)
*(scaffold target: Sprint S20 — Shariah-compliant. See `.claude/skills/writing-ar-rahnu.md` for Tawarruq 4-leg invariant. Per BNM-MPS 2019: profit margin must be fixed at contract inception, never floating.)*

## Domain: PDPA
*(scaffold target: Sprint S25 — see `.claude/skills/writing-pdpa-handlers.md` for SLAs and handler patterns)*

---

**Note on initial state:** This stub was written by `bootstrap.sh` on Day 1. Domain sections will be populated as Sprints ship. Until then, `ARCHITECTURE.md` Section 5 (Domain Boundaries) and `BUSINESS_FLOWS.md` are the source of truth for designed business logic. The cross-domain invariants section above is authoritative from Day 1 and must not be relaxed without an explicit DECISIONS_LOG entry.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing docs/living/INTEGRATION_LOG.md (5,632 bytes)"
mkdir -p "docs/living"
cat > 'docs/living/INTEGRATION_LOG.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
  LIVING DOCUMENT — updated after every sprint via /sprint-close. Reflects actual live integrations.
-->

# Integration Log — AMIR
**Last updated:** Sprint 0 (bootstrap), 2026-05-06
**Status:** STUB — populate as integrations are wired. Read this file before changing anything that touches external services.

> **What goes here:** every third-party service, API, package, and external system the application talks to. *As wired in code*, not as planned. Captures: which classes call each service, required config keys, sandbox/test mode, known quirks, and failure handling.

## Sprint-close update instruction
For every integration touched in this sprint:
- **New integration:** add a complete section using the template below
- **Existing integration with new behaviour:** update relevant fields (new methods called, new config keys, changed failure handling)
- **Removed integration:** mark `[REMOVED in Sprint N: reason]` — keep the section for history

---

## Integration template
```
## [Service Name]
**Status:** Live / Stubbed / Planned
**Purpose:** [what we use it for]
**Package/SDK:** `[package-name]` v[version]
**Config keys required:**
- `[ENV_KEY]` — [what it is, where to get it]

**How it's called:**
- `[Class or Action]` → `[method]` to [do what]

**Sandbox/test behaviour:** [how to test without hitting live]

**Known quirks:**
- [quirk]

**Failure handling:** [behaviour when service is down]
```

---

## Planned integrations for AMIR (status as of Day 1)

| Service | Status | Sprint target | Notes |
|---------|--------|---------------|-------|
| **LHDN MyInvois (e-Invoicing)** | Planned | S08 | Critical for B2B/B2C ≥ RM10,000. v4.6 spec. Sandbox: preprod portal. Cert + key from LHDN Public Key API — see PREFLIGHT_CHECKLIST.md. |
| **WhatsApp Business API** | Planned | S10 | Templates require Meta pre-approval (2–4 week lead time). Notification delivery channel. Use 360dialog or Meta Cloud API — TBD. |
| **AWS S3** | Planned | S01 | Document storage (invoices PDF, receipts, e-invoice XML). Region `ap-southeast-5` (Malaysia) per data residency. |
| **AWS SES** | Planned | S10 | Email delivery (invoice send, password reset, notifications). Domain verification + DKIM required. |
| **AWS KMS** | Planned | S01 | Envelope encryption for `ic_no_encrypted`, `bank_account_number_encrypted` per E3. Key rotation: yearly. |
| **HashiCorp Vault** | Planned | S01 | Alternative to AWS KMS for secret management. **DECISION PENDING** — KMS is simpler if AWS-only stack. |
| **Sentry** | Planned | S01 | Error tracking and performance monitoring. Project: `amir-laravel`, `amir-react`. |
| **Stripe** | Deferred — Post-tender | S30+ | Payment gateway for SaaS subscriptions. Stripe Atlas or local entity TBD. |
| **iPay88 / Billplz / Curlec** | Planned | S30+ | Local Malaysian payment gateways for SaaS subscriptions. Choose 1 of 3 based on rate negotiations. |
| **Google reCAPTCHA v3** | Planned | S01 | Bot protection on `/auth/register`, `/auth/login`. Site key + secret from Google Cloud Console. |
| **GitHub Actions** | Live (Day 1) | — | CI: `composer install`, `php artisan test`, `pint`, `phpstan`. Configured in `.github/workflows/ci.yml`. |
| **Laravel Forge** | Planned | S01 | Production + staging deploy automation. Webhook from `main` and `prod` branches. |
| **Sanctum** | Live (Day 1) | — | API token auth — bundled with Laravel. Stateful (cookie-based for SPA, token for mobile). |
| **Spatie Permission v6** | Live (Day 1) | — | Role/permission management. Tenant-scoped roles. |
| **Spatie ActivityLog v4** | Live (Day 1) | — | Audit log for every model change. `audit_logs` table. |
| **Horizon** | Live (Day 1) | — | Redis queue dashboard at `/horizon`. Auth: super admin only. |
| **Pest** | Live (Day 1) | — | Test framework. See `.claude/skills/writing-tests.md`. |
| **Playwright** | Planned | S05 | E2E browser testing. CI runs against staging URL. |

---

## Integrations live as of Day 1 (bootstrap)

These are scaffolded by `composer create-project laravel/laravel` + bootstrap.sh additional packages. Treat their config as authoritative.

### Laravel framework
**Status:** Live
**Purpose:** Application framework
**Package/SDK:** `laravel/framework` v12.x
**Config keys required:**
- `APP_KEY` — generated by `php artisan key:generate` during bootstrap
- `APP_URL` — set in `.env` per environment
- `DB_*` — Postgres connection

### PostgreSQL 16
**Status:** Live (local) / Planned for staging+prod
**Purpose:** Primary database
**Connection:** Via Laravel's pgsql driver
**Config keys required:**
- `DB_CONNECTION=pgsql`
- `DB_HOST`, `DB_PORT=5432`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`

### Redis
**Status:** Live (local) / Planned for staging+prod
**Purpose:** Cache, queue (Horizon), session store
**Config keys required:**
- `REDIS_HOST`, `REDIS_PORT=6379`, `REDIS_PASSWORD`
- `QUEUE_CONNECTION=redis`
- `CACHE_DRIVER=redis`
- `SESSION_DRIVER=redis`

### Inertia + React 18 + Tailwind 3
**Status:** Live (Day 1)
**Purpose:** SPA frontend
**Package/SDK:** `@inertiajs/react`, `react@18`, `tailwindcss@3`
**Config keys required:** none (build-time only)

---

**Note on initial state:** This stub was written by `bootstrap.sh` on Day 1. Each "Planned" integration above gets a full section when its sprint ships. Treat the table above as the project's integration roadmap until each row is migrated to a full section here.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing docs/living/DEVIATION_LOG.md (4,470 bytes)"
mkdir -p "docs/living"
cat > 'docs/living/DEVIATION_LOG.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
  LIVING DOCUMENT — updated after every sprint via /sprint-close. Records all intentional divergences from plan.
-->

# Deviation Log — AMIR
**Last updated:** Sprint 0 (bootstrap), 2026-05-06
**Status:** STUB — populate as deviations occur. Read this BEFORE assuming a planning doc reflects current reality.

> **How to use this:** If something in the codebase conflicts with `ARCHITECTURE.md`, `USER_STORIES.md`, `BUSINESS_FLOWS.md`, or any planning document — check here first.
> - **Logged here →** the deviation is **intentional**. Trust the code, not the plan.
> - **Not logged here →** it may be a bug. Investigate.
>
> Planning documents are not updated retroactively — they represent the original design at planning time. This log is the bridge between the original plan and what was actually shipped.

## Sprint-close update instruction
For everything built in this sprint that differs from the planning documents:
- Add a complete entry using the template below
- Include the sprint number, what the plan said, what was built, and **why**
- List affected files

If nothing deviated this sprint, add a one-line entry: `## Sprint [N] [date]: no deviations from plan.` Never leave a sprint unrecorded — the absence of an entry is ambiguous (forgot? or no deviations?).

---

## Entry template
```
## Sprint [N] — [Short description of deviation] — [date]
**Type:** Schema change / Feature removed / Feature deferred / Pattern changed / API changed / Scope reduced / New dependency / Other
**Original plan:** [quote or paraphrase from ARCHITECTURE.md / USER_STORIES.md / etc.]
**What was actually built:** [what the code does]
**Why:** [reason — user feedback, technical constraint, founder call, regulatory, dependency unavailable, etc.]
**Impact on other docs:** [list planning docs that are now stale — do NOT update them, just note here]
**Affected files:** [list key files that reflect the deviation]
**Follow-ups:** [what, if anything, needs to happen later — e.g. "revisit when MyInvois v4.7 ships"]
```

---

## Deviations from original plan

### Sprint 0 (bootstrap) — 2026-05-06: no deviations from plan.

The project starts in alignment with all planning documents. Phase 11 produced:
- `SPRINT_PLAN.md` (43 sprints, 605 tasks)
- `ARCHITECTURE.md` (schema, API design, infra)
- `USER_STORIES.md` (stories with ACs)
- `DECISIONS_LOG.md` (D1–D33 + R-revisions)
- `BUSINESS_FLOWS.md`, `UI_UX_SPEC.md`, `SCREEN_BRIEFS.md`, `TEST_SCENARIOS.md`, `CONTENT_COPY.md`, `ANALYTICS_PLAN.md`, `ACCOUNTING_INVARIANTS.md`

The bootstrap scaffold (Laravel 12, Postgres 16, Redis, Inertia/React 18, Tailwind 3, Spatie Permission v6, Sanctum, Fortify, Horizon, ActivityLog v4, Pest, Playwright, Sentry) matches Phase 5 (Technical Decisions) and Phase 6 (Architecture) exactly.

### Future sprints — populate as deviations occur

---

## Common categories to watch for

When deciding whether something is a deviation worth logging, ask: *"Would a new Claude reading USER_STORIES.md or ARCHITECTURE.md be confused by what they find in the code?"* If yes — log it.

**Always log:**
- Schema changes (table renamed, columns added/dropped beyond original migration plan)
- API contract changes (request/response shape, auth requirement, route path)
- Removed features (story shipped without an AC because it was reduced in scope)
- Deferred features (planned for Sprint X, moved to Sprint Y)
- Pattern changes (e.g. moved from Action class to Service class for some reason)
- New dependencies not in `ARCHITECTURE.md` Section 5
- State machine changes (state added, transition added/removed)
- Decision reversals (if D-decision in `DECISIONS_LOG.md` was effectively reversed during build)

**Don't need to log:**
- Implementation details (what method names you chose, how you organised helper functions)
- Refactors that don't change behaviour or external contract
- Bug fixes that align code to the plan (those are *converging*, not deviating)
- Adding tests, fixing typos, formatter changes

---

**Note on initial state:** This stub was written by `bootstrap.sh` on Day 1. The first deviation entry will appear after the first sprint that diverges. Until then, this file should contain only "no deviations" entries — one per sprint.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing docs/living/VELOCITY_LOG.md (4,191 bytes)"
mkdir -p "docs/living"
cat > 'docs/living/VELOCITY_LOG.md' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
<!--
  Parsec Sdn. Bhd. · AMIR
  Generated by the Parsec Sdn. Bhd. AI Development Framework v2
  © 2026 Parsec Sdn. Bhd. All rights reserved.
  Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised projects is prohibited.
  LIVING DOCUMENT — appended after every sprint via /sprint-close. Tracks sprint duration, task duration, agent session metrics.
-->

# Velocity Log — AMIR
**Project started:** 2026-05-06
**Status:** STUB — appended to after every `/sprint-close`. Each sprint gets a new section.

> **What this is:** historical data on how long sprints, tasks, and agent sessions actually took. Used to calibrate future estimates and identify framework-level friction.
> **What this is NOT:** a status dashboard for the current sprint. Use `/sprint-status` for that. This file is the long-term ledger.

## How sprint-close populates this file
Each `/sprint-close` appends a new section to the bottom of this file using data collected from `.sprint-backlog.json`:
- Sprint window (started_at → closed_at)
- Per-task duration and agent session count
- Per-session start/end and outcome (verified+committed / failed verification step N / abandoned)
- Insights: slowest task, most common verification failure, total rework count
- Framework improvement notes: what to adjust in `CLAUDE.md`, prompts, or agent definitions

The `session-close-log.sh` hook also appends raw session-end timestamps automatically — that data is reconciled into the structured table at sprint-close.

---

## Section template
```
## Sprint [N] — [date range]
- **Started:** [ISO timestamp]
- **Closed:** [ISO timestamp]
- **Duration:** [d Xh Ym]
- **Tasks completed:** [N]
- **Total task-hours:** [Xh]
- **Rework tasks:** [N]
- **Sprint outcome:** [shipped clean / shipped with rework / partial — N tasks deferred to Sprint N+1]

### Task Breakdown
| Task | Title | Started | Completed | Duration | Sessions | Rework |
|------|-------|---------|-----------|----------|----------|--------|
| ... | ... | ... | ... | ... | ... | Yes/No |

### Agent Session Log
| Task | Session | Worktree | Started | Ended | Duration | Outcome |
|------|---------|----------|---------|-------|----------|---------|
| ... | 1 | za | ... | ... | ... | Verified + committed |
| ... | 1 | zb | ... | ... | ... | Failed verification (Step 4: performance) |

### Sprint Insights
- **Slowest task:** [Task] — [why]
- **Verification failure patterns:** Step [N] failed [N]x, Step [M] failed [N]x
- **Most common rework cause:** [pattern]
- **Velocity trend vs prior sprint:** [faster / slower / same — by what factor]

### Framework Improvement Notes
- [What to adjust in CLAUDE.md, .claude/skills/, prompts, or agent definitions based on this sprint]
- [Tasks where prompts were underspecified — consider rewriting]
- [Verification steps that failed repeatedly — consider adding a CLAUDE.md DO NOT rule]
- [Agent sessions that took disproportionately long — consider clarifying architecture docs]
```

---

## Sprint history

### Sprint 0 (bootstrap) — 2026-05-06
- **Status:** Bootstrap day. No agent work yet.
- **Note:** This is the project's Day 1. The first real sprint entry will be Sprint S00 (Demo) when development begins.
- **Bootstrap duration:** ~30 min — `bootstrap.sh` end-to-end including npm install, composer install, initial commit + push, worktree creation, shell aliases injection.

---

*(Sprint S00, S01, S02, … will be appended here as `/sprint-close` runs at the end of each sprint.)*

---

## Aggregate metrics (updated at every sprint-close)

These metrics span the whole project and are recalculated on every `/sprint-close` from the section data above.

| Metric | Value | Trend |
|--------|-------|-------|
| Sprints shipped | 0 | — |
| Tasks shipped | 0 of 605 (0%) | — |
| Average sprint duration | — | — |
| Average task duration | — | — |
| Average sessions per task | — | — |
| Most-failed verify step | — | — |
| Total rework rate | — | — |

---

**Note on initial state:** This stub was written by `bootstrap.sh` on Day 1. The first sprint section appears at the end of Sprint S00. Until then, this file contains only the bootstrap entry above and the empty aggregate metrics table.
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "  → writing .github/workflows/ci.yml (5,702 bytes)"
mkdir -p ".github/workflows"
cat > '.github/workflows/ci.yml' << '__AMIR_BOOTSTRAP_EOF_SENTINEL__'
# ─────────────────────────────────────────────────────────────
# Parsec Sdn. Bhd. · AMIR
# Generated by the Parsec Sdn. Bhd. AI Development Framework v2
# © 2026 Parsec Sdn. Bhd. All rights reserved.
# Internal use only. Unauthorised use outside Parsec Sdn. Bhd.-authorised
# projects is prohibited.
# ─────────────────────────────────────────────────────────────
# CI workflow — agent-assisted 4-branch model
# Triggers on push to: main, dev, staging, prod
# Triggers on PR targeting: main, dev
# Steps: composer install → key:generate → migrate:fresh → test → pint --test → phpstan
# main merge → Forge webhook → staging deploy
# prod merge → Forge webhook → production deploy
# dev and staging: human-managed promotion only — no auto-deploy

name: CI

on:
  push:
    branches: [main, dev, staging, prod]
  pull_request:
    branches: [main, dev]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read

env:
  PHP_VERSION: '8.3'
  NODE_VERSION: '22'

jobs:
  test:
    name: PHP ${{ matrix.php }} · Postgres ${{ matrix.postgres }}
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        php: ['8.3']
        postgres: ['16']

    services:
      postgres:
        image: postgres:${{ matrix.postgres }}
        env:
          POSTGRES_USER: amir
          POSTGRES_PASSWORD: amir
          POSTGRES_DB: amir_test
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PHP ${{ matrix.php }}
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mbstring, intl, pgsql, redis, bcmath, gd, sodium, openssl
          coverage: none
          tools: composer:v2

      - name: Setup Node ${{ env.NODE_VERSION }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Cache composer dependencies
        uses: actions/cache@v4
        with:
          path: vendor
          key: composer-${{ runner.os }}-${{ matrix.php }}-${{ hashFiles('composer.lock') }}
          restore-keys: |
            composer-${{ runner.os }}-${{ matrix.php }}-

      - name: Install composer dependencies
        run: composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader

      - name: Install npm dependencies
        run: npm ci

      - name: Build frontend assets
        run: npm run build

      - name: Copy .env
        run: cp .env.example .env

      - name: Generate APP_KEY
        run: php artisan key:generate

      - name: Configure CI env
        run: |
          {
            echo "APP_ENV=testing"
            echo "DB_CONNECTION=pgsql"
            echo "DB_HOST=127.0.0.1"
            echo "DB_PORT=5432"
            echo "DB_DATABASE=amir_test"
            echo "DB_USERNAME=amir"
            echo "DB_PASSWORD=amir"
            echo "REDIS_HOST=127.0.0.1"
            echo "REDIS_PORT=6379"
            echo "QUEUE_CONNECTION=sync"
            echo "CACHE_DRIVER=redis"
            echo "SESSION_DRIVER=redis"
            echo "MAIL_MAILER=log"
          } >> .env

      - name: Run migrations (fresh DB)
        run: php artisan migrate:fresh --seed --force

      - name: Run Pest test suite
        run: php artisan test --stop-on-failure --compact

  lint:
    name: Lint · Pint
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ env.PHP_VERSION }}
          tools: composer:v2

      - name: Cache composer dependencies
        uses: actions/cache@v4
        with:
          path: vendor
          key: composer-${{ runner.os }}-${{ env.PHP_VERSION }}-${{ hashFiles('composer.lock') }}

      - name: Install composer dependencies
        run: composer install --prefer-dist --no-interaction --no-progress

      - name: Run Pint (style check, no auto-fix)
        run: ./vendor/bin/pint --test

  static-analysis:
    name: Static Analysis · PHPStan
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ env.PHP_VERSION }}
          tools: composer:v2

      - name: Cache composer dependencies
        uses: actions/cache@v4
        with:
          path: vendor
          key: composer-${{ runner.os }}-${{ env.PHP_VERSION }}-${{ hashFiles('composer.lock') }}

      - name: Install composer dependencies
        run: composer install --prefer-dist --no-interaction --no-progress

      - name: Run PHPStan
        run: ./vendor/bin/phpstan analyse --no-progress --memory-limit=512M

  frontend-typecheck:
    name: Frontend · TypeScript
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node ${{ env.NODE_VERSION }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install npm dependencies
        run: npm ci

      - name: TypeScript check
        run: npm run typecheck

      - name: ESLint
        run: npm run lint
__AMIR_BOOTSTRAP_EOF_SENTINEL__

echo "→ Making scripts executable..."
chmod +x ".claude/hooks/post-edit-enum-sync-check.sh"
chmod +x ".claude/hooks/post-edit-format.sh"
chmod +x ".claude/hooks/post-edit-tenant-scope-check.sh"
chmod +x ".claude/hooks/pre-commit-guard.sh"
chmod +x ".claude/hooks/session-close-log.sh"
chmod +x "dispatch.sh"
chmod +x "review.sh"
chmod +x "tmux-work"
echo "✓ Scripts are executable"



# ── Step 9: Copy .sprint-backlog.json from sibling location ──
say "Copying .sprint-backlog.json from bootstrap package..."
cp "$SPRINT_BACKLOG_SOURCE" .sprint-backlog.json
SPRINT_BACKLOG_BYTES="$(wc -c < .sprint-backlog.json | tr -d ' ')"
ok ".sprint-backlog.json copied (${SPRINT_BACKLOG_BYTES} bytes)"



# ── Step 10: Install dependencies ────────────────────────────
say "Installing composer dependencies (this takes 2-3 minutes)..."
composer install --prefer-dist --no-progress
ok "Composer dependencies installed"

say "Installing npm dependencies (this takes 1-2 minutes)..."
npm install
ok "npm dependencies installed"

# Install AMIR-specific packages
say "Installing AMIR-specific packages..."
composer require \
  spatie/laravel-permission:^6.0 \
  spatie/laravel-activitylog:^4.0 \
  laravel/horizon \
  laravel/sanctum \
  laravel/fortify \
  inertiajs/inertia-laravel \
  sentry/sentry-laravel \
  --no-progress

composer require --dev \
  pestphp/pest \
  pestphp/pest-plugin-laravel \
  larastan/larastan \
  laravel/pint \
  --no-progress

# Frontend packages
npm install \
  @inertiajs/react \
  react@18 \
  react-dom@18 \
  @types/react \
  @types/react-dom \
  tailwindcss@3 \
  @headlessui/react \
  lucide-react

npm install -D \
  @vitejs/plugin-react \
  typescript \
  prettier

ok "AMIR-specific packages installed"



# ── Step 11: Environment setup ───────────────────────────────
say "Setting up .env..."
[[ -f .env ]] || cp .env.example .env
php artisan key:generate

# AMIR-specific env additions (placeholders)
{
  echo ""
  echo "# ── AMIR-specific (set these before running) ──"
  echo "ANTHROPIC_API_KEY="
  echo "AWS_KMS_KEY_ID="
  echo "AWS_S3_BUCKET=amir-${YEAR}"
  echo "AWS_DEFAULT_REGION=ap-southeast-5"
  echo ""
  echo "# MyInvois (Sprint S08)"
  echo "MYINVOIS_CLIENT_ID="
  echo "MYINVOIS_CLIENT_SECRET="
  echo "MYINVOIS_BASE_URL=https://preprod-api.myinvois.hasil.gov.my"
  echo ""
  echo "# WhatsApp Business API (Sprint S10)"
  echo "WHATSAPP_API_TOKEN="
  echo "WHATSAPP_PHONE_NUMBER_ID="
  echo ""
  echo "# Sentry"
  echo "SENTRY_LARAVEL_DSN="
  echo ""
  echo "# Feature flags"
  echo "AMIR_DEMO_MODE=false"
  echo "AMIR_KOPERASI_PACK=true"
} >> .env

# Update .gitignore
{
  grep -q "^\.env\$" .gitignore 2>/dev/null || echo ".env"
  echo ".claude/review-logs/"
  echo ".team-plans/"
  echo "logs/"
  echo "*.log"
} >> .gitignore

ok ".env configured (review and fill in API keys before running)"



# ── Step 13: Initial commit ──────────────────────────────────
say "Creating initial commit..."

git add -A

# Verify there's something to commit
if git diff --cached --quiet 2>/dev/null; then
  warn "Nothing to commit (already on initial commit?)"
else
  git commit -m "chore: initial AMIR project bootstrap — Parsec AI Development Framework v2
.--. .- .-. ... . -.-.

Stack: Laravel 12 + PHP 8.3 + PostgreSQL 16 + Redis + Inertia/React 18
Domains: Tenant, Auth, COA, Journal, Invoice, Bill, Payment, Bank, Tax,
         Reporting, Member, Loan, ArRahnu, Notification, PDPA
Development level: 3 (Manual) — promotes to L4/L5 after Sprint 1 retrospective
Sprint backlog: 605 tasks across 43 sprints (demo S00 + production S01-S42)
Bootstrap: Parsec Sdn. Bhd. AI Development Framework v2"
  ok "Initial commit created on main"
fi

# Create dev, staging, prod branches (4-branch agent-assisted model)
say "Creating branches..."
git checkout -b dev 2>/dev/null || git checkout dev
ok "Branch: dev"

git checkout -b staging 2>/dev/null || git checkout staging
ok "Branch: staging"

git checkout -b prod 2>/dev/null || git checkout prod
ok "Branch: prod"

git checkout dev    # land on dev (where development happens)
ok "Switched to dev (development branch)"



# ── Step 14: Push or print instructions ──────────────────────
if [[ "$PUSH_REMOTE" == "true" ]]; then
  say "Pushing branches to origin..."

  # Verify remote exists
  if ! git remote get-url origin &>/dev/null; then
    warn "No 'origin' remote configured. Add it with: git remote add origin git@github.com:$GITHUB_ORG/$PROJECT_NAME.git"
  else
    git push -u origin main
    git push -u origin dev
    git push -u origin staging
    git push -u origin prod
    ok "Pushed main, dev, staging, prod to origin"
  fi
else
  cat <<EOF

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
${BOLD}Next steps — connect to GitHub:${NC}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  1. Create a GitHub repo:
       https://github.com/new
       Name: $PROJECT_NAME
       Owner: $GITHUB_ORG (or your GitHub username)
       Visibility: Private
       Initialize: leave EVERYTHING unchecked (no README, no .gitignore, no license)

  2. Connect the remote (run from this directory: $TARGET_DIR):
       git remote add origin git@github.com:$GITHUB_ORG/$PROJECT_NAME.git

  3. Push all branches:
       git checkout main    && git push -u origin main
       git checkout dev     && git push -u origin dev
       git checkout staging && git push -u origin staging
       git checkout prod    && git push -u origin prod
       git checkout dev

  4. Configure branch protection (GitHub → Settings → Branches → Add rule):
       Protected branches: main, staging, prod
       Required: Require PR before merging, Require CI to pass, Require linear history
       (dev is unprotected — agents commit feature branches here, then PR to main)

  5. Add Forge webhooks (if using Forge):
       main merge → staging deploy
       prod merge → production deploy

EOF
  ok "Push instructions printed"
fi



# ── Step 15: Create worktrees ────────────────────────────────
say "Creating agent worktrees..."

# git worktree requires at least one commit — Step 13 ensured this
# Worktrees are created sibling to TARGET_DIR
WORKTREE_BASE="$(dirname "$TARGET_DIR")/$PROJECT_NAME"

cd "$TARGET_DIR"

git worktree add -b agent/b "${WORKTREE_BASE}-b" dev 2>&1 | grep -v "Preparing worktree" || true
git worktree add -b agent/c "${WORKTREE_BASE}-c" dev 2>&1 | grep -v "Preparing worktree" || true
git worktree add -b agent/d "${WORKTREE_BASE}-d" dev 2>&1 | grep -v "Preparing worktree" || true
git worktree add -b agent/q "${WORKTREE_BASE}-q" dev 2>&1 | grep -v "Preparing worktree" || true

ok "Worktrees created: ${WORKTREE_BASE}-b, -c, -d, -q"



# ── Step 16: Inject shell aliases ────────────────────────────
say "Adding shell aliases to ~/.zshrc..."

ZSHRC="$HOME/.zshrc"
[[ -f "$ZSHRC" ]] || touch "$ZSHRC"

add_alias() {
  local alias_line="$1"
  if ! grep -qF "$alias_line" "$ZSHRC" 2>/dev/null; then
    echo "$alias_line" >> "$ZSHRC"
  fi
}

WORKTREE_BASE="$(dirname "$TARGET_DIR")/$PROJECT_NAME"

add_alias "# AMIR aliases (added by bootstrap.sh)"
add_alias "alias za=\"cd $TARGET_DIR && claude\""
add_alias "alias zb=\"cd ${WORKTREE_BASE}-b && claude\""
add_alias "alias zc=\"cd ${WORKTREE_BASE}-c && claude\""
add_alias "alias zd=\"cd ${WORKTREE_BASE}-d && claude\""
add_alias "alias zq=\"cd ${WORKTREE_BASE}-q && claude\""

# Try to source — but warn user if it fails (interactive subshells only)
if [[ -n "${ZSH_VERSION:-}" ]]; then
  source "$ZSHRC" 2>/dev/null || true
fi

ok "Aliases added: za, zb, zc, zd, zq (open a new terminal to use them)"



# ── Done summary ─────────────────────────────────────────────
cat <<EOF

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
${GREEN}${BOLD}✓ Bootstrap complete — $PROJECT_DISPLAY${NC}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

What was created:
  ✦ Laravel 12 project scaffolded at: $TARGET_DIR
  ✦ 15 AMIR domain folders under app/Domain/
  ✦ 51 framework files written (.claude/, CLAUDE.md, DEVELOPER_GUIDE.md, etc.)
  ✦ .sprint-backlog.json — 605 tasks across 43 sprints (S00–S42)
  ✦ docs/living/ — 7 living document stubs ready
  ✦ .github/workflows/ci.yml — agent-assisted 4-branch CI
  ✦ Initial commit on main; dev, staging, prod branches created
  ✦ Branch protection: configure on GitHub for main, staging, prod
  ✦ 4 agent worktrees created (${WORKTREE_BASE}-b, -c, -d, -q)
  ✦ Shell aliases added to ~/.zshrc (za, zb, zc, zd, zq)

${BOLD}Before you start coding:${NC}
  1. Open ${YELLOW}PREFLIGHT_CHECKLIST.md${NC} — actionable third-party setup with
     deadlines (some items have 7-14 day lead times)
  2. Fill in API keys in ${YELLOW}.env${NC} (ANTHROPIC_API_KEY at minimum)
  3. Set up local PostgreSQL 16 + Redis 7 (Docker compose works)
  4. Run: ${YELLOW}php artisan migrate:fresh --seed${NC} to verify DB connection

${BOLD}Start Sprint S00 (Demo Sprint):${NC}
  ${YELLOW}za${NC}                   ← if not found, open a new terminal tab first
  ${YELLOW}/sprint-status${NC}      ← see Sprint S00 tasks (92 demo tasks)
  ${YELLOW}/plan D1.01${NC}         ← plan the first task

${BOLD}Tender deadline: 21 May 2026 (16 days from today).${NC}
${BOLD}Sprint S00 (Demo) is the SKM tender ammunition. Ship clean.${NC}

Happy building. 🚀
EOF
