ZeNorm

Org-scoped API keys, spec read API, and verifiable outbound webhooks

Org-scoped API credentials authenticate a read-only REST API for specs, blocks, and tasks, plus signed webhooks for spec lifecycle events. Webhooks retry with backoff and record delivery attempts but carry no ordering guarantee. A curated view model decouples the public contract from the internal storage schema.

July 25, 2026grounded in ACE-72/zenormorgscoped-api-keys-spec-read-api
The request that started it

Teams want to hook ZeNorm into their own tools.

4Rounds
2Questions
5Gate refusals
17Blocks

The grilling

Round 1read 8 files

Teams want to hook ZeNorm into their own tools.

ZeNorm

Let me start by exploring the repo to understand what exists todayLet me understand the existing webhook/webhook-related code to understand what's already builtThis is a specification-writing tool called ZeNorm. There are no existing webhook endpoints for user-facing integrations. Let me also check if there's any gallery or API export mechanism. No existing webhook infrastructure for third-party integrations. Let me understand what ZeNorm is and how spec data flows. Good. ZeNorm is a spec-writing tool — it manages "specs" (product specifications). Teams want to hook ZeNorm into their own tools, likely meaning they want webhooks or an API so external tools can react to spec events. Let me look at the specs route and data model to understand what events could be meaningfulGood. Now I have a solid picture. ZeNorm is a spec-writing tool. The spec system has: activity events (`created`, `updated`, `status_changed`, etc.), collaborators, versions. No outgoing webhook infrastructure exists. Let me ask the key opening question. ZeNorm currently has no webhook or public API for external tools. The collaboration panel shows the app already tracks activity events internally (created, updated, status_changed, etc.), so the data layer for spec events exists. The question is what integration pattern the teams actually need — are they looking to push events out, pull data in, or both? I've asked: **what specific gap do teams hit today that makes them want to hook ZeNorm into their own tools?** Push-only (webhook), pull-only (API), both, or something else entirely.

Files the agent read
  • read_repo_structure whole repo
  • grep webhook|webhook
  • grep gallery|api.?key|token|secret
  • grep webhook|outgoing|integration|partner
  • read_repo_file README.md
  • read_repo_file apps/web/src/routes/dashboard/spec-editor/api-helpers.ts
  • read_repo_file apps/api/src/routes/specs.ts
  • read_repo_file apps/web/src/routes/dashboard/spec-editor/collaboration-panel.tsx
Round 2read 13 files

Both, and there's a third thing neither of your options names, which is the part that actually makes this a product rather than two endpoints. The gap: today, ZeNorm is a closed loop. A team's specs live here, their issue tracker lives somewhere else, their CI lives somewhere else, and there is no seam. Every conversation I have with a team that likes the product ends with "can it talk to our stuff," and the answer is no, and then they either don't adopt or they hand-copy things. So: a public REST API for reading spec state, and outbound webhooks for spec lifecycle events. Push and pull, because they solve different halves. Webhooks let their system react the moment something happens — a spec's tasks get emitted, kick off their planning workflow. The API lets their system ask questions on its own schedule — nightly sync, or a dashboard that renders our specs inside their wiki. Neither substitutes for the other; a webhook with no API to call back into is a notification with no detail, and an API with no webhooks means polling. The third thing, and the reason this isn't small: none of it works without an authentication story for machines. Everything today assumes a Clerk session belonging to a human. A CI job doesn't have one. So this feature necessarily includes org-scoped API credentials — issue a key, scope it to an org, show it once, let someone revoke it, and make every API and webhook path authenticate against that instead of a user session. That's not a prerequisite somebody else builds; it's the foundation of this feature and it's in this spec. So the surface is: machine credentials, a read API over specs, and outbound webhooks with delivery you can trust. That's the brief. Constraints I'll set now: - Org-scoped, absolutely. A key issued in one org reads that org and nothing else, ever. This is the failure that ends the company, so it gets stated explicitly rather than assumed. - Read-only for v1 on the API. I'm not opening up writes from outside yet — I want to see what people actually do with reads before I let external systems mutate specs. That's a genuine scope cut and I'm making it deliberately. - Webhook deliveries must be verifiable by the receiver. If someone's endpoint gets a POST claiming to be from us, they need to be able to prove it. Signed payloads, not just a shared URL that anyone who learns it can spoof into. - Don't build a new events table if activity_events already covers this. Go look — the other thing I'm working on found that table. Reuse the spine if it fits. Go read what's there for auth and for events, then tell me what you found before you write blocks. Particularly: is there any existing notion of a non-human caller anywhere in the auth layer, or is Clerk-session the only path?

