Imported from krus210/archspec (
skills/architecture-investigate/SKILL.md). Install upstream withnpx skills add krus210/archspec --skill architecture-investigate. Copyright stays with the author.
architecture-investigate
Invoking this skill. Claude Code: /archspec:investigate. Codex:
type $architecture-investigate or pick it from /skills. opencode: it loads via the
skill tool, or /archspec/investigate if you installed the command files. Under
Codex/opencode there is no CLAUDE_PLUGIN_ROOT; before the first bash block,
export ARCHSPEC_SKILL_DIR=<the absolute path your host shows for this skill>.
Read-side workflow. Read-only for code and contracts — the only file it writes is the persisted plan artifact docs/plans/<date>-<slug>.archplan.md (step 9b). Produces:
- A short summary of the relevant slice of
SERVICE_MAP.yaml. - Clarifying questions for every ambiguity the prompt and the contract do not already settle.
- An inline Mermaid sequence diagram showing only the change the user is proposing.
- A bulleted list of YAML edits the user should make before writing code.
- A persisted, independently-reviewed plan artifact that
/archspec:implementconsumes.
Output contract
Every run should end with the same shape, so the user can act on it without guessing what is next:
- Contract slice — 5-8 lines citing exact field paths from
docs/SERVICE_MAP.yaml. - Reference cross-check — when a reference/golden architecture spec was supplied, a short note on where the proposed event names, RPC names, dedup keys, and invariants agree with or deviate from it. Deviations are called out, never silently renamed away.
- Open questions — only the ambiguity dimensions not already answered by the prompt or contract.
- Change diagram — chat-only Mermaid
sequenceDiagramscoped to the proposed change (sync->>vs async-)arrows,alt/elsefor terminal branches, new interactions marked%% new); a flowchart only for intra-service branch logic. - YAML patch — unified-diff snippet the user can apply before coding; no files are edited by this skill.
- Event/key fan-out — from a scan of the full
SERVICE_MAP.yamlset, the complete list of producers and consumers for each new or changed event and dedup/join key, with each dead-end branch's terminal state + notification; undetermined fan-out marked# UNCONFIRMED. - Invariant/deviation notes — explicit callouts when the proposal touches ownership, write path, or declared invariants.
- State-ownership map — a table with one row per piece of persistent state the change creates, mutates, or transitions:
State touched | System-of-record service | Where it lives (domain/aggregate) | Write originates via | Deviation?. Emitted whenever the change touches persistent state; it is the forcing artifact for the State ownership clarify dimension, the same way the fan-out trace forces every event subscriber to be named. - Risk register (
edge_cases) — every gap, deviation,# UNCONFIRMED, and join-key risk surfaced above, restated as a concreteedge_cases[]entry inside the YAML patch (id+ adescriptionthat carries the given/when/then + atest:path). This is the bridge that carries a finding into code: a sentence in chat is forgotten the moment the plan step takes over, but anedge_cases[]entry persists in the contract and DET-003 blocks the commit until its test file exists. - Self-review — one line in the literal shape
Self-review: <N> pass(es), <findings or "no findings">recording the loop result (always emitted). - Persisted archplan — the full output contract written to
docs/plans/<YYYY-MM-DD>-<slug>.archplan.md(step 9b), so the plan survives the chat and/archspec:implementcan check code against it. - Plan review — one line in the literal shape
Plan-review: APPROVED after <N> round(s), <summary>recording the independent review gate result (step 9c, always emitted). - Definition of done — an explicit checklist stating that the change is not done on a green build: it is done only when
/archspec:validate(and, for cross-service work,/archspec:check-architecture) is green and everyedge_cases[]entry added above has a test that exercises it. - Next loop —
/archspec:implement <archplan>(applies the YAML edits, runs/archspec:sync, implements with conformance gates, then/archspec:validate+/archspec:check-architecturewhen the change spans services).
When to run
- User asks "let's add X", "investigate Y", "how does Z work?".
- Before any new feature or bugfix that touches an endpoint, dependency, event, or aggregate.
- User typed
/archspec:investigate.
Procedure
-
Locate the contract:
test -f docs/SERVICE_MAP.yaml || echo "no SERVICE_MAP.yaml — run /archspec:init"If missing, stop and tell the user.
-
Read the slice that matters. Read
docs/SERVICE_MAP.yaml. Identify which sections relate to the user's question:User mention Relevant sections endpoint, route, handler api.endpoints,architecture_rules.required_layerscall, dependency, downstream dependencies.downstream,dependencies.storageevent, kafka, topic events.published,events.consumed,consistency.write_pathaggregate, lock, conflict concurrency.aggregates,consistency.bounded_aggregateretry, fallback, failure dependencies.downstream.sync.*.fallback,failure_modes
2a. Optional: ingest a reference / golden architecture spec. A feature prompt rarely states the naming conventions or the out-of-prompt invariants the team already expects (e.g. "reassignment reuses the initial match snapshot — do not re-run the search"; the canonical subject is offer.declined, not a freshly invented task.offer_rejected). Ask the user once — use your host's structured multiple-choice tool if it has one (Claude Code AskUserQuestion, opencode question), otherwise present the options as a numbered list and stop until the user replies:
"Is there a reference / golden architecture spec for this change — a design doc, an RFC, a target-state diagram, a naming convention? Paste a path, or answer
skip."
- Path provided — Read it and keep it as conversation context only (do not parse it into the contract). Use it to cross-check the design you are about to draw: event/topic names, RPC names, dedup/idempotency keys, and the invariants it mandates. When your proposal diverges from the reference, name the divergence in the Reference cross-check note — never silently rename
offer.declined→task.offer_rejectedor quietly drop an invariant the spec requires. The spec is a hint, never an override: if it contradicts the liveSERVICE_MAP.yamlor the code, prefer reality and say so. skip/ none — proceed, and note in the output that no reference spec was supplied, so naming and any out-of-prompt invariants were inferred from the contract alone (a known blind spot — the prompt cannot be assumed complete).
-
Clarify ambiguities before proposing anything (gate). A feature prompt is almost never complete enough to design a cross-service change. Walk the checklist below and, for every dimension the prompt and
SERVICE_MAP.yamldo not already answer unambiguously, ask the user (use your host's structured multiple-choice tool if it has one — Claude CodeAskUserQuestion, opencodequestion— otherwise present the options as a numbered list and stop until the user replies; ask several questions together in one message where possible; do not proceed until the user answers). Asking is read-only — it modifies nothing.Dimension What to pin down Failure it prevents Entry point & ownership Which service receives the trigger? When the trigger comes from an external actor, does the request enter through the public edge (api-gateway / BFF) and not just the internal owner — i.e. is the whole reference flow (client → gateway → owner) wired, not only the last hop? Does an existing endpoint/flow already own this action (a decline/cancel path)? (Which service's write path must originate the state mutation is the State ownership dimension below — don't discharge it here.) Bolting the trigger onto the wrong service, or leaving no public entry point. State ownership (system-of-record) Enumerate every piece of persistent state this change creates, mutates, or transitions (status fields, counters, sets like declined_worker_ids). For each, name the service whosedomain/aggregate is its system-of-record — the write must originate in that service via itsconsistency.write_path(a command + outbox/event), not a sync RPC from another service reaching in. If the field already lives in another service'sdomain/aggregate, that service owns the transition — route through it. Prefer the owner-applies-async-command shape over a sync RPC that mutates a foreign aggregate inline. Mark any cross-service write that bypasses the SoR# UNCONFIRMED: foreign-state mutation.An orchestrator service absorbing a state transition owned by the aggregate's service, bypassing its outbox and its reassignment counter. Trusted identity & actor Separate sub-question — do not consider it answered just because the entry point is settled. Is any identifier in the request payload (e.g. worker_id,user_id,account_id) used as the actor's identity instead of being derived from the authenticated session/caller? Whoever the action runs as must come from the caller's credentials, not the body. If the source is unproven, mark the field# UNCONFIRMED: trusted from clientand ask.A client spoofing another actor by passing their id in the request body (declining an offer on behalf of any worker). Async state & ordering Does the trigger read or mutate state that a different async path writes (e.g. it checks assigned_worker, which amatch.foundconsumer sets)? Can the trigger arrive before that write lands, or can a stale/replayed copy of that event arrive after the trigger and overwrite what it just changed?Trigger no-ops because the state it depends on isn't there yet; a late or duplicate event resurrects state the trigger just cleared. Delivery semantics & idempotency Is the trigger event at-least-once? What is the dedup key, and which side dedups? If the dedup key changes (e.g. task_id→(task_id, attempt)), is the new key applied to every consumer of that event, not just the one you touched?Duplicate delivery double-counting attempts; a dedup key fixed in one consumer but left stale in another. Numeric limits & boundaries Exact meaning of every limit ("max 3 retries" = 3 after the first attempt, or 3 total?). Off-by-one in retry/attempt caps. Identity & join keys Which identifiers join entities across services ( city_idvs free-text city,task_idvsmatch_id)? When a lookup can fail to resolve (free-text "Saint Petersburg" never maps to acity_id), what happens — and is that failure silent?Joining on the wrong field; a lookup that silently degrades the result for every row. Failure & terminal paths For every branch that can dead-end (limit exhausted, and no candidates found, and a downstream returns empty), which state transition and which notification happen? Not just the happy path, and not just one terminal. Silent terminal failures; a dead-end branch that logs-and-returns, leaving the aggregate stuck. Write-path conformance Must the new publish/persist go through this service's consistency.write_path.pattern(e.g.outbox)? May the publish error be swallowed?Publishing straight to the broker outside the outbox; returning success after a swallowed error. Rules:
- Treat every sub-question inside a dimension as its own checkbox. Answering the entry-point half ("it enters via api-gateway") does not discharge the identity-trust half ("but where does
worker_idcome from?"). A dimension is cleared only when every clause in it is answered or quoted from the contract — a dimension that bundles two questions is the most common place one half gets silently skipped. - Skip a dimension only when the contract already answers it — quote the field that does.
- Never invent an answer and bake it into the proposed YAML. A guessed dedup key or ownership boundary in the contract is worse than an open question: it looks decided.
- If the user is unavailable, list the open questions explicitly in the output and mark the affected YAML lines
# UNCONFIRMEDinstead of asserting them.
- Treat every sub-question inside a dimension as its own checkbox. Answering the entry-point half ("it enters via api-gateway") does not discharge the identity-trust half ("but where does
-
Summarise what the contract says, in 5–8 lines. Quote field paths (e.g.
consistency.write_path.pattern: outbox) so the user can verify. -
Draw a chat-only Mermaid sequence diagram of the proposed change. Embed it in the response — do not write code or contract files. For any flow that crosses a service boundary the diagram must be a
sequenceDiagram, not a flowchart: a sequence diagram is the only Mermaid form that shows who calls whom, in what order, sync vs async, and where each branch terminates — the exact properties a reviewer needs to catch a sync RPC where an event belongs or a dead-end with no terminal state. Aflowchartis allowed only as the named exception for intra-service branch logic (one service's internal decision tree, no cross-service arrows).Conventions:
- One
participantper service, plus one for the broker (NATS/Kafka) when events are involved, plus the external actor (Client) when the trigger is external. ->>for synchronous calls (HTTP/gRPC);-)for asynchronous event publish/consume — never draw an event as a sync arrow.alt/elseblocks for every limit and terminal branch (limit exhausted, no candidates, empty result) — eachelseleg must end in a state transition and a notification, mirroring the Failure & terminal paths dimension.- Mark new or changed interactions with a trailing
%% newcomment or aNote overso the delta is visible.
sequenceDiagram participant C as Client participant GW as api-gateway participant TS as task-service participant N as NATS participant MS as matching-service C->>GW: POST /tasks/{id}/decline-offer GW->>TS: DeclineOffer(task_id) %% new alt reassignment_count <= limit TS-)N: offer.declined %% new (outbox) N-)MS: offer.declined else limit exhausted TS-)N: task.failed %% new — terminal state + client notification end - One
-
Propose YAML edits as a unified-diff snippet. Don't apply them — let the user accept, tweak, then run /archspec:sync. Example shape:
api: endpoints: + - name: BulkCreateListings + protocol: HTTP + idempotency: + required: true + key_source: "header: X-Idempotency-Key" + storage: "redis: idemp:{key}" + contract: "api/openapi.yaml#/paths/~1listings~1bulk/post" + sla: { p99_latency: "300ms", availability: "99.9%" }Deviation guard: when an edit crosses an existing boundary — changes who owns an action, adds a publish that sidesteps
consistency.write_path.pattern, or relaxes an entry inservice.invariants/consistency.cross_service_invariants— call it out in one line ("this deviates from<field>:<why>") and get explicit confirmation. A generated contract line must not silently ratify a design the user never affirmed. -
Trace every new or changed event and key across all producers and consumers. Step 2 let you read only the slice that matters; this step is the exception — you must scan the full
SERVICE_MAP.yamlset (every service's contract in the monorepo), because the "dedup fixed in one consumer but missed in another" class is invisible from a single slice. Operationally, for each event you add or change and each dedup / idempotency / join key you change:- Grep/scan every contract for
events.published,events.consumed, and the topic/event name, plus anyidempotency/ dedup / join-key field that references it. - Producers — who emits it, and through which write path.
- Consumers — every subscriber found by the scan, and for each: does it dedup on the (possibly new) key? does it order-depend on another event? does it even have a handler for this event, or does it silently drop it?
- Dead-ends — branches where processing stops (no candidates, empty result, exhausted limit). Each must end in a state transition and the notification the terminal-path dimension demands.
If the fan-out cannot be fully determined from the contracts (a consumer's dedup key is undocumented, a topic's subscriber set is unclear), it is not a free pass — raise it as an open question and mark the affected YAML lines
# UNCONFIRMEDrather than assuming the fan-out is complete.A single event must not carry two unrelated semantic roles. One
task.reassignment_requestedused both as the matching trigger and as a "we are reassigning" client notification fires the notification before a new worker is actually found — that is prohibited, not merely discouraged. Either propose separate events (one trigger, one notification emitted only after the outcome is known) or, if you cannot resolve the split yourself, block the YAML patch and surface it as an open question. Do not ship a single dual-role event with only a warning attached. - Grep/scan every contract for
-
Flag invariants the user must preserve, citing
service.invariantsandconsistency.cross_service_invariants.
8a. Materialise every risk as an edge_cases[] entry — the investigate→code bridge. This is the step that stops a finding from dying as chat prose. The strongest investigate output is worthless if it never reaches the code: an agent that writes the plan next, then dispatches implementation subagents, does not re-read this chat — it reads the contract. So for each gap, deviation, # UNCONFIRMED, join-key risk, and dead-end terminal you surfaced in steps 3–8, add a concrete edge_cases[] entry to the YAML patch:
edge_cases:
- id: EC-014
description: "worker city joins to geo by city_id, not free-text city_name; an unresolved city_id must fail loudly, never silently collapse the distance tie-breaker to a default"
test: "services/matching-service/usecase/matching_geo_test.go::TestEC014"
Why an edge_cases[] entry works where prose does not:
- It persists in the contract and renders into
ARCHITECTURE.md, so the plan-writer and the implementation subagents see it without the chat history. - The
test:path is enforced by DET-003 at commit time — the commit is blocked until that test file exists. A risk you cannot yet test is a risk you must at least name. - Deleting it later trips DET-007 (no edge_case removal without an ADR), so the guard cannot be quietly dropped during implementation.
Schema is closed — exactly three fields: id matches ^EC-\d+$ (use the next free EC-NNN); description is one line carrying the given/when/then; test is a path::TestName reference. The single highest-value entry is usually a join-key risk (the field a lookup resolves on) or a reuse-vs-recompute invariant (a snapshot that must not be recomputed) — exactly the findings that read as obvious in chat and then vanish. Ownership findings belong here too: when a state transition must originate in its system-of-record service, encode that as an edge_cases[] entry (e.g. "the rejection transition and its reassignment-count increment must originate in task-service via its outbox; a sync RejectOffer mutating them in matching-service is a deviation") so the plan-writer cannot route the write through the wrong service. If a risk genuinely cannot be expressed as a test, it stays an open question, not a silent omission.
8b. Emit the State-ownership map. Before the YAML patch is final, build the table named in the Output contract: one row per persistent-state item the change touches — State touched | System-of-record service | Where it lives (domain/aggregate) | Write originates via | Deviation?. This is the forcing artifact for the State ownership dimension (step 3), the same way the fan-out trace (step 7) forces every event subscriber to be named. The highest-signal row is a status/counter/set that a different service's handler tries to write: its "Write originates via" must be the system-of-record's own write path, and any sync-RPC alternative goes in the "Deviation?" column for explicit confirmation.
-
Self-review loop — turn the checklist on your own draft, not just the prompt. The clarify gate (step 3) interrogates the requirements; this step interrogates the design you just drew. Re-read your own diagram and YAML and walk the checklist again, plus the anti-pattern list below. Loop until a full pass surfaces nothing new — the first pass routinely does.
Anti-patterns to hunt in your own proposal:
- A state read in the trigger that races an async write (Async state & ordering): does the trigger rely on a field (e.g.
assigned_worker) that arrives via a separate event consumer, and can the two reorder? - A changed dedup / join key applied to one consumer but not its siblings (Delivery semantics): did the new
(task_id, attempt)key reach every handler of that event, including the one in the owning service? - A dead-end branch that logs-and-returns with no state transition or notification (Failure & terminal paths): what marks the entity terminal when the pipeline finds no candidates?
- A fallback that silently changes the result (Identity & join keys): does an unresolved lookup quietly collapse every value to a default (e.g.
maxDistance), killing a tie-breaker with no signal? - One event with two semantic consumers where one is a client notification fired before the outcome is known (step 7).
- An external trigger with no public-edge entry point, or one that trusts a client-supplied identity (Entry point & ownership / Trusted identity & actor).
- Recompute instead of reuse in a retry/reassignment loop: does the loop re-run an expensive or non-deterministic pipeline (a text analysis, a candidate search) on every attempt, when the result should be snapshotted on the first attempt and reused? Re-running both wastes the downstream call and lets the candidate set drift between attempts.
- Attempt/version identity reconstructed from consumer memory: does a consumer infer "which attempt is this" from its own in-memory state instead of reading it from the event payload? On restart or replay that state is gone and a stale event re-triggers the flow. The attempt/version must travel in the durable event.
- Two events from one handler that must be consistent but are appended separately: when a handler emits two events that must both land or neither (a worker offer and a client "you were reassigned" notice), are they written in the same outbox transaction, or can a crash between the two appends deliver one without the other?
- State transition and its notification not atomic: does a terminal notification ("task failed") fire even when the state transition it announces (
FailTask) failed or was swallowed? The notification must be gated on the transition actually committing. - Terminal-state invariant narrower than the real input states: is a transition guard (e.g. "fail only from
open") tighter than the states the entity can actually be in when the branch fires (it may beassignedby then), so the terminal silently no-ops? - A dead-end on the first attempt, not just the last: the first hop can also dead-end (the initial match finds no candidates). Does that first-attempt empty result transition the entity to a terminal state and notify, or does it log-and-return and leave the entity stuck?
- Foreign-state mutation via a sync RPC: does a new synchronous endpoint on service A mutate state whose system-of-record is service B (a status, a counter, a set), instead of B owning the transition through its own write path? Re-derive the State-ownership map (step 8b) for the drafted design and confirm each write originates in its SoR — an orchestrator reaching into another aggregate is a deviation, not a shortcut.
- Idempotency asserted but not traced under replay: for each new command/endpoint, follow a literal duplicate invocation through the body — does the second call re-execute the side effect (create a new attempt, re-increment a counter, re-emit an offer), or is there a CAS / dedup / state guard that makes it a no-op? A declared "idempotent" invariant is not the same as a code path that is, under replay.
- N+1 where a batch endpoint exists: does a loop call a downstream's singular method per item (
GetDistance) when that downstream exposes a batch variant (GetDistancesBatch,GetWorkersBatch)? Each iteration is a network round-trip the batch call would collapse — collect inputs and call the batch method once. - A finding left as prose with no
edge_cases[]entry (step 8a): did every gap / deviation /# UNCONFIRMED/ join-key risk become a testableedge_cases[]entry or an explicit open question, or is one still sitting in the narrative where the plan step will scroll past it?
Record the outcome as a one-line note in the output using the literal prefix and shape
Self-review: <N> pass(es), <what was found and fixed, or "no findings">— write the count grammatically (1 pass,2 passes). E.g.Self-review: 2 passes, found+fixed premature client notify and a stale dedup key; no remaining findings. Always emit this line, even on a clean first pass (Self-review: 1 pass, no findings). If a finding can't be resolved without the user, raise it as a new open question rather than shipping it. - A state read in the trigger that races an async write (Async state & ordering): does the trigger rely on a field (e.g.
9b. Persist the plan as an .archplan.md artifact. Write the complete output contract (contract slice, reference cross-check, open questions, sequence diagram, YAML patch, fan-out trace, state-ownership map, edge_cases register, self-review line) to docs/plans/<YYYY-MM-DD>-<slug>.archplan.md. This is the only file this skill writes — code, contracts, and generated docs stay untouched. Why a file and not chat: the agent that writes the coding plan and the subagents that implement it do not re-read this conversation — in task_3 the plan step silently flipped the topology to sync RPC, invented a SearchBySkills method, and dropped the snapshot-reuse invariant precisely because the investigation lived only in chat. The artifact is the contract /archspec:implement later checks the code against.
9c. Independent plan review — a gate, not a courtesy. Self-review by the author's own context is weak: it re-reads its own assumptions. Dispatch an independent reviewer with a fresh context (a subagent if your host supports one, otherwise a separate CLI session / new chat with no history) and give it only: the .archplan.md artifact path, the list of every SERVICE_MAP.yaml in the repo, proto/contract directories, and the reference spec path if one was supplied. Its instruction is to adversarially try to reject the plan against this rubric — one verdict per item, with file:line / field-path evidence:
- Requirement trace — every stated requirement of the task maps to a concrete plan element (event, endpoint, field, edge case); name the plan element per requirement. A requirement with no plan element is a REVISE.
- No invented methods — every downstream method the plan calls exists in the callee's
SERVICE_MAP.yaml/ proto. A method that does not exist anywhere is a REVISE, no matter how plausible its name. - Reuse vs recompute — any expensive or non-deterministic pipeline (text analysis, candidate search) consumed by a retry/reassignment loop is snapshotted on the first attempt and reused; a plan that re-runs it per attempt must justify why, or REVISE.
- Batch endpoints — wherever the callee exposes a batch variant, the plan uses it; per-item loops over singular calls are a REVISE.
- Topology & ownership — triggers and notifications flow as events per the owner's
consistency.write_path; no sync RPC mutates a foreign aggregate; each state transition originates in its system-of-record. - End-to-end field threading — every new field is traced from the public entry point (gateway proto / HTTP body / seed & fixture data) through the owner to every consumer; a field added to an internal proto but absent from the public edge is a REVISE.
- Dedup & atomicity — dedup keys cover every consumer of the changed event; dedup marking is atomic with (or after) the side effects, never before. Per-attempt identity: name every identifier that is reused across retry/reassignment attempts (a match id, an offer id) — each such ID must either be regenerated per attempt or be part of every consumer's dedup key; an ID that survives into attempt N+1 while any consumer dedups on it silently swallows the retry. A plan line like "dedup on remains, no key change" in a retry loop is a REVISE, not a reassurance.
- No
# UNCONFIRMEDsurvives — every marker is resolved, asked, or explicitly carried as an open question + edge_cases entry; an unresolved marker baked into the final YAML is a REVISE. - Terminal branches — every dead-end (limit exhausted, no candidates, empty result, including the first attempt) ends in a state transition and a notification.
- Diagram conformance — the sequence diagram and the YAML patch describe the same design (same events, same sync/async split, same terminal branches).
The reviewer returns APPROVED or REVISE + findings. On REVISE: fix the artifact, then dispatch a new reviewer (fresh context again). Loop at most 3 rounds; if findings remain after 3 rounds, stop and surface them to the user as open questions instead of shipping the plan. Always emit the literal line Plan-review: APPROVED after <N> round(s), <one-line summary of what the rounds caught>.
Solo degradation is not approval. If you cannot dispatch subagents, the gate degrades to re-reading your own plan — the exact weakness this step exists to fix. Walk the rubric anyway, but emit Plan-review: SELF-ONLY after <N> pass(es), <summary> instead of APPROVED, so the downstream implement phase and the human can see the plan was never independently reviewed.
- End with the full loop, not just sync. The contract is only safe if code is checked back against it. Spell out the path: apply the YAML edits →
/archspec:sync→ implement →/archspec:validate(runs the behavioural linters — outbox, idempotency, optimistic-locking) →/archspec:check-architecturefor any change that spans more than one service. A green build or passing unit tests is not a substitute for/archspec:validate: those tests usually cover only the happy path that was just written.
Spell out the Definition of done as a literal checklist, because the agent that closes the branch is often a separate finishing-a-development-branch pass that knows nothing about archspec and will otherwise stop at green unit tests:
- every
edge_cases[]entry added in step 8a has a test that actually exercises it (not merely a file that exists); -
/archspec:validateis green, or every finding has anexceptions[]entry with a reason; -
/archspec:check-architectureis green for cross-service changes; - every
# UNCONFIRMEDmarker is now resolved, or carried into anedge_cases[]entry or an ADR.
A green go build / go test clears none of these boxes.
Do not
- Modify any file. This skill is read-only.
- Skip the clarify gate (step 3) because "the prompt looks clear". Cross-service prompts that look clear are exactly where ownership and idempotency get assumed wrong.
- Skip the self-review loop (step 9) because the draft "looks complete". The bugs that survive the clarify gate live in the design you just drew — async races, a dedup key fixed in only one consumer, a dead-end branch, an event doing two jobs.
- Invent answers to the clarify checklist and write them into the YAML as if decided.
- Let a finding live only as chat prose. Every gap, deviation,
# UNCONFIRMED, and join-key risk must become anedge_cases[]entry (step 8a) or an explicit open question — never a sentence the plan step will scroll past. - Let an orchestrator service silently absorb a state transition owned by another aggregate's service (its status, its counter) via a sync RPC. The write must originate in the system-of-record, or the deviation must be named.
- Silently rename an event/RPC or drop an invariant that a supplied reference spec mandates. Name the divergence (step 2a) and let the user decide.
- Run /archspec:validate here — that is a separate command for after the code change.
- Read code that is unrelated to the user's question. Stay scoped to the contract.