Skip to content

v0.2.1: dependency autonomy, write-ahead outbox, security + ops fixes - #55

Open
vnovick wants to merge 8 commits into
mainfrom
v0.2.1
Open

v0.2.1: dependency autonomy, write-ahead outbox, security + ops fixes#55
vnovick wants to merge 8 commits into
mainfrom
v0.2.1

Conversation

@vnovick

@vnovick vnovick commented Aug 9, 2026

Copy link
Copy Markdown
Owner

What this is

The complete dependency-autonomy decomposition (sub-projects 3 → 1 → 2 → 4a → 4b → 5), plus the Linear rate-limit reductions and daemon-stability fixes that accumulated alongside it. 118 files, +15.3k/−1.1k.

The headline: itervox now only takes issues that aren't blocked, orders work by what unblocks the most downstream effort, detects dependency cycles, escalates stuck blockers, keeps its dependency analysis fresh automatically, and captures far more real dependencies from both trackers — while making ~10x fewer tracker requests on the hot paths.

By area

Dispatch and the dependency graph

  • Inferred (LLM) edges soft-gate dispatch: confidence ≥ 0.7, fresher than 168h, no per-issue override, source known and non-terminal. Tracker edges stay hard. Kill switch: dependencies.inferred_gating: false.
  • Critical-path ordering (default on): priority band → transitive dependents → longest chain, via Tarjan SCC condensation. dependencies.ordering: simple restores legacy.
  • Cycles are first-class alerts (LiveOps tile, Deps-tab banner + red edges, heartbeat); members stay blocked, never auto-released. Escalation surfaces issues blocked >48h.
  • The dependency audit's up-to-20 synchronous tracker fetches moved off the event loop (batched, throttled, watchdogged).

Analyzer

  • Cancellable, timeout-bounded (10m), chunked (75/turn) — and now autonomous: change-driven incremental passes (content fingerprints; unchanged pairs revalidated), 5m debounce, 60m floor. A transient empty fetch can no longer wipe the sidecar.

Tracker adapters

  • GitHub: blocked by/on, depends on/upon, requires, waiting on/for + #N lists now create hard edges; blocker states cached 5 min (probe-measured: ~480 req/hr vs ~4,800 uncached at 40 refs).
  • Linear: sub-issues gate their parent; workflow-state UUIDs cached (state transitions cost 1 request, not 2); FetchIssuesByStates honors the runtime project filter.

Daemon

  • server.port defaults to fixed 8090; the HTTP socket survives config reloads (persistent listener + per-run generations — no more port re-rolls, no EADDRINUSE races). WORKFLOW.md edits debounce into one reload.

⚠️ Behavior changes for release notes

  1. Previously dispatchable issues may now be blocked — parents with open sub-issues, issues whose bodies match the widened phrases. Every hold is explained in the dashboard.
  2. A Linear parent with a blocks relation on its own child now forms a visible 2-cycle: both held + alert. Remove the relation to resolve.
  3. Blocker-unblock detection worst case ≈15 min via the refresh path (5-min state cache + 10-min refresh interval). Stale always fails safe (still-blocked).
  4. Runtime project filter now narrows automations, dashboard, and TUI lists — previously only dispatch.
  5. Port default changed from ephemeral to fixed 8090; multi-daemon setups need distinct explicit ports or server.port: 0.

Verification

Every task TDD'd with mutation checks (delete the change → named test fails). Each sub-project got an adversarial whole-branch review on top of per-task reviews; those final reviews caught and fixed four production-fatal bugs that green scoped reviews missed (all the same class — fixtures shaped unlike production; the class and its countermeasure are documented in #51#53). make verify exit 0 on the final tree (note: run web tests with node ≤24 until vitest/jsdom handle node 26's experimental storage globals).

Deferred follow-ups with rulings: #45, #46, #47, #50, #51, #52, #53. Fixes #44; implements #42's suggestions A+B.

🤖 Generated with Claude Code

vnovick and others added 6 commits August 8, 2026 20:09
Lands five sub-projects that were staged but never committed. Specs live
under docs/superpowers/specs/:

- Unified dependency graph (2026-08-04): orchestrator ingests inferred
  edges and soft-gates dispatch on them.
- Critical-path ordering (2026-08-04): graph-aware dispatch ordering,
  cycle detection, escalation. Measured tick cost ~4.9ms at 500 issues
  / 2000 edges.
- Analyzer autonomy (2026-08-04): scheduled + incremental analysis.
  Linear FetchIssuesByStates now honors the runtime project filter,
  matching FetchCandidateIssues.
- Tracker-edge widening (2026-08-05): widened blocker-phrase capture for
  GitHub, Linear parent/sub-issue edges, 5-minute blocker-state cache.
- Write-ahead outbox (2026-08-06): durable tracker writes, closing the
  read-starves-write loop measured in #42.

Recorded deferrals and operator-visible behavior changes for each are
tracked in #50, #51, #52, #53 and #54.

Verified: go build ./cmd/... ./internal/... exit 0; go test -race
./cmd/... ./internal/... passes. TestNewDepsAnalyzerService_Notifies
OnJobStartAndFinish is load-sensitive - it failed once under the full
parallel race run and passed 10/10 in isolation and on a clean re-run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…verdict

Fast-follow from #54's final review, adversarially re-reviewed:

- The absent-issue reconcile pass (batch FetchIssueStatesByIDs for pending
  entries invisible to the candidate fetch) now runs on its own
  fire-and-forget goroutine with an overlap guard — a slow tracker read can
  no longer delay outbox delivery ticks (probe-proven pre-fix: an entry due
  within 10ms waited out a 250ms fetch; pinned by
  TestOutboxFlusherAbsentReconcileDoesNotBlockDelivery).