ZeNorm

Let me read the auth layer, the activity events schema, and check for any existing API-key or machine-credential concept. Now let me read the activity_events schema and check the db package more carefullyNow let me read the full activity_events table definition and check the spec store for how it reads dataLet me find the activity_events table definition and see how events are written in the mutators The file is very large and the grep tells me the activityEvents table is at line 2097 but the snippet keeps being truncated before that. Let me read exactly the right lines. The file keeps being truncated because schema/index.ts is 88658 bytes. Let me read the exact activityEvents table definition. Now I have a complete picture. Let me capture everything into intent blocks and update the metadata. The quality gate rejected the first block for specifying credential lifecycle mechanics without PM evidence. You explicitly stated: "issue a key, scope it to an org, show it once, let someone revoke it." That's not invented — it's your stated constraint. But to pass the gate, let me retry without over-specifying the lifecycle and with just your stated outcomesGood, blocks are in. The auth layer is Clerk-only today, with no concept of a non-human caller. Now let me ask about the open credential design choices before I can score intentHere's what I found in the repo: **Auth layer** — Clerk-session-only. The `resolveAuthContext` function in `apps/api/src/auth.ts` handles Clerk JWTs, dev headers (`x-zenorm-user-id`, `x-zenorm-org-id`), and OAuth access tokens (`oat_` prefixed for the CLI). Every path produces a `{ orgId, userId, isAdmin }` context. There is zero notion of a non-human, machine-only credential anywhere in the codebase. **activity_events table** (`db/schema/index.ts:2097`) — Already exists and is in active use. Schema: `id`, `orgId`, `specId`, `targetType` (block or task), `targetId`, `userId` (nullable text), `action` (enum: created, updated, status_changed, assigned, commented, done_toggled, deleted, priority_changed, linked, unlinked), `previousValue`, `newValue`, `metadata` (jsonb), `createdAt`. Indexed on (targetType, targetId, createdAt) and on specId. The `server-mutators.ts` already writes to this table during mutations. Using this as the webhook event spine means no new events table — you hook into the same insert path. **Auth routes** — `apps/api/src/routes/` has Hono router files. The spec store at `apps/api/src/spec-store.ts` handles reads. The question I'm asking now: you said "org-scoped" — keys at the org level or per-spec?

Refusedinsert_blocksunsupported state mechanic

Requirement invents durable identity/access/lifecycle mechanic(s) absent from authoritative PM or repository evidence: session/account/access termination. Preserve the confirmed outcome without adding a storage, revocation, queryability, or presentation mechanism; ask only if it materially changes behavior.

What the agent was told to do instead

Remove or reword the unsupported claim: keep only content the PM stated, selected, or the repository verified. If the claim is load-bearing, ask the PM instead of asserting it. This gate family will not block again this session for a rewrite without the claim — but resubmitting this same rejected content is rejected every time; retry once without the claim, then move on; do not loop on rewording. Block inserts are atomic: nothing from this call was committed. Retry the batch without the rejected claim.

Files the agent read
  • read_repo_file apps/api/src/auth.ts
  • read_repo_file apps/api/src/zero/server-mutators.ts
  • grep activity_events|activityEvents
  • read_repo_file db/schema/index.ts
  • read_repo_file apps/api/src/env.ts
  • read_repo_file .env.example
  • read_repo_file db/schema/index.ts
  • read_repo_file apps/api/src/zero/server-mutators.ts
  • grep db/schema/index.ts
  • read_repo_file db/schema/index.ts
  • read_repo_file db/schema/index.ts
  • grep db/schema
  • read db/schema/index.ts
Round 3read 9 files

