ZeNorm

Spec review and approval

Non-blocking human approval for specs, bound to the spec revision it was granted against. Any org member can record an attestation via API or UI button; stale approvals are visibly marked when the spec changes. No workflow states, no execution gating.

July 26, 2026grounded in ACE-72/zenormspec-review-approval
The request that started it

Specs go straight from written to being worked on and nobody signs off.

5Rounds
4Questions
2Gate refusals
15Blocks

The grilling

Round 1read 22 files

Specs go straight from written to being worked on and nobody signs off. That's the whole ask, in the words I'd use at a whiteboard. Before you ask me anything, go read the repo and figure out what it actually means here. Specifically: what states a spec and a task can be in today, whether anything in the product already represents "approved", and what the agent review endpoint under /v1/agent/review actually does — I want to know whether it's a human workflow or an automated pass. Then grill me. For orientation: I mean this at feature scale, not an "approve" button. The question is what approval means in a product where an agent writes most of the spec and a quality gate already refuses to emit tasks. Don't answer that for me — go find out what exists, tell me what you found, and then ask me the most scope-defining question you have.

ZeNorm

Now let me look at the review endpoint and the agent workflow phases. Let me check the spec store for task status transitions and the workflow state machine. Here's what I found in the repo: **Task lifecycle** (`specTasks.status`): `planned` → `todo` → `active` → `done`. No "approved" state exists. **Workflow phases**: `intent` → `constraints` → `acceptance` → `tasks` → `review` → `complete`. The `review` phase already exists, but it's entirely automated: - `GET /v1/agent/review/:specId/preflight` — deterministic structural checks (missing goals, unmeasurable ACs, task cycle detection, uncovered criteria). Not a human workflow. - `POST /v1/agent/review/:specId` — an async *LLM agent* review that generates questions about completeness. Also not a human. - The workflow scheduler processes the agent review results and can advance through `review` to `complete` without any human clicking "approved." **The gate for execution**: `startSpecExecution()` checks `workflow.phase === "complete" && workflow.status === "complete" && terminalOutcome === "completed"` before flipping `planned` → `todo` so the CLI daemon can claim tasks. Nothing in that chain requires human sign-off. So your description is accurate: no human approval step exists. The agent writes, the agent reviews itself (preflight + LLM pass), then the workflow advances to complete and tasks unlock for the coding daemon. Given that reality, here's the most scope-defining question: The automated review phase exists but nothing forces a human to actually look at the output before tasks become claimable. The most natural spot to insert a human step is gating the transition from `review` to `complete` in the workflow — that way the automated preflight still catches structural issues first, but a real person must explicitly advance the workflow before the daemon can claim tasks. The three options above cover the main alternatives.

Files the agent read
  • read_repo_structure whole repo
  • ls whole repo
  • ls apps
  • ls db
  • ls .agents
  • ls apps/api
  • ls db/schema
  • ls apps/api/src
  • read db/schema/index.ts
  • ls apps/api/src/routes
  • read apps/api/src/task-quality-gates.ts
  • read apps/api/src/task-plan-readiness.ts
  • read apps/api/src/routes/spec-agent-workflow.ts
  • grep apps/api/src/routes
  • read apps/api/src/routes/agent.ts
  • read apps/api/src/section-semantic-coverage.ts
  • read apps/api/src/spec-agent-workflow.ts
  • grep apps/api/src
  • read apps/api/src/task-store.ts
  • grep apps/api/src/routes
  • read apps/api/src/task-store.ts
  • read apps/api/src/task-store.ts
Round 2

Good — you found the thing I was worried you'd miss, which is that isSpecApproved is derived from task status and isn't a human decision at all. But you're asking me to choose between blocking and advisory approval, and I'm going to overrule the framing before I answer it. You recommended the blocking gate. I'm not taking it. Here's why. This product already has a gate that refuses — the 0.9 quality gate on intent, constraints and acceptance. That gate is the differentiator and it is machine-checkable. If I add a second gate that a human has to clear, I've built a product where the bottleneck is a person's inbox, and the thing people will do is approve everything unread to get moving. An approval that's always granted is worse than no approval, because now the record says someone reviewed it and nobody did. So: approval is a recorded human decision that does NOT block task execution. It's an attestation, not a lock. What it changes is what the record says, not what the user can do. Now grill me on the part that actually matters: if it doesn't block, what makes anyone bother to do it?

ZeNorm