- The two hand-duplicated reconciliation rule sets (event-loop candidates
  path and flusher absent path) collapse into one pure
  outbox.ReconcileVerdict helper; a mutation in the shared rule now fails
  all three suites simultaneously.
- Both trackers' FetchIssueStatesByIDs tests assert UpdatedAt is populated
  (the superseded_by_tracker rule depends on it).

Follow-ups tracked in #54 (accepted risks unchanged).
vnovick and others added 2 commits August 13, 2026 23:25
Two orchestrator tests hardcoded `InferredAt: 2026-08-04` while onTick
evaluates edge staleness against the wall clock. With staleness_hours at
168 the edges went stale on 2026-08-11 and every run after that failed —
the same SHA passed CI on 2026-08-09. Build the sidecar timestamps
relative to time.Now() instead, matching depsOverrideSidecarFixture,
which already did this correctly.

Swept the only other sidecar-plus-onTick call site
(deps_override_test.go); it was already correct and needed no change.

Separately, golangci-lint reported 6 pre-existing SA5011 errors across
five test files once the lint cache went cold: staticcheck does not model
t.Fatal as terminating, so every dereference following a nil check was
flagged as a possible nil deref. Added explicit returns.

Both failures pre-date this branch's current work — `make verify` could
not pass before this commit, so the gate could not distinguish a new
regression from the standing failure.

Verified by:
  go test -race -run '^(TestEventLoopDerivesCyclesAndAttention|TestEventLoopPopulatesInferredDeps)$' ./internal/orchestrator/... -v
    -> --- PASS: TestEventLoopDerivesCyclesAndAttention
    -> --- PASS: TestEventLoopPopulatesInferredDeps
  golangci-lint run ./cmd/... ./internal/...
    -> 0 issues.
  (mutation) forcing entry.Gating = false in inferred_deps.go makes both
    tests FAIL, confirming they were repaired rather than silenced.

Closes #56

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqFfbams4DKeTbQW3hPDXX
… reviewer fan-out

Three related additions to the dependency-graph and review surfaces.

1. dependencies.ordering: critical_path_strict

The existing critical_path mode compares the priority band FIRST, so
TransitiveDependents/LongestChain only break ties within one band. On a
tracker with consistently distinct priorities the graph metrics are never
consulted and the mode behaves like `simple` — a P1 leaf that unblocks
nothing dispatches ahead of a P2 root that unblocks a dozen issues.

The async-dependency-audit design doc named this exact defect ("only
breaks ties within a priority band") as the thing sub-project 2 was
chartered to remove; sub-project 2 shipped the transitive closure, the
critical path, and cycle detection but reproduced the limitation.

critical_path_strict compares graph leverage first and demotes priority
to a tiebreaker. It is NOT the default, because it deliberately overrides
an explicit operator signal. The shared comparator stages are factored
out so the only difference between the two modes is the position of
cmpPriority in the chain.

2. Dispatch pressure metrics

Nothing previously answered "if I raise max_concurrent_agents, will
anything get faster?" — capacity was an instantaneous gauge only. Each
tick is now classified as slot-bound (no free slots, eligible work
waiting) or dependency-bound (free slots idle because remaining work is
blocked), mutually exclusive by construction, with idle ticks charged to
neither. Surfaced through the snapshot to a LiveOpsStrip tile.

Classification runs against a probe copy of State with the slot gate
neutralized: ineligibleReasonShared checks AvailableSlots BEFORE the
blocker gates, so on a saturated tick every candidate reports "no_slots"
and the dependency signal would be masked exactly when it matters most.
Reusing IneligibleReason via the probe avoids a second hand-written copy
of the eligibility ladder that would drift.

3. Multi-reviewer fan-out with a quorum gate

agent.reviewer_profiles (ordered list) and agent.review_quorum
(any_block | majority | unanimous). Reviewers run SEQUENTIALLY: Running
stays keyed by issue.ID, so dispatch, the janitor, retry, automation, and
worktree isolation are untouched. Fan-out buys independence of judgement,
not wall-clock.

Each reviewer records a verdict at
.itervox/review/<identifier>/<profile>/verdict.json; the instruction is
injected into the reviewer's prompt automatically, so enabling the
feature cannot silently block every issue. A reviewer that runs but
records no parseable verdict counts as a BLOCK — dropping it would shrink
the quorum denominator, letting a crashing reviewer pass the gate by
attrition. any_block is the default for the same reason: adding a
reviewer must never make it easier to ship.

The quorum outcome is recorded and logged, not enforced. What a blocked
review should DO (move state, pause, comment) is a separate policy
decision; State.ReviewOutcomes carries the result for that work.

Also documents the tick graph's candidate-only scope (#59): a parked
direct blocker changes no dispatchable issue's ordering (verified
identical metrics both ways), while a parked issue mid-chain severs
transitive reachability and is not fixable without fetching non-candidate
blockers. Adds the missing `dependencies:` section to the site docs (#57).

Verified by:
  make verify -> exit 0
  go test -race -run '^(TestEventLoopOrdersByCriticalPathStrict|TestDispatchPressure|TestAdvanceReviewChain|TestEvaluateReviewQuorum|TestNormalizeReviewVerdict)' ./internal/orchestrator/...
    -> all PASS
  (mutation) routing the strict arm to SortForDispatchCriticalPath,
    removing the probe's slot-gate neutralization, and disabling the
    verdict-error branch each make their pinning test FAIL.

Closes #57, #58, #59

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqFfbams4DKeTbQW3hPDXX
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Daemon binds a different port on every config reload; reload rebinds even when the listener is unaffected

1 participant