Org level. Not per-spec. Per-spec keys sound more secure and are worse in practice: a team with forty specs ends up with forty credentials to rotate, their CI config becomes a keyring, and the first time someone adds a spec their integration silently doesn't cover it. The failure mode of org-level is "the key sees more than one spec," which is what the integrator wants anyway — they're syncing their workspace, not one document. Scope the key to the org, and if someone later needs narrower, that's a scoping refinement on top of a working feature, not a v1 requirement. Good find on activity_events — reuse it, and note that the other spec I'm running is also closing gaps in what gets written there. Don't design around those gaps; assume the table is the event spine and that its coverage is being fixed separately. If a webhook event type has no corresponding row today, say so in the spec so the dependency is visible, but don't absorb that work here. Remaining decisions so you can finish: Delivery. Webhooks have to retry, because receiving endpoints go down and a delivery lost to a thirty-second blip is a data-integrity problem for the integrator, not an inconvenience. Retry with backoff, give up after a bounded number of attempts, and make the outcome inspectable — someone whose integration broke on Tuesday needs to see that we tried and what came back. That means delivery attempts are recorded, not just fired and forgotten. Ordering: don't promise it. I'd rather be honest that events can arrive out of order than build sequencing and be slow. Say so explicitly in the spec so an integrator designs for it. The API surface: specs, their blocks, their tasks. Enough to render a spec somewhere else and enough to react to a webhook by fetching detail. Not the agent transcript, not the trace graph, not usage data — those are internal and I don't want to commit to their shape as a public contract this early. That's a deliberate cut. One thing I want you to think about rather than take from me: this is a public contract, and public contracts are permanent in a way internal code isn't. Whatever the API returns, we're stuck with. Is there something about how the spec data is shaped internally that would be a mistake to expose directly — something that's fine as an internal representation but that we'd regret publishing as a stable contract? You've read the schema and I haven't. If the answer is "just serialize the rows," fine. But if there's a version-shaped hazard in there, I want it in the constraints now rather than discovering it after someone's integration depends on it.

ZeNorm

Let me also check what activity_events looks like, and look for existing API routes or webhook infrastructure. Good — I've read the schema thoroughly. Let me answer your schema exposure question directly, then update the blocks with all the decisions captured. **Schema hazard analysis**: Yes, there are two version-shaped hazards in direct serialization: 1. **`spec_versions.content`** is a `json<ReadonlyJSONValue>` with no schema enforcement. The seed data shows it as `{format: "markdown", body: "..."}` but this is an internal shape that will evolve as the editor does — exposing it raw means the first consumer who depends on its structure becomes a permanent drag on internal refactoring. Any change to how the editor serializes content becomes a breaking change. 2. **`spec_editor_blocks.attrs`** is another untyped JSON blob carrying editor-internal metadata (cursor positions, formatting hints, block-specific state). Exposing this directly publishes an undocumented internal shape the editor team needs freedom to change. Neither is fatal — the fix is to define a **stable view model** for the API responses that decouples the public contract from the storage schema. For spec content, return blocks as a typed array (type, text, order) and omit `attrs`. For spec metadata, flatten the relevant columns into a response object and omit internal FKs like `currentVersionId`. Now let me capture all the confirmed decisions as blocks and update the spec. Two issues: summary exceeds 400 chars (I tried to summarize the blocks instead of writing an elevator pitch) and I asserted "3 attempts" when the PM only said "a bounded number of attempts." Let me fix both. I'll update existing blocks with the richer content from your decisions and add new constraint blocks for the decisions that don't have a home yet.

Refusedupdate_spec_metadatasummary too long

This summary is 536 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.

Refusedinsert_blocksunsupported operational constraint

Requirement invents operational numeric target(s) absent from authoritative PM evidence: 3 attempts. Remove the target or ask the PM to choose it before recording the requirement.

What the agent was told to do instead

Remove or reword the unsupported claim: keep only content the PM stated, selected, or the repository verified. If the claim is load-bearing, ask the PM instead of asserting it. This gate family will not block again this session for a rewrite without the claim — but resubmitting this same rejected content is rejected every time; retry once without the claim, then move on; do not loop on rewording. Block inserts are atomic: nothing from this call was committed. Retry the batch without the rejected claim.

Files the agent read
  • read_repo_structure packages
  • read_repo_file packages/zero/src/schema.ts
  • read_repo_file packages/zero/src/queries.ts
  • grep packages/zero/src/schema.ts
  • read_repo_structure apps
  • read packages/zero/src/schema.ts
  • read apps/api/src/app.ts
  • read apps/api/src/auth.ts
  • read apps/api/src/spec-store.ts
Round 4

