news / engineering
state of loswf: the canonical reference

state of loswf: the canonical reference

a code-grounded read of how the factory is actually wired today — 20 agents, 15 phases, three layers of dispatch enforcement, an eight-ledger observability graph, and a 15-row discrepancy ledger against the SRS.

Factory at night

the gap that surfaced

specs/SRS.md was written in February. The pipeline it describes had 16 roles and a triage → intake → planning flow. The pipeline on main today has 20 roles, 15 phases, a Product universal gate that landed on 2026-05-20, and an observability surface that emits to eight-plus JSONL ledgers. The SRS was not wrong when it was written. It was wrong by the time you read it.

Issue #645 set out to close that drift. It closed on 2026-05-23 — completed. The output is a code-grounded research pass: 51 Facts, 12 Insights, 7 Recommendations, and a 15-row discrepancy ledger, plus a canonical architecture reference that supersedes the SRS sections it disagrees with. The reference lives at docs/ARCHITECTURE.md; this post is the engineering essay that goes with it. The 15 discrepancy rows are now individual tracked follow-ups against the SRS.

what the canonical reference does

Three jobs:

  1. Describe the factory as it runs. Eight sections — overview, layered architecture, roles, phases, flows, design primitives, observability, discrepancy ledger. Every section opens with the Fact / Insight / Recommendation IDs it cites against Atoms/645-findings.md. Anchor citations are file:line against the current main.
  2. Settle authority order. Code > docs/ARCHITECTURE.md > specs/SRS.md > specs/VISION.md. The SRS is no longer the single source for “how is the factory wired”; the SRS is the intent document, the architecture reference is the observed document, and the discrepancy ledger is where they meet.
  3. Make drift a tracked artifact. Every disagreement between SRS and code is a numbered row (D1–D15), a severity, a recommended fix, and a follow-up issue. Drift is no longer the kind of debt you discover three iterations after it bit you.

Six deep-dives sit under docs/architecture/ for the parts that needed their own page: the state machine, the three-layer dispatch guard, shadow mode, runtime adapters, the renderer + shared prompts, and observability.

Layered architecture

the shape of the factory

loswf2 is the agency operating engine, not a product. The agency takes client intent — a vision, a roadmap, an issue — and turns it into shipped code without an operator at the console.

The architecture has four layers, and they hold each other apart through narrow contracts:

  • Distribution. bin/release.sh is the sole writer of plugin/ and docs/CHANGELOG.md. Hand-edit either between releases and release-bot expectations break. The plugin syncs from .claude/ and factory/; project-scope installs (hooks, ADWs, settings) go through a separate top-level installer.
  • Runtime adapter. A loswf_runtime env knob (default claude, codex available; written all-caps in the actual env) chooses which adapter runs the agent loop. Adapters expose a five-operation contract: headless-run.sh, doctor.sh, run-interactive.sh, enforce-validators.sh, and bootstrap/install-project-scope.sh. Claude ships 5 scripts; Codex ships about 30 — the extra surface is the skill/agent loader Codex lacks natively.
  • Shared prompts. factory/prompts/{roles,commands,skills}/ plus factory/manifests/roles.yaml. Four roles are promoted — authored once, rendered into both runtimes by bin/render-runtime-assets.mjs, and verified by a parity validator that fails CI on drift. The remaining 16 roles still ship as per-runtime files. Promotion proceeds one role at a time.
  • Orchestration. A sweep loop (adws/adw_sweep.sh + _lib.sh) walks the config-declared sweep.sequence, fans out parallel builders, and reconciles GitHub state into .loswf/state/. Twenty-two sequence entries are declared today; some ship disabled. Sequence is data. Reordering a phase is a YAML edit, not a code patch.

Twenty agents, fifteen phases

twenty agents, fifteen phases

The roster has expanded since the SRS was written. Twenty agents live under .claude/agents/loswf-*.md: architect, architect-review, builder, curator, decomposer, designer, documenter, editor, escalation, harvester, intake, investigator, plan-reviewer, planner, product, red-team, researcher, reviewer, scout, setup.

Thirteen of those are phase-bound — they operate from a specific factory:phase:* label and the orchestrator routes work to them when an issue carries that phase. The other eight are unbound: curator runs on a cadence, harvester drains an inbox, setup is install-time only, and so on.

The fifteen phases are declared in .loswf/config.yaml lines 116–131. They are not invented in code; they are a YAML list the orchestrator walks. The Product universal gate (policy 2026-05-20) is the most recent shape change: every issue that survives intake — except bug, spike, investigate, and the research-derived path — flows through product for either a PRD v1 (features, research) or a lightweight signoff (chore, refactor, docs) before reaching planning.

Three-layer dispatch guard

three layers, one predicate

The state machine is enforced at three layers but the predicate is the same — validate_dispatch. A transition that is wrong in one place is wrong in all three.

  • Layer 0 is the orchestrator. loswf_post_agent.py reads markers (<!-- loswf:product:prd-v1 -->, <!-- loswf:research:complete -->, shadow verdicts) and decides the label change.
  • Layer 1 is the DISPATCH table inside the orchestrator — fourteen entries mapping (phase, marker) to (next_phase, agent).
  • Layer 2 is the PreToolUse hook _dispatch_gate.py. Every Bash invocation that looks like gh issue edit … factory:phase:* is screened before it runs. Exit 70 denies; exit 71 signals an internal gate error and applies needs-attention. The only bypass is the explicit loswf_skip_dispatch_gate=1 env knob — reserved for emergency recovery. loswf_self_hosted=1 does not bypass it. The two flags are deliberately separate.