You rejected the blocking-gate framing, and the constraints above capture that cleanly. Now the real question you're inviting. Score clamped to 0.5 because I need 2+ goals. The answer to your question will define what the second goal is. So: The same primitive — a recorded attestation (who, when) — can serve all three motivations. A badge on the spec list gives teams a reason to want it approved. An audit trail satisfies compliance requirements. Notifications turn it into a social workflow. And all three don't gate execution, which matches your constraint. But that's my take — which of these (or what else) is actually driving the need?

Round 3

Right question. The answer is that approval has to be attached to a specific version of the spec, not to the spec as a mutable thing. Approval means "I, a named human, read THIS content and vouched for it." The moment the content changes, that attestation is stale and must visibly say so. Otherwise approval is decorative — someone approves a two-line spec, the agent then rewrites every constraint, and the badge still says approved. That's the failure mode that makes people stop trusting the whole surface, and it's the same class of problem as the derived isSpecApproved signal you found. So record this as the central constraint, in strong terms, because it's the thing that will get eroded by someone trying to be helpful later: An approval is bound to the spec revision it was granted against. When the spec's revision advances past the approved one, the approval does not disappear and does not silently persist — it is displayed as stale, naming who approved it and which revision they saw. Re-approval is a new decision by a named human, never an automatic carry-forward, and nothing in the system may mark a spec approved without a human identity attached to it. I noticed the specs table carries a monotonic revision column already. Use it — don't invent a parallel notion of "changed".

ZeNorm

This is the key insight. Let me capture it as blocks and close out that question. Scored 0.65 — not clamped, but audience is the weakest dimension. Who can approve? The simplest model that matches "attestation, not gate" is any collaborator can record one. But you may have a different line — owner-only keeps the signal higher quality, named reviewers makes it a request workflow, tiered visibility separates "someone looked at it" from "someone authoritative signed off."

Round 4

On who can approve, since that's the dimension you flagged as unresolved: anyone in the org except the person who requested the approval. Not an admin-only power. My reasoning is that this product's buyer is a solo builder or a small team where the PM and the engineer are often the same person. If I make approval admin-only, in a two-person team it means one specific human is a permanent bottleneck, and in a one-person org it means the feature is unusable. Excluding the requester is enough to make it a second pair of eyes rather than theatre. And if a solo user approves their own spec, I'd rather the record honestly say "self-approved" than pretend a review happened — so allow it, label it, don't block it. Also: no approval workflow states beyond what we've discussed. No "changes requested" round-trip, no assignment, no reminders. That's a review tool and it's not what we're building. Record that as an explicit non-goal. Now score acceptance and generate the tasks.

ZeNorm

Intent is complete. Moving to acceptance. Before I insert criteria, here's the one question I need answered to make them concrete: While you consider that, here are the acceptance criteria I can already write from locked decisions: Five criteria inserted covering the locked decisions. The remaining open dimension is how approval is initiated (button on spec page, API, or both) — that'll determine criteria about the UI surface and any API contract.

Round 5

Both, and the API is the one that matters. A button on the spec page for the obvious case, and an API endpoint because approval is exactly the kind of thing a team will eventually want to trigger from their own process — and if the button is the only path, they'll drive it with a headless browser, which is worse for everyone. Same authorization rules on both paths, enforced server-side. The button is a client of the endpoint, not a second implementation with its own rules. That's the part I want stated explicitly, because a permissions check that lives in the UI is not a permissions check. That's every open dimension answered. Score constraints and acceptance and generate the tasks.

ZeNorm

All three sections at 0.9+. Let me update the summary to reflect locked scope, then generate the single capability task. Now for the task. This is one capability — the API, schema, staleness logic, and UI button together deliver one user-visible outcome ("a human can review and approve a spec, and the spec shows its approval state"). Let me verify once more that this shouldn't be two tasks. The acceptance criteria cover: - Non-blocking behavior (AC1) - Staleness display on spec page (AC2) - Re-approval creates new record (AC3) - Self-approval labeling (AC4) - No workflow states (AC5) - API endpoint + server-side auth (AC6) - UI button as API client (AC7) - Approval record fields (AC8) AC6 and AC7 are the same capability — the button calls the API. AC2 and AC4 are the same display surface. One task. Task created. Now building the complete trace graph. Spec complete. Here's what was built: **Title:** Spec review and approval **Summary:** Non-blocking human approval for specs, bound to the spec revision it was granted against. Any org member can record an attestation via API or UI button; stale approvals are visibly marked when the spec changes. No workflow states, no execution gating. **Key decisions your grilling surfaced:** - Approval is a recorded attestation, not a lock — never blocks workflow advancement, task creation, or task claiming - Bound to the existing `specs.revision` column; staleness is derived, not a parallel state - Any org member except the requester can approve; self-approval is allowed but labeled - API is authoritative; the Approve button is a client of the same endpoint with server-side-only authorization - No review workflow states (no "changes requested," no assignments, no reminders) **One task:** `da3720e2-6bc0-4ae4-997f-ffb15e5992e7` — Adds the `spec_approvals` table, the approval API endpoint, and the approval-state badge on the spec page, covering all 8 acceptance criteria.