That's exactly the answer I was hoping you'd find, and it becomes a constraint. Record it: the public API returns a stable view model, not serialized rows. Untyped internal JSON — spec_versions.content, spec_editor_blocks.attrs — is never exposed. Blocks come back as a typed shape (type, text, order). Internal foreign keys like currentVersionId stay internal. The reason goes in the constraint too: the first integrator who depends on an internal shape freezes the editor team forever, and I'd rather pay for a mapping layer now than have a refactor blocked in a year. Bounded retries: you were right to refuse the number I hadn't given you. Make it 5 attempts with exponential backoff, then stop and mark the delivery permanently failed. That's my number, now it's authorized. Everything else is decided. Write the remaining constraints and acceptance criteria and get me tasks. Acceptance criteria I'll judge this on: - Someone with a key can read a spec's content from outside the product, with no browser and no user session. - A key from org A returns nothing for org B's specs. Not an empty list that could be confused with "no specs" — a refusal. - An event on a spec reaches a registered endpoint, and the receiver can verify it actually came from us. - A receiver that's down gets retried, and after the bounded attempts the delivery is marked failed and is visible as failed rather than vanishing. - Revoking a key stops it working immediately. Not at the next cache expiry — immediately. - The API response shape contains no internal JSON blob. That's the constraint above, stated as something testable. For tasks: I'd expect this to come out as credentials plus read API as one shippable capability, and webhook registration plus delivery as another. Two coarse pieces. If you see it differently after everything you've read, argue for your grouping — you've got a better view of the code than I do. But I don't want six tasks, and I don't want one.

ZeNorm

Your grouping matches what the code suggests. The credentials+read API are tightly coupled — you can't demo one without the other, and they share the auth middleware surface. Webhooks are a separate runtime concern (a delivery worker with retry queue, separate from the request-response path). Two tasks is right. Let me insert the remaining constraints (stable view model, 5-attempt retry, no ordering, immediate revocation), then all six acceptance criteria, then score through and generate tasks. The "test receiver" phrasing tripped the validation-block gate. Let me fix AC3 to describe only observable product behavior, then retry the batch. The PM authorized 5 attempts in the message that arrived before I submitted — the advisory is stale relative to that. The blocks committed successfully. Let me score the sections now and then create tasks per your directive. All three sections complete. Now creating the two tasks — credentials+read API and webhook registration+delivery — exactly as you outlined. Task 2 created. Task 1 rejected because I invented HTTP status codes the PM didn't specify. The PM said "a refusal" for cross-org, not "403." Let me fix the descriptions and remove the invented protocol mechanics. Both tasks staged. Now I need to build the complete trace graph. Let me trace every constraint and AC to its goal, and every task to its ACs. Spec is complete. Here's the final state: **Title**: Org-scoped API keys, spec read API, and verifiable outbound webhooks **3 goal blocks** — org-scoped machine auth, read-only spec REST API, verifiable outbound webhooks **8 constraint blocks** — org isolation, read-only v1, signed payloads, activity_events spine, stable view model, 5-attempt retry with backoff, no ordering guarantees, immediate key revocation **6 acceptance criteria** — API-key read without Clerk session, cross-org isolation (refusal, not empty list), signed webhook delivery to registered endpoint, 5-retry with permanent-failure visibility, immediate revocation, no internal JSON blobs in responses **2 tasks**: 1. **Add API key auth and GET endpoints for specs, blocks, and tasks** — API key management (create/revoke per org), Hono auth middleware accepting API keys alongside Clerk JWTs, GET endpoints returning a stable view model, cross-org refusal, immediate revocation 2. **Add webhook registration and signed delivery with retry** — endpoints for managing webhook URLs per org, delivery worker reading from activity_events, HMAC-signed payloads, 5-attempt exponential backoff with permanent failure and inspectable delivery history, no ordering guarantees Trace graph connects every constraint and AC to its goal, and both tasks to their ACs — complete coverage.

Refusedinsert_blocksstandalone validation block

This acceptance criterion describes only CI/test/fixture/review validation, not user-visible behavior. Rewrite it as an observable product outcome and put the validation detail in the owning task description.

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.

Advisoryinsert_blocksunsupported operational constraint

Requirement invents operational numeric target(s) absent from authoritative PM evidence: 5 attempts. Remove the target or ask the PM to choose it before recording the requirement.

Refusedcreate_taskunsupported authoritative mechanic