Dispatch resolution itself prefers receipts. A runtime-neutral phase-receipt schema (factory/schemas/phase-receipt.schema.json) is the canonical signal that a phase has advanced. The legacy transcript-scan fallback is still opt-in at .loswf/config.yaml:221-222 for migration safety. Receipts are primary; the transcript path is legacy debt the renderer migration will eventually retire.

shadow mode is the promotion path

Every gating lens in loswf2 lands in shadow before it ever blocks promotion. The per-step config knob is gating: shadow | enforce. In shadow mode the lens emits a two-line marker (<!-- loswf:shadow:<phase>:verdict -->\n<keyword>), an aggregator parses the corpus, and the catch-rate-vs-noise distribution gets tuned in the JSON before the lens flips to enforce.

This is how plan-review got tuned. How reviewer verdicts got tuned. How architect-review will get tuned — it ships disabled today, opt-in via gating: shadow on the sequence step. The promotion checklist is in docs/architecture/shadow-mode.md.

R-A007 calls for bin/doctor.sh to surface catch-rate inline so the shadow → enforce decision does not require a separate aggregator invocation. That is filed as a follow-up.

Observability is a graph

observability is a graph

There is no single feed. Eight-plus JSONL ledgers under .loswf/state/ carry pieces of the truth, and a reconciler stitches them by issue number on every sweep:

  • events.jsonl — coarse events.
  • metrics.jsonl — schema_v1 metrics for dashboards.
  • receipts.jsonl + per-run dir — structured per-phase receipts.
  • devlog-queue/*.json — pending devlog writes.
  • devlog-triggered.jsonl — dedup checkpoint (kept separate from the orchestrator’s dispatched.jsonl — conflating either has caused double-publishes in the past).
  • shadow/*.json — aggregator output.
  • subagent_timing.jsonl + subagent_closed.jsonl — SubagentStop hook output.
  • sessions/*.json — per-session statusline tokens.

Every event carries (code, retry_class, category). retry_class drives the reconciler’s re-enqueue decision. category is the dashboard rollup. Reconcile errors never block the sweep — the system degrades visibly, applies needs-attention, and the sweep moves on.

the fifteen discrepancies

The ledger is the most operationally useful thing in this pass. Three Highs:

  • D1. SRS still draws triage → intake → planning. The code routes non-bug/spike/research/investigate through product first.
  • D2. SRS lists 16 roles. The repo has 20.
  • D3. loswf_label_transition.py’s LEGAL table is missing the researching, product, red-team, and architect-review phases. The orchestrator bypasses LEGAL by design (it is the trusted writer), but the validator should still know about every active phase.

Seven Mediums and five Lows fill out the table. Each row is filed as a docs(srs): or chore(factory): issue with no factory:phase:* label — the curator triages on the next sweep, no intake storm. The authority order (code > ARCHITECTURE.md > SRS) is what lets the ledger sit comfortably as a tracked artifact rather than a backlog to clear.

#647 — what shored the foundation

A week before this pass landed, PR #647 fixed four primitives the research above depended on observing reliably:

  • C1. snapshot_phases() was using AND-semantic GitHub label search; the empty corpus broke the idle gate, which in turn broke builder fan-out accounting. Replaced with a single full-listing query and a client-side jq filter.
  • C2. Three shell call-sites still ran gh pr list --search "linked:$issue" after the Python equivalent had been patched in #305. Ship and review were the affected phases — a wrong-PR routing risk. Ported find_open_pr_number to bash and replaced the call-sites.
  • C3. SHADOW_VERDICT_MARKER_PREFIX was a duplicated literal inside the orchestrator instead of an import from the canonical shadow_markers.py module. Silent grammar drift was one rename away.
  • H7. devlog_write.py embedded the PAT in the git clone URL, visible in the process table. It leaked into a session stderr on 2026-05-23 and forced a rotation. Replaced with a GIT_CONFIG_COUNT-injected credential helper; the clone URL is clean and subprocess output is captured.

Each fix is observable in the architecture reference — F-A013’s DISPATCH count, F-A026’s idle gate, F-A036’s shadow marker constant, and the operational note on the devlog publishing path.

Start with docs/ARCHITECTURE.md. Read the eight sections top-to-bottom; jump to a deep-dive when a mechanic surprises you.

If you are operating the factory: §5 (Flows) and §8 (Discrepancy ledger) are the two pages you will return to.

If you are extending the factory: §6 (Design primitives) plus the shadow-mode deep-dive describe the promotion path for any new gating lens. The renderer deep-dive describes how to promote a role from per-runtime to shared.

If you are auditing the factory: every section opens with the atom IDs it cites. Atoms/645-findings.md is the bottom of the chain — 51 Facts, 12 Insights, 7 Recommendations, all anchored at file:line against main as of 2026-05-23.

The next iteration’s retro will track how many of the 15 discrepancy rows close. The SRS will catch up, one section at a time.


Update — 2026-05-23 16:03 UTC. #645 closed as completed. The full deliverable set landed on main: the canonical docs/ARCHITECTURE.md, six deep-dive pages under docs/architecture/, the Atoms/645-findings.md research vault, and the structurally-accurate Mermaid PNGs the figures on this page render from. Discrepancy ledger rows D1–D15 are tracked as individual SRS-alignment follow-ups; iteration retros report the close rate.