Skip to content

Architecture

mairp edited this page Sep 19, 2026 · 2 revisions

Architecture

Specstride is three roles communicating only through files under .specstride/gates/. No role calls another directly; the on-disk gate files are the contract.

The loop

orchestrator.sh   (derives the current phase N from disk; reads the spec)
  │
  ├─(1) PROPOSER — run a headless coding-agent loop for phase N until it writes
  │       .specstride/gates/GATE<N>-EVIDENCE.md (atomically), then the loop exits.
  │
  ├─(2) CRITIC — lib/critic.py reads phase N's acceptance criteria + the evidence,
  │       does a read-only grounding pass over the files the evidence cites, and
  │       asks an LLM for a strict verdict:
  │           APPROVED → writes an empty .specstride/gates/GATE<N>-APPROVED marker
  │           REJECTED → writes .specstride/gates/GATE<N>-FEEDBACK.md (the specific gaps)
  │
  ├─(3a) APPROVED → git-checkpoint the workdir, N := N+1, back to (1).
  └─(3b) REJECTED → archive the rejected evidence, re-run the proposer for the
           SAME phase with the feedback. Bounded by MAX_REJECTS; on exceed, halt
           and leave everything on disk for a human.

The roles

Literal role names are used everywhere — code, files, flags, env vars. Three scripts, five passes:

Role Script Job
Orchestrator orchestrator.sh Derives the current phase from GATE* markers, drives proposer↔critic, checkpoints on approval, archives on reject, tracks each phase's unmet-criteria signature, enforces budgets/locks.
Proposer proposer.sh Runs a headless coding-agent CLI in a fresh-context loop until phase N's GATE<N>-EVIDENCE.md exists.
Critic lib/critic.py Reads criteria + evidence, grounds cited files (read-only, byte budget scaled to the backend's context window), asks the LLM for a nonce-bound verdict.
Diagnostician lib/critic.py --diagnose Fires once per NEW unmet-criteria signature: same critic backend, the FULL untruncated cited files, no grounding budget. Writes GATE<N>-HINT.md (CASE: GROUNDING or CASE: REAL-GAP + the fix). Advisory only. SPECSTRIDE_DIAGNOSTICIAN=false disables.
Accelerator proposer.sh --role accelerator The attempt right after a new hint: the proposer with a prompt narrowed to the unmet criteria, the feedback, the hint and the evidence to splice. Once per hint, never twice in a row, counts toward MAX_REJECTS; writes GATE<N>-ACCELERATION.md for the next wide pass. SPECSTRIDE_ACCELERATOR=false disables.

Sequence

sequenceDiagram
    autonumber
    actor Human
    participant O as orchestrator.sh<br/>(orchestrator)
    participant P as proposer.sh<br/>(proposer · coding-agent CLI)
    participant A as proposer.sh --role accelerator<br/>(accelerator · same tools, narrowed prompt)
    participant C as lib/critic.py<br/>(critic · LLM gate)
    participant D as lib/critic.py --diagnose<br/>(diagnostician · same backend, no budget)
    participant FS as .specstride/gates/<br/>(on-disk contract)

    Human->>O: run -w WORKDIR -s SPECS.md
    O->>FS: derive phase N from GATE* markers
    Note over O: no stored counter — phase is derived

    loop until all phases APPROVED (or halt)
        alt attempt right after a NEW diagnostician hint<br/>(once per signature, never twice in a row)
            O->>A: narrowed prompt: ONLY the unmet criteria<br/>+ critic feedback + hint (primary instruction)
            activate A
            A->>FS: read GATE<N>-HINT.md + the archived (rejected) evidence
            A->>A: fix ONLY the unmet criteria<br/>(footprint rule: touch just the files they cite)
            A->>FS: write GATE<N>-EVIDENCE.md<br/>(previous evidence spliced, atomic)
            A-->>O: pass exits (test -f passes)
            deactivate A
            O->>FS: write GATE<N>-ACCELERATION.md<br/>(files this pass changed)
        else ordinary attempt
            O->>P: full phase prompt<br/>(+ feedback, hint, acceleration note if present)
            activate P
            loop until evidence exists
                P->>P: read PROGRESS.md, do the work
                P->>FS: write GATE<N>-EVIDENCE.md (atomic)
            end
            P-->>O: loop exits (test -f passes)
            deactivate P
        end

        O->>C: judge phase N (criteria + evidence)
        activate C
        C->>FS: read-only grounding pass over cited files<br/>(byte budget scaled to the backend's context window)
        C->>C: LLM verdict, nonce-bound
        alt APPROVED
            C->>FS: write GATE<N>-APPROVED (empty marker)
            C-->>O: VERDICT nonce: APPROVED
            O->>O: git checkpoint · N := N+1
        else REJECTED (attempt < MAX_REJECTS)
            C->>FS: write GATE<N>-FEEDBACK.md (the gaps)
            C-->>O: VERDICT nonce: REJECTED
            O->>O: unmet-criteria signature<br/>(task IDs the feedback names, or a prose hash)
            alt signature is NEW for this phase
                O->>D: diagnose phase N<br/>(full rejection history, same critic backend)
                activate D
                D->>FS: read the FULL, untruncated cited files<br/>(no grounding budget)
                D->>D: classify the stall:<br/>CASE: GROUNDING (restage the proof)<br/>or CASE: REAL-GAP (the concrete fix)
                D->>FS: write GATE<N>-HINT.md
                D-->>O: hint written (advisory — never approves or rejects)
                deactivate D
                Note over O,A: next attempt = ACCELERATOR
            else same signature as last time
                Note over O,P: next attempt = wide PROPOSER<br/>(reads feedback + hint + acceleration note)
            end
            O->>FS: archive stale evidence<br/>(+ feedback, hint, acceleration note)
        else MAX_REJECTS exceeded (accelerator attempts count too)
            C-->>O: still REJECTED
            O->>Human: halt (exit 2) — arbitrate
        end
        deactivate C
    end

    O->>Human: all phases approved (exit 0)
Loading

No file-watcher

Detection is deterministic, not event-driven. The proposer loop's gate is a plain test -f .specstride/gates/GATE<N>-EVIDENCE.md. Because that loop has already exited when control returns to the orchestrator, the orchestrator hands the critic the exact path — no race, no half-written file, nothing to poll. Evidence is written atomically (temp file + rename) so the critic never observes a partial write.

Phase is derived, never stored

There is no counter file. On every start the orchestrator scans the GATE* markers on disk and resumes at the first phase lacking a GATE<N>-APPROVED. Kill the run anywhere, rerun the same command, and it continues. --start-phase N overrides. This is what makes the loop crash-safe (see Hardening).

The Python components

All Python lives under lib/; the Bash entry points stay at the top level.

Component Role
lib/critic.py The critic — grounding pass + LLM verdict + nonce parsing
lib/specstride_spec.py The single spec-parsing source of truth (bash and critic both delegate) — see Spec Formats
lib/verification_plan.py Pre-loop VerificationPlan v1 derivation + test scaffolding
lib/agent_stream.py The proposer's stream-json tap that emits agent_* events
lib/present.py The live presenter (inline timeline + status card)
lib/ralph_loki_ship.py / lib/ralph_otel_ship.py The two telemetry shippers
lib/verdict_pins.py Verdict-parsing pins/guards

Next: On-Disk Contract · Hardening

Clone this wiki locally