Requirement invents surface/queue/identifier/protocol mechanic(s) absent from authoritative PM or original-request evidence: exact protocol status 403, exact protocol status 401. Keep the confirmed generic outcome, or ask only if this exact mechanic is product-owned.

What the agent was told to do instead

Remove or reword the unsupported claim: keep only content the PM stated, selected, or the repository verified. If the claim is load-bearing, ask the PM instead of asserting it. This gate family will not block again this session for a rewrite without the claim — but resubmitting this same rejected content is rejected every time; retry once without the claim, then move on; do not loop on rewording.

Advisorycreate_tasktask description sprawl

This task description runs 9 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.

Advisorycreate_tasktask description sprawl

This task description runs 5 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 it asked

Q1intent

What can users NOT do today that this feature should enable?

  • ANotify external tools on spec eventsAgent's pick

    React to spec created, updated, status changed — trigger builds, sync docs, post to Slack

  • BPull spec data via external API

    Read/render spec content from outside ZeNorm — embed in a wiki, export to a CMS

  • CBoth — events for automation, API for queries

    Webhooks to push spec changes to tools + a read API to pull spec content on demand

  • DSomething else entirely

    E.g. SSO, import from external tools, or a different integration pattern

What the PM said

Both, and there's a third thing neither of your options names, which is the part that actually makes this a product rather than two endpoints. The gap: today, ZeNorm is a closed loop. A team's specs live here, their issue tracker lives somewhere else, their CI lives somewhere else, and there is no seam. Every conversation I have with a team that likes the product ends with "can it talk to our stuff," and the answer is no, and then they either don't adopt or they hand-copy things. So: a public REST API for reading spec state, and outbound webhooks for spec lifecycle events. Push and pull, because they solve different halves. Webhooks let their system react the moment something happens — a spec's tasks get emitted, kick off their planning workflow. The API lets their system ask questions on its own schedule — nightly sync, or a dashboard that renders our specs inside their wiki. Neither substitutes for the other; a webhook with no API to call back into is a notification with no detail, and an API with no webhooks means polling. The third thing, and the reason this isn't small: none of it works without an authentication story for machines. Everything today assumes a Clerk session belonging to a human. A CI job doesn't have one. So this feature necessarily includes org-scoped API credentials — issue a key, scope it to an org, show it once, let someone revoke it, and make every API and webhook path authenticate against that instead of a user session. That's not a prerequisite somebody else builds; it's the foundation of this feature and it's in this spec. So the surface is: machine credentials, a read API over specs, and outbound webhooks with delivery you can trust. That's the brief. Constraints I'll set now: - Org-scoped, absolutely. A key issued in one org reads that org and nothing else, ever. This is the failure that ends the company, so it gets stated explicitly rather than assumed. - Read-only for v1 on the API. I'm not opening up writes from outside yet — I want to see what people actually do with reads before I let external systems mutate specs. That's a genuine scope cut and I'm making it deliberately. - Webhook deliveries must be verifiable by the receiver. If someone's endpoint gets a POST claiming to be from us, they need to be able to prove it. Signed payloads, not just a shared URL that anyone who learns it can spoof into. - Don't build a new events table if activity_events already covers this. Go look — the other thing I'm working on found that table. Reuse the spine if it fits. Go read what's there for auth and for events, then tell me what you found before you write blocks. Particularly: is there any existing notion of a non-human caller anywhere in the auth layer, or is Clerk-session the only path?

Q2intent

Who manages API keys for the org?

  • ASpec owner manages keys per spec

    Each spec's owner (created-by user or sole owner) can issue/revoke keys for that spec

  • BOrg-level key managementAgent's pick

    Keys are issued at the org level — one key gives access to all specs in the org that org owns

  • CBoth — org-level plus per-spec scoping

    Org-level keys default to all specs; per-spec scoped keys limit to a single spec's data/events

What the PM said

