Skip to content
GRPNR.

12 — PRD: work-ledger

Brief section 15 (pm:write-prd). Requirements, MVP scope, acceptance criteria, kill criteria for work-ledger — one thin Encore.ts service in the internal plane of monorepo-development. This PRD assumes the BUILD-THIN decision (00-SYNTHESIS.md verdict #1, 11-build-vs-buy.md score 4.53) and specifies what to build and how we’ll know it worked — not whether. Evidence tiers and the advisory-board challenges that shaped scope live in 02-research.md; ground-truth IDs [E1]–[E10] in plans/001-factory-apps-validation/01-deep-dive.md.

Altitude: requirements, not schema. The canonical data model, enum values, and endpoint contracts are 10-architecture.md’s call; where this PRD names a state or column it defers to that doc. Confidence tags per house convention: confirmed / supported / hypothesis / assumption / unknown.

MVP definition used here: the smallest credible product that puts the riskiest assumptions under real fleet traffic — not a shrunk copy of the full vision. The riskiest assumptions are agent-usability of the claim API unassisted, and Tomas’s trust in the disposition read (both untested), not claim atomicity (feasibility — over-proven across four research passes).


1. Executive summary

work-ledger is the durable system-of-record for Robert’s legacy-rebuild fleet: one Postgres table set behind a typed Encore service where every unit of rebuild work has, at all times, exactly one claimant, one execution state, one permanent disposition, and one attributed cost. Agents are the primary users and reach it only by API; two humans (Robert operates, Tomas reads at kill gates) read it via Grafana/SQL. There is no UI, ever.

It exists because the failure mode is already in the estate — two half-finished stranglers rotting in code at 1-developer scale [E6] — and becomes fatal at 10→64 concurrent agents: double-spent tokens, merge collisions, silently half-done work, and un-attributable cost. A June-2026 arXiv preprint quantifies that a bare lock queue reproduces the highest redundant-rework rate of any tested condition and that git-native file trackers silently drop concurrent field writes [priorArt digest §2/§4] — so the fix is not “a lock,” it is a DB-enforced state machine + durable attempt/disposition record. No 2026 product covers the requirement bundle (atomic claim at 64-concurrent + disposition taxonomy + per-unit cost + one-way Temporal projection); the closest analog, OpenAI’s Symphony, ships without all three and its authors declined to productize it [competitive digest §b].

Scope discipline is the whole product. The durable table gets built regardless — the fleet monitor (Robert’s committed, first-to-ship deliverable) needs the identical per-unit store on day one [temporalCase digest; research/12 §2]. Product-wise, work-ledger ships as the data layer of the fleet-monitor deliverable — same lane, same sprint, no separate roadmap; “work-ledger” names the schema + claim endpoints, not a fourth product. One Encore internal-plane service exists underneath it as a technical necessity (endpoints require registerInternalService), not as a separately-branded product boundary. Promotion to an independently-roadmapped standalone product happens only on a named trigger — a second fleet (Tomas’s) claims through it, or the v2 Temporal projection lands (§20 Q1, §25). This PRD specifies v1 to be days of work, local-first, zero core-plane PRs.


2. Problem statement + evidence

Problem: at fleet scale, unclaimed-and-untracked work destroys trust in the whole rebuild program, and the record needed to defend a kill-gate decision does not survive where execution truth lives.

# Sub-problem Evidence Tier
PB1 Two concurrent agents claim the same unit → double token spend + merge collision arXiv:2606.19616 (advisory-lease-only = highest redundant-rediscovery, 180 instances); race-to-close leaves no git trace confirmed source, hypothesis-grade finding (different system, untested here) [priorArt §2]
PB2 A locks-only queue does not solve claim-before-work — it stops collision, not re-doing finished work same preprint; closes open question #1 of 01-initial-recommendation.md confirmed source, hypothesis-grade finding (different system, untested here) [priorArt §4]
PB3 Disposition + cost + evidence must be readable months after a unit closes; 72h is the operate-skill’s namespace-create example — nothing is provisioned yet and retention is an explicit tuning knob, but the structural argument stands at any realistic setting: closed workflows are purged from persistence and visibility, and Archival keeps replay-only blobs, not a queryable table temporal-operate SKILL; Temporal docs confirmed [temporalCase B1]
PB4 Abandoned migrations become invisible; the estate already carries two rotting half-stranglers (VIS3→legacy VIS; identity-service abandoned 2022) 26-repo / 1.05M-LOC legacy sample confirmed [E6]
PB5 No off-the-shelf product covers the bundle; external SaaS trackers are a documented 2026 anti-pattern as a 64-concurrent claim substrate (GitHub 5k req/hr, 8.4× overrun) four research passes converge confirmed [competitive §b; oss; priorArt §3]
PB6 Cost accrues with no per-unit attribution once the session ends OTel carries live per-worktree cost but nothing durable past 72h supported [research/12 §2; temporalCase]

Steelman against building (carried, not buried): the durable core is ~1 table + ~150 LOC + 2 views that the monitor needs anyway, so a separate named service may be redundant packaging [Prosecutor case; advisory board — Jobs/Christensen]. Resolution: 11-build-vs-buy.md — agents need the SKIP LOCKED guard wrapped in a typed, full-state-returning endpoint (07-ux-ui.md §3), which in Encore requires registerInternalService; the divergence from the “no service” camp is naming/packaging, not build/no-build. The fold-into-monitor path stays a first-class kill/fold trigger (§25), not a dismissed objection.


3. Goals / Non-goals

Goals

# Goal Measured by
G1 No unit of work is ever in an ambiguous state (unclaimed-but-being-worked, double-claimed, closed-without-disposition) Zero double-claims (hard invariant); anti-metric “units claimed never transitioned” (§15)
G2 Every terminal unit carries a disposition + cost + evidence link that survives Temporal retention and is readable at M4/M8/M12 North Star: verified throughput (§15); weekly reconciliation audit pass rate
G3 An agent goes from base-URL + token to a claimed unit in one HTTP call, guided only by error strings (no docs site) Time-to-first-value < 1 min, self-tested in the prototype (07-ux-ui.md §11)
G4 Ships in days, local-first, zero core-plane PRs, no billing-gate dependency Walking skeleton on encore run :4001 [10-arch §11]

Non-goals (aggressive — each is a standing decision, not a backlog item)

# Non-goal Why Escape hatch
N1 No UI / kanban / board / comments in v1 Grafana + SQL is the entire human surface; a screen is the “worse Linear two people use” trap One server-rendered read-only table only when a 3rd human needs interactive access without SQL (07-ux-ui.md §8)
N2 No second tenant required Tomas’s fleet claiming through it is upside, not a v1 dependency (open question #3, untested by design) Revisit if his fleet starts claiming domain-migration units
N3 No priorities / sprints / assignee-routing / notifications claim() picks; the monitor alerts; the ledger records. Every commercial product of this shape (Vibe Kanban/Bloop, Terragon) is defunct Never — category-error risk, not a roadmap item
N4 No core-plane PRs in v1 External-claim endpoint + internal-plane-only tables; zero negotiated ask of Tomas Any future ask (tasks TaskType) stays a single explicit PR — never implicit coupling
N5 Never execution truth Temporal is the only execution truth, permanently; work-ledger is a one-way projection + the business fields Temporal purges — not a parallel state machine that could disagree Never — this is the load-bearing invariant (pre-mortem #3; temporalCase)
N6 No evidence-file storage Links only; artifacts live in the harness/R2 Never — PII/PCI stays out of work-ledger by construction (§15)
N7 No embeddings / dedup / rate-limit That is triage’s R&D lane, not the ledger’s Belongs to the triage sibling

4. Product principles

Carried from 10-architecture.md §1 (P1–P7), compressed:

  1. Accounting system, not an app — value is the data model + invariants, not screens.
  2. Temporal is the only execution truth — one-way projection; never writes back.
  3. Affordances at the schema layer — states that cannot be misused (enum + CHECK), claims that cannot race (SKIP LOCKED), errors that name the violated invariant.
  4. One vocabularyunit / claim / attempt / disposition. Never ticket / task (collides with Core’s tasks svc) / issue.
  5. Survive retention — every field Temporal purges at 72h lives durably here, readable at M12.
  6. Thinner than plan 001 — reuse Temporal + workflow_executions + workflow-id uniqueness where units are workflows; build only what Temporal purges + the external-claim path.
  7. Zero core-plane PRs in v1 — local-first on encore run.

5. Personas (agents are first-class)

Full profiles in 03-journey.md. Summary:

Persona Role Path Frequency Stakes
Fleet agent (10→64 concurrent Claude sessions) Claims units, works, transitions, records cost, blocks with questions Write (primary user) — via external-claim API + grpn_ bearer token Constant (sub-second at 64-concurrent) Must never double-claim, must recover from crash mid-unit
Robert Operator: watches queue depth, stall, cost/merged-unit, rework; owns the concurrency ramp Read (Grafana/SQL) + occasional operator writes (register backlog, force-dispose) Daily Fleet legibility; his committed lane
Tomas Read-only stakeholder at kill gates: disposition table with evidence Read (SQL/Grafana table) ~3×/year (M4/M8/M12) Highest-stakes single read; his trust is the moat
Harness / triage / monitor (sibling services) Integrators: write parity results, poll questions, read views Service-account auth (triage/monitor); harness is external, grpn_ bearer token like a fleet agent (§13) Continuous Correct integration without coupling

Commercial personas (marketing segments, buyers) — N/A, internal.


6. Jobs-to-be-done

Job Hirer Frequency
“When I ask for work, give me exactly one unit nobody else is doing, atomically” Fleet agent Constant
“When I crash mid-unit, make my unit reclaimable and don’t silently lose my attempt” Fleet agent On failure
“When I can’t establish parity, park the unit with my question and free the claim” Fleet agent / harness Per block
“Show me if unclaimed work is piling up faster than the fleet finishes it” Robert Daily
“At the kill gate, show me what happened to each component — MIGRATED/DROPPED/DEFERRED/DEAD, with evidence — and let me trust it without re-verifying” Tomas Per gate

7. Journey summary

Full maps in 03-journey.md (8-stage internal-adoption journey + 5 failure paths). The load-bearing “purchase” analog is the A1 lane negotiation (Tomas grants a CODEOWNERS-partitioned lane) — the actual go/no-go on placement, distinct from Tomas’s separate read-only role at gates. Steady-state loop:

flowchart LR
  REG[register backlog] --> READY[unit: ready]
  READY -->|claim SKIP LOCKED| WORK[agent works in worktree<br/>heartbeat extends lease]
  WORK -->|parity miss| BLOCK[blocked + question → triage]
  BLOCK -->|answer written back| WORK
  WORK -->|parity pass| VERIFIED[verified + evidence link]
  VERIFIED -->|dispose| DONE[MIGRATED / DROPPED / DEFERRED / DEAD + cost]
  WORK -->|agent dies, lease expires| READY
  DONE --> VIEW[(SQL views → Grafana / Tomas kill-gate read)]

8. User-stories summary

Full set in 04-user-stories.md (26 stories US-00…US-25 + 7 rejected). Critical path: US-00 → US-01 → US-02 → US-04 → US-09 → US-16 → US-17 (claim → transition → block → stale-takeover → external-auth → kill-gate disposition table). All v1 stories route through the external-claim path — no Temporal-modelled units exist yet (repoRecon: only one toy workflow on the internal-plane substrate the stream work-ledger would project from; Core’s merchant-contract-signing workflow runs on Core’s own workers and is irrelevant to the projection point); projection stories are post-MVP.

P0 (ships or MVP doesn’t work): US-00, 01, 02, 04, 05, 06, 07, 09, 15, 16, 17, 22, 23, 24. Rejected outright (traceable to non-goals §3): kanban UI, comments, priorities, notifications, assignee routing, in-ledger embedding dedup, multi-tenant roles.


9. Functional requirements

Each requirement’s acceptance criteria are testable and concrete. Endpoint/enum names defer to 10-architecture.md §3/§6.

F1 — Unit registry (backlog import)

POST /units (batch) registers work units keyed by a unique external_ref (legacy component/capability key). Idempotent register.

  • AC1.1 Registering the same external_ref twice inserts one row; the second call is a no-op returning the existing row (proven by unique-index test on external_ref).
  • AC1.2 A newly registered unit is state='ready', disposition=null, claimed_by=null.

F2 — Atomic claim with lease + heartbeat + stale takeover

POST /units/claim (pull-next, FOR UPDATE SKIP LOCKED) and POST /units/:id/claim (named, conditional UPDATE … RETURNING). Claim carries a lease (lease_expires_at, default ~15 min — calibration knob, not a hardcoded constant); POST /attempts/:id/heartbeat extends it. Pull-next also matches state='claimed' AND lease_expires_at < now() (stale reclaim, attempt_no++, stale_reclaimed event).

  • AC2.1 Two concurrent claims on one ready unit: exactly one 200 (claimed row returned), exactly one 409 with error string #1 verbatim — proven by a vitest race test against real Docker Postgres (new infra; no house pattern to copy — repoRecon §5).
  • AC2.2 N concurrent claimers vs M<N ready units → exactly M distinct units claimed, none twice (same test, parameterized).
  • AC2.3 Same agent calling claim() again while holding a unit returns its same held unit, never a second (idempotent-per-agent).
  • AC2.4 A claim whose lease_expires_at is in the past is reclaimable by pull-next; reclaim bumps attempt_no and appends a stale_reclaimed event (the dead agent’s work leaves a trace) [US-06, US-09].
  • AC2.5 A named claim on a live-held unit returns 0 rows → 409 with error string #1 (US-07).

F3 — Transitions with DB-enforced state machine

POST /units/:id/transition moves a unit per the enum-guarded allow-map (10-architecture.md §3 state diagram). Illegality is rejected by the DB (UPDATE … WHERE state=$expected → 0 rows), not an app if.

  • AC3.1 An illegal transition (e.g. blocked → verified directly) returns error string #2 verbatim naming the violated invariant + legal next states — not a generic 500 (US-08).
  • AC3.2 A transition by an agent not holding the active claim returns error string #3 (permissionDenied) (US-02 edge).
  • AC3.3 transition → verified without evidence_url returns error string #4 (invalidArgument).
  • AC3.4 Every write returns the complete current row (full-state feedback), never {ok:true} (07-ux-ui.md §3.2).

F4 — Block with question

POST /units/:id/block parks a claimed unit → blocked, creates a blocking_questions row (status='open', free-form assigneeGroupSlug default agent-questions), releases the active claim’s work-hold, appends a blocked event.

  • AC4.1 After block, the unit is state='blocked' and one blocking_questions row exists with status='open' and the supplied question text (US-04).
  • AC4.2 The open-question set is pollable by the triage sibling via blocking_questions WHERE status='open' (index-backed) — zero core-plane PR (US-20).
  • AC4.3 unblock writes the answer back → in_progress, sets status='answered', appends unblocked event.

F5 — Dispositions (absorbs the decommission ledger)

Terminal disposition enum MIGRATED | DROPPED | DEFERRED | DEAD set once, immutable, on dispose. Rolls up per unit (~1000 rows); component-tier decisions (~26 repos) stay in reasons-and-decisions/ ADRs, cross-linked not merged [temporalCase concession].

  • AC5.1 dispose sets disposition + terminal state (done/abandoned) + an evidence link; a disposed event is appended (US-05).
  • AC5.2 Attempting to overwrite a set disposition returns error string #5 verbatim (“dispositions are immutable … record a superseding ADR”) — failedPrecondition (US-05 edge).
  • AC5.3 All four dispositions are equally terminal and equally count toward verified throughput — a correctly-diagnosed DEAD unit is as much progress as MIGRATED (09-north-star.md §7).

F6 — Cost entries

cost_cents recorded per attempt (POST /attempts/:id/cost, additive by source_event_id, or folded into transition-to-terminal), rolled to work_units.cost_cents.

  • AC6.1 Recording cost twice with the same source_event_id/Idempotency-Key counts once (no double-count on retry) (US-03).
  • AC6.2 work_units.cost_cents equals the sum of its attempts’ cost_cents after any sequence of recordings (roll-up invariant test).
  • AC6.3 Per-model/per-phase cost breakdown is out of MVP — a single number per attempt satisfies cost/unit + rework views (D8; §20).

F7 — SQL read views (the human “screens”)

Exactly the MVP set, 1:1 with monitor panels: v_queue_depth, v_cost_per_unit (per unit + per merged unit), v_disposition_summary (counts + evidence links — Tomas’s kill-gate table), v_rework (attempt_count > 1).

  • AC7.1 Each view returns correct counts against a seeded fixture (view-level assertion).
  • AC7.2 An empty queue renders as “clear,” not a broken chart; no MIGRATED yet renders “no completions,” not zero (07-ux-ui.md §6 empty-states).
  • AC7.3 v_disposition_summary is drillable to row-level (unit_id, component, disposition, evidence_url, cost_cents, disposed_at) via a saved SQL query / Grafana table — no server-rendered page in v1 (US-17).

F8 — Idempotency + append-only audit

Every write carries an Idempotency-Key header → source_event_id; a replay returns the prior result via the events unique constraint. work_unit_events is append-only (auditlog pattern, no updated_at, keyset index).

  • AC8.1 A replayed transition/block/dispose POST with the same Idempotency-Key produces the same result and no duplicate event row (US-23).
  • AC8.2 No UPDATE/DELETE path exists against work_unit_events (append-only enforced by code-review rule + absence of a repository mutate method).

F9 — Projection from Temporal (POST-MVP, spec’d as the contract)

When fleet units run as internal-plane Temporal workflows, work-ledger projects execution events one-way (event-id dedup via work_unit_events.source_event_id, read-only generated client).

  • AC9.1 (v2) Replaying the same Temporal event stream twice produces transitions once (source_event_id unique blocks the dupe) [T2].
  • AC9.2 (v2) work-ledger never writes execution state back to Temporal or Core workflow_executions (one-way invariant, code-review-enforced + weekly reconciliation query) (US-22).
  • Not in v1 — the interceptor hook is a hardcoded file in Tomas-owned workflow-proxy with only one toy workflow flowing through it; wiring it is a negotiated substrate touch, not a free read (repoRecon §3; 10-architecture.md §5).

10. Non-functional requirements

# NFR Target Test
NF1 Zero double-claims, ever Hard invariant, not an SLO — one occurrence stops the ramp (§24) AC2.1/AC2.2 race test; double-claim counter alert
NF2 Claim latency p99 < 200 ms local (single SKIP LOCKED statement + one attempt insert + one event insert, one tx) measured against Encore dev PG under the race test load
NF3 Append-only audit work_unit_events never mutated/deleted AC8.2
NF4 Restore-from-backup drill A pg_dump → fresh-DB restore reproduces every unit’s current state + full attempt/event history with zero loss; drill run once before first real Orders unit and documented operational drill (§17)
NF5 Concurrency ceiling Correct at 10→64 concurrent; the SKIP LOCKED vacuum/MultiXact pressure point (thousands of workers) is irrelevant here — 1000 = queued units, never concurrent editors [E9] AC2.2 at N=64
NF6 Local-first / no billing gate Full v1 runs on encore run :4001 against Docker PG; cross-plane clients stay lazy (app-ids empty) walking-skeleton deploy
NF7 Simplicity budget v1 = 4 tables + 5 enums + ~150–200 LOC service + 4 views; if a field takes >1 sentence to explain, cut it code review

11. Data requirements

# Requirement Detail
D-1 Retention: the ledger outlives Temporal retention — that is its reason to exist Rows are permanent for the life of the program (through M12+). No TTL, no purge. 72h is the operate-skill’s namespace-create example, not a provisioned value — retention is a tuning knob, but at any realistic setting Temporal purges execution truth; work-ledger’s disposition/cost/evidence must be readable months later [PB3; temporalCase B1]
D-2 One row per unit, forever work_units mutable registry + live claim + current state; unique external_ref
D-3 One row per attempt work_unit_attempts append-friendly; cost + evidence per attempt; attempt_no 1-based; rework = attempt_count > 1
D-4 Append-only event log work_unit_events (auditlog precedent — explicit columns, mode:"string" timestamps, (created_at desc, id desc) keyset index); also the exactly-once projection dedup home
D-5 Free text confined to 3 columns question, answer, evidence_url (+ detail jsonb on events). Everything enumerable is an enum with CHECK
D-6 Evidence = links only evidence_urls array of R2/harness URLs, never payloads (§15)
D-7 Backlog cardinality ~1000 unit rows (Orders beachhead first), ~26 component-tier decisions stay in ADRs

12. Integrations

Integrator Mechanism Direction v1?
Dual-run harness (CLI/CI, outside Encore — addendum #4) transition(verified, evidence=<parity-report-url>) on parity pass, block(question) on miss; report artifact lives in R2, ledger stores the link harness → work-ledger Yes (US-19)
Triage Polls blocking_questions WHERE status='open', dedups/rate-limits, routes to human tasks INFORMATION_REQUEST (assigneeGroupSlug='agent-questions'), writes answer back via unblock work-ledger ↔ triage ↔ Core tasks v1 poll surface yes; tasks-minting v2 (US-20)
Fleet monitor Grafana SQL datasource reads the 4 views directly; OTel carries live per-call cost, work-ledger carries the durable per-unit roll-up — no overlap monitor → work-ledger views Yes (US-21)
workflow-proxy stream One-way projection of internal-plane workflow events (negotiated substrate touch) workflow-proxy → work-ledger Post-MVP (F9)

13. Permissions

Caller Mechanism Source
Non-Encore Claude agents (the v1 reality) Authorization: Bearer grpn_<prefix>_<random>, minted via Core api-tokens (HMAC-SHA256 hash stored, token shown once); validated identically on both planes since commit eada68c “One credential header” — no new mechanism to build repoRecon §6; confirmed
In-cluster services (triage, monitor) Service-account bearer via libs/service-account.ts (internal:<svc> in-memory token), same Authorization header repoRecon §6
Harness (dual-run/parity, runs outside Encore) External caller, same Authorization: Bearer grpn_<prefix>_<random> path as fleet agents — not a privileged in-cluster caller by virtue of being Robert’s own tooling plan 001 architect addendum #4; 04-user-stories.md US-19
Authorization granularity Role/permission check in the controller only (.claude/rules/encore-ts.md), not deeper; claim-holder identity checked on transition/block/dispose (error string #3 on mismatch) 10-arch §9
No new auth surface v1 adds zero auth code — reuses the existing single-header path non-goal N4

14. Analytics & instrumentation

The metric set is 09-north-star.md’s taxonomy; instrumentation is what the schema + views must emit.

Category Metric Instrumented from
North Star Verified throughput = units/week reaching a terminal disposition with both cost and evidence_url populated (rate, not stock) v_disposition_summary + cost/evidence non-null filter
Activation First agent-claimed unit (fleet + per-agent) first successful claim() — not US-00 (04-user-stories.md’s “Activation story”), which is the first unit going claim→work→verified end-to-end: the end-to-end proof gate, a different, later event
Time-to-value Median cycle time disposed_at − claimed_at events
Engagement Daily claiming agents (distinct IDs, ≥1 claim()/day) claim events
Retention-equivalent Weekly reconciliation audit pass rate reconciliation cron (§17) — this is the trust metric
Quality Rework rate (attempt_count > 1) v_rework
Reliability Claim p99 + zero double-claims (hard invariant) latency histogram + double-claim counter
Guardrails Claim→verified conversion; stale-claim rate; ledger coverage % of fleet work events + stall view
Anti-metrics Units claimed never transitioned (“ledger theater”); row growth without dispositions claim-without-follow-up query

Reporting rule: disposition mix is reported alongside verified throughput (stacked, not blended) — DEFERRED is the cheapest disposition to assign, so a rising DEFERRED share is a smell test on the headline number (09-north-star.md §7.1).


15. Security & privacy

Concern Design Sign-off
PII / PCI Evidence is LINKS only — R2/harness URLs, never payloads. Recorded legacy traffic behind those links is PII/PCI
Security-architect sign-off (blocking item) ⚠ REQUIRED before any evidence link may point at raw recorded legacy traffic. Ship the link column; do not populate it with raw-traffic URLs until sign-off. Fixture retention/masking is out of work-ledger’s scope but gates the harness integration open — §17/§20
Prompt leakage OTEL_LOG_USER_PROMPTS stays OFF research/12 §2
Least privilege Controller-only authz; claim-holder check on mutating calls 10-arch §9
Free-text junk Confined to 3 columns; everything else enum+CHECK; risk that an agent dumps reasoning into question/evidence_url is mitigated structurally, monitored via Grafana anomaly UX risk table 07-ux-ui.md §10

Commercial privacy surfaces (consent flows, data-subject requests, retention policy pages) — N/A, internal.


16. Operational requirements

# Requirement Detail
O1 Local-first run pnpm backend:internalencore run :4001 against Encore’s Docker-backed Postgres; no GCP billing gate [E5]
O2 Migrations Generated only via pnpm drizzle (never hand-edited); pnpm drizzle --check proves sync; per-service db/migrations/* committed verbatim (repoRecon §4)
O3 Validate gate pnpm validate (biome + type-check + test) before every commit; no coverage threshold exists in the repo — do not invent one (repoRecon §5)
O4 Weekly reconciliation audit job *.cron.ts: for units with a workflow_id, compare work_units.state vs Core workflow_executions.status (generated client). Any mismatch is an alert, not an auto-fix — recovery is idempotent read-before-write, never an out-of-band manual edit. This is the detection signal for the two-sources-of-truth failure (pre-mortem #3). In v1 (no workflow-modelled units) the job’s scope is the empty set — it lands with projection (F9) but the audit discipline (sampled cost/evidence/disposition spot-check) runs from day one as the retention-equivalent metric
O5 Restore drill NF4 — run once before the first real Orders unit, documented in the decision log
O6 Decision log Append-only reasons-and-decisions/07-decision-log.md, supersede-never-edit; every work-ledger design decision logged (root CLAUDE.md)
O7 LLM run evidence Any manual/batch/offline LLM run on the fleet’s behalf logged per the run-evidence rule — but note work-ledger itself makes no model calls (deterministic service, 10-arch §8)

17. Dependencies (works under both A1 and A-lite)

Dep Status If unmet
A1 — Tomas grants a CODEOWNERS-partitioned lane in the internal plane assumption, untested (assumption A1, plans/001-factory-apps-validation/01-deep-dive.md §Assumption log); likelihood Medium A-lite fallback: Robert-owned repo, API-only integration — same schema, only the deploy target changes (D5). The PRD is placement-agnostic by construction; nothing in v1 requires the lane to exist
Encore + Drizzle + Docker PG confirmed available (repo runs)
grpn_ bearer-token auth confirmed current (commit eada68c)
Grafana (local-first, docker-compose) fleet-monitor deliverable; mandated Ships with the monitor; work-ledger only exposes views
Security-architect sign-off open (§15) Evidence-link column ships empty of raw-traffic URLs until granted
Temporal projection substrate not needed v1 Post-MVP; external-claim path is standalone and complete

This PRD is valid whether or not A1 resolves. Placement is a deploy-target decision, not a requirements decision.


18. Constraints

  • Postgres-first, TDD seams, DDD, simplicity — Robert’s constitution [E10].
  • House conventions non-negotiable: layered controller/service/repository, BaseRepository extension, keyset pagination for the append-only table (auditlog precedent), registerInternalService, no relative import climbing >1 level, English-only, no em-dash, every number carries source/window/confidence.
  • No Docker-PG vitest pattern exists in the repo — the atomic-claim concurrency test (AC2.1) is new infrastructure, not a reuse; budget for it (repoRecon §5).
  • Review capacity is the real ceiling — AI-authored PRs wait 4.6× longer, 32.7% acceptance [E8; research/12 §1.7]. work-ledger supplies the verified-throughput numerator; it does not own the review-capacity metric.
  • 64-concurrent hard ceiling — nothing published validates orchestration past 64 [E9].

19. Risks (top 5 — full 18-month set in 13-pre-mortem.md)

# Risk Likelihood Detection Mitigation
R1 The lane never existed — A1 drags, Robert builds nothing for a quarter Medium No OWNERSHIP.md within 2 weeks of the Tomas conversation (research/14) Trigger A-lite immediately — portfolio is placement-agnostic (§17)
R2 Two sources of truth — ledger and Temporal disagree during an incident, trust in both collapses Low–Medium First weekly reconciliation mismatch One-way projection by construction; projection is v2, so v1 cannot drift at all (D4, F9)
R3 Ledger theater — agents claim but the API is too awkward to use unassisted, so real work flows around it Medium <50% of fleet work through claims after 4 weeks; anti-metric climbing Error-strings-as-docs (§9 F3); walking-skeleton usability self-test (§23); kill/fold trigger (§25)
R4 Tomas never reads the disposition table — the audit half is built for an assumed reader Medium No kill-gate read at M4; open question #3 stays untested Get his explicit written yes/no before the disposition schema is treated as final (§20 Q3); the claim half stands alone if he says no
R5 Blind scale past 64 — fleet ramps on the calendar, rework explodes High if ignored [E9] Rework rate vs concurrency curve (v_rework) Ramp 10→50→64 gated on ledger metrics (§24); hard cap until data says otherwise

20. Open questions

# Question Owner Resolve by
Q1 Service vs. fold-into-monitor — does the durable table ship as a standalone, independently-roadmapped work-ledger product or as the data layer of the fleet-monitor deliverable? (The build-vs-buy verdict, 11-build-vs-buy.md Bottom line + 02-research.md §5.3: fold-into-monitor by default — one Encore service exists because endpoints require registerInternalService, but that’s packaging, not product boundary. Promotion to standalone gated on a named trigger: a second fleet claims through it, or the v2 Temporal projection lands) Robert Before v1 deploy; re-evaluated at the 4-week kill gate (§25)
Q2 A1 lane — granted, or A-lite? Tomas / Robert 2 weeks post-research/14
Q3 Does Tomas actually read a disposition table (vs. the ADR log he already reads)? Untested prior — one message, not a doc Tomas Before disposition schema is final
Q4 DEFERRED vs DROPPED — is the distinction operationally real at a kill gate, or should the 4-way enum collapse? (Advisory challenge — Jobs/Ive) Robert / Tomas From first 4 weeks of real disposition data, not decided a priori
Q5 State granularity — 7-state enum (10-arch) vs 5-state (07-ux-ui). If agents conflate tiers after the prototype, collapse — don’t add a doc Robert After walking-skeleton agent test
Q6 Claim TTL / lease duration — not knowable from first principles; set an initial value, watch the stall-list panel, tune Robert Continuous calibration knob
Q7 Has a real collision actually happened at current (~10-agent) scale, or is the 64-concurrent failure mode entirely modeled? Robert Instrument from claim #1; report at 4-week gate

21. MVP scope

MVP = the walking skeleton grown to a complete claim+disposition loop for one domain (Orders), local-first. Not the full vision; not a claim-only spike either — because the disposition/cost half is the reason to own it at all (open question #3) and the monitor forces the same table on day one, so deferring it saves nothing (temporalCase crossover).

In v1 Reason Out of v1 Reason
4 tables + 5 enums (10-arch §3) Irreducible core surviving retention Temporal projection (F9) No workflow-modelled units exist; negotiated substrate touch
Claim (pull-next + named) w/ lease/heartbeat/stale takeover The mechanism that makes 64-concurrent safe Temporal workflow-id claim v2 — for workflow-modelled units only
DB-enforced transitions + 5 error strings verbatim Error strings are the agent’s only docs tasks INFORMATION_REQUEST minting + cross-plane bridge Triage’s lane; no cross-plane signal bridge exists
Block-with-question + pollable open set Parks units without core-plane PR cost_entries per-model/per-phase breakdown One number/attempt suffices; add only if Grafana asks (D8)
4-way disposition (immutable) + evidence links The audit half; absorbs decommission ledger Kanban / UI / comments / priorities / notifications Non-goals N1–N3
cost_cents per attempt → rolled to unit Cost/unit + rework views Second tenant / cross-fleet claim Non-goal N2; open question #3
4 SQL views + kill-gate drill-down query Human read surface Server-rendered disposition page Only at 3rd human without SQL (N1 escape hatch)
External-claim grpn_ auth v1 fleet reality Cloud deploy After billing gate
Idempotency keys + append-only events No double-count, full audit trail Coverage-threshold gate None exists in repo; don’t invent
Atomic-claim concurrency test (new infra) Proves NF1

22. Post-MVP roadmap

flowchart LR
  V1["v1: 4 tables + claim/transition/block/dispose/cost<br/>+ 4 views + external-claim auth (local-first)"]
  V2["v2: Temporal projection (workflow-proxy fan-out)<br/>+ workflow-id claim + tasks minting + cloud deploy"]
  V3["v3: cost breakdown table + old-estate 1→N unit types<br/>+ second-fleet onboarding (if open Q3 flips)"]
  V1 -->|"real workflow units exist<br/>AND/OR billing gate clears"| V2
  V2 -->|"Grafana needs per-model splits<br/>OR ledger >500 units"| V3
v2 item Trigger
Projection from internal-plane workflow stream (F9) Fleet units become internal-plane Temporal workflows + negotiated workflow-proxy ask
Workflow-id claim primitive Same
tasks INFORMATION_REQUEST minting + answer bridge Triage graduates; single negotiated PR
Cloud deploy (GKE, Cloud SQL) Billing gate clears [E5]
v3 item Trigger
cost_entries breakdown table Grafana needs per-model/per-phase splits (D8)
Old-estate 1→N split as new unit row types Ledger >500 units (00-SYNTHESIS.md evolution path)
Second-fleet (Tomas’s) onboarding Open question #3 flips — his fleet starts claiming

23. Release strategy

Three phases, each with a gate. Phase 1 is literally TDD step 1–2 of 00-SYNTHESIS.md.

Phase Scope Success gate
1. Walking skeleton (days) work_units migration + claim() atomic guard + v_queue_depth + one Grafana panel + the concurrency test. Nothing else AC2.1 (2 concurrent claims → exactly one 200, one 409 verbatim) passes on real Docker PG; a fresh agent reaches a claimed unit in <1 min guided only by error strings (§23 self-test, 07-ux-ui.md §11)
2. First real unit on Orders Full v1 surface (transition/block/dispose/cost + 4 views + external auth), one real Orders unit driven claim→verified→MIGRATED with cost + evidence One Orders unit reaches a terminal disposition with cost + evidence populated; Tomas reads the v_disposition_summary drill-down once (closes Q3 with a real read, not an assertion)
3. Ramp gates Fleet ramps 10 → 50 → 64, each unlock metric-gated Each step’s 4 conditions hold (§24) before adding agents

Restore-from-backup drill (NF4) runs between phase 1 and 2.


24. Ramp gating

Concurrency is unlocked by metrics, not the calendar (09-north-star.md §9):

Step Unlock condition (all must hold) If violated
10 → 50 Verified throughput trending up ≥2 wks at 10; rework rate flat/falling; zero double-claims; claim→verified conversion ≥ floor (set from first 4 weeks) Hold at 10; fix the broken guardrail before adding agents
50 → 64 Same four sustained ≥2 wks at 50, plus stale-claim rate not rising with concurrency Hold at 50
Past 64 Not attempted — nothing published past 64 [E9]; 1000 = queued units, never concurrent editors Hard ceiling by evidence

25. Success metrics, pivot criteria, kill criteria

Success (v1 declared successful when)

  • Verified throughput is a non-zero, reported weekly rate with disposition mix.
  • Zero double-claims across the whole ramp (hard invariant held).
  • Tomas has read v_disposition_summary at ≥1 real kill gate and the reconciliation audit found no discrepancy (the moat exists).
  • ≥50% of fleet work flows through claims (ledger coverage floor).

Pivot criteria (change shape, don’t kill)

Trigger Pivot
Agents conflate the 2-tier state model repeatedly after the prototype Collapse the state enum (Q5) — don’t add docs
Rising DEFERRED share games verified throughput Report mix stacked; add a DEFERRED-share guardrail threshold from real data
Rework rate climbs with concurrency Recalibrate claim TTL first (Q6), then hold the ramp — not the fleet’s fault first

Kill / fold criteria (concrete)

Trigger Action
After 4 weeks of fleet operation, <50% of fleet work flows through claims Kill or fold into the fleet monitor’s schema — the ledger isn’t earning its service boundary; the table survives inside the monitor (Q1)
Tomas confirms (Q3) he will read the ADR log, not a disposition table, at gates, and no collision has occurred at current scale (Q7) Fold the disposition half into ADRs; keep only claim + the monitor’s cost/queue table — the standalone service is not justified
A1 rejected and A-lite also blocked The problem is placement, not product — escalate; do not build homeless
First reconciliation discrepancy that erodes Tomas’s trust and cannot be root-caused to a fixable projection bug Stop the ramp; the moat is trust and it’s spent (pre-mortem #3)

The honest through-line: the durable table gets built regardless (11-build-vs-buy.md, temporalCase). It ships as the data layer of the fleet-monitor deliverable by default — same lane, same sprint — with one Encore internal-plane service underneath it out of technical necessity (registerInternalService), not as a fourth product. Promotion to a standalone, independently-roadmapped service happens only on a named trigger (a second fleet claims through it, or the v2 Temporal projection lands); the fold-into-monitor kill trigger below is that same default made concrete at the 4-week gate, not a fallback.


Cross-references

Doc What it owns that this PRD doesn’t
02-research.md Evidence tiers, advisory board, steelman both ways
03-journey.md Full stakeholder + journey maps, 5 failure paths
04-user-stories.md Full US-00…US-25 + rejected stories
07-ux-ui.md Endpoint surface detail, 5 error strings verbatim, Grafana panel specs
09-north-star.md Full metric taxonomy + NSM defense
10-architecture.md Canonical schema, enums, claim algorithm, projection contract, decision table
11-build-vs-buy.md The 8-option sourcing decision + the service-vs-fold resolution
13-pre-mortem.md Full 18-month failure modes behind §19
14-validation-plan.md Riskiest assumptions → cheapest disproving tests