Refusedupdate_spec_metadatasummary too long

This summary is 422 characters — past the 1-3 sentence, 400-char budget. A summary states the decided scope once, like an elevator pitch; it must not re-enumerate the goals, constraints, or acceptance criteria the blocks already carry. Rewrite it as 1-3 sentences of what ships and for whom.

What the agent was told to do instead

Stop the current edit sequence. Treat this as a plan-shape failure, not a wording issue: re-plan from the acceptance criteria into the smallest set of coarse, independently shippable user slices before retrying — as few as the distinct capabilities require (commonly 1-3, hard cap 7, no minimum); one narrow regression fix is exactly 1 slice. If two slices are steps of the same capability, merge them into one. If bad tasks or blocks already exist, update/delete that bad shape before adding more. Metadata updates are atomic: nothing from this call was committed. Retry with every requested field, preserving valid fields unchanged and correcting only rejected fields.

Refusedcreate_tasktask description sprawl

This task description runs 6 sentences — past the 1-3 sentence capability handoff. Keep the outcome, the surfaces it touches, and what proves it works; cut restated acceptance assertions, step lists, and implementation detail the blocks and trace graph already carry.

What the agent was told to do instead

Stop the current edit sequence. Treat this as a plan-shape failure, not a wording issue: re-plan from the acceptance criteria into the smallest set of coarse, independently shippable user slices before retrying — as few as the distinct capabilities require (commonly 1-3, hard cap 7, no minimum); one narrow regression fix is exactly 1 slice. If two slices are steps of the same capability, merge them into one. If bad tasks or blocks already exist, update/delete that bad shape before adding more.

What it asked

Q1intent

What should "approval" mean as a human step?

  • AGate at workflow phase 'review'Agent's pick

    A person must explicitly approve before workflow advances past 'review' to 'complete' — tasks stay locked until sign-off

  • BNew 'approved' status on the spec itself

    A person marks the entire spec as approved via a toggle/action; unrelated to the workflow phase

  • CPer-task approval gate

    Each task needs a human review/approve action before the daemon can claim it, independent of the spec-level flow

What the PM said

Good — you found the thing I was worried you'd miss, which is that isSpecApproved is derived from task status and isn't a human decision at all. But you're asking me to choose between blocking and advisory approval, and I'm going to overrule the framing before I answer it. You recommended the blocking gate. I'm not taking it. Here's why. This product already has a gate that refuses — the 0.9 quality gate on intent, constraints and acceptance. That gate is the differentiator and it is machine-checkable. If I add a second gate that a human has to clear, I've built a product where the bottleneck is a person's inbox, and the thing people will do is approve everything unread to get moving. An approval that's always granted is worse than no approval, because now the record says someone reviewed it and nobody did. So: approval is a recorded human decision that does NOT block task execution. It's an attestation, not a lock. What it changes is what the record says, not what the user can do. Now grill me on the part that actually matters: if it doesn't block, what makes anyone bother to do it?

Q2intent

What makes people bother to approve?

  • AQuality-score badge on the spec

    The spec list shows whether each spec has been human-approved; teams treat it as a readiness signal for their own confidence

  • BAudit trail for compliance

    Regulated teams need a recorded reviewer for each spec before execution; the product is the compliance record even without a gate

  • CReview-request notification

    The spec author explicitly requests review from named people, who get a notification; approval is a social completion signal

  • DTwo separate needs, same featureAgent's pick

    All of the above — badge visibility for quality, audit trail for compliance, and notifications for social workflow; they layer on the same attestation record

What the PM said

