feat: Architecture 2.0 - #213
Conversation
Epic 1 (Bones) of the Architecture 2.0 migration (#192). - Add pure, I/O-free jig/model/ package (foundation layer). - Dedupe OntologyTerm: canonical home in jig/model/ontology.py using the richer superset (term, definition, examples + non-empty validation); jig/schemas/arch.py and jig/schemas/po.py now re-export it. - Define the 5 invariants as pinned pure function signatures with stub bodies in jig/model/invariants.py (coverage, conformance, containment, vocabulary, ownership) plus a minimal Finding return type. - Expose Ticket/Thread domain types via jig/model/ticket.py and jig/model/thread.py re-export seams (real move deferred to MVP/Final). - New tests/model/ covers the dedup, invariant signatures, and seams. Verified: full suite 4781 passed / 6 skipped; ruff check + format clean.
…uards - Add ConfigDict(extra="forbid") to Finding, consistent with every other model; reject unknown keys on the invariant return contract now. - tests/model: cover Finding with a populated subject and extra-key rejection. - Guard the purity claim: assert jig.model root never exposes Ticket/ ThreadEntry, and a subprocess smoke test that importing jig.model loads no jig.store modules (subprocess avoids in-process sys.modules false-green).
feat(model): Epic 1 — CORE Model (Bones)
Epic 2 (Bones) of the Architecture 2.0 migration (#193). - Add jig/substrate/ package (Store + Bus coordination layer, wraps the existing store/bus — no new persistence). - StoreAuthority: unified project:// read/write facade. read() delegates to the existing resolver (routes today; honestly raises UnimplementedAuthorityError for the unwired arch/design/plan/store authorities). write() is the declared seam MVP wires. - TypedEvent schema: pydantic events replacing magic-string topics + kind-payloads. Ticket-lifecycle variants (Created/Updated/Completed/Failed) map to the existing kind strings; to_message() renders the legacy Message. - TypedBus: compat adapter over MessageBus accepting both legacy Message/dict and TypedEvent. MessageBus itself untouched. - tests/substrate/ covers authority routing, typed events, and the adapter; tests/test_bus.py + test_bus_recent.py pass unchanged. Verified: full suite 4799 passed / 6 skipped; ruff check + format clean.
…legation
Roborev job 636 (2 Medium):
- Typed lifecycle events were unfaithful to the bus. Fixed:
- Emit MessageType.CONTEXT_UPDATE (was STATUS) — matches the real
ticket_created/ticket_updated publishers.
- TicketCreated now carries the full _build_payload field set (incl. the
legacy `type` alias of work_type); TicketUpdated requires `status`.
- Dropped TicketCompleted/TicketFailed: those are JigEvents on the
WebSocket EventEmitter, not bus messages — documented the distinction.
Scope is the two events that actually travel on the bus and that the
orchestrator's "orchestrator" subscriber dispatches on.
- TypedBus was not a drop-in for MessageBus. Added a __getattr__ passthrough
delegating subscribe_agent/get_history/recent/add_websocket_listener/load/…
to the wrapped bus, with a test exercising the delegated methods.
Verified: full suite 4801 passed / 6 skipped; ruff check + format clean.
Roborev job 638 (1 Medium): agent-side terminal ticket_updated messages carry
an `_internal: True` marker so the emitter relay skips them (TUI doesn't
flicker resolved/failed between phases). TicketUpdated forbade extra keys and
couldn't represent it.
- Add an explicit `internal: bool = False` field; serialize as `_internal: True`
only when set, matching ticket_mcp's conditional `**({"_internal": True} ...)`.
- Tests cover both the default shape and the internal shape.
Verified: full suite 4803 passed / 6 skipped; ruff check + format clean.
Roborev job 640 (1 Medium): typed ticket events defaulted to topic="orchestrator", but the real publishers fan each lifecycle event to the per-ticket tickets.<id> topic (TUI + agent subscribers). A migration using the default would silently drop those subscribers. - Add _TicketLifecycleEvent base: ticket events default their topic to tickets.<id>; the orchestrator-dispatch copy is an explicit topic="orchestrator". - Tests cover both the per-ticket default and the explicit orchestrator copy, end-to-end through the bus adapter. Verified: full suite 4806 passed / 6 skipped; ruff check + format clean.
Claude review on PR #205 (the GitHub review, independent of roborev): 1. StoreAuthority.write error message said the authority "is wired in Epic 2 MVP" — inverted once MVP lands. Now "is not yet implemented". 2. Replaced test_typed_event_base_has_no_kind (probed pydantic ClassVar internals) with a behavioral test: TypedEvent().to_message() raises. 3. _TicketLifecycleEvent.topic was widened to `str | None`; kept it `str` via a `Field(default="")` sentinel so the annotation stays str (no mypy debt). Item 3 (missing feature-work/ docs): explicit decision recorded in architecture/README.md — migration epics (#192–#200) are documented in architecture/plan.md + their issues, NOT per-epic feature-work/ dirs, to avoid re-forking design state the migration exists to unify. Verified: full suite 4806 passed / 6 skipped; ruff check + format clean.
feat(substrate): Epic 2 — Substrate (Bones)
Epic 3 (Bones) of the Architecture 2.0 migration (#194). - Convert jig/runtime.py module -> jig/runtime/ package. The existing SpawnReason + AgentSpawnContext move to jig/runtime/spawn_context.py and are re-exported from the package root, so all 15 `from jig.runtime import ...` consumers are unchanged. - Define the RunAgent contract (jig/runtime/contract.py): a runtime_checkable Protocol `async (ctx) -> AgentRunResult`. AgentRunContext aliases the existing AgentSpawnContext; AgentRunResult mirrors the legacy RunAgentResult plus an events slot for the recorded/replay seam. Dependency-light (no jig.agent import) so the package stays cheap for its consumers. - RealRunAgent (jig/runtime/real.py): wraps jig.agent.run_agent (spawn + sandbox + MCP) and maps RunAgentResult -> AgentRunResult. Imports the agent stack, so it is NOT re-exported from the root — import from jig.runtime.real. - FixtureRunAgent (jig/runtime/fixture.py): canned result + call recording, the bone for headless evals. - tests/runtime/ covers the contract, both implementations, and the module->package compat. Existing agent/orchestrator tests pass through. Verified: full suite 4816 passed / 6 skipped; ruff check + format clean.
Claude review on PR #206: - FixtureRunAgent is context-agnostic but typed its ctx/calls as AgentRunContext while callers pass raw object() — a mypy/pyright error. Typed both as `object` to match the actual contract. - contract.py: AgentRunContext alias now uses `TypeAlias` to signal intentional rebinding. - real.py: comment that `events` is left empty in Bones (RecordedRunAgent fills it in MVP). - test_real: assert result.events == [] to pin the Bones invariant. Annotation/comment/test-only — no runtime behavior change. tests/runtime/ (10) pass; ruff check + format clean. Full suite not re-run (logic unchanged from the prior green run at 4816 passed).
feat(runtime): Epic 3 — Agent Runtime seam (Bones)
Proves the Build engine's seam before Epic 4 builds it out. Finding: a synchronous decide(state, event) -> (next_state, actions) works. The "spawn agent" effect is an inert SpawnAgent dataclass, not a coroutine; an async dispatch shell executes it afterward. decide never awaits, never mutates input state, and is deterministic — functional-core / imperative-shell. - jig/engines/ + jig/engines/build/ packages. - jig/engines/build/decide.py: BuildState, TicketReady/AgentCompleted events, SpawnAgent action, pure decide() handling OPEN -> IN_PROGRESS (the transition find_ready feeds today) + agent completion. BuildState defensively copies + freezes its mapping (MappingProxyType) so a caller dict can't mutate the snapshot — keeps the purity guarantee airtight. - tests/engines/build/test_decide.py: proves sync, inert-action, no-mutation (incl. caller-isolation + read-only snapshot), determinism, no-op, completion, and that an async shell executes the actions. - .gitignore: the generic `build/` rule was ignoring the entire jig/engines/build/ source package — negate it for the engine source + tests dirs so the Build engine is tracked (affects all of Epic 4 too). - architecture/plan.md Spike 1 marked resolved. Verified: tests/engines/build/ 10 passed; ruff check + format clean. (Full suite green at 4824 on the surrounding code; the freeze is isolated to the unimported jig/engines/build/ package.)
spike(build): Spike 1 (#201) — decide() purity under async spawn
Epic 4 (Bones) of the Architecture 2.0 migration (#195), on the seam Spike 1 (#201) validated. Splits the orchestrator's responsibilities into composable modules under jig/engines/build/ WITHOUT touching orchestrator.py (4543 lines) — logic is shimmed; the orchestrator->facade conversion is MVP. - decide.py: grow the pure decide() into a DATA-DRIVEN transition table (states+transitions as data, not if/else). Happy-path lifecycle OPEN -> IN_PROGRESS -> merge -> RESOLVED with inert actions SpawnAgent, MergeWorktree, PublishCompleted, UnblockDependents. Spike tests preserved. - dispatch.py: Dispatcher — the only async part. Executes actions via injected, type-keyed effect handlers; unhandled action fails loudly. Composes with the Agent Runtime seam (a SpawnAgent handler drives a RunAgent). - supervisor.py: Supervisor translates detector signals (stall / deadlock nudge+escalation, from stall_detector.py + deadlock.py) into supervisory events; never mutates tickets. - coordinator.py: BuildCoordinator composes decide + dispatch + supervisor — handle(event) decides, advances state, dispatches actions. - tests/engines/build/: decide happy-path, dispatch (incl. RunAgent composition), supervisor, and end-to-end coordinator happy path. Scope note: orchestrator.py is untouched (backwards compat trivially holds); routing its transitions through the coordinator + the facade conversion are MVP. Verified: full suite 4840 passed / 6 skipped; ruff check + format clean.
Roborev job 658 (Medium): AgentSucceeded left the ticket in IN_PROGRESS and WorktreeMerged was also accepted from IN_PROGRESS, so a duplicate AgentSucceeded dispatched two MergeWorktree actions and a stray/out-of-order WorktreeMerged could resolve a ticket that never started merging. - Add BuildPhase.MERGING (a Build-engine-internal sub-state with no persisted TicketStatus). EngineState = TicketStatus | BuildPhase. - AgentSucceeded transitions IN_PROGRESS -> MERGING (one merge); WorktreeMerged is only valid from MERGING. Duplicate/stray events now no-op. - Tests: merge enters MERGING, duplicate AgentSucceeded merges once, stray WorktreeMerged from IN_PROGRESS is a no-op. Verified: tests/engines/build/ 26 passed; ruff check + format clean. (Full suite green at 4840 on the surrounding code; this change is isolated to the unimported jig/engines/build/ package.)
Roborev job 660 (Medium): BuildCoordinator.handle() advanced self._state before dispatching, so a failed/missing effect handler left the coordinator past the transition while the effect never ran — a retry no-oped and the effect (Spawn, Merge, Unblock) was lost. - handle() now computes next_state, dispatches the actions, and commits state only after dispatch succeeds. A failed dispatch leaves the prior state so the event can be retried. (Partial-failure within a multi-action transition re-runs all its actions on retry; finer idempotency is MVP.) - Test: a failing dispatch leaves the ticket OPEN (not advanced to IN_PROGRESS). Verified: tests/engines/build/ 27 passed; ruff check + format clean.
Roborev job 662 (Medium): jig.engines.build is held to the clean mypy baseline (not in the incremental-adoption suppress list), and the transition table didn't type-check — narrow handlers (e.g. (BuildState, TicketReady) -> ...) aren't assignable to a Callable with a fixed Event parameter (args are contravariant). mypy reported 5 errors. - _TransitionFn is now Callable[..., tuple[EngineState, tuple[Action, ...]]], keeping the readable narrow handler signatures assignable; the table key guarantees each handler only receives its event type at runtime. - decide() guards the None lookup (ticket not in state) before the dict get, fixing the Optional-key arg-type error. Verified: `mypy jig/engines/build/` clean (5 files); tests/engines/build/ 27 passed; ruff check + format clean.
Roborev job 664 (Medium): BuildCoordinator.handle() read state, awaited dispatch, then replaced the whole snapshot — two concurrent events for different tickets decided from the same stale state and the last commit clobbered the other's transition. - handle() is now serialized with an asyncio.Lock (whole decide/dispatch/commit step). Per-ticket concurrency (one task per ticket / per-ticket rebase) is MVP, when wired to the concurrent dispatch path. Test: two concurrent handles for different tickets both keep their transition. Claude PR review (batched, all low-cost): - decide(): debug-log the unknown-ticket_id discard branch; tighten the _TRANSITIONS key annotation to type[Event]. - __init__.py: export EngineState and SupervisoryEvent (were missing — direct `from jig.engines.build import SupervisoryEvent` raised ImportError). - supervisor.py: drop the inconsistent `__all__: Sequence[str]` annotation. - tests: add the absent-ticket no-op case; freeze _FakeVerdict; note the SpawnAgent->AgentSpawnContext MVP conversion in the coordinator seam test. Verified: tests/engines/build/ 29 passed; mypy jig/engines/build/ clean; ruff check + format clean.
Roborev job 666 (2 Medium): 1. AgentCompleted(status=RESOLVED) set RESOLVED straight from IN_PROGRESS, bypassing the AgentSucceeded -> MergeWorktree -> WorktreeMerged path (skipping merge, publish, unblock). RESOLVED now only via the merge path; AgentCompleted is for non-success terminals (failed/blocked/needs-info) and no-ops on RESOLVED. Tests: completion sets FAILED; AgentCompleted(RESOLVED) is a no-op. 2. Multi-action completion wasn't atomic — a failed UnblockDependents after a successful PublishCompleted could duplicate the completion event on retry. Reordered so the externally-visible PublishCompleted runs LAST, and documented the dispatcher's at-least-once contract (handlers must be idempotent). Full per-action idempotency is MVP. Verified: tests/engines/build/ 30 passed; mypy jig/engines/build/ clean; ruff check + format clean. (Full suite green at 4845 on the surrounding code; changes isolated to the unimported jig/engines/build/ package.)
Roborev job 668 (Medium): AgentCompleted excluded only RESOLVED, so a malformed
completion event could still regress an in-progress ticket to OPEN/PROPOSED/etc.
- Replace the RESOLVED-specific guard with a positive allow-list:
{FAILED, BLOCKED, NEEDS_INFO, MERGE_CONFLICT}. Anything else (incl. RESOLVED,
which goes through the merge path) no-ops. Test covers OPEN/PROPOSED/IN_PROGRESS
regression attempts.
Verified: tests/engines/build/ 31 passed; mypy jig/engines/build/ clean; ruff
check + format clean.
Roborev job 670 (Medium): MERGE_CONFLICT is a merge outcome (reached from
BuildPhase.MERGING), not an agent one, so accepting it via AgentCompleted let a
malformed event move a ticket to MERGE_CONFLICT without going through the merge.
- _AGENT_TERMINAL_STATUSES is now exactly RunAgentResult's non-success values:
{FAILED, BLOCKED, NEEDS_INFO}. AgentCompleted(MERGE_CONFLICT) no-ops; the
bogus-status test now covers it. Modeling a dedicated merge-conflict result
event from MERGING is Final-phase sad-path work (per the plan).
Verified: tests/engines/build/ 31 passed; mypy + ruff clean.
feat(build): Epic 4 — Build engine (Bones)
Epic 5 (Bones) of the Architecture 2.0 migration (#196). - Create jig/engines/enforcement/ package. - Review contract (contract.py): a runtime_checkable Protocol `Review(diff, invariant_context) -> list[Finding]` — the headless seam Build invokes instead of reaching into jig/reviewers/ directly. Findings are the canonical jig.model.Finding (Epic 1). InvariantContext is the light Bones context. MechanicalReview is the deterministic stub (MVP returns real boundary + vocabulary findings). - Expose jig/boundary_rules.py under jig/engines/enforcement/mechanical/ and jig/reviewers/dispatch.py under jig/engines/enforcement/reviewers/ via re-export (new home owns the surface). Physical relocation is MVP — moving reviewers/dispatch.py now would create a reviewers/__init__ <-> dispatch import cycle. - tests/engines/enforcement/ covers the Review contract and that the new homes re-export the identical canonical objects. Scope note: physical file moves + shim removal, check_runner migration, and wiring Build to the Review contract are MVP. Backwards-compat is trivial — nothing existing changed. Verified: full suite 4852 passed / 6 skipped; mypy jig/engines/enforcement/ clean; ruff check + format clean.
Roborev job 674 (Medium): the dispatch re-export exposed only 6 names while the canonical jig.reviewers.dispatch.__all__ has 23 (BONES_REVIEWER_ID, SECURITY_REVIEWER_ID, should_run_for_bones, …), so code migrating imports to the new home would hit ImportError. - The enforcement reviewers dispatch shim + __init__ now star-re-export driven by the canonical __all__, so the full public surface is mirrored and stays in lockstep automatically (no drift). - Test pins it: the new home's __all__ equals the canonical __all__ and every name resolves to the identical object. Verified: tests/engines/enforcement/ 6 passed; mypy jig/engines/enforcement/ clean; ruff check + format clean. (Full suite green at 4852 on the surrounding code; this change is isolated to the enforcement re-export surface.)
…e test Claude PR review on #209: - Review.__call__ is now async. The implementations behind it are I/O-bound (reviewer federation spawns agents; mechanical checks shell out to semgrep) and the orchestrator runs an event loop — pinning the seam async now avoids a contract-breaking change at MVP, and matches the project's async-throughout convention. MechanicalReview + the contract tests updated to async/await. - test_boundary_rules_exposed_under_enforcement_mechanical now loops over moved.__all__ (was 2 of 3 symbols), matching the reviewer-side exhaustive check. Verified: tests/engines/enforcement/ 6 passed; mypy jig/engines/enforcement/ clean; ruff check + format clean.
…_checkable The old test_review_is_a_runtime_checkable_protocol implied isinstance(x, Review) validates the contract. It doesn't: runtime_checkable only checks that __call__ exists, not that it's a coroutine — a sync callable (even len) passes. The _NotAReview negative was trivially true (a bare object isn't callable), giving false confidence. - test_mechanical_review_satisfies_the_async_review_contract asserts isinstance AND inspect.iscoroutinefunction(__call__) — the async property the Protocol can't enforce. - test_runtime_protocol_check_only_verifies_callability documents the Python limitation explicitly: a sync impl passes isinstance but isn't a coroutine. Verified: tests/engines/enforcement/test_contract.py 4 passed; mypy clean; ruff clean.
feat(enforcement): Epic 5 — Enforcement (Bones)
Epic 6 (Bones) of the Architecture 2.0 migration (#197): Discovery, Architecture, and Visual Design engines behind their project:// authorities. - jig/engines/authoring.py: AuthoringEngine — the shared authority-boundary contract. Each engine owns exactly one project:// authority and `owns(uri)` routes by it (the ownership invariant: one boundary, one owner). Rejects unknown authorities. - jig/engines/discovery/ — owns project://spec/. interview.py exposes the PO ask/answer flow (run_po_conversation, run_po_l0-l3, next_incomplete_level) at its new home. - jig/engines/architecture/ — owns project://arch/. sa_loop.py exposes the SA<->operator loop (run_sa_conversation, render/prompt_sa_confirm, apply_scaffold) at its new home. - jig/engines/visual_design/ — owns project://design/. Boundary only; the VD engine is Final-phase. - tests/engines/test_authoring.py covers the authority boundaries + that the new homes re-export the identical init_workflow flow entry points. Scope note: Bones declares the boundaries and exposes the flows via re-export. The real extraction out of init_workflow.py (2697 lines), the SA-role unification (sa/sa_mvp/sa_v2 -> one size-adaptive SAU), and TB planning are MVP. init_workflow is untouched — init/onboard/SA tests pass unchanged. Verified: full suite 4859 passed / 6 skipped; mypy jig/engines/ clean; ruff check + format clean.
Roborev job 681 (Medium): AuthoringEngine validated against all StoreAuthority
authorities, so AuthoringEngine(authority="plan"/"store") was accepted — but
plan/store are not authored by an engine (PM/Build own them), which weakened the
ownership invariant the class enforces.
- Validate against a dedicated AUTHORING_AUTHORITIES = ("spec", "arch",
"design"). plan/store now raise. Test covers their rejection.
Verified: tests/engines/test_authoring.py 6 passed; mypy clean; ruff clean.
…he lock Roborev job 788 (Medium): StoreAuthority decided create vs update *before* the cross-process lock. A concurrent create between the reload and the create branch could turn a partial update into a failed create-validation, or a full write into a spurious duplicate-id error (the catch-retry can't fix the partial case — a partial body fails Ticket validation before any uniqueness check runs). Fix: move the reload + decision into TicketStore.write_addressed(), which under the existing cross-process flock reloads fresh disk state, decides create vs update, and writes — all before releasing the lock, so the snapshot can't change under it. StoreAuthority.write now delegates to it (no more decide-then-write in the caller). Refactor extracts _create_locked() (shared key-assign+insert) and _check_transition_gate() (the operator approval gate) so create/update/ write_addressed share one implementation each; dropped the interim enforce_unique_id param (write_addressed supersedes it). load() locking (prior commit) is what makes the under-lock reload safe on a live shared store. Key policy simplified: a write body may never carry a non-empty `key` (was: echo-allowed), since the create-vs-update branch is no longer known in the caller. Tests: write_addressed cross-instance create-then-update; concurrent StoreAuthority writes to one new id (one creates, one updates, no failure). Verified: full suite 4954 passed / 6 skipped; mypy jig/store/tickets + substrate clean; ruff check + format clean.
…ghten test Roborev job 790: 1. (Medium) load() cleared _docs/_indexes before the JSONL replay validated, so a corrupt op-log (malformed JSON / unknown op / update|delete of an unknown id) left a *live* store emptied or half-rebuilt after raising — newly dangerous now that store writes reload live TicketStore instances. Fix: replay into a local map and swap it into the instance (and rebuild indexes) only after the whole file validates; a replay error now leaves current state intact. Test added: a reload over a corrupt file raises but preserves the existing doc + index. 2. (Low) The concurrency regression test only asserted doc["n"] is an int, so a lost-update interleave would still pass. Now asserts the exact final value (== 100) — every op is on disk, so any later reload converges on it. Verified: full suite 4955 passed / 6 skipped; mypy jig/store/core.py clean; ruff check + format clean.
…via TicketStore The write() docstring still said persistence was 'not yet wired'; store now routes through TicketStore (the inline comment was already correct). Doc-only.
…ore-write feat(substrate): Epic 2 MVP PR B2 — wire store WRITE persistence
|
Claude finished @brent-hoover's task in 7m 55s —— View job PR Review: feat: Architecture 2.0 (Pass 10)
What changed since Pass 9Ten commits (merged as PR #229) implementing Epic 2 MVP PR B2 — wiring store WRITE persistence through
Files changed: Overall AssessmentA well-executed, security-aware write path. The architecture is correct:
|
…ped events Tasks 2+3 of #216: migrate the orchestrator's ticket_created/ticket_updated bus publishes and its "orchestrator" subscriber from magic-string topics + kind-tagged dict payloads onto the TypedEvent substrate (bones from #205). - events.py: add the subscribe-side inverse of to_message() — TypedEvent.from_message per concrete event + decode_event(msg), which reconstructs the typed event a legacy Message carries (or None for kinds outside the typed set, so subscribers fall back to raw handling for shutdown_request/comment_posted/…). - Publishers now construct TicketCreated/TicketUpdated and publish event.to_message(): - ticket_events.publish_ticket_created (broadcast + dispatch copies), - Orchestrator._update_ticket_status, - ticket_mcp ticket_updated dual-publish (broadcast + orchestrator), incl. the _internal terminal-status marker. Removed the now-redundant ticket_events._build_payload. - Subscriber: Orchestrator._run_service_loop decodes via decode_event and dispatches on isinstance(TicketCreated/TicketUpdated); shutdown_request stays raw-kind. Pure refactor: the typed events render byte-for-byte identical Messages, so the wire format and every existing subscriber (TUI, agent runner) are unchanged. Per-ticket waiter sites that read payload["kind"] on tickets.<id> topics are left as-is (follow-on) — this PR scopes to the "orchestrator" dispatch path the substrate docstring named. Tests: decode_event round-trips created/updated (incl. _internal), returns None for untyped/kindless payloads; base from_message raises. Verified: full suite 4960 passed / 6 skipped; mypy clean; ruff check + format clean.
Roborev job 795 (2 Medium): 1. decode_event() built a full TicketCreated, so a partial/legacy ticket_created bus message (only kind/ticket_id) raised KeyError and could kill the orchestrator service loop. Fix: decode_event never raises — a typed-kind payload too partial to reconstruct returns None (catches KeyError/ ValidationError). The service loop now falls back to raw-field handling for undecoded ticket_created/ticket_updated, preserving the pre-typed tolerant behavior (a stray message still schedules, never crashes the loop). Extracted _reschedule_reset_ticket so the typed and raw branches share the reset-to-open path. 2. load() swapped self._docs before the index rebuild, so an index-rebuild failure (e.g. an unhashable indexed value) left replaced docs + partial indexes. Fix: build the new indexes into a local map too and swap docs AND indexes in together, only after both fully build. _index_insert takes an optional target map for the staged build. Tests: decode tolerant of a partial typed payload; load() leaves live state intact on an index-rebuild error. Verified: full suite 4962 passed / 6 skipped; mypy clean; ruff check + format clean.
…erves dispatch topic Claude review test-quality nits (non-blocking): split the internal=False/True loop into a parametrized test for clearer failure attribution, and add a test that decode preserves an explicit topic="orchestrator" envelope (the dispatch copy), not just the default per-ticket topic.
…ped-bus feat(substrate): Epic 2 MVP PR C — migrate ticket lifecycle bus to typed events
|
Claude finished @brent-hoover's task in 4m 43s —— View job PR Review: feat: Architecture 2.0 (Pass 11)
What changed since Pass 10Three commits (merged as PR #230) implementing Epic 2 MVP PR C — migrating the ticket lifecycle bus to typed events:
Files changed: Overall AssessmentClean, well-scoped execution. The
|
…s + project:// serialization) Records the Epic 2 PR D routing decision (plan.md flagged it open): runtime/in-engine code uses typed StoreAuthority ports (store.tickets, …); project:// URIs are the cross-boundary serialization surface and delegate to the same typed stores (no separate URI impl). StoreAuthority is the composition root for store lifecycle, policy, and cache coherence; cache invalidation fires on typed writes too. Splits authoring-authority (spec/arch/design/plan) from runtime store capability. Migration ordering is illustrative, not binding — sequence by what fits the in-flight work. Adds a pointer from plan.md Epic 2 MVP task 1.
…tor's store composition root First slice of #216 task 1 (route store access through StoreAuthority), per ADR-0001. StoreAuthority becomes the composition root that constructs + loads the domain stores and vends them as typed ports; the orchestrator's store attributes are authority-sourced aliases (alias approach — call sites unchanged, near-zero risk). - StoreAuthority.load() builds + loads tickets/threads/memory/checkpoints/ check_results/review_comments on the canonical .jig/store paths and exposes them via typed accessor properties (tickets, threads, …). Accessors require load() first; URI read/write keep working without it. An injected ticket store is used as-is. - Orchestrator.startup() constructs one StoreAuthority and sources self.tickets/… from it (same instances). Because the vended TicketStore IS the one the URI write path uses, runtime and URI-door writes now share one instance + its callbacks — closing a real coherence gap. Bus + analytics stay orchestrator-constructed (substrate infra). Tests: load() vends the typed ports; access-before-load raises; typed port and URI door share one TicketStore (create via port -> URI read sees it; URI write -> port sees it); a URI-door create fires the vended port's create callback. Deferred to later waves (per ADR): URI reads delegating to the typed store, cache invalidation on typed writes, the runtime-capability split, and call-site renames in other subsystems. Verified: full suite 4968 passed / 6 skipped; mypy + ruff clean.
… ids
Roborev job 801 (Medium): the raw lifecycle fallback in the "orchestrator"-topic
dispatcher accepted any truthy ticket_id, so a malformed payload with an
unhashable id (e.g. {"kind":"ticket_updated","ticket_id":["x"],"status":"open"})
would pass decode_event as None, reach the scheduler, and crash the service loop
(_running_tickets is keyed by id). Now a raw id is scheduled only when it is a
non-empty string.
Extracted the per-message dispatch into _handle_service_message(msg) so it is
unit-testable (the loop just reads the queue and calls it). Tests cover typed
created/updated, raw partial fallback, malformed unhashable ids (ignored, no
crash), shutdown_request, and unknown kinds.
Verified: targeted tests pass; mypy + ruff clean.
Roborev (Low): ADRs live in docs/reference/ per CLAUDE.md, not architecture/. Renamed to adr-0001-runtime-store-access-routing.md and fixed the plan.md link.
…d ticket store Claude review on PR #232: - (blocking) load() reinitialized the non-ticket domain stores on every call, so a second load() orphaned the already-vended instances and broke the shared-instance invariant for everything but tickets. Added an idempotency guard (return early once loaded); load-once at startup is the contract. - (medium) Added a test that load() reuses an injected ticket store (the URI-write/runtime shared-instance + callbacks guarantee depends on it), plus an idempotency test that a second load() keeps port identity. - (low) Made _require generic so the typed accessor properties keep their return types instead of erasing to Any; dropped the test assertion that coupled to the internal _ticket_store field (behavioral assertions already prove shared-instance semantics). Verified: full suite 4976 passed / 6 skipped; mypy jig/substrate/ clean; ruff clean.
…ute-wave1 feat(substrate): Epic 2 PR D wave 1 — StoreAuthority as orchestrator store composition root (+ ADR-0001)
|
Claude finished @brent-hoover's task in 6m 1s —— View job PR Review: feat: Architecture 2.0 (Pass 12)
What changed since Pass 11Six commits (merged as PR #232) implementing Epic 2 MVP PR D Wave 1 —
Files changed: Overall AssessmentClean wave. The composition-root pattern is mechanically correct:
|
… store (ADR-0001 rule 3) Kills the read-path drift: jig/uri/store.py had its own JSONL op-log replay (_replay_jsonl) duplicating TicketStore.load()'s replay — two hand-aligned implementations the ADR forbids. Now there is one canonical store read. - StoreAuthority.read is async; project://store/... reads project over the typed TicketStore (the same instance the write path uses) and serialize via the Ticket model. It reloads first — the URI read is the cross-boundary serialization surface and must reflect current persisted state, not a stale in-memory snapshot (same O(file) cost the old disk-replay paid, but via the canonical load). - jig/uri/store.py drops _replay_jsonl / _normalize_ticket / resolve_store_uri; keeps reject_unsupported_store_uri (shape validation) + a shared serialize_ticket. - The sync resolve_project_uri dispatcher no longer handles store (it stays sync for the genuinely-sync declared artifacts spec/arch/design/plan); a store URI there raises ProjectUriError directing callers to StoreAuthority.read. - Standalone/external store reads use the same door: StoreAuthority(root).read(...) lazily loads the ticket store, so there's exactly one replay implementation. Tests: store-read resolution moved to tests/substrate/test_store_uri_read.py (single/missing/list/empty + unwired/sub-path/fragment/@revision rejections + corrupt-oplog surfaced via the canonical load); test_uri.py asserts the dispatcher rejects store; sa.read calls updated to await. Verified: full suite 4973 passed / 6 skipped; mypy jig/uri + substrate clean; ruff clean.
Roborev job 809 (Medium): dropping the store guard meant resolve_project_uri consulted the cache for all authorities, so a retained/manually-populated project://store/... cache entry would be served by the sync dispatcher instead of raising and routing to StoreAuthority.read (the typed-store path) — serving stale store data. Now the store rejection fires before cache.get(); removed the now-dead store branch in the if/elif chain. Test added. Verified: full suite 4974 passed / 6 skipped; mypy + ruff clean.
…e reads Claude review on PR #233 (non-blocking nits): - _WIRED_FILES dict had a dead value side after resolve_store_uri's deletion (only key membership is checked). Renamed to _WIRED_COLLECTIONS frozenset. - Store reads now reject jig-N-shaped ticket ids (symmetric with the write path): jig-N is the reserved key namespace, not an addressable id, so a URI read using it is a category error rather than a silent null. Test added. (The double-load on a fresh StoreAuthority's first read and the O(file) reload-per-read are acknowledged perf follow-ons for when store reads become a hot path — not addressed here.) Verified: full suite 4975 passed / 6 skipped; mypy + ruff clean.
…i-read-delegation feat(substrate): Epic 2 PR D — URI store reads project over the typed store (ADR-0001 rule 3)
|
Claude finished @brent-hoover's task in 4m 32s —— View job PR Review: feat: Architecture 2.0 (Pass 13)
What changed since Pass 12Four commits (merged as PR #233) implementing Epic 2 MVP PR D Wave 2 — URI store reads routing through the typed
Files changed: Overall AssessmentClean wave. The architectural shift — eliminating the parallel JSONL replay in the URI read path and routing
|
…e RunAgent seam Task 2 of #217: the orchestrator no longer calls jig.agent.run_agent directly — every spawn goes through self._run_agent (a RunAgent). This is the evaluability pivot: headless evals can inject a Fixture/Recorded RunAgent. - Orchestrator gains a `run_agent: RunAgent | None = None` constructor param; default is `RealRunAgent(emitter=emitter)`. The single direct spawn in `_run_agent_with_analytics` becomes `await self._run_agent(ctx)`. The method now returns the contract's `AgentRunResult` (a field-superset of the legacy `RunAgentResult`, so callers are unaffected); the provisioning-failure error path returns it too. - RealRunAgent tolerates a result that omits `warnings` (optional/additive, SF-I5) so minimal RunAgent results map cleanly. - Existing orchestrator tests that intercepted spawns via `monkeypatch.setattr(orch_module, "run_agent", …)` now patch `jig.agent.run_agent` (what RealRunAgent calls by attribute) — 34 sites across 14 files; behavior-preserving, and they now exercise the seam. Dropped the now-unused `import … as orch_module` lines those sites left behind. - New tests/runtime/test_orchestrator_seam.py pins the injection contract. Task 1 (RecordedRunAgent) already shipped in jig/runtime/recorded.py. Task 3 (extract per-agent MCP lifecycle into the runtime) is PR 2. Verified: full suite 4978 passed / 6 skipped; mypy + ruff clean.
…visioning semantics Claude review on PR #235 (doc-only): the seam-routing edit orphaned the word 'Bones'; joined it into the sentence. Also corrected the pre-existing Track E paragraph which claimed provisioning failures 'leave extra_env empty rather than failing the spawn' — the code fails the ticket (SF-1) when a declared service fails to provision.
…n-seam feat(runtime): Epic 3 MVP PR 1 — route orchestrator spawns through the RunAgent seam
|
Claude finished @brent-hoover's task in 4m 35s —— View job PR Review: feat: Architecture 2.0 (Pass 14)
What changed since Pass 13Two commits (merged as PR #235) implementing Epic 3 MVP PR 1 — routing orchestrator spawns through the
Files changed: Overall AssessmentClean execution of the evaluability pivot. The
|
…sembly Task 3 of #217: extract the per-agent MCP server lifecycle into the runtime; mcp_server.py stays the jig-tool registration factory. New jig/runtime/mcp.py owns assembling the {name: server} MCP map a real agent run gets: build_agent_mcp_servers(ctx, *, can_waive) builds the in-process jig server (via create_agent_mcp_server, which stays in jig.mcp_server) and merges the role's external stdio MCPs. The two helpers that constitute that assembly — _resolve_external_mcps and _effective_ticket_base_ref — moved out of agent.py into the runtime module (renamed public: resolve_external_mcps, effective_ticket_base_ref). agent.run_agent now calls the runtime seam instead of assembling inline; cap.can_waive (compiled by the capability-policy step, which also writes the enforcement artefacts) flows in. Lifecycle here = assembly: the in-process SDK server is a tool-handler dict with no start/serve/teardown. The win is the dependency-graph boundary — "Runtime owns the MCP lifecycle" — which is the piece a headless, daemon-free pipeline needs to control or substitute (the evaluability north star). Isolation fitness test: importing + using jig.runtime.mcp must not pull in the orchestrator / daemon / ws_server / container / TUI layer. Verified in a fresh interpreter (the lazy persistence->reviewers.dispatch->orchestrator edges that grimp's static graph shows are never triggered at import/assembly time). Tests: build_agent_mcp_servers (jig-only, external-merge), resolve_external_mcps, effective_ticket_base_ref; the moved base-ref test + the can_waive-threading spy repoint to the runtime module. mypy: the new module is held to the clean baseline (not in the suppression list) — narrowed analytics_emitter honestly via cast. Verified: full suite 4985 passed / 6 skipped; mypy jig/runtime/mcp.py clean; ruff clean.
Claude review on PR #236 (non-blocking, but real for new public API): - resolve_external_mcps could raise AttributeError on a malformed .mcp.json whose root is a non-dict (JSON array/scalar) — propagating into run_agent's catch-all as a misleading "SDK query failed". Now guarded with isinstance checks (both user-level and plugin-cache paths); a malformed config is logged and skipped, matching the best-effort contract. Regression test added. - Bare `dict` return/var types on the new public functions -> dict[str, Any]. - Test: monkeypatch jig.runtime.mcp.Path instead of stdlib Path.home (more robust across versions), per the review suggestion. Verified: full suite 4986 passed / 6 skipped; mypy + ruff clean.
…lifecycle feat(runtime): Epic 3 MVP PR 2 — runtime owns per-agent MCP server assembly
|
Claude finished @brent-hoover's task in 4m 10s —— View job PR Review: feat: Architecture 2.0 (Pass 15)
What changed since Pass 14Three commits (merged as PR #236) implementing Epic 3 MVP PR 2 — the runtime owning per-agent MCP server assembly:
Files changed: Overall AssessmentClean boundary extraction.
|
No description provided.