Org level. Not per-spec. Per-spec keys sound more secure and are worse in practice: a team with forty specs ends up with forty credentials to rotate, their CI config becomes a keyring, and the first time someone adds a spec their integration silently doesn't cover it. The failure mode of org-level is "the key sees more than one spec," which is what the integrator wants anyway — they're syncing their workspace, not one document. Scope the key to the org, and if someone later needs narrower, that's a scoping refinement on top of a working feature, not a v1 requirement. Good find on activity_events — reuse it, and note that the other spec I'm running is also closing gaps in what gets written there. Don't design around those gaps; assume the table is the event spine and that its coverage is being fixed separately. If a webhook event type has no corresponding row today, say so in the spec so the dependency is visible, but don't absorb that work here. Remaining decisions so you can finish: Delivery. Webhooks have to retry, because receiving endpoints go down and a delivery lost to a thirty-second blip is a data-integrity problem for the integrator, not an inconvenience. Retry with backoff, give up after a bounded number of attempts, and make the outcome inspectable — someone whose integration broke on Tuesday needs to see that we tried and what came back. That means delivery attempts are recorded, not just fired and forgotten. Ordering: don't promise it. I'd rather be honest that events can arrive out of order than build sequencing and be slow. Say so explicitly in the spec so an integrator designs for it. The API surface: specs, their blocks, their tasks. Enough to render a spec somewhere else and enough to react to a webhook by fetching detail. Not the agent transcript, not the trace graph, not usage data — those are internal and I don't want to commit to their shape as a public contract this early. That's a deliberate cut. One thing I want you to think about rather than take from me: this is a public contract, and public contracts are permanent in a way internal code isn't. Whatever the API returns, we're stuck with. Is there something about how the spec data is shaped internally that would be a mistake to expose directly — something that's fine as an internal representation but that we'd regret publishing as a stable contract? You've read the schema and I haven't. If the answer is "just serialize the rows," fine. But if there's a version-shaped hazard in there, I want it in the constraints now rather than discovering it after someone's integration depends on it.

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.

Refusedinsert_blocksunsupported state mechanic

Requirement invents durable identity/access/lifecycle mechanic(s) absent from authoritative PM or repository evidence: session/account/access termination. Preserve the confirmed outcome without adding a storage, revocation, queryability, or presentation mechanism; ask only if it materially changes behavior.

What the agent was told to do instead

Remove or reword the unsupported claim: keep only content the PM stated, selected, or the repository verified. If the claim is load-bearing, ask the PM instead of asserting it. This gate family will not block again this session for a rewrite without the claim — but resubmitting this same rejected content is rejected every time; retry once without the claim, then move on; do not loop on rewording. Block inserts are atomic: nothing from this call was committed. Retry the batch without the rejected claim.

Refusedupdate_spec_metadatasummary too long

This summary is 536 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.

Refusedinsert_blocksunsupported operational constraint

Requirement invents operational numeric target(s) absent from authoritative PM evidence: 3 attempts. Remove the target or ask the PM to choose it before recording the requirement.

What the agent was told to do instead

Remove or reword the unsupported claim: keep only content the PM stated, selected, or the repository verified. If the claim is load-bearing, ask the PM instead of asserting it. This gate family will not block again this session for a rewrite without the claim — but resubmitting this same rejected content is rejected every time; retry once without the claim, then move on; do not loop on rewording. Block inserts are atomic: nothing from this call was committed. Retry the batch without the rejected claim.

Refusedinsert_blocksstandalone validation block

This acceptance criterion describes only CI/test/fixture/review validation, not user-visible behavior. Rewrite it as an observable product outcome and put the validation detail in the owning task description.

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.

Refusedcreate_taskunsupported authoritative mechanic

Requirement invents surface/queue/identifier/protocol mechanic(s) absent from authoritative PM or original-request evidence: exact protocol status 403, exact protocol status 401. Keep the confirmed generic outcome, or ask only if this exact mechanic is product-owned.

What the agent was told to do instead

Remove or reword the unsupported claim: keep only content the PM stated, selected, or the repository verified. If the claim is load-bearing, ask the PM instead of asserting it. This gate family will not block again this session for a rewrite without the claim — but resubmitting this same rejected content is rejected every time; retry once without the claim, then move on; do not loop on rewording.

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.95

Three goal blocks cover the full scope: org-scoped machine auth, read-only spec API, and verifiable outbound webhooks. Audience (integrators/CI systems), priority (v1), and non-goals (no writes, no per-spec keys, no agent transcript/trace graph/usage data) are explicit. User-visible outcomes are concrete — no vague quality words.

constraints0.95

All key constraints captured: org-scoped isolation, read-only v1, signed payloads, activity_events spine, 5-attempt retry with backoff, no ordering guarantees, stable view model, immediate key revocation. Every constraint constrains at least one goal. No vague or unsourced numbers — 5 attempts authorized by PM.