Right question. The answer is that approval has to be attached to a specific version of the spec, not to the spec as a mutable thing. Approval means "I, a named human, read THIS content and vouched for it." The moment the content changes, that attestation is stale and must visibly say so. Otherwise approval is decorative — someone approves a two-line spec, the agent then rewrites every constraint, and the badge still says approved. That's the failure mode that makes people stop trusting the whole surface, and it's the same class of problem as the derived isSpecApproved signal you found. So record this as the central constraint, in strong terms, because it's the thing that will get eroded by someone trying to be helpful later: An approval is bound to the spec revision it was granted against. When the spec's revision advances past the approved one, the approval does not disappear and does not silently persist — it is displayed as stale, naming who approved it and which revision they saw. Re-approval is a new decision by a named human, never an automatic carry-forward, and nothing in the system may mark a spec approved without a human identity attached to it. I noticed the specs table carries a monotonic revision column already. Use it — don't invent a parallel notion of "changed".

Q3intent

Who should be allowed to approve a spec?

  • AAny spec collaboratorAgent's pick

    Any user with editor or owner role on the spec can record an approval

  • BSpec owners only

    Only the spec owner or org-level admins can approve; editors can view but not approve

  • CNamed reviewers requested by author

    The spec author designates specific people who can approve; no one else

  • DAny viewer can attest, owners' approval visible

    Anyone who can see the spec can record a review attestation, but only owner-level approvals are surfaced as 'approved'

What the PM said

On who can approve, since that's the dimension you flagged as unresolved: anyone in the org except the person who requested the approval. Not an admin-only power. My reasoning is that this product's buyer is a solo builder or a small team where the PM and the engineer are often the same person. If I make approval admin-only, in a two-person team it means one specific human is a permanent bottleneck, and in a one-person org it means the feature is unusable. Excluding the requester is enough to make it a second pair of eyes rather than theatre. And if a solo user approves their own spec, I'd rather the record honestly say "self-approved" than pretend a review happened — so allow it, label it, don't block it. Also: no approval workflow states beyond what we've discussed. No "changes requested" round-trip, no assignment, no reminders. That's a review tool and it's not what we're building. Record that as an explicit non-goal. Now score acceptance and generate the tasks.

Q4acceptance

How does a human initiate an approval?

  • AExplicit approve button on spec page

    A visible 'Approve' action/button in the spec editor toolbar or header that records approval at the current revision

  • BAPI-only with agent requesting it

    No UI button — the spec agent can request review, sending a notification/link; approval happens via the API directly

  • CBoth: button + programmatic APIAgent's pick

    UI button on the spec page for casual use, plus a REST endpoint for automation and agent-requested reviews

What the PM said

Both, and the API is the one that matters. A button on the spec page for the obvious case, and an API endpoint because approval is exactly the kind of thing a team will eventually want to trigger from their own process — and if the button is the only path, they'll drive it with a headless browser, which is worse for everyone. Same authorization rules on both paths, enforced server-side. The button is a client of the endpoint, not a second implementation with its own rules. That's the part I want stated explicitly, because a permissions check that lives in the UI is not a permissions check. That's every open dimension answered. Score constraints and acceptance and generate the tasks.

Where the server refused

Each of these rejected the agent's own tool call. The agent had to fix its output and try again before the spec could advance — this is the gate, not a prompt the model can talk past.

Refusedupdate_spec_metadatasummary too long

This summary is 422 characters — past the 1-3 sentence, 400-char budget. A summary states the decided scope once, like an elevator pitch; it must not re-enumerate the goals, constraints, or acceptance criteria the blocks already carry. Rewrite it as 1-3 sentences of what ships and for whom.

What the agent was told to do instead

Stop the current edit sequence. Treat this as a plan-shape failure, not a wording issue: re-plan from the acceptance criteria into the smallest set of coarse, independently shippable user slices before retrying — as few as the distinct capabilities require (commonly 1-3, hard cap 7, no minimum); one narrow regression fix is exactly 1 slice. If two slices are steps of the same capability, merge them into one. If bad tasks or blocks already exist, update/delete that bad shape before adding more. Metadata updates are atomic: nothing from this call was committed. Retry with every requested field, preserving valid fields unchanged and correcting only rejected fields.

Refusedcreate_tasktask description sprawl

This task description runs 6 sentences — past the 1-3 sentence capability handoff. Keep the outcome, the surfaces it touches, and what proves it works; cut restated acceptance assertions, step lists, and implementation detail the blocks and trace graph already carry.

What the agent was told to do instead

Stop the current edit sequence. Treat this as a plan-shape failure, not a wording issue: re-plan from the acceptance criteria into the smallest set of coarse, independently shippable user slices before retrying — as few as the distinct capabilities require (commonly 1-3, hard cap 7, no minimum); one narrow regression fix is exactly 1 slice. If two slices are steps of the same capability, merge them into one. If bad tasks or blocks already exist, update/delete that bad shape before adding more.

What it had to clear

Every section had to score at or above 0.90 before create_task would run at all. This is the gate.

intent0.923 attempts

Two clear goals (record attestation, revision-bound approval with staleness). Audience resolved: any org member except requester; self-approval labeled. Non-goals explicit: no review workflow states. Constraints cover: non-gating, revision binding, actor boundary, no-incentive-model-footgun. Scope boundaries defined by what's excluded (no approval workflows, no execution gating).

constraints0.93

Five constraint blocks covering: non-gating of execution (99a30911), no lock/incentive-model footgun (c31338fb), revision binding via existing spec.revision column (73af1bf0), actor boundary with explicit self-approval rules (902e0739), and explicit scope fence excluding review workflow states (23ef4800). Tradeoffs are explicit. No missing constraint dimensions.

acceptance0.94

Eight acceptance criteria, all measurable and observable. Non-blocking behavior (ff90d816, measurable by attempting task claim with zero approvals). Revision staleness (2690bca7, measurable by comparing spec.revision to approvedAtRevision). Re-approval creates new record (bd2671ce). Self-approval labeling (0998bc41). No workflow states (396c78ca). API endpoint + server-side auth (3d5565fd). UI button as API client (eaeb8bc1). Record fields defined (03bea69d).

The spec it produced

Goals
Record human spec approval as attestation
Revision-bound approval with visible staleness
Constraints
Approval must not gate execution
No approval-as-lock incentive model
Approval binds to spec revision, not mutable spec
Approval actor boundary
No review workflow states
Acceptance criteria
Non-blocking approval record
Revision-bound staleness display
Re-approval creates new record
Self-approval visibly labeled
No review workflow states exist
API endpoint for approval
UI button as API client
Approval record fields

How it hangs together

goal

Record human spec approval as attestation

  • constrainsApproval must not gate execution
  • constrainsNo approval-as-lock incentive model
  • constrainsApproval actor boundary
  • constrainsNo review workflow states
  • validatesNon-blocking approval record
  • validatesSelf-approval visibly labeled
  • validatesNo review workflow states exist
  • validatesAPI endpoint for approval
  • validatesUI button as API client
goal

Revision-bound approval with visible staleness

  • constrainsApproval binds to spec revision, not mutable spec
  • validatesRevision-bound staleness display
  • validatesRe-approval creates new record
  • validatesApproval record fields
acceptance

Non-blocking approval record

  • implementsAdd Approve button and revision-bound approval badge to the spec page, backed by a server-enforced approval API
acceptance

Revision-bound staleness display

  • implementsAdd Approve button and revision-bound approval badge to the spec page, backed by a server-enforced approval API
acceptance

Re-approval creates new record

  • implementsAdd Approve button and revision-bound approval badge to the spec page, backed by a server-enforced approval API
acceptance

Self-approval visibly labeled

  • implementsAdd Approve button and revision-bound approval badge to the spec page, backed by a server-enforced approval API
acceptance

No review workflow states exist

  • implementsAdd Approve button and revision-bound approval badge to the spec page, backed by a server-enforced approval API
acceptance

API endpoint for approval

  • implementsAdd Approve button and revision-bound approval badge to the spec page, backed by a server-enforced approval API
acceptance

UI button as API client

  • implementsAdd Approve button and revision-bound approval badge to the spec page, backed by a server-enforced approval API
acceptance

Approval record fields

  • implementsAdd Approve button and revision-bound approval badge to the spec page, backed by a server-enforced approval API

What the gate released

Add Approve button and revision-bound approval badge to the spec page, backed by a server-enforced approval API

Expose a `POST /v1/specs/:specId/approve` endpoint that records an approval against the spec's current `revision` in a new `spec_approvals` table, enforcing server-side authorization. Add an Approve button on the spec page and an approval-state badge that shows the approver and revision, transitioning to stale when the spec revision advances past the approved revision. Assert that task claiming is never blocked by approval state and that self-approval carries a distinct label. Automated assertions: - Non-blocking approval record — never blocks any execution path - Revision-bound staleness display — The approval record itself is preserved — it is not removed or modified — no staleness indicator is shown - Re-approval creates new record — No automatic or system-generated re-approvals occur - Self-approval visibly labeled — never hidden or treated as a peer review - No review workflow states exist - API endpoint for approval - UI button as API client - Approval record fields

Then, in your terminal
zenorm work

Your coding agent claims the task, implements it against the full spec, and gates itself on typecheck, lint, and tests.

One dissected spec per week

A real request, the questions the agent asked, and every place the server refused its output. No product announcements.