acceptance0.95

Six measurable acceptance criteria cover: API key read without session, cross-org isolation (403 refusal), signed webhook delivery, retry with failure visibility, immediate revocation, and no internal JSON blobs. Each describes observable product behavior — no test-harness language.

The spec it produced

Goals
Org-scoped machine authentication
Read-only spec REST API for machines
Verifiable outbound webhooks for spec events
Constraints
Org-scoped isolation only — no cross-org data
API is read-only for v1
Signed webhook payloads for receiver verification
Reuse activity_events as the event spine
Stable view model for API responses
5-attempt retry with exponential backoff
No webhook ordering guarantees
Immediate key revocation
Acceptance criteria
Spec readable via API key with no Clerk session
Cross-org data isolation enforcement
Signed webhook reaches registered endpoint
Failed delivery retried and marked failed
Immediate revocation of API key
No internal JSON blobs in API responses

How it hangs together

goal

Org-scoped machine authentication

  • constrainsOrg-scoped isolation only — no cross-org data
  • constrainsImmediate key revocation
  • validatesSpec readable via API key with no Clerk session
  • validatesCross-org data isolation enforcement
  • validatesImmediate revocation of API key
goal

Read-only spec REST API for machines

  • constrainsOrg-scoped isolation only — no cross-org data
  • constrainsAPI is read-only for v1
  • constrainsStable view model for API responses
  • validatesSpec readable via API key with no Clerk session
  • validatesCross-org data isolation enforcement
  • validatesNo internal JSON blobs in API responses
goal

Verifiable outbound webhooks for spec events

  • constrainsSigned webhook payloads for receiver verification
  • constrainsReuse activity_events as the event spine
  • constrainsStable view model for API responses
  • constrains5-attempt retry with exponential backoff
  • constrainsNo webhook ordering guarantees
  • validatesSigned webhook reaches registered endpoint
  • validatesFailed delivery retried and marked failed
acceptance

Spec readable via API key with no Clerk session

  • implementsAdd API key auth and GET endpoints for specs, blocks, and tasks
acceptance

Cross-org data isolation enforcement

  • implementsAdd API key auth and GET endpoints for specs, blocks, and tasks
acceptance

Immediate revocation of API key

  • implementsAdd API key auth and GET endpoints for specs, blocks, and tasks
acceptance

No internal JSON blobs in API responses

  • implementsAdd API key auth and GET endpoints for specs, blocks, and tasks
acceptance

Signed webhook reaches registered endpoint

  • implementsAdd webhook registration and signed delivery with retry
acceptance

Failed delivery retried and marked failed

  • implementsAdd webhook registration and signed delivery with retry

What the gate released

Add webhook registration and signed delivery with retry

Build endpoints to register/update/delete/list webhook URLs per org (storing the target URL and signing secret). Build a delivery worker that reads spec lifecycle events from the activity_events table, constructs an HMAC-signed payload, and POSTs to the registered URL. On failure (5xx or unreachable), retry with exponential backoff up to 5 attempts. After the 5th failure, mark the delivery permanently failed and persist per-attempt details (HTTP status, response excerpt, error). Expose delivery status via a GET endpoint so integrators can inspect outcomes. No ordering guarantees — events may arrive out of sequence. Assertions: a spec event reaches a registered endpoint with verifiable HMAC signature; an unreachable endpoint results in 5 recorded attempts with final 'failed' status; delivery payloads use the same stable view model as the read API. Automated assertions: - Signed webhook reaches registered endpoint - Failed delivery retried and marked failed — does not retry further

Add API key auth and GET endpoints for specs, blocks, and tasks

Build API key management (create/revoke per org) and extend the Hono auth middleware to accept API keys alongside Clerk JWTs as an alternative auth path. Add GET endpoints returning a stable view model for specs, editor blocks (id/type/text/sortOrder — no attrs), and spec tasks. Cross-org requests are refused (not an empty response). Revoked keys stop authenticating immediately with no propagation delay. Every response body contains no raw internal JSON from spec_versions.content or spec_editor_blocks.attrs. Automated assertions: - Spec readable via API key with no Clerk session - Cross-org data isolation enforcement — does not exist' - Immediate revocation of API key — No period where the revoked key still works - No internal JSON blobs in API